From 0849a0ae676222a59113f8d21442fd1d52e48d31 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Mon, 8 Jun 2026 09:47:52 +0300 Subject: [PATCH] feat: quality property --- .../20260608053836_Results1.Designer.cs | 131 ++++++++++++++++++ .../Migrations/20260608053836_Results1.cs | 40 ++++++ .../Migrations/ResultsContextModelSnapshot.cs | 8 ++ .../Database/Results/ResultRecord.cs | 4 + .../AmineContent/AnalysisViewModel.cs | 73 +++++++++- GSS2/Views/AmineContent/AnalysisView.axaml | 15 +- GSS2/Views/AmineContent/AnalysisView.axaml.cs | 1 + 7 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.Designer.cs create mode 100644 GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.cs diff --git a/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.Designer.cs b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.Designer.cs new file mode 100644 index 0000000..37d5b97 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.Designer.cs @@ -0,0 +1,131 @@ +// +using System; +using GSS2.Core.Analysis.AmineContent.Database.Results; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations +{ + [DbContext(typeof(ResultsContext))] + [Migration("20260608053836_Results1")] + partial class Results1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Results.ResultRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Уникальный идентификатор"); + + b.Property("Image") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Изображение"); + + b.Property("MaskImage") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Маска"); + + b.Property("MaxValue") + .HasColumnType("REAL") + .HasComment("Максимальное значение"); + + b.Property("MeanQuality") + .HasColumnType("REAL") + .HasComment("Среднее качество обработки"); + + b.Property("MeanValue") + .HasColumnType("REAL") + .HasComment("Среднее значение"); + + b.PrimitiveCollection("MeanValues") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Средние значения"); + + b.Property("MeanVariance") + .HasColumnType("REAL") + .HasComment("Средняя дисперсия"); + + b.Property("MinValue") + .HasColumnType("REAL") + .HasComment("Минимальное значение"); + + b.Property("ParticlesCount") + .HasColumnType("INTEGER") + .HasComment("Количество частиц"); + + b.PrimitiveCollection("Qualities") + .HasColumnType("TEXT") + .HasComment("Качества обработки"); + + b.Property("ResultImage") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Изображение результата"); + + b.Property("SampleBrand") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Марка пробы"); + + b.Property("SampleComment") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация о пробе"); + + b.Property("SampleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название пробы"); + + b.Property("SamplingDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время отбора пробы"); + + b.Property("SamplingPlace") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Место отбора пробы"); + + b.Property("SeparatorType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Тип сепаратора"); + + b.Property("VarianceOfMean") + .HasColumnType("REAL") + .HasComment("Дисперсия среднего"); + + b.PrimitiveCollection("Variances") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Дисперсии"); + + b.HasKey("Id"); + + b.ToTable("ResultRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.cs b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.cs new file mode 100644 index 0000000..1507277 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/20260608053836_Results1.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations +{ + /// + public partial class Results1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MeanQuality", + table: "ResultRecords", + type: "REAL", + nullable: true, + comment: "Среднее качество обработки"); + + migrationBuilder.AddColumn( + name: "Qualities", + table: "ResultRecords", + type: "TEXT", + nullable: true, + comment: "Качества обработки"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MeanQuality", + table: "ResultRecords"); + + migrationBuilder.DropColumn( + name: "Qualities", + table: "ResultRecords"); + } + } +} diff --git a/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/ResultsContextModelSnapshot.cs b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/ResultsContextModelSnapshot.cs index 2d89871..801fec2 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/ResultsContextModelSnapshot.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Results/Migrations/ResultsContextModelSnapshot.cs @@ -40,6 +40,10 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations .HasColumnType("REAL") .HasComment("Максимальное значение"); + b.Property("MeanQuality") + .HasColumnType("REAL") + .HasComment("Среднее качество обработки"); + b.Property("MeanValue") .HasColumnType("REAL") .HasComment("Среднее значение"); @@ -61,6 +65,10 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations .HasColumnType("INTEGER") .HasComment("Количество частиц"); + b.PrimitiveCollection("Qualities") + .HasColumnType("TEXT") + .HasComment("Качества обработки"); + b.Property("ResultImage") .IsRequired() .HasMaxLength(256) diff --git a/GSS2.Core/Analysis/AmineContent/Database/Results/ResultRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/Results/ResultRecord.cs index c74df96..3938e27 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Results/ResultRecord.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Results/ResultRecord.cs @@ -46,8 +46,12 @@ public class ResultRecord public required double MeanVariance { get; set; } [Comment("Дисперсия среднего")] public required double VarianceOfMean { get; set; } + [Comment("Среднее качество обработки")] + public required double? MeanQuality { get; set; } [Comment("Средние значения")] public required double[] MeanValues { get; set; } [Comment("Дисперсии")] public required double[] Variances { get; set; } + [Comment("Качества обработки")] + public required double[]? Qualities { get; set; } } diff --git a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs index a3da682..aa3c77b 100644 --- a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs +++ b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs @@ -21,11 +21,16 @@ using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using LiveChartsCore.SkiaSharpView.Painting.Effects; using GSS2.UI.Core.Services; +using GSS2.Core.Hardware; namespace GSS2.ViewModels.AmineContent; public partial class AnalysisViewModel : ViewModelBase { + const double QUALITY_MIN_MEAN = 2.0; + const double QUALITY_ALPHA = 0.7; + const double QUALITY_BETA = 0.3; + public event EventHandler? UpdateCharts; private readonly ILogger _logger; @@ -35,6 +40,7 @@ public partial class AnalysisViewModel : ViewModelBase private readonly AnalyzerService _analyzer; private readonly DependencyInjectionViewLocator _viewLocator; private readonly NavigationService _navigation; + private readonly LightsService _lights; private CancellationTokenSource? _captureImagesTaskCts = null; @@ -81,6 +87,10 @@ public partial class AnalysisViewModel : ViewModelBase [ObservableProperty] public partial Axis[] VariancesXAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial Axis[] VariancesYAxis { get; set; } = [new Axis()]; + [ObservableProperty] public partial ISeries[] QualitiesHistogram { get; set; } = []; + [ObservableProperty] public partial Axis[] QualitiesXAxis { get; set; } = [new Axis()]; + [ObservableProperty] public partial Axis[] QualitiesYAxis { get; set; } = [new Axis()]; + public AnalysisViewModel( ILogger logger, ResultsContext context, @@ -88,7 +98,8 @@ public partial class AnalysisViewModel : ViewModelBase ImageStorageService imageStorage, AnalyzerService analyzer, DependencyInjectionViewLocator viewLocator, - NavigationService navigation + NavigationService navigation, + LightsService lights ) { _logger = logger; @@ -98,6 +109,7 @@ public partial class AnalysisViewModel : ViewModelBase _analyzer = analyzer; _viewLocator = viewLocator; _navigation = navigation; + _lights = lights; _logger.LogInformation("Инициализация"); // Получаем записи из базы данных @@ -125,7 +137,9 @@ public partial class AnalysisViewModel : ViewModelBase MeanVariance = -1, VarianceOfMean = -1, MeanValues = [], - Variances = [] + Variances = [], + Qualities = [], + MeanQuality = -1 }; _logger.LogInformation("Инициализировано"); @@ -159,7 +173,9 @@ public partial class AnalysisViewModel : ViewModelBase MeanVariance = -1, VarianceOfMean = -1, MeanValues = [], - Variances = [] + Variances = [], + Qualities = [], + MeanQuality = -1 }; } else @@ -224,6 +240,13 @@ public partial class AnalysisViewModel : ViewModelBase leftText: "Количество частиц", bottomText: "Дисперсия" ); + (QualitiesHistogram, QualitiesXAxis, QualitiesYAxis) = BuildHistogram( + value.Qualities ?? [], + 0, + 100, + leftText: "Количество частиц", + bottomText: "Качество обработки" + ); } catch (Exception ex) { @@ -338,6 +361,8 @@ public partial class AnalysisViewModel : ViewModelBase SelectedRecord.MeanValue = EditingRecord.MeanValue; SelectedRecord.MeanVariance = EditingRecord.MeanVariance; SelectedRecord.VarianceOfMean = EditingRecord.VarianceOfMean; + SelectedRecord.Qualities = EditingRecord.Qualities; + SelectedRecord.MeanQuality = EditingRecord.MeanQuality; // Если записи нет в базе данных if (_context.Entry(SelectedRecord).State is EntityState.Detached) @@ -496,6 +521,7 @@ public partial class AnalysisViewModel : ViewModelBase // Вычисляем среднее и дисперсию каждого контура var means = new List(); var variances = new List(); + var qualities = new List(); // Создаём временную маску для каждого контура var contourMask = new OpenCvSharp.Mat( @@ -520,6 +546,7 @@ public partial class AnalysisViewModel : ViewModelBase // Добавляем значения в лист means.Add(mean.Val0); variances.Add(Math.Pow(stdDev.Val0, 2)); + qualities.Add(CalculateQuality(mean.Val0, Math.Pow(stdDev.Val0, 2))); } contourMask.Release(); @@ -581,21 +608,48 @@ public partial class AnalysisViewModel : ViewModelBase EditingRecord.VarianceOfMean = varianceOfMean; EditingRecord.MeanValues = means.ToArray(); EditingRecord.Variances = variances.ToArray(); + EditingRecord.MeanQuality = qualities.Average(); + EditingRecord.Qualities = qualities.ToArray(); // Обновляем данные в редактируемой записи // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord)); + + // Если ошибок не было, то мигаем светодиодами зелёным + _ = 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(); + }); } } else { _logger.LogError("Ошибка при выполнении анализа"); + // Если была ошибка, то мигаем светодиодами красным + _ = Task.Run(async () => + { + await Task.Delay(1000); + _lights.Flash(1, 1, System.Drawing.Color.Red); + await Task.Delay(3000); + _lights.Disconnect(); + }); } }, cancellationToken); @@ -666,7 +720,15 @@ public partial class AnalysisViewModel : ViewModelBase MeanVariance = source.MeanVariance, VarianceOfMean = source.VarianceOfMean, MeanValues = source.MeanValues, - Variances = source.Variances + Variances = source.Variances, + Qualities = source.Qualities ?? + Enumerable.Zip(source.MeanValues, source.Variances) + .Select((pair) => CalculateQuality(pair.First, pair.Second)) + .ToArray(), + MeanQuality = source.MeanQuality ?? + Enumerable.Zip(source.MeanValues, source.Variances) + .Select((pair) => CalculateQuality(pair.First, pair.Second)) + .Average(), }; } @@ -785,4 +847,7 @@ public partial class AnalysisViewModel : ViewModelBase } private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") => BuildHistogram(values.Select(v => (float)v), min, max, leftText, rightText, bottomText, barLegend, lineLegend); + + private static double CalculateQuality(double mean, double variance) => + 100.0 * Math.Exp(-1 * (QUALITY_ALPHA * Math.Max(0, QUALITY_MIN_MEAN - mean) + QUALITY_BETA * Math.Pow(variance, 2))); } diff --git a/GSS2/Views/AmineContent/AnalysisView.axaml b/GSS2/Views/AmineContent/AnalysisView.axaml index 759c6a6..a110c33 100644 --- a/GSS2/Views/AmineContent/AnalysisView.axaml +++ b/GSS2/Views/AmineContent/AnalysisView.axaml @@ -192,6 +192,8 @@ + + @@ -233,7 +235,10 @@ - + + + + @@ -344,6 +349,14 @@ + + +