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 @@
+
+
+