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 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user