From 4de442f51f19992540a49b54b9eeee647a8d859e Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Thu, 18 Dec 2025 09:26:00 +0300 Subject: [PATCH] feat: CameraService camera image capture Add GSS2.FrameDecoders project for image decoding realizations Add IFrameDecoder interface and FrameDecoderHelper class Add CameraService.CaptureImage method --- GSS2.Core/GSS2.Core.csproj | 4 +- GSS2.Core/Hardware/CameraService.cs | 102 +++++++++++++++++-- GSS2.FrameDecoders/FrameDecoderHelper.cs | 63 ++++++++++++ GSS2.FrameDecoders/GSS2.FrameDecoders.csproj | 15 +++ GSS2.FrameDecoders/IFrameDecoder.cs | 17 ++++ GranuSightSoftware2.sln | 14 +++ 6 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 GSS2.FrameDecoders/FrameDecoderHelper.cs create mode 100644 GSS2.FrameDecoders/GSS2.FrameDecoders.csproj create mode 100644 GSS2.FrameDecoders/IFrameDecoder.cs diff --git a/GSS2.Core/GSS2.Core.csproj b/GSS2.Core/GSS2.Core.csproj index ccefd78..affbaaa 100644 --- a/GSS2.Core/GSS2.Core.csproj +++ b/GSS2.Core/GSS2.Core.csproj @@ -17,13 +17,15 @@ + - + + diff --git a/GSS2.Core/Hardware/CameraService.cs b/GSS2.Core/Hardware/CameraService.cs index 80ebfd7..908eb4b 100644 --- a/GSS2.Core/Hardware/CameraService.cs +++ b/GSS2.Core/Hardware/CameraService.cs @@ -269,10 +269,10 @@ public class CameraService : IDisposable } private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken) { - if (_frameBufferAllocator is null) - throw new InvalidOperationException("FrameBufferAllocator not initialized"); if (_imageCaptureStream is null) throw new InvalidOperationException("ImageCaptureStream not initialized"); + if (_frameBufferAllocator is null) + throw new InvalidOperationException("FrameBufferAllocator not initialized"); cancellationToken.ThrowIfCancellationRequested(); @@ -304,7 +304,7 @@ public class CameraService : IDisposable if (address == new IntPtr(-1)) { 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 @@ -320,6 +320,95 @@ public class CameraService : IDisposable _state |= State.ImageCaptureFrameBuffersMapped; } + public async Task 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>(); + 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(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) { if (_mappedBuffers is null) @@ -373,8 +462,10 @@ public class CameraService : IDisposable _frameBufferAllocator?.Dispose(); _cameraManager?.Dispose(); } - catch - { } + catch (Exception e) + { + _logger.LogError(e, "Error while CameraService cleanup"); + } finally { _cameraManager = null; @@ -386,5 +477,4 @@ public class CameraService : IDisposable _state = State.NotInitialized; } } - } diff --git a/GSS2.FrameDecoders/FrameDecoderHelper.cs b/GSS2.FrameDecoders/FrameDecoderHelper.cs new file mode 100644 index 0000000..81d759f --- /dev/null +++ b/GSS2.FrameDecoders/FrameDecoderHelper.cs @@ -0,0 +1,63 @@ +using GSS2.FrameDecoders; + +using LibCameraSharp; + +public static class FrameDecoderHelper +{ + private static readonly IReadOnlyList _decoders; + + static FrameDecoderHelper() + { + _decoders = LoadDecoders(); + } + + private static IReadOnlyList 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() + .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); + } +} \ No newline at end of file diff --git a/GSS2.FrameDecoders/GSS2.FrameDecoders.csproj b/GSS2.FrameDecoders/GSS2.FrameDecoders.csproj new file mode 100644 index 0000000..05b6437 --- /dev/null +++ b/GSS2.FrameDecoders/GSS2.FrameDecoders.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/GSS2.FrameDecoders/IFrameDecoder.cs b/GSS2.FrameDecoders/IFrameDecoder.cs new file mode 100644 index 0000000..88012b1 --- /dev/null +++ b/GSS2.FrameDecoders/IFrameDecoder.cs @@ -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> planes); +} diff --git a/GranuSightSoftware2.sln b/GranuSightSoftware2.sln index d2a5f97..24bf55b 100644 --- a/GranuSightSoftware2.sln +++ b/GranuSightSoftware2.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSharpClient", "CSharpClien EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.LightsControl.CSharpClient", "GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj", "{D0130B90-6ECA-49CA-A126-EFEFF481FC74}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.FrameDecoders\GSS2.FrameDecoders.csproj", "{05085293-E710-41D6-8F61-701ADBA52FA9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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|x86.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE