Files
GSS2Rework/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs
T

2061 lines
113 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;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SkiaSharp;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.Painting.Effects;
using UnitsNet;
using MathNet.Numerics.Statistics;
using GSS2.Core.Hardware;
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 AnalysisViewModel : ViewModelBase
{
/// <summary>
/// Внутренняя модель элементов меню
/// </summary>
public partial class MenuItemViewModel : ViewModelBase
{
[ObservableProperty] public partial int Id { get; set; }
[ObservableProperty] public partial string SampleName { get; set; }
public MenuItemViewModel(int id, string sampleName)
{
Id = id;
SampleName = sampleName;
}
}
/// <summary>
/// Внутренняя модель элементов выпадающего списка сепараторов
/// </summary>
public partial class SeparatorComboBoxItemViewModel : ViewModelBase
{
[ObservableProperty] public partial int Id { get; set; }
[ObservableProperty] public partial string Type { get; set; }
public SeparatorComboBoxItemViewModel(int id, string type)
{
Id = id;
Type = type;
}
}
/// <summary>
/// Внутренняя модель элементов выпадающего списка марок
/// </summary>
public partial class BrandComboBoxItemViewModel : ViewModelBase
{
[ObservableProperty] public partial int Id { get; set; }
[ObservableProperty] public partial string BrandName { get; set; }
[ObservableProperty] public partial string MixtureName { get; set; }
public BrandComboBoxItemViewModel(int id, string brandName, string mixtureName)
{
Id = id;
BrandName = brandName;
MixtureName = mixtureName;
}
}
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
public const double UNPROCESSED_THRESHOLD = 0.6;
public const double HOTSPOT_THRESHOLD = 1.1;
public const double EPSILON = 1e-10;
public const double UNPROCESSED_FRACTION_THRESHOLD = 50;
/// <summary>
/// Модель гистограммы
/// </summary>
public partial class HistogramViewModel : ViewModelBase
{
/// <summary>
/// Оси абсцисс
/// </summary>
[ObservableProperty] public partial Axis[] XAxis { get; set; } = [new Axis()];
/// <summary>
/// Оси ординат
/// </summary>
[ObservableProperty] public partial Axis[] YAxis { get; set; } = [new Axis()];
/// <summary>
/// Серии (значения)
/// </summary>
[ObservableProperty] public partial ISeries[] Series { get; set; } = [];
/// <summary>
/// Очистить гистограмму
/// </summary>
public void Clear()
{
XAxis = [new Axis()];
YAxis = [new Axis()];
Series = Array.Empty<ISeries>();
}
/// <summary>
/// Заполнить гистограмму
/// </summary>
/// <param name="values">Значения</param>
/// <param name="min">Левая граница оси абсцисс</param>
/// <param name="max">Правая граница оси абсцисс</param>
/// <param name="leftText">Текст слева</param>
/// <param name="bottomText">Текст снизу</param>
/// <param name="legend">Текст легенды</param>
public void Set(IEnumerable<float> values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "")
{
if (values is null || values.Count() == 0)
{
Clear();
return;
}
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}";
}
Series =
[
new ColumnSeries<int>
{
Values = binsPDF,
Name = legend,
Fill = new SolidColorPaint(SKColors.White),
ScalesYAt = 0
}
];
XAxis =
[
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)
}
];
YAxis =
[
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)
}
];
}
/// <summary>
/// Заполнить гистограмму
/// </summary>
/// <param name="values">Значения</param>
/// <param name="min">Левая граница оси абсцисс</param>
/// <param name="max">Правая граница оси абсцисс</param>
/// <param name="leftText">Текст слева</param>
/// <param name="bottomText">Текст снизу</param>
/// <param name="legend">Текст легенды</param>
public void Set(IEnumerable<double> values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "") =>
Set(values.Select(v => (float)v), min, max, leftText, bottomText, legend);
}
#region Основные свойства
/// <summary>
/// <see cref="ResultRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="ResultRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="ResultRecord.SampleName"/>
/// </summary>
[ObservableProperty] public partial string SampleName { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.SampleComment"/>
/// </summary>
[ObservableProperty] public partial string SampleComment { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.SamplingDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="ResultRecord.SamplingPlace"/>
/// </summary>
[ObservableProperty] public partial string SamplingPlace { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.SeparatorType"/>
/// </summary>
[ObservableProperty] public partial string SeparatorType { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.BrandName"/>
/// </summary>
[ObservableProperty] public partial string BrandName { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.MixtureName"/>
/// </summary>
[ObservableProperty] public partial string MixtureName { get; set; } = "";
/// <summary>
/// <see cref="ResultRecord.MixtureNormalRate"/>
/// </summary>
[ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1;
/// <summary>
/// Представление <see cref="ResultRecord.ImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.MaskImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel MaskImage { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.ResultImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ResultImage { get; set; } = new();
/// <summary>
/// <see cref="ResultRecord.ParticlesCount"/>
/// </summary>
[ObservableProperty] public partial int ParticlesCount { get; set; } = -1;
/// <summary>
/// Данные <see cref="ResultRecord.ParticlesValues"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Values { get; set; } = new();
/// <summary>
/// Данные <see cref="ResultRecord.ParticlesXs"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Xs { get; set; } = new();
/// <summary>
/// Данные <see cref="ResultRecord.ParticlesYs"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Ys { get; set; } = new();
#endregion
#region Общие свойства
/// <summary>
/// Средняя нормированная обработанная площадь, %
/// </summary>
[ObservableProperty] public partial double MeanProcessedArea { get; set; } = new();
/// <summary>
/// Средняя нормированная площадь хотспотов, %
/// </summary>
[ObservableProperty] public partial double MeanHotspotArea { get; set; } = new();
/// <summary>
/// Средний интегральный оптический показатель
/// </summary>
[ObservableProperty] public partial double MeanOpticalIntegral { get; set; } = new();
/// <summary>
/// Средняя внутригранульная неоднородность
/// </summary>
[ObservableProperty] public partial double MeanInnerHeterogeneity { get; set; } = new();
/// <summary>
/// Среднее внутригранульное среднеквадратичное отклонение
/// </summary>
[ObservableProperty] public partial double MeanInnerStdDev { get; set; } = new();
/// <summary>
/// 50-й процентиль нормированной обработанной площади (медиана), %
/// </summary>
[ObservableProperty] public partial double ProcessedAreaP50 { get; set; } = new();
/// <summary>
/// 10-й процентиль нормированной обработанной площади, %
/// </summary>
[ObservableProperty] public partial double ProcessedAreaP10 { get; set; } = new();
/// <summary>
/// 90-й процентиль нормированной обработанной площади, %
/// </summary>
[ObservableProperty] public partial double ProcessedAreaP90 { get; set; } = new();
/// <summary>
/// Ширина центрального диапазона распределения нормированной обработанной площади, п.п.
/// </summary>
[ObservableProperty] public partial double ProcessedAreaDeltaP90P10 { get; set; } = new();
/// <summary>
/// Среднее квадратичное отклонение нормированной обработанной площади
/// </summary>
[ObservableProperty] public partial double ProcessedAreaStdDev { get; set; } = new();
/// <summary>
/// Межгранульная неоднородность
/// </summary>
[ObservableProperty] public partial double InterHeterogeneity { get; set; } = new();
/// <summary>
/// Доля необработанных гранул, тех гранул нормированная НЕобработанная площадь которых больше <see cref="UNPROCESSED_FRACTION_THRESHOLD"/> %
/// </summary>
[ObservableProperty] public partial double UnprocessedFraction { get; set; } = new();
/// <summary>
/// Нормализованные значения, где <br/>
/// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до <see cref="UNPROCESSED_THRESHOLD"/> * x) <br/>
/// от 1 до 2 - обработанный пиксель (в изначальных значениях от <see cref="UNPROCESSED_THRESHOLD"/> * x до <see cref="HOTSPOT_THRESHOLD"/> * x)<br/>
/// от 2 до 3 - хотспот (в изначальных значениях от <see cref="HOTSPOT_THRESHOLD"/> * x до максимального значения)
/// </summary>
public List<double[]> NormalizedValues { get; set; } = new();
/// <summary>
/// Нормированные обработанные площади
/// </summary>
public List<double> ProcessedAreas { get; set; } = new();
/// <summary>
/// Нормированные необработанные площади
/// </summary>
public List<double> UnprocessedAreas { get; set; } = new();
/// <summary>
/// Нормированные площади хотспотов
/// </summary>
public List<double> HotspotAreas { get; set; } = new();
/// <summary>
/// Интегральные оптические показатели
/// </summary>
public List<double> OpticalIntegrals { get; set; } = new();
/// <summary>
/// Внутригранульные среднеквадратичные отклонения
/// </summary>
public List<double> InnerStdDevs { get; set; } = new();
/// <summary>
/// Внутригранульные неоднородности
/// </summary>
public List<double> InnerHeterogeneities { get; set; } = new();
/// <summary>
/// Гистограмма нормированных обработанных площадей
/// </summary>
[ObservableProperty] public partial HistogramViewModel ProcessedAreasHistogram { get; set; } = new();
/// <summary>
/// Гистограмма нормированных необработанных площадей
/// </summary>
[ObservableProperty] public partial HistogramViewModel UnprocessedAreasHistogram { get; set; } = new();
/// <summary>
/// Гистограмма нормированных площадей хотспотов
/// </summary>
[ObservableProperty] public partial HistogramViewModel HotspotAreasHistogram { get; set; } = new();
/// <summary>
/// Гистограмма интегральных оптических показателей
/// </summary>
[ObservableProperty] public partial HistogramViewModel OpticalIntegralsHistogram { get; set; } = new();
/// <summary>
/// Гистограмма внутригранульных среднеквадратичных отклонений
/// </summary>
[ObservableProperty] public partial HistogramViewModel InnerStdDevsHistogram { get; set; } = new();
/// <summary>
/// Гистограмма внутригранульных неоднородностей
/// </summary>
[ObservableProperty] public partial HistogramViewModel InnerHeterogeneitiesHistogram { get; set; } = new();
#endregion
#region Свойства отдельных гранул
/// <summary>
/// Индекс выбранной гранулы
/// </summary>
[ObservableProperty] public partial int ParticleIndex { get; set; } = 0;
/// <summary>
/// Выбрано ли изображение <see cref="ParticleImage"/>
/// </summary>
[ObservableProperty] public partial bool ParticleImageIsVisible { get; set; } = true;
/// <summary>
/// Выбрано ли изображение <see cref="ParticleMaskImage"/>
/// </summary>
[ObservableProperty] public partial bool ParticleMaskImageIsVisible { get; set; } = false;
/// <summary>
/// Выбрано ли изображение <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial bool ParticleResultImageIsVisible { get; set; } = false;
/// <summary>
/// Масштаб изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial float ParticleImagesZoom { get; set; } = 1;
/// <summary>
/// Смещение по X изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial float ParticleImagesPanX { get; set; } = 0;
/// <summary>
/// Смещение по Y изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial float ParticleImagesPanY { get; set; } = 0;
/// <summary>
/// Ширина изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial Rect ParticleImagesBounds { get; set; } = new Rect();
/// <summary>
/// <summary>
/// Нормированная обработанная площадь выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleProcessedArea { get; set; } = new();
/// <summary>
/// Нормированная необработанная площадь выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleUnprocessedArea { get; set; } = new();
/// <summary>
/// Нормированная площадь хотспотов выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleHotspotArea { get; set; } = new();
/// <summary>
/// Интегральный оптический показатель выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleOpticalIntegral { get; set; } = new();
/// <summary>
/// Внутригранульное среднеквадратичное отклонение выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleInnerStdDev { get; set; } = new();
/// <summary>
/// Внутригранульная неоднородность выбранной гранулы
/// </summary>
[ObservableProperty] public partial double ParticleInnerHeterogeneity { get; set; } = new();
/// <summary>
/// Гистограмма нормированных значений выбранной гранулы, где <br/>
/// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до <see cref="UNPROCESSED_THRESHOLD"/> * x) <br/>
/// от 1 до 2 - обработанный пиксель (в изначальных значениях от <see cref="UNPROCESSED_THRESHOLD"/> * x до <see cref="HOTSPOT_THRESHOLD"/> * x)<br/>
/// от 2 до 3 - хотспот (в изначальных значениях от <see cref="HOTSPOT_THRESHOLD"/> * x до максимального значения)
/// </summary>
[ObservableProperty] public partial HistogramViewModel ParticleValuesHistogram { get; set; } = new();
#endregion
/// <summary>
/// Пересчитать вычисляемые свойства
/// </summary>
public void CalculateProperties()
{
// Очищаем списки
NormalizedValues.Clear();
ProcessedAreas.Clear();
UnprocessedAreas.Clear();
HotspotAreas.Clear();
OpticalIntegrals.Clear();
InnerStdDevs.Clear();
InnerHeterogeneities.Clear();
if (Values.Count() == 0)
{
MeanProcessedArea = -1;
ProcessedAreaP10 = -1;
ProcessedAreaP50 = -1;
ProcessedAreaP90 = -1;
ProcessedAreaStdDev = -1;
InterHeterogeneity = -1;
ProcessedAreaDeltaP90P10 = -1;
UnprocessedFraction = -1;
MeanInnerHeterogeneity = -1;
MeanInnerStdDev = -1;
MeanHotspotArea = -1;
MeanOpticalIntegral = -1;
ProcessedAreasHistogram.Clear();
UnprocessedAreasHistogram.Clear();
HotspotAreasHistogram.Clear();
OpticalIntegralsHistogram.Clear();
InnerStdDevsHistogram.Clear();
InnerHeterogeneitiesHistogram.Clear();
ParticleIndex = 0;
return;
}
// Для каждой гранулы
var max = Values.Max(vs => vs.Max());
foreach (var value in Values)
{
// Находим нормализованное значение
var normalized = new double[value.Count()];
for (int i = 0; i < value.Length; i++)
if (value[i] <= MixtureNormalRate * UNPROCESSED_THRESHOLD)
normalized[i] = value[i] / (MixtureNormalRate * UNPROCESSED_THRESHOLD);
else if (value[i] <= MixtureNormalRate * HOTSPOT_THRESHOLD)
normalized[i] = 1 + (value[i] - (MixtureNormalRate * UNPROCESSED_THRESHOLD)) / ((MixtureNormalRate * HOTSPOT_THRESHOLD) - (MixtureNormalRate * UNPROCESSED_THRESHOLD));
else
normalized[i] = 2 + (value[i] - (MixtureNormalRate * HOTSPOT_THRESHOLD)) / (max - (MixtureNormalRate * HOTSPOT_THRESHOLD));
// Вычисляем значения
var area = normalized.Count();
var processedArea = normalized.Count(v => v > 1) * 100.0 / area;
var unprocessedArea = normalized.Count(v => v <= 1) * 100.0 / area;
var hotspotArea = normalized.Count(v => v > 2) * 100.0 / area;
var opticalIntegral = normalized.Sum() * 100.0 / area;
var innerStdDev = normalized.StdDev();
var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON);
// Добавляем значения в списки
NormalizedValues.Add(normalized);
ProcessedAreas.Add(processedArea);
UnprocessedAreas.Add(unprocessedArea);
HotspotAreas.Add(hotspotArea);
OpticalIntegrals.Add(opticalIntegral);
InnerStdDevs.Add(innerStdDev);
InnerHeterogeneities.Add(innerHeterogeneity);
}
// Вычисляем общие значения
MeanProcessedArea = ProcessedAreas.Mean();
ProcessedAreaP10 = ProcessedAreas.Percentile(10);
ProcessedAreaP50 = ProcessedAreas.Percentile(50);
ProcessedAreaP90 = ProcessedAreas.Percentile(90);
ProcessedAreaStdDev = ProcessedAreas.StdDev();
InterHeterogeneity = ProcessedAreaStdDev / MeanProcessedArea * 100;
ProcessedAreaDeltaP90P10 = ProcessedAreaP90 - ProcessedAreaP10;
UnprocessedFraction = (double)UnprocessedAreas.Count(v => v > UNPROCESSED_FRACTION_THRESHOLD) / UnprocessedAreas.Count() * 100;
MeanInnerHeterogeneity = InnerHeterogeneities.Mean();
MeanInnerStdDev = InnerStdDevs.Mean();
MeanHotspotArea = HotspotAreas.Mean();
MeanOpticalIntegral = OpticalIntegrals.Mean();
// Обновляем гистограммы
ProcessedAreasHistogram.Set(ProcessedAreas, 0, 100, "Количество гранул, шт", "Нормированная обработанная площадь, %");
UnprocessedAreasHistogram.Set(UnprocessedAreas, 0, 100, "Количество гранул, шт", "Нормированная необработанная площадь, %");
HotspotAreasHistogram.Set(HotspotAreas, 0, 100, "Количество гранул, шт", "Нормированная \"горячая\" площадь, %");
OpticalIntegralsHistogram.Set(OpticalIntegrals, 0, 3, "Количество гранул, шт", "Оптический интегральный показатель, %");
InnerStdDevsHistogram.Set(InnerStdDevs, InnerStdDevs.Min(), InnerStdDevs.Max(), "Количество гранул, шт", "Внутригранульное среднеквадратичное отклонение");
InnerHeterogeneitiesHistogram.Set(InnerHeterogeneities, InnerHeterogeneities.Min(), InnerHeterogeneities.Max(), "Количество гранул, шт", "Внутригранульная однородность");
// Сбрасываем индекс выбранной гранулы
ParticleIndex = 0;
ZoomPanOnParticle();
}
/// <summary>
/// Переключится на следующую гранулу
/// </summary>
[RelayCommand]
public void NextParticle()
{
if (ParticlesCount == 0)
return;
ParticleIndex = (ParticleIndex + 1) % ParticlesCount;
}
/// <summary>
/// Переключится на предыдущую гранулу
/// </summary>
[RelayCommand]
public void PreviousParticle()
{
if (ParticlesCount == 0)
return;
// ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount;
}
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.Image"/>
/// </summary>
[RelayCommand]
private void SwitchToImage()
{
ParticleImageIsVisible = true;
ParticleMaskImageIsVisible = false;
ParticleResultImageIsVisible = false;
}
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.MaskImage"/>
/// </summary>
[RelayCommand]
private void SwitchToMaskImage()
{
ParticleImageIsVisible = false;
ParticleMaskImageIsVisible = true;
ParticleResultImageIsVisible = false;
}
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[RelayCommand]
private void SwitchToResultImage()
{
ParticleImageIsVisible = false;
ParticleMaskImageIsVisible = false;
ParticleResultImageIsVisible = true;
}
/// <summary>
/// Вызывается при изменении <see cref="ParticleIndex"/>
/// </summary>
partial void OnParticleIndexChanged(int value)
{
// Если количество частиц меньше или равно 0
if (ParticlesCount <= 0)
{
// Сбрасываем общие значения
ParticleProcessedArea = -1;
ParticleUnprocessedArea = -1;
ParticleHotspotArea = -1;
ParticleOpticalIntegral = -1;
ParticleInnerStdDev = -1;
ParticleInnerHeterogeneity = -1;
ParticleValuesHistogram.Clear();
return;
}
// Заполняем общие значения из списков
ParticleProcessedArea = ProcessedAreas[value];
ParticleUnprocessedArea = UnprocessedAreas[value];
ParticleHotspotArea = HotspotAreas[value];
ParticleOpticalIntegral = OpticalIntegrals[value];
ParticleInnerStdDev = InnerStdDevs[value];
ParticleInnerHeterogeneity = InnerHeterogeneities[value];
// Обновляем гистограмму
ParticleValuesHistogram.Set(NormalizedValues[value], 0, 3, "Количество, шт", "Значение");
// Обновляем изображение
ZoomPanOnParticle();
}
partial void OnParticleImagesBoundsChanged(Rect value)
{
ZoomPanOnParticle();
}
private void ZoomPanOnParticle()
{
if (ParticleIndex < 0 ||
ParticleIndex >= Xs.Count() ||
ParticleIndex >= Ys.Count())
return;
if (ParticleImagesBounds.Width == 0 ||
ParticleImagesBounds.Height == 0)
return;
var minX = Xs[ParticleIndex].Min();
var maxX = Xs[ParticleIndex].Max();
var minY = Ys[ParticleIndex].Min();
var maxY = Ys[ParticleIndex].Max();
ParticleImagesZoom = MathF.Max((float)ParticleImagesBounds.Width, (float)ParticleImagesBounds.Height) * MathF.Max(maxX - minX, maxY - minY) / AnalyzerService.ANALYSIS_IMAGE_SIZE;
ParticleImagesPanX = -((maxX + minX) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Width * ParticleImagesZoom;
ParticleImagesPanY = -((maxY + minY) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Height * ParticleImagesZoom;
}
}
public event EventHandler? UpdateCharts;
/// <summary>
/// Логгер
/// </summary>
private readonly ILogger<AnalysisViewModel> _logger;
/// <summary>
/// Контекст базы данных
/// </summary>
private readonly Context _context;
/// <summary>
/// Сервис для получения изображений с камеры
/// </summary>
private readonly IImageCapturerService _imageCapturer;
/// <summary>
/// Сервис анализатора
/// </summary>
private readonly AnalyzerService _analyzer;
/// <summary>
/// Сервис управления сигнальными светодиодами
/// </summary>
private readonly LightsService _lights;
/// <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>
/// Меню открыто
/// </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>
/// Запись
/// </summary>
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary>
/// Выбрано ли изображение <see cref="RecordViewModel.Image"/>
/// </summary>
[ObservableProperty] public partial bool ImageIsVisible { get; set; } = true;
/// <summary>
/// Выбрано ли изображение <see cref="RecordViewModel.MaskImage"/>
/// </summary>
[ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false;
/// <summary>
/// Выбрано ли изображение <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false;
/// <summary>
/// Масштаб изображений <see cref="RecordViewModel.Image"/>, <see cref="RecordViewModel.MaskImage"/> и <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[ObservableProperty] public partial float ImagesZoom { get; set; } = 1;
/// <summary>
/// Смещение по X изображений <see cref="RecordViewModel.Image"/>, <see cref="RecordViewModel.MaskImage"/> и <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[ObservableProperty] public partial float ImagesPanX { get; set; } = 0;
/// <summary>
/// Смещение по Y изображений <see cref="RecordViewModel.Image"/>, <see cref="RecordViewModel.MaskImage"/> и <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[ObservableProperty] public partial float ImagesPanY { get; set; } = 0;
/// <summary>
/// Список элементов выпадающего списка сепараторов
/// </summary>
[ObservableProperty] public partial ObservableCollection<SeparatorComboBoxItemViewModel> SeparatorComboBoxItems { get; set; } = new();
/// <summary>
/// Текущий выбранный элемент выпадающего списка сепараторов. Равен -1 если элемент не выбран
/// </summary>
[ObservableProperty] public partial int SeparatorComboBoxSelectedIndex { get; set; } = -1;
/// <summary>
/// Список элементов выпадающего списка марок
/// </summary>
[ObservableProperty] public partial ObservableCollection<BrandComboBoxItemViewModel> BrandComboBoxItems { get; set; } = new();
/// <summary>
/// Текущий выбранный элемент выпадающего списка марок. Равен -1 если элемент не выбран
/// </summary>
[ObservableProperty] public partial int BrandComboBoxSelectedIndex { get; set; } = -1;
/// <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 int InfoSectionCurrentIndex { get; set; } = 0;
/// <summary>
/// Можно ли переходить на следующий экран информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
/// <summary>
/// Можно ли переходить на предыдущий экран информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
/// <summary>
/// Происходит ли переход между экранами информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionIsScrolling { 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 CanInteract { get; set; } = true;
/// <summary>
/// Создаётся ли новая запись
/// </summary>
[ObservableProperty] public partial bool IsNewRecord { get; set; } = true;
public AnalysisViewModel(
ILogger<AnalysisViewModel> logger,
Context context,
IImageCapturerService imageCapturer,
AnalyzerService analyzer,
LightsService lights,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
)
{
// Устанавливаем readonly поля
_logger = logger;
_context = context;
_imageCapturer = imageCapturer;
_analyzer = analyzer;
_lights = lights;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация");
// Получаем записи выпадающего списка сепараторов из базы данных
SeparatorComboBoxItems = new ObservableCollection<SeparatorComboBoxItemViewModel>(_context.SeparatorRecords.Select(r => new SeparatorComboBoxItemViewModel(r.Id, r.Type)));
// Получаем записи выпадающего списка марок из базы данных
BrandComboBoxItems = new ObservableCollection<BrandComboBoxItemViewModel>(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName)));
// Подписываемся на изменения базы данных для отслеживания изменений списка сепараторов и марок
_context.ChangeTracker.StateChanged += (_, ea) =>
{
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() =>
{
// Если запись является SeparatorRecord
if (ea.Entry.Entity is SeparatorRecord separatorRecord)
{
// Если запись добавлена
if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged)
{
// Добавляем запись
SeparatorComboBoxItems.Add(new SeparatorComboBoxItemViewModel(separatorRecord.Id, separatorRecord.Type));
}
// Если запись удалена
else if (ea.NewState is EntityState.Deleted)
{
// Ищем удалённую запись в выпадающем списке сепараторов
var deletedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id);
// Если запись не найдена в выпадающем списке марок, то логгируем как ошибку
if (deletedComboBoxItem is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems));
return;
}
// Берём индекс удалённого элемента в выпадающем списке марок
var deletedComboBoxIndex = SeparatorComboBoxItems.IndexOf(deletedComboBoxItem);
// Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран
if (SeparatorComboBoxSelectedIndex == deletedComboBoxIndex)
SeparatorComboBoxSelectedIndex = -1;
// Удаляем элемент из выпадающего списка марок
SeparatorComboBoxItems.RemoveAt(deletedComboBoxIndex);
}
// Если запись изменилась
else if (ea.NewState is EntityState.Modified)
{
// Ищем изменённую запись в выпадающем списке марок
var editedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id);
// Если запись не найдена в выпадающем списке марок, то логгируем как ошибку
if (editedComboBoxItem is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems));
return;
}
// Берём индекс изменённого элемента в выпадающем списке марок
var editedComboBoxIndex = SeparatorComboBoxItems.IndexOf(editedComboBoxItem);
// Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи
if (SeparatorComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord)
Record.SeparatorType = separatorRecord.Type;
// Обновляем элемент из выпадающего списка марок
editedComboBoxItem.Type = separatorRecord.Type;
}
}
// Если запись является BrandRecord
if (ea.Entry.Entity is BrandRecord brandRecord)
{
// Если запись добавлена
if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged)
{
// Добавляем запись
BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName));
}
// Если запись удалена
else if (ea.NewState is EntityState.Deleted)
{
// Ищем удалённую запись в выпадающем списке марок
var deletedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id);
// Если запись не найдена в выпадающем списке марок, то логгируем как ошибку
if (deletedComboBoxItem is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems));
return;
}
// Берём индекс удалённого элемента в выпадающем списке марок
var deletedComboBoxIndex = BrandComboBoxItems.IndexOf(deletedComboBoxItem);
// Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран
if (BrandComboBoxSelectedIndex == deletedComboBoxIndex)
BrandComboBoxSelectedIndex = -1;
// Удаляем элемент из выпадающего списка марок
BrandComboBoxItems.RemoveAt(deletedComboBoxIndex);
}
// Если запись изменилась
else if (ea.NewState is EntityState.Modified)
{
// Ищем изменённую запись в выпадающем списке марок
var editedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id);
// Если запись не найдена в выпадающем списке марок, то логгируем как ошибку
if (editedComboBoxItem is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems));
return;
}
// Берём индекс изменённого элемента в выпадающем списке марок
var editedComboBoxIndex = BrandComboBoxItems.IndexOf(editedComboBoxItem);
// Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи
if (BrandComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord)
{
Record.BrandName = brandRecord.BrandName;
Record.MixtureName = brandRecord.MixtureName;
Record.MixtureNormalRate = brandRecord.MixtureNormalRate;
}
// Обновляем элемент из выпадающего списка марок
editedComboBoxItem.BrandName = brandRecord.BrandName;
editedComboBoxItem.MixtureName = brandRecord.MixtureName;
}
}
});
};
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.ResultRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
if (MenuItems.Count() > 0) // Если записи есть,
MenuSelectedIndex = 0; // Выбираем первую запись
else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanInteract и обновления графиков
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
if (ea.PropertyName == nameof(MenuSelectedIndex) ||
ea.PropertyName == nameof(InfoSectionCurrentIndex))
UpdateCharts?.Invoke(this, EventArgs.Empty);
};
_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.ResultRecords.Find(MenuItems[value].Id);
// Если запись не найдена
if (selectedRecord is null)
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.ResultRecords));
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
MenuSelectedIndex = -1;
// Удаляем несуществующий элемент из списка
MenuItems.RemoveAt(nonExistingIndex);
return;
}
// Если запись найдена, то заполняем поля из базы данных
// Заполняем простые поля
Record.Id = selectedRecord.Id;
Record.Uuid = selectedRecord.Uuid;
Record.CreateDateTime = selectedRecord.CreateDateTime;
Record.EditDateTime = selectedRecord.EditDateTime;
Record.SampleName = selectedRecord.SampleName;
Record.SampleComment = selectedRecord.SampleComment;
Record.SamplingDateTime = selectedRecord.SamplingDateTime;
Record.SamplingPlace = selectedRecord.SamplingPlace;
Record.SeparatorType = selectedRecord.SeparatorType;
Record.BrandName = selectedRecord.BrandName;
Record.MixtureName = selectedRecord.MixtureName;
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
Record.ParticlesCount = selectedRecord.ParticlesCount;
Record.Values = selectedRecord.ValueData.Chunk(selectedRecord.ValueCount).Select(v => v.ToArray()).ToList();
Record.Xs = selectedRecord.XData.Chunk(selectedRecord.XCount).Select(v => v.ToArray()).ToList();
Record.Ys = selectedRecord.YData.Chunk(selectedRecord.YCount).Select(v => v.ToArray()).ToList();
// Ищем изображение в базе данных
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку
if (imageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение
Record.Image.Data = imageRecord?.Base64 ?? "";
// Ищем изображение маски в базе данных
var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку
if (maskImageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение
Record.MaskImage.Data = maskImageRecord?.Base64 ?? "";
// Ищем изображение результата в базе данных
var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку
if (resultImageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение
Record.ResultImage.Data = resultImageRecord?.Base64 ?? "";
// Сбрасываем приближение изображений
ImagesPanX = 0;
ImagesPanY = 0;
ImagesZoom = 1;
// Устанавливаем, что запись выбрана из базы данных
IsNewRecord = false;
// Вычисляем дополнительные свойства
Record.CalculateProperties();
return;
}
// Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент");
var dateTime = DateTime.UtcNow;
Record.Id = 0;
Record.Uuid = Guid.NewGuid().ToString();
Record.CreateDateTime = dateTime;
Record.EditDateTime = dateTime;
Record.SampleName = "";
Record.SampleComment = "";
Record.SamplingDateTime = dateTime;
Record.SamplingPlace = "";
Record.SeparatorType = "";
Record.BrandName = "";
Record.MixtureName = "";
Record.MixtureNormalRate = -1;
Record.Image.Data = "";
Record.MaskImage.Data = "";
Record.MaskImage.Data = "";
Record.Values = new List<float[]>();
Record.Xs = new List<float[]>();
Record.Ys = new List<float[]>();
// Сбрасываем приближение изображений
ImagesPanX = 0;
ImagesPanY = 0;
ImagesZoom = 1;
// Устанавливаем, что запись не выбрана из базы данных
IsNewRecord = true;
// Вычисляем дополнительные свойства
Record.CalculateProperties();
}
/// <summary>
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndex"/>
/// </summary>
partial void OnBrandComboBoxSelectedIndexChanged(int value)
{
if (!IsNewRecord)
return;
// Если значение выходит за верхнюю границу списка элементов выпадающего списка марок, то логгируем как ошибку
if (value >= BrandComboBoxItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
// Если значение меньше 0, то значит элемент выпадающего списка марок не выбран
if (value < 0 || value >= BrandComboBoxItems.Count())
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[value].Id, nameof(_context.BrandRecords));
// Сохраняем несуществующий индекс
var nonExistingIndex = BrandComboBoxSelectedIndex;
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
BrandComboBoxSelectedIndex = -1;
// Удаляем несуществующий элемент из списка
BrandComboBoxItems.RemoveAt(nonExistingIndex);
return;
}
// Находим запись в базе данных
var selectedRecord = _context.BrandRecords.Find(BrandComboBoxItems[value].Id);
// Если запись найдена, то заполняем поля из базы данных
if (selectedRecord is not null)
{
Record.BrandName = selectedRecord.BrandName;
Record.MixtureName = selectedRecord.MixtureName;
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
return;
}
// Если элемент не выбран, заполняем поля пустыми значениями
Record.BrandName = "";
Record.MixtureName = "";
Record.MixtureNormalRate = -1;
}
/// <summary>
/// Вызывается при изменении <see cref="SeparatorComboBoxSelectedIndex"/>
/// </summary>
partial void OnSeparatorComboBoxSelectedIndexChanged(int value)
{
if (!IsNewRecord)
return;
// Если значение выходит за верхнюю границу списка элементов выпадающего списка сепараторов, то логгируем как ошибку
if (value >= SeparatorComboBoxItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(SeparatorComboBoxSelectedIndex), SeparatorComboBoxSelectedIndex, nameof(SeparatorComboBoxItems), SeparatorComboBoxItems.Count());
// Если значение меньше 0, то значит элемент выпадающего списка сепараторов не выбран
if (value < 0 || value >= SeparatorComboBoxItems.Count())
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[value].Id, nameof(_context.SeparatorRecords));
// Сохраняем несуществующий индекс
var nonExistingIndex = SeparatorComboBoxSelectedIndex;
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
SeparatorComboBoxSelectedIndex = -1;
// Удаляем несуществующий элемент из списка
SeparatorComboBoxItems.RemoveAt(nonExistingIndex);
return;
}
// Находим запись в базе данных
var selectedRecord = _context.SeparatorRecords.Find(SeparatorComboBoxItems[value].Id);
// Если запись найдена, то заполняем поля из базы данных
if (selectedRecord is not null)
{
Record.SeparatorType = selectedRecord.Type;
return;
}
// Если элемент не выбран, заполняем поля пустыми значениями
Record.SeparatorType = "";
}
/// <summary>
/// Обновить значение <see cref="CanInteract"/>
/// </summary>
private void UpdateCanInteract()
{
CanInteract = !MenuOpened;
CanInteract &= !MainSectionIsScrolling;
CanInteract &= !InfoSectionIsScrolling;
}
// Команды управления меню
/// <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 InfoSectionPrevious() => InfoSectionCurrentIndex--;
/// <summary>
/// Переключить информационную секцию на предыдущую
/// </summary>
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.Image"/>
/// </summary>
[RelayCommand]
private void SwitchToImage()
{
ImageIsVisible = true;
MaskImageIsVisible = false;
ResultImageIsVisible = false;
}
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.MaskImage"/>
/// </summary>
[RelayCommand]
private void SwitchToMaskImage()
{
ImageIsVisible = false;
MaskImageIsVisible = true;
ResultImageIsVisible = false;
}
/// <summary>
/// Переключить на изображение <see cref="RecordViewModel.ResultImage"/>
/// </summary>
[RelayCommand]
private void SwitchToResultImage()
{
ImageIsVisible = false;
MaskImageIsVisible = false;
ResultImageIsVisible = true;
}
/// <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()
{
try
{
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName);
// Если запись не выбрана, то просто создаём новую запись
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 = Record.Id;
var deletedRecord = _context.ResultRecords.Find(deletedRecordId);
// Создаём новую запись
CreateRecord();
// Удаляем запись из списка
MenuItems.RemoveAt(deletedMenuIndex);
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.ResultRecords));
return;
}
// Удаляем запись из базы данных и сохраняем изменения в базе данных
_context.ResultRecords.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(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName);
// Если запись не выбрана
if (MenuSelectedIndex < 0)
{
// Создаём запись изображения
var imageId = 0;
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.Image.Data);
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
imageId = attachedImageRecord.Entity.Id;
}
// Создаём запись маски
var maskImageId = 0;
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.MaskImage.Data);
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
maskImageId = attachedImageRecord.Entity.Id;
}
// Создаём запись изображения результата
var resultImageId = 0;
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.ResultImage.Data);
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
resultImageId = attachedImageRecord.Entity.Id;
}
_logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Заполняем запись
var newRecord = new ResultRecord(
uuid: Record.Uuid,
createDateTime: dateTime,
editDateTime: dateTime,
sampleName: Record.SampleName,
sampleComment: Record.SampleComment,
samplingDateTime: Record.SamplingDateTime,
samplingPlace: Record.SamplingPlace,
separatorType: Record.SeparatorType,
brandName: Record.BrandName,
mixtureName: Record.MixtureName,
mixtureNormalRate: Record.MixtureNormalRate,
imageId: imageId,
maskImageId: maskImageId,
resultImageId: resultImageId,
particlesCount: Record.ParticlesCount,
values: Record.Values,
xs: Record.Xs,
ys: Record.Ys
);
// Добавляем запись в базу данных
var attachedRecord = _context.ResultRecords.Add(newRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись сохранена");
// Добавляем новый элемент в меню
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName));
// Выбираем последний элемент из меню
MenuSelectedIndex = MenuItems.Count() - 1;
}
// Если запись выбрана
else
{
// Находим запись в базе данных
var attachedRecord = _context.ResultRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.ResultRecords));
return;
}
_logger.LogTrace("Записываем изменения в базу данных");
// Обновляем изображение
{
// Ищем запись изображения в базе данных
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId);
// Если запись изображения не найдена
if (attachedImageRecord is null)
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.Image.Data);
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.ImageId = attachedImageRecord.Id;
}
}
// Если запись изображения найдена
else
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения
attachedImageRecord.Base64 = Record.Image.Data;
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
// Если изображения нет
else
{
// Удаляем изображение из базы данных
_context.ImageRecords.Remove(attachedImageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
attachedRecord.ImageId = 0;
}
}
}
// Обновляем маску
{
// Ищем запись маски в базе данных
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.MaskImageId);
// Если запись изображения не найдена
if (attachedImageRecord is null)
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.MaskImage.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.MaskImage.Data);
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.MaskImageId = attachedImageRecord.Id;
}
}
// Если запись изображения найдена
else
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.MaskImage.Data))
{
_logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения
attachedImageRecord.Base64 = Record.MaskImage.Data;
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
// Если изображения нет
else
{
// Удаляем изображение из базы данных
_context.ImageRecords.Remove(attachedImageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
attachedRecord.MaskImageId = 0;
}
}
}
// Обновляем изображение результата
{
// Ищем запись маски в базе данных
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ResultImageId);
// Если запись изображения не найдена
if (attachedImageRecord is null)
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.ResultImage.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.ResultImage.Data);
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.ResultImageId = attachedImageRecord.Id;
}
}
// Если запись изображения найдена
else
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.ResultImage.Data))
{
_logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения
attachedImageRecord.Base64 = Record.ResultImage.Data;
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
// Если изображения нет
else
{
// Удаляем изображение из базы данных
_context.ImageRecords.Remove(attachedImageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
attachedRecord.ResultImageId = 0;
}
}
}
// Определяем Id выбранной марки
var selectedBrandId = 0;
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
// Если значение больше 0, то достаём Id из списка элементов
else if (BrandComboBoxSelectedIndex >= 0)
selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id;
// Записываем изменения
attachedRecord.EditDateTime = DateTime.UtcNow;
attachedRecord.SampleName = Record.SampleName;
attachedRecord.SampleComment = Record.SampleComment;
attachedRecord.SamplingDateTime = Record.SamplingDateTime;
attachedRecord.SamplingPlace = Record.SamplingPlace;
attachedRecord.SeparatorType = Record.SeparatorType;
attachedRecord.BrandName = Record.BrandName;
attachedRecord.MixtureName = Record.MixtureName;
attachedRecord.MixtureNormalRate = Record.MixtureNormalRate;
attachedRecord.ParticlesCount = Record.ParticlesCount;
attachedRecord.ValueCount = Record.Values.Select(fs => fs.Count()).ToList();
attachedRecord.ValueData = Record.Values.SelectMany(fs => fs).ToList();
attachedRecord.XCount = Record.Xs.Select(fs => fs.Count()).ToList();
attachedRecord.XData = Record.Xs.SelectMany(fs => fs).ToList();
attachedRecord.YCount = Record.Ys.Select(fs => fs.Count()).ToList();
attachedRecord.YData = Record.Ys.SelectMany(fs => fs).ToList();
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись сохранена");
// Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].SampleName = Record.SampleName;
// Устанавливаем, что запись выбрана из базы данных
IsNewRecord = false;
}
}
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;
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-analysis-{visible}-{uv365}-{uv254}.png");
image.ImWrite(path);
// Освобождаем память изображения
image.Dispose();
// Добавляем данные изображения в список
return new ImageData(path, visible, uv365, uv254);
}, cancellationToken);
// Добавляем задачу сохранения изображения в список
imagesData.Add(imagesDataTask);
}, cancellationToken);
// var captureTask = Task.Run(() => {
// foreach (var (visible, uv365, uv254) in ImageCapturerService.IlluminatorIntensities)
// {
// var imagesDataTask = Task.Run(() =>
// {
// var path = Path.Join(Path.GetTempPath(), $"gss2-analysis-{visible}-{uv365}-{uv254}.png");
// return new ImageData(path, visible, uv365, uv254);
// }, cancellationToken);
// imagesData.Add(imagesDataTask);
// }
// });
// Создаём задачу обработки изображений,
// эта задача начнётся только после того как завершится
// задача съёмки изображений и все задачи сохранения изображений
var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{
// Обновляем данные в интерфейсе
// 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 brand = _context.BrandRecords.Find(BrandComboBoxItems[BrandComboBoxSelectedIndex].Id);
var separator = _context.SeparatorRecords.Find(SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id);
if (brand is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[BrandComboBoxSelectedIndex].Id, nameof(_context.BrandRecords));
return;
}
if (brand.MixtureNormalRate <= 0)
{
_logger.LogError("Норма расхода выбранной марки меньше или равена 0");
return;
}
if (separator is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords));
return;
}
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей");
await _analyzer.Initialize(brand, separator);
// Проверяем что анализатор инициализирован
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 null)
{
_logger.LogError("Ошибка при выполнении анализа");
// Если была ошибка, то мигаем светодиодами красным
_ = Task.Run(async () =>
{
await Task.Delay(1000);
_lights.Flash(1, 1, System.Drawing.Color.Red);
await Task.Delay(3000);
_lights.Disconnect();
});
return;
}
// Распаковываем результат
var (avgImage, _, mask, contours, resultImage) = result.Value;
try
{
Record.ParticlesCount = contours.Count();
// Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
Record.Image.Data = Convert.ToBase64String(avgData);
// Освобождаем память среднего изображения
avgImage.Dispose();
// Создаём маску контура
var contourMask = new OpenCvSharp.Mat(
new OpenCvSharp.Size(
AnalyzerService.ANALYSIS_IMAGE_SIZE,
AnalyzerService.ANALYSIS_IMAGE_SIZE
),
OpenCvSharp.MatType.CV_8UC1,
new OpenCvSharp.Scalar(0)
);
var values = new List<float[]>();
// Проходим по каждому контуру
foreach (var contour in contours)
{
// Обнуляем маску контура
contourMask.SetTo(new OpenCvSharp.Scalar(0));
// Заполняем контур на маске
OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1);
// Чуть-чуть обрезаем его по краям
OpenCvSharp.Cv2.Erode(contourMask, contourMask, OpenCvSharp.Cv2.GetStructuringElement(OpenCvSharp.MorphShapes.Ellipse, new OpenCvSharp.Size(5, 5)));
// Находим прямоугольник ограничивающий конур
var bbox = OpenCvSharp.Cv2.BoundingRect(contour);
// Проходимся по каждому пикселю в прямоугольнике и если он попадает в маску, то сохраняем значение результата
var contourValues = new List<float>();
for (int i = bbox.Top; i < bbox.Bottom; i++)
for (int j = bbox.Left; j < bbox.Right; j++)
if (contourMask.Get<byte>(i, j) > 127)
contourValues.Add(resultImage.Get<float>(i, j));
// Сохраняем полученные значения
values.Add(contourValues.ToArray());
}
Record.Values = values;
// Освобождаем память маски контура
contourMask.Dispose();
// Преобразуем конура в массивы координат
Record.Xs = contours.Select(c => c.Select(p => (float)p.X).ToArray()).ToList();
Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList();
// Вычисляем пороговые значения
var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * brand.MixtureNormalRate;
var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * brand.MixtureNormalRate;
// Выичсляем максимальное значение
OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max);
// Если иаксмиальное значниеz слишком мало, то устанавливаем его чуть больше hotspotsThreshold, чтобы не возникло деления на ноль и отридцательных значений
if (max <= hotspotsThreshold)
max = hotspotsThreshold + 0.001;
// Создаём цветное изображение результата
var coloredResultImage = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC3, OpenCvSharp.Scalar.All(0));
// Расшрашиваем диапазон 0...unprocessedThreshold -> чёрный...белый
ApplyGradientToRange(resultImage, 0, unprocessedThreshold, OpenCvSharp.Scalar.Black, OpenCvSharp.Scalar.White, coloredResultImage);
// Расшрашиваем диапазон unprocessedThreshold...hotspotsThreshold -> белый...зелёный
ApplyGradientToRange(resultImage, unprocessedThreshold, hotspotsThreshold, OpenCvSharp.Scalar.White, OpenCvSharp.Scalar.Green, coloredResultImage);
// Расшрашиваем диапазон hotspotsThreshold...max -> зелёный...крастный
ApplyGradientToRange(resultImage, hotspotsThreshold, max, OpenCvSharp.Scalar.Green, OpenCvSharp.Scalar.Red, coloredResultImage);
// Устанавливаем в 0 всё что не попадает в маску
var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0));
OpenCvSharp.Cv2.BitwiseNot(mask, invMask);
coloredResultImage.SetTo(OpenCvSharp.Scalar.All(0), invMask);
// Освобождаем память, оригинальное изображение результата можно удалить
invMask.Dispose();
resultImage.Dispose();
// Кодируем маску
OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData);
Record.MaskImage.Data = Convert.ToBase64String(maskData);
// Освобождаем память маски
mask.Release();
// Кодируем изображение результата
OpenCvSharp.Cv2.ImEncode(".png", coloredResultImage, out var resultData);
Record.ResultImage.Data = Convert.ToBase64String(resultData);
// Освобождаем память изображения результата
coloredResultImage.Release();
// Пересчитываем значения свойств
Record.CalculateProperties();
// Если ошибок не было, то мигаем светодиодами зелёным
_ = Task.Run(async () =>
{
await Task.Delay(1000);
_lights.Flash(1, 1, System.Drawing.Color.Green);
await Task.Delay(3000);
_lights.Disconnect();
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение во время анализа");
// Если была ошибка, то мигаем светодиодами красным
_ = Task.Run(async () =>
{
await Task.Delay(1000);
_lights.Flash(1, 1, System.Drawing.Color.Red);
await Task.Delay(3000);
_lights.Disconnect();
});
}
}, 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;
});
});
});
}
/// <summary>
/// Отменить задачу съёмки изображения
/// </summary>
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
/// <summary>
/// Раскрасить диапазон в градиент
/// </summary>
public static void ApplyGradientToRange(OpenCvSharp.Mat source, double minVal, double maxVal, OpenCvSharp.Scalar minColor, OpenCvSharp.Scalar maxColor, OpenCvSharp.Mat destination)
{
// Проверяем что все типы данных
if (source.Type() != OpenCvSharp.MatType.CV_32FC1)
throw new ArgumentException("Исходное изображение должно быть типа CV_32FC1", nameof(source));
if (destination.Type() != OpenCvSharp.MatType.CV_8UC3)
throw new ArgumentException("Выходное изображение должно быть типа CV_8UC3", nameof(destination));
if (source.Size() != destination.Size())
throw new ArgumentException("Размеры исходного и выходного изображений должны совпадать");
if (maxVal <= minVal)
throw new ArgumentException("Максимальное значение диапазона должно быть больше минимального.");
// Создаем маску для целевого диапазона значений
using var mask = new OpenCvSharp.Mat();
OpenCvSharp.Cv2.InRange(source, OpenCvSharp.Scalar.All(minVal), OpenCvSharp.Scalar.All(maxVal), mask);
// Нормализуем значения из диапазона minVal...maxVal в 0.0...1.0
using var normalized = new OpenCvSharp.Mat();
OpenCvSharp.Cv2.Subtract(source, OpenCvSharp.Scalar.All(minVal), normalized);
normalized.ConvertTo(normalized, OpenCvSharp.MatType.CV_32FC1, 1.0 / (maxVal - minVal));
// Чтобы избежать выхода за границы, ограничиваем диапазон от 0.0...1.0
OpenCvSharp.Cv2.Threshold(normalized, normalized, 1.0, 1.0, OpenCvSharp.ThresholdTypes.Trunc);
OpenCvSharp.Cv2.Threshold(normalized, normalized, 0.0, 0.0, OpenCvSharp.ThresholdTypes.Tozero);
// Вычисляем градиент для каждого из трех каналов (BGR)
using var bChannel = new OpenCvSharp.Mat();
using var gChannel = new OpenCvSharp.Mat();
using var rChannel = new OpenCvSharp.Mat();
double diffB = maxColor.Val0 - minColor.Val0;
double diffG = maxColor.Val1 - minColor.Val1;
double diffR = maxColor.Val2 - minColor.Val2;
normalized.ConvertTo(bChannel, OpenCvSharp.MatType.CV_8UC1, diffB, minColor.Val0);
normalized.ConvertTo(gChannel, OpenCvSharp.MatType.CV_8UC1, diffG, minColor.Val1);
normalized.ConvertTo(rChannel, OpenCvSharp.MatType.CV_8UC1, diffR, minColor.Val2);
// Объеденяем каналы в одно изображение
using var gradientColor = new OpenCvSharp.Mat();
OpenCvSharp.Cv2.Merge(new[] { bChannel, gChannel, rChannel }, gradientColor);
// Копируем полученный градиент в итоговое изображение по маске
gradientColor.CopyTo(destination, mask);
}
}