forked from amkovkov/GranuSightSoftware2
446 lines
23 KiB
C#
446 lines
23 KiB
C#
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;
|
|
using GSS2.Core.Analysis.AmineContent;
|
|
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
|
using GSS2.UI.Core.ViewModels;
|
|
using GSS2.UI.Core.Services;
|
|
|
|
namespace GSS2.ViewModels.AmineContent;
|
|
|
|
public partial class SeparatorCalibrationViewModel : ViewModelBase
|
|
{
|
|
private readonly ILogger<SeparatorCalibrationViewModel> _logger;
|
|
private readonly CalibrationContext _context;
|
|
private readonly ImageCapturerService _imageCapturer;
|
|
private readonly ImageStorageService _imageStorage;
|
|
private readonly DependencyInjectionViewLocator _viewLocator;
|
|
private readonly NavigationService _navigation;
|
|
|
|
private CancellationTokenSource? _captureImagesTaskCts = null;
|
|
|
|
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
|
[ObservableProperty] public partial ObservableCollection<SeparatorRecord> Records { get; set; }
|
|
|
|
[ObservableProperty] public partial SeparatorRecord? SelectedRecord { get; set; }
|
|
[ObservableProperty] public partial SeparatorRecord EditingRecord { get; set; }
|
|
|
|
[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 ImagesIsCapturing { get; set; } = false;
|
|
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
|
|
|
[ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null;
|
|
|
|
|
|
public SeparatorCalibrationViewModel(
|
|
ILogger<SeparatorCalibrationViewModel> logger,
|
|
CalibrationContext context,
|
|
ImageCapturerService imageCapturer,
|
|
ImageStorageService imageStorage,
|
|
DependencyInjectionViewLocator viewLocator,
|
|
NavigationService navigation
|
|
)
|
|
{
|
|
_logger = logger;
|
|
_context = context;
|
|
_imageCapturer = imageCapturer;
|
|
_imageStorage = imageStorage;
|
|
_viewLocator = viewLocator;
|
|
_navigation = navigation;
|
|
_logger.LogInformation("Инициализация");
|
|
|
|
// Получаем записи из базы данных
|
|
Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords);
|
|
|
|
// Берём первую запись
|
|
SelectedRecord = Records.FirstOrDefault();
|
|
// Если записей нет, то создаём пустую
|
|
// на самом деле это излишне, потому что при изменении SelectedRecord
|
|
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
|
if (SelectedRecord is null)
|
|
EditingRecord = new()
|
|
{
|
|
Type = "",
|
|
Batch = "",
|
|
Image = "",
|
|
VectorRecords = new()
|
|
};
|
|
else
|
|
EditingRecord = CopyRecord(SelectedRecord);
|
|
|
|
_logger.LogInformation("Инициализировано");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
|
|
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
partial void OnSelectedRecordChanged(SeparatorRecord? value)
|
|
{
|
|
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
|
// то создаём пустую запись
|
|
if (SelectedRecord is null)
|
|
{
|
|
EditingRecord = new()
|
|
{
|
|
Type = "",
|
|
Batch = "",
|
|
Image = "",
|
|
VectorRecords = new()
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Иначе копируем запись, отвязывая её от базы данных
|
|
EditingRecord = CopyRecord(SelectedRecord);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
|
/// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
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)
|
|
{
|
|
// Копируем её
|
|
var newRecord = CopyRecord(EditingRecord);
|
|
|
|
// Для каждого вектора
|
|
for (int i = 0; i < newRecord.VectorRecords.Count(); i++)
|
|
{
|
|
var vector = newRecord.VectorRecords[i];
|
|
|
|
// Добавляем вектор в базу
|
|
var attachedRecord = _context.VectorRecords.Add(vector);
|
|
// Удаляем отвязанный от базы данных вектор из записи
|
|
newRecord.VectorRecords.RemoveAt(i);
|
|
// Добавляем привязанный к базе данных вектор в запись
|
|
newRecord.VectorRecords.Insert(i, attachedRecord.Entity);
|
|
}
|
|
|
|
{
|
|
// То добавляем запись в базу
|
|
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
|
|
// Заменяем текущую запись на привязанную к базе данных
|
|
newRecord = attachedRecord.Entity;
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_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++)
|
|
{
|
|
var vector = SelectedRecord.VectorRecords[i];
|
|
// Если вектор есть в базе данных, то пропускаем его
|
|
if (_context.Entry(vector).State is not EntityState.Detached)
|
|
continue;
|
|
|
|
// Если вектора нет в базе данных, то добавляем его в базу
|
|
var attachedRecord = _context.VectorRecords.Add(vector);
|
|
// Удаляем отвязанный от базы данных вектор из записи
|
|
SelectedRecord.VectorRecords.RemoveAt(i);
|
|
// Добавляем привязанный вектор к записи
|
|
SelectedRecord.VectorRecords.Insert(i, attachedRecord.Entity);
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
}
|
|
}
|
|
|
|
// Команда изменения основной секции (информация <-> изображения)
|
|
[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)
|
|
return;
|
|
|
|
// Создаём токен отмены задачи
|
|
_captureImagesTaskCts = new CancellationTokenSource();
|
|
var cancellationToken = _captureImagesTaskCts.Token;
|
|
|
|
// Обновляем данные в интерфейсе
|
|
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
|
|
|
// Запускаем получение изображений асинхронно
|
|
CaptureImagesTask = 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(() =>
|
|
{
|
|
// Сохраняем png изображение во временную папку
|
|
var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png");
|
|
image.ImWrite(path);
|
|
|
|
// Освобождаем память изображения
|
|
image.Release();
|
|
|
|
// Добавляем данные изображения в список
|
|
return new ImageData(path, visible, uv365, uv254);
|
|
});
|
|
imagesData.Add(imagesDataTask);
|
|
}, cancellationToken);
|
|
|
|
// Обрабатываем изображения
|
|
var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
|
|
{
|
|
// Обновляем данные в интерфейсе
|
|
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
|
|
// Проверяем исключения задачи получения изображений
|
|
if (t.Exception is not null)
|
|
{
|
|
_logger.LogError(t.Exception, "Исключение во время получения изображений");
|
|
foreach (var ex in t.Exception.InnerExceptions ?? [])
|
|
_logger.LogError(ex, "Исключение во время получения изображений");
|
|
return;
|
|
}
|
|
// Проверяем исключения задач сохранения изображений
|
|
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, "Исключение во время получения изображения");
|
|
return;
|
|
}
|
|
|
|
// Вычисляем вектор признаков
|
|
var result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result));
|
|
// Если вектор признаков вычислен
|
|
if (result is not 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("Ошибка при вычислении векторов признаков");
|
|
}
|
|
}, cancellationToken);
|
|
|
|
// Сбрасываем состояние задачи съёмки изображений
|
|
_ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t =>
|
|
{
|
|
// Обновляем данные в интерфейсе
|
|
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ImagesIsCapturing = false;
|
|
ImagesCapturingProgress = "";
|
|
CaptureImagesTask = null;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Команда отмены задачи съёмки изображений
|
|
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
|
|
|
/// <summary>
|
|
/// Копирует <see cref="SeparatorRecord"/>, отвязывая её от базы данных
|
|
/// </summary>
|
|
/// <returns>Скопированная <see cref="SeparatorRecord"/></returns>
|
|
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()
|
|
};
|
|
}
|
|
/// <summary>
|
|
/// Копирует <see cref="VectorRecord"/>, отвязывая её от базы данных
|
|
/// </summary>
|
|
/// <returns>Скопированная <see cref="VectorRecord"/></returns>
|
|
private static VectorRecord CopyRecord(VectorRecord source)
|
|
{
|
|
return new VectorRecord
|
|
{
|
|
Id = source.Id,
|
|
Values = (float[])source.Values.Clone()
|
|
};
|
|
}
|
|
}
|