diff --git a/.gitignore b/.gitignore
index 46ca6dd..52d5917 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
/qrCamera/bin
/qrCamera/obj
/qrCamera/cache
+/qrCameraAI/bin
+/qrCameraAI/obj
+/qrCameraAI/cache
/createqrcodes/bin
/createqrcodes/obj
/cache
diff --git a/qrCameraAI/CameraPreview.csproj b/qrCameraAI/CameraPreview.csproj
new file mode 100644
index 0000000..e337f10
--- /dev/null
+++ b/qrCameraAI/CameraPreview.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qrCameraAI/Program.cs b/qrCameraAI/Program.cs
new file mode 100644
index 0000000..e6418e8
--- /dev/null
+++ b/qrCameraAI/Program.cs
@@ -0,0 +1,283 @@
+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.60f; // 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 = 60;
+
+ static readonly HttpClient Http = new HttpClient();
+ static readonly ConcurrentDictionary ImageCache = new(); // text -> decoded image (null = failed)
+ static readonly ConcurrentDictionary Pending = new(); // text currently downloading
+
+ static readonly QRCodeReader QrReader = new QRCodeReader();
+ static readonly GenericMultipleBarcodeReader MultiReader = new GenericMultipleBarcodeReader(QrReader);
+ static readonly Dictionary Hints = new()
+ {
+ { DecodeHintType.POSSIBLE_FORMATS, new List { BarcodeFormat.QR_CODE } },
+ { DecodeHintType.TRY_HARDER, TryHarderMultiDecode },
+ };
+
+ static byte[] _grayBuffer;
+ static readonly Dictionary Tracked = new();
+
+ class TrackedQr
+ {
+ public Point2f[] Quad; // TL, TR, BR, BL (frame coordinates)
+ 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);
+ Result[] results = ReadQrCodes(gray);
+
+ var seenThisFrame = new HashSet();
+
+ foreach (var result in results)
+ {
+ if (result?.Text == null) continue;
+ var points = result.ResultPoints;
+ if (points == null || points.Length < 3) continue;
+
+ // 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.
+ 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);
+
+ Point2f[] quad = ExpandQuad(pTL, pTR, pBR, pBL, QuadExpansion);
+
+ seenThisFrame.Add(result.Text);
+ if (Tracked.TryGetValue(result.Text, out var t))
+ {
+ t.Quad = quad;
+ t.MissedFrames = 0;
+ }
+ else
+ {
+ Tracked[result.Text] = new TrackedQr { Quad = quad, MissedFrames = 0 };
+ }
+
+ EnsureImageLoaded(result.Text);
+ }
+
+ // Age out codes that weren't seen this frame; keep them briefly to avoid flicker.
+ List toRemove = null;
+ foreach (var kv in Tracked)
+ {
+ if (seenThisFrame.Contains(kv.Key)) continue;
+ kv.Value.MissedFrames++;
+ if (kv.Value.MissedFrames > MaxMissedFrames)
+ (toRemove ??= new List()).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);
+ }
+ }
+ }
+
+ /// Expand a quad (TL,TR,BR,BL) outward from its centroid by a scale factor.
+ 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) };
+ }
+
+ ///
+ /// 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.
+ ///
+ 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)
+ {
+ if (!gray.IsContinuous())
+ gray = gray.Clone();
+
+ int size = (int)(gray.Total() * gray.ElemSize());
+ if (_grayBuffer == null || _grayBuffer.Length != size)
+ _grayBuffer = new byte[size];
+ Marshal.Copy(gray.Data, _grayBuffer, 0, size);
+
+ // Gray8 skips ZXing's internal RGB->luminance conversion entirely - the fastest
+ // format to hand it, and matches what BGR2GRAY already computed for us in OpenCV.
+ var luminanceSource = new RGBLuminanceSource(_grayBuffer, gray.Width, gray.Height, RGBLuminanceSource.BitmapFormat.Gray8);
+ var bitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
+
+ try
+ {
+ return MultiReader.decodeMultiple(bitmap, Hints) ?? Array.Empty();
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ // ---- 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 _);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file