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 OpenCvSharp;
using GSS2.Core.Extensions; 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; namespace GSS2.Core.Analysis.AmineContent;
@@ -29,6 +32,8 @@ public partial class AnalyzerService
public const int SEPARATOR_WIDTH = 2969; public const int SEPARATOR_WIDTH = 2969;
public const int SEPARATOR_HEIGHT = 2965; public const int SEPARATOR_HEIGHT = 2965;
public const double FILTERING_THRESHOLD = 5.0;
private class ClusterizationData private class ClusterizationData
{ {
[ColumnName("Label")] [ColumnName("Label")]
@@ -72,7 +77,7 @@ public partial class AnalyzerService
} }
private readonly ILogger<AnalyzerService> _logger; private readonly ILogger<AnalyzerService> _logger;
private readonly CalibrationContext _calibrationContext; private readonly Context _context;
private MLContext? _ml = null; private MLContext? _ml = null;
private PredictionEngine<ClusterizationData, ClusterizationPrediction>? _clusterizationEngine = null; private PredictionEngine<ClusterizationData, ClusterizationPrediction>? _clusterizationEngine = null;
@@ -80,13 +85,13 @@ public partial class AnalyzerService
public bool Initialized { get; private set; } = false; public bool Initialized { get; private set; } = false;
public AnalyzerService(ILogger<AnalyzerService> logger, CalibrationContext calibrationContext) public AnalyzerService(ILogger<AnalyzerService> logger, Context Context)
{ {
_logger = logger; _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; Initialized = false;
@@ -101,7 +106,7 @@ public partial class AnalyzerService
try try
{ {
await InitializeMlClusterizationEngine(cancellationToken); await InitializeMlClusterizationEngine(brand, separator, cancellationToken);
await InitializeMlRegressionEngine(brand, cancellationToken); await InitializeMlRegressionEngine(brand, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
@@ -112,83 +117,134 @@ public partial class AnalyzerService
Initialized = true; Initialized = true;
} }
private async Task InitializeMlClusterizationEngine(CancellationToken cancellationToken) private async Task InitializeMlClusterizationEngine(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Инициализация модели кластеризации"); _logger.LogInformation("Инициализация модели кластеризации для марки: \"{} - {}\" и сепаратора: \"{}\"", brand.BrandName, brand.MixtureName, separator.Type);
var separatorPrefix = "SEPARATOR";
var samplePrefix = "SAMPLE";
if (_ml is null) 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(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Создание конвейера"); _logger.LogInformation("Создание конвейера");
var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label") var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label")
.Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy()) .Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy())
.Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel")); .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(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Обучение модели"); _logger.LogInformation("Обучение модели");
var model = pipeline.Fit(trainData); var model = pipeline.Fit(trainData);
cancellationToken.ThrowIfCancellationRequested();
_clusterizationEngine = _ml.Model.CreatePredictionEngine<ClusterizationData, ClusterizationPrediction>(model); _clusterizationEngine = _ml.Model.CreatePredictionEngine<ClusterizationData, ClusterizationPrediction>(model);
model.Dispose(); 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) if (_ml is null)
throw new InvalidOperationException(); throw new InvalidOperationException("Анализатор не инициализирован");
_logger.LogInformation("Создание конвейера");
var pipeline = _ml.Regression.Trainers.FastTree();
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Подготовка данных"); _logger.LogInformation("Подготовка данных");
var trainDataSet = _calibrationContext.VectorRecords var sampleRecordIds = _context.SampleRecords
.Where(r => r.SampleRecord != null) .Where(r => r.BrandId == brand.Id)
.Where(r => brand == null || r.SampleRecord!.SampleBrand == brand) .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 => .Select(r =>
new RegressionData {
var sample = _context.SampleRecords.First(s => s.Id == r.SampleRecordId);
return new RegressionData
{ {
Value = (float)( Value = (float)(
r.SampleRecord!.MeasuredContent >= 0 ? sample.MeasuredContent >= 0 ? sample.MeasuredContent :
r.SampleRecord!.MeasuredContent : sample.MixtureActualRate
r.SampleRecord!.MixtureActualRate >= 0 ?
r.SampleRecord!.MixtureActualRate :
r.SampleRecord!.MixtureNormalRate
), ),
Features = r.Values Features = r.Values.ToArray()
}) };
.ToList(); })
trainDataSet = trainDataSet.Shuffle().ToList(); .Shuffle();
_logger.LogInformation("Записей для обучения: {}", trainDataSet.Count()); _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count());
var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); 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(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Обучение модели"); _logger.LogInformation("Обучение модели");
@@ -311,9 +367,9 @@ public partial class AnalyzerService
// var scale = oldMean / newMean; // var scale = oldMean / newMean;
// Cv2.ConvertScaleAbs(result, result, scale); // Cv2.ConvertScaleAbs(result, result, scale);
// Добавляем эрозию к результатм и маске, удаляющую края, и пересчитываем контура // Добавляем эрозию к результатам и маске, удаляющую края, и пересчитываем контура
// поскольку края грунул подсвечиваются близлежайщими гранулами и сепаратором, // поскольку края гранул подсвечиваются близлежащими гранулами и сепаратором,
// их нельзя считаль представительными // их нельзя считать представительными
Cv2.Erode(result, result, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3))); 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.Erode(mask, mask, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)));
Cv2.FindContours(mask, out var newContours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple); Cv2.FindContours(mask, out var newContours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
@@ -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 && 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; 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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" /> <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.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" 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" Version="5.0.0" />
<PackageReference Include="Microsoft.ML.FastTree" Version="5.0.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" /> <PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" />
<PackageReference Include="System.Device.Gpio" Version="4.0.1" /> <PackageReference Include="System.Device.Gpio" Version="4.0.1" />
<PackageReference Include="Iot.Device.Bindings" 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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj" /> <ProjectReference Include="..\GSS2.LightsControl\CSharpClient\GSS2.LightsControl.CSharpClient.csproj" />
<ProjectReference Include="..\GSS2.FrameDecoders\GSS2.FrameDecoders.csproj" /> <ProjectReference Include="..\GSS2.FrameDecoders\GSS2.FrameDecoders.csproj" />
<!-- <ProjectReference Include="..\..\LibCameraSharp\LibCameraSharp\LibCameraSharp.csproj" /> -->
</ItemGroup> </ItemGroup>
</Project> </Project>