feat: CameraService camera image capture

Add GSS2.FrameDecoders project for image decoding realizations
Add IFrameDecoder interface and FrameDecoderHelper class
Add CameraService.CaptureImage method
This commit is contained in:
2025-12-18 09:26:00 +03:00
parent 650fd03bfe
commit 4de442f51f
6 changed files with 208 additions and 7 deletions
+3 -1
View File
@@ -17,13 +17,15 @@
</PackageReference> </PackageReference>
<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="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-20251028" /> <PackageReference Include="LibCameraSharp" Version="0.5.2-20251214" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj" /> <ProjectReference Include="..\GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj" />
<ProjectReference Include="..\GSS2.FrameDecoders\GSS2.FrameDecoders.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+96 -6
View File
@@ -269,10 +269,10 @@ public class CameraService : IDisposable
} }
private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken) private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken)
{ {
if (_frameBufferAllocator is null)
throw new InvalidOperationException("FrameBufferAllocator 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)
throw new InvalidOperationException("FrameBufferAllocator not initialized");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -304,7 +304,7 @@ public class CameraService : IDisposable
if (address == new IntPtr(-1)) if (address == new IntPtr(-1))
{ {
int error = Marshal.GetLastWin32Error(); int error = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"mmap failed for fd={fd}, error={error}"); throw new InvalidOperationException($"Error while buffers mapping, mmap failed for fd={fd}, error={error}");
} }
_mappedBuffers.Add(new MappedBuffer _mappedBuffers.Add(new MappedBuffer
@@ -320,6 +320,95 @@ public class CameraService : IDisposable
_state |= State.ImageCaptureFrameBuffersMapped; _state |= State.ImageCaptureFrameBuffersMapped;
} }
public async Task<OpenCvSharp.Mat?> CaptureImage(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_imageCaptureStream is null)
throw new InvalidOperationException("ImageCaptureStream not initialized");
if (_frameBufferAllocator is null)
throw new InvalidOperationException("FrameBufferAllocator not initialized");
if (_mappedBuffers is null)
throw new InvalidOperationException("Buffers not mapped");
var effectiveCancellationToken = CancellationTokenSource
.CreateLinkedTokenSource(_disposeCts.Token, cancellationToken)
.Token;
OpenCvSharp.Mat? image = null;
try
{
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 result = request.AddBuffer(_imageCaptureStream, buffer);
if (result < 0)
throw new Exception("Error while request create");
effectiveCancellationToken.ThrowIfCancellationRequested();
var cts = new TaskCompletionSource();
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
{
if (e.Request.Handle == request.Handle)
cts.SetResult();
};
_camera.RequestCompleted += RequestCompleted;
await cts.Task.WaitAsync(effectiveCancellationToken);
_camera.RequestCompleted -= RequestCompleted;
var planesData = new List<Memory<byte>>();
foreach (var plane in buffer.Planes)
{
effectiveCancellationToken.ThrowIfCancellationRequested();
var pointer = GetPlanePointer(plane);
var data = new byte[plane.Length];
Marshal.Copy(pointer, data, 0, checked((int)plane.Length));
planesData.Add(new Memory<byte>(data));
}
effectiveCancellationToken.ThrowIfCancellationRequested();
var config = _imageCaptureStream.Configuration();
var size = config.Size;
var fourcc = config.PixelFormat.Fourcc;
var modifier = config.PixelFormat.Modifier;
var primariesEnum = config.ColorSpace!.Primaries;
var transferFunctionEnum = config.ColorSpace.TransferFunction;
var ycbcrEncodingEnum = config.ColorSpace.YcbcrEncoding;
var rangeEnum = config.ColorSpace.Range;
var stride = config.Stride;
effectiveCancellationToken.ThrowIfCancellationRequested();
var decoder = FrameDecoderHelper.FindDecoder(fourcc, modifier, primariesEnum, transferFunctionEnum, ycbcrEncodingEnum, rangeEnum);
if (decoder is null)
throw new Exception($"Cannot find decoder for fourcc={fourcc} modifier={modifier} primariesEnum={primariesEnum} transferFunctionEnum={transferFunctionEnum} ycbcrEncodingEnum={ycbcrEncodingEnum} rangeEnum={rangeEnum}");
effectiveCancellationToken.ThrowIfCancellationRequested();
try
{
image = decoder.Decode(checked((int)size.Width), checked((int)size.Height), checked((int)stride), planesData);
}
catch (Exception e)
{
_logger.LogError(e, "Error while image decode");
throw;
}
}
catch (Exception e)
{
_logger.LogError(e, "Error while image capture");
}
return image;
}
private IntPtr GetPlanePointer(FrameBuffer.Plane plane) private IntPtr GetPlanePointer(FrameBuffer.Plane plane)
{ {
if (_mappedBuffers is null) if (_mappedBuffers is null)
@@ -373,8 +462,10 @@ public class CameraService : IDisposable
_frameBufferAllocator?.Dispose(); _frameBufferAllocator?.Dispose();
_cameraManager?.Dispose(); _cameraManager?.Dispose();
} }
catch catch (Exception e)
{ } {
_logger.LogError(e, "Error while CameraService cleanup");
}
finally finally
{ {
_cameraManager = null; _cameraManager = null;
@@ -386,5 +477,4 @@ public class CameraService : IDisposable
_state = State.NotInitialized; _state = State.NotInitialized;
} }
} }
} }
+63
View File
@@ -0,0 +1,63 @@
using GSS2.FrameDecoders;
using LibCameraSharp;
public static class FrameDecoderHelper
{
private static readonly IReadOnlyList<IFrameDecoder> _decoders;
static FrameDecoderHelper()
{
_decoders = LoadDecoders();
}
private static IReadOnlyList<IFrameDecoder> LoadDecoders()
{
var decoderInterface = typeof(IFrameDecoder);
var assembly = decoderInterface.Assembly;
var decoders = assembly
.GetTypes()
.Where(t =>
!t.IsAbstract &&
!t.IsInterface &&
decoderInterface.IsAssignableFrom(t))
.Select(CreateDecoderInstance)
.Where(d => d is not null)
.Cast<IFrameDecoder>()
.ToList();
return decoders;
}
private static IFrameDecoder? CreateDecoderInstance(Type type)
{
if (type.GetConstructor(Type.EmptyTypes) is null)
return null;
try
{
return (IFrameDecoder?)Activator.CreateInstance(type);
}
catch
{
return null;
}
}
public static IFrameDecoder? FindDecoder(
uint fourcc,
ulong modifier,
ColorSpace.PrimariesEnum primariesEnum,
ColorSpace.TransferFunctionEnum transferFunctionEnum,
ColorSpace.YcbcrEncodingEnum ycbcrEncodingEnum,
ColorSpace.RangeEnum rangeEnum)
{
return _decoders.FirstOrDefault(d =>
d.Fourcc == fourcc &&
d.Modifier == modifier &&
d.PrimariesEnum == primariesEnum &&
d.TransferFunctionEnum == transferFunctionEnum &&
d.YcbcrEncodingEnum == ycbcrEncodingEnum &&
d.RangeEnum == rangeEnum);
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="7.1.1" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="LibCameraSharp" Version="0.5.2-20251028" />
</ItemGroup>
</Project>
+17
View File
@@ -0,0 +1,17 @@
using LibCameraSharp;
using OpenCvSharp;
namespace GSS2.FrameDecoders;
public interface IFrameDecoder
{
public uint Fourcc { get; }
public ulong Modifier { get; }
public ColorSpace.PrimariesEnum PrimariesEnum { get; }
public ColorSpace.TransferFunctionEnum TransferFunctionEnum { get; }
public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum { get; }
public ColorSpace.RangeEnum RangeEnum { get; }
public Mat Decode(int width, int height, int stride, List<Memory<byte>> planes);
}
+14
View File
@@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSharpClient", "CSharpClien
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.LightsControl.CSharpClient", "GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj", "{D0130B90-6ECA-49CA-A126-EFEFF481FC74}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.LightsControl.CSharpClient", "GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj", "{D0130B90-6ECA-49CA-A126-EFEFF481FC74}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.FrameDecoders\GSS2.FrameDecoders.csproj", "{05085293-E710-41D6-8F61-701ADBA52FA9}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -59,6 +61,18 @@ Global
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x64.Build.0 = Release|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x64.Build.0 = Release|Any CPU
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x86.ActiveCfg = Release|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x86.ActiveCfg = Release|Any CPU
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x86.Build.0 = Release|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Release|x86.Build.0 = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|x64.ActiveCfg = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|x64.Build.0 = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|x86.ActiveCfg = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Debug|x86.Build.0 = Debug|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|Any CPU.Build.0 = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x64.ActiveCfg = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x64.Build.0 = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x86.ActiveCfg = Release|Any CPU
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE