forked from amkovkov/GranuSightSoftware2
1540 lines
66 KiB
C#
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 indencities to correlate with separator images intencities");
|
|
|
|
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)> CalcaulateAmineContent(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 проценталь (1% самых горячих пикселей), которые скорее всего являются шумом или загрязенением
|
|
var oldMean = Cv2.Mean(result, sampleMask).Val0;
|
|
values.Sort();
|
|
float p99 = values[(int)(values.Length * 0.99)];
|
|
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));
|
|
}
|
|
}
|