====== 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);
}