From dc8593f8400df719778429d17a2abe403eb312ae Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Thu, 18 Jun 2026 15:28:48 +0300 Subject: [PATCH] feat: fix separator calibration after database reforge + add comments + refactoring --- .../SeparatorCalibrationViewModel.cs | 863 ++++++++++++------ .../SeparatorCalibrationView.axaml | 420 ++++++--- 2 files changed, 850 insertions(+), 433 deletions(-) diff --git a/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs b/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs index 38e7c97..478fb92 100644 --- a/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs +++ b/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; @@ -8,9 +9,10 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using GSS2.Core; using GSS2.Core.Analysis.AmineContent; -using GSS2.Core.Analysis.AmineContent.Database.Calibration; +using GSS2.Core.Analysis.AmineContent.Database; +using GSS2.Core.Extensions; + using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.Services; @@ -18,268 +20,519 @@ namespace GSS2.ViewModels.AmineContent; public partial class SeparatorCalibrationViewModel : ViewModelBase { + /// + /// Внутренняя модель элементов меню + /// + public partial class MenuItemViewModel : ViewModelBase + { + [ObservableProperty] public partial int Id { get; set; } + [ObservableProperty] public partial string Type { get; set; } + + public MenuItemViewModel(int id, string type) + { + Id = id; + Type = type; + } + } + + /// + /// Логгер + /// private readonly ILogger _logger; - private readonly CalibrationContext _context; - private readonly ImageCapturerService _imageCapturer; - private readonly ImageStorageService _imageStorage; + /// + /// Контекст базы данных + /// + private readonly Context _context; + /// + /// Сервис для получения изображений с камеры + /// + private readonly IImageCapturerService _imageCapturer; + /// + /// ViewLocator для получения других View по их моделям + /// private readonly DependencyInjectionViewLocator _viewLocator; + /// + /// Сервис навигации между экранами + /// private readonly NavigationService _navigation; + /// + /// Задача съёмки изображений + /// + private Task? _сaptureImagesTask = null; + /// + /// Токен отмены задачи съёмки изображений + /// private CancellationTokenSource? _captureImagesTaskCts = null; + /// + /// Данные выбранного элемента + /// + private string? _selectedImageData = null; + /// + /// Данные выбранного элемента + /// + private List _selectedFeaturesData = new(); + + /// + /// Меню открыто + /// [ObservableProperty] public partial bool MenuOpened { get; set; } = false; - [ObservableProperty] public partial ObservableCollection Records { get; set; } + /// + /// Элементы меню + /// + [ObservableProperty] public partial ObservableCollection MenuItems { get; set; } = new(); + /// + /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран + /// + [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; - [ObservableProperty] public partial SeparatorRecord? SelectedRecord { get; set; } - [ObservableProperty] public partial SeparatorRecord EditingRecord { get; set; } + /// + /// выбранного элемента + /// + [ObservableProperty] public partial int SelectedId { get; set; } = 0; + /// + /// выбранного элемента + /// + [ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString(); + /// + /// выбранного элемента + /// + [ObservableProperty] public partial string SelectedType { get; set; } = ""; + /// + /// Представление выбранного элемента + /// + [ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new(); + /// + /// Текущий экран главной секции + /// [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; + /// + /// Можно ли переходить на следующий экран главной секции + /// [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; - [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true; - [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true; - - [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; - [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; - [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true; - [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true; - - [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; + /// + /// Можно ли переходить на предыдущий экран главной секции + /// + [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false; + /// + /// Происходит ли переход между экранами главной секции + /// + [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; + /// + /// Проводится ли съёмка изображений + /// [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; + /// + /// Прогресс съёмки изображений + /// [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; - [ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null; - + /// + /// Можно ли производить перемещение между экранами + /// + [ObservableProperty] public partial bool CanScroll { get; set; } public SeparatorCalibrationViewModel( ILogger logger, - CalibrationContext context, - ImageCapturerService imageCapturer, - ImageStorageService imageStorage, + Context context, + IImageCapturerService imageCapturer, DependencyInjectionViewLocator viewLocator, NavigationService navigation ) { + // Устанавливаем readonly поля _logger = logger; _context = context; _imageCapturer = imageCapturer; - _imageStorage = imageStorage; _viewLocator = viewLocator; _navigation = navigation; _logger.LogInformation("Инициализация"); - // Получаем записи из базы данных - Records = new ObservableCollection(_context.SeparatorRecords); + // Получаем записи меню из базы данных + MenuItems = new ObservableCollection(_context.SeparatorRecords.Select(r => new MenuItemViewModel(r.Id, r.Type))); + if (MenuItems.Count() > 0) // Если записи есть, + MenuSelectedIndex = 0; // Выбираем первую запись + else // Если записей нет, + MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана - // Берём первую запись - SelectedRecord = Records.FirstOrDefault(); - // Если записей нет, то создаём пустую - // на самом деле это излишне, потому что при изменении SelectedRecord - // вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений - if (SelectedRecord is null) - EditingRecord = new() - { - Type = "", - Batch = "", - Image = "", - VectorRecords = new() - }; - else - EditingRecord = CopyRecord(SelectedRecord); + // Подписываемся на изменения свойств для обновления CanScroll + PropertyChanged += (_, ea) => + { + if (ea.PropertyName == nameof(MenuOpened) || + ea.PropertyName == nameof(MainSectionIsScrolling)) + UpdateCanScroll(); + }; _logger.LogInformation("Инициализировано"); } /// - /// Вызывается при изменении .
- /// Копирует в или издаёт пустую . + /// Вызывается при изменении ///
- partial void OnSelectedRecordChanged(SeparatorRecord? value) + partial void OnMenuSelectedIndexChanged(int value) { - // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) - // то создаём пустую запись - if (SelectedRecord is null) + // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку + if (value >= MenuItems.Count()) + _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count()); + // Если значение больше или равно 0, то значит выбран один из элементов меню + if (value >= 0 && value < MenuItems.Count()) { - EditingRecord = new() + _logger.LogTrace("Выбран элемент из списка: {}", value); + // Находим запись в базе данных + var selectedRecord = _context.SeparatorRecords.Find(MenuItems[value].Id); + // Если запись найдена, то заполняем поля из базы данных + if (selectedRecord is not null) { - Type = "", - Batch = "", - Image = "", - VectorRecords = new() - }; + // Заполняем простые поля + SelectedId = selectedRecord.Id; + SelectedUuid = selectedRecord.Uuid; + SelectedType = selectedRecord.Type; + + // Ищем изображение в базе данных + var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (imageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение, оно заполнится null если запись не найдена + _selectedImageData = imageRecord?.Base64; + SelectedImage.Data = _selectedImageData; + + // Ищем все признаки в базе данных + _selectedFeaturesData = _context.FeatureRecords + .Where(r => r.SeparatorRecordId == SelectedId) + .Select(r => r.Values.ToArray()) + .ToList(); + + return; + } + // Если значение не найдено в базе данных + else + { + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords)); + var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс + // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями + MenuSelectedIndex = -1; + // Удаляем несуществующий элемент из списка + MenuItems.RemoveAt(nonExistingIndex); + return; + } } + + // Если элемент не выбран, заполняем поля пустыми значениями + _logger.LogTrace("Выбран новый элемент"); + SelectedId = 0; + SelectedUuid = Guid.NewGuid().ToString(); + SelectedType = ""; + SelectedImage.Data = null; + } + + /// + /// Обновить значение + /// + private void UpdateCanScroll() + { + CanScroll = !MenuOpened; + CanScroll &= !MainSectionIsScrolling; + CanScroll &= !ImagesIsCapturing; + } + + // Команды управления меню + /// + /// Открыть меню + /// + [RelayCommand] private void OpenMenu() => MenuOpened = true; + /// + /// Закрыть меню + /// + [RelayCommand] private void CloseMenu() => MenuOpened = false; + /// + /// Переключить видимость меню + /// + [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; + + /// + /// Переключить главную секцию информация <-> изображения + /// + [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; + + /// + /// Создать новую запись + /// + [RelayCommand] + private void CreateRecord() + { + _logger.LogInformation("Создание новой записи"); + // Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и + // при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется, + // потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1, + // поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged + if (MenuSelectedIndex != -1) + MenuSelectedIndex = -1; else + OnMenuSelectedIndexChanged(-1); + } + + /// + /// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана + /// + [RelayCommand] + private void DeleteRecord() + { + // TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи + try { - // Иначе копируем запись, отвязывая её от базы данных - EditingRecord = CopyRecord(SelectedRecord); + _logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType); + // Если запись не выбрана, то просто создаём новую запись + if (MenuSelectedIndex < 0) + { + _logger.LogTrace("Запись не выбрана, создаём новую"); + CreateRecord(); + return; + } + + // Создаём модель для экрана подтверждения + var confirmViewModel = new DeleteConfirmViewModel(_navigation) + { + Question = "Вы уверены, что хотите удалить запись?" + }; + + // Создаём коллбек который вызовется при экрана подтверждения + void OnConfirmCloses(object? sender, bool result) + { + // Если пользователь отменил удаление, то выходим из коллбека + if (!result) + { + _logger.LogTrace("Пользователь отменил удаление"); + return; + } + + // Если запись не выбрана, то просто создаём новую запись + if (MenuSelectedIndex < 0) + { + _logger.LogTrace("Запись не выбрана, создаём новую"); + CreateRecord(); + return; + } + + // Сохраняем индекс удалённый записи и находим её в базе данных + var deletedMenuIndex = MenuSelectedIndex; + var deletedRecordId = SelectedId; + var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId); + // Создаём новую запись + CreateRecord(); + + // Удаляем запись из списка + MenuItems.RemoveAt(deletedMenuIndex); + // Если удалённая запись не найдена в базе данных, то логгируем как ошибку + if (deletedRecord is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords)); + return; + } + + // Удаляем запись из базы данных и сохраняем изменения в базе данных + _context.SeparatorRecords.Remove(deletedRecord); + _context.SaveChanges(); + _logger.LogTrace("Запись удалена"); + + // Отписываемся от события закрытия экрана подтверждения + confirmViewModel.Closes -= OnConfirmCloses; + } + + // Создаём View экрана подтверждения + var confirmView = _viewLocator.Build(confirmViewModel); + // Если View не найден, то логгируем как ошибку + if (confirmView is null) + { + _logger.LogError("Не удалось найти View для экрана подтверждения"); + return; + } + + _logger.LogTrace("Задаём вопрос"); + // Подписываемся на событие закрытия экрана подтверждения + confirmViewModel.Closes += OnConfirmCloses; + // Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий + _navigation.NavigateTo(confirmView); + } + catch (Exception ex) + { + _logger.LogError(ex, "Исключение при удалении записи"); } } /// - /// Вызывается при изменении .
- /// Изменяет изображением из . + /// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи ///
- partial void OnEditingRecordChanged(SeparatorRecord value) - { - - var imagePath = _imageStorage.GetFullPath(value.Image, "calibration-sample"); - if (!File.Exists(imagePath)) - { - Image = null; - return; - } - - if (Image is null) - Image = new AsyncImageRecordViewModel(imagePath); - else - Image.Path = imagePath; - } - - // Просто команды управления меню - [RelayCommand] private void OpenMenu() => MenuOpened = true; - [RelayCommand] private void CloseMenu() => MenuOpened = false; - [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; - - // Команда создания новой записи - // вызывает цепочку действий по изменению EditingRecord и Images - [RelayCommand] - private void CreateRecord() - { - // Если уже создана новая запись, то SelectedRecord == null и - // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, - // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, - // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged - if (SelectedRecord is not null) - SelectedRecord = null; - else - OnSelectedRecordChanged(null); - } - - // Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных - [RelayCommand] - private void DeleteRecord() - { - var confirmViewModel = new DeleteConfirmViewModel(_navigation) - { - Question = "Вы уверены, что хотиите удаить запись?" - }; - if (SelectedRecord is null) - { - CreateRecord(); - return; - } - confirmViewModel.Closes += (_, ea) => - { - if (ea != true) - return; - - // Сохраняем текущую запись - var deletedRecord = SelectedRecord; - // Создаём новую запись - CreateRecord(); - - // Если запись на была сохранена в базу данных, то ничего не делаем - if (deletedRecord is null) - return; - - // Если запись была в без данных, то - // она была и в Records, удаляем её из Records - Records.Remove(deletedRecord); - // Удаляем запись из базы данных и сохраняем изменения - // TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления, - // но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, - // тогда удалённые записи будут заполнять диск - // TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища - _context.SeparatorRecords.Remove(deletedRecord); - _context.SaveChanges(); - }; - - var convfirmView = _viewLocator.Build(confirmViewModel); - if (convfirmView is not null) - _navigation.NavigateTo(convfirmView); - } - - // // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной [RelayCommand] private void SaveRecord() { - // Если это новая запись - if (SelectedRecord is null) + try { - // Копируем её - var newRecord = CopyRecord(EditingRecord); - - // Для каждого вектора - for (int i = 0; i < newRecord.VectorRecords.Count(); i++) + _logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType); + // Если запись не выбрана + if (MenuSelectedIndex < 0) { - var vector = newRecord.VectorRecords[i]; + // Создаём запись изображения + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: _selectedImageData ?? ""); + // Добавляем запись изображения в базу данных + var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); - // Добавляем вектор в базу - var attachedRecord = _context.VectorRecords.Add(vector); - // Удаляем отвязанный от базы данных вектор из записи - newRecord.VectorRecords.RemoveAt(i); - // Добавляем привязанный к базе данных вектор в запись - newRecord.VectorRecords.Insert(i, attachedRecord.Entity); - } - - { - // То добавляем запись в базу + _logger.LogTrace("Создаём новую запись"); + // Заполняем запись + var newRecord = new SeparatorRecord( + uuid: SelectedUuid, + type: SelectedType, + imageId: attachedImageRecord.Entity.Id, + featureIds: [] + ); + // Добавляем запись в базу данных var attachedRecord = _context.SeparatorRecords.Add(newRecord); - // Заменяем текущую запись на привязанную к базе данных - newRecord = attachedRecord.Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись сохранена"); + + _logger.LogTrace("Создаём новые записи признаков"); + // Заполняем записи признаков и добавляем их в базу данных + var attachedFeatureRecords = new List(); + foreach (var feature in _selectedFeaturesData) + { + var featureRecord = new FeatureRecord( + values: feature, + sampleRecordId: attachedRecord.Entity.Id + ); + var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord); + attachedFeatureRecords.Add(attachedFeatureRecord.Entity); + } + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Записи признаков сохранены"); + + _logger.LogTrace("Заполняем признаки в сохранённой записи"); + // Добавляем идентификаторы признаков в запись + attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id)); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Сохранение записи завершено"); + + // Добавляем новый элемент в меню + MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.Type)); + // Выбираем последний элемент из меню + MenuSelectedIndex = MenuItems.Count() - 1; } - - // Сохраняем изменения в базе данных - _context.SaveChanges(); - // Добавляем запись в список - Records.Add(newRecord); - // Выбираем запись - SelectedRecord = newRecord; - } - // Если эта запись есть в базе - else - { - // Переносим данные из редактируемой записи в выбранную - SelectedRecord.Type = EditingRecord.Type; - SelectedRecord.Batch = EditingRecord.Batch; - SelectedRecord.Image = EditingRecord.Image; - SelectedRecord.VectorRecords = EditingRecord.VectorRecords; - - // Для каждого вектора в выбранной записи - for (int i = 0; i < SelectedRecord.VectorRecords.Count(); i++) + // Если запись выбрана + else { - var vector = SelectedRecord.VectorRecords[i]; - // Если вектор есть в базе данных, то пропускаем его - if (_context.Entry(vector).State is not EntityState.Detached) - continue; + // Находим запись в базе данных + var attachedRecord = _context.SeparatorRecords.Find(SelectedId); + // Если запись не найдена, то логгируем как ошибку + if (attachedRecord is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords)); + return; + } - // Если вектора нет в базе данных, то добавляем его в базу - var attachedRecord = _context.VectorRecords.Add(vector); - // Удаляем отвязанный от базы данных вектор из записи - SelectedRecord.VectorRecords.RemoveAt(i); - // Добавляем привязанный вектор к записи - SelectedRecord.VectorRecords.Insert(i, attachedRecord.Entity); + _logger.LogTrace("Записываем изменения в базу данных"); + + // Ищем запись изображения в базе данных + var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId); + // Если запись не найдена + if (attachedImageRecord is null) + { + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords)); + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: _selectedImageData ?? ""); + // Добавляем запись изображения в базу данных + attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + } + + // Ищем записи признаков в базе данных + var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SeparatorRecordId == SelectedId); + // Если количество признаков в базе данных больше чем текущее + if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count()) + { + // Находим количество признаков которое нужно удалить + var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count(); + // Логгируем + _logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count()); + // Удаляем необходимое количество признаков из конца + attachedFeatureRecords.TakeLast(featuresRecordsToRemove); + } + + // Заполняем все имеющиеся признаки в базе данных новыми значениями + foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate()) + attachedFeatureRecord.Values = _selectedFeaturesData[i]; + + // Если количество признаков в безе данных меньше чем текущее + if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count()) + { + // Находим количество признаков которое нужно добавить + var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count(); + // Логгируем + _logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count()); + // Заполняем записи недостающих признаков и добавляем их в базу данных + for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++) + { + var featureRecord = new FeatureRecord( + values: _selectedFeaturesData[i], + sampleRecordId: attachedRecord.Id + ); + var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord); + attachedFeatureRecords.Append(attachedFeatureRecord.Entity); + } + } + + _context.SaveChanges(); + _logger.LogTrace("Записи признаков сохранена"); + + // Если в данных есть изменения, то записываем их + if (attachedRecord.Type != SelectedType) + attachedRecord.Type = SelectedType; + if (attachedRecord.ImageId != attachedImageRecord.Id) + attachedRecord.ImageId = attachedImageRecord.Id; + // Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее + attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList(); + + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись сохранена"); + + // Записываем изменения в меню + _logger.LogTrace("Записываем изменения в меню"); + MenuItems[MenuSelectedIndex].Type = SelectedType; } - - // Сохраняем изменения в базе данных - _context.SaveChanges(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Исключение при сохранении записи"); } } - // Команда изменения основной секции (информация <-> изображения) - [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; - - // Команда навигации по информации - [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; - [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; - - // Команда съёмки изображений + /// + /// Снять изображения + /// [RelayCommand] private void CaptureImage() { - // Если съёмка уже начата, то ничего не делаем - if (CaptureImagesTask is not null) + // Если съёмка уже начата, то логгируем как ошибку + if (_сaptureImagesTask is not null) + { + _logger.LogError("Задача съёмки уже запущена"); return; + } // Создаём токен отмены задачи _captureImagesTaskCts = new CancellationTokenSource(); @@ -291,7 +544,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}"); // Запускаем получение изображений асинхронно - CaptureImagesTask = Task.Run(async () => + _сaptureImagesTask = Task.Run(async () => { // Список полученных изображений var imagesData = new List>(); @@ -306,7 +559,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase { // Распаковываем данные var (i, visible, uv365, uv254, image) = data; - // Выбрасываем исключение если задача отменена + // Выбрасываем исключение, если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Обновляем данные в интерфейсе @@ -314,97 +567,171 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); + // Создаём задачу сохранения изображения var imagesDataTask = Task.Run(() => { - // Сохраняем png изображение во временную папку - var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png"); + // Выбрасываем исключение, если задача отменена + cancellationToken.ThrowIfCancellationRequested(); + // Сохраняем PNG изображение во временную папку + var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-separator-{visible}-{uv365}-{uv254}.png"); image.ImWrite(path); // Освобождаем память изображения - image.Release(); + image.Dispose(); // Добавляем данные изображения в список return new ImageData(path, visible, uv365, uv254); - }); + }, cancellationToken); + // Добавляем задачу сохранения изображения в список imagesData.Add(imagesDataTask); }, cancellationToken); - // Обрабатываем изображения - var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => + // Создаём задачу обработки изображений, + // эта задача начнётся только после того как завершится + // задача съёмки изображений и все задачи сохранения изображений + var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => { + // Выбрасываем исключение, если задача отменена + cancellationToken.ThrowIfCancellationRequested(); + // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); - // Проверяем исключения задачи получения изображений - if (t.Exception is not null) + + // Если во время выполнения задачи съёмки изображений произошло исключение, + // то логгируем все исключения и выходим + if (captureTask.Exception is not null) { - _logger.LogError(t.Exception, "Исключение во время получения изображений"); - foreach (var ex in t.Exception.InnerExceptions ?? []) - _logger.LogError(ex, "Исключение во время получения изображений"); + _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); + foreach (var ex in captureTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время получения изображения"); return; } - // Проверяем исключения задач сохранения изображений + + // Если во время выполнения задач сохранения изображений произошло исключение, + // то логгируем все исключения и выходим + bool anyImageDataException = false; foreach (var imageData in imagesData) + { if (imageData.Exception is not null) { - _logger.LogError(imageData.Exception, "Исключение во время получения изображения"); + _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); foreach (var ex in imageData.Exception.InnerExceptions ?? []) - _logger.LogError(ex, "Исключение во время получения изображения"); - return; + _logger.LogError(ex, "Исключение во время сохранения изображения"); + anyImageDataException = true; } + } + if (anyImageDataException) + return; + + // Если во время выполнения произошло исключение, + // то логгируем все исключения и выходим + if (t.Exception is not null) + { + _logger.LogError(t.Exception, "Исключение во время обработки изображений"); + foreach (var ex in t.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } // Вычисляем вектор признаков var result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result)); - // Если вектор признаков вычислен - if (result is not null) + // Если вектор признаков не вычислен, то логгируем как ошибку + if (result is null) { - // Распаковываем результат - var (avgImage, roi, features) = result.Value; - _logger.LogInformation($"Длинна вектора признаков: {features.Count()}"); - // Обрабатываем среднее изображение - avgImage = new OpenCvSharp.Mat(avgImage, roi); - OpenCvSharp.Cv2.Resize( - avgImage, - avgImage, - new OpenCvSharp.Size( - AnalyzerService.ANALYSIS_IMAGE_SIZE, - AnalyzerService.ANALYSIS_IMAGE_SIZE - ) - ); - - // Кодируем и сохраняем среднее изображение - OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); - string avgPath; - using (var stream = new MemoryStream(avgData)) - avgPath = await _imageStorage.SaveAsync(stream, ".png", "calibration-sample"); - - // Заполняем поля в редактируемой записи - EditingRecord.Image = avgPath; - EditingRecord.VectorRecords = features.Select(f => new VectorRecord { Values = f }).ToList(); - - // Обновляем данные в редактируемой записи - // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, - // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан - _ = Dispatcher.UIThread.InvokeAsync(() => OnEditingRecordChanged(EditingRecord)); - } - else - { - _logger.LogError("Ошибка при вычислении векторов признаков"); + _logger.LogError("Не удалось вычислить вектор признаков"); + return; } + + // Распаковываем результат + var (avgImage, roi, features) = result.Value; + + // Сохраняем вектор признаков + _selectedFeaturesData = features; + + // Обрабатываем среднее изображение + avgImage = new OpenCvSharp.Mat(avgImage, roi); + OpenCvSharp.Cv2.Resize( + avgImage, + avgImage, + new OpenCvSharp.Size( + AnalyzerService.ANALYSIS_IMAGE_SIZE, + AnalyzerService.ANALYSIS_IMAGE_SIZE + ) + ); + + // Кодируем среднее изображение + OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); + _selectedImageData = Convert.ToBase64String(avgData); + + // Очищаем память среднего изображения + avgImage.Dispose(); + + // Обновляем данные в интерфейсе + // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан + _ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData); }, cancellationToken); - // Сбрасываем состояние задачи съёмки изображений - _ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t => + // Создаём задачу которая выполнится после завершения всех других + _ = Task.WhenAll([captureTask, .. imagesData, processTask]).ContinueWith(t => { + // Если во время выполнения задачи съёмки изображений произошло исключение, + // то логгируем все исключения и выходим + if (captureTask.Exception is not null) + { + _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); + foreach (var ex in captureTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время получения изображения"); + return; + } + + // Если во время выполнения задач сохранения изображений произошло исключение, + // то логгируем все исключения и выходим + bool anyImageDataException = false; + foreach (var imageData in imagesData) + { + if (imageData.Exception is not null) + { + _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); + foreach (var ex in imageData.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время сохранения изображения"); + anyImageDataException = true; + } + } + if (anyImageDataException) + return; + + // Если во время выполнения задачи обработки изображений произошло исключение, + // то логгируем все исключения и выходим + if (processTask.Exception is not null) + { + _logger.LogError(processTask.Exception, "Исключение во время обработки изображений"); + foreach (var ex in processTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } + + // Если во время выполнения произошло исключение, + // то логгируем все исключения и выходим + if (t.Exception is not null) + { + _logger.LogError(t.Exception, "Исключение во время обработки изображений"); + foreach (var ex in t.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } + // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { + // Сбрасываем состояние задачи съёмки изображений ImagesIsCapturing = false; ImagesCapturingProgress = ""; - CaptureImagesTask = null; + _сaptureImagesTask = null; }); }); }); @@ -412,34 +739,4 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase // Команда отмены задачи съёмки изображений [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); - - /// - /// Копирует , отвязывая её от базы данных - /// - /// Скопированная - private static SeparatorRecord CopyRecord(SeparatorRecord source) - { - return new SeparatorRecord - { - Id = source.Id, - Type = source.Type, - Batch = source.Batch, - Image = source.Image, - VectorRecords = source.VectorRecords - .Select(CopyRecord) - .ToList() - }; - } - /// - /// Копирует , отвязывая её от базы данных - /// - /// Скопированная - private static VectorRecord CopyRecord(VectorRecord source) - { - return new VectorRecord - { - Id = source.Id, - Values = (float[])source.Values.Clone() - }; - } } diff --git a/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml b/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml index 8afe7c2..aca0287 100644 --- a/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml +++ b/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml @@ -1,205 +1,264 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:vm="using:GSS2.ViewModels.AmineContent" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns="https://github.com/avaloniaui" +> - + + + + + + + + + 5 - + - - - - - + 5 - + Orientation="Vertical" + > - - - - - - - - - - - + + 5 + - + + + + + + + + + 5 + + + + + + + + + + 5 + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + - + + 5 + - + + + + + + - + + - + - - + - - - - - + + - + + + + + + - - + + + + + + + + - + + - + + + + + + + + + + + + - - @@ -208,22 +267,37 @@ - - - - - - - - - + + + + + + + + - + - + - -