feat: CameraService

Add stable optainitng of raw image from camera
This commit is contained in:
2026-01-15 14:58:11 +03:00
parent 717efe6781
commit ced7e294eb
8 changed files with 467 additions and 90 deletions
@@ -0,0 +1,7 @@
namespace GSS2.Core.Extentions;
static class EnumerableExtentions
{
public static IEnumerable<(int i, T value)> Enumerate<T>(this IEnumerable<T> values) => values.Select((value, i) => (i, value));
}
+2 -1
View File
@@ -18,9 +18,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" />
<PackageReference Include="System.Device.Gpio" Version="4.0.1" /> <PackageReference Include="System.Device.Gpio" Version="4.0.1" />
<PackageReference Include="Iot.Device.Bindings" Version="4.0.1" /> <PackageReference Include="Iot.Device.Bindings" Version="4.0.1" />
<PackageReference Include="LibCameraSharp" Version="0.5.2-20251214" /> <PackageReference Include="LibCameraSharp" Version="0.5.2-20260114" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+266 -74
View File
@@ -2,6 +2,8 @@ using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LibCameraSharp; using LibCameraSharp;
using Stream = LibCameraSharp.Stream; using Stream = LibCameraSharp.Stream;
using System.Buffers;
using GSS2.Core.Extentions;
namespace GSS2.Core.Hardware; namespace GSS2.Core.Hardware;
@@ -28,7 +30,7 @@ public class CameraService : IDisposable
public static CameraServiceConfiguration Default => new CameraServiceConfiguration public static CameraServiceConfiguration Default => new CameraServiceConfiguration
{ {
CameraId = "", CameraId = "",
FrameBufferCount = 5, ViewFinderFrameBufferCount = 5,
ViewFinderWidth = 1920, ViewFinderWidth = 1920,
ViewFinderHeight = 1080, ViewFinderHeight = 1080,
ViewFinderFps = 30, ViewFinderFps = 30,
@@ -37,10 +39,11 @@ public class CameraService : IDisposable
}; };
public string CameraId { get; set; } = ""; public string CameraId { get; set; } = "";
public int FrameBufferCount { get; set; } public int ViewFinderFrameBufferCount { get; set; }
public int ViewFinderWidth { get; set; } public int ViewFinderWidth { get; set; }
public int ViewFinderHeight { get; set; } public int ViewFinderHeight { get; set; }
public int ViewFinderFps { get; set; } public int ViewFinderFps { get; set; }
public int ImageCaptureFrameBufferCount { get; set; }
public int ImageCaptureWidth { get; set; } public int ImageCaptureWidth { get; set; }
public int ImageCaptureHeight { get; set; } public int ImageCaptureHeight { get; set; }
} }
@@ -64,10 +67,13 @@ public class CameraService : IDisposable
private CameraManager? _cameraManager; private CameraManager? _cameraManager;
private Camera? _camera; private Camera? _camera;
private CameraConfiguration? _cameraConfiguration; private CameraConfiguration? _cameraConfiguration;
private FrameBufferAllocator? _frameBufferAllocator; private StreamConfiguration? _viewFinderStreamConfiguration;
private StreamConfiguration? _imageCaptureStreamConfiguration;
private FrameBufferAllocator? _viewFinderFrameBufferAllocator;
private FrameBufferAllocator? _imageCaptureFrameBufferAllocator;
private Stream? _viewFinderStream; private Stream? _viewFinderStream;
private Stream? _imageCaptureStream; private Stream? _imageCaptureStream;
private List<MappedBuffer>? _mappedBuffers; private List<MappedBuffer>? _imageCaptureMappedBuffers;
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
public CameraService(ILogger<CameraService> logger, CameraServiceConfiguration? configuration = null, bool autoInitialize = false) public CameraService(ILogger<CameraService> logger, CameraServiceConfiguration? configuration = null, bool autoInitialize = false)
@@ -87,6 +93,14 @@ public class CameraService : IDisposable
} }
_logger.LogDebug(@$"CameraService use following configuration: _logger.LogDebug(@$"CameraService use following configuration:
CameraId: {_configuration.CameraId}
ViewFinderFrameBufferCount: {_configuration.ViewFinderFrameBufferCount}
ViewFinderWidth: {_configuration.ViewFinderWidth}
ViewFinderHeight: {_configuration.ViewFinderHeight}
ViewFinderFps: {_configuration.ViewFinderFps}
ImageCaptureFrameBufferCount: {_configuration.ImageCaptureFrameBufferCount}
ImageCaptureWidth: {_configuration.ImageCaptureWidth}
ImageCaptureHeight: {_configuration.ImageCaptureHeight}
"); ");
if (autoInitialize) if (autoInitialize)
@@ -115,9 +129,12 @@ public class CameraService : IDisposable
CameraSelect(effectiveCancellationToken); CameraSelect(effectiveCancellationToken);
CameraAcquire(effectiveCancellationToken); CameraAcquire(effectiveCancellationToken);
CameraConfigure(effectiveCancellationToken); CameraConfigure(effectiveCancellationToken);
FrameBuffersAllocate(effectiveCancellationToken); // ViewFinderFrameBuffersAllocate(effectiveCancellationToken);
ImageCaptureFrameBuffersMap(effectiveCancellationToken); ImageCaptureFrameBuffersAllocate(effectiveCancellationToken);
CameraStart(effectiveCancellationToken); CameraStart(effectiveCancellationToken);
// ViewFinderFrameBuffersInitialize(effectiveCancellationToken);
ImageCaptureFrameBuffersInitialize(effectiveCancellationToken);
ImageCaptureFrameBuffersMap(effectiveCancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -189,20 +206,42 @@ public class CameraService : IDisposable
_cameraConfiguration = _camera.GenerateConfiguration( _cameraConfiguration = _camera.GenerateConfiguration(
StreamRoleEnum.Viewfinder, StreamRoleEnum.Viewfinder,
StreamRoleEnum.StillCapture); StreamRoleEnum.Raw);
var viewFinderConfig = _cameraConfiguration.First(); _viewFinderStreamConfiguration = _cameraConfiguration.First();
viewFinderConfig.BufferCount = (uint)_configuration.FrameBufferCount; _viewFinderStreamConfiguration.BufferCount = checked((uint)_configuration.ViewFinderFrameBufferCount);
viewFinderConfig.Size = new Size( _viewFinderStreamConfiguration.Size = new Size(
(uint)_configuration.ViewFinderWidth, checked((uint)_configuration.ViewFinderWidth),
(uint)_configuration.ViewFinderHeight); checked((uint)_configuration.ViewFinderHeight)
);
_imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First();
_imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount);
_imageCaptureStreamConfiguration.Size = new Size(
checked((uint)_configuration.ImageCaptureWidth),
checked((uint)_configuration.ImageCaptureHeight)
);
if (_cameraConfiguration.Validate() == CameraConfiguration.Status.Invalid) if (_cameraConfiguration.Validate() == CameraConfiguration.Status.Invalid)
{ {
_logger.LogCritical("Error while camera configuration validation"); _logger.LogCritical("Error while camera configuration validation");
throw new Exception("Error while camera configuration validation"); throw new Exception("Error while camera configuration validation");
} }
_logger.LogDebug(@$"ViewFinder stream configuration:
Size: {_viewFinderStreamConfiguration.Size.ToString(true)}
FrameSize: {_viewFinderStreamConfiguration.FrameSize}
Stride: {_viewFinderStreamConfiguration.Stride}
ColorSpace: {_viewFinderStreamConfiguration.ColorSpace?.ToString(true)}
PixelFormat: {_viewFinderStreamConfiguration.PixelFormat?.ToString(true)}");
_logger.LogDebug(@$"ImageCapture stream configuration:
Size: {_imageCaptureStreamConfiguration.Size.ToString(true)}
FrameSize: {_imageCaptureStreamConfiguration.FrameSize}
Stride: {_imageCaptureStreamConfiguration.Stride}
ColorSpace: {_imageCaptureStreamConfiguration.ColorSpace?.ToString(true)}
PixelFormat: {_imageCaptureStreamConfiguration.PixelFormat?.ToString(true)}");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int result = _camera.Configure(_cameraConfiguration); int result = _camera.Configure(_cameraConfiguration);
@@ -212,8 +251,8 @@ public class CameraService : IDisposable
throw new Exception($"Error while camera configuration {result}"); throw new Exception($"Error while camera configuration {result}");
} }
_viewFinderStream = _cameraConfiguration.First().Stream(); _viewFinderStream = _viewFinderStreamConfiguration.Stream();
_imageCaptureStream = _cameraConfiguration.Skip(1).First().Stream(); _imageCaptureStream = _imageCaptureStreamConfiguration.Stream();
} }
private void CameraStart(CancellationToken cancellationToken) private void CameraStart(CancellationToken cancellationToken)
{ {
@@ -231,22 +270,20 @@ public class CameraService : IDisposable
_state |= State.CameraStarted; _state |= State.CameraStarted;
} }
private void FrameBuffersAllocate(CancellationToken cancellationToken) private void ViewFinderFrameBuffersAllocate(CancellationToken cancellationToken)
{ {
if (_camera is null) if (_camera is null)
throw new InvalidOperationException("Camera not initialized"); throw new InvalidOperationException("Camera not initialized");
if (_viewFinderStream is null) if (_viewFinderStream is null)
throw new InvalidOperationException("ViewFinderStream not initialized"); throw new InvalidOperationException("ViewFinderStream not initialized");
if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_frameBufferAllocator = new FrameBufferAllocator(_camera); _viewFinderFrameBufferAllocator = new FrameBufferAllocator(_camera);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int result = _frameBufferAllocator.Allocate(_viewFinderStream); int result = _viewFinderFrameBufferAllocator.Allocate(_viewFinderStream);
if (result < 0) if (result < 0)
{ {
_logger.LogCritical("Error while view finder frame buffers allocation {Result}", result); _logger.LogCritical("Error while view finder frame buffers allocation {Result}", result);
@@ -254,10 +291,21 @@ public class CameraService : IDisposable
} }
_state |= State.ViewFinderFrameBufferAllocated; _state |= State.ViewFinderFrameBufferAllocated;
}
private void ImageCaptureFrameBuffersAllocate(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
result = _frameBufferAllocator.Allocate(_imageCaptureStream); _imageCaptureFrameBufferAllocator = new FrameBufferAllocator(_camera);
cancellationToken.ThrowIfCancellationRequested();
int result = _imageCaptureFrameBufferAllocator.Allocate(_imageCaptureStream);
if (result < 0) if (result < 0)
{ {
_logger.LogCritical("Error while image capture frame buffers allocation {Result}", result); _logger.LogCritical("Error while image capture frame buffers allocation {Result}", result);
@@ -266,47 +314,137 @@ public class CameraService : IDisposable
_state |= State.ImageCaptureFrameBufferAllocated; _state |= State.ImageCaptureFrameBufferAllocated;
} }
private void ViewFinderFrameBuffersInitialize(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_viewFinderStream is null)
throw new InvalidOperationException("ViewFinderStream not initialized");
if (_viewFinderFrameBufferAllocator is null)
throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized");
cancellationToken.ThrowIfCancellationRequested();
var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream);
_logger.LogDebug("View finder frame buffers initialization");
foreach (var buffer in buffers)
{
cancellationToken.ThrowIfCancellationRequested();
var request = _camera.CreateRequest(checked((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
var result = request.AddBuffer(_viewFinderStream, buffer);
if (result < 0)
throw new Exception("Error while request create");
var cts = new TaskCompletionSource();
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
{
if (e.Request.Handle == request.Handle)
cts.SetResult();
}
_camera.RequestCompleted += RequestCompleted;
result = _camera.QueueRequest(request);
if (result < 0)
{
_camera.RequestCompleted -= RequestCompleted;
throw new Exception("Error while request queue");
}
cts.Task.WaitAsync(cancellationToken).GetAwaiter().GetResult();
_camera.RequestCompleted -= RequestCompleted;
_logger.LogDebug("Request complited: \n\tResult: {} \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
}
}
private void ImageCaptureFrameBuffersInitialize(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized");
if (_imageCaptureFrameBufferAllocator is null)
throw new InvalidOperationException("ImageCaptureFrameBufferAllocator not initialized");
cancellationToken.ThrowIfCancellationRequested();
var buffers = _imageCaptureFrameBufferAllocator.Buffers(_imageCaptureStream);
_logger.LogDebug("Image capture frame buffers initialization");
foreach (var buffer in buffers)
{
cancellationToken.ThrowIfCancellationRequested();
var request = _camera.CreateRequest(checked((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
var result = request.AddBuffer(_imageCaptureStream, buffer);
if (result < 0)
throw new Exception("Error while request create");
var cts = new TaskCompletionSource();
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
{
if (e.Request.Handle == request.Handle)
cts.SetResult();
}
_camera.RequestCompleted += RequestCompleted;
result = _camera.QueueRequest(request);
if (result < 0)
{
_camera.RequestCompleted -= RequestCompleted;
throw new Exception("Error while request queue");
}
cts.Task.WaitAsync(cancellationToken).GetAwaiter().GetResult();
_camera.RequestCompleted -= RequestCompleted;
_logger.LogDebug("Request complited: \n\tResult: {} \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
}
}
private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken) private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken)
{ {
if (_imageCaptureStream is null) if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized"); throw new InvalidOperationException("ImageCaptureStream not initialized");
if (_frameBufferAllocator is null) if (_imageCaptureFrameBufferAllocator is null)
throw new InvalidOperationException("FrameBufferAllocator not initialized"); throw new InvalidOperationException("ImageCaptureFrameBufferAllocator not initialized");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_mappedBuffers = new List<MappedBuffer>(); _imageCaptureMappedBuffers = new List<MappedBuffer>();
var frameBuffers = _frameBufferAllocator.Buffers(_imageCaptureStream); var frameBuffers = _imageCaptureFrameBufferAllocator.Buffers(_imageCaptureStream);
foreach (var buffer in frameBuffers) foreach (var buffer in frameBuffers)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var planesByFd = buffer.Planes.GroupBy(p => p.Fd); var planesByFd = buffer.Planes.GroupBy(p => p.Fd.Get());
foreach (var fdGroup in planesByFd) foreach (var fdGroup in planesByFd)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int fd = fdGroup.Key.Get(); int fd = fdGroup.Key;
ulong minOffset = fdGroup.Min(p => p.Offset); ulong minOffset = fdGroup.Min(p => p.Offset);
ulong maxEnd = fdGroup.Max(p => p.Offset + p.Length); ulong maxOffset = fdGroup.Max(p => p.Offset + p.Length);
ulong pageSize = (ulong)Environment.SystemPageSize; ulong pageSize = (ulong)Environment.SystemPageSize;
ulong alignedOffset = minOffset & ~(pageSize - 1); ulong alignedOffset = minOffset & ~(pageSize - 1);
ulong alignedLength = maxEnd - alignedOffset; ulong alignedLength = (maxOffset - alignedOffset + pageSize - 1) & ~(pageSize - 1);
IntPtr address = LibC.mmap(IntPtr.Zero, alignedLength, LibC.PROT_READ, LibC.MAP_SHARED, fd, alignedOffset); IntPtr address = LibC.mmap(IntPtr.Zero, alignedLength, LibC.PROT_READ, LibC.MAP_SHARED, fd, alignedOffset);
if (address == new IntPtr(-1)) if (address == new IntPtr(-1))
{ {
int error = Marshal.GetLastWin32Error(); int error = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"Error while buffers mapping, mmap failed for fd={fd}, error={error}"); throw new InvalidOperationException($"Error while buffers mapping, mmap failed for fd=0x{fd:X}, error={error}");
} }
_mappedBuffers.Add(new MappedBuffer _logger.LogDebug("Mapped buffer fd=0x{:X}, address=0x{:X}, length=0x{:X}", fd, address, alignedLength);
_imageCaptureMappedBuffers.Add(new MappedBuffer
{ {
Fd = fd, Fd = fd,
Address = address, Address = address,
@@ -325,10 +463,12 @@ public class CameraService : IDisposable
throw new InvalidOperationException("Camera not initialized"); throw new InvalidOperationException("Camera not initialized");
if (_imageCaptureStream is null) if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized"); throw new InvalidOperationException("ImageCaptureStream not initialized");
if (_frameBufferAllocator is null) if (_imageCaptureStreamConfiguration is null)
throw new InvalidOperationException("FrameBufferAllocator not initialized"); throw new InvalidOperationException("ImageCaptureStreamConfiguration not initialized");
if (_mappedBuffers is null) if (_imageCaptureFrameBufferAllocator is null)
throw new InvalidOperationException("Buffers not mapped"); throw new InvalidOperationException("ImageCaptureFrameBufferAllocator not initialized");
if (_imageCaptureMappedBuffers is null)
throw new InvalidOperationException("ImageCaptureMappedBuffers not mapped");
var effectiveCancellationToken = CancellationTokenSource var effectiveCancellationToken = CancellationTokenSource
.CreateLinkedTokenSource(_disposeCts.Token, cancellationToken) .CreateLinkedTokenSource(_disposeCts.Token, cancellationToken)
@@ -336,15 +476,16 @@ public class CameraService : IDisposable
OpenCvSharp.Mat? image = null; OpenCvSharp.Mat? image = null;
var fdsData = new Dictionary<int, byte[]>();
var planesData = new List<byte[]>();
try try
{ {
effectiveCancellationToken.ThrowIfCancellationRequested(); effectiveCancellationToken.ThrowIfCancellationRequested();
var buffers = _frameBufferAllocator.Buffers(_imageCaptureStream);
var buffer = buffers.Where(b => b.Metadata.Status == FrameMetadata.StatusEnum.FrameStartup || b.Metadata.Status == FrameMetadata.StatusEnum.FrameSuccess).First();
var request = _camera.CreateRequest(checked((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())); var request = _camera.CreateRequest(checked((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
var buffers = _imageCaptureFrameBufferAllocator.Buffers(_imageCaptureStream);
var buffer = buffers.First();
var result = request.AddBuffer(_imageCaptureStream, buffer); var result = request.AddBuffer(_imageCaptureStream, buffer);
if (result < 0) if (result < 0)
throw new Exception("Error while request create"); throw new Exception("Error while request create");
@@ -353,42 +494,85 @@ public class CameraService : IDisposable
var cts = new TaskCompletionSource(); var cts = new TaskCompletionSource();
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e) void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
{ {
if (e.Request.Handle == request.Handle) if (e.Request.Handle != request.Handle)
cts.SetResult(); return;
};
var bufferStatus = request.FindBuffer(_imageCaptureStream).Metadata.Status;
_logger.LogDebug("Request buffers status: {}", bufferStatus);
if (bufferStatus == FrameMetadata.StatusEnum.FrameError || bufferStatus == FrameMetadata.StatusEnum.FrameStartup)
{
request.Reuse(Request.ReuseFlagEnum.ReuseBuffers);
result = _camera.QueueRequest(request);
if (result >= 0)
return;
}
cts.SetResult();
}
_camera.RequestCompleted += RequestCompleted; _camera.RequestCompleted += RequestCompleted;
result = _camera.QueueRequest(request);
if (result < 0)
{
_camera.RequestCompleted -= RequestCompleted;
throw new Exception("Error while request queue");
}
await cts.Task.WaitAsync(effectiveCancellationToken); await cts.Task.WaitAsync(effectiveCancellationToken);
_camera.RequestCompleted -= RequestCompleted; _camera.RequestCompleted -= RequestCompleted;
var planesData = new List<Memory<byte>>(); buffer = request.FindBuffer(_imageCaptureStream);
_logger.LogDebug("Request complited: \n\tResult: {} \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
_logger.LogDebug("Request buffer planes: ");
foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate())
_logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed);
foreach (var (i, plane) in buffer.Planes.Enumerate())
_logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset);
effectiveCancellationToken.ThrowIfCancellationRequested();
foreach (var plane in buffer.Planes) foreach (var plane in buffer.Planes)
{ {
effectiveCancellationToken.ThrowIfCancellationRequested(); var fd = plane.Fd.Get();
if (fdsData.ContainsKey(fd))
continue;
var mappedBuffer = _imageCaptureMappedBuffers.Find(b => b.Fd == fd);
if (mappedBuffer is null)
continue;
var fdData = new byte[mappedBuffer.Length];
Marshal.Copy(mappedBuffer.Address, fdData, 0, checked((int)mappedBuffer.Length));
fdsData.Add(fd, fdData);
}
var pointer = GetPlanePointer(plane); foreach (var plane in buffer.Planes)
var data = new byte[plane.Length]; {
Marshal.Copy(pointer, data, 0, checked((int)plane.Length)); var fd = plane.Fd.Get();
planesData.Add(new Memory<byte>(data)); if (!fdsData.ContainsKey(fd))
continue;
var fdData = fdsData[fd];
var planeData = fdData
.Skip(checked((int)plane.Offset))
.Take(checked((int)plane.Length))
.ToArray();
planesData.Add(planeData);
} }
effectiveCancellationToken.ThrowIfCancellationRequested(); effectiveCancellationToken.ThrowIfCancellationRequested();
var config = _imageCaptureStream.Configuration(); var config = _imageCaptureStreamConfiguration;
var size = config.Size; var size = config.Size;
var fourcc = config.PixelFormat.Fourcc; var fourcc = config.PixelFormat.Fourcc;
var modifier = config.PixelFormat.Modifier; var modifier = config.PixelFormat.Modifier;
var primariesEnum = config.ColorSpace!.Primaries; var primariesEnum = config.ColorSpace?.Primaries;
var transferFunctionEnum = config.ColorSpace.TransferFunction; var transferFunctionEnum = config.ColorSpace?.TransferFunction;
var ycbcrEncodingEnum = config.ColorSpace.YcbcrEncoding; var ycbcrEncodingEnum = config.ColorSpace?.YcbcrEncoding;
var rangeEnum = config.ColorSpace.Range; var rangeEnum = config.ColorSpace?.Range;
var stride = config.Stride; var stride = config.Stride;
effectiveCancellationToken.ThrowIfCancellationRequested(); effectiveCancellationToken.ThrowIfCancellationRequested();
var decoder = FrameDecoderHelper.FindDecoder(fourcc, modifier, primariesEnum, transferFunctionEnum, ycbcrEncodingEnum, rangeEnum); var decoder = FrameDecoderHelper.FindDecoder(fourcc, modifier, primariesEnum, transferFunctionEnum, ycbcrEncodingEnum, rangeEnum);
if (decoder is null) if (decoder is null)
throw new Exception($"Cannot find decoder for fourcc={fourcc} modifier={modifier} primariesEnum={primariesEnum} transferFunctionEnum={transferFunctionEnum} ycbcrEncodingEnum={ycbcrEncodingEnum} rangeEnum={rangeEnum}"); throw new Exception($"Cannot find decoder for fourcc=0x{fourcc:X}({new string(BitConverter.GetBytes(fourcc).Select(b => (char)b).ToArray())}) modifier=0x{modifier:X} primariesEnum={primariesEnum} transferFunctionEnum={transferFunctionEnum} ycbcrEncodingEnum={ycbcrEncodingEnum} rangeEnum={rangeEnum}");
effectiveCancellationToken.ThrowIfCancellationRequested(); effectiveCancellationToken.ThrowIfCancellationRequested();
@@ -396,9 +580,9 @@ public class CameraService : IDisposable
{ {
image = decoder.Decode(checked((int)size.Width), checked((int)size.Height), checked((int)stride), planesData); image = decoder.Decode(checked((int)size.Width), checked((int)size.Height), checked((int)stride), planesData);
} }
catch (Exception e) catch (Exception ex)
{ {
_logger.LogError(e, "Error while image decode"); _logger.LogError(ex, "Error while image decode");
throw; throw;
} }
} }
@@ -406,17 +590,19 @@ public class CameraService : IDisposable
{ {
_logger.LogError(e, "Error while image capture"); _logger.LogError(e, "Error while image capture");
} }
finally
{
foreach (var key in fdsData.Keys)
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
fdsData[key] = null;
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
for (int i = 0; i < planesData.Count(); i++)
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
planesData[i] = null;
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
}
return image; return image;
} }
private IntPtr GetPlanePointer(FrameBuffer.Plane plane)
{
if (_mappedBuffers is null)
throw new InvalidOperationException("Buffers not mapped");
var mapped = _mappedBuffers.First(m => m.Fd == plane.Fd.Get());
ulong offsetInMapping = plane.Offset - mapped.AlignedOffset;
return IntPtr.Add(mapped.Address, checked((int)offsetInMapping));
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
@@ -435,20 +621,24 @@ public class CameraService : IDisposable
Dispose(disposing: true); Dispose(disposing: true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private void Cleanup() private void Cleanup()
{ {
try try
{ {
if (_state.HasFlag(State.ImageCaptureFrameBuffersMapped) && _mappedBuffers is not null) if (_state.HasFlag(State.ImageCaptureFrameBuffersMapped) && _imageCaptureMappedBuffers is not null)
{ {
foreach (var buffer in _mappedBuffers) foreach (var buffer in _imageCaptureMappedBuffers)
{
_logger.LogDebug("MUNMAP address=0x{:X}, length=0x{:X}", buffer.Address, buffer.Length);
LibC.munmap(buffer.Address, buffer.Length); LibC.munmap(buffer.Address, buffer.Length);
_mappedBuffers.Clear(); }
_imageCaptureMappedBuffers.Clear();
} }
if (_state.HasFlag(State.ViewFinderFrameBufferAllocated) && _viewFinderStream is not null) if (_state.HasFlag(State.ViewFinderFrameBufferAllocated) && _viewFinderStream is not null)
_frameBufferAllocator?.Free(_viewFinderStream); _viewFinderFrameBufferAllocator?.Free(_viewFinderStream);
if (_state.HasFlag(State.ImageCaptureFrameBufferAllocated) && _imageCaptureStream is not null) if (_state.HasFlag(State.ImageCaptureFrameBufferAllocated) && _imageCaptureStream is not null)
_frameBufferAllocator?.Free(_imageCaptureStream); _imageCaptureFrameBufferAllocator?.Free(_imageCaptureStream);
if (_state.HasFlag(State.CameraStarted)) if (_state.HasFlag(State.CameraStarted))
_camera?.Stop(); _camera?.Stop();
if (_state.HasFlag(State.CameraAcquired)) if (_state.HasFlag(State.CameraAcquired))
@@ -456,9 +646,10 @@ public class CameraService : IDisposable
if (_state.HasFlag(State.CameraManagerStarted)) if (_state.HasFlag(State.CameraManagerStarted))
_cameraManager?.Stop(); _cameraManager?.Stop();
_viewFinderStream?.Dispose(); // _viewFinderStream?.Dispose();
_imageCaptureStream?.Dispose(); // _imageCaptureStream?.Dispose();
_frameBufferAllocator?.Dispose(); // _viewFinderFrameBufferAllocator?.Dispose();
// _imageCaptureFrameBufferAllocator?.Dispose();
_cameraManager?.Dispose(); _cameraManager?.Dispose();
} }
catch (Exception e) catch (Exception e)
@@ -469,10 +660,11 @@ public class CameraService : IDisposable
{ {
_cameraManager = null; _cameraManager = null;
_camera = null; _camera = null;
_frameBufferAllocator = null; _viewFinderFrameBufferAllocator = null;
_imageCaptureFrameBufferAllocator = null;
_viewFinderStream = null; _viewFinderStream = null;
_imageCaptureStream = null; _imageCaptureStream = null;
_mappedBuffers = null; _imageCaptureMappedBuffers = null;
_state = State.NotInitialized; _state = State.NotInitialized;
} }
} }
@@ -0,0 +1,152 @@
using System.Buffers.Binary;
using OpenCvSharp;
using LibCameraSharp;
namespace GSS2.FrameDecoders;
public class BGGR_PISP_COMP1_RAW_FrameDecoder : IFrameDecoder // BGGR_PISP_COMP1/RAW
{
// Не знаю как оно работает, но работает
static class PispDecompressor
{
const int COMPRESS_OFFSET = 2048;
public static void DecompressPisp(ReadOnlySpan<byte> src, int width, int height, int stride, Span<ushort> dst)
{
int paddedWidth = (width + 7) & ~7;
for (int y = 0; y < height; y++)
{
int srcRow = y * stride;
int dstRow = y * paddedWidth;
int sp = srcRow;
int dp = dstRow;
for (int x = 0; x < paddedWidth; x += 8)
{
uint w0 = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(sp, 4));
uint w1 = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(sp + 4, 4));
sp += 8;
SubBlock(dst, dp, w0); // even pixels
SubBlock(dst, dp + 1, w1); // odd pixels
for (int i = 0; i < 8; i++)
{
dst[dp + i] = Postprocess(dst[dp + i]);
}
dp += 8;
}
}
}
static ushort Postprocess(ushort a)
{
int v = a + COMPRESS_OFFSET;
return (ushort)Math.Min(0xFFFF, v);
}
static void SubBlock(Span<ushort> d, int baseIndex, uint w)
{
int qmode = (int)(w & 3);
int[] q = new int[4];
if (qmode < 3)
{
int field0 = (int)((w >> 2) & 511);
int field1 = (int)((w >> 11) & 127);
int field2 = (int)((w >> 18) & 127);
int field3 = (int)((w >> 25) & 127);
if (qmode == 2 && field0 >= 384)
{
q[1] = field0;
q[2] = field1 + 384;
}
else
{
q[1] = (field1 >= 64) ? field0 : field0 + 64 - field1;
q[2] = (field1 >= 64) ? field0 + field1 - 64 : field0;
}
int p1 = Math.Max(0, q[1] - 64);
int p2 = Math.Max(0, q[2] - 64);
if (qmode == 2)
{
p1 = Math.Min(384, p1);
p2 = Math.Min(384, p2);
}
q[0] = p1 + field2;
q[3] = p2 + field3;
}
else
{
int pack0 = (int)((w >> 2) & 32767);
int pack1 = (int)((w >> 17) & 32767);
q[0] = (pack0 & 15) + 16 * ((pack0 >> 8) / 11);
q[1] = (pack0 >> 4) % 176;
q[2] = (pack1 & 15) + 16 * ((pack1 >> 8) / 11);
q[3] = (pack1 >> 4) % 176;
}
d[baseIndex + 0] = Dequantize(q[0], qmode);
d[baseIndex + 2] = Dequantize(q[1], qmode);
d[baseIndex + 4] = Dequantize(q[2], qmode);
d[baseIndex + 6] = Dequantize(q[3], qmode);
}
static ushort Dequantize(int q, int qmode)
{
int v = qmode switch
{
0 => (q < 320) ? 16 * q : 32 * (q - 160),
1 => 64 * q,
2 => 128 * q,
_ => (q < 94) ? 256 * q : Math.Min(0xFFFF, 512 * (q - 47))
};
return (ushort)Math.Min(0xFFFF, v);
}
}
public uint Fourcc => 0x32525942; // BYR2
// Модификатор 0xC00000000000001 в соответствии с libcamera является PISP_FORMAT_MOD_COMPRESS_MODE1
// https://github.com/raspberrypi/libcamera/blob/f0e40f1c50bd0afe65727d6e407d0dcb42666ada/include/linux/drm_fourcc.h#L1692
// Режимы компрессии Raspberry Pi PiSP описаны в документации ядра linux:
// 2.6.1.3.1. Raspberry Pi PiSP compressed 8-bit Bayer formats
// https://docs.kernel.org/userspace-api/media/v4l/pixfmt-srggb8-pisp-comp.html
// Пример реализации декодера приведён в репозитории https://github.com/raspberrypi/rpicam-apps
// https://github.com/raspberrypi/rpicam-apps/blob/d821489c2ec89f541915393a823555632653a58f/image/dng.cpp
public ulong Modifier => 0xC00000000000001; // PISP_FORMAT_MOD_COMPRESS_MODE1
public ColorSpace.PrimariesEnum PrimariesEnum => ColorSpace.PrimariesEnum.Raw;
public ColorSpace.TransferFunctionEnum TransferFunctionEnum => ColorSpace.TransferFunctionEnum.Linear;
public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum => ColorSpace.YcbcrEncodingEnum.None;
public ColorSpace.RangeEnum RangeEnum => ColorSpace.RangeEnum.Full;
public Mat Decode(int width, int height, int stride, List<byte[]> planes)
{
// Дектоирование этого формата состоит из двух этапов:
// 1) Распаковка исходных данных в которых каждые 8 байт представляют 8 пикселей (это не значит что 1 байт представляет 1 пиксель!)
// 2) Демозаика полученого Bayer изображения в RGB (BGR)
if (planes.Count != 1)
throw new ArgumentException($"Single plane expected, got {planes.Count}");
var source = planes[0];
var unpacked = new ushort[width * height];
PispDecompressor.DecompressPisp(source, width, height, stride, unpacked);
using var bayer = Mat.FromPixelData(height, width, MatType.CV_16UC1, unpacked);
var bgr = new Mat();
Cv2.CvtColor(bayer, bgr, ColorConversionCodes.BayerRG2BGR);
return bgr;
}
}
+4 -4
View File
@@ -47,10 +47,10 @@ public static class FrameDecoderHelper
public static IFrameDecoder? FindDecoder( public static IFrameDecoder? FindDecoder(
uint fourcc, uint fourcc,
ulong modifier, ulong modifier,
ColorSpace.PrimariesEnum primariesEnum, ColorSpace.PrimariesEnum? primariesEnum,
ColorSpace.TransferFunctionEnum transferFunctionEnum, ColorSpace.TransferFunctionEnum? transferFunctionEnum,
ColorSpace.YcbcrEncodingEnum ycbcrEncodingEnum, ColorSpace.YcbcrEncodingEnum? ycbcrEncodingEnum,
ColorSpace.RangeEnum rangeEnum) ColorSpace.RangeEnum? rangeEnum)
{ {
return _decoders.FirstOrDefault(d => return _decoders.FirstOrDefault(d =>
d.Fourcc == fourcc && d.Fourcc == fourcc &&
+1 -1
View File
@@ -13,5 +13,5 @@ public interface IFrameDecoder
public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum { get; } public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum { get; }
public ColorSpace.RangeEnum RangeEnum { get; } public ColorSpace.RangeEnum RangeEnum { get; }
public Mat Decode(int width, int height, int stride, List<Memory<byte>> planes); public Mat Decode(int width, int height, int stride, List<byte[]> planes);
} }
+29 -7
View File
@@ -6,21 +6,43 @@ namespace GSS2.Test;
public class Worker( public class Worker(
ILogger<Worker> logger, ILogger<Worker> logger,
AmineContentCalibrationContext calibrationContext, IHostApplicationLifetime applicationLifetime,
LightsService lightsService AmineContentCalibrationContext amineCalibrationContext,
AmineContentResultsContext amineResultsContext,
LightsService lightsService,
IlluminatorService illuminatorService,
CameraService cameraService
) : BackgroundService ) : BackgroundService
{ {
private readonly ILogger<Worker> _logger = logger; private readonly ILogger<Worker> _logger = logger;
private readonly AmineContentCalibrationContext _calibrationContext = calibrationContext; private readonly IHostApplicationLifetime _applicationLifetime = applicationLifetime;
private readonly AmineContentCalibrationContext _amineCalibrationContext = amineCalibrationContext;
private readonly AmineContentResultsContext _amineResultsContext = amineResultsContext;
private readonly LightsService _lightsService = lightsService; private readonly LightsService _lightsService = lightsService;
private readonly IlluminatorService _illuminatorService = illuminatorService;
private readonly CameraService _cameraService = cameraService;
protected override async Task ExecuteAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
await _cameraService.InitializeAsync(cancellationToken);
_ = _lightsService.Run(cancellationToken); _ = _lightsService.Run(cancellationToken);
_ = _lightsService.SnowfallAsync(0.3, 1, Color.Aqua, cancellationToken); _ = _lightsService.SnowfallAsync(0.3, 1, Color.Aqua, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{ _illuminatorService.SetIntensity(0.5, 0, 0);
await Task.Delay(5000); _illuminatorService.TurnOn();
}
await Task.Delay(1000);
var img = await _cameraService.CaptureImage(cancellationToken);
img?.SaveImage("IMAGE.png");
await Task.Delay(1000);
_illuminatorService.TurnOff();
await _lightsService.DisconnectAsync(cancellationToken);
applicationLifetime.StopApplication();
} }
} }
+6 -3
View File
@@ -6,8 +6,8 @@
"Microsoft.EntityFrameworkCore.Migrations": "Warning", "Microsoft.EntityFrameworkCore.Migrations": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"GSS2.Core.Hardware.LightsService": "Information", "GSS2.Core.Hardware.LightsService": "Information",
"GSS2.Core.Hardware.IlluminatorService":"Information", "GSS2.Core.Hardware.IlluminatorService": "Information",
"GSS2.Core.Hardware.CameraService":"Information", "GSS2.Core.Hardware.CameraService": "Debug",
"LibCamera": "Information", "LibCamera": "Information",
"LibCamera-Camera": "Information", "LibCamera-Camera": "Information",
"LibCamera-RPI": "Information", "LibCamera-RPI": "Information",
@@ -35,11 +35,14 @@
"MaxUv365PwmDuty": 1.0 "MaxUv365PwmDuty": 1.0
}, },
"Camera": { "Camera": {
"CameraId": "",
"ViewFinderFrameBufferCount": 10,
"ViewFinderWidth": 1920, "ViewFinderWidth": 1920,
"ViewFinderHeight": 1080, "ViewFinderHeight": 1080,
"ViewFinderFps": 30, "ViewFinderFps": 30,
"ImageCaptureFrameBufferCount": 2,
"ImageCaptureWidth": 4056, "ImageCaptureWidth": 4056,
"ImageCaptureHeight": 3080 "ImageCaptureHeight": 3040
}, },
"TemperatureHumidity": { "TemperatureHumidity": {
"Bus": 6, "Bus": 6,