feat: amine content separator calibration db editor

This commit is contained in:
2026-03-02 15:27:28 +03:00
parent 53a9e9ee30
commit b4c8a92c63
4 changed files with 1157 additions and 0 deletions
@@ -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<AmineContentCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context;
private readonly IlluminatorService _illuminatorService;
private readonly CameraService _cameraService;
private readonly ImageStorageService _imageStorageService;
private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] private ObservableCollection<CalibrationRecord> _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<KeyValuePair<IImage, ImageRecord>> _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<KeyValuePair<IImage, ImageRecord>> _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<AmineContentCalibrationViewModel> 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<CalibrationRecord>(_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<ImageRecord>(),
SeparatorImages = new List<ImageRecord>()
};
IsNewRecord = true;
}
_logger.LogInformation("Initialized");
}
partial void OnRecordsChanged(ObservableCollection<CalibrationRecord>? oldValue, ObservableCollection<CalibrationRecord> newValue)
{
oldValue?.CollectionChanged -= OnRecordsCollectionChanged;
newValue?.CollectionChanged += OnRecordsCollectionChanged;
}
partial void OnCurrentRecordChanged(CalibrationRecord value)
{
SeparatorImages = new(
value.SeparatorImages
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key), kv.Value))
);
SampleImages = new(
value.SampleImages
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(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<ImageRecord>(),
SeparatorImages = new List<ImageRecord>()
};
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<ImageRecord>();
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<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(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<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(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()
// {
// }
}
@@ -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<AmineContentSeparatorCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context;
private readonly IlluminatorService _illuminatorService;
private readonly CameraService _cameraService;
private readonly ImageStorageService _imageStorageService;
private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] private ObservableCollection<SeparatorRecord> _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<KeyValuePair<IImage, ImageRecord>> _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<AmineContentSeparatorCalibrationViewModel> 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<SeparatorRecord>(_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<SeparatorRecord>? oldValue, ObservableCollection<SeparatorRecord> newValue)
{
oldValue?.CollectionChanged -= OnRecordsCollectionChanged;
newValue?.CollectionChanged += OnRecordsCollectionChanged;
}
partial void OnCurrentRecordChanged(SeparatorRecord value)
{
var images = value.Images
.ToAsyncEnumerable()
.Select(i => new KeyValuePair<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i))
.Where(kv => kv.Key is not null)
.Select(kv => new KeyValuePair<IImage, ImageRecord>(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<ImageRecord>();
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<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i))
.Where(kv => kv.Key is not null)
.Select(kv => new KeyValuePair<IImage, ImageRecord>(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++;
}