====== Connecting a Camera to the jPC ====== The jPC is equipped with a CSI port for solutions that may require computer vision. The jPC administration program demonstrates one such use case with a QR code reader. The jPC's CSI port is configured by default for the imx708 image sensor and tested with the [[https://www.raspberrypi.com/products/camera-module-3/|Raspberry Pi Camera 3 module]]. Other camera sensors are possible, but have not yet been tested. **CAREFULLY** connect the camera module to the CSI port. ===== Test with GStreamer ===== Use [[https://gstreamer.freedesktop.org/|GStreamer]] to obtain a camera feed for your applications. The following GStreamer command will show a real-time camera feed on the local display. gst-launch-1.0 libcamerasrc ! video/x-raw,width=800,height=480,framerate=30/1,format=NV12 \ ! videoconvert ! queue ! fpsdisplaysink video-sink=waylandsink ===== QR Code Scanning Demo ===== The following code uses the [[https://github.com/mchehab/zbar|ZBar]] library to scan camera frames produced by [[https://gstreamer.freedesktop.org/|GStreamer]] for QR codes. The GUI is created using [[https://gircore.github.io/|GirCore]]. {{ https://downloads.comfiletech.com/jPC/videos/jpc-qrcode-demo.mp4?476x424 }} #:package GirCore.GLib-2.0@0.6.3 #:package GirCore.GdkPixbuf-2.0@0.6.3 #:package GirCore.Gtk-4.0@0.6.3 using GdkPixbuf; using Gio; using Gtk; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Channels; using Task = System.Threading.Tasks.Task; public class Program { const int Width = 480; const int Height = 320; const int FrameSize = Width * Height * 3; static IntPtr _scanner; static Channel _previewChannel = Channel.CreateUnbounded(); static Channel _scanChannel = Channel.CreateUnbounded(); public static int Main(string[] args) { var app = Gtk.Application.New("org.example.QrDemo", ApplicationFlags.FlagsNone); app.OnActivate += (gioApp, _) => CreateGUI((Gtk.Application)gioApp); var status = app.RunWithSynchronizationContext(args); return status; } static void CreateGUI(Gtk.Application app) { var window = ApplicationWindow.New(app); window.Title = "GTK + ZBar QR Demo"; window.SetDefaultSize(Width, Height + 80); window.SetResizable(false); var vbox = Box.New(Orientation.Vertical, 4); var picture = Picture.New(); picture.SetSizeRequest(Width, Height); picture.SetHalign(Align.Center); var label = Label.New("Waiting for QR..."); vbox.Append(picture); vbox.Append(label); window.Child = vbox; window.Present(); _scanner = zbar_image_scanner_create(); StreamFramesAndScan(picture, label); } private static void StreamFramesAndScan(Picture picture, Label label) { var psi = new ProcessStartInfo { FileName = "gst-launch-1.0", Arguments = "libcamerasrc ! " + $"video/x-raw,width={Width},height={Height},framerate=30/1,format=NV12 ! " + "videoconvert ! " + $"video/x-raw,format=RGB ! " + "multipartmux boundary=frame ! " + "fdsink fd=1", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, CreateNoWindow = true }; if (Process.Start(psi) is not Process process) { GLib.Functions.IdleAdd(0, () => { label.SetText("Failed to start GStreamer"); return false; }); return; } _previewChannel = Channel.CreateUnbounded(); _scanChannel = Channel.CreateUnbounded(); var stream = process.StandardOutput.BaseStream; var cts = new CancellationTokenSource(); ParseFrames(stream, process, label, cts); GeneratePreview(picture, cts); ScanForQRCode(label, cts); } static void ParseFrames(Stream stream, Process process, Label label, CancellationTokenSource cts) { Task.Run(async () => { try { var previewWriter = _previewChannel.Writer; var scanWriter = _scanChannel.Writer; while (true) { // Skip to frame boundary "\r\n\r\n" while (stream.ReadByte() != '\r') { } if (stream.ReadByte() != '\n' || stream.ReadByte() != '\r' || stream.ReadByte() != '\n') { continue; } var frame = new byte[FrameSize]; stream.ReadExactly(frame); if (frame.Length == FrameSize) { await previewWriter.WriteAsync(frame, cts.Token); await scanWriter.WriteAsync(frame, cts.Token); } } } finally { cts.Cancel(); _previewChannel.Writer.TryComplete(); _scanChannel.Writer.TryComplete(); } }); } static void GeneratePreview(Picture picture, CancellationTokenSource cts) { Task.Run(async () => { try { byte[]? lastFrame = null; await foreach (var frame in _previewChannel.Reader.ReadAllAsync(cts.Token)) { if (ReferenceEquals(frame, lastFrame)) continue; lastFrame = frame; GLib.Functions.IdleAdd(0, () => { try { var pix = Pixbuf.NewFromBytes(GLib.Bytes.New(frame), Colorspace.Rgb, false, 8, Width, Height, Width * 3); picture.SetPixbuf(pix); } catch { } return false; }); } } catch (OperationCanceledException) { } }); } static void ScanForQRCode(Label label, CancellationTokenSource cts) { Task.Run(async () => { try { byte[]? lastFrame = null; await foreach (var frame in _scanChannel.Reader.ReadAllAsync(cts.Token)) { if (ReferenceEquals(frame, lastFrame)) continue; lastFrame = frame; var text = ScanQrFromRgb(frame); if (!string.IsNullOrEmpty(text)) GLib.Functions.IdleAdd(0, () => { label.SetText($"QR Code: {text}"); return false; }); } } catch (OperationCanceledException) { } }); } readonly static uint FourCC = 'Y' | ((uint)'8' << 8) | ((uint)'0' << 16) | ((uint)'0' << 24); private static string? ScanQrFromRgb(ReadOnlySpan rgb) { // Convert RGB to Grayscale var gray = new byte[Width * Height]; for (int i = 0; i < rgb.Length; i += 3) { int y = (int)((rgb[i] * 0.299) + (rgb[i + 1] * 0.587) + (rgb[i + 2] * 0.114)); gray[i / 3] = (byte)(y < 0 ? 0 : y > 255 ? 255 : y); } IntPtr image = zbar_image_create(); zbar_image_set_format(image, FourCC); zbar_image_set_size(image, Width, Height); IntPtr dataPtr = Marshal.AllocHGlobal(gray.Length); try { Marshal.Copy(gray, 0, dataPtr, gray.Length); zbar_image_set_data(image, dataPtr, (UIntPtr)gray.Length, IntPtr.Zero); if (zbar_scan_image(_scanner, image) <= 0) { zbar_image_destroy(image); return null; } IntPtr symbol = zbar_image_first_symbol(image); string? result = symbol != IntPtr.Zero ? Marshal.PtrToStringAnsi(zbar_symbol_get_data(symbol)) : null; zbar_image_destroy(image); return result; } finally { Marshal.FreeHGlobal(dataPtr); } } const string ZBarLib = "libzbar.so.0"; [DllImport(ZBarLib)] private static extern IntPtr zbar_image_scanner_create(); [DllImport(ZBarLib)] private static extern IntPtr zbar_image_create(); [DllImport(ZBarLib)] private static extern void zbar_image_destroy(IntPtr image); [DllImport(ZBarLib)] private static extern void zbar_image_set_format(IntPtr image, uint fourcc); [DllImport(ZBarLib)] private static extern void zbar_image_set_size(IntPtr image, uint width, uint height); [DllImport(ZBarLib)] private static extern void zbar_image_set_data(IntPtr image, IntPtr data, UIntPtr length, IntPtr cleanup); [DllImport(ZBarLib)] private static extern int zbar_scan_image(IntPtr scanner, IntPtr image); [DllImport(ZBarLib)] private static extern IntPtr zbar_image_first_symbol(IntPtr image); [DllImport(ZBarLib)] private static extern IntPtr zbar_symbol_get_data(IntPtr symbol); }