From 5b8db828946328425b98fb66b2dfc3f4b9a07987 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Tue, 17 Feb 2026 14:42:50 +0300 Subject: [PATCH] feat: camera tuning + capturing command + raw image correction --- GSS2.Core/Hardware/CameraService.cs | 112 ++++--- .../BGGR_PISP_COMP1_RAW_FrameDecoder.cs | 25 +- GSS2.FrameDecoders/IFrameDecoder.cs | 2 +- GSS2.Test/Program.cs | 75 ++++- GSS2.Test/ProgramCameraTuning.cs | 279 +++++++++++++++++- GSS2.Test/ProgramCapture.cs | 122 ++++++++ GSS2.Test/appsettings.json | 34 +-- 7 files changed, 579 insertions(+), 70 deletions(-) create mode 100644 GSS2.Test/ProgramCapture.cs diff --git a/GSS2.Core/Hardware/CameraService.cs b/GSS2.Core/Hardware/CameraService.cs index 0458912..002435a 100644 --- a/GSS2.Core/Hardware/CameraService.cs +++ b/GSS2.Core/Hardware/CameraService.cs @@ -58,7 +58,11 @@ public class CameraService : IDisposable ImageCaptureFrameBufferCount = 2, ImageCaptureWidth = 4056, ImageCaptureHeight = 3080, - CameraHardSettings = null + CameraHardSettings = null, + DecodeGainR = 1, + DecodeGainG = 1, + DecodeGainB = 1, + DecodeBlackLevel = 256 }; public string CameraId { get; init; } = ""; @@ -70,6 +74,10 @@ public class CameraService : IDisposable public int ImageCaptureWidth { get; init; } public int ImageCaptureHeight { get; init; } public IConfigurationSection? CameraHardSettings { get; init; } + public double DecodeGainR { get; set; } + public double DecodeGainG { get; set; } + public double DecodeGainB { get; set; } + public int DecodeBlackLevel { get; set; } } private bool _disposedValue; @@ -94,6 +102,7 @@ public class CameraService : IDisposable public bool IsImageCapturing { get; private set; } = false; public List ViewFinderBufferQueue { get; private set; } = new List(); + public CameraServiceConfiguration Configuration => _configuration; public CameraService(ILogger logger, CameraServiceConfiguration? configuration = null, bool autoInitialize = false) { @@ -298,7 +307,8 @@ public class CameraService : IDisposable cancellationToken.ThrowIfCancellationRequested(); - int result = _camera.Start(); + // int result = _camera.Start(); + int result = _camera.Start(GetSettingsControlList(_cameraHardSettings)); if (result < 0) { _logger.LogCritical("Error while camera start {Result}", result); @@ -504,6 +514,7 @@ public class CameraService : IDisposable _state |= State.ImageCaptureFrameBuffersMapped; } + private string GetSettings(int indent = 0) { if (_camera is null) @@ -562,7 +573,60 @@ public class CameraService : IDisposable return sb.ToString(); } - public async Task CaptureImage(CancellationToken cancellationToken, int framesCount = 1) + private void ApplyRequestSettings(Request request, Dictionary settings) + { + if (_camera is null) + throw new InvalidOperationException("Camera not initialized"); + + foreach (var (id, info) in _camera.Controls) + { + if (!settings.TryGetValue(id.Name, out var parameter)) + continue; + + ControlValue controlValue; + try + { + controlValue = ControlValueHelper.Convert(parameter, id, info); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while converting value (control name:{}, control type:{}, value type:{}, value:{})", id.Name, id.Type, parameter.GetType(), parameter); + continue; + } + + request.Controls.Set(id.Id, controlValue); + } + } + private ControlList GetSettingsControlList(Dictionary settings) + { + if (_camera is null) + throw new InvalidOperationException("Camera not initialized"); + + var controlList = new ControlList(); + + foreach (var (id, info) in _camera.Controls) + { + if (!settings.TryGetValue(id.Name, out var parameter)) + continue; + + ControlValue controlValue; + try + { + controlValue = ControlValueHelper.Convert(parameter, id, info); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while converting value (control name:{}, control type:{}, value type:{}, value:{})", id.Name, id.Type, parameter.GetType(), parameter); + continue; + } + + controlList.Set(id.Id, controlValue); + } + + return controlList; + } + + public async Task CaptureImage(CancellationToken cancellationToken, int framesCount = 1, Dictionary? settings = null) { ArgumentOutOfRangeException.ThrowIfLessThan(framesCount, 1, nameof(framesCount)); @@ -581,6 +645,8 @@ public class CameraService : IDisposable .CreateLinkedTokenSource(_disposeCts.Token, cancellationToken) .Token; + if (IsImageCapturing) + return null; IsImageCapturing = true; _logger.LogDebug("Capturing {} frame(s)", framesCount); @@ -609,11 +675,13 @@ public class CameraService : IDisposable foreach (var buffer in buffers) { var request = _camera.CreateRequest(1); - ApplySettings(request, _cameraHardSettings); var result = request.AddBuffer(_imageCaptureStream, buffer); if (result < 0) throw new Exception("Error while request create"); + + if (settings is not null) + ApplyRequestSettings(request, settings); requests.Add(request); } _logger.LogDebug("{} request(s) created", requests.Count()); @@ -726,7 +794,7 @@ public class CameraService : IDisposable try { - 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, _configuration.DecodeGainR, _configuration.DecodeGainG, _configuration.DecodeGainB, _configuration.DecodeBlackLevel); } catch (Exception ex) { @@ -756,40 +824,6 @@ public class CameraService : IDisposable IsImageCapturing = false; return image; } - private void ApplySettings(Request request, Dictionary settings) - { - if (_camera is null) - throw new InvalidOperationException("Camera not initialized"); - - foreach (var (id, info) in _camera.Controls) - { - if (!settings.ContainsKey(id.Name)) - continue; - - var value = settings[id.Name]; - ControlValue? controlValue; - try - { - if (!id.IsArray) - controlValue = ControlValueHelper.ConvertToControlValue(value, id, info); - else - controlValue = ControlValueHelper.ConvertToControlArray(value, id, info); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while converting value (control name:{}, control type:{}, value type:{}, value:{})", id.Name, id.Type, value.GetType(), value); - continue; - } - - if (controlValue is null) - { - _logger.LogWarning("Cannot convert to control value (control name:{}, control type:{}, value type:{}, value:{})", id.Name, id.Type, value.GetType(), value); - continue; - } - - request.Controls.Set(id.Id, controlValue); - } - } public void StartViewFinder() { diff --git a/GSS2.FrameDecoders/BGGR_PISP_COMP1_RAW_FrameDecoder.cs b/GSS2.FrameDecoders/BGGR_PISP_COMP1_RAW_FrameDecoder.cs index 1d957d5..2ac1ada 100644 --- a/GSS2.FrameDecoders/BGGR_PISP_COMP1_RAW_FrameDecoder.cs +++ b/GSS2.FrameDecoders/BGGR_PISP_COMP1_RAW_FrameDecoder.cs @@ -129,7 +129,7 @@ public class BGGR_PISP_COMP1_RAW_FrameDecoder : IFrameDecoder // BGGR_PISP_COMP1 public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum => ColorSpace.YcbcrEncodingEnum.None; public ColorSpace.RangeEnum RangeEnum => ColorSpace.RangeEnum.Full; - public Mat Decode(int width, int height, int stride, List planes) + public Mat Decode(int width, int height, int stride, List planes, double rGain, double gGain, double bGain, int blackLevel) { // Дектоирование этого формата состоит из двух этапов: // 1) Распаковка исходных данных в которых каждые 8 байт представляют 8 пикселей (это не значит что 1 байт представляет 1 пиксель!) @@ -137,7 +137,7 @@ public class BGGR_PISP_COMP1_RAW_FrameDecoder : IFrameDecoder // BGGR_PISP_COMP1 if (planes.Count < 1) throw new ArgumentException($"At least 1 plane expected, got {planes.Count}"); - + var pixelCount = width * height; var unpackedSum = new uint[pixelCount]; @@ -153,6 +153,27 @@ public class BGGR_PISP_COMP1_RAW_FrameDecoder : IFrameDecoder // BGGR_PISP_COMP1 for (int i = 0; i < pixelCount; i++) unpackedAvg[i] = (ushort)(unpackedSum[i] / planes.Count()); + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + { + int idx = y * width + x; + double value = unpackedAvg[idx]; + + value = Math.Max(0, value - blackLevel); + + bool isBlue = (y % 2 == 0) && (x % 2 == 0); + bool isRed = (y % 2 == 1) && (x % 2 == 1); + + if (isRed) + value *= rGain; + else if (isBlue) + value *= bGain; + else + value *= gGain; + + unpackedAvg[idx] = (ushort)Math.Min(value, ushort.MaxValue); + } + using var bayer = Mat.FromPixelData(height, width, MatType.CV_16UC1, unpackedAvg); var bgr = new Mat(); Cv2.CvtColor(bayer, bgr, ColorConversionCodes.BayerRG2BGR); diff --git a/GSS2.FrameDecoders/IFrameDecoder.cs b/GSS2.FrameDecoders/IFrameDecoder.cs index f47e1ed..a38c589 100644 --- a/GSS2.FrameDecoders/IFrameDecoder.cs +++ b/GSS2.FrameDecoders/IFrameDecoder.cs @@ -13,5 +13,5 @@ public interface IFrameDecoder public ColorSpace.YcbcrEncodingEnum YcbcrEncodingEnum { get; } public ColorSpace.RangeEnum RangeEnum { get; } - public Mat Decode(int width, int height, int stride, List planes); + public Mat Decode(int width, int height, int stride, List planes, double rGain, double gGain, double bGain, int blackLevel); } diff --git a/GSS2.Test/Program.cs b/GSS2.Test/Program.cs index 5adbb80..90eab2e 100644 --- a/GSS2.Test/Program.cs +++ b/GSS2.Test/Program.cs @@ -14,11 +14,68 @@ public partial class Program } private static string[]? _args = null; - private static readonly RootCommand _rootCommand = new("GranuSight Software 2"); + private static readonly RootCommand _rootCommand = new("Run UI application"); + private static readonly Command _cameraTuningCommand = new("camera-tuning") { Description = "Tune camera parameters" }; + private static readonly Option _cameraTuningOutputFile = new("--output") + { + Description = "Output image file", + Required = false + }; + private static readonly Option _cameraTuningGainR = new("--gain-r") + { + Description = "Color gain (red channel) to start from", + DefaultValueFactory = (_) => 1 + }; + private static readonly Option _cameraTuningGainG = new("--gain-g") + { + Description = "Color gain (green channel) to start from", + DefaultValueFactory = (_) => 1 + }; + private static readonly Option _cameraTuningGainB = new("--gain-b") + { + Description = "Color gain (blue channel) to start from", + DefaultValueFactory = (_) => 1 + }; + private static readonly Option _cameraTuningThreshold = new("--threshold") + { + Description = "Threshold to stop on", + DefaultValueFactory = (_) => 0.1 + }; + private static readonly Option _cameraTuningSteps = new("--steps") + { + Description = "Maximum steps count", + DefaultValueFactory = (_) => 10 + }; + + private static readonly Command _captureCommand = new("capture") + { + Description = "Capture image from camera" + }; + private static readonly Option _captureOutputFile = new("--output") + { + Description = "Output image file", + DefaultValueFactory = (_) => new FileInfo("image.png") + }; + private static readonly Option _captureIntencityWhite = new("--intencity-white") + { + Description = "Illuminator intencity for white (from 0 to 1)", + DefaultValueFactory = (_) => 1 + }; + private static readonly Option _captureIntencityUv365nm = new("--intencity-uv365nm") + { + Description = "Illuminator intencity for UV 365 nm (from 0 to 1)", + DefaultValueFactory = (_) => 0 + }; + private static readonly Option _captureIntencityUv254nm = new("--intencity-uv254nm") + { + Description = "Illuminator intencity for UV 254 nm (from 0 to 1)", + DefaultValueFactory = (_) => 0 + }; + private static readonly Option _renderingModeArgument = new("--rendering-mode") { Description = "Rendering mode to use", @@ -38,11 +95,25 @@ public partial class Program _args = args; _rootCommand.Add(_cameraTuningCommand); + _cameraTuningCommand.Add(_cameraTuningOutputFile); + _cameraTuningCommand.Add(_cameraTuningGainR); + _cameraTuningCommand.Add(_cameraTuningGainG); + _cameraTuningCommand.Add(_cameraTuningGainB); + _cameraTuningCommand.Add(_cameraTuningThreshold); + _cameraTuningCommand.Add(_cameraTuningSteps); + + _captureCommand.Add(_captureOutputFile); + _captureCommand.Add(_captureIntencityWhite); + _captureCommand.Add(_captureIntencityUv365nm); + _captureCommand.Add(_captureIntencityUv254nm); + _rootCommand.Add(_captureCommand); + _rootCommand.Add(_renderingModeArgument); _rootCommand.Add(_hideConsoleArgument); - + _rootCommand.SetAction(Ui); _cameraTuningCommand.SetAction(CameraTuning); + _captureCommand.SetAction(Capture); Console.CancelKeyPress += new ConsoleCancelEventHandler(OnProgramShutdown); Console.WriteLine("Press Ctrl+C to shut down."); diff --git a/GSS2.Test/ProgramCameraTuning.cs b/GSS2.Test/ProgramCameraTuning.cs index dca597e..01f9a0b 100644 --- a/GSS2.Test/ProgramCameraTuning.cs +++ b/GSS2.Test/ProgramCameraTuning.cs @@ -6,6 +6,8 @@ using GSS2.Core.Logging; using Microsoft.Extensions.Logging.Console; +using OpenCvSharp; + using ConsoleFormatter = GSS2.Core.Logging.ConsoleFormatter; namespace GSS2.Test; @@ -15,26 +17,290 @@ public partial class Program private class CameraTuningWorker : BackgroundService { private readonly ILogger _logger; - private readonly IHostApplicationBuilder _applicationLifetime; + private readonly IHostApplicationLifetime _applicationLifetime; private readonly CameraService _cameraService; private readonly IlluminatorService _illuminatorService; + private readonly FileInfo? _outputFile; + private double _gainR; + private double _gainG; + private double _gainB; + private readonly double _threshold; + private readonly uint _steps; + private static readonly Scalar[] KnownSegments = + { + new Scalar(0,255,255), // Cyan + new Scalar(0,0,255), // Blue + new Scalar(255,0,255), // Magenta + new Scalar(128,128,128), // Gray (skip) + new Scalar(255,0,0), // Red + new Scalar(255,128,0), // Orange + new Scalar(255,255,0), // Yellow + new Scalar(0,255,0) // Green + }; + private const double PatternOuterRadius = 180.0; + private const double PatternCenterRadius = 60.0; - public CameraTuningWorker(ILogger logger, IHostApplicationBuilder applicationLifetime, CameraService cameraService, IlluminatorService illuminatorService) + public CameraTuningWorker(ILogger logger, IHostApplicationLifetime applicationLifetime, CameraService cameraService, IlluminatorService illuminatorService, FileInfo? outputFile, double gainR, double gainG, double gainB, double threshold, uint steps) { _applicationLifetime = applicationLifetime; _logger = logger; _cameraService = cameraService; _illuminatorService = illuminatorService; + _outputFile = outputFile; + _gainR = gainR; + _gainG = gainG; + _gainB = gainB; + _threshold = threshold; + _steps = steps; _logger.LogInformation("Initialized"); + _logger.LogInformation("Output File: {}", _outputFile); + _logger.LogInformation("Gain R: {}", _gainR); + _logger.LogInformation("Gain G: {}", _gainG); + _logger.LogInformation("Gain B: {}", _gainB); + _logger.LogInformation("Threshold: {}", _threshold); + _logger.LogInformation("Steps: {}", _steps); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Running camera fine tuning"); + + Mat? image; + + int i = 0; + bool optimalGainsFound = false; + + _illuminatorService.SetIntensity(1, 0, 0); + _illuminatorService.TurnOn(); + + while (i < _steps) + { + _cameraService.Configuration.DecodeGainR = _gainR; + _cameraService.Configuration.DecodeGainG = _gainG; + _cameraService.Configuration.DecodeGainB = _gainB; + + try + { + image = await _cameraService.CaptureImage(stoppingToken, 2); + + if (image is null) + { + _logger.LogError("Cannot get image"); + break; + } + + if (image.Type() != MatType.CV_8UC3 && + image.Type() != MatType.CV_8UC4 && + image.Type() != MatType.CV_16UC3 && + image.Type() != MatType.CV_16UC4) + { + _logger.LogCritical("Unsupported image type: {}", image.Type().ToString()); + break; + } + + if (image.Type() == MatType.CV_16UC3) + image.ConvertTo(image, MatType.CV_8UC3, 1 / 256.0); + if (image.Type() == MatType.CV_16UC4) + image.ConvertTo(image, MatType.CV_8UC4, 1 / 256.0); + + var channels = image.Split(); + Cv2.Merge([channels[0], channels[1], channels[2]], image); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while image capturing"); + break; + } + + try + { + if (_outputFile is not null) + image.ImWrite(_outputFile.FullName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while image saving"); + } + + try + { + var gains = ComputeWhiteBalanceGains(image); + if (gains is null) + { + _logger.LogError("Cannot compute gains"); + break; + } + + var (r, g, b) = gains.Value; + if (Math.Abs(_gainR - r) < _threshold && + Math.Abs(_gainG - g) < _threshold && + Math.Abs(_gainB - b) < _threshold) + { + _logger.LogInformation("Optimal color gains: r={}, g={}, b:={}", r, g, b); + optimalGainsFound = true; + break; + } + _gainR = (_gainR + r) / 2; + _gainG = (_gainG + g) / 2; + _gainB = (_gainB + b) / 2; + _logger.LogInformation("Step {}/{} color gains: r={}, g={}, b:={}", i + 1, _steps, _gainR, _gainG, _gainB); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while gains computing"); + break; + } + i++; + } + _illuminatorService.TurnOff(); + + if (!optimalGainsFound) + _logger.LogWarning("Optimal gains not found"); + + _applicationLifetime.StopApplication(); + } + + private (double rGain, double gGain, double bGain)? ComputeWhiteBalanceGains(Mat image) + { + var circle = FindCircle(image); + if (circle == null) + return null; + + var (rx, ry, rr) = circle.Value; + + double scale = rr / PatternOuterRadius; + double innerPx = PatternCenterRadius * scale; + int midR = (int)((innerPx + rr) / 2.0); + + double phi0 = DetectOrientation(image, rx, ry, rr); + if (double.IsNaN(phi0)) + phi0 = Math.PI / 2.0; + + var samples = new List(); + var refs = new List(); + + int N = KnownSegments.Length; + + for (int i = 0; i < N; i++) + { + if (i == 3) continue; // skip gray + + double theta = phi0 - (2 * Math.PI / N) * i; + + int cx = (int)(rx + midR * Math.Cos(theta)); + int cy = (int)(ry - midR * Math.Sin(theta)); + + var roi = new Rect( + Math.Max(cx - 8, 0), + Math.Max(cy - 8, 0), + Math.Min(16, image.Width - Math.Max(cx - 8, 0)), + Math.Min(16, image.Height - Math.Max(cy - 8, 0))); + + if (roi.Width < 8 || roi.Height < 8) + continue; + + using var patch = new Mat(image, roi); + var mean = Cv2.Mean(patch); + + // BGR → RGB + samples.Add(new Vec3d(mean.Val2, mean.Val1, mean.Val0)); + + var refColor = KnownSegments[i]; + refs.Add(new Vec3d(refColor.Val2, refColor.Val1, refColor.Val0)); + } + + if (samples.Count < 3) + return null; + + // Формируем матрицы для least squares + var S = new Mat(samples.Count, 3, MatType.CV_64F); + var R = new Mat(samples.Count, 3, MatType.CV_64F); + + for (int i = 0; i < samples.Count; i++) + { + S.Set(i, 0, samples[i][0]); + S.Set(i, 1, samples[i][1]); + S.Set(i, 2, samples[i][2]); + + R.Set(i, 0, refs[i][0]); + R.Set(i, 1, refs[i][1]); + R.Set(i, 2, refs[i][2]); + } + + var M = new Mat(); + Cv2.Solve(S, R, M, DecompTypes.Normal); + + double rGain = Math.Max(M.At(0, 0), 1.0); + double gGain = 1.0; + double bGain = Math.Max(M.At(2, 2), 1.0); + + return (rGain, gGain, bGain); + } + private (int x, int y, int r)? FindCircle(Mat image) + { + using var gray = new Mat(); + Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY); + Cv2.GaussianBlur(gray, gray, new Size(9, 9), 2); + + var circles = Cv2.HoughCircles( + gray, + HoughModes.Gradient, + dp: 1.2, + minDist: gray.Rows / 3, + param1: 100, + param2: 30, + minRadius: (int)(gray.Rows * 0.3), + maxRadius: (int)(gray.Rows * 0.6)); + + if (circles.Length == 0) + return null; + + var c = circles[0]; + return ((int)c.Center.X, (int)c.Center.Y, (int)c.Radius); + } + private double DetectOrientation(Mat image, int rx, int ry, int rr) + { + double scale = rr / PatternOuterRadius; + int innerR = (int)(PatternCenterRadius * scale); + + using var gray = new Mat(); + Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY); + + using var mask = Mat.Zeros(image.Size(), MatType.CV_8U).ToMat(); + Cv2.Circle(mask, new Point(rx, ry), innerR, Scalar.White, -1); + + var whitePoints = new List(); + + for (int y = 0; y < gray.Rows; y++) + { + for (int x = 0; x < gray.Cols; x++) + { + if (mask.At(y, x) == 255 && + gray.At(y, x) > 128) + { + whitePoints.Add(new Point(x, y)); + } + } + } + + if (whitePoints.Count < 20) + return double.NaN; + + double cx = whitePoints.Average(p => p.X); + double cy = whitePoints.Average(p => p.Y); + + return Math.Atan2(ry - cy, cx - rx); } } private static void CameraTuning(ParseResult parseResult) { + var outputFile = parseResult.GetValue(_cameraTuningOutputFile); + var gainR = parseResult.GetRequiredValue(_cameraTuningGainR); + var gainG = parseResult.GetRequiredValue(_cameraTuningGainG); + var gainB = parseResult.GetRequiredValue(_cameraTuningGainB); + var threshold = parseResult.GetRequiredValue(_cameraTuningThreshold); + var steps = parseResult.GetRequiredValue(_cameraTuningSteps); + var builder = Host.CreateApplicationBuilder(); builder.Logging.ClearProviders(); @@ -47,7 +313,14 @@ public partial class Program builder.Services.AddIlluminatorService("Hardware:Illuminator"); builder.Services.AddCameraService("Hardware:Camera", true); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(services => + { + var logger = services.GetRequiredService>(); + var applicationLifetime = services.GetRequiredService(); + var cameraService = services.GetRequiredService(); + var illuminatorService = services.GetRequiredService(); + return new CameraTuningWorker(logger, applicationLifetime, cameraService, illuminatorService, outputFile, gainB, gainG, gainR, threshold, steps); + }); var host = builder.Build(); host.Services.GetRequiredService(); diff --git a/GSS2.Test/ProgramCapture.cs b/GSS2.Test/ProgramCapture.cs new file mode 100644 index 0000000..5933797 --- /dev/null +++ b/GSS2.Test/ProgramCapture.cs @@ -0,0 +1,122 @@ +using System.CommandLine; + +using GSS2.Core; +using GSS2.Core.Hardware; +using GSS2.Core.Logging; + +using Microsoft.Extensions.Logging.Console; + +using OpenCvSharp; + +using ConsoleFormatter = GSS2.Core.Logging.ConsoleFormatter; + +namespace GSS2.Test; + +public partial class Program +{ + private class CaptureWorker : BackgroundService + { + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly CameraService _cameraService; + private readonly IlluminatorService _illuminatorService; + private readonly FileInfo _outputFile; + private readonly double _intencityWhite; + private readonly double _intencityUv365nm; + private readonly double _intencityUv254nm; + + public CaptureWorker(ILogger logger, IHostApplicationLifetime applicationLifetime, CameraService cameraService, IlluminatorService illuminatorService, FileInfo outputFile, double intencityWhite, double intencityUv365nm, double intencityUv254nm) + { + _applicationLifetime = applicationLifetime; + _logger = logger; + _cameraService = cameraService; + _illuminatorService = illuminatorService; + _outputFile = outputFile; + _intencityWhite = intencityWhite; + _intencityUv365nm = intencityUv365nm; + _intencityUv254nm = intencityUv254nm; + + _logger.LogInformation("Initialized"); + _logger.LogInformation("Output File: {}", _outputFile); + _logger.LogInformation("Intencity White: {}", _intencityWhite); + _logger.LogInformation("Intencity UV 365 nm: {}", _intencityUv365nm); + _logger.LogInformation("Intencity UV 254 nm: {}", _intencityUv254nm); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Running camera fine tuning"); + _illuminatorService.SetIntensity(_intencityWhite, _intencityUv365nm, _intencityUv254nm); + _illuminatorService.TurnOn(); + + Mat? image; + try + { + image = await _cameraService.CaptureImage(stoppingToken, 5); + + if (image is null) + { + _logger.LogCritical("Cannot get image"); + _applicationLifetime.StopApplication(); + return; + } + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Exception occured while image capturing"); + _applicationLifetime.StopApplication(); + return; + } + finally + { + _illuminatorService.TurnOff(); + } + + try + { + image.ImWrite(_outputFile.FullName); + _logger.LogInformation("Image saved: {}", _outputFile.FullName); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Exception occured while image saving"); + } + + _applicationLifetime.StopApplication(); + } + + } + private static void Capture(ParseResult parseResult) + { + var outputFile = parseResult.GetRequiredValue(_captureOutputFile); + var intencityWhite = parseResult.GetRequiredValue(_captureIntencityWhite); + var intencityUv365nm = parseResult.GetRequiredValue(_captureIntencityUv365nm); + var intencityUv254nm = parseResult.GetRequiredValue(_captureIntencityUv254nm); + + var builder = Host.CreateApplicationBuilder(); + + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(options => options.FormatterName = nameof(ConsoleFormatter)); + builder.Logging.AddConsoleFormatter(); + builder.Logging.AddProvider(new FileLoggerProvider("full.log", true)); + builder.Logging.AddProvider(new FileLoggerProvider("last_run.log", false)); + builder.Services.AddSingleton(); + + builder.Services.AddIlluminatorService("Hardware:Illuminator"); + builder.Services.AddCameraService("Hardware:Camera", true); + + builder.Services.AddHostedService(services => + { + var logger = services.GetRequiredService>(); + var applicationLifetime = services.GetRequiredService(); + var cameraService = services.GetRequiredService(); + var illuminatorService = services.GetRequiredService(); + return new CaptureWorker(logger, applicationLifetime, cameraService, illuminatorService, outputFile, intencityWhite, intencityUv365nm, intencityUv254nm); + }); + + var host = builder.Build(); + host.Services.GetRequiredService(); + + host.Run(); + } +} diff --git a/GSS2.Test/appsettings.json b/GSS2.Test/appsettings.json index 2f69b1e..b8a18f0 100644 --- a/GSS2.Test/appsettings.json +++ b/GSS2.Test/appsettings.json @@ -32,7 +32,7 @@ "Uv365RelayPin": 20, "FanPin": 21, "PwmFrequency": 200, - "MaxWhitePwmDuty": 0.03, + "MaxWhitePwmDuty": 0.05, "MaxUv254PwmDuty": 1.0, "MaxUv365PwmDuty": 1.0 }, @@ -48,39 +48,27 @@ "CameraHardSettings": { "AeEnable": false, "ExposureValue": 8.0, - "ExposureTime": 1000000, + "ExposureTime": 30000, "ExposureTimeMode": "ExposureTimeModeManual", - "AnalogueGain": 1.0, + "AnalogueGain": 23.0, "AnalogueGainMode": "AnalogueGainModeManual", - "AeFlickerMode": "FlickerOff", - "AeFlickerPeriod": 5000, "Brightness": 0.0, "Contrast": 1.0, - "AwbEnable": true, + "AwbEnable": false, "ColourGains": [ - 3.3, - 1.5 - ], - "ColourTemperature": 5500, - "Saturation": 1.0, - "Sharpness": 1.0, - "ScalerCrop": [ - 0, - 0, - 4056, - 3040 - ], - "FrameDurationLimits": [ - 100000, - 694434742 + 1.2, + 1.0 ], "HdrMode": "HdrModeOff", "NoiseReductionMode": "NoiseReductionModeOff", "StatsOutputEnable": true, "SyncMode": "SyncModeOff", - "SyncFrames": 1, "CnnEnableInputTensor": false - } + }, + "DecodeGainR": 3.2, + "DecodeGainG": 1.0, + "DecodeGainB": 1.0, + "DecodeBlackLevel": 4096 }, "TemperatureHumidity": { "Bus": 6,