forked from amkovkov/GranuSightSoftware2
feat: amine content analyzer changes
add filtering with Mahalanobis fix some typos also fix it after database reforge
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user