Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ public static extern ExceptionStatus wechat_qrcode_create1([MarshalAs(UnmanagedT
[MarshalAs(UnmanagedType.LPStr)] string detector_caffe_model_path,
[MarshalAs(UnmanagedType.LPStr)] string super_resolution_prototxt_path ,
[MarshalAs(UnmanagedType.LPStr)] string super_resolution_caffe_model_path,out IntPtr ptr);

[DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern ExceptionStatus wechat_qrcode_WeChatQRCode_detectAndDecode(IntPtr obj, IntPtr inputImage, IntPtr points, IntPtr texts);

[DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern ExceptionStatus wechat_qrcode_WeChatQRCode_detectAndDecode_points(IntPtr obj, IntPtr inputImage, IntPtr points, IntPtr texts);


[DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern ExceptionStatus wechat_qrcode_delete(IntPtr ptr);
Expand Down
82 changes: 54 additions & 28 deletions src/OpenCvSharp/Modules/wechat_qrcode/WeChatQRCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,81 @@ namespace OpenCvSharp;
/// </summary>
public class WeChatQRCode : CvObject
{
internal WeChatQRCode(IntPtr ptr)
{
SetSafeHandle(new OpenCvPtrSafeHandle(ptr, ownsHandle: true,
releaseAction: h => NativeMethods.HandleException(NativeMethods.wechat_qrcode_delete(h))));
}

/// <summary>
/// Initialize the WeChatQRCode.
/// It includes two models, which are packaged with caffe format.
/// Therefore, there are prototxt and caffe models (In total, four paramenters).
/// Pass empty strings to create a detector without neural network models.
/// </summary>
/// <param name="detectorPrototxtPath">prototxt file path for the detector</param>
/// <param name="detectorCaffeModelPath">caffe model file path for the detector</param>
/// <param name="superResolutionPrototxtPath">prototxt file path for the super resolution model</param>
/// <param name="superResolutionCaffeModelPath">caffe file path for the super resolution model</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static WeChatQRCode Create(
string detectorPrototxtPath,
string detectorCaffeModelPath,
string superResolutionPrototxtPath,
string superResolutionCaffeModelPath)
public WeChatQRCode(
string detectorPrototxtPath = "",
string detectorCaffeModelPath = "",
string superResolutionPrototxtPath = "",
string superResolutionCaffeModelPath = "")
{
if (string.IsNullOrWhiteSpace(detectorPrototxtPath))
throw new ArgumentException("empty string", nameof(detectorPrototxtPath));
if (string.IsNullOrWhiteSpace(detectorCaffeModelPath))
throw new ArgumentException("empty string", nameof(detectorCaffeModelPath));
if (string.IsNullOrWhiteSpace(superResolutionPrototxtPath))
throw new ArgumentException("empty string", nameof(superResolutionPrototxtPath));
if (string.IsNullOrWhiteSpace(superResolutionCaffeModelPath))
throw new ArgumentException("empty string", nameof(superResolutionCaffeModelPath));
if (detectorPrototxtPath is null)
throw new ArgumentNullException(nameof(detectorPrototxtPath));
if (detectorCaffeModelPath is null)
throw new ArgumentNullException(nameof(detectorCaffeModelPath));
if (superResolutionPrototxtPath is null)
throw new ArgumentNullException(nameof(superResolutionPrototxtPath));
if (superResolutionCaffeModelPath is null)
throw new ArgumentNullException(nameof(superResolutionCaffeModelPath));

NativeMethods.HandleException(
NativeMethods.wechat_qrcode_create1(
detectorPrototxtPath, detectorCaffeModelPath, superResolutionPrototxtPath, superResolutionCaffeModelPath,
out var ptr));

return new WeChatQRCode(ptr);
SetSafeHandle(new OpenCvPtrSafeHandle(ptr, ownsHandle: true,
releaseAction: h => NativeMethods.HandleException(NativeMethods.wechat_qrcode_delete(h))));
}

/// <summary>
/// Both detects and decodes QR code.
/// To simplify the usage, there is a only API: detectAndDecode
/// </summary>
/// <param name="inputImage">supports grayscale or color(BGR) image.</param>
/// <param name="bbox">optional output array of vertices of the found QR code quadrangle.Will be empty if not found.</param>
/// <param name="results">list of decoded string.</param>
public void DetectAndDecode(InputArray inputImage, out Mat[] bbox, out string[] results)
/// <param name="points">
/// output array of vertices of the found QR code quadrangles.
/// Each element is an array of 4 <see cref="Point2f"/> representing the corners of one detected QR code.
/// Will be empty if not found.
/// </param>
/// <returns>list of decoded string.</returns>
public string[] DetectAndDecode(InputArray inputImage, out Point2f[][] points)
{
if (inputImage is null)
throw new ArgumentNullException(nameof(inputImage));
inputImage.ThrowIfDisposed();

using var pointsVec = new VectorOfVectorPoint2f();
using var texts = new VectorOfString();
NativeMethods.HandleException(
NativeMethods.wechat_qrcode_WeChatQRCode_detectAndDecode_points(
CvPtr, inputImage.CvPtr, pointsVec.CvPtr, texts.CvPtr));

points = pointsVec.ToArray();
GC.KeepAlive(this);
GC.KeepAlive(inputImage);
return texts.ToArray();
}

/// <summary>
/// Both detects and decodes QR code.
/// Returns each QR code's corner points as a raw <see cref="Mat"/> (4x2, CV_32FC1),
/// which can be passed directly to other OpenCV functions.
/// </summary>
/// <param name="inputImage">supports grayscale or color(BGR) image.</param>
/// <param name="bbox">
/// output array of vertices of the found QR code quadrangles as raw <see cref="Mat"/> (4x2, CV_32FC1).
/// Will be empty if not found.
/// </param>
/// <returns>list of decoded string.</returns>
public string[] DetectAndDecodeRaw(InputArray inputImage, out Mat[] bbox)
{
if (inputImage is null)
throw new ArgumentNullException(nameof(inputImage));
Expand All @@ -71,9 +98,8 @@ public void DetectAndDecode(InputArray inputImage, out Mat[] bbox, out string[]
CvPtr, inputImage.CvPtr, bboxVec.CvPtr, texts.CvPtr));

bbox = bboxVec.ToArray();
results = texts.ToArray();
GC.KeepAlive(this);
GC.KeepAlive(inputImage);
return texts.ToArray();
}

}
}
18 changes: 18 additions & 0 deletions src/OpenCvSharpExtern/wechat_qrcode.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#ifndef NO_CONTRIB

#include "include_opencv.h"

CVAPI(ExceptionStatus) wechat_qrcode_create1(const char *detector_prototxt_path,
const char *detector_caffe_model_path ,
const char *super_resolution_prototxt_path ,
Expand All @@ -14,6 +15,7 @@ CVAPI(ExceptionStatus) wechat_qrcode_create1(const char *detector_prototxt_path,
super_resolution_prototxt_path, super_resolution_caffe_model_path);
END_WRAP
}

CVAPI(ExceptionStatus) wechat_qrcode_delete(cv::wechat_qrcode::WeChatQRCode* obj)
{
BEGIN_WRAP
Expand All @@ -29,4 +31,20 @@ CVAPI(ExceptionStatus) wechat_qrcode_WeChatQRCode_detectAndDecode(cv::wechat_qrc
END_WRAP
}

CVAPI(ExceptionStatus) wechat_qrcode_WeChatQRCode_detectAndDecode_points(cv::wechat_qrcode::WeChatQRCode* obj, cv::_InputArray* inputImage, std::vector<std::vector<cv::Point2f> >* points, std::vector<std::string>* texts)
{
BEGIN_WRAP
std::vector<cv::Mat> matPoints;
*texts = obj->detectAndDecode(*inputImage, matPoints);
points->clear();
for (const auto& mat : matPoints)
{
std::vector<cv::Point2f> pts;
for (int i = 0; i < mat.rows; i++)
pts.emplace_back(mat.at<float>(i, 0), mat.at<float>(i, 1));
points->push_back(std::move(pts));
}
END_WRAP
}

#endif // NO_CONTRIB
211 changes: 193 additions & 18 deletions test/OpenCvSharp.Tests/wechat_qrcode/WeChatQRCodeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,208 @@

namespace OpenCvSharp.Tests.WeChatQRCode;

#pragma warning disable CA1707 // Identifiers should not contain underscores

public class WeChatQRCodeTest(ITestOutputHelper testOutputHelper) : TestBase
{
private const string WechatQcodeDetectorPrototxtPath = "_data/wechat_qrcode/detect.prototxt";
private const string WechatQcodeDetectorCaffeModelPath = "_data/wechat_qrcode/detect.caffemodel";
private const string WechatQcodeSuperResolutionPrototxtPath = "_data/wechat_qrcode/sr.prototxt";
private const string WechatQcodeSuperResolutionCaffeModelPath = "_data/wechat_qrcode/sr.caffemodel";
private const string DetectorPrototxtPath = "_data/wechat_qrcode/detect.prototxt";
private const string DetectorCaffeModelPath = "_data/wechat_qrcode/detect.caffemodel";
private const string SuperResolutionPrototxtPath = "_data/wechat_qrcode/sr.prototxt";
private const string SuperResolutionCaffeModelPath = "_data/wechat_qrcode/sr.caffemodel";

private static readonly string[] ExpectedMultiQRTexts =
[
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[]^_`{|}",
"Helloこんにちは你好안녕하세요"
];

/// <summary>
/// Requires no model files. Verifies the no-arg constructor succeeds.
/// </summary>
[Fact]
public void Constructor_Default_DoesNotThrow()
{
using var qr = new OpenCvSharp.WeChatQRCode();
}

/// <summary>
/// Passing null for any string argument must throw ArgumentNullException.
/// </summary>
[Theory]
[InlineData(null, "", "", "")]
[InlineData("", null, "", "")]
[InlineData("", "", null, "")]
[InlineData("", "", "", null)]
public void Constructor_NullArguments_ThrowsArgumentNullException(
string? a, string? b, string? c, string? d)
{
Assert.Throws<ArgumentNullException>(() => new OpenCvSharp.WeChatQRCode(a!, b!, c!, d!));
}

/// <summary>
/// DetectAndDecode must throw ArgumentNullException when inputImage is null.
/// Does not require model files.
/// </summary>
[Fact]
public void DetectAndDecode_NullInput_ThrowsArgumentNullException()
{
using var qr = new OpenCvSharp.WeChatQRCode();
Assert.Throws<ArgumentNullException>(() => qr.DetectAndDecode(null!, out _));
}

/// <summary>
/// Grayscale image containing 2 QR codes. Both must be decoded correctly.
/// </summary>
[Fact]
public void DetectAndDecode_WithModels_MultiQR_ReturnsTexts()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);

var texts = qr.DetectAndDecode(src, out _);

Assert.Equal(2, texts.Length);
foreach (var text in texts)
{
testOutputHelper.WriteLine(text);
Assert.NotEmpty(text);
}
Assert.Equal(
ExpectedMultiQRTexts.OrderBy(x => x),
texts.OrderBy(x => x));
}

/// <summary>
/// Point2f[][] overload must return one array of 4 corners per detected QR code.
/// </summary>
[Fact]
public void WechatQrcodeDecodeRun()
public void DetectAndDecode_WithModels_Point2fOverload_Returns4CornersPerQR()
{
Assert.True(File.Exists(WechatQcodeDetectorPrototxtPath), $"DetectorPrototxt '{WechatQcodeDetectorPrototxtPath}' not found");
Assert.True(File.Exists(WechatQcodeDetectorCaffeModelPath), $"DetectorcaffeModel '{WechatQcodeDetectorCaffeModelPath}' not found");
Assert.True(File.Exists(WechatQcodeSuperResolutionPrototxtPath), $"SuperResolutionprototxt '{WechatQcodeSuperResolutionPrototxtPath}' not found");
Assert.True(File.Exists(WechatQcodeSuperResolutionCaffeModelPath), $"SuperResolutionCaffe_model '{WechatQcodeSuperResolutionCaffeModelPath}' not found");
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);

var texts = qr.DetectAndDecode(src, out var points);

Assert.Equal(2, texts.Length);
Assert.Equal(2, points.Length);
foreach (var corners in points)
{
Assert.Equal(4, corners.Length);
}
}

/// <summary>
/// Mat[] overload (DetectAndDecodeRaw) must return one non-empty Mat per detected QR code,
/// with 4 rows (corners) and 2 columns (x, y).
/// </summary>
[Fact]
public void DetectAndDecodeRaw_WithModels_MatOverload_ReturnsCorrectShape()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);

using var wechatQrcode = OpenCvSharp.WeChatQRCode.Create(
WechatQcodeDetectorPrototxtPath, WechatQcodeDetectorCaffeModelPath,
WechatQcodeSuperResolutionPrototxtPath, WechatQcodeSuperResolutionCaffeModelPath);
using var src = Cv2.ImRead(@"_data/image/qr_multi.png", ImreadModes.Grayscale);
var texts = qr.DetectAndDecodeRaw(src, out var bbox);

wechatQrcode.DetectAndDecode(src, out var rects, out var texts);
Assert.NotEmpty(texts);
Assert.Equal(2, texts.Length);
foreach (var item in texts)
Assert.Equal(2, bbox.Length);
foreach (var mat in bbox)
{
testOutputHelper.WriteLine(item);
Assert.NotEmpty(item);
Assert.False(mat.Empty());
// each corner mat stores 4 (x,y) values → Total() * Channels() == 8
Assert.Equal(8, (int)(mat.Total() * mat.Channels()));
}
}

/// <summary>
/// QR code containing single-byte (ASCII) characters must decode to the expected string.
/// </summary>
[Fact]
public void DetectAndDecode_WithModels_SinglebyteLetters()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = Cv2.ImRead("_data/image/qr_singlebyte_letters.png", ImreadModes.Grayscale);

var texts = qr.DetectAndDecode(src, out _);

Assert.Single(texts);
Assert.Equal(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[]^_`{|}",
texts[0]);
}

/// <summary>
/// QR code containing multibyte (Unicode) characters must decode to the expected string.
/// </summary>
[Fact]
public void DetectAndDecode_WithModels_MultibyteLetters()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = Cv2.ImRead("_data/image/qr_multibyte_letters.png", ImreadModes.Grayscale);

var texts = qr.DetectAndDecode(src, out _);

Assert.Single(texts);
Assert.Equal("Helloこんにちは你好안녕하세요", texts[0]);
}

/// <summary>
/// detectAndDecode accepts a color (BGR) image in addition to grayscale.
/// The result must be identical to processing the grayscale version.
/// </summary>
[Fact]
public void DetectAndDecode_WithModels_ColorBGRImage_DetectsQR()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var colorSrc = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Color);

var texts = qr.DetectAndDecode(colorSrc, out _);

Assert.Equal(2, texts.Length);
Assert.Equal(
ExpectedMultiQRTexts.OrderBy(x => x),
texts.OrderBy(x => x));
}

/// <summary>
/// An image that contains no QR codes must yield empty result arrays.
/// </summary>
[Fact]
public void DetectAndDecode_WithModels_NoQRCode_ReturnsEmpty()
{
SkipIfModelFilesNotFound();

using var qr = CreateWithModels();
using var src = LoadImage("lenna.png", ImreadModes.Grayscale);

var texts = qr.DetectAndDecode(src, out var points);

Assert.Empty(texts);
Assert.Empty(points);
}

// -------------------------------------------------------------------------

private static OpenCvSharp.WeChatQRCode CreateWithModels() =>
new(DetectorPrototxtPath, DetectorCaffeModelPath,
SuperResolutionPrototxtPath, SuperResolutionCaffeModelPath);

private static void SkipIfModelFilesNotFound()
{
Assert.True(File.Exists(DetectorPrototxtPath), $"Model file not found: '{DetectorPrototxtPath}'");
Assert.True(File.Exists(DetectorCaffeModelPath), $"Model file not found: '{DetectorCaffeModelPath}'");
Assert.True(File.Exists(SuperResolutionPrototxtPath), $"Model file not found: '{SuperResolutionPrototxtPath}'");
Assert.True(File.Exists(SuperResolutionCaffeModelPath), $"Model file not found: '{SuperResolutionCaffeModelPath}'");
}
}
Loading