forked from amkovkov/GranuSightSoftware2
2033 lines
110 KiB
C#
2033 lines
110 KiB
C#
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();
|
||
|
||
(
|
||
ParticlesCount,
|
||
_,
|
||
NormalizedValues,
|
||
ProcessedAreas,
|
||
UnprocessedAreas,
|
||
HotspotAreas,
|
||
OpticalIntegrals,
|
||
InnerStdDevs,
|
||
InnerHeterogeneities,
|
||
MeanProcessedArea,
|
||
ProcessedAreaP10,
|
||
ProcessedAreaP50,
|
||
ProcessedAreaP90,
|
||
ProcessedAreaStdDev,
|
||
InterHeterogeneity,
|
||
ProcessedAreaDeltaP90P10,
|
||
UnprocessedFraction,
|
||
MeanInnerHeterogeneity,
|
||
MeanInnerStdDev,
|
||
MeanHotspotArea,
|
||
MeanOpticalIntegral
|
||
) = ResultsPostProcessor.PostProcess(Values, MixtureNormalRate);
|
||
|
||
// Обновляем гистограммы
|
||
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 CanCapture { 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(BrandComboBoxSelectedIndex) ||
|
||
ea.PropertyName == nameof(SeparatorComboBoxSelectedIndex))
|
||
UpdateCanCapture();
|
||
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();
|
||
|
||
CanCapture = false;
|
||
|
||
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.ResultImage.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();
|
||
|
||
BrandComboBoxSelectedIndex = -1;
|
||
SeparatorComboBoxSelectedIndex = -1;
|
||
}
|
||
|
||
/// <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>
|
||
/// Обновить значение <see cref="CanCapture"/>
|
||
/// </summary>
|
||
private void UpdateCanCapture()
|
||
{
|
||
CanCapture = BrandComboBoxSelectedIndex >= 0;
|
||
CanCapture &= SeparatorComboBoxSelectedIndex >= 0;
|
||
}
|
||
|
||
// Команды управления меню
|
||
/// <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);
|
||
// Если максимальное значние слишком мало, то устанавливаем его чуть больше 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);
|
||
}
|
||
}
|