From 96ca781fe76f2c36e6f82f63001ac7e1c53b7b2f Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Wed, 12 Aug 2026 05:42:06 +0300 Subject: [PATCH] feat: add ResultsExporter move results postprocessing to separate class fix few typos --- .../Analysis/AmineContent/ResultExporter.cs | 122 +++++++++++++ .../AmineContent/ResultsPostProcessor.cs | 167 ++++++++++++++++++ .../AmineContent/AnalysisViewModel.cs | 108 +++-------- 3 files changed, 317 insertions(+), 80 deletions(-) create mode 100644 GSS2.Core/Analysis/AmineContent/ResultExporter.cs create mode 100644 GSS2.Core/Analysis/AmineContent/ResultsPostProcessor.cs diff --git a/GSS2.Core/Analysis/AmineContent/ResultExporter.cs b/GSS2.Core/Analysis/AmineContent/ResultExporter.cs new file mode 100644 index 0000000..f2c12d7 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/ResultExporter.cs @@ -0,0 +1,122 @@ +using System.Text; +using System.Globalization; + +using GSS2.Core.Analysis.AmineContent.Database; + +namespace GSS2.Core.Analysis.AmineContent; + +public static class ResultExporter +{ + public static void ExportCsv(string path, IEnumerable records) + { + NumberFormatInfo nfi = new NumberFormatInfo + { + NumberDecimalSeparator = "," + }; + + var rows = new List>(); + + rows.Add(new List + { + "ИД", + "Название пробы", + "Дата и время отбора пробы", + "Место отбора пробы", + "Дополнительная информация о пробе", + "Дата и время создания записи", + "Дата и время редактирования записи", + "Тип сепаратора", + "Марка пробы", + "Марка кондиционирующей смеси", + "Норма расхода кондиционирующей смеси, кг/т", + "Количество частиц", + "Средняя нормированная обработанная площадь", + "50-й процентиль", + "10-й процентиль", + "90-й процентиль", + "Ширина центрального диапазона", + "Межгранульное среднеквадратичное отклонение", + "Межгранульная неоднородность", + "Доля необработанных гранул", + "Средняя внутригранульная неоднородность", + "Средняя горячая площадь", + "Средний интегральный оптический показатель" + }); + + foreach (var record in records) + { + var results = ResultsPostProcessor.PostProcess(record); + + var row = new List + { + record.Id.ToString(), + record.SampleName, + record.SamplingDateTime.ToString("dd.MM.yyyy hh:mm:ss"), + record.SamplingPlace, + record.SampleComment, + record.CreateDateTime.ToString("dd.MM.yyyy hh:mm:ss"), + record.EditDateTime.ToString("dd.MM.yyyy hh:mm:ss"), + record.SeparatorType, + record.BrandName, + record.MixtureName, + record.MixtureNormalRate.ToString("F3", nfi), + results.values.Count().ToString(), + results.meanProcessedArea.ToString("F3", nfi), + results.processedAreaP50.ToString("F3", nfi), + results.processedAreaP10.ToString("F3", nfi), + results.processedAreaP90.ToString("F3", nfi), + results.processedAreaDeltaP90P10.ToString("F3", nfi), + results.processedAreaStdDev.ToString("F3", nfi), + results.interHeterogeneity.ToString("F3", nfi), + results.unprocessedFraction.ToString("F3", nfi), + results.meanInnerHeterogeneity.ToString("F3", nfi), + results.meanHotspotArea.ToString("F3", nfi), + results.meanOpticalIntegral.ToString("F3", nfi) + }; + rows.Add(row); + + row = new List(23); + row.Add("Нормированная обработанная площадь"); + foreach (var v in results.processedAreas) + row.Add(v.ToString("F3", nfi)); + rows.Add(row); + + row = new List(23); + row.Add("Горячая площадь"); + foreach (var v in results.hotspotAreas) + row.Add(v.ToString("F3", nfi)); + rows.Add(row); + + row = new List(23); + row.Add("Оптический интегральный показатель"); + foreach (var v in results.opticalIntegrals) + row.Add(v.ToString("F3", nfi)); + rows.Add(row); + + row = new List(23); + row.Add("Нормированная обработанная площадь"); + foreach (var v in results.innerStdDevs) + row.Add(v.ToString("F3", nfi)); + rows.Add(row); + + row = new List(23); + row.Add("Внутригранульная неоднородность"); + foreach (var v in results.innerHeterogeneities) + row.Add(v.ToString("F3", nfi)); + rows.Add(row); + } + + var sb = new StringBuilder(); + foreach (var row in rows) + { + foreach (var cell in row) + { + sb.Append(cell); + sb.Append(";"); + } + sb.Append("\n"); + } + + File.WriteAllText(path, sb.ToString()); + } +} diff --git a/GSS2.Core/Analysis/AmineContent/ResultsPostProcessor.cs b/GSS2.Core/Analysis/AmineContent/ResultsPostProcessor.cs new file mode 100644 index 0000000..fcf1a97 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/ResultsPostProcessor.cs @@ -0,0 +1,167 @@ +using MathNet.Numerics.Statistics; + +using GSS2.Core.Analysis.AmineContent.Database; +using GSS2.Core.Extensions; + +namespace GSS2.Core.Analysis.AmineContent; + +public static class ResultsPostProcessor +{ + public const double UNPROCESSED_THRESHOLD = 0.5; + public const double HOTSPOT_THRESHOLD = 1.2; + public const double EPSILON = 1e-10; + public const double UNPROCESSED_FRACTION_THRESHOLD = 10; + + + public static ( + int particlesCount, + List values, + List normalizedValues, + List processedAreas, + List unprocessedAreas, + List hotspotAreas, + List opticalIntegrals, + List innerStdDevs, + List innerHeterogeneities, + double meanProcessedArea, + double processedAreaP10, + double processedAreaP50, + double processedAreaP90, + double processedAreaStdDev, + double interHeterogeneity, + double processedAreaDeltaP90P10, + double unprocessedFraction, + double meanInnerHeterogeneity, + double meanInnerStdDev, + double meanHotspotArea, + double meanOpticalIntegral + ) PostProcess(ResultRecord record) => PostProcess(record.ValueData, record.ValueCount, record.MixtureNormalRate); + + public static ( + int particlesCount, + List values, + List normalizedValues, + List processedAreas, + List unprocessedAreas, + List hotspotAreas, + List opticalIntegrals, + List innerStdDevs, + List innerHeterogeneities, + double meanProcessedArea, + double processedAreaP10, + double processedAreaP50, + double processedAreaP90, + double processedAreaStdDev, + double interHeterogeneity, + double processedAreaDeltaP90P10, + double unprocessedFraction, + double meanInnerHeterogeneity, + double meanInnerStdDev, + double meanHotspotArea, + double meanOpticalIntegral + ) PostProcess(List valueData, List valueCount, double mixtureNormalRate) => PostProcess(valueData.Chunk(valueCount).Select(v => v.ToArray()).ToList(), mixtureNormalRate); + + public static ( + int particlesCount, + List values, + List normalizedValues, + List processedAreas, + List unprocessedAreas, + List hotspotAreas, + List opticalIntegrals, + List innerStdDevs, + List innerHeterogeneities, + double meanProcessedArea, + double processedAreaP10, + double processedAreaP50, + double processedAreaP90, + double processedAreaStdDev, + double interHeterogeneity, + double processedAreaDeltaP90P10, + double unprocessedFraction, + double meanInnerHeterogeneity, + double meanInnerStdDev, + double meanHotspotArea, + double meanOpticalIntegral + ) PostProcess(List values, double mixtureNormalRate) + { + var normalizedValues = new List(); + var processedAreas = new List(); + var unprocessedAreas = new List(); + var hotspotAreas = new List(); + var opticalIntegrals = new List(); + var innerStdDevs = new List(); + var innerHeterogeneities = new List(); + + var max = values.Max(vs => vs.Max()); + foreach (var value in values) + { + // Находим нормализованное значение + var normalized = new double[value.Count()]; + for (int i = 0; i < value.Count(); i++) + if (value[i] <= mixtureNormalRate * UNPROCESSED_THRESHOLD) + normalized[i] = value[i] / (mixtureNormalRate * UNPROCESSED_THRESHOLD); + else if (value[i] <= mixtureNormalRate * HOTSPOT_THRESHOLD) + normalized[i] = 1 + (value[i] - (mixtureNormalRate * UNPROCESSED_THRESHOLD)) / ((mixtureNormalRate * HOTSPOT_THRESHOLD) - (mixtureNormalRate * UNPROCESSED_THRESHOLD)); + else + normalized[i] = 2 + (value[i] - (mixtureNormalRate * HOTSPOT_THRESHOLD)) / (max - (mixtureNormalRate * HOTSPOT_THRESHOLD)); + + // Вычисляем значения + var area = normalized.Count(); + var processedArea = normalized.Count(v => v > 1) / area * 100; + var unprocessedArea = normalized.Count(v => v <= 1) / area * 100; + var hotspotArea = normalized.Count(v => v > 2) / area * 100; + var opticalIntegral = normalized.Sum() / area * 100; + var innerStdDev = normalized.StdDev(); + var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON); + + // Добавляем значения в списки + normalizedValues.Add(normalized); + processedAreas.Add(processedArea); + unprocessedAreas.Add(unprocessedArea); + hotspotAreas.Add(hotspotArea); + opticalIntegrals.Add(opticalIntegral); + innerStdDevs.Add(innerStdDev); + innerHeterogeneities.Add(innerHeterogeneity); + } + + // Вычисляем общие значения + var meanProcessedArea = processedAreas.Mean(); + var processedAreaP10 = processedAreas.Percentile(10); + var processedAreaP50 = processedAreas.Percentile(50); + var processedAreaP90 = processedAreas.Percentile(90); + var processedAreaStdDev = processedAreas.StdDev(); + var interHeterogeneity = processedAreaStdDev / meanProcessedArea * 100; + var processedAreaDeltaP90P10 = processedAreaP90 - processedAreaP10; + var unprocessedFraction = (double)unprocessedAreas.Count(v => v > UNPROCESSED_FRACTION_THRESHOLD) / unprocessedAreas.Count() * 100; + var meanInnerHeterogeneity = innerHeterogeneities.Mean(); + var meanInnerStdDev = innerStdDevs.Mean(); + var meanHotspotArea = hotspotAreas.Mean(); + var meanOpticalIntegral = opticalIntegrals.Mean(); + + return + ( + values.Count(), + values, + normalizedValues, + processedAreas, + unprocessedAreas, + hotspotAreas, + opticalIntegrals, + innerStdDevs, + innerHeterogeneities, + meanProcessedArea, + processedAreaP10, + processedAreaP50, + processedAreaP90, + processedAreaStdDev, + interHeterogeneity, + processedAreaDeltaP90P10, + unprocessedFraction, + meanInnerHeterogeneity, + meanInnerStdDev, + meanHotspotArea, + meanOpticalIntegral + ); + } +} diff --git a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs index d561af4..ae40699 100644 --- a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs +++ b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs @@ -494,86 +494,34 @@ public partial class AnalysisViewModel : ViewModelBase InnerStdDevs.Clear(); InnerHeterogeneities.Clear(); - ParticlesCount = Values.Count(); - - if (Values.Count() == 0) - { - MeanProcessedArea = -1; - ProcessedAreaP10 = -1; - ProcessedAreaP50 = -1; - ProcessedAreaP90 = -1; - ProcessedAreaStdDev = -1; - InterHeterogeneity = -1; - ProcessedAreaDeltaP90P10 = -1; - UnprocessedFraction = -1; - MeanInnerHeterogeneity = -1; - MeanInnerStdDev = -1; - MeanHotspotArea = -1; - MeanOpticalIntegral = -1; - - ProcessedAreasHistogram.Clear(); - UnprocessedAreasHistogram.Clear(); - HotspotAreasHistogram.Clear(); - OpticalIntegralsHistogram.Clear(); - InnerStdDevsHistogram.Clear(); - InnerHeterogeneitiesHistogram.Clear(); - - ParticleIndex = 0; - - return; - } - - // Для каждой гранулы - var max = Values.Max(vs => vs.Max()); - foreach (var value in Values) - { - // Находим нормализованное значение - var normalized = new double[value.Count()]; - for (int i = 0; i < value.Length; i++) - if (value[i] <= MixtureNormalRate * UNPROCESSED_THRESHOLD) - normalized[i] = value[i] / (MixtureNormalRate * UNPROCESSED_THRESHOLD); - else if (value[i] <= MixtureNormalRate * HOTSPOT_THRESHOLD) - normalized[i] = 1 + (value[i] - (MixtureNormalRate * UNPROCESSED_THRESHOLD)) / ((MixtureNormalRate * HOTSPOT_THRESHOLD) - (MixtureNormalRate * UNPROCESSED_THRESHOLD)); - else - normalized[i] = 2 + (value[i] - (MixtureNormalRate * HOTSPOT_THRESHOLD)) / (max - (MixtureNormalRate * HOTSPOT_THRESHOLD)); - - // Вычисляем значения - var area = normalized.Count(); - var processedArea = normalized.Count(v => v > 1) * 100.0 / area; - var unprocessedArea = normalized.Count(v => v <= 1) * 100.0 / area; - var hotspotArea = normalized.Count(v => v > 2) * 100.0 / area; - var opticalIntegral = normalized.Sum() * 100.0 / area; - var innerStdDev = normalized.StdDev(); - var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON); - - // Добавляем значения в списки - NormalizedValues.Add(normalized); - ProcessedAreas.Add(processedArea); - UnprocessedAreas.Add(unprocessedArea); - HotspotAreas.Add(hotspotArea); - OpticalIntegrals.Add(opticalIntegral); - InnerStdDevs.Add(innerStdDev); - InnerHeterogeneities.Add(innerHeterogeneity); - } - - // Вычисляем общие значения - MeanProcessedArea = ProcessedAreas.Mean(); - ProcessedAreaP10 = ProcessedAreas.Percentile(10); - ProcessedAreaP50 = ProcessedAreas.Percentile(50); - ProcessedAreaP90 = ProcessedAreas.Percentile(90); - ProcessedAreaStdDev = ProcessedAreas.StdDev(); - InterHeterogeneity = ProcessedAreaStdDev / MeanProcessedArea * 100; - ProcessedAreaDeltaP90P10 = ProcessedAreaP90 - ProcessedAreaP10; - UnprocessedFraction = (double)UnprocessedAreas.Count(v => v > UNPROCESSED_FRACTION_THRESHOLD) / UnprocessedAreas.Count() * 100; - MeanInnerHeterogeneity = InnerHeterogeneities.Mean(); - MeanInnerStdDev = InnerStdDevs.Mean(); - MeanHotspotArea = HotspotAreas.Mean(); - MeanOpticalIntegral = OpticalIntegrals.Mean(); + ( + 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, "Количество гранул, шт", "Нормированная \"горячая\" площадь, %"); + HotspotAreasHistogram.Set(HotspotAreas, 0, 100, "Количество гранул, шт", "Нормированная горячая площадь, %"); OpticalIntegralsHistogram.Set(OpticalIntegrals, 0, 3, "Количество гранул, шт", "Оптический интегральный показатель, %"); InnerStdDevsHistogram.Set(InnerStdDevs, InnerStdDevs.Min(), InnerStdDevs.Max(), "Количество гранул, шт", "Внутригранульное среднеквадратичное отклонение"); InnerHeterogeneitiesHistogram.Set(InnerHeterogeneities, InnerHeterogeneities.Min(), InnerHeterogeneities.Max(), "Количество гранул, шт", "Внутригранульная однородность"); @@ -1904,17 +1852,17 @@ public partial class AnalysisViewModel : ViewModelBase // Выичсляем максимальное значение OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max); - // Если иаксмиальное значниеz слишком мало, то устанавливаем его чуть больше hotspotsThreshold, чтобы не возникло деления на ноль и отридцательных значений + // Если максимальное значние слишком мало, то устанавливаем его чуть больше 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 -> чёрный...белый + // Раскрашиваем диапазон 0...unprocessedThreshold -> чёрный...белый ApplyGradientToRange(resultImage, 0, unprocessedThreshold, OpenCvSharp.Scalar.Black, OpenCvSharp.Scalar.White, coloredResultImage); - // Расшрашиваем диапазон unprocessedThreshold...hotspotsThreshold -> белый...зелёный + // Раскрашиваем диапазон unprocessedThreshold...hotspotsThreshold -> белый...зелёный ApplyGradientToRange(resultImage, unprocessedThreshold, hotspotsThreshold, OpenCvSharp.Scalar.White, OpenCvSharp.Scalar.Green, coloredResultImage); - // Расшрашиваем диапазон hotspotsThreshold...max -> зелёный...крастный + // Раскрашиваем диапазон hotspotsThreshold...max -> зелёный...красный ApplyGradientToRange(resultImage, hotspotsThreshold, max, OpenCvSharp.Scalar.Green, OpenCvSharp.Scalar.Red, coloredResultImage); // Устанавливаем в 0 всё что не попадает в маску