forked from amkovkov/GranuSightSoftware2
789 lines
39 KiB
C#
789 lines
39 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.Results;
|
|
using GSS2.Core.Extensions;
|
|
using GSS2.UI.Core.ViewModels;
|
|
|
|
using LiveChartsCore;
|
|
using LiveChartsCore.SkiaSharpView;
|
|
using LiveChartsCore.SkiaSharpView.Painting;
|
|
|
|
using SkiaSharp;
|
|
using LiveChartsCore.SkiaSharpView.Painting.Effects;
|
|
using GSS2.UI.Core.Services;
|
|
|
|
namespace GSS2.ViewModels.AmineContent;
|
|
|
|
public partial class AnalysisViewModel : ViewModelBase
|
|
{
|
|
public event EventHandler? UpdateCharts;
|
|
|
|
private readonly ILogger<AnalysisViewModel> _logger;
|
|
private readonly ResultsContext _context;
|
|
private readonly ImageCapturerService _imageCapturer;
|
|
private readonly ImageStorageService _imageStorage;
|
|
private readonly AnalyzerService _analyzer;
|
|
private readonly DependencyInjectionViewLocator _viewLocator;
|
|
private readonly NavigationService _navigation;
|
|
|
|
private CancellationTokenSource? _captureImagesTaskCts = null;
|
|
|
|
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
|
[ObservableProperty] public partial ObservableCollection<ResultRecord> Records { get; set; }
|
|
|
|
[ObservableProperty] public partial ResultRecord? SelectedRecord { get; set; }
|
|
[ObservableProperty] public partial ResultRecord 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;
|
|
[ObservableProperty] public partial AsyncImageRecordViewModel? MaskImage { get; set; } = null;
|
|
[ObservableProperty] public partial AsyncImageRecordViewModel? ResultImage { get; set; } = null;
|
|
|
|
[ObservableProperty] public partial bool ImageIsVisible { get; set; } = true;
|
|
[ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false;
|
|
[ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false;
|
|
|
|
[ObservableProperty] public partial float ImagesZoom { get; set; } = 1;
|
|
[ObservableProperty] public partial float ImagesPanX { get; set; } = 0;
|
|
[ObservableProperty] public partial float ImagesPanY { get; set; } = 0;
|
|
|
|
[ObservableProperty] public partial SolidColorPaint LegendTextPaint { get; set; } = new SolidColorPaint(SKColors.White);
|
|
|
|
[ObservableProperty] public partial ISeries[] MeanValuesHistogram { get; set; } = [];
|
|
[ObservableProperty] public partial Axis[] MeanValuesXAxis { get; set; } = [new Axis()];
|
|
[ObservableProperty] public partial Axis[] MeanValuesYAxis { get; set; } = [new Axis()];
|
|
|
|
[ObservableProperty] public partial ISeries[] VariancesHistogram { get; set; } = [];
|
|
[ObservableProperty] public partial Axis[] VariancesXAxis { get; set; } = [new Axis()];
|
|
[ObservableProperty] public partial Axis[] VariancesYAxis { get; set; } = [new Axis()];
|
|
|
|
public AnalysisViewModel(
|
|
ILogger<AnalysisViewModel> logger,
|
|
ResultsContext context,
|
|
ImageCapturerService imageCapturer,
|
|
ImageStorageService imageStorage,
|
|
AnalyzerService analyzer,
|
|
DependencyInjectionViewLocator viewLocator,
|
|
NavigationService navigation
|
|
)
|
|
{
|
|
_logger = logger;
|
|
_context = context;
|
|
_imageCapturer = imageCapturer;
|
|
_imageStorage = imageStorage;
|
|
_analyzer = analyzer;
|
|
_viewLocator = viewLocator;
|
|
_navigation = navigation;
|
|
_logger.LogInformation("Инициализация");
|
|
|
|
// Получаем записи из базы данных
|
|
Records = new ObservableCollection<ResultRecord>(_context.ResultRecords);
|
|
|
|
// Создаём пустые записи
|
|
SelectedRecord = null;
|
|
// На самом деле заполнять EditingRecord излишне, потому что при изменении SelectedRecord
|
|
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
|
EditingRecord = new()
|
|
{
|
|
SampleName = "",
|
|
SampleComment = "",
|
|
SamplingDateTime = DateTime.UtcNow,
|
|
SamplingPlace = "",
|
|
Image = "",
|
|
SeparatorType = "",
|
|
SampleBrand = "",
|
|
MaskImage = "",
|
|
ResultImage = "",
|
|
MinValue = -1,
|
|
MaxValue = -1,
|
|
ParticlesCount = -1,
|
|
MeanValue = -1,
|
|
MeanVariance = -1,
|
|
VarianceOfMean = -1,
|
|
MeanValues = [],
|
|
Variances = []
|
|
};
|
|
|
|
_logger.LogInformation("Инициализировано");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
|
|
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
partial void OnSelectedRecordChanged(ResultRecord? value)
|
|
{
|
|
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
|
// то создаём пустую запись
|
|
if (SelectedRecord is null)
|
|
{
|
|
EditingRecord = new()
|
|
{
|
|
SampleName = "",
|
|
SampleComment = "",
|
|
SamplingDateTime = DateTime.UtcNow,
|
|
SamplingPlace = "",
|
|
Image = "",
|
|
SeparatorType = "",
|
|
SampleBrand = "",
|
|
MaskImage = "",
|
|
ResultImage = "",
|
|
MinValue = -1,
|
|
MaxValue = -1,
|
|
ParticlesCount = -1,
|
|
MeanValue = -1,
|
|
MeanVariance = -1,
|
|
VarianceOfMean = -1,
|
|
MeanValues = [],
|
|
Variances = []
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Иначе копируем запись, отвязывая её от базы данных
|
|
EditingRecord = CopyRecord(SelectedRecord);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
|
/// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
partial void OnEditingRecordChanged(ResultRecord value)
|
|
{
|
|
var avgImagePath = _imageStorage.GetFullPath(value.Image, "result-avg");
|
|
if (File.Exists(avgImagePath))
|
|
{
|
|
if (Image is null)
|
|
Image = new AsyncImageRecordViewModel(avgImagePath);
|
|
else
|
|
Image.Path = avgImagePath;
|
|
}
|
|
else
|
|
Image = null;
|
|
|
|
var maskImagePath = _imageStorage.GetFullPath(value.MaskImage, "result-mask");
|
|
if (File.Exists(maskImagePath))
|
|
{
|
|
if (MaskImage is null)
|
|
MaskImage = new AsyncImageRecordViewModel(maskImagePath);
|
|
else
|
|
MaskImage.Path = maskImagePath;
|
|
}
|
|
else
|
|
MaskImage = null;
|
|
|
|
var resultImagePath = _imageStorage.GetFullPath(value.ResultImage, "result");
|
|
if (File.Exists(resultImagePath))
|
|
{
|
|
if (ResultImage is null)
|
|
ResultImage = new AsyncImageRecordViewModel(resultImagePath);
|
|
else
|
|
ResultImage.Path = resultImagePath;
|
|
}
|
|
else
|
|
ResultImage = null;
|
|
|
|
try
|
|
{
|
|
(MeanValuesHistogram, MeanValuesXAxis, MeanValuesYAxis) = BuildHistogram(
|
|
value.MeanValues,
|
|
0,
|
|
Math.Max(value.MeanValues.Count() > 0 ? value.MeanValues.Max() : 0, 1),
|
|
leftText: "Количество частиц",
|
|
bottomText: "Среднее знечение"
|
|
);
|
|
(VariancesHistogram, VariancesXAxis, VariancesYAxis) = BuildHistogram(
|
|
value.Variances,
|
|
0,
|
|
Math.Max(value.Variances.Count() > 0 ? value.Variances.Max() : 0, 1),
|
|
leftText: "Количество частиц",
|
|
bottomText: "Дисперсия"
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "");
|
|
}
|
|
|
|
}
|
|
|
|
// Просто команды управления меню
|
|
[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.ResultRecords.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);
|
|
|
|
// Если записи нет в базе данных
|
|
if (_context.Entry(newRecord).State is EntityState.Detached)
|
|
{
|
|
// То добавляем её в базу
|
|
var attachedRecord = _context.ResultRecords.Add(newRecord);
|
|
// Заменяем текущую запись на привязанную к базе данных
|
|
newRecord = attachedRecord.Entity;
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
// Добавляем запись в список
|
|
Records.Add(newRecord);
|
|
// Выбираем запись
|
|
SelectedRecord = newRecord;
|
|
}
|
|
// Если эта запись есть в базе
|
|
else
|
|
{
|
|
// Переносим данные из редактируемой записи в выбранную
|
|
SelectedRecord.SampleName = EditingRecord.SampleName;
|
|
SelectedRecord.SampleComment = EditingRecord.SampleComment;
|
|
SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime;
|
|
SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace;
|
|
SelectedRecord.Image = EditingRecord.Image;
|
|
SelectedRecord.SeparatorType = EditingRecord.SeparatorType;
|
|
SelectedRecord.SampleBrand = EditingRecord.SampleBrand;
|
|
SelectedRecord.MaskImage = EditingRecord.MaskImage;
|
|
SelectedRecord.ResultImage = EditingRecord.ResultImage;
|
|
SelectedRecord.MeanValue = EditingRecord.MeanValue;
|
|
SelectedRecord.MeanVariance = EditingRecord.MeanVariance;
|
|
SelectedRecord.VarianceOfMean = EditingRecord.VarianceOfMean;
|
|
|
|
// Если записи нет в базе данных
|
|
if (_context.Entry(SelectedRecord).State is EntityState.Detached)
|
|
{
|
|
// То добавляем её в базу
|
|
var attachedRecord = _context.ResultRecords.Add(SelectedRecord);
|
|
// Находим текущую запись в списке
|
|
var index = Records.IndexOf(SelectedRecord);
|
|
// Заменяем её в списке на привязанную к базе данных
|
|
Records[index] = attachedRecord.Entity;
|
|
// Заменяем выбранную запись на привязанную
|
|
SelectedRecord = attachedRecord.Entity;
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
}
|
|
}
|
|
|
|
// Команда изменения основной секции (информация <-> изображения)
|
|
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
|
|
|
// Команда навигации по информации
|
|
[RelayCommand]
|
|
private void InfoSectionPrevious()
|
|
{
|
|
InfoSectionCurrentIndex--;
|
|
UpdateCharts?.Invoke(this, new EventArgs());
|
|
}
|
|
[RelayCommand]
|
|
private void InfoSectionNext()
|
|
{
|
|
InfoSectionCurrentIndex++;
|
|
UpdateCharts?.Invoke(this, new EventArgs());
|
|
}
|
|
|
|
// Команда съёмки изображений
|
|
[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-result-{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;
|
|
}
|
|
|
|
// Инициализируем анализатор
|
|
if (!_analyzer.Initialized)
|
|
{
|
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей");
|
|
await _analyzer.Initialize();
|
|
}
|
|
// Инициализируем анализатор
|
|
if (!_analyzer.Initialized)
|
|
{
|
|
_logger.LogError("Ошибка при инициализации анализатора");
|
|
return;
|
|
}
|
|
|
|
// Обновляем данные в редактируемой записи
|
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Анализ");
|
|
|
|
// Проводим анализ
|
|
var result = await _analyzer.Analyze(imagesData.Select(i => i.Result));
|
|
// Если анализ проведён
|
|
if (result is not null)
|
|
{
|
|
_logger.LogInformation("Анализ завершён");
|
|
// Распаковываем результат
|
|
var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value;
|
|
|
|
try
|
|
{
|
|
_logger.LogInformation("Анализ завершён [2]");
|
|
// Вычисляем минимальное и максимальное значение
|
|
OpenCvSharp.Cv2.MinMaxLoc(resultImage, out double minValue, out double maxValue, out _, out _, mask);
|
|
// Вычисляем среднее значение
|
|
var meanValue = OpenCvSharp.Cv2.Mean(resultImage, mask).Val0;
|
|
_logger.LogInformation("Миниальное значение: {}, Максимальное значние: {}", minValue, maxValue);
|
|
_logger.LogInformation("Колличество конутров: {}, Среднее значние: {}", contours.Count(), meanValue);
|
|
|
|
_logger.LogInformation("Анализ завершён [3]");
|
|
// Вычисляем среднее и дисперсию каждого контура
|
|
var means = new List<double>();
|
|
var variances = new List<double>();
|
|
|
|
// Создаём временную маску для каждого контура
|
|
var contourMask = new OpenCvSharp.Mat(
|
|
new OpenCvSharp.Size(
|
|
AnalyzerService.ANALYSIS_IMAGE_SIZE,
|
|
AnalyzerService.ANALYSIS_IMAGE_SIZE
|
|
),
|
|
OpenCvSharp.MatType.CV_8UC1,
|
|
new OpenCvSharp.Scalar(0)
|
|
);
|
|
|
|
// Проходим по каждому контуру
|
|
foreach (var contour in contours)
|
|
{
|
|
// Обнуляем маску
|
|
contourMask.SetTo(new OpenCvSharp.Scalar(0));
|
|
// Заполняем контур на маске
|
|
OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1);
|
|
|
|
// Вычисляем среднее и среднеквадратическое отклонение
|
|
OpenCvSharp.Cv2.MeanStdDev(resultImage, out var mean, out var stdDev, contourMask);
|
|
// Добавляем значения в лист
|
|
means.Add(mean.Val0);
|
|
variances.Add(Math.Pow(stdDev.Val0, 2));
|
|
}
|
|
contourMask.Release();
|
|
|
|
_logger.LogInformation("Анализ завершён [4]");
|
|
// Вычисляем среднюю дисперсию и дисперсию среднего
|
|
double meanVariance = variances.Average();
|
|
double varianceOfMean = means.Variance();
|
|
_logger.LogInformation("Средняя дивергенция: {}, Дивергенция среднего: {}", meanVariance, varianceOfMean);
|
|
|
|
_logger.LogInformation("Анализ завершён [5]");
|
|
// Кодируем и сохраняем среднее изображение
|
|
OpenCvSharp.Cv2.DrawContours(avgImage, contours, -1, OpenCvSharp.Scalar.Red, 1, OpenCvSharp.LineTypes.Link8);
|
|
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
|
|
string avgPath;
|
|
using (var stream = new MemoryStream(avgData))
|
|
avgPath = await _imageStorage.SaveAsync(stream, ".png", "result-avg");
|
|
|
|
// Освобождаем память среднего изображения
|
|
avgImage.Release();
|
|
avgData = null;
|
|
|
|
_logger.LogInformation("Анализ завершён [6]");
|
|
// Кодируем и сохраняем маску
|
|
OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData);
|
|
string maskPath;
|
|
using (var stream = new MemoryStream(maskData))
|
|
maskPath = await _imageStorage.SaveAsync(stream, ".png", "result-mask");
|
|
|
|
// Освобождаем память маски
|
|
mask.Release();
|
|
maskData = null;
|
|
|
|
_logger.LogInformation("Анализ завершён [7]");
|
|
// Преобразуем изображение (0...maxValue) -> (0...255)
|
|
resultImage.ConvertTo(resultImage, OpenCvSharp.MatType.CV_8UC1, 255 / (maxValue - 0), 255 * 0 / (maxValue - 0));
|
|
|
|
// Кодируем и сохраняем изображение результата
|
|
OpenCvSharp.Cv2.ImEncode(".png", resultImage, out var resultData);
|
|
string resultPath;
|
|
using (var stream = new MemoryStream(resultData))
|
|
resultPath = await _imageStorage.SaveAsync(stream, ".png", "result");
|
|
|
|
// Освобождаем память изображения результата
|
|
resultImage.Release();
|
|
resultData = null;
|
|
|
|
_logger.LogInformation("Анализ завершён [8]");
|
|
// Заполняем поля в редактируемой записи
|
|
EditingRecord.Image = avgPath;
|
|
EditingRecord.SeparatorType = separatorType;
|
|
EditingRecord.SampleBrand = sampleBrand;
|
|
EditingRecord.MaskImage = maskPath;
|
|
EditingRecord.ResultImage = resultPath;
|
|
EditingRecord.MinValue = minValue;
|
|
EditingRecord.MaxValue = maxValue;
|
|
EditingRecord.ParticlesCount = contours.Count();
|
|
EditingRecord.MeanValue = meanValue;
|
|
EditingRecord.MeanVariance = meanVariance;
|
|
EditingRecord.VarianceOfMean = varianceOfMean;
|
|
EditingRecord.MeanValues = means.ToArray();
|
|
EditingRecord.Variances = variances.ToArray();
|
|
|
|
// Обновляем данные в редактируемой записи
|
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Исключение во время анализа");
|
|
}
|
|
|
|
}
|
|
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();
|
|
|
|
// Команды переключения изображений
|
|
[RelayCommand]
|
|
private void SwitchToImage()
|
|
{
|
|
ImageIsVisible = true;
|
|
MaskImageIsVisible = false;
|
|
ResultImageIsVisible = false;
|
|
}
|
|
[RelayCommand]
|
|
private void SwitchToMaskImage()
|
|
{
|
|
ImageIsVisible = false;
|
|
MaskImageIsVisible = true;
|
|
ResultImageIsVisible = false;
|
|
}
|
|
[RelayCommand]
|
|
private void SwitchToResultImage()
|
|
{
|
|
ImageIsVisible = false;
|
|
MaskImageIsVisible = false;
|
|
ResultImageIsVisible = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Копирует <see cref="ResultRecord"/>, отвязывая её от базы данных
|
|
/// </summary>
|
|
/// <returns>Скопированная <see cref="ResultRecord"/></returns>
|
|
private static ResultRecord CopyRecord(ResultRecord source)
|
|
{
|
|
return new ResultRecord
|
|
{
|
|
Id = source.Id,
|
|
SampleName = source.SampleName,
|
|
SampleComment = source.SampleComment,
|
|
SamplingDateTime = source.SamplingDateTime,
|
|
SamplingPlace = source.SamplingPlace,
|
|
Image = source.Image,
|
|
SeparatorType = source.SeparatorType,
|
|
SampleBrand = source.SampleBrand,
|
|
MaskImage = source.MaskImage,
|
|
ResultImage = source.ResultImage,
|
|
MinValue = source.MinValue,
|
|
MaxValue = source.MaxValue,
|
|
ParticlesCount = source.ParticlesCount,
|
|
MeanValue = source.MeanValue,
|
|
MeanVariance = source.MeanVariance,
|
|
VarianceOfMean = source.VarianceOfMean,
|
|
MeanValues = source.MeanValues,
|
|
Variances = source.Variances
|
|
};
|
|
}
|
|
|
|
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) EmptyHistogram()
|
|
{
|
|
return (Array.Empty<ISeries>(), [new Axis()], [new Axis()]);
|
|
}
|
|
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable<float> values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "")
|
|
{
|
|
if (values == null || values.Count() == 0)
|
|
return EmptyHistogram();
|
|
|
|
var vals = values.Select(v => (double)v).ToArray();
|
|
double minVal = min ?? vals.Min();
|
|
double maxVal = max ?? vals.Max();
|
|
int binCount = 20;
|
|
|
|
double binSize = (maxVal - minVal) / binCount;
|
|
var binsPDF = new int[binCount];
|
|
var binsCDF = new float[binCount];
|
|
|
|
foreach (var v in vals)
|
|
{
|
|
int index = (int)((v - minVal) / binSize);
|
|
if (index >= binCount)
|
|
index = binCount - 1;
|
|
if (index < 0)
|
|
index = 0;
|
|
binsPDF[index]++;
|
|
for (int i = index; i < binCount; i++)
|
|
binsCDF[i] += 100.0f / values.Count();
|
|
}
|
|
|
|
// Подписи для оси X
|
|
var labels = new string[binCount];
|
|
for (int i = 0; i < binCount; i++)
|
|
{
|
|
double from = minVal + i * binSize;
|
|
double to = from + binSize;
|
|
labels[i] = $"{from:F2}";
|
|
}
|
|
|
|
return
|
|
(
|
|
new ISeries[]
|
|
{
|
|
new ColumnSeries<int>
|
|
{
|
|
Values = binsPDF,
|
|
Name = barLegend,
|
|
Fill = new SolidColorPaint(SKColors.White),
|
|
ScalesYAt = 0
|
|
},
|
|
new LineSeries<float>
|
|
{
|
|
Values = binsCDF,
|
|
Name = lineLegend,
|
|
Fill = new SolidColorPaint(SKColors.Transparent),
|
|
Stroke = new SolidColorPaint(SKColors.Red),
|
|
GeometryFill = new SolidColorPaint(SKColors.Red),
|
|
GeometryStroke = new SolidColorPaint(SKColors.Red),
|
|
GeometrySize = 8,
|
|
ScalesYAt = 1
|
|
}
|
|
},
|
|
new Axis[]
|
|
{
|
|
new Axis
|
|
{
|
|
Labels = labels,
|
|
LabelsRotation = 45,
|
|
TextSize = 14,
|
|
Name = bottomText,
|
|
NameTextSize = 14,
|
|
NamePaint=new SolidColorPaint(SKColors.White),
|
|
SeparatorsPaint = new SolidColorPaint(SKColors.White),
|
|
LabelsPaint = new SolidColorPaint(SKColors.White),
|
|
TicksPaint = new SolidColorPaint(SKColors.White),
|
|
SubticksPaint = new SolidColorPaint(SKColors.White)
|
|
}
|
|
},
|
|
new Axis[]
|
|
{
|
|
new Axis
|
|
{
|
|
TextSize = 14,
|
|
MinLimit = 0,
|
|
Name=leftText,
|
|
NameTextSize = 14,
|
|
NamePaint=new SolidColorPaint(SKColors.White),
|
|
Position = LiveChartsCore.Measure.AxisPosition.Start,
|
|
SeparatorsPaint = new SolidColorPaint(SKColors.White),
|
|
LabelsPaint = new SolidColorPaint(SKColors.White),
|
|
TicksPaint = new SolidColorPaint(SKColors.White),
|
|
SubticksPaint = new SolidColorPaint(SKColors.White)
|
|
},
|
|
new Axis
|
|
{
|
|
TextSize = 14,
|
|
MinLimit = 0f,
|
|
MaxLimit = 100f,
|
|
Name=rightText,
|
|
NameTextSize = 14,
|
|
NamePaint=new SolidColorPaint(SKColors.White),
|
|
Position = LiveChartsCore.Measure.AxisPosition.End,
|
|
SeparatorsPaint = new SolidColorPaint(SKColors.White)
|
|
{
|
|
PathEffect = new DashEffect([4, 4])
|
|
},
|
|
LabelsPaint = new SolidColorPaint(SKColors.White),
|
|
TicksPaint = new SolidColorPaint(SKColors.White),
|
|
SubticksPaint = new SolidColorPaint(SKColors.White)
|
|
}
|
|
}
|
|
);
|
|
}
|
|
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable<double> values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") =>
|
|
BuildHistogram(values.Select(v => (float)v), min, max, leftText, rightText, bottomText, barLegend, lineLegend);
|
|
}
|