Files
GSS2Rework/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs
T
2026-06-18 15:28:48 +03:00

743 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Data;
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GSS2.Core.Analysis.AmineContent;
using GSS2.Core.Analysis.AmineContent.Database;
using GSS2.Core.Extensions;
using GSS2.UI.Core.ViewModels;
using GSS2.UI.Core.Services;
namespace GSS2.ViewModels.AmineContent;
public partial class SeparatorCalibrationViewModel : ViewModelBase
{
/// <summary>
/// Внутренняя модель элементов меню
/// </summary>
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;
}
}
/// <summary>
/// Логгер
/// </summary>
private readonly ILogger<SeparatorCalibrationViewModel> _logger;
/// <summary>
/// Контекст базы данных
/// </summary>
private readonly Context _context;
/// <summary>
/// Сервис для получения изображений с камеры
/// </summary>
private readonly IImageCapturerService _imageCapturer;
/// <summary>
/// ViewLocator для получения других View по их моделям
/// </summary>
private readonly DependencyInjectionViewLocator _viewLocator;
/// <summary>
/// Сервис навигации между экранами
/// </summary>
private readonly NavigationService _navigation;
/// <summary>
/// Задача съёмки изображений
/// </summary>
private Task? _сaptureImagesTask = null;
/// <summary>
/// Токен отмены задачи съёмки изображений
/// </summary>
private CancellationTokenSource? _captureImagesTaskCts = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
private string? _selectedImageData = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.FeatureIds"/> выбранного элемента
/// </summary>
private List<float[]> _selectedFeaturesData = new();
/// <summary>
/// Меню открыто
/// </summary>
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
/// <summary>
/// Элементы меню
/// </summary>
[ObservableProperty] public partial ObservableCollection<MenuItemViewModel> MenuItems { get; set; } = new();
/// <summary>
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
/// </summary>
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary>
/// <see cref="SeparatorRecord.Id"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
/// <summary>
/// <see cref="SeparatorRecord.Uuid"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SeparatorRecord.Type"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedType { get; set; } = "";
/// <summary>
/// Представление <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
/// <summary>
/// Текущий экран главной секции
/// </summary>
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
/// <summary>
/// Можно ли переходить на следующий экран главной секции
/// </summary>
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
/// <summary>
/// Можно ли переходить на предыдущий экран главной секции
/// </summary>
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false;
/// <summary>
/// Происходит ли переход между экранами главной секции
/// </summary>
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
/// <summary>
/// Проводится ли съёмка изображений
/// </summary>
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
/// <summary>
/// Прогресс съёмки изображений
/// </summary>
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
/// <summary>
/// Можно ли производить перемещение между экранами
/// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; }
public SeparatorCalibrationViewModel(
ILogger<SeparatorCalibrationViewModel> logger,
Context context,
IImageCapturerService imageCapturer,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
)
{
// Устанавливаем readonly поля
_logger = logger;
_context = context;
_imageCapturer = imageCapturer;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация");
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SeparatorRecords.Select(r => new MenuItemViewModel(r.Id, r.Type)));
if (MenuItems.Count() > 0) // Если записи есть,
MenuSelectedIndex = 0; // Выбираем первую запись
else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling))
UpdateCanScroll();
};
_logger.LogInformation("Инициализировано");
}
/// <summary>
/// Вызывается при изменении <see cref="MenuSelectedIndex"/>
/// </summary>
partial void OnMenuSelectedIndexChanged(int value)
{
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
if (value >= MenuItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count());
// Если значение больше или равно 0, то значит выбран один из элементов меню
if (value >= 0 && value < MenuItems.Count())
{
_logger.LogTrace("Выбран элемент из списка: {}", value);
// Находим запись в базе данных
var selectedRecord = _context.SeparatorRecords.Find(MenuItems[value].Id);
// Если запись найдена, то заполняем поля из базы данных
if (selectedRecord is not null)
{
// Заполняем простые поля
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;
}
/// <summary>
/// Обновить значение <see cref="CanScroll"/>
/// </summary>
private void UpdateCanScroll()
{
CanScroll = !MenuOpened;
CanScroll &= !MainSectionIsScrolling;
CanScroll &= !ImagesIsCapturing;
}
// Команды управления меню
/// <summary>
/// Открыть меню
/// </summary>
[RelayCommand] private void OpenMenu() => MenuOpened = true;
/// <summary>
/// Закрыть меню
/// </summary>
[RelayCommand] private void CloseMenu() => MenuOpened = false;
/// <summary>
/// Переключить видимость меню
/// </summary>
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
/// <summary>
/// Переключить главную секцию информация <-> изображения
/// </summary>
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
/// <summary>
/// Создать новую запись
/// </summary>
[RelayCommand]
private void CreateRecord()
{
_logger.LogInformation("Создание новой записи");
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
if (MenuSelectedIndex != -1)
MenuSelectedIndex = -1;
else
OnMenuSelectedIndexChanged(-1);
}
/// <summary>
/// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
/// </summary>
[RelayCommand]
private void DeleteRecord()
{
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try
{
_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, "Исключение при удалении записи");
}
}
/// <summary>
/// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
/// </summary>
[RelayCommand]
private void SaveRecord()
{
try
{
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
// Если запись не выбрана
if (MenuSelectedIndex < 0)
{
// Создаём запись изображения
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
_logger.LogTrace("Создаём новую запись");
// Заполняем запись
var newRecord = new SeparatorRecord(
uuid: SelectedUuid,
type: SelectedType,
imageId: attachedImageRecord.Entity.Id,
featureIds: []
);
// Добавляем запись в базу данных
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись сохранена");
_logger.LogTrace("Создаём новые записи признаков");
// Заполняем записи признаков и добавляем их в базу данных
var attachedFeatureRecords = new List<FeatureRecord>();
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;
}
// Если запись выбрана
else
{
// Находим запись в базе данных
var attachedRecord = _context.SeparatorRecords.Find(SelectedId);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords));
return;
}
_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;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение при сохранении записи");
}
}
/// <summary>
/// Снять изображения
/// </summary>
[RelayCommand]
private void CaptureImage()
{
// Если съёмка уже начата, то логгируем как ошибку
if (_сaptureImagesTask is not null)
{
_logger.LogError("Задача съёмки уже запущена");
return;
}
// Создаём токен отмены задачи
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем получение изображений асинхронно
_сaptureImagesTask = Task.Run(async () =>
{
// Список полученных изображений
var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
// Получаем изображения
var captureTask = _imageCapturer.CaptureImages(async (data) =>
{
// Распаковываем данные
var (i, visible, uv365, uv254, image) = data;
// Выбрасываем исключение, если задача отменена
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Создаём задачу сохранения изображения
var imagesDataTask = Task.Run(() =>
{
// Выбрасываем исключение, если задача отменена
cancellationToken.ThrowIfCancellationRequested();
// Сохраняем PNG изображение во временную папку
var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-separator-{visible}-{uv365}-{uv254}.png");
image.ImWrite(path);
// Освобождаем память изображения
image.Dispose();
// Добавляем данные изображения в список
return new ImageData(path, visible, uv365, uv254);
}, cancellationToken);
// Добавляем задачу сохранения изображения в список
imagesData.Add(imagesDataTask);
}, cancellationToken);
// Создаём задачу обработки изображений,
// эта задача начнётся только после того как завершится
// задача съёмки изображений и все задачи сохранения изображений
var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{
// Выбрасываем исключение, если задача отменена
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
// Если во время выполнения задачи съёмки изображений произошло исключение,
// то логгируем все исключения и выходим
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 (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 null)
{
_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, 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 = "";
_сaptureImagesTask = null;
});
});
});
}
// Команда отмены задачи съёмки изображений
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
}