Skip to content

Commit bf8f6dd

Browse files
authored
Merge pull request #1859 from shimat/qrcode_fix
Fix WeChatQRCode wrapper: replace static factory with constructor, add Point2f[][] overload, expand tests
2 parents 149aaff + 577eb0c commit bf8f6dd

4 files changed

Lines changed: 269 additions & 46 deletions

File tree

src/OpenCvSharp/Internal/PInvoke/NativeMethods/NativeMethods_wechat_qrcode.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ public static extern ExceptionStatus wechat_qrcode_create1([MarshalAs(UnmanagedT
1515
[MarshalAs(UnmanagedType.LPStr)] string detector_caffe_model_path,
1616
[MarshalAs(UnmanagedType.LPStr)] string super_resolution_prototxt_path ,
1717
[MarshalAs(UnmanagedType.LPStr)] string super_resolution_caffe_model_path,out IntPtr ptr);
18+
1819
[DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
1920
public static extern ExceptionStatus wechat_qrcode_WeChatQRCode_detectAndDecode(IntPtr obj, IntPtr inputImage, IntPtr points, IntPtr texts);
2021

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

2226
[DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
2327
public static extern ExceptionStatus wechat_qrcode_delete(IntPtr ptr);

src/OpenCvSharp/Modules/wechat_qrcode/WeChatQRCode.cs

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,54 +11,81 @@ namespace OpenCvSharp;
1111
/// </summary>
1212
public class WeChatQRCode : CvObject
1313
{
14-
internal WeChatQRCode(IntPtr ptr)
15-
{
16-
SetSafeHandle(new OpenCvPtrSafeHandle(ptr, ownsHandle: true,
17-
releaseAction: h => NativeMethods.HandleException(NativeMethods.wechat_qrcode_delete(h))));
18-
}
19-
2014
/// <summary>
2115
/// Initialize the WeChatQRCode.
2216
/// It includes two models, which are packaged with caffe format.
2317
/// Therefore, there are prototxt and caffe models (In total, four paramenters).
18+
/// Pass empty strings to create a detector without neural network models.
2419
/// </summary>
2520
/// <param name="detectorPrototxtPath">prototxt file path for the detector</param>
2621
/// <param name="detectorCaffeModelPath">caffe model file path for the detector</param>
2722
/// <param name="superResolutionPrototxtPath">prototxt file path for the super resolution model</param>
2823
/// <param name="superResolutionCaffeModelPath">caffe file path for the super resolution model</param>
29-
/// <returns></returns>
30-
/// <exception cref="ArgumentException"></exception>
31-
public static WeChatQRCode Create(
32-
string detectorPrototxtPath,
33-
string detectorCaffeModelPath,
34-
string superResolutionPrototxtPath,
35-
string superResolutionCaffeModelPath)
24+
public WeChatQRCode(
25+
string detectorPrototxtPath = "",
26+
string detectorCaffeModelPath = "",
27+
string superResolutionPrototxtPath = "",
28+
string superResolutionCaffeModelPath = "")
3629
{
37-
if (string.IsNullOrWhiteSpace(detectorPrototxtPath))
38-
throw new ArgumentException("empty string", nameof(detectorPrototxtPath));
39-
if (string.IsNullOrWhiteSpace(detectorCaffeModelPath))
40-
throw new ArgumentException("empty string", nameof(detectorCaffeModelPath));
41-
if (string.IsNullOrWhiteSpace(superResolutionPrototxtPath))
42-
throw new ArgumentException("empty string", nameof(superResolutionPrototxtPath));
43-
if (string.IsNullOrWhiteSpace(superResolutionCaffeModelPath))
44-
throw new ArgumentException("empty string", nameof(superResolutionCaffeModelPath));
30+
if (detectorPrototxtPath is null)
31+
throw new ArgumentNullException(nameof(detectorPrototxtPath));
32+
if (detectorCaffeModelPath is null)
33+
throw new ArgumentNullException(nameof(detectorCaffeModelPath));
34+
if (superResolutionPrototxtPath is null)
35+
throw new ArgumentNullException(nameof(superResolutionPrototxtPath));
36+
if (superResolutionCaffeModelPath is null)
37+
throw new ArgumentNullException(nameof(superResolutionCaffeModelPath));
4538

4639
NativeMethods.HandleException(
4740
NativeMethods.wechat_qrcode_create1(
4841
detectorPrototxtPath, detectorCaffeModelPath, superResolutionPrototxtPath, superResolutionCaffeModelPath,
4942
out var ptr));
5043

51-
return new WeChatQRCode(ptr);
44+
SetSafeHandle(new OpenCvPtrSafeHandle(ptr, ownsHandle: true,
45+
releaseAction: h => NativeMethods.HandleException(NativeMethods.wechat_qrcode_delete(h))));
5246
}
5347

5448
/// <summary>
5549
/// Both detects and decodes QR code.
5650
/// To simplify the usage, there is a only API: detectAndDecode
5751
/// </summary>
5852
/// <param name="inputImage">supports grayscale or color(BGR) image.</param>
59-
/// <param name="bbox">optional output array of vertices of the found QR code quadrangle.Will be empty if not found.</param>
60-
/// <param name="results">list of decoded string.</param>
61-
public void DetectAndDecode(InputArray inputImage, out Mat[] bbox, out string[] results)
53+
/// <param name="points">
54+
/// output array of vertices of the found QR code quadrangles.
55+
/// Each element is an array of 4 <see cref="Point2f"/> representing the corners of one detected QR code.
56+
/// Will be empty if not found.
57+
/// </param>
58+
/// <returns>list of decoded string.</returns>
59+
public string[] DetectAndDecode(InputArray inputImage, out Point2f[][] points)
60+
{
61+
if (inputImage is null)
62+
throw new ArgumentNullException(nameof(inputImage));
63+
inputImage.ThrowIfDisposed();
64+
65+
using var pointsVec = new VectorOfVectorPoint2f();
66+
using var texts = new VectorOfString();
67+
NativeMethods.HandleException(
68+
NativeMethods.wechat_qrcode_WeChatQRCode_detectAndDecode_points(
69+
CvPtr, inputImage.CvPtr, pointsVec.CvPtr, texts.CvPtr));
70+
71+
points = pointsVec.ToArray();
72+
GC.KeepAlive(this);
73+
GC.KeepAlive(inputImage);
74+
return texts.ToArray();
75+
}
76+
77+
/// <summary>
78+
/// Both detects and decodes QR code.
79+
/// Returns each QR code's corner points as a raw <see cref="Mat"/> (4x2, CV_32FC1),
80+
/// which can be passed directly to other OpenCV functions.
81+
/// </summary>
82+
/// <param name="inputImage">supports grayscale or color(BGR) image.</param>
83+
/// <param name="bbox">
84+
/// output array of vertices of the found QR code quadrangles as raw <see cref="Mat"/> (4x2, CV_32FC1).
85+
/// Will be empty if not found.
86+
/// </param>
87+
/// <returns>list of decoded string.</returns>
88+
public string[] DetectAndDecodeRaw(InputArray inputImage, out Mat[] bbox)
6289
{
6390
if (inputImage is null)
6491
throw new ArgumentNullException(nameof(inputImage));
@@ -71,9 +98,8 @@ public void DetectAndDecode(InputArray inputImage, out Mat[] bbox, out string[]
7198
CvPtr, inputImage.CvPtr, bboxVec.CvPtr, texts.CvPtr));
7299

73100
bbox = bboxVec.ToArray();
74-
results = texts.ToArray();
75101
GC.KeepAlive(this);
76102
GC.KeepAlive(inputImage);
103+
return texts.ToArray();
77104
}
78-
79-
}
105+
}

src/OpenCvSharpExtern/wechat_qrcode.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#ifndef NO_CONTRIB
44

55
#include "include_opencv.h"
6+
67
CVAPI(ExceptionStatus) wechat_qrcode_create1(const char *detector_prototxt_path,
78
const char *detector_caffe_model_path ,
89
const char *super_resolution_prototxt_path ,
@@ -14,6 +15,7 @@ CVAPI(ExceptionStatus) wechat_qrcode_create1(const char *detector_prototxt_path,
1415
super_resolution_prototxt_path, super_resolution_caffe_model_path);
1516
END_WRAP
1617
}
18+
1719
CVAPI(ExceptionStatus) wechat_qrcode_delete(cv::wechat_qrcode::WeChatQRCode* obj)
1820
{
1921
BEGIN_WRAP
@@ -29,4 +31,20 @@ CVAPI(ExceptionStatus) wechat_qrcode_WeChatQRCode_detectAndDecode(cv::wechat_qrc
2931
END_WRAP
3032
}
3133

34+
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)
35+
{
36+
BEGIN_WRAP
37+
std::vector<cv::Mat> matPoints;
38+
*texts = obj->detectAndDecode(*inputImage, matPoints);
39+
points->clear();
40+
for (const auto& mat : matPoints)
41+
{
42+
std::vector<cv::Point2f> pts;
43+
for (int i = 0; i < mat.rows; i++)
44+
pts.emplace_back(mat.at<float>(i, 0), mat.at<float>(i, 1));
45+
points->push_back(std::move(pts));
46+
}
47+
END_WRAP
48+
}
49+
3250
#endif // NO_CONTRIB

test/OpenCvSharp.Tests/wechat_qrcode/WeChatQRCodeTest.cs

Lines changed: 193 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,208 @@
22

33
namespace OpenCvSharp.Tests.WeChatQRCode;
44

5+
#pragma warning disable CA1707 // Identifiers should not contain underscores
6+
57
public class WeChatQRCodeTest(ITestOutputHelper testOutputHelper) : TestBase
68
{
7-
private const string WechatQcodeDetectorPrototxtPath = "_data/wechat_qrcode/detect.prototxt";
8-
private const string WechatQcodeDetectorCaffeModelPath = "_data/wechat_qrcode/detect.caffemodel";
9-
private const string WechatQcodeSuperResolutionPrototxtPath = "_data/wechat_qrcode/sr.prototxt";
10-
private const string WechatQcodeSuperResolutionCaffeModelPath = "_data/wechat_qrcode/sr.caffemodel";
9+
private const string DetectorPrototxtPath = "_data/wechat_qrcode/detect.prototxt";
10+
private const string DetectorCaffeModelPath = "_data/wechat_qrcode/detect.caffemodel";
11+
private const string SuperResolutionPrototxtPath = "_data/wechat_qrcode/sr.prototxt";
12+
private const string SuperResolutionCaffeModelPath = "_data/wechat_qrcode/sr.caffemodel";
13+
14+
private static readonly string[] ExpectedMultiQRTexts =
15+
[
16+
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[]^_`{|}",
17+
"Helloこんにちは你好안녕하세요"
18+
];
19+
20+
/// <summary>
21+
/// Requires no model files. Verifies the no-arg constructor succeeds.
22+
/// </summary>
23+
[Fact]
24+
public void Constructor_Default_DoesNotThrow()
25+
{
26+
using var qr = new OpenCvSharp.WeChatQRCode();
27+
}
28+
29+
/// <summary>
30+
/// Passing null for any string argument must throw ArgumentNullException.
31+
/// </summary>
32+
[Theory]
33+
[InlineData(null, "", "", "")]
34+
[InlineData("", null, "", "")]
35+
[InlineData("", "", null, "")]
36+
[InlineData("", "", "", null)]
37+
public void Constructor_NullArguments_ThrowsArgumentNullException(
38+
string? a, string? b, string? c, string? d)
39+
{
40+
Assert.Throws<ArgumentNullException>(() => new OpenCvSharp.WeChatQRCode(a!, b!, c!, d!));
41+
}
42+
43+
/// <summary>
44+
/// DetectAndDecode must throw ArgumentNullException when inputImage is null.
45+
/// Does not require model files.
46+
/// </summary>
47+
[Fact]
48+
public void DetectAndDecode_NullInput_ThrowsArgumentNullException()
49+
{
50+
using var qr = new OpenCvSharp.WeChatQRCode();
51+
Assert.Throws<ArgumentNullException>(() => qr.DetectAndDecode(null!, out _));
52+
}
53+
54+
/// <summary>
55+
/// Grayscale image containing 2 QR codes. Both must be decoded correctly.
56+
/// </summary>
57+
[Fact]
58+
public void DetectAndDecode_WithModels_MultiQR_ReturnsTexts()
59+
{
60+
SkipIfModelFilesNotFound();
61+
62+
using var qr = CreateWithModels();
63+
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);
1164

65+
var texts = qr.DetectAndDecode(src, out _);
66+
67+
Assert.Equal(2, texts.Length);
68+
foreach (var text in texts)
69+
{
70+
testOutputHelper.WriteLine(text);
71+
Assert.NotEmpty(text);
72+
}
73+
Assert.Equal(
74+
ExpectedMultiQRTexts.OrderBy(x => x),
75+
texts.OrderBy(x => x));
76+
}
77+
78+
/// <summary>
79+
/// Point2f[][] overload must return one array of 4 corners per detected QR code.
80+
/// </summary>
1281
[Fact]
13-
public void WechatQrcodeDecodeRun()
82+
public void DetectAndDecode_WithModels_Point2fOverload_Returns4CornersPerQR()
1483
{
15-
Assert.True(File.Exists(WechatQcodeDetectorPrototxtPath), $"DetectorPrototxt '{WechatQcodeDetectorPrototxtPath}' not found");
16-
Assert.True(File.Exists(WechatQcodeDetectorCaffeModelPath), $"DetectorcaffeModel '{WechatQcodeDetectorCaffeModelPath}' not found");
17-
Assert.True(File.Exists(WechatQcodeSuperResolutionPrototxtPath), $"SuperResolutionprototxt '{WechatQcodeSuperResolutionPrototxtPath}' not found");
18-
Assert.True(File.Exists(WechatQcodeSuperResolutionCaffeModelPath), $"SuperResolutionCaffe_model '{WechatQcodeSuperResolutionCaffeModelPath}' not found");
84+
SkipIfModelFilesNotFound();
85+
86+
using var qr = CreateWithModels();
87+
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);
88+
89+
var texts = qr.DetectAndDecode(src, out var points);
90+
91+
Assert.Equal(2, texts.Length);
92+
Assert.Equal(2, points.Length);
93+
foreach (var corners in points)
94+
{
95+
Assert.Equal(4, corners.Length);
96+
}
97+
}
98+
99+
/// <summary>
100+
/// Mat[] overload (DetectAndDecodeRaw) must return one non-empty Mat per detected QR code,
101+
/// with 4 rows (corners) and 2 columns (x, y).
102+
/// </summary>
103+
[Fact]
104+
public void DetectAndDecodeRaw_WithModels_MatOverload_ReturnsCorrectShape()
105+
{
106+
SkipIfModelFilesNotFound();
107+
108+
using var qr = CreateWithModels();
109+
using var src = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Grayscale);
19110

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

25-
wechatQrcode.DetectAndDecode(src, out var rects, out var texts);
26-
Assert.NotEmpty(texts);
27113
Assert.Equal(2, texts.Length);
28-
foreach (var item in texts)
114+
Assert.Equal(2, bbox.Length);
115+
foreach (var mat in bbox)
29116
{
30-
testOutputHelper.WriteLine(item);
31-
Assert.NotEmpty(item);
117+
Assert.False(mat.Empty());
118+
// each corner mat stores 4 (x,y) values → Total() * Channels() == 8
119+
Assert.Equal(8, (int)(mat.Total() * mat.Channels()));
32120
}
33121
}
122+
123+
/// <summary>
124+
/// QR code containing single-byte (ASCII) characters must decode to the expected string.
125+
/// </summary>
126+
[Fact]
127+
public void DetectAndDecode_WithModels_SinglebyteLetters()
128+
{
129+
SkipIfModelFilesNotFound();
130+
131+
using var qr = CreateWithModels();
132+
using var src = Cv2.ImRead("_data/image/qr_singlebyte_letters.png", ImreadModes.Grayscale);
133+
134+
var texts = qr.DetectAndDecode(src, out _);
135+
136+
Assert.Single(texts);
137+
Assert.Equal(
138+
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[]^_`{|}",
139+
texts[0]);
140+
}
141+
142+
/// <summary>
143+
/// QR code containing multibyte (Unicode) characters must decode to the expected string.
144+
/// </summary>
145+
[Fact]
146+
public void DetectAndDecode_WithModels_MultibyteLetters()
147+
{
148+
SkipIfModelFilesNotFound();
149+
150+
using var qr = CreateWithModels();
151+
using var src = Cv2.ImRead("_data/image/qr_multibyte_letters.png", ImreadModes.Grayscale);
152+
153+
var texts = qr.DetectAndDecode(src, out _);
154+
155+
Assert.Single(texts);
156+
Assert.Equal("Helloこんにちは你好안녕하세요", texts[0]);
157+
}
158+
159+
/// <summary>
160+
/// detectAndDecode accepts a color (BGR) image in addition to grayscale.
161+
/// The result must be identical to processing the grayscale version.
162+
/// </summary>
163+
[Fact]
164+
public void DetectAndDecode_WithModels_ColorBGRImage_DetectsQR()
165+
{
166+
SkipIfModelFilesNotFound();
167+
168+
using var qr = CreateWithModels();
169+
using var colorSrc = Cv2.ImRead("_data/image/qr_multi.png", ImreadModes.Color);
170+
171+
var texts = qr.DetectAndDecode(colorSrc, out _);
172+
173+
Assert.Equal(2, texts.Length);
174+
Assert.Equal(
175+
ExpectedMultiQRTexts.OrderBy(x => x),
176+
texts.OrderBy(x => x));
177+
}
178+
179+
/// <summary>
180+
/// An image that contains no QR codes must yield empty result arrays.
181+
/// </summary>
182+
[Fact]
183+
public void DetectAndDecode_WithModels_NoQRCode_ReturnsEmpty()
184+
{
185+
SkipIfModelFilesNotFound();
186+
187+
using var qr = CreateWithModels();
188+
using var src = LoadImage("lenna.png", ImreadModes.Grayscale);
189+
190+
var texts = qr.DetectAndDecode(src, out var points);
191+
192+
Assert.Empty(texts);
193+
Assert.Empty(points);
194+
}
195+
196+
// -------------------------------------------------------------------------
197+
198+
private static OpenCvSharp.WeChatQRCode CreateWithModels() =>
199+
new(DetectorPrototxtPath, DetectorCaffeModelPath,
200+
SuperResolutionPrototxtPath, SuperResolutionCaffeModelPath);
201+
202+
private static void SkipIfModelFilesNotFound()
203+
{
204+
Assert.True(File.Exists(DetectorPrototxtPath), $"Model file not found: '{DetectorPrototxtPath}'");
205+
Assert.True(File.Exists(DetectorCaffeModelPath), $"Model file not found: '{DetectorCaffeModelPath}'");
206+
Assert.True(File.Exists(SuperResolutionPrototxtPath), $"Model file not found: '{SuperResolutionPrototxtPath}'");
207+
Assert.True(File.Exists(SuperResolutionCaffeModelPath), $"Model file not found: '{SuperResolutionCaffeModelPath}'");
208+
}
34209
}

0 commit comments

Comments
 (0)