feat: amine content analyzer changes

add filtering with Mahalanobis
fix some typos
also fix it after database reforge
This commit is contained in:
2026-06-17 10:39:40 +03:00
parent 668136d2c9
commit 81ed14fd99
2 changed files with 149 additions and 59 deletions
@@ -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<AnalyzerService> _logger;
private readonly CalibrationContext _calibrationContext;
private readonly Context _context;
private MLContext? _ml = null;
private PredictionEngine<ClusterizationData, ClusterizationPrediction>? _clusterizationEngine = null;
@@ -80,13 +85,13 @@ public partial class AnalyzerService
public bool Initialized { get; private set; } = false;
public AnalyzerService(ILogger<AnalyzerService> logger, CalibrationContext calibrationContext)
public AnalyzerService(ILogger<AnalyzerService> 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<ClusterizationData, ClusterizationPrediction>(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<double> 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<double> meanVector = matrix.ColumnSums() / matrix.RowCount;
Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector);
Matrix<double> 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<double> x, Vector<double> mean, Matrix<double> invCovariance)
{
var diff = x - mean;
var distanceSq = diff * invCovariance * diff;
return Math.Sqrt(distanceSq);
}
// Ручной расчет матрицы ковариации (для экономии памяти и контроля)
private static Matrix<double> MatrixCovariance(Matrix<double> matrix, Vector<double> 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;
}
}
+5 -1
View File
@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
@@ -18,16 +19,19 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.ML" Version="5.0.0" />
<PackageReference Include="Microsoft.ML.FastTree" Version="5.0.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" />
<PackageReference Include="System.Device.Gpio" Version="4.0.1" />
<PackageReference Include="Iot.Device.Bindings" Version="4.0.1" />
<PackageReference Include="LibCameraSharp" Version="0.5.2-20260216-4" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="9.0.15" />
<PackageReference Include="LibCameraSharp" Version="0.5.2-20260615" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj" />
<ProjectReference Include="..\GSS2.FrameDecoders\GSS2.FrameDecoders.csproj" />
<!-- <ProjectReference Include="..\..\LibCameraSharp\LibCameraSharp\LibCameraSharp.csproj" /> -->
</ItemGroup>
</Project>