From b4c8a92c6374755e8305f04f09b77a48a69b18b4 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Mon, 2 Mar 2026 15:27:28 +0300 Subject: [PATCH] feat: amine content separator calibration db editor --- .../AmineContentCalibrationViewModel.cs | 343 ++++++++++++ ...ineContentSeparatorCalibrationViewModel.cs | 278 ++++++++++ ...AmineContentSeparatorCalibrationView.axaml | 525 ++++++++++++++++++ ...neContentSeparatorCalibrationView.axaml.cs | 11 + 4 files changed, 1157 insertions(+) create mode 100644 GSS2.UI.Core/ViewModels/AmineContentCalibrationViewModel.cs create mode 100644 GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs create mode 100644 GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml create mode 100644 GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml.cs diff --git a/GSS2.UI.Core/ViewModels/AmineContentCalibrationViewModel.cs b/GSS2.UI.Core/ViewModels/AmineContentCalibrationViewModel.cs new file mode 100644 index 0000000..14bc72e --- /dev/null +++ b/GSS2.UI.Core/ViewModels/AmineContentCalibrationViewModel.cs @@ -0,0 +1,343 @@ +using System.Collections.Specialized; +using System.Collections.ObjectModel; + +using Microsoft.Extensions.Logging; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +using GSS2.Core.Hardware; +using GSS2.Core.Extentions; +using GSS2.Core.Analysis.AmineContent.Database.Calibration; + +namespace GSS2.UI.Core.ViewModels; + +public partial class AmineContentCalibrationViewModel : ViewModelBase +{ + private readonly ILogger _logger; + private readonly AmineContentCalibrationContext _context; + private readonly IlluminatorService _illuminatorService; + private readonly CameraService _cameraService; + private readonly ImageStorageService _imageStorageService; + + private CancellationTokenSource? _captureImagesTaskCts = null; + + [ObservableProperty] private ObservableCollection _records; + [ObservableProperty] private CalibrationRecord _currentRecord; + [ObservableProperty] private bool _isMenuOpen = false; + [ObservableProperty] private bool _isNewRecord = false; + + [ObservableProperty] private int _mainSectionsCurrentIndex = 0; + [ObservableProperty] private string _switchMainSectionButtonText = "К изображениям"; + + [ObservableProperty] private int _imageTypeSectionCurrentIndex = 0; + [ObservableProperty] private string _switchImageTypeSectionButtonText = "К изображениям пробы"; + + [ObservableProperty] private Task? _captureImagesTask = null; + + [ObservableProperty] private ObservableCollection> _separatorImages = new(); + [ObservableProperty] private bool _separatorImagesIsCapturing = false; + [ObservableProperty] private string _separatorImagesCapturingProgress = ""; + [ObservableProperty] private int _separatorImagesSectionCurrentIndex = 0; + [ObservableProperty] private bool _separatorImagesSectionCanScrollForward = false; + [ObservableProperty] private bool _separatorImagesSectionCanScrollBackward = false; + + [ObservableProperty] private ObservableCollection> _sampleImages = new(); + [ObservableProperty] private bool _sampleImagesIsCapturing = false; + [ObservableProperty] private string _sampleImagesCapturingProgress = ""; + [ObservableProperty] private int _sampleImagesSectionCurrentIndex = 0; + [ObservableProperty] private bool _sampleImagesSectionCanScrollForward = false; + [ObservableProperty] private bool _sampleImagesSectionCanScrollBackward = false; + + public AmineContentCalibrationViewModel(ILogger logger, AmineContentCalibrationContext context, IlluminatorService illuminatorService, CameraService cameraService, ImageStorageService imageStorageService) + { + _logger = logger; + _logger.LogInformation("Initialization"); + + _context = context; + _illuminatorService = illuminatorService; + _cameraService = cameraService; + _imageStorageService = imageStorageService; + + Records = new ObservableCollection(_context.CalibrationRecords); + + if (Records.Count() > 0) + CurrentRecord = Records.First(); + else + { + CurrentRecord = new CalibrationRecord() + { + SampleName = null, + SampleComment = null, + SampleBrand = null, + SampleMass = null, + SamplingDateTime = null, + SamplingPlace = null, + MixtureName = null, + MixtureAmineContent = null, + MixtureNormalRate = null, + MixtureActualRate = null, + SampleImages = new List(), + SeparatorImages = new List() + }; + IsNewRecord = true; + } + + _logger.LogInformation("Initialized"); + } + + partial void OnRecordsChanged(ObservableCollection? oldValue, ObservableCollection newValue) + { + oldValue?.CollectionChanged -= OnRecordsCollectionChanged; + newValue?.CollectionChanged += OnRecordsCollectionChanged; + } + partial void OnCurrentRecordChanged(CalibrationRecord value) + { + SeparatorImages = new( + value.SeparatorImages + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i)) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key), kv.Value)) + ); + + SampleImages = new( + value.SampleImages + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i)) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key), kv.Value)) + ); + } + + private void OnRecordsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs ea) + { + Console.WriteLine($"RecordsCollectionChanged {ea.Action}"); + } + + [RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen; + [RelayCommand] private void CloseMenu() => IsMenuOpen = false; + + [RelayCommand] + private void CreateRecord() + { + var record = new CalibrationRecord() + { + SampleName = null, + SampleComment = null, + SampleBrand = null, + SampleMass = null, + SamplingDateTime = null, + SamplingPlace = null, + MixtureName = null, + MixtureAmineContent = null, + MixtureNormalRate = null, + MixtureActualRate = null, + SampleImages = new List(), + SeparatorImages = new List() + }; + CurrentRecord = record; + IsNewRecord = true; + } + + // [RelayCommand] + // private void DeleteRecord() + // { + // //TODO + // // if (Record == null) return; + + // // _context.CalibrationRecords.Remove(Record); + // // Records.Remove(Record); + // // if (Records.Count() > 0) + // // Record = Records.First(); + // // else + // // { + // // var record = new CalibrationRecord(); + // // _context.CalibrationRecords.Add(record); + // // Records.Add(record); + // // Record = record; + // // } + // IsNewRecord = true; + // CreateRecord(); + // } + + // [RelayCommand] + // private void SaveRecord() + // { + // //TODO + // // _context.SaveChanges(); + // IsNewRecord = false; + // } + + [RelayCommand] + private void SwitchMainSection() + { + MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0; + SwitchMainSectionButtonText = MainSectionsCurrentIndex == 0 ? + "К изображениям" : + "К общей информации"; + } + + [RelayCommand] + private void SwitchImageTypeSection() + { + ImageTypeSectionCurrentIndex = ImageTypeSectionCurrentIndex == 0 ? 1 : 0; + SwitchImageTypeSectionButtonText = ImageTypeSectionCurrentIndex == 0 ? + "К изображениям пробы" : + "К изображениям сепаратора"; + } + + [RelayCommand] + private void CaptureImage() + { + if (CaptureImagesTask is not null) + return; + + _captureImagesTaskCts = new CancellationTokenSource(); + var cancellationToken = _captureImagesTaskCts.Token; + + var imageTypeSectionCurrentIndex = ImageTypeSectionCurrentIndex; + CaptureImagesTask = new Task(async () => + { + _illuminatorService.TurnOn(); + (double visible, double uv365, double uv254)[] intensities = + { + (0.02500, 0.0, 0.0), + (0.01250, 0.0, 0.0), + (0.00625, 0.0, 0.0), + (0.00000, 1.0, 0.0), + (0.00000, 0.0, 1.0), + (0.00000, 1.0, 1.0), + (0.01250, 1.0, 0.0), + (0.01250, 0.0, 1.0), + (0.01250, 1.0, 1.0), + (0.00625, 1.0, 0.0), + (0.00625, 0.0, 1.0), + (0.00625, 1.0, 1.0) + }; + + Dispatcher.UIThread.Invoke(() => + { + if (imageTypeSectionCurrentIndex == 0) + SeparatorImagesIsCapturing = true; + else + SampleImagesIsCapturing = true; + }); + + try + { + var imageRecords = new List(); + + foreach (var (i, intensity) in intensities.Enumerate()) + { + cancellationToken.ThrowIfCancellationRequested(); + + Dispatcher.UIThread.Invoke(() => + { + if (imageTypeSectionCurrentIndex == 0) + SeparatorImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}"; + else + SampleImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}"; + }); + + _illuminatorService.SetIntensity(intensity.visible, intensity.uv365, intensity.uv254); + + OpenCvSharp.Mat? image; + image = await _cameraService.CaptureImage(cancellationToken, 5); + if (image is null) + { + _logger.LogError("Cannot get image"); + return; + } + + var bytes = image.ImEncode(".png"); + string fileName; + using (var stream = new MemoryStream(bytes)) + fileName = await _imageStorageService.SaveAsync(stream, ".png", "calibration", cancellationToken); + + image.Dispose(); + image = null; + + imageRecords.Add( + new ImageRecord() + { + ImagePath = fileName, + VisibleIntensity = intensity.visible, + Uv365Intensity = intensity.uv365, + Uv254Intensity = intensity.uv254, + } + ); + } + + _illuminatorService.TurnOff(); + + Dispatcher.UIThread.Invoke(() => + { + if (imageTypeSectionCurrentIndex == 0) + { + CurrentRecord.SeparatorImages.Clear(); + CurrentRecord.SeparatorImages.AddRange(imageRecords); + + SeparatorImages.Clear(); + imageRecords + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i)) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key), kv.Value)) + .ToList() + .ForEach(SeparatorImages.Add); + } + else + { + CurrentRecord.SampleImages.Clear(); + CurrentRecord.SampleImages.AddRange(imageRecords); + + SampleImages.Clear(); + imageRecords + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i)) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key), kv.Value)) + .ToList() + .ForEach(SampleImages.Add); + } + }); + } + catch (TaskCanceledException) + { } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while image capturing"); + } + finally + { + _illuminatorService.TurnOff(); + _captureImagesTaskCts = null; + Dispatcher.UIThread.Invoke(() => + { + if (imageTypeSectionCurrentIndex == 0) + { + SeparatorImagesIsCapturing = false; + SeparatorImagesCapturingProgress = ""; + } + else + { + SampleImagesIsCapturing = false; + SampleImagesCapturingProgress = ""; + } + CaptureImagesTask = null; + }); + } + }, cancellationToken); + + CaptureImagesTask.Start(); + } + + [RelayCommand] private void CencelImageCapturing() => _captureImagesTaskCts?.Cancel(); + [RelayCommand] private void SeparatorImagesSectionPrevious() => SeparatorImagesSectionCurrentIndex--; + [RelayCommand] private void SeparatorImagesSectionNext() => SeparatorImagesSectionCurrentIndex++; + [RelayCommand] private void SampleImagesSectionPrevious() => SampleImagesSectionCurrentIndex--; + [RelayCommand] private void SampleImagesSectionNext() => SampleImagesSectionCurrentIndex++; + + // [RelayCommand] + // private void SelectImagesFromAnotherRecord() + // { + + // } +} \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs b/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs new file mode 100644 index 0000000..ed38f43 --- /dev/null +++ b/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs @@ -0,0 +1,278 @@ +using System.Collections.Specialized; +using System.Collections.ObjectModel; + +using Microsoft.Extensions.Logging; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +using GSS2.Core.Hardware; +using GSS2.Core.Extentions; +using GSS2.Core.Analysis.AmineContent.Database.Calibration; +using Microsoft.EntityFrameworkCore; + +namespace GSS2.UI.Core.ViewModels; + +public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase +{ + private readonly ILogger _logger; + private readonly AmineContentCalibrationContext _context; + private readonly IlluminatorService _illuminatorService; + private readonly CameraService _cameraService; + private readonly ImageStorageService _imageStorageService; + + private CancellationTokenSource? _captureImagesTaskCts = null; + + [ObservableProperty] private ObservableCollection _records; + [ObservableProperty] private SeparatorRecord _currentRecord; + [ObservableProperty] private bool _isMenuOpen = false; + [ObservableProperty] private bool _isNewRecord = false; + + [ObservableProperty] private int _mainSectionsCurrentIndex = 0; + [ObservableProperty] private string _switchMainSectionButtonText = "К изображениям"; + + [ObservableProperty] private Task? _captureImagesTask = null; + + [ObservableProperty] private ObservableCollection> _images = new(); + [ObservableProperty] private bool _imagesIsCapturing = false; + [ObservableProperty] private string _imagesCapturingProgress = ""; + [ObservableProperty] private int _imagesSectionCurrentIndex = 0; + [ObservableProperty] private bool _imagesSectionCanScrollForward = false; + [ObservableProperty] private bool _imagesSectionCanScrollBackward = false; + + public AmineContentSeparatorCalibrationViewModel(ILogger logger, AmineContentCalibrationContext context, IlluminatorService illuminatorService, CameraService cameraService, ImageStorageService imageStorageService) + { + _logger = logger; + _logger.LogInformation("Initialization"); + + _context = context; + _illuminatorService = illuminatorService; + _cameraService = cameraService; + _imageStorageService = imageStorageService; + + Records = new ObservableCollection(_context.SeparatorRecords.Include(r => r.Images)); + + if (Records.Count() > 0) + { + CurrentRecord = Records.First(); + IsNewRecord = false; + } + else + { + CurrentRecord = new SeparatorRecord() + { + Type = "", + Batch = "", + Images = new() + }; + IsNewRecord = true; + } + + _logger.LogInformation("Initialized"); + } + + partial void OnRecordsChanged(ObservableCollection? oldValue, ObservableCollection newValue) + { + oldValue?.CollectionChanged -= OnRecordsCollectionChanged; + newValue?.CollectionChanged += OnRecordsCollectionChanged; + } + partial void OnCurrentRecordChanged(SeparatorRecord value) + { + var images = value.Images + .ToAsyncEnumerable() + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) + .Where(kv => kv.Key is not null) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key!), kv.Value)); + + Images = new(images.ToBlockingEnumerable()); + + IsNewRecord = _context.Entry(value).State == EntityState.Detached; + } + + private void OnRecordsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs ea) + { + Console.WriteLine($"RecordsCollectionChanged {ea.Action}"); + } + + [RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen; + [RelayCommand] private void CloseMenu() => IsMenuOpen = false; + + [RelayCommand] + private void CreateRecord() + { + var record = new SeparatorRecord() + { + Type = "", + Batch = "", + Images = new() + }; + CurrentRecord = record; + IsNewRecord = true; + } + + [RelayCommand] + private void DeleteRecord() + { + var deletedRecord = CurrentRecord; + var deletedIsNewRecord = IsNewRecord; + IsNewRecord = true; + CreateRecord(); + + if (!deletedIsNewRecord) + { + Records.Remove(deletedRecord); + _context.SeparatorRecords.Remove(deletedRecord); + _context.SaveChanges(); + } + } + + [RelayCommand] + private void SaveRecord() + { + for (int i = 0; i < CurrentRecord.Images.Count(); i++) + { + var image = CurrentRecord.Images[i]; + if (_context.Entry(image).State == EntityState.Detached) + { + var attachedImage = _context.ImageRecords.Add(image); + CurrentRecord.Images.RemoveAt(i); + CurrentRecord.Images.Insert(i, attachedImage.Entity); + } + } + if (_context.Entry(CurrentRecord).State == EntityState.Detached) + { + var attachedRecord = _context.SeparatorRecords.Add(CurrentRecord); + CurrentRecord = attachedRecord.Entity; + } + if (IsNewRecord) + Records.Add(CurrentRecord); + + _context.SaveChanges(); + IsNewRecord = false; + } + + [RelayCommand] + private void SwitchMainSection() + { + MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0; + SwitchMainSectionButtonText = MainSectionsCurrentIndex == 0 ? + "К изображениям" : + "К информации"; + } + + [RelayCommand] + private void CaptureImage() + { + if (CaptureImagesTask is not null) + return; + + _captureImagesTaskCts = new CancellationTokenSource(); + var cancellationToken = _captureImagesTaskCts.Token; + + CaptureImagesTask = new Task(async () => + { + _illuminatorService.TurnOn(); + (double visible, double uv365, double uv254)[] intensities = + { + (0.02500, 0.0, 0.0), + (0.01250, 0.0, 0.0), + (0.00625, 0.0, 0.0), + (0.00000, 1.0, 0.0), + (0.00000, 0.0, 1.0), + (0.00000, 1.0, 1.0), + (0.01250, 1.0, 0.0), + (0.01250, 0.0, 1.0), + (0.01250, 1.0, 1.0), + (0.00625, 1.0, 0.0), + (0.00625, 0.0, 1.0), + (0.00625, 1.0, 1.0) + }; + + Dispatcher.UIThread.Invoke(() => ImagesIsCapturing = true); + + try + { + var imageRecords = new List(); + + foreach (var (i, intensity) in intensities.Enumerate()) + { + cancellationToken.ThrowIfCancellationRequested(); + + Dispatcher.UIThread.Invoke(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}"); + + _illuminatorService.SetIntensity(intensity.visible, intensity.uv365, intensity.uv254); + + OpenCvSharp.Mat? image; + image = await _cameraService.CaptureImage(cancellationToken, 5); + if (image is null) + { + _logger.LogError("Cannot get image"); + return; + } + + var bytes = image.ImEncode(".png"); + string fileName; + using (var stream = new MemoryStream(bytes)) + fileName = await _imageStorageService.SaveAsync(stream, ".png", "separator", cancellationToken); + + image.Dispose(); + image = null; + + imageRecords.Add( + new ImageRecord() + { + ImagePath = fileName, + VisibleIntensity = intensity.visible, + Uv365Intensity = intensity.uv365, + Uv254Intensity = intensity.uv254, + } + ); + } + + _illuminatorService.TurnOff(); + + var images = imageRecords + .ToAsyncEnumerable() + .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) + .Where(kv => kv.Key is not null) + .Select(kv => new KeyValuePair(new Bitmap(kv.Key!), kv.Value)); + + Dispatcher.UIThread.Invoke(() => + { + CurrentRecord.Images.Clear(); + CurrentRecord.Images.AddRange(imageRecords); + + Images.Clear(); + Images = new(images.ToBlockingEnumerable()); + }); + } + catch (TaskCanceledException) + { } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while image capturing"); + } + finally + { + _illuminatorService.TurnOff(); + _captureImagesTaskCts = null; + Dispatcher.UIThread.Invoke(() => + { + ImagesIsCapturing = false; + ImagesCapturingProgress = ""; + CaptureImagesTask = null; + }); + } + }, cancellationToken); + + CaptureImagesTask.Start(); + } + + [RelayCommand] private void CencelImageCapturing() => _captureImagesTaskCts?.Cancel(); + [RelayCommand] private void ImagesSectionPrevious() => ImagesSectionCurrentIndex--; + [RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++; +} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml b/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml new file mode 100644 index 0000000..267cd8a --- /dev/null +++ b/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +