11 Commits
Author SHA1 Message Date
Kq c4b0e597a9 new file: assets/picture21.jpg 2026-08-14 00:59:09 +02:00
Kq 765df60730 assety 2026-08-13 19:32:41 +02:00
Kq 7e9f11eaa2 make 2026-08-12 21:07:53 +02:00
Kq 3df7c68a3f update 2026-08-12 21:06:30 +02:00
Kq 2315c31d8c added AI changes 2026-08-12 20:58:37 +02:00
Kq 84c433ad19 modified: Makefile 2026-08-12 19:50:23 +02:00
Kq 71c48a5a6f modified: qrCamera/Program.cs 2026-08-12 19:44:59 +02:00
Kq 90120ee2cd modified: qrCamera/Program.cs 2026-08-12 19:41:53 +02:00
Kq 66ba176204 update 2026-08-12 19:31:18 +02:00
Kq a884b974cc update 2026-08-12 19:30:40 +02:00
Kq 4059a3b67b update 2026-08-12 19:11:44 +02:00
26 changed files with 524 additions and 74 deletions
+5
View File
@@ -1,7 +1,12 @@
/qrCamera/bin /qrCamera/bin
/qrCamera/obj /qrCamera/obj
/qrCamera/cache /qrCamera/cache
/qrCameraAI/bin
/qrCameraAI/obj
/qrCameraAI/cache
/createqrcodes/bin /createqrcodes/bin
/createqrcodes/obj /createqrcodes/obj
/cache
/qr /qr
/createqr /createqr
/qrcam
+9
View File
@@ -5,9 +5,18 @@ qr:
clean: clean:
rm -rf qrCamera/bin/ rm -rf qrCamera/bin/
rm -rf qrCamera/obj/ rm -rf qrCamera/obj/
rm -rf qrCamera/cache/
rm -rf cache/
rm -rf createqrcodes/obj/ rm -rf createqrcodes/obj/
rm -rf createqrcodes/bin/ rm -rf createqrcodes/bin/
rm -f createqr rm -f createqr
rm -f qrcam
rm -rf qr/ rm -rf qr/
run: run:
dotnet run -p:qrCamera/CameraPreview.csproj dotnet run -p:qrCamera/CameraPreview.csproj
runai:
dotnet run -p:qrCameraAI/CameraPreview.csproj
publish:
dotnet publish qrCamera/CameraPreview.csproj --self-contained true
publishai:
dotnet publish qrCameraAI/CameraPreview.csproj --self-contained true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+103 -71
View File
@@ -7,6 +7,9 @@ using ZXing.Common;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Drawing; using System.Drawing;
using System.Net; using System.Net;
using System.Runtime.InteropServices.Marshalling;
using Emgu.CV.Face;
using System.Transactions;
// using Emgu.CV.CvEnum; // using Emgu.CV.CvEnum;
@@ -15,6 +18,7 @@ namespace BarcodeReaderExample
{ {
class Program class Program
{ {
static bool draw;
static void Main(string[] args) static void Main(string[] args)
{ {
VideoCapture camera = new VideoCapture("/dev/video0", VideoCaptureAPIs.V4L2); VideoCapture camera = new VideoCapture("/dev/video0", VideoCaptureAPIs.V4L2);
@@ -24,16 +28,21 @@ namespace BarcodeReaderExample
camera.Set(VideoCaptureProperties.FrameWidth, 1280); camera.Set(VideoCaptureProperties.FrameWidth, 1280);
camera.Set(VideoCaptureProperties.FrameHeight, 720); camera.Set(VideoCaptureProperties.FrameHeight, 720);
camera.Set(VideoCaptureProperties.Fps, 24); int fps = 60;
if(Directory.Exists("cache")) int avalibeImages = 20;
camera.Set(VideoCaptureProperties.Fps, fps);
if (Directory.Exists("cache"))
{ {
Directory.Delete("cache",true); Directory.Delete("cache", true);
} }
Directory.CreateDirectory("cache"); Directory.CreateDirectory("cache");
Mat hey = new Mat("photo.png"); Mat hey = new Mat("photo.png");
Mat[] Cached = new Mat[10]; Mat[] Cached = new Mat[avalibeImages];
draw = true;
int imgX = 0;
int imgY = 0;
using var rgb = new Mat(); using var rgb = new Mat();
@@ -44,35 +53,40 @@ namespace BarcodeReaderExample
Cv2.NamedWindow("Camera preview", WindowFlags.AutoSize); Cv2.NamedWindow("Camera preview", WindowFlags.AutoSize);
int photocount = 0; // Mat src = Cv2.ImRead("test.png");
Mat src = Cv2.ImRead("test.png"); int whenWasLastTimeQrDetected = 0;
Mat Lastimage = new Mat();
IEnumerable<IEnumerable<OpenCvSharp.Point>> contours = null;
// PointF[] location;
string[] files = new string[10]; Mat previue = new Mat();
int files_index =0; string[] files = new string[avalibeImages];
int files_index = 0;
while (true) while (true)
{ {
// if (!camera.Read(frame) || frame.Empty())
// {
// Console.Error.WriteLine("Nie udało się odczytać klatki");
// break;
// }
camera.Read(frame); camera.Read(frame);
Result result = ReadQrCode(frame);
// Console.WriteLine(Lastimage.Empty());
if (!(Lastimage.Empty()))
{
Cv2.Resize(Lastimage, previue, new OpenCvSharp.Size(500, 500));
Mat roi2 = frame[new Rect(0, 0, previue.Width, previue.Height)];
previue.CopyTo(roi2);
}
Result result = GetResult(frame);
if (result != null) if (result != null)
{ {
whenWasLastTimeQrDetected = 5;
List<ResultPoint> points = result.ResultPoints.ToList(); List<ResultPoint> points = result.ResultPoints.ToList();
float[] pos1 = new float[2] { points[0].X, points[0].Y }; /// lewy dolny róg float[] pos1 = new float[2] { points[0].X, points[0].Y }; /// lewy dolny róg
float[] pos2 = new float[2] { points[2].X, points[2].Y }; ///prawy górny róg float[] pos2 = new float[2] { points[2].X, points[2].Y }; ///prawy górny róg
float[] pos3 = new float[2] { points[1].X, points[1].Y }; ///lewy górny róg float[] pos3 = new float[2] { points[1].X, points[1].Y }; ///lewy górny róg
int width = (int)(pos2[0] - pos3[0]); int width = (int)(pos2[0] - pos3[0]);
int heigh = (int)(pos1[1] - pos3[1]); int heigh = (int)(pos1[1] - pos3[1]);
// Cv2.Circle(frame, (int)pos1[0], (int)pos1[1], 5, Scalar.Red, 2);
// Cv2.Circle(frame, (int)pos2[0], (int)pos2[1], 5, Scalar.Green, 2);
// Cv2.Circle(frame, (int)pos3[0], (int)pos3[1], 5, Scalar.Blue, 2);
PointF[] location; PointF[] location;
try try
{ {
@@ -83,19 +97,6 @@ namespace BarcodeReaderExample
{ {
continue; continue;
} }
// Mat roi = frame[new Rect((int)pos3[0],(int)pos3[1], src.Width+3, src.Height)];
Mat roi = frame[new Rect((int)pos3[0], (int)pos3[1], width,heigh)];
src.CopyTo(roi);
// Console.WriteLine($"szerokość:{width}");
// Console.WriteLine($"wysokosc:{heigh}");
// Console.WriteLine($"x:{pos3[0]}");
// Console.WriteLine($"y:{pos3[1]}");
// DrawLine(location); //draw red squre
var points1 = new List<OpenCvSharp.Point> var points1 = new List<OpenCvSharp.Point>
{ {
GetPoint(location[0]), GetPoint(location[0]),
@@ -103,23 +104,28 @@ namespace BarcodeReaderExample
GetPoint(location[3]), GetPoint(location[3]),
GetPoint(location[2]) GetPoint(location[2])
}; };
int qrWidth = GetPoint(location[1]).X - GetPoint(location[0]).X;
// Zagnieźdź w dodatkową kolekcję: int qrHeigh = GetPoint(location[2]).Y - GetPoint(location[0]).Y;
IEnumerable<IEnumerable<OpenCvSharp.Point>> contours = new[] { points1 };
// Cv2.FillPoly(frame, contours, Scalar.White); // fill with white imgX = GetPoint(location[0]).X;
imgY = GetPoint(location[0]).Y;
contours = new[] { points1 };
Cv2.PutText(frame, result.Text, new OpenCvSharp.Point((int)(pos2[0]-pos3[0])/2+(int)pos3[0]-160, (int)(pos2[1]-pos3[1])/2+(int)pos3[1]+60), HersheyFonts.HersheySimplex, 0.8, Scalar.Black, 2); if (draw)
Cv2.FillPoly(frame, contours, Scalar.White); // fill with white
//Write Text
// Cv2.PutText(frame, result.Text, new OpenCvSharp.Point((int)(pos2[0]-pos3[0])/2+(int)pos3[0]-160, (int)(pos2[1]-pos3[1])/2+(int)pos3[1]+60), HersheyFonts.HersheySimplex, 0.8, Scalar.Black, 2);
if (!files.Contains(result.Text)) if (!files.Contains(result.Text))
{ {
files[files_index] = result.Text; files[files_index] = result.Text;
Console.WriteLine("downloading: "+result.Text); Console.WriteLine("downloading: " + result.Text);
using (var client = new WebClient()) using (var client = new WebClient())
{ {
client.DownloadFile(result.Text, "cache/"+files_index + ".png"); client.DownloadFile(result.Text, "cache/" + files_index + ".png");
// Thread.Sleep(2000); // Thread.Sleep(2000);
} }
@@ -128,37 +134,59 @@ namespace BarcodeReaderExample
} }
else else
{ {
// try
// {
int toload = files.IndexOf(result.Text); int toload = files.IndexOf(result.Text);
Mat toDraw= new Mat();
InputArray inputArray = toDraw;
// Console.WriteLine($"{qrWidth} , {qrHeigh}");
try
{
Cv2.Resize(Cached[toload],toDraw,new OpenCvSharp.Size(qrWidth,qrHeigh));
}
catch
{ continue; }
Lastimage = toDraw;
// toDraw.Resize(1,3);
try
{
// Mat roi2 = frame[new Rect(GetPoint(location[0]).X, GetPoint(location[0]).Y, toDraw.Width, toDraw.Height)];
// toDraw.CopyTo(roi2);
DrawImage(toDraw, GetPoint(location[0]).X, GetPoint(location[0]).Y);
}
catch { }
Mat roi2 = frame[new Rect((int)pos3[0], (int)pos3[1], Cached[toload].Width, Cached[toload].Height)];
Cached[toload].CopyTo(roi2);
// Console.WriteLine("work");
// }
// catch { }
} }
// Cv2.draw
// Cv2.Circle(frame, (int)(RightBottomX), (int)(RightBottomY), 5, Scalar.Green, 2);
//Cv2.Rectangle(frame, new Point((int)X, (int)Y), new Point ((int)Width, (int)Height), Scalar.Red, 2);
// Console.WriteLine(result.Text);
} }
else if (whenWasLastTimeQrDetected > 0)
{
if (draw)
Cv2.FillPoly(frame, contours, Scalar.White); // fill with white
try
{
// Mat roi2 = frame[new Rect(imgX, imgY, Lastimage.Width, Lastimage.Height)];
// Lastimage.CopyTo(roi2);
DrawImage(Lastimage,imgX,imgY);
}
catch { }
whenWasLastTimeQrDetected--;
}
// frame.SaveImage($"photos/photo_{photocount}.png");
// photocount++;
Cv2.ImShow("Camera preview", frame); Cv2.ImShow("Camera preview", frame);
Thread.Sleep(1000 / 24); Thread.Sleep(1000 / fps);
// Console.WriteLine("tick");
int key = Cv2.WaitKey(1); int key = Cv2.WaitKey(1);
if (key == 27 || key == 'q') if (key == 27 || key == 'q')
break; break;
@@ -167,9 +195,19 @@ namespace BarcodeReaderExample
Cv2.DestroyAllWindows(); Cv2.DestroyAllWindows();
} }
static void DrawImage(Mat image, int posX, int posY)
{
if(draw)
{
Mat roi2 = frame[new Rect(posX, posY, image.Width, image.Height)];
image.CopyTo(roi2);
}
}
static OpenCvSharp.Point GetPoint(PointF pointf) static OpenCvSharp.Point GetPoint(PointF pointf)
{ {
return new OpenCvSharp.Point(pointf.X,pointf.Y); return new OpenCvSharp.Point(pointf.X, pointf.Y);
} }
static PointF[] Calculate(float[] pos1, float[] pos2, float[] pos3, float[] pos4) static PointF[] Calculate(float[] pos1, float[] pos2, float[] pos3, float[] pos4)
{ {
@@ -207,7 +245,7 @@ namespace BarcodeReaderExample
PointF topRight = Expand(p2); PointF topRight = Expand(p2);
PointF bottomRight = ExpandLB(p4); PointF bottomRight = ExpandLB(p4);
PointF bottomLeft = Expand(p1); PointF bottomLeft = Expand(p1);
PointF[] Location = new PointF[] {topLeft,topRight,bottomLeft,bottomRight}; PointF[] Location = new PointF[] { topLeft, topRight, bottomLeft, bottomRight };
return Location; return Location;
// 4. Now draw the 4 lines // 4. Now draw the 4 lines
// Example with System.Drawing or Unity or whatever you use: // Example with System.Drawing or Unity or whatever you use:
@@ -215,7 +253,11 @@ namespace BarcodeReaderExample
} }
static void DrawLine(PointF p1, PointF p2) static void DrawLine(PointF p1, PointF p2)
{ {
if(draw)
{
Cv2.Line(frame, (int)p1.X, (int)p1.Y, (int)p2.X, (int)p2.Y, Scalar.Red, 2); Cv2.Line(frame, (int)p1.X, (int)p1.Y, (int)p2.X, (int)p2.Y, Scalar.Red, 2);
}
} }
static void DrawLine(PointF[] Location) static void DrawLine(PointF[] Location)
@@ -234,7 +276,7 @@ namespace BarcodeReaderExample
} }
static Mat frame = new Mat(); static Mat frame = new Mat();
static Result GetResult(Mat frame) static Result ReadQrCode(Mat frame)
{ {
QRCodeReader reader = new QRCodeReader(); QRCodeReader reader = new QRCodeReader();
@@ -288,13 +330,3 @@ namespace BarcodeReaderExample
} }
} }
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Emgu.CV" Version="4.13.0.5924" />
<PackageReference Include="Emgu.CV.Bitmap" Version="4.13.0.5924" />
<PackageReference Include="OpenCvSharp4" Version="4.13.0.20260627" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.13.0.20260627" />
<PackageReference Include="OpenCvSharp4.official.runtime.linux-x64" Version="4.13.0.20260627" />
<PackageReference Include="ZXing.Net" Version="0.16.11" />
<PackageReference Include="ZXing.Net.Bindings.Windows.Compatibility" Version="0.16.14" />
</ItemGroup>
</Project>
+384
View File
@@ -0,0 +1,384 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using OpenCvSharp;
using ZXing;
using ZXing.Common;
using ZXing.Multi;
using ZXing.QrCode;
namespace BarcodeReaderExample
{
class Program
{
// ---- tunables -------------------------------------------------
const bool EnableOverlay = true; // master draw switch (debug)
const bool TryHarderMultiDecode = true; // more reliable multi-QR detection, costs some speed
const float QuadExpansion = 1.50f; // how much bigger than the QR the overlay is drawn
const int MaxMissedFrames = 8; // keep last known position this many frames if briefly lost
const int TargetFps = 24;
const float LocalRedetectPadding = 0.6f; // extra search margin around last known box, as a fraction of its size
const int LocalRedetectUpscale = 3; // how much to enlarge the crop before decoding it
static readonly HttpClient Http = new HttpClient();
static readonly ConcurrentDictionary<string, Mat> ImageCache = new(); // text -> decoded image (null = failed)
static readonly ConcurrentDictionary<string, byte> Pending = new(); // text currently downloading
static readonly QRCodeReader QrReader = new QRCodeReader();
static readonly GenericMultipleBarcodeReader MultiReader = new GenericMultipleBarcodeReader(QrReader);
static readonly Dictionary<DecodeHintType, object> Hints = new()
{
{ DecodeHintType.POSSIBLE_FORMATS, new List<BarcodeFormat> { BarcodeFormat.QR_CODE } },
{ DecodeHintType.TRY_HARDER, TryHarderMultiDecode },
};
// Used only for the small, targeted local-redetect crop, so the extra cost is negligible there.
static readonly Dictionary<DecodeHintType, object> LocalHints = new()
{
{ DecodeHintType.POSSIBLE_FORMATS, new List<BarcodeFormat> { BarcodeFormat.QR_CODE } },
{ DecodeHintType.TRY_HARDER, true },
{ DecodeHintType.ALSO_INVERTED, true },
};
static readonly CLAHE Clahe = Cv2.CreateCLAHE(clipLimit: 3.0, tileGridSize: new Size(8, 8));
static byte[] _grayBuffer;
static readonly Dictionary<string, TrackedQr> Tracked = new();
class TrackedQr
{
public Point2f[] RawQuad; // TL, TR, BR, BL as detected, unexpanded - used to seed the next search
public Point2f[] Quad; // TL, TR, BR, BL, expanded - used for drawing
public int MissedFrames;
}
static void Main(string[] args)
{
using VideoCapture camera = new VideoCapture("/dev/video0", VideoCaptureAPIs.V4L2);
if (!camera.IsOpened())
throw new Exception("Nie można otworzyć kamery /dev/video0");
camera.Set(VideoCaptureProperties.FrameWidth, 1280);
camera.Set(VideoCaptureProperties.FrameHeight, 720);
camera.Set(VideoCaptureProperties.Fps, TargetFps);
Cv2.NamedWindow("Camera preview", WindowFlags.AutoSize);
using var frame = new Mat();
using var gray = new Mat();
var sw = Stopwatch.StartNew();
var frameTimer = Stopwatch.StartNew();
int frameCount = 0;
double fps = 0;
try
{
while (true)
{
frameTimer.Restart();
if (!camera.Read(frame) || frame.Empty())
continue;
try
{
ProcessFrame(frame, gray);
}
catch (Exception ex)
{
// Never let a single bad frame kill the capture loop.
Console.WriteLine("Frame processing error: " + ex.Message);
}
frameCount++;
if (sw.ElapsedMilliseconds >= 500)
{
fps = frameCount / (sw.ElapsedMilliseconds / 1000.0);
frameCount = 0;
sw.Restart();
}
Cv2.PutText(frame, $"{fps:0.0} fps", new Point(10, frame.Height - 15),
HersheyFonts.HersheySimplex, 0.6, Scalar.Lime, 2);
Cv2.ImShow("Camera preview", frame);
// Only sleep for whatever time is left in the frame budget instead of
// always sleeping the full 1000/fps on top of WaitKey (which halved
// the effective frame rate in the original code).
int budgetMs = 1000 / TargetFps;
int elapsedMs = (int)frameTimer.ElapsedMilliseconds;
int waitMs = Math.Max(1, budgetMs - elapsedMs);
int key = Cv2.WaitKey(waitMs);
if (key == 27 || key == 'q')
break;
}
}
finally
{
foreach (var mat in ImageCache.Values)
mat?.Dispose();
Cv2.DestroyAllWindows();
}
}
static void ProcessFrame(Mat frame, Mat gray)
{
Cv2.CvtColor(frame, gray, ColorConversionCodes.BGR2GRAY);
// Contrast-normalize before decoding - this is the single biggest lever for detection
// reliability under uneven lighting (glare, shadow across part of the code, dim rooms).
using var enhanced = new Mat();
Clahe.Apply(gray, enhanced);
Result[] results = ReadQrCodes(enhanced);
var seenThisFrame = new HashSet<string>();
foreach (var result in results)
{
if (result?.Text == null) continue;
var (pTL, pTR, pBR, pBL) = CornersFromResult(result);
if (pTL == null) continue;
UpdateTracked(result.Text, pTL.Value, pTR.Value, pBR.Value, pBL.Value);
seenThisFrame.Add(result.Text);
EnsureImageLoaded(result.Text);
}
// For codes we're already tracking but the global pass didn't find this frame, actively
// re-search a small, upscaled crop around their last known position instead of just
// freezing them in place. This recovers far more often for codes that are small,
// partially blurred, or at a steep angle - the global full-frame pass is much less
// likely to catch those, but a zoomed-in targeted decode often still can.
foreach (var kv in Tracked)
{
if (seenThisFrame.Contains(kv.Key)) continue;
if (TryLocalRedetect(enhanced, kv.Value.RawQuad, out var pTL, out var pTR, out var pBR, out var pBL))
{
UpdateTracked(kv.Key, pTL, pTR, pBR, pBL);
seenThisFrame.Add(kv.Key);
}
}
// Age out codes that weren't seen this frame; keep them briefly to avoid flicker.
List<string> toRemove = null;
foreach (var kv in Tracked)
{
if (seenThisFrame.Contains(kv.Key)) continue;
kv.Value.MissedFrames++;
if (kv.Value.MissedFrames > MaxMissedFrames)
(toRemove ??= new List<string>()).Add(kv.Key);
}
if (toRemove != null)
foreach (var key in toRemove) Tracked.Remove(key);
if (!EnableOverlay) return;
foreach (var kv in Tracked)
{
if (ImageCache.TryGetValue(kv.Key, out Mat img) && img != null && !img.Empty())
{
OverlayImagePerspective(frame, img, kv.Value.Quad);
}
}
}
static void UpdateTracked(string text, Point2f tl, Point2f tr, Point2f br, Point2f bl)
{
Point2f[] raw = { tl, tr, br, bl };
Point2f[] quad = ExpandQuad(tl, tr, br, bl, QuadExpansion);
if (Tracked.TryGetValue(text, out var t))
{
t.RawQuad = raw;
t.Quad = quad;
t.MissedFrames = 0;
}
else
{
Tracked[text] = new TrackedQr { RawQuad = raw, Quad = quad, MissedFrames = 0 };
}
}
/// <summary>
/// ZXing QR point order: [0]=bottom-left, [1]=top-left, [2]=top-right, optional [3]=alignment
/// pattern. We deliberately do NOT use points[3] for the bottom-right corner: the alignment
/// pattern sits several modules inset from the actual edge of the code, so it's always
/// noticeably closer to the center than the true corner is - using it directly shrinks that
/// whole side of the overlay. Instead derive the true bottom-right corner from the 3
/// finder-pattern corners, which ZXing locates precisely, assuming the QR is (near) square:
/// BR = TR + BL - TL.
/// </summary>
static (Point2f? tl, Point2f? tr, Point2f? br, Point2f? bl) CornersFromResult(Result result)
{
var points = result?.ResultPoints;
if (points == null || points.Length < 3) return (null, null, null, null);
Point2f pBL = new Point2f(points[0].X, points[0].Y);
Point2f pTL = new Point2f(points[1].X, points[1].Y);
Point2f pTR = new Point2f(points[2].X, points[2].Y);
Point2f pBR = new Point2f(pTR.X + pBL.X - pTL.X, pTR.Y + pBL.Y - pTL.Y);
return (pTL, pTR, pBR, pBL);
}
/// <summary>Expand a quad (TL,TR,BR,BL) outward from its centroid by a scale factor.</summary>
static Point2f[] ExpandQuad(Point2f tl, Point2f tr, Point2f br, Point2f bl, float scale)
{
float cx = (tl.X + tr.X + br.X + bl.X) / 4f;
float cy = (tl.Y + tr.Y + br.Y + bl.Y) / 4f;
Point2f Expand(Point2f p) => new Point2f(cx + (p.X - cx) * scale, cy + (p.Y - cy) * scale);
return new[] { Expand(tl), Expand(tr), Expand(br), Expand(bl) };
}
/// <summary>
/// Warp `overlayImage` onto the quadrilateral `quad` (TL,TR,BR,BL) in `frame`, so the
/// pasted image follows the QR code's rotation/perspective instead of a plain axis-aligned resize.
/// Only the quad's bounding box is warped, not the whole frame, to keep this fast with
/// several codes on screen at once.
/// </summary>
static void OverlayImagePerspective(Mat frame, Mat overlayImage, Point2f[] quad)
{
var intPoints = quad.Select(p => new Point((int)p.X, (int)p.Y)).ToArray();
Rect rect = Cv2.BoundingRect(intPoints);
rect = rect.Intersect(new Rect(0, 0, frame.Width, frame.Height));
if (rect.Width <= 1 || rect.Height <= 1) return;
int w = overlayImage.Width, h = overlayImage.Height;
Point2f[] src = { new(0, 0), new(w, 0), new(w, h), new(0, h) };
Point2f[] dstLocal = quad.Select(p => new Point2f(p.X - rect.X, p.Y - rect.Y)).ToArray();
using Mat homography = Cv2.GetPerspectiveTransform(src, dstLocal);
using var warped = new Mat();
Cv2.WarpPerspective(overlayImage, warped, homography, rect.Size);
using var mask = new Mat(rect.Size, MatType.CV_8UC1, Scalar.Black);
var localPoly = dstLocal.Select(p => new Point((int)p.X, (int)p.Y)).ToArray();
Cv2.FillPoly(mask, new[] { localPoly }, Scalar.White);
using var roi = new Mat(frame, rect);
warped.CopyTo(roi, mask);
}
// ---- decoding ---------------------------------------------------
static Result[] ReadQrCodes(Mat gray)
{
var luminanceSource = ToLuminanceSource(gray, ref _grayBuffer);
var bitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
try
{
return MultiReader.decodeMultiple(bitmap, Hints) ?? Array.Empty<Result>();
}
catch
{
return Array.Empty<Result>();
}
}
/// <summary>
/// Crop tightly around a code's last known position (with padding for movement), upscale
/// that crop, and try to decode just it. Works much better than a full-frame scan for a
/// code that's already small, tilted, or motion-blurred, since the decoder effectively gets
/// a bigger, cleaner version of just the region that matters.
/// </summary>
static bool TryLocalRedetect(Mat enhanced, Point2f[] lastRaw, out Point2f tl, out Point2f tr, out Point2f br, out Point2f bl)
{
tl = tr = br = bl = default;
if (lastRaw == null) return false;
var intPoints = lastRaw.Select(p => new Point((int)p.X, (int)p.Y)).ToArray();
Rect box = Cv2.BoundingRect(intPoints);
int pad = (int)(Math.Max(box.Width, box.Height) * LocalRedetectPadding);
Rect search = new Rect(box.X - pad, box.Y - pad, box.Width + pad * 2, box.Height + pad * 2);
search = search.Intersect(new Rect(0, 0, enhanced.Width, enhanced.Height));
if (search.Width < 15 || search.Height < 15) return false;
using var crop = new Mat(enhanced, search);
using var scaled = new Mat();
Cv2.Resize(crop, scaled, new Size(crop.Width * LocalRedetectUpscale, crop.Height * LocalRedetectUpscale),
0, 0, InterpolationFlags.Cubic);
byte[] localBuffer = null;
var luminanceSource = ToLuminanceSource(scaled, ref localBuffer);
var bitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
Result result;
try { result = QrReader.decode(bitmap, LocalHints); }
catch { return false; }
if (result == null) return false;
var (cTl, cTr, cBr, cBl) = CornersFromResult(result);
if (cTl == null) return false;
// Map corners from the upscaled crop back into full-frame coordinates.
Point2f ToFrame(Point2f p) => new Point2f(
search.X + p.X / LocalRedetectUpscale,
search.Y + p.Y / LocalRedetectUpscale);
tl = ToFrame(cTl.Value);
tr = ToFrame(cTr.Value);
br = ToFrame(cBr.Value);
bl = ToFrame(cBl.Value);
return true;
}
static RGBLuminanceSource ToLuminanceSource(Mat gray, ref byte[] buffer)
{
Mat continuous = gray.IsContinuous() ? gray : gray.Clone();
int size = (int)(continuous.Total() * continuous.ElemSize());
if (buffer == null || buffer.Length != size)
buffer = new byte[size];
Marshal.Copy(continuous.Data, buffer, 0, size);
// Gray8 skips ZXing's internal RGB->luminance conversion entirely - the fastest
// format to hand it, and matches what BGR2GRAY/CLAHE already produced for us.
var source = new RGBLuminanceSource(buffer, continuous.Width, continuous.Height, RGBLuminanceSource.BitmapFormat.Gray8);
if (!ReferenceEquals(continuous, gray))
continuous.Dispose();
return source;
}
// ---- async image download/caching --------------------------------
static void EnsureImageLoaded(string url)
{
if (ImageCache.ContainsKey(url) || Pending.ContainsKey(url)) return;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
// Not a downloadable URL - remember that so we don't keep retrying every frame.
ImageCache[url] = null;
return;
}
Pending[url] = 0;
_ = Task.Run(async () =>
{
try
{
byte[] bytes = await Http.GetByteArrayAsync(uri);
Mat decoded = Mat.FromImageData(bytes, ImreadModes.Color);
ImageCache[url] = decoded.Empty() ? null : decoded;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to download {url}: {ex.Message}");
ImageCache[url] = null; // negative-cache so we stop retrying a bad URL
}
finally
{
Pending.TryRemove(url, out _);
}
});
}
}
}