using OpenCvSharp; using System.Diagnostics; using System.Threading.Channels; using VisionBuilder.UI.Common; using VisionBuilder.UI.Common.Processing; namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera { public class LibcameraImageSource : IImageSource, IVisionBuilderModule { private LibcameraCaptureSettings _settings; public LibcameraImageSource(LibcameraCaptureSettings settings) { _settings = settings; } public async Task GetImage(CancellationToken token) { return await _imageChannel.Reader.ReadAsync(token); } public void InitializeModule() { Start(); } byte[] JpegHeader = new byte[] { 0xff, 0xd8 }; byte[] JpegFooter = new byte[] { 0xff, 0xd9 }; int ChunkSize = 1024; Channel _imageChannel = Channel.CreateBounded(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest }); public void Start() { Thread th = new Thread(StartLoop); th.IsBackground = true; th.Start(); } private void StartLoop() { try { var psi = new ProcessStartInfo { FileName = "rpicam-vid", Arguments = _settings.BuildArguments(), RedirectStandardOutput = true, UseShellExecute = false }; using (Process process = Process.Start(psi)) using (BinaryReader br = new BinaryReader(process.StandardOutput.BaseStream)) { byte[] imageBuffer = new byte[1024 * 1024]; // decode MJPEG buffer var buff = br.ReadBytes(ChunkSize); int frameId = 0; while (true) { var imageStart = Find(buff, JpegHeader); if (imageStart != -1) { var size = buff.Length - imageStart; Array.Copy(buff, imageStart, imageBuffer, 0, size); while (true) { buff = br.ReadBytes(ChunkSize); var imageEnd = Find(buff, JpegFooter); if (imageEnd != -1) { var frame = new byte[size + imageEnd]; Array.Copy(imageBuffer, frame, size); Array.Copy(buff, 0, frame, size, imageEnd); // process frame frameId++; var decoded = Mat.ImDecode(frame); _imageChannel.Writer.WriteAsync(decoded, CancellationToken.None); // copy the leftover data to the start Array.Copy(buff, imageEnd, buff, 0, buff.Length - imageEnd); // fill the remainder of the buffer with new data and start over var temp = br.ReadBytes(imageEnd); Array.Copy(temp, 0, buff, buff.Length - imageEnd, temp.Length); break; } // copy all of the data to the imageBuffer Array.Copy(buff, 0, imageBuffer, size, buff.Length); size += buff.Length; } } else { Console.WriteLine("JPEG header not found."); break; } } process.Kill(); } } catch (Exception e) { Serilog.Log.Error($"Error starting libcamera: {e}"); } } public static int Find(byte[] buff, byte[] search) { // enumerate the buffer but don't overstep the bounds for (int start = 0; start < buff.Length - search.Length; start++) { // we found the first character if (buff[start] == search[0]) { int next; // traverse the rest of the bytes for (next = 1; next < search.Length; next++) { // if we don't match, bail if (buff[start + next] != search[next]) break; } if (next == search.Length) return start; } } // not found return -1; } public async Task GetImageAsync(CancellationToken cancellationToken) { return await _imageChannel.Reader.ReadAsync(cancellationToken); } } }