Files
GSS2Rework/GSS2.Core/Analysis/AmineContent/AmineContentAnalyzerService.cs
T

1540 lines
66 KiB
C#

using System.Diagnostics;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using GSS2.Core.Hardware;
using GSS2.Core.Extensions;
using GSS2.Core.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.ML;
using Microsoft.ML.Data;
using OpenCvSharp;
namespace GSS2.Core.Analysis.AmineContent;
public class AmineContentAnalyzerService
{
public const int HISTOGRAM_ROWS = 500;
public const int HISTOGRAM_COLORS = 256;
public const int HISTOGRAM_CHANNELS = 3;
public const int HISTOGRAM_TOTAL_COUNT = 12;
public const int HISTOGRAM_VISIBLE_ONLY_COUNT = 3;
public const int HISTOGRAM_UV_COUNT = 9;
public const int SEPARATOR_TYPE_FEATURES_LENGTH = (HISTOGRAM_ROWS * HISTOGRAM_COLORS * HISTOGRAM_CHANNELS + 3) * HISTOGRAM_TOTAL_COUNT;
public const int SAMPLE_BRAND_FEATURES_LENGTH = (HISTOGRAM_COLORS * HISTOGRAM_CHANNELS + 1) * HISTOGRAM_VISIBLE_ONLY_COUNT;
public const int SAMPLE_AMINE_CONTENT_FEATURES_LENGTH = (HISTOGRAM_CHANNELS) * HISTOGRAM_UV_COUNT + 1;
public const float CONTOUR_MAX_SIZE = 100;
public const float CONTOUR_MAX_AREA = (float)Math.PI * (CONTOUR_MAX_SIZE * CONTOUR_MAX_SIZE) / 4.0f;
public const float CONTOUR_MIN_SIZE = 12;
public const float CONTOUR_MIN_AREA = (float)Math.PI * (CONTOUR_MIN_SIZE * CONTOUR_MIN_SIZE) / 4.0f / 4.0f;
private const string _cachePath = "./cache";
private class SeparatorTypeData
{
[ColumnName("Label")]
public required int Id { get; set; }
[ColumnName("Features")]
[VectorType(SEPARATOR_TYPE_FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class SeparatorTypePredictionResult
{
[ColumnName("PredictedLabel")]
public int Id { get; set; }
public float Probability { get; set; }
public float[] Score { get; set; }
public SeparatorTypePredictionResult()
{
Id = -1;
Probability = 0;
Score = new float[2];
}
}
private class SampleBrandData
{
[ColumnName("Label")]
public required int Id { get; set; }
[ColumnName("Features")]
[VectorType(SAMPLE_BRAND_FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class SampleBrandPredictionResult
{
[ColumnName("PredictedLabel")]
public int Id { get; set; }
public float Probability { get; set; }
public float[] Score { get; set; }
public SampleBrandPredictionResult()
{
Id = -1;
Probability = 0;
Score = new float[2];
}
}
private class SampleAmineContentClusterizationData
{
[ColumnName("Label")]
public required bool IsProcessed { get; set; }
[ColumnName("Features")]
[VectorType(SAMPLE_AMINE_CONTENT_FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class SampleAmineContentClusterizationPredictionResult
{
[ColumnName("PredictedLabel")]
public bool IsProcessed { get; set; }
public float Probability { get; set; }
public float Score { get; set; }
public SampleAmineContentClusterizationPredictionResult()
{
IsProcessed = false;
Probability = 0;
Score = 0;
}
}
private class SampleAmineContentRegressionData
{
[ColumnName("Label")]
public required float AmineContent { get; set; }
[ColumnName("Features")]
[VectorType(SAMPLE_AMINE_CONTENT_FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class SampleAmineContentRegressionPredictionResult
{
[ColumnName("Score")]
public float AmineContent { get; set; }
public SampleAmineContentRegressionPredictionResult()
{
AmineContent = -1;
}
}
private readonly ILogger<AmineContentAnalyzerService> _logger;
private readonly AmineContentCalibrationContext _calibrationContext;
private readonly ImageStorageService _imageStorage;
private MLContext? _ml = null;
private PredictionEngine<SeparatorTypeData, SeparatorTypePredictionResult>? _separatorTypePredictionEngine = null;
private PredictionEngine<SampleBrandData, SampleBrandPredictionResult>? _sampleBrandPredictionEngine = null;
private PredictionEngine<SampleAmineContentRegressionData, SampleAmineContentRegressionPredictionResult>? _sampleAmineContentRegressionPredictionEngine = null;
private PredictionEngine<SampleAmineContentClusterizationData, SampleAmineContentClusterizationPredictionResult>? _sampleAmineContentClusterizationPredictionEngine = null;
public bool Initialized { get; private set; } = false;
public AmineContentAnalyzerService(ILogger<AmineContentAnalyzerService> logger, AmineContentCalibrationContext calibrationContext, ImageStorageService imageStorage)
{
_logger = logger;
_calibrationContext = calibrationContext;
_imageStorage = imageStorage;
}
public async Task Initialize(CancellationToken cancellationToken = default)
{
Initialized = false;
await InitializeMl(cancellationToken);
Initialized = true;
}
private async Task InitializeMl(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_ml = new MLContext();
_ml.Log += (_, ea) =>
{
if (ea.Kind >= Microsoft.ML.Runtime.ChannelMessageKind.Info)
_logger.LogInformation("ML Log: [{}] {}", ea.Kind, ea.Message);
};
await InitializeMlSeparatorTypePredictor(cancellationToken);
await InitializeMlSampleBrandPredictor(cancellationToken);
await InitializeMlSampleAmineContentRegressionPredictor(cancellationToken);
// await InitializeMlSampleAmineContentClusterizationPredictor(cancellationToken);
}
private async Task InitializeMlSeparatorTypePredictor(CancellationToken cancellationToken)
{
_logger.LogInformation("Initializing separator type predictor");
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
IEnumerable<SeparatorTypeData> TrainDataGenerator()
{
_logger.LogInformation("Preparing train data");
foreach (var separatorRecord in _calibrationContext.SeparatorRecords.Include(r => r.Images))
{
var imagePaths = GetImageRecordsFilePaths(separatorRecord.Images);
var roi = CalculateSeparatorRoi(imagePaths, 5, cancellationToken);
if (roi is null)
continue;
var features = CreateSeparatorTypeFeatureVector(separatorRecord.Images, roi.Value, cancellationToken);
yield return new SeparatorTypeData
{
Id = separatorRecord.Id,
Features = features
};
}
}
cancellationToken.ThrowIfCancellationRequested();
var modelPath = "separator_predictor.zip";
if (File.Exists(modelPath))
{
var model = _ml.Model.Load(modelPath, out _);
_separatorTypePredictionEngine = _ml.Model.CreatePredictionEngine<SeparatorTypeData, SeparatorTypePredictionResult>(model);
(model as IDisposable)?.Dispose();
}
else
{
_logger.LogInformation("Building pipeline");
var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label")
.Append(_ml.MulticlassClassification.Trainers.NaiveBayes())
.Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
var trainData = _ml.Data.LoadFromEnumerable(TrainDataGenerator());
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Training model");
var model = pipeline.Fit(trainData);
_ml.Model.Save(model, trainData.Schema, modelPath);
_separatorTypePredictionEngine = _ml.Model.CreatePredictionEngine<SeparatorTypeData, SeparatorTypePredictionResult>(model);
model.Dispose();
}
}
private float[] CreateSeparatorTypeFeatureVector(IEnumerable<ImageRecord> imageRecords, Rect roi, CancellationToken cancellationToken)
{
var features = new float[SEPARATOR_TYPE_FEATURES_LENGTH];
int i = 0;
foreach (var imageRecord in imageRecords
.OrderBy(r => r.VisibleIntensity)
.OrderBy(r => r.Uv365Intensity)
.OrderBy(r => r.Uv254Intensity)
)
{
var imagePath = _imageStorage.GetFullPath(imageRecord.ImagePath);
if (imagePath is null)
throw new Exception($"Image not found {imageRecord.ImagePath}");
var histogram = CalculateRowHistogram(imagePath, roi, cancellationToken);
features[i++] = (float)imageRecord.VisibleIntensity;
features[i++] = (float)imageRecord.Uv365Intensity;
features[i++] = (float)imageRecord.Uv254Intensity;
for (int r = 0; r < HISTOGRAM_ROWS; r++)
for (int c = 0; c < HISTOGRAM_COLORS; c++)
for (int ch = 0; ch < HISTOGRAM_CHANNELS; ch++)
features[i++] = histogram[r, c, ch];
}
return features;
}
private async Task InitializeMlSampleBrandPredictor(CancellationToken cancellationToken)
{
_logger.LogInformation("Initializing sample brand predictor");
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
IEnumerable<SampleBrandData> TrainDataGenerator()
{
_logger.LogInformation("Preparing train data");
foreach (var calibrationRecord in _calibrationContext.CalibrationRecords
.Include(r => r.SampleImages)
.Include(r => r.SeparatorImages)
)
{
cancellationToken.ThrowIfCancellationRequested();
var sampleImagePaths = GetImageRecordsFilePaths(calibrationRecord.SampleImages);
var separatorImagePaths = GetImageRecordsFilePaths(calibrationRecord.SeparatorImages);
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken);
if (sampleRoi is null)
continue;
var features = CreateSampleBrandFeatureVector(calibrationRecord.SampleImages, calibrationRecord.SeparatorImages, sampleRoi.Value, cancellationToken);
yield return new SampleBrandData
{
Id = calibrationRecord.Id,
Features = features
};
}
}
cancellationToken.ThrowIfCancellationRequested();
var modelPath = "brand_predictor.zip";
if (File.Exists(modelPath))
{
var model = _ml.Model.Load(modelPath, out _);
_sampleBrandPredictionEngine = _ml.Model.CreatePredictionEngine<SampleBrandData, SampleBrandPredictionResult>(model);
(model as IDisposable)?.Dispose();
}
else
{
_logger.LogInformation("Building pipeline");
var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label")
.Append(_ml.MulticlassClassification.Trainers.NaiveBayes())
.Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
var trainData = _ml.Data.LoadFromEnumerable(TrainDataGenerator());
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Training model");
var model = pipeline.Fit(trainData);
_ml.Model.Save(model, trainData.Schema, modelPath);
_sampleBrandPredictionEngine = _ml.Model.CreatePredictionEngine<SampleBrandData, SampleBrandPredictionResult>(model);
model.Dispose();
}
}
private float[] CreateSampleBrandFeatureVector(IEnumerable<ImageRecord> sampleImageRecords, IEnumerable<ImageRecord> separatorImageRecords, Rect sampleRoi, CancellationToken cancellationToken)
{
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
var separatorImagePaths = GetImageRecordsFilePaths(separatorImageRecords);
var features = new float[SAMPLE_BRAND_FEATURES_LENGTH];
int i = 0;
_logger.LogInformation("Segmenting images");
var contours = CalculateContours(sampleImagePaths, separatorImagePaths, cancellationToken);
var visibleSampleImageRecords = sampleImageRecords
.Where(r => r.Uv365Intensity == 0 && r.Uv254Intensity == 0)
.OrderBy(r => r.VisibleIntensity);
foreach (var imageRecord in visibleSampleImageRecords)
{
var imagePath = _imageStorage.GetFullPath(imageRecord.ImagePath);
if (imagePath is null)
throw new Exception($"Image not found {imageRecord.ImagePath}");
var histogram = CalculateContoursHistogram(imagePath, sampleRoi, contours, cancellationToken);
features[i++] = (float)imageRecord.VisibleIntensity;
for (int c = 0; c < HISTOGRAM_COLORS; c++)
for (int ch = 0; ch < HISTOGRAM_CHANNELS; ch++)
features[i++] = histogram[c, ch];
}
return features;
}
private async Task InitializeMlSampleAmineContentRegressionPredictor(CancellationToken cancellationToken)
{
_logger.LogInformation("Initializing sample amine content regression predictor");
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
IEnumerable<SampleAmineContentRegressionData> TrainDataGenerator()
{
_logger.LogInformation("Preparing train data");
var calibrationRecords = _calibrationContext.CalibrationRecords
.Include(r => r.SampleImages)
.Include(r => r.SeparatorImages)
.ToList();
var ratios = calibrationRecords
.GroupBy(r => r.SampleBrand)
.Where(g => g.Key is not null)
.Select(g => new KeyValuePair<string, float>(
g.Key!,
Math.Clamp(
(float)g.Count(r => r.MixtureActualRate is not null && r.MixtureActualRate == 0) * 180 /
(float)g.Count(r => r.MixtureActualRate is not null && r.MixtureActualRate != 0),
1,
100
))
).ToDictionary();
foreach (var ratio in ratios)
_logger.LogInformation("Correction of data quantity imbalance ratio: {} = {}", ratio.Key, ratio.Value);
var dataYielded = new Dictionary<string, (int processed, int nonProcessed)>();
foreach (var (i, calibrationRecord) in calibrationRecords.Enumerate())
{
if (calibrationRecord.MixtureActualRate is null)
continue;
if (calibrationRecord.SampleBrand is null)
continue;
_logger.LogInformation("{}: Id={}, Brand={}, Name={}, AmineContent={}", i, calibrationRecord.Id, calibrationRecord.SampleBrand, calibrationRecord.SampleName, (float)calibrationRecord.MixtureActualRate.Value);
cancellationToken.ThrowIfCancellationRequested();
var sampleImageRecords = calibrationRecord.SampleImages;
var separatorImageRecords = calibrationRecord.SeparatorImages;
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
var separatorImagePaths = GetImageRecordsFilePaths(separatorImageRecords);
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken);
if (sampleRoi is null)
continue;
var contours = CalculateContours(sampleImagePaths, separatorImagePaths, cancellationToken);
var sampleMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
foreach (var contour in contours)
{
var bbox = Cv2.BoundingRect(contour);
var area = Cv2.ContourArea(contour);
if (bbox.Width > CONTOUR_MAX_SIZE ||
bbox.Height > CONTOUR_MAX_SIZE ||
area > CONTOUR_MAX_AREA)
continue;
if (bbox.Width < CONTOUR_MIN_SIZE ||
bbox.Height < CONTOUR_MIN_SIZE ||
area < CONTOUR_MIN_AREA)
continue;
Cv2.DrawContours(sampleMask, [contour], 0, new Scalar(255), -1, LineTypes.Link8);
}
cancellationToken.ThrowIfCancellationRequested();
var uvSampleImages = sampleImageRecords
.Where(r => r.Uv365Intensity != 0 || r.Uv254Intensity != 0);
if (calibrationRecord.MixtureAmineContent != 0)
// if (true)
{
var features = new float[SAMPLE_AMINE_CONTENT_FEATURES_LENGTH];
int j = 0;
features[j++] = 0;
// features[i++] = (float)calibrationRecord.Id; TODO
foreach (var sampleImageRecord in uvSampleImages)
{
var sampleImagePath = _imageStorage.GetFullPath(sampleImageRecord.ImagePath);
if (sampleImagePath is null)
throw new Exception($"Image not found {sampleImageRecord.ImagePath}");
// features[j++] = (float)sampleImageRecord.VisibleIntensity;
// features[j++] = (float)sampleImageRecord.Uv365Intensity;
// features[j++] = (float)sampleImageRecord.Uv254Intensity;
var sampleImage = Cv2.ImRead(sampleImagePath);
sampleImage = new Mat(sampleImage, sampleRoi.Value);
Cv2.GaussianBlur(sampleImage, sampleImage, new Size(5, 5), 1);
Cv2.Resize(sampleImage, sampleImage, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
var mean = Cv2.Mean(sampleImage, sampleMask);
features[j++] = (float)mean.Val0 / 255;
features[j++] = (float)mean.Val1 / 255;
features[j++] = (float)mean.Val2 / 255;
sampleImage.Release();
}
if (ratios.TryGetValue(calibrationRecord.SampleBrand, out var ratio))
{
for (int k = 0; k < ratio; k++)
{
if (!dataYielded.ContainsKey(calibrationRecord.SampleBrand))
dataYielded.Add(calibrationRecord.SampleBrand, new(0, 0));
dataYielded[calibrationRecord.SampleBrand] = new(dataYielded[calibrationRecord.SampleBrand].processed + 1, dataYielded[calibrationRecord.SampleBrand].nonProcessed);
yield return new SampleAmineContentRegressionData
{
AmineContent = (float)calibrationRecord.MixtureActualRate.Value,
Features = features
};
}
}
else
{
if (!dataYielded.ContainsKey(calibrationRecord.SampleBrand))
dataYielded.Add(calibrationRecord.SampleBrand, new(0, 0));
dataYielded[calibrationRecord.SampleBrand] = new(dataYielded[calibrationRecord.SampleBrand].processed + 1, dataYielded[calibrationRecord.SampleBrand].nonProcessed);
yield return new SampleAmineContentRegressionData
{
AmineContent = (float)calibrationRecord.MixtureActualRate.Value,
Features = features
};
}
}
else
{
var histograms = new List<float[,]>();
foreach (var sampleImageRecord in uvSampleImages)
{
var sampleImagePath = _imageStorage.GetFullPath(sampleImageRecord.ImagePath);
if (sampleImagePath is null)
throw new Exception($"Image not found {sampleImageRecord.ImagePath}");
histograms.Add(CalculateContoursHistogram(sampleImagePath, sampleRoi.Value, contours, cancellationToken));
}
for (var color = 0; color < HISTOGRAM_COLORS; color++)
{
if (histograms.All(h => h[color, 0] == 0 && h[color, 1] == 0 && h[color, 2] == 0))
continue;
var features = new float[SAMPLE_AMINE_CONTENT_FEATURES_LENGTH];
int j = 0;
features[j++] = 0;
// features[i++] = (float)calibrationRecord.Id; TODO
foreach (var pair in Enumerable.Zip(uvSampleImages, histograms))
{
// features[j++] = (float)pair.First.VisibleIntensity;
// features[j++] = (float)pair.First.Uv365Intensity;
// features[j++] = (float)pair.First.Uv254Intensity;
features[j++] = (float)pair.Second[color, 0] / 255;
features[j++] = (float)pair.Second[color, 1] / 255;
features[j++] = (float)pair.Second[color, 2] / 255;
}
if (!dataYielded.ContainsKey(calibrationRecord.SampleBrand))
dataYielded.Add(calibrationRecord.SampleBrand, new(0, 0));
dataYielded[calibrationRecord.SampleBrand] = new(dataYielded[calibrationRecord.SampleBrand].processed, dataYielded[calibrationRecord.SampleBrand].nonProcessed + 1);
yield return new SampleAmineContentRegressionData
{
AmineContent = (float)calibrationRecord.MixtureActualRate.Value,
Features = features
};
}
}
}
_logger.LogInformation("Train data preparation finished");
foreach (var yielded in dataYielded)
_logger.LogInformation("Current data quantity imbalance ratio (non processed per processed): {} = {}", yielded.Key, (float)yielded.Value.nonProcessed / (float)yielded.Value.processed);
}
cancellationToken.ThrowIfCancellationRequested();
var modelPath = "amine_content_regression_predictor.zip";
var datasetPath = "amine_content_regression_dataset.bin";
if (File.Exists(modelPath))
{
var model = _ml.Model.Load(modelPath, out _);
_sampleAmineContentRegressionPredictionEngine = _ml.Model.CreatePredictionEngine<SampleAmineContentRegressionData, SampleAmineContentRegressionPredictionResult>(model);
(model as IDisposable)?.Dispose();
}
else
{
_logger.LogInformation("Building pipeline");
var pipeline = _ml.Regression.Trainers.Sdca(maximumNumberOfIterations: 30000); // Работает, в принципе можно юзать
IDataView trainData;
if (File.Exists(datasetPath))
{
trainData = _ml.Data.LoadFromBinary(datasetPath);
}
else
{
var trainDataSet = TrainDataGenerator().ToList();
trainDataSet.Shuffle();
trainData = _ml.Data.LoadFromEnumerable(trainDataSet);
using (var stream = File.OpenWrite(datasetPath))
_ml.Data.SaveAsBinary(trainData, stream);
}
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Training model");
var model = pipeline.Fit(trainData);
_ml.Model.Save(model, trainData.Schema, modelPath);
_sampleAmineContentRegressionPredictionEngine = _ml.Model.CreatePredictionEngine<SampleAmineContentRegressionData, SampleAmineContentRegressionPredictionResult>(model);
model.Dispose();
}
}
private async Task InitializeMlSampleAmineContentClusterizationPredictor(CancellationToken cancellationToken)
{
_logger.LogInformation("Initializing sample amine content clusterization predictor");
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
IEnumerable<SampleAmineContentClusterizationData> TrainDataGenerator()
{
_logger.LogInformation("Preparing train data");
var calibrationRecords = _calibrationContext.CalibrationRecords
.Include(r => r.SampleImages)
.Include(r => r.SeparatorImages)
.ToList();
foreach (var (i, calibrationRecord) in calibrationRecords.Enumerate())
{
if (calibrationRecord.MixtureActualRate is null)
continue;
if (calibrationRecord.SampleBrand is null)
continue;
_logger.LogInformation("{}: Id={}, Brand={}, Name={}, IsProcessed={}", i, calibrationRecord.Id, calibrationRecord.SampleBrand, calibrationRecord.SampleName, calibrationRecord.MixtureActualRate.Value > 0);
cancellationToken.ThrowIfCancellationRequested();
var sampleImageRecords = calibrationRecord.SampleImages;
var separatorImageRecords = calibrationRecord.SeparatorImages;
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
var separatorImagePaths = GetImageRecordsFilePaths(separatorImageRecords);
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken);
if (sampleRoi is null)
continue;
var contours = CalculateContours(sampleImagePaths, separatorImagePaths, cancellationToken);
var sampleMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
foreach (var contour in contours)
{
var bbox = Cv2.BoundingRect(contour);
var area = Cv2.ContourArea(contour);
if (bbox.Width > CONTOUR_MAX_SIZE ||
bbox.Height > CONTOUR_MAX_SIZE ||
area > CONTOUR_MAX_AREA)
continue;
if (bbox.Width < CONTOUR_MIN_SIZE ||
bbox.Height < CONTOUR_MIN_SIZE ||
area < CONTOUR_MIN_AREA)
continue;
Cv2.DrawContours(sampleMask, [contour], 0, new Scalar(255), -1, LineTypes.Link8);
}
cancellationToken.ThrowIfCancellationRequested();
var uvSampleImages = sampleImageRecords
.Where(r => r.Uv365Intensity != 0 || r.Uv254Intensity != 0);
var histograms = new List<float[,]>();
foreach (var sampleImageRecord in uvSampleImages)
{
var sampleImagePath = _imageStorage.GetFullPath(sampleImageRecord.ImagePath);
if (sampleImagePath is null)
throw new Exception($"Image not found {sampleImageRecord.ImagePath}");
histograms.Add(CalculateContoursHistogram(sampleImagePath, sampleRoi.Value, contours, cancellationToken));
}
for (var color = 0; color < HISTOGRAM_COLORS; color++)
{
if (histograms.All(h => h[color, 0] == 0 && h[color, 1] == 0 && h[color, 2] == 0))
continue;
var features = new float[SAMPLE_AMINE_CONTENT_FEATURES_LENGTH];
int j = 0;
features[j++] = 0;
// features[i++] = (float)calibrationRecord.Id; TODO
foreach (var pair in Enumerable.Zip(uvSampleImages, histograms))
{
features[j++] = (float)pair.Second[color, 0] / 255;
features[j++] = (float)pair.Second[color, 1] / 255;
features[j++] = (float)pair.Second[color, 2] / 255;
}
yield return new SampleAmineContentClusterizationData
{
IsProcessed = calibrationRecord.MixtureActualRate.Value > 0,
Features = features
};
}
}
}
cancellationToken.ThrowIfCancellationRequested();
var modelPath = "amine_content_clusterization_predictor.zip";
var datasetPath = "amine_content_clusterization_dataset.bin";
if (File.Exists(modelPath))
{
var model = _ml.Model.Load(modelPath, out _);
_sampleAmineContentClusterizationPredictionEngine = _ml.Model.CreatePredictionEngine<SampleAmineContentClusterizationData, SampleAmineContentClusterizationPredictionResult>(model);
(model as IDisposable)?.Dispose();
}
else
{
_logger.LogInformation("Building pipeline");
var pipeline = _ml.BinaryClassification.Trainers.LinearSvm();
IDataView trainData;
if (File.Exists(datasetPath))
{
trainData = _ml.Data.LoadFromBinary(datasetPath);
}
else
{
var trainDataSet = TrainDataGenerator().ToList();
trainDataSet.Shuffle();
trainData = _ml.Data.LoadFromEnumerable(trainDataSet);
using (var stream = File.OpenWrite(datasetPath))
_ml.Data.SaveAsBinary(trainData, stream);
}
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Training model");
var model = pipeline.Fit(trainData);
_ml.Model.Save(model, trainData.Schema, modelPath);
_sampleAmineContentClusterizationPredictionEngine = _ml.Model.CreatePredictionEngine<SampleAmineContentClusterizationData, SampleAmineContentClusterizationPredictionResult>(model);
model.Dispose();
}
}
public async Task<SeparatorRecord?> PredictSeparatorType(IEnumerable<ImageRecord> imageRecords, CancellationToken cancellationToken = default)
{
if (!Initialized)
throw new Exception("Not initialized");
cancellationToken.ThrowIfCancellationRequested();
if (imageRecords.Count() != HISTOGRAM_TOTAL_COUNT)
throw new Exception($"Image records count mismatch, expected {HISTOGRAM_TOTAL_COUNT}, got {imageRecords.Count()}");
var imagePaths = GetImageRecordsFilePaths(imageRecords);
var roi = CalculateSeparatorRoi(imagePaths, 5, cancellationToken);
if (roi is null)
throw new Exception($"Cannot calculate ROI");
var features = CreateSeparatorTypeFeatureVector(imageRecords, roi.Value, cancellationToken);
var predictionData = new SeparatorTypeData
{
Id = -1,
Features = features
};
_logger.LogInformation("Predicting separator");
var predictionResult = _separatorTypePredictionEngine?.Predict(predictionData);
if (predictionResult is null)
throw new UnreachableException();
_logger.LogInformation("Separator prediction result: id={}, probability={}, score={}", predictionResult.Id, predictionResult.Probability, predictionResult.Score);
var separatorRecord = _calibrationContext.SeparatorRecords.Include(r => r.Images)
.FirstOrDefault(r => r.Id == predictionResult.Id);
if (separatorRecord is null)
throw new UnreachableException();
return separatorRecord;
}
public async Task<float> CalculatePollutionRate(IEnumerable<ImageRecord> imageRecords, SeparatorRecord separatorRecord, CancellationToken cancellationToken = default)
{
if (!Initialized)
throw new Exception("Not initialized");
cancellationToken.ThrowIfCancellationRequested();
if (imageRecords.Count() != separatorRecord.Images.Count())
throw new Exception("Provided image records count not equal to provided separator record images count");
var pairs = Enumerable.Zip(
imageRecords
.OrderBy(r => r.VisibleIntensity)
.OrderBy(r => r.Uv365Intensity)
.OrderBy(r => r.Uv254Intensity),
separatorRecord.Images
.OrderBy(r => r.VisibleIntensity)
.OrderBy(r => r.Uv365Intensity)
.OrderBy(r => r.Uv254Intensity)
);
if (pairs.Any(p =>
p.First.VisibleIntensity != p.Second.VisibleIntensity ||
p.First.Uv365Intensity != p.Second.Uv365Intensity ||
p.First.Uv254Intensity != p.Second.Uv254Intensity))
throw new Exception("Image records intensities to correlate with separator images intensities");
var imagePaths = GetImageRecordsFilePaths(imageRecords);
var roi = CalculateSeparatorRoi(imagePaths, 5, cancellationToken);
if (roi is null)
throw new Exception($"Cannot calculate ROI");
var calibrationImagePaths = GetImageRecordsFilePaths(separatorRecord.Images);
var calibrationRoi = CalculateSeparatorRoi(calibrationImagePaths, 5, cancellationToken);
if (calibrationRoi is null)
throw new Exception($"Cannot calculate ROI");
var separatorMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
Cv2.Circle(separatorMask, HISTOGRAM_ROWS, HISTOGRAM_ROWS, HISTOGRAM_ROWS, new Scalar(255), -1);
float pollutionRateSum = 0;
_logger.LogInformation("Calculating pollution rate");
foreach (var pair in pairs)
{
var imageRecord = pair.First;
var imagePath = _imageStorage.GetFullPath(imageRecord.ImagePath);
if (imagePath is null)
throw new Exception($"Image not found {imageRecord.ImagePath}");
var image = Cv2.ImRead(imagePath);
image = new Mat(image, roi.Value);
Cv2.GaussianBlur(image, image, new Size(5, 5), 1);
Cv2.Resize(image, image, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Cv2.CvtColor(image, image, ColorConversionCodes.BGR2HSV);
var calibrationImageRecord = pair.Second;
var calibrationImagePath = _imageStorage.GetFullPath(calibrationImageRecord.ImagePath);
if (calibrationImagePath is null)
throw new Exception($"Image not found {calibrationImageRecord.ImagePath}");
var calibrationImage = Cv2.ImRead(calibrationImagePath);
calibrationImage = new Mat(calibrationImage, calibrationRoi.Value);
Cv2.GaussianBlur(calibrationImage, calibrationImage, new Size(5, 5), 1);
Cv2.Resize(calibrationImage, calibrationImage, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Cv2.CvtColor(calibrationImage, calibrationImage, ColorConversionCodes.BGR2HSV);
var diff = new Mat();
Cv2.Absdiff(image, calibrationImage, diff);
pollutionRateSum += (float)Cv2.Mean(diff, separatorMask).Val0 / 255;
pollutionRateSum += (float)Cv2.Mean(diff, separatorMask).Val1 / 255;
pollutionRateSum += (float)Cv2.Mean(diff, separatorMask).Val2 / 255;
diff.Release();
image.Release();
calibrationImage.Release();
}
var pollutionRate = pollutionRateSum / 3 / pairs.Count() / 0.2f;
return pollutionRate;
}
public async Task<CalibrationRecord> PredictSampleBrand(IEnumerable<ImageRecord> sampleImageRecords, IEnumerable<ImageRecord> separatorImageRecords, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
var separatorImagePaths = GetImageRecordsFilePaths(separatorImageRecords);
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken);
if (sampleRoi is null)
throw new Exception(/* TODO */);
var features = CreateSampleBrandFeatureVector(sampleImageRecords, separatorImageRecords, sampleRoi.Value, cancellationToken);
var predictionData = new SampleBrandData
{
Id = -1,
Features = features
};
_logger.LogInformation("Predicting sample");
var predictionResult = _sampleBrandPredictionEngine?.Predict(predictionData);
if (predictionResult is null)
throw new UnreachableException();
_logger.LogInformation("Sample prediction result: id={}, probability={}, score={}", predictionResult.Id, predictionResult.Probability, predictionResult.Score);
var calibrationRecord = _calibrationContext.CalibrationRecords
.Include(r => r.SampleImages)
.Include(r => r.SeparatorImages)
.FirstOrDefault(r => r.Id == predictionResult.Id);
if (calibrationRecord is null)
throw new UnreachableException();
return calibrationRecord;
}
public async Task<(Mat result, Mat mask)> CalculateAmineContent(IEnumerable<ImageRecord> sampleImageRecords, IEnumerable<ImageRecord> separatorImageRecords, CalibrationRecord calibrationRecord, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
var separatorImagePaths = GetImageRecordsFilePaths(separatorImageRecords);
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken);
if (sampleRoi is null)
throw new Exception(/* TODO */);
var contours = CalculateContours(sampleImagePaths, separatorImagePaths, cancellationToken);
var sampleMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
cancellationToken.ThrowIfCancellationRequested();
foreach (var contour in contours)
{
var bbox = Cv2.BoundingRect(contour);
var area = Cv2.ContourArea(contour);
if (bbox.Width > CONTOUR_MAX_SIZE ||
bbox.Height > CONTOUR_MAX_SIZE ||
area > CONTOUR_MAX_AREA)
continue;
if (bbox.Width < CONTOUR_MIN_SIZE ||
bbox.Height < CONTOUR_MIN_SIZE ||
area < CONTOUR_MIN_AREA)
continue;
Cv2.DrawContours(sampleMask, [contour], 0, new Scalar(255), -1, LineTypes.Link8);
}
Cv2.Resize(sampleMask, sampleMask, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
var uvSampleImageRecords = sampleImageRecords
.Where(r => r.Uv365Intensity != 0 || r.Uv254Intensity != 0)
.OrderBy(r => r.VisibleIntensity)
.OrderBy(r => r.Uv365Intensity)
.OrderBy(r => r.Uv254Intensity);
var uvSampleImagePaths = GetImageRecordsFilePaths(uvSampleImageRecords);
var uvSampleImages = uvSampleImagePaths.Select(uvSampleImagePath =>
{
var uvSampleImage = Cv2.ImRead(uvSampleImagePath);
uvSampleImage = new Mat(uvSampleImage, sampleRoi.Value);
Cv2.GaussianBlur(uvSampleImage, uvSampleImage, new Size(5, 5), 1);
Cv2.Resize(uvSampleImage, uvSampleImage, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
return uvSampleImage;
});
var uvSampleImage = new Mat();
Cv2.Merge(uvSampleImages.SelectMany(i => i.Split()).ToArray(), uvSampleImage);
uvSampleImages.ToList().ForEach(i => i.Release());
var result = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_32FC1, new Scalar(0));
var values = new float[HISTOGRAM_ROWS * HISTOGRAM_ROWS * 4];
for (int row = 0; row < HISTOGRAM_ROWS * 2; row++)
{
for (int column = 0; column < HISTOGRAM_ROWS * 2; column++)
{
cancellationToken.ThrowIfCancellationRequested();
if (sampleMask.Get<byte>(row, column) == 0)
continue;
var features = new float[SAMPLE_AMINE_CONTENT_FEATURES_LENGTH];
int i = 0;
// features[i++] = (float)calibrationRecord.Id;
features[i++] = 0;
foreach (var (image, uvSampleImageRecord) in uvSampleImageRecords.Enumerate())
{
cancellationToken.ThrowIfCancellationRequested();
features[i++] = (float)uvSampleImage.Get<byte>([row, column, image * 3 + 0]) / 255;
features[i++] = (float)uvSampleImage.Get<byte>([row, column, image * 3 + 1]) / 255;
features[i++] = (float)uvSampleImage.Get<byte>([row, column, image * 3 + 2]) / 255;
}
// var clusterizationPredictionData = new SampleAmineContentClusterizationData
// {
// IsProcessed = false,
// Features = features
// };
// var clusterizationPredictionResult = _sampleAmineContentClusterizationPredictionEngine?.Predict(clusterizationPredictionData);
// if (clusterizationPredictionResult is null)
// throw new UnreachableException();
// if (!clusterizationPredictionResult.IsProcessed)
// {
// result.Set<float>(row, column, 0);
// values[row * HISTOGRAM_ROWS * 2 + column] = 0;
// continue;
// }
// else
// {
// result.Set<float>(row, column, 1);
// values[row * HISTOGRAM_ROWS * 2 + column] = 1;
// continue;
// }
var regressionPredictionData = new SampleAmineContentRegressionData
{
AmineContent = -1,
Features = features
};
var regressionPredictionResult = _sampleAmineContentRegressionPredictionEngine?.Predict(regressionPredictionData);
if (regressionPredictionResult is null)
throw new UnreachableException();
result.Set<float>(row, column, regressionPredictionResult.AmineContent);
values[row * HISTOGRAM_ROWS * 2 + column] = regressionPredictionResult.AmineContent;
}
}
// Срезаем 99.9 процентиль (0.1% самых горячих пикселей), которые скорее всего являются шумом или загрязнением
var oldMean = Cv2.Mean(result, sampleMask).Val0;
values.Sort();
float p99 = values[(int)(values.Length * 0.999)];
Cv2.Threshold(result, result, p99, p99, ThresholdTypes.Trunc);
var newMean = Cv2.Mean(result, sampleMask).Val0;
var scale = oldMean / newMean;
Cv2.ConvertScaleAbs(result, result, scale);
uvSampleImage.Release();
return (result, sampleMask);
}
private IEnumerable<string> GetImageRecordsFilePaths(IEnumerable<ImageRecord> imageRecords) =>
imageRecords.Select(r =>
{
var imagePath = _imageStorage.GetFullPath(r.ImagePath);
if (imagePath is null)
throw new Exception($"Image not found {r.ImagePath}");
return imagePath;
});
private Mat CalculateAverageImage(IEnumerable<string> imagePaths, CancellationToken cancellationToken)
{
_logger.LogDebug("Calculating average image");
cancellationToken.ThrowIfCancellationRequested();
if (imagePaths.Count() == 0)
return new Mat();
cancellationToken.ThrowIfCancellationRequested();
var cachePath = Path.Join(_cachePath, "avg_" + GetShortCacheKey(imagePaths) + ".bmp");
if (File.Exists(cachePath))
{
_logger.LogDebug("Average image found in cache");
return Cv2.ImRead(cachePath);
}
_logger.LogDebug("Average image not found in cache");
Mat avgImage = new Mat();
Mat? image = null;
try
{
foreach (var imagePath in imagePaths)
{
cancellationToken.ThrowIfCancellationRequested();
image = Cv2.ImRead(imagePath);
if (image.Channels() != 3)
throw new Exception($"Image channels count mismatch, expected 3, got {image.Channels()}");
if (avgImage.Empty())
avgImage = new Mat(image.Size(), MatType.CV_16UC3, new Scalar(0, 0, 0));
if (image.Size().Width != avgImage.Size().Width ||
image.Size().Height != avgImage.Size().Height)
throw new Exception($"Image sizes mismatch, expected {avgImage.Size()}, got {image.Size()}");
Cv2.Add(image, avgImage, avgImage, dtype: (int)MatType.CV_16UC3);
image.Release();
}
avgImage.ConvertTo(avgImage, MatType.CV_8UC(avgImage.Channels()), 1f / imagePaths.Count());
if (!avgImage.Empty())
avgImage.ImWrite(path: cachePath);
}
finally
{
image?.Release();
}
return avgImage;
}
private Rect? CalculateSeparatorRoi(IEnumerable<string> imagePaths, int threshold, CancellationToken cancellationToken)
{
_logger.LogDebug("Calculating separator ROI");
cancellationToken.ThrowIfCancellationRequested();
if (imagePaths.Count() == 0)
return null;
cancellationToken.ThrowIfCancellationRequested();
var cachePath = Path.Join(_cachePath, "roi_" + GetShortCacheKey(imagePaths) + ".mda");
if (File.Exists(cachePath))
{
_logger.LogDebug("Separator ROI found in cache");
Array array;
using (var stream = File.OpenRead(cachePath))
array = MultiDimensionalArraySerializer.Deserialize<int>(stream);
if (array is int[] typedArray &&
typedArray.Length == 4)
return new Rect(typedArray[0], typedArray[1], typedArray[2], typedArray[3]);
_logger.LogWarning("Separator ROI cache data corrupted");
}
_logger.LogDebug("Separator ROI not found in cache");
Mat? avgImage = null;
Mat? mask = null;
Point[][]? contours = null;
try
{
cancellationToken.ThrowIfCancellationRequested();
avgImage = CalculateAverageImage(imagePaths, cancellationToken);
if (avgImage.Empty())
return null;
cancellationToken.ThrowIfCancellationRequested();
avgImage = avgImage.CvtColor(ColorConversionCodes.BGR2GRAY);
mask = new Mat();
cancellationToken.ThrowIfCancellationRequested();
Cv2.Threshold(avgImage, mask, threshold, 255, ThresholdTypes.Binary);
cancellationToken.ThrowIfCancellationRequested();
Cv2.FindContours(mask, out contours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
}
finally
{
avgImage?.Release();
mask?.Release();
}
cancellationToken.ThrowIfCancellationRequested();
var maxContour = contours.MaxBy(c => Cv2.ContourArea(c));
if (maxContour is null)
return null;
var roi = Cv2.BoundingRect(maxContour);
using (var stream = File.OpenWrite(cachePath))
MultiDimensionalArraySerializer.Serialize(stream, new int[4] { roi.X, roi.Y, roi.Width, roi.Height });
return roi;
}
private float[,,] CalculateRowHistogram(string imagePath, Rect roi, CancellationToken cancellationToken)
{
_logger.LogDebug("Calculating row histogram, imagePath: {}", imagePath);
cancellationToken.ThrowIfCancellationRequested();
var cachePath = Path.Join(_cachePath, "rows_hist_" + GetShortCacheKey([imagePath]) + ".mda");
if (File.Exists(cachePath))
{
_logger.LogDebug("Row histogram found in cache");
Array array;
using (var stream = File.OpenRead(cachePath))
array = MultiDimensionalArraySerializer.Deserialize<float>(stream);
if (array is float[,,] typedArray &&
typedArray.GetLength(0) == HISTOGRAM_ROWS &&
typedArray.GetLength(1) == HISTOGRAM_COLORS &&
typedArray.GetLength(2) == HISTOGRAM_CHANNELS)
return typedArray;
_logger.LogWarning("Row histogram cache data corrupted");
}
_logger.LogDebug("Row histogram not found in cache");
var image = Cv2.ImRead(imagePath);
image = new Mat(image, roi);
Cv2.GaussianBlur(image, image, new Size(5, 5), 1);
Cv2.Resize(image, image, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Mat[]? channels = null;
var histogram = new float[HISTOGRAM_ROWS, HISTOGRAM_COLORS, HISTOGRAM_CHANNELS];
try
{
cancellationToken.ThrowIfCancellationRequested();
if (image.Channels() != 3)
throw new Exception($"Image channels count mismatch, expected 3, got {image.Channels()}");
cancellationToken.ThrowIfCancellationRequested();
Cv2.WarpPolar(image, image, new Size(HISTOGRAM_ROWS, HISTOGRAM_ROWS), new Point(HISTOGRAM_ROWS, HISTOGRAM_ROWS), HISTOGRAM_ROWS, InterpolationFlags.Area, WarpPolarMode.Linear);
Cv2.Rotate(image, image, RotateFlags.Rotate90Clockwise);
cancellationToken.ThrowIfCancellationRequested();
Cv2.Split(image, out channels);
for (int row = 0; row < HISTOGRAM_ROWS; row++)
for (int channel = 0; channel < HISTOGRAM_CHANNELS; channel++)
{
cancellationToken.ThrowIfCancellationRequested();
var rowMat = channels[channel].Row(row);
var channelHistogram = new Mat();
Cv2.CalcHist([rowMat], [0], null, channelHistogram, 1, [HISTOGRAM_COLORS], [[0, HISTOGRAM_COLORS]]);
Cv2.Transpose(channelHistogram, channelHistogram);
channelHistogram.ConvertTo(channelHistogram, MatType.CV_32FC1, 1f / HISTOGRAM_ROWS);
channelHistogram.GetArray(out float[] channelHistogramData);
for (int col = 0; col < HISTOGRAM_COLORS; col++)
histogram[row, col, channel] = channelHistogramData[col];
channelHistogram.Release();
rowMat.Release();
}
}
finally
{
image.Release();
channels?.ToList()
.ForEach(i => i.Release());
}
using (var stream = File.OpenWrite(cachePath))
MultiDimensionalArraySerializer.Serialize(stream, histogram);
return histogram;
}
private float[,] CalculateContoursHistogram(string imagePath, Rect roi, IEnumerable<Point[]> contours, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var cachePath = Path.Join(_cachePath, "conts_hist_" + GetShortCacheKey([imagePath]) + ".mda");
if (File.Exists(cachePath))
{
Array array;
using (var stream = File.OpenRead(cachePath))
array = MultiDimensionalArraySerializer.Deserialize<float>(stream);
if (array is float[,] typedArray &&
typedArray.GetLength(0) == HISTOGRAM_COLORS &&
typedArray.GetLength(1) == HISTOGRAM_CHANNELS)
return typedArray;
}
var image = Cv2.ImRead(imagePath);
image = new Mat(image, roi);
Cv2.GaussianBlur(image, image, new Size(5, 5), 1);
Cv2.Resize(image, image, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Mat[]? channels = null;
var histogram = new float[HISTOGRAM_COLORS, HISTOGRAM_CHANNELS];
try
{
cancellationToken.ThrowIfCancellationRequested();
if (image.Channels() != 3)
throw new Exception($"Image channels count mismatch, expected 3, got {image.Channels()}");
var sampleMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
cancellationToken.ThrowIfCancellationRequested();
foreach (var contour in contours)
{
var bbox = Cv2.BoundingRect(contour);
var area = Cv2.ContourArea(contour);
if (bbox.Width > CONTOUR_MAX_SIZE ||
bbox.Height > CONTOUR_MAX_SIZE ||
area > CONTOUR_MAX_AREA)
continue;
if (bbox.Width < CONTOUR_MIN_SIZE ||
bbox.Height < CONTOUR_MIN_SIZE ||
area < CONTOUR_MIN_AREA)
continue;
Cv2.DrawContours(sampleMask, [contour], 0, new Scalar(255), -1, LineTypes.Link8);
}
var maskedPixelsCount = Cv2.CountNonZero(sampleMask);
cancellationToken.ThrowIfCancellationRequested();
Cv2.Split(image, out channels);
for (int channel = 0; channel < HISTOGRAM_CHANNELS; channel++)
{
cancellationToken.ThrowIfCancellationRequested();
var channelMat = channels[channel];
Cv2.BitwiseAnd(channelMat, sampleMask, channelMat);
var channelHistogram = new Mat();
Cv2.CalcHist([channelMat], [0], null, channelHistogram, 1, [HISTOGRAM_COLORS], [[0, HISTOGRAM_COLORS]]);
Cv2.Transpose(channelHistogram, channelHistogram);
channelHistogram.ConvertTo(channelHistogram, MatType.CV_32FC1, 1f / maskedPixelsCount);
channelHistogram.GetArray(out float[] channelHistogramData);
for (int col = 0; col < HISTOGRAM_COLORS; col++)
histogram[col, channel] = channelHistogramData[col];
channelHistogram.Release();
}
}
finally
{
image.Release();
channels?.ToList()
.ForEach(i => i.Release());
}
using (var stream = File.OpenWrite(cachePath))
MultiDimensionalArraySerializer.Serialize(stream, histogram);
return histogram;
}
private IEnumerable<Point[]> CalculateContours(IEnumerable<string> sampleImagePaths, IEnumerable<string> separatorImagePaths, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (sampleImagePaths.Count() == 0 ||
separatorImagePaths.Count() == 0)
return [];
var cachePath = Path.Join(_cachePath, "conts_" + GetShortCacheKey([.. sampleImagePaths, .. separatorImagePaths]) + ".mda");
if (File.Exists(cachePath))
{
Array array;
using (var stream = File.OpenRead(cachePath))
array = MultiDimensionalArraySerializer.Deserialize<int>(stream);
if (array is int[] typedArray)
{
var loadedContours = new List<Point[]>();
var i = 0;
var contoursCount = typedArray[i++];
for (var j = 0; j < contoursCount; j++)
{
var pointsCount = typedArray[i++];
var contour = new Point[pointsCount];
for (var k = 0; k < pointsCount; k++)
contour[k] = new Point(typedArray[i++], typedArray[i++]);
loadedContours.Add(contour);
}
loadedContours.Where(c =>
{
var bbox = Cv2.BoundingRect(c);
var area = Cv2.ContourArea(c);
return CONTOUR_MIN_SIZE < bbox.Width && bbox.Width < CONTOUR_MAX_SIZE &&
CONTOUR_MIN_SIZE < bbox.Height && bbox.Height < CONTOUR_MAX_SIZE &&
CONTOUR_MIN_AREA < area && area < CONTOUR_MAX_AREA;
});
}
}
var str = "20|CMY-2,HSV-0,HSV-1";
var threshStr = str.Split('|')
.First()
.Trim();
var thresh = int.Parse(threshStr);
var channelsStrs = str.Split('|')
.Last()
.Split(',')
.Select(s => s.Trim())
.ToList();
if (channelsStrs.Count() != 3)
throw new Exception();
var separatorMask = new Mat(new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2), MatType.CV_8UC1, new Scalar(0));
Cv2.Circle(separatorMask, HISTOGRAM_ROWS, HISTOGRAM_ROWS, HISTOGRAM_ROWS, new Scalar(255), -1);
cancellationToken.ThrowIfCancellationRequested();
var sampleRoi = CalculateSeparatorRoi(sampleImagePaths, 5, cancellationToken) ?? throw new Exception();
var sample = CalculateAverageImage(sampleImagePaths, cancellationToken);
sample = new Mat(sample, sampleRoi);
Cv2.Resize(sample, sample, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
var separatorRoi = CalculateSeparatorRoi(separatorImagePaths, 5, cancellationToken) ?? throw new Exception();
var separator = CalculateAverageImage(separatorImagePaths, cancellationToken);
separator = new Mat(separator, separatorRoi);
Cv2.Resize(separator, separator, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Mat[] sampleChannels = new Mat[3];
Mat[] separatorChannels = new Mat[3];
for (int ch = 0; ch < 3; ch++)
{
cancellationToken.ThrowIfCancellationRequested();
var colorSpace = channelsStrs[ch].Split("-").First();
var channelNumberStr = channelsStrs[ch].Split("-").Last();
var channelNumber = int.Parse(channelNumberStr);
var sampleTemp = new Mat();
sample.CopyTo(sampleTemp);
var separatorTemp = new Mat();
separator.CopyTo(separatorTemp);
switch (colorSpace)
{
case "RGB":
Cv2.CvtColor(sampleTemp, sampleTemp, ColorConversionCodes.BGR2RGB);
Cv2.CvtColor(separatorTemp, separatorTemp, ColorConversionCodes.BGR2RGB);
break;
case "HSV":
Cv2.CvtColor(sampleTemp, sampleTemp, ColorConversionCodes.BGR2HSV);
Cv2.CvtColor(separatorTemp, separatorTemp, ColorConversionCodes.BGR2HSV);
break;
case "LAB":
Cv2.CvtColor(sampleTemp, sampleTemp, ColorConversionCodes.BGR2Lab);
Cv2.CvtColor(separatorTemp, separatorTemp, ColorConversionCodes.BGR2Lab);
break;
case "CMY":
Cv2.Split(sampleTemp, out var sampleTempChannels);
Cv2.Merge([
(sampleTempChannels[0] / 2 + sampleTempChannels[1] / 2).ToMat(),
(sampleTempChannels[0] / 2 + sampleTempChannels[2] / 2).ToMat(),
(sampleTempChannels[1] / 2 + sampleTempChannels[2] / 2).ToMat(),
], sampleTemp);
sampleTempChannels.ToList().ForEach(c => c.Release());
Cv2.Split(separatorTemp, out var separatorTempChannels);
Cv2.Merge([
(separatorTempChannels[0] / 2 + separatorTempChannels[1] / 2).ToMat(),
(separatorTempChannels[0] / 2 + separatorTempChannels[2] / 2).ToMat(),
(separatorTempChannels[1] / 2 + separatorTempChannels[2] / 2).ToMat(),
], separatorTemp);
separatorTempChannels.ToList().ForEach(c => c.Release());
break;
}
{
Cv2.Split(sampleTemp, out var sampleTempChannels);
sampleChannels[ch] = sampleTempChannels[channelNumber];
sampleTempChannels.Where((_, i) => i != channelNumber).ToList().ForEach(c => c.Release());
Cv2.Split(separatorTemp, out var separatorTempChannels);
separatorChannels[ch] = separatorTempChannels[channelNumber];
separatorTempChannels.Where((_, i) => i != channelNumber).ToList().ForEach(c => c.Release());
}
sampleTemp.Release();
separatorTemp.Release();
}
Cv2.Merge(sampleChannels, sample);
Cv2.Merge(separatorChannels, separator);
sampleChannels.ToList().ForEach(c => c.Release());
separatorChannels.ToList().ForEach(c => c.Release());
var diff = new Mat();
Cv2.Absdiff(sample, separator, diff);
sample.Release();
sample = CalculateAverageImage(sampleImagePaths, cancellationToken);
sample = new Mat(sample, sampleRoi);
Cv2.Resize(sample, sample, new Size(HISTOGRAM_ROWS * 2, HISTOGRAM_ROWS * 2));
Cv2.CvtColor(diff, diff, ColorConversionCodes.BGR2GRAY);
Cv2.BitwiseAnd(diff, separatorMask, diff);
Cv2.GaussianBlur(diff, diff, new Size(5, 5), 1);
Cv2.Threshold(diff, diff, thresh, 255, ThresholdTypes.Binary);
Cv2.MorphologyEx(diff, diff, MorphTypes.Close, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)), iterations: 2);
Cv2.MorphologyEx(diff, diff, MorphTypes.Open, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)), iterations: 2);
Cv2.Dilate(diff, diff, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)));
Cv2.GaussianBlur(diff, diff, new Size(5, 5), 1);
Cv2.Threshold(diff, diff, 127, 255, ThresholdTypes.Binary);
Cv2.FindContours(diff, out var contours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
// int y = 0;
// foreach (var contour in contours)
// {
// var bbox = Cv2.BoundingRect(contour);
// var area = Cv2.ContourArea(contour);
// if (bbox.Width > AmineContentAnalyzerService.CONTOUR_MAX_SIZE ||
// bbox.Height > AmineContentAnalyzerService.CONTOUR_MAX_SIZE ||
// area > AmineContentAnalyzerService.CONTOUR_MAX_AREA)
// {
// Cv2.DrawContours(sample, [contour], 0, new Scalar(0, 255, 0), 1, LineTypes.AntiAlias);
// _logger.LogInformation($"Contour is big {y}, {bbox.Width}x{bbox.Height} ({area})");
// Cv2.PutText(sample, $"{y++}", new Point((bbox.Left + bbox.Right) / 2, (bbox.Top + bbox.Bottom) / 2), HersheyFonts.HersheySimplex, 2, Scalar.White, 2);
// continue;
// }
// if (bbox.Width < AmineContentAnalyzerService.CONTOUR_MIN_SIZE ||
// bbox.Height < AmineContentAnalyzerService.CONTOUR_MIN_SIZE ||
// area < AmineContentAnalyzerService.CONTOUR_MIN_AREA)
// {
// Cv2.DrawContours(sample, [contour], 0, new Scalar(255, 0, 0), 1, LineTypes.AntiAlias);
// _logger.LogInformation($"Contour is small {y}, {bbox.Width}x{bbox.Height} ({area})");
// Cv2.PutText(sample, $"{y++}", new Point((bbox.Left + bbox.Right) / 2, (bbox.Top + bbox.Bottom) / 2), HersheyFonts.HersheySimplex, 0.5, Scalar.White, 1);
// continue;
// }
// Cv2.DrawContours(sample, [contour], 0, new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);
// }
// Cv2.ImWrite($"_{i}_0_sam.jpg", sample);
sample.Release();
separator.Release();
{
var array = new int[contours.Sum(c => c.Length * 2 + 1) + 1];
var i = 0;
array[i++] = contours.Count();
foreach (var contour in contours)
{
array[i++] = contour.Count();
foreach (var point in contour)
{
array[i++] = point.X;
array[i++] = point.Y;
}
}
using (var stream = File.OpenWrite(cachePath))
MultiDimensionalArraySerializer.Serialize(stream, array);
}
return contours.Where(c =>
{
var bbox = Cv2.BoundingRect(c);
var area = Cv2.ContourArea(c);
return CONTOUR_MIN_SIZE < bbox.Width && bbox.Width < CONTOUR_MAX_SIZE &&
CONTOUR_MIN_SIZE < bbox.Height && bbox.Height < CONTOUR_MAX_SIZE &&
CONTOUR_MIN_AREA < area && area < CONTOUR_MAX_AREA;
});
}
private static string GetShortCacheKey(IEnumerable<string> imagePaths, int length = 16)
{
if (imagePaths.Count() == 0)
throw new Exception($"Provided {nameof(imagePaths)} does not contain elements");
var paths = imagePaths.OrderBy(p => p);
var normalized = string.Join("|", paths.Select(p => (p ?? string.Empty).Trim()));
using var sha = System.Security.Cryptography.SHA256.Create();
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(normalized));
string base64 = Convert.ToBase64String(hash)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
return base64.Substring(0, Math.Min(length, base64.Length));
}
}