From 81ed14fd99542aef31ba688e6ce35709b69540b9 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Wed, 17 Jun 2026 10:39:40 +0300 Subject: [PATCH] feat: amine content analyzer changes add filtering with Mahalanobis fix some typos also fix it after database reforge --- .../Analysis/AmineContent/AnalyzerService.cs | 202 +++++++++++++----- GSS2.Core/GSS2.Core.csproj | 6 +- 2 files changed, 149 insertions(+), 59 deletions(-) diff --git a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs index 195aa80..87d0f62 100644 --- a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs +++ b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs @@ -5,7 +5,10 @@ using Microsoft.ML.Data; using OpenCvSharp; using GSS2.Core.Extensions; -using GSS2.Core.Analysis.AmineContent.Database.Calibration; +using GSS2.Core.Analysis.AmineContent.Database; + +using MathNet.Numerics.LinearAlgebra; +using MathNet.Numerics.LinearAlgebra.Double; namespace GSS2.Core.Analysis.AmineContent; @@ -29,6 +32,8 @@ public partial class AnalyzerService public const int SEPARATOR_WIDTH = 2969; public const int SEPARATOR_HEIGHT = 2965; + public const double FILTERING_THRESHOLD = 5.0; + private class ClusterizationData { [ColumnName("Label")] @@ -72,7 +77,7 @@ public partial class AnalyzerService } private readonly ILogger _logger; - private readonly CalibrationContext _calibrationContext; + private readonly Context _context; private MLContext? _ml = null; private PredictionEngine? _clusterizationEngine = null; @@ -80,13 +85,13 @@ public partial class AnalyzerService public bool Initialized { get; private set; } = false; - public AnalyzerService(ILogger logger, CalibrationContext calibrationContext) + public AnalyzerService(ILogger logger, Context Context) { _logger = logger; - _calibrationContext = calibrationContext; + _context = Context; } - public async Task Initialize(string? brand = null, CancellationToken cancellationToken = default) + public async Task Initialize(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken = default) { Initialized = false; @@ -101,7 +106,7 @@ public partial class AnalyzerService try { - await InitializeMlClusterizationEngine(cancellationToken); + await InitializeMlClusterizationEngine(brand, separator, cancellationToken); await InitializeMlRegressionEngine(brand, cancellationToken); } catch (Exception ex) @@ -112,83 +117,134 @@ public partial class AnalyzerService Initialized = true; } - private async Task InitializeMlClusterizationEngine(CancellationToken cancellationToken) + private async Task InitializeMlClusterizationEngine(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken) { - _logger.LogInformation("Инициализация модели кластеризации"); - - var separatorPrefix = "SEPARATOR"; - var samplePrefix = "SAMPLE"; + _logger.LogInformation("Инициализация модели кластеризации для марки: \"{} - {}\" и сепаратора: \"{}\"", brand.BrandName, brand.MixtureName, separator.Type); if (_ml is null) - throw new InvalidOperationException(); + throw new InvalidOperationException("Анализатор не инициализирован"); + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Подготовка данных"); + var sampleRecordIds = _context.SampleRecords + .Where(r => r.BrandId == brand.Id) + .Select(r => r.Id); + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); + if (sampleRecordIds.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); + + var separatorDataSet = _context.FeatureRecords + .Where(r => r.SeparatorRecordId == separator.Id) + .Select(r => new ClusterizationData + { + Label = "separator", + Features = r.Values.ToArray() + }); + _logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count()); + if (separatorDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\""); + cancellationToken.ThrowIfCancellationRequested(); + + var sampleDataSet = _context.FeatureRecords + .Where(r => r.SeparatorRecordId != 0) + .Where(r => sampleRecordIds.Contains(r.SampleRecordId)) + .Select(r => new ClusterizationData + { + Label = "sample", + Features = r.Values.ToArray() + }) + .Shuffle(); + _logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count()); + if (separatorDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); + + var trainDataSet = Enumerable.Concat(separatorDataSet, sampleDataSet); + _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); + var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Создание конвейера"); var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label") .Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy()) .Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel")); - - cancellationToken.ThrowIfCancellationRequested(); - - _logger.LogInformation("Подготовка данных"); - var trainDataSet = _calibrationContext.VectorRecords.Select(r => - new ClusterizationData - { - Label = r.SeparatorRecord != null ? - $"{separatorPrefix}|{r.SeparatorRecord.Type}" : - r.SampleRecord != null ? - $"{samplePrefix}|{r.SampleRecord.SampleBrand}" : - "", - Features = r.Values - }) - .ToList(); - trainDataSet = trainDataSet.Shuffle().ToList(); - _logger.LogInformation("Записей для обучения: {}", trainDataSet.Count()); - var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); - cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Обучение модели"); var model = pipeline.Fit(trainData); + cancellationToken.ThrowIfCancellationRequested(); _clusterizationEngine = _ml.Model.CreatePredictionEngine(model); model.Dispose(); } - private async Task InitializeMlRegressionEngine(string? brand, CancellationToken cancellationToken) + private async Task InitializeMlRegressionEngine(BrandRecord brand, CancellationToken cancellationToken) { - _logger.LogInformation("Инициализация модели регрессии для марки \"{}\"", brand); + _logger.LogInformation("Инициализация модели регрессии для марки \"{} - {}\"", brand.BrandName, brand.MixtureName); if (_ml is null) - throw new InvalidOperationException(); - - _logger.LogInformation("Создание конвейера"); - - var pipeline = _ml.Regression.Trainers.FastTree(); - + throw new InvalidOperationException("Анализатор не инициализирован"); cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Подготовка данных"); - var trainDataSet = _calibrationContext.VectorRecords - .Where(r => r.SampleRecord != null) - .Where(r => brand == null || r.SampleRecord!.SampleBrand == brand) + var sampleRecordIds = _context.SampleRecords + .Where(r => r.BrandId == brand.Id) + .Select(r => r.Id); + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); + if (sampleRecordIds.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); + + _logger.LogInformation("Подготовка данных"); + var trainDataSet = _context.FeatureRecords + .Where(r => r.SeparatorRecordId != 0) + .Where(r => sampleRecordIds.Contains(r.SampleRecordId)) + .ToList() .Select(r => - new RegressionData + { + var sample = _context.SampleRecords.First(s => s.Id == r.SampleRecordId); + return new RegressionData { Value = (float)( - r.SampleRecord!.MeasuredContent >= 0 ? - r.SampleRecord!.MeasuredContent : - r.SampleRecord!.MixtureActualRate >= 0 ? - r.SampleRecord!.MixtureActualRate : - r.SampleRecord!.MixtureNormalRate + sample.MeasuredContent >= 0 ? sample.MeasuredContent : + sample.MixtureActualRate ), - Features = r.Values - }) - .ToList(); - trainDataSet = trainDataSet.Shuffle().ToList(); - _logger.LogInformation("Записей для обучения: {}", trainDataSet.Count()); - var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); + Features = r.Values.ToArray() + }; + }) + .Shuffle(); + _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); + if (trainDataSet.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); + cancellationToken.ThrowIfCancellationRequested(); + int rowCount = trainDataSet.Count(); + double[][] featuresArray = trainDataSet.Select(d => d.Features.Select(f => (double)f).ToArray()).ToArray(); + Matrix matrix = DenseMatrix.OfRowArrays(featuresArray); + for (int i = 0; i < IMAGES_COUNT; i++) + { + matrix = matrix.RemoveColumn(i * 3); + matrix = matrix.RemoveColumn(i * 3); + matrix = matrix.RemoveColumn(i * 3); + } + cancellationToken.ThrowIfCancellationRequested(); + + Vector meanVector = matrix.ColumnSums() / matrix.RowCount; + Matrix covarianceMatrix = MatrixCovariance(matrix, meanVector); + Matrix invCovarianceMatrix = covarianceMatrix.Inverse(); + trainDataSet = trainDataSet.Where((data, index) => + { + var row = matrix.Row(index); + double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); + + return distance <= FILTERING_THRESHOLD; + }).ToList(); + _logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count()); + var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); + cancellationToken.ThrowIfCancellationRequested(); + + _logger.LogInformation("Создание конвейера"); + var pipeline = _ml.Regression.Trainers.FastTree(); cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Обучение модели"); @@ -311,9 +367,9 @@ public partial class AnalyzerService // var scale = oldMean / newMean; // Cv2.ConvertScaleAbs(result, result, scale); - // Добавляем эрозию к результатм и маске, удаляющую края, и пересчитываем контура - // поскольку края грунул подсвечиваются близлежайщими гранулами и сепаратором, - // их нельзя считаль представительными + // Добавляем эрозию к результатам и маске, удаляющую края, и пересчитываем контура + // поскольку края гранул подсвечиваются близлежащими гранулами и сепаратором, + // их нельзя считать представительными Cv2.Erode(result, result, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3))); Cv2.Erode(mask, mask, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3))); Cv2.FindContours(mask, out var newContours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple); @@ -563,7 +619,7 @@ public partial class AnalyzerService // Mat avgImageClone = new Mat(); // Mat mask = new Mat(); // Rect? roi = null; - + // avgImage.CopyTo(avgImageClone); // avgImageClone = avgImageClone.CvtColor(ColorConversionCodes.BGR2GRAY); @@ -588,4 +644,34 @@ public partial class AnalyzerService return CONTOUR_MIN_SIZE < bbox.Size.Width && CONTOUR_MIN_SIZE < bbox.Size.Height && CONTOUR_MIN_AREA < area && CONTOUR_MAX_SIZE > bbox.Size.Width && CONTOUR_MAX_SIZE > bbox.Size.Height && CONTOUR_MAX_AREA > area; } + + // Формула расстояния Махаланобиса: d = sqrt((x - mu)^T * Sigma^-1 * (x - mu)) + private static double CalculateMahalanobis(Vector x, Vector mean, Matrix invCovariance) + { + var diff = x - mean; + var distanceSq = diff * invCovariance * diff; + return Math.Sqrt(distanceSq); + } + + // Ручной расчет матрицы ковариации (для экономии памяти и контроля) + private static Matrix MatrixCovariance(Matrix matrix, Vector meanRows) + { + int rows = matrix.RowCount; + int cols = matrix.ColumnCount; + var cov = new DenseMatrix(cols, cols); + + for (int i = 0; i < cols; i++) + for (int j = i; j < cols; j++) + { + double sum = 0; + for (int k = 0; k < rows; k++) + { + sum += (matrix[k, i] - meanRows[i]) * (matrix[k, j] - meanRows[j]); + } + double val = sum / (rows - 1); + cov[i, j] = val; + cov[j, i] = val; + } + return cov; + } } diff --git a/GSS2.Core/GSS2.Core.csproj b/GSS2.Core/GSS2.Core.csproj index 2465a29..b987a23 100644 --- a/GSS2.Core/GSS2.Core.csproj +++ b/GSS2.Core/GSS2.Core.csproj @@ -7,6 +7,7 @@ + @@ -18,16 +19,19 @@ + - + + +