Files
GSS2Rework/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs
T
amkovkov 72ae26eee7 comprehensive update
1) remove unused components: camera view (due it causes segmentation faults), compute resources and all related, illuminator controller (due in useless without camera view), image storage service, temperature and humidity service
2) database schema - reduce tables references, features storing in records themselves in compressed form, add records creation and editing date and time, add separator comment column
3) analysis - rework of pipeline and ui, now database storing only raw data and all display values calculated from it
4) lights service - add reconnection if disconnected
5) add width and height command line arguments
6) fix some typos and other issues
2026-06-29 15:13:29 +03:00

1915 lines
103 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Data;
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SkiaSharp;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.Painting.Effects;
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;
using UnitsNet;
using MathNet.Numerics.Statistics;
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.5;
public const double HOTSPOT_THRESHOLD = 1.2;
public const double EPSILON = 1e-10;
public const double UNPROCESSED_FRACTION_THRESHOLD = 10;
/// <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="ResultRecord.ImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleImage { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.MaskImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleMaskImage { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.ResultImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleResultImage { get; set; } = new();
/// <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 ImagesPanY { get; set; } = 0;
/// <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();
// Для каждой гранулы
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) / area * 100;
var unprocessedArea = normalized.Count(v => v <= 1) / area * 100;
var hotspotArea = normalized.Count(v => v > 2) / area * 100;
var opticalIntegral = normalized.Sum() / area * 100;
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;
}
/// <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, "Количество, шт", "Значение");
}
}
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;
}
// Обновляем элемент из выпадающего списка марок
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;
}
// Обновляем элемент из выпадающего списка марок
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; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
};
_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 not null)
{
// Заполняем простые поля
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.MaskImage.Data = resultImageRecord?.Base64 ?? "";
// Сбрасываем приближение изображений
ImagesPanX = 0;
ImagesPanY = 0;
ImagesZoom = 1;
// Устанавливаем, что запись выбрана из базы данных
IsNewRecord = false;
// Вычисляем дополнительные свойства
Record.CalculateProperties();
return;
}
// Если значение не найдено в базе данных
else
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
MenuSelectedIndex = -1;
// Удаляем несуществующий элемент из списка
MenuItems.RemoveAt(nonExistingIndex);
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="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.Image.Data))
{
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.Image.Data);
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.MaskImageId = 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.MaskImageId = 0;
}
}
}
// Обновляем изображение результата
{
// Ищем запись маски в базе данных
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ResultImageId);
// Если запись изображения не найдена
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.ResultImageId = 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.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-calibration-sample-{visible}-{uv365}-{uv254}.png");
image.ImWrite(path);
// Освобождаем память изображения
image.Dispose();
// Добавляем данные изображения в список
return new ImageData(path, visible, uv365, uv254);
}, cancellationToken);
// Добавляем задачу сохранения изображения в список
imagesData.Add(imagesDataTask);
}, cancellationToken);
// Создаём задачу обработки изображений,
// эта задача начнётся только после того как завершится
// задача съёмки изображений и все задачи сохранения изображений
var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{
// Обновляем данные в интерфейсе
// 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 (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();
});
}
// Распаковываем результат
var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value;
try
{
// Кодируем среднее изображение
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)
);
Record.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));
// Сохраняем полученные значения
Record.Values.Add(contourValues.ToArray());
}
// Освобождаем память маски контура
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 * Record.MixtureNormalRate;
var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * Record.MixtureNormalRate;
OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max);
if (max <= hotspotsThreshold)
max = hotspotsThreshold + 0.001;
var lutIndices = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC1, OpenCvSharp.Scalar.All(0));
using (var mask1 = new OpenCvSharp.Mat())
{
OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(0), OpenCvSharp.Scalar.All(unprocessedThreshold), mask1);
var scale1 = 85.0 / unprocessedThreshold;
using (var res1 = new OpenCvSharp.Mat())
{
resultImage.ConvertTo(res1, OpenCvSharp.MatType.CV_8UC1, scale1);
res1.CopyTo(lutIndices, mask1);
}
}
using (var mask2 = new OpenCvSharp.Mat())
{
OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(unprocessedThreshold), OpenCvSharp.Scalar.All(hotspotsThreshold), mask2);
double scale2 = 85.0 / (hotspotsThreshold - unprocessedThreshold);
double shift2 = -unprocessedThreshold * scale2 + 85.0;
using (var res2 = new OpenCvSharp.Mat())
{
resultImage.ConvertTo(res2, OpenCvSharp.MatType.CV_8UC1, scale2, shift2);
res2.CopyTo(lutIndices, mask2);
}
}
using (var mask3 = new OpenCvSharp.Mat())
{
OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(hotspotsThreshold), OpenCvSharp.Scalar.All(max), mask3);
double scale3 = 85.0 / (max - hotspotsThreshold);
double shift3 = -hotspotsThreshold * scale3 + 170.0;
using (var res3 = new OpenCvSharp.Mat())
{
resultImage.ConvertTo(res3, OpenCvSharp.MatType.CV_8UC1, scale3, shift3);
res3.CopyTo(lutIndices, mask3);
}
}
// Создаём и применяем LUT
var lut = CreateLut();
var coloredResult = new OpenCvSharp.Mat();
OpenCvSharp.Cv2.LUT(lutIndices, lut, coloredResult);
// Устанавливаем в 0 всё что не попадает в маску
var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0));
OpenCvSharp.Cv2.BitwiseNot(mask, invMask);
coloredResult.SetTo(OpenCvSharp.Scalar.All(0), invMask);
// Освобождаем память, оригинальное изображение результата можно удалить
lutIndices.Dispose();
lut.Dispose();
invMask.Dispose();
resultImage.Dispose();
// Кодируем маску
OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData);
Record.MaskImage.Data = Convert.ToBase64String(maskData);
// Освобождаем память маски
mask.Release();
// Кодируем изображение результата
OpenCvSharp.Cv2.ImEncode(".png", coloredResult, out var resultData);
Record.ResultImage.Data = Convert.ToBase64String(resultData);
// Освобождаем память изображения результата
coloredResult.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();
private static OpenCvSharp.Mat CreateLut()
{
var lut = new OpenCvSharp.Mat(256, 1, OpenCvSharp.MatType.CV_8UC3);
var indexer = lut.GetGenericIndexer<OpenCvSharp.Vec3b>();
for (int i = 0; i <= 85; i++)
{
var val = (byte)Math.Round(i * (255.0 / 85.0));
indexer[i, 0] = new OpenCvSharp.Vec3b(val, val, val);
}
for (int i = 86; i <= 170; i++)
{
var factor = (i - 85) / 85.0; // от 0 до 1
var rbVal = (byte)Math.Round(255 * (1.0 - factor));
indexer[i, 0] = new OpenCvSharp.Vec3b(rbVal, 255, rbVal);
}
for (int i = 171; i <= 255; i++)
{
var factor = (i - 170) / 85.0; // от 0 до 1
var gVal = (byte)Math.Round(255 * (1.0 - factor));
var rVal = (byte)Math.Round(255 * factor);
indexer[i, 0] = new OpenCvSharp.Vec3b(0, gVal, rVal); // BGR
}
return lut;
}
}