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 _logger; private readonly AmineContentCalibrationContext _calibrationContext; private readonly ImageStorageService _imageStorage; private MLContext? _ml = null; private PredictionEngine? _separatorTypePredictionEngine = null; private PredictionEngine? _sampleBrandPredictionEngine = null; private PredictionEngine? _sampleAmineContentRegressionPredictionEngine = null; private PredictionEngine? _sampleAmineContentClusterizationPredictionEngine = null; public bool Initialized { get; private set; } = false; public AmineContentAnalyzerService(ILogger 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 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(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(model); model.Dispose(); } } private float[] CreateSeparatorTypeFeatureVector(IEnumerable 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 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(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(model); model.Dispose(); } } private float[] CreateSampleBrandFeatureVector(IEnumerable sampleImageRecords, IEnumerable 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 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( 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(); 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(); 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(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(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 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(); 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(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(model); model.Dispose(); } } public async Task PredictSeparatorType(IEnumerable 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 CalculatePollutionRate(IEnumerable 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 PredictSampleBrand(IEnumerable sampleImageRecords, IEnumerable 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 sampleImageRecords, IEnumerable 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(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([row, column, image * 3 + 0]) / 255; features[i++] = (float)uvSampleImage.Get([row, column, image * 3 + 1]) / 255; features[i++] = (float)uvSampleImage.Get([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(row, column, 0); // values[row * HISTOGRAM_ROWS * 2 + column] = 0; // continue; // } // else // { // result.Set(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(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 GetImageRecordsFilePaths(IEnumerable 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 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 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(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(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 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(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 CalculateContours(IEnumerable sampleImagePaths, IEnumerable 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(stream); if (array is int[] typedArray) { var loadedContours = new List(); 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 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)); } }