From 5b6abc045bcd61104199ab4ced1edd003007c7bf Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Wed, 8 Jul 2026 08:25:09 +0300 Subject: [PATCH] feat: testing and fixes --- .../Analysis/AmineContent/AnalyzerService.cs | 28 +- GSS2.Core/Hardware/CameraService.cs | 3 +- GSS2.UI.Core/ZoomPanImage.cs | 18 + .../AmineContent/AnalysisViewModel.cs | 507 ++++++++---- GSS2/Views/AmineContent/AnalysisView.axaml | 778 +++++++++--------- GSS2/Views/AmineContent/AnalysisView.axaml.cs | 2 - 6 files changed, 730 insertions(+), 606 deletions(-) diff --git a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs index 8b1bf25..f4dc459 100644 --- a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs +++ b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs @@ -172,6 +172,7 @@ public partial class AnalyzerService } var sampleDataSet = sampleRecords + .AsEnumerable() .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) .SelectMany(r => r.Features.Chunk(FEATURES_LENGTH)) .Select(f => new ClusterizationData @@ -231,6 +232,7 @@ public partial class AnalyzerService _logger.LogInformation("Подготовка данных"); var trainDataSet = sampleRecords + .AsEnumerable() .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0) .SelectMany(r => @@ -290,7 +292,7 @@ public partial class AnalyzerService model.Dispose(); } - public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable contours, Mat result, string separatorType, string sampleBrand)?> Analyze(IEnumerable imagesData) + public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable contours, Mat result)?> Analyze(IEnumerable imagesData) { if (!Initialized) return null; @@ -304,7 +306,7 @@ public partial class AnalyzerService if (segmentationResult is null) return null; - var (avgImage, roi, mask, contours, separatorType, sampleBrand) = segmentationResult.Value; + var (avgImage, roi, mask, contours) = segmentationResult.Value; var images = new List<(Mat mat, double visible, double uv365, double uv254)>(); foreach (var imageData in imagesData @@ -410,7 +412,7 @@ public partial class AnalyzerService Cv2.Erode(mask, mask, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3))); Cv2.FindContours(mask, out var newContours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple); - return (avgImage, roi, mask, contours, result, separatorType, sampleBrand); + return (avgImage, roi, mask, contours, result); } catch (Exception ex) { @@ -418,10 +420,10 @@ public partial class AnalyzerService return null; } } - private async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable contours, string separatorType, string sampleBrand)?> Segment(IEnumerable imagesData) + private async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable contours)?> Segment(IEnumerable imagesData) { - var separatorPrefix = "SEPARATOR"; - var samplePrefix = "SAMPLE"; + var separatorPrefix = "separator"; + var samplePrefix = "sample"; if (imagesData.Count() != IMAGES_COUNT) return null; @@ -522,24 +524,14 @@ public partial class AnalyzerService result = new Mat(new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE), MatType.CV_8UC1, new Scalar(0)); Cv2.DrawContours(result, contours.Where(ContourIsValid), -1, new Scalar(255), -1); - _logger.LogInformation("Найдено гранул: {}", contours.Where(ContourIsValid).Count()); + _logger.LogInformation("Найдено гранул: {}", contours.Count(ContourIsValid)); _logger.LogInformation("Детекторованные обекты:\n\t{}", string.Join("\n\t", frequency.Select(f => $"{f.Key}: {f.Value}"))); return ( avgImage, roi.Value, result, - contours.Where(ContourIsValid), - frequency - .Where(kv => kv.Key.StartsWith(separatorPrefix)) - .MaxBy(kv => kv.Value) - .Key - .Substring(separatorPrefix.Count() + 1), - frequency - .Where(kv => kv.Key.StartsWith(samplePrefix)) - .MaxBy(kv => kv.Value) - .Key - .Substring(samplePrefix.Count() + 1) + contours.Where(ContourIsValid) ); } diff --git a/GSS2.Core/Hardware/CameraService.cs b/GSS2.Core/Hardware/CameraService.cs index edcd31f..63c1612 100644 --- a/GSS2.Core/Hardware/CameraService.cs +++ b/GSS2.Core/Hardware/CameraService.cs @@ -82,7 +82,6 @@ public class CameraService : IDisposable private FrameBufferAllocator? _imageCaptureFrameBufferAllocator; private Stream? _imageCaptureStream; private List? _imageCaptureMappedBuffers; - private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null; private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); private readonly Dictionary _cameraHardSettings = new Dictionary(); @@ -238,7 +237,7 @@ public class CameraService : IDisposable _cameraConfiguration = _camera.GenerateConfiguration(StreamRoleEnum.Raw); - _imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First(); + _imageCaptureStreamConfiguration = _cameraConfiguration.First(); _imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount); _imageCaptureStreamConfiguration.Size = new Size( checked((uint)_configuration.ImageCaptureWidth), diff --git a/GSS2.UI.Core/ZoomPanImage.cs b/GSS2.UI.Core/ZoomPanImage.cs index 1c17558..b18eafe 100644 --- a/GSS2.UI.Core/ZoomPanImage.cs +++ b/GSS2.UI.Core/ZoomPanImage.cs @@ -54,6 +54,12 @@ public class ZoomPanImage : Control get => GetValue(PanYProperty); set => SetValue(PanYProperty, value); } + public static readonly StyledProperty CanUserZoomPanProperty = AvaloniaProperty.Register(nameof(CanUserZoomPan), defaultValue: true); + public bool CanUserZoomPan + { + get => GetValue(CanUserZoomPanProperty); + set => SetValue(CanUserZoomPanProperty, value); + } public ZoomPanImage() : base() @@ -64,6 +70,10 @@ public class ZoomPanImage : Control PanX = 0f; PanY = 0f; }; + + AffectsRender(ZoomProperty); + AffectsRender(PanXProperty); + AffectsRender(PanYProperty); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -75,6 +85,8 @@ public class ZoomPanImage : Control protected override void OnPointerWheelChanged(PointerWheelEventArgs ea) { + if (!CanUserZoomPan) + return; if (ea.Pointer.Type == PointerType.Mouse) { var position = ea.GetPosition(this); @@ -107,6 +119,8 @@ public class ZoomPanImage : Control } protected override void OnPointerPressed(PointerPressedEventArgs ea) { + if (!CanUserZoomPan) + return; if (ea.Pointer.Type == PointerType.Mouse) { if (ea.Properties.IsLeftButtonPressed) @@ -136,6 +150,8 @@ public class ZoomPanImage : Control } protected override void OnPointerReleased(PointerReleasedEventArgs ea) { + if (!CanUserZoomPan) + return; if (ea.Pointer.Type == PointerType.Mouse) { if (!ea.Properties.IsLeftButtonPressed) @@ -151,6 +167,8 @@ public class ZoomPanImage : Control } protected override void OnPointerMoved(PointerEventArgs ea) { + if (!CanUserZoomPan) + return; if (ea.Pointer.Type == PointerType.Mouse) { if (_lastMousePosition is null || Zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем diff --git a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs index a6f0f61..372a76b 100644 --- a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs +++ b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Avalonia; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -16,6 +17,10 @@ using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.Painting.Effects; +using UnitsNet; + +using MathNet.Numerics.Statistics; + using GSS2.Core.Hardware; using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent.Database; @@ -24,10 +29,6 @@ using GSS2.Core.Extensions; using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.Services; -using UnitsNet; - -using MathNet.Numerics.Statistics; - namespace GSS2.ViewModels.AmineContent; public partial class AnalysisViewModel : ViewModelBase @@ -85,8 +86,8 @@ public partial class AnalysisViewModel : ViewModelBase /// public partial class RecordViewModel : ViewModelBase { - public const double UNPROCESSED_THRESHOLD = 0.5; - public const double HOTSPOT_THRESHOLD = 1.2; + public const double UNPROCESSED_THRESHOLD = 0.6; + public const double HOTSPOT_THRESHOLD = 1.1; public const double EPSILON = 1e-10; public const double UNPROCESSED_FRACTION_THRESHOLD = 10; @@ -415,19 +416,6 @@ public partial class AnalysisViewModel : ViewModelBase /// [ObservableProperty] public partial int ParticleIndex { get; set; } = 0; - /// - /// Представление выбранной гранулы - /// - [ObservableProperty] public partial AsyncImageRecordViewModel ParticleImage { get; set; } = new(); - /// - /// Представление выбранной гранулы - /// - [ObservableProperty] public partial AsyncImageRecordViewModel ParticleMaskImage { get; set; } = new(); - /// - /// Представление выбранной гранулы - /// - [ObservableProperty] public partial AsyncImageRecordViewModel ParticleResultImage { get; set; } = new(); - /// /// Выбрано ли изображение /// @@ -452,7 +440,12 @@ public partial class AnalysisViewModel : ViewModelBase /// /// Смещение по Y изображений , и /// - [ObservableProperty] public partial float ImagesPanY { get; set; } = 0; + [ObservableProperty] public partial float ParticleImagesPanY { get; set; } = 0; + /// + /// Ширина изображений , и + /// + [ObservableProperty] public partial Rect ParticleImagesBounds { get; set; } = new Rect(); + /// /// /// Нормированная обработанная площадь выбранной гранулы @@ -501,6 +494,33 @@ public partial class AnalysisViewModel : ViewModelBase InnerStdDevs.Clear(); InnerHeterogeneities.Clear(); + if (Values.Count() == 0) + { + MeanProcessedArea = -1; + ProcessedAreaP10 = -1; + ProcessedAreaP50 = -1; + ProcessedAreaP90 = -1; + ProcessedAreaStdDev = -1; + InterHeterogeneity = -1; + ProcessedAreaDeltaP90P10 = -1; + UnprocessedFraction = -1; + MeanInnerHeterogeneity = -1; + MeanInnerStdDev = -1; + MeanHotspotArea = -1; + MeanOpticalIntegral = -1; + + ProcessedAreasHistogram.Clear(); + UnprocessedAreasHistogram.Clear(); + HotspotAreasHistogram.Clear(); + OpticalIntegralsHistogram.Clear(); + InnerStdDevsHistogram.Clear(); + InnerHeterogeneitiesHistogram.Clear(); + + ParticleIndex = 0; + + return; + } + // Для каждой гранулы var max = Values.Max(vs => vs.Max()); foreach (var value in Values) @@ -517,10 +537,10 @@ public partial class AnalysisViewModel : ViewModelBase // Вычисляем значения var area = normalized.Count(); - var processedArea = normalized.Count(v => v > 1) / area * 100; - var unprocessedArea = normalized.Count(v => v <= 1) / area * 100; - var hotspotArea = normalized.Count(v => v > 2) / area * 100; - var opticalIntegral = normalized.Sum() / area * 100; + var processedArea = normalized.Count(v => v > 1) * 100.0 / area; + var unprocessedArea = normalized.Count(v => v <= 1) * 100.0 / area; + var hotspotArea = normalized.Count(v => v > 2) * 100.0 / area; + var opticalIntegral = normalized.Sum() * 100.0 / area; var innerStdDev = normalized.StdDev(); var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON); @@ -558,6 +578,7 @@ public partial class AnalysisViewModel : ViewModelBase // Сбрасываем индекс выбранной гранулы ParticleIndex = 0; + ZoomPanOnParticle(); } /// @@ -578,7 +599,7 @@ public partial class AnalysisViewModel : ViewModelBase { if (ParticlesCount == 0) return; - ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount; + // ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount; } /// /// Переключить на изображение @@ -638,8 +659,35 @@ public partial class AnalysisViewModel : ViewModelBase ParticleInnerHeterogeneity = InnerHeterogeneities[value]; // Обновляем гистограмму ParticleValuesHistogram.Set(NormalizedValues[value], 0, 3, "Количество, шт", "Значение"); + // Обновляем изображение + + ZoomPanOnParticle(); } + partial void OnParticleImagesBoundsChanged(Rect value) + { + ZoomPanOnParticle(); + } + + private void ZoomPanOnParticle() + { + if (ParticleIndex < 0 || + ParticleIndex >= Xs.Count() || + ParticleIndex >= Ys.Count()) + return; + if (ParticleImagesBounds.Width == 0 || + ParticleImagesBounds.Height == 0) + return; + + var minX = Xs[ParticleIndex].Min(); + var maxX = Xs[ParticleIndex].Max(); + var minY = Ys[ParticleIndex].Min(); + var maxY = Ys[ParticleIndex].Max(); + + ParticleImagesZoom = MathF.Max((float)ParticleImagesBounds.Width, (float)ParticleImagesBounds.Height) * MathF.Max(maxX - minX, maxY - minY) / AnalyzerService.ANALYSIS_IMAGE_SIZE; + ParticleImagesPanX = -((maxX + minX) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Width * ParticleImagesZoom; + ParticleImagesPanY = -((maxY + minY) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Height * ParticleImagesZoom; + } } public event EventHandler? UpdateCharts; @@ -868,6 +916,11 @@ public partial class AnalysisViewModel : ViewModelBase _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); return; } + // Берём индекс изменённого элемента в выпадающем списке марок + var editedComboBoxIndex = SeparatorComboBoxItems.IndexOf(editedComboBoxItem); + // Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи + if (SeparatorComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord) + Record.SeparatorType = separatorRecord.Type; // Обновляем элемент из выпадающего списка марок editedComboBoxItem.Type = separatorRecord.Type; } @@ -911,6 +964,15 @@ public partial class AnalysisViewModel : ViewModelBase _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); return; } + // Берём индекс изменённого элемента в выпадающем списке марок + var editedComboBoxIndex = BrandComboBoxItems.IndexOf(editedComboBoxItem); + // Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи + if (BrandComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord) + { + Record.BrandName = brandRecord.BrandName; + Record.MixtureName = brandRecord.MixtureName; + Record.MixtureNormalRate = brandRecord.MixtureNormalRate; + } // Обновляем элемент из выпадающего списка марок editedComboBoxItem.BrandName = brandRecord.BrandName; editedComboBoxItem.MixtureName = brandRecord.MixtureName; @@ -926,13 +988,16 @@ public partial class AnalysisViewModel : ViewModelBase else // Если записей нет, MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана - // Подписываемся на изменения свойств для обновления CanScroll + // Подписываемся на изменения свойств для обновления CanInteract и обновления графиков PropertyChanged += (_, ea) => { if (ea.PropertyName == nameof(MenuOpened) || ea.PropertyName == nameof(MainSectionIsScrolling) || ea.PropertyName == nameof(InfoSectionIsScrolling)) UpdateCanInteract(); + if (ea.PropertyName == nameof(MenuSelectedIndex) || + ea.PropertyName == nameof(InfoSectionCurrentIndex) ) + UpdateCharts?.Invoke(this, EventArgs.Empty); }; _logger.LogInformation("Инициализировано"); @@ -946,81 +1011,79 @@ public partial class AnalysisViewModel : ViewModelBase // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку if (value >= MenuItems.Count()) _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count()); - // Если значение больше или равно 0, то значит выбран один из элементов меню - if (value >= 0 && value < MenuItems.Count()) + // Если значение меньше 0, то значит элемент меню не выбран + if (value < 0 || value > MenuItems.Count()) { - _logger.LogTrace("Выбран элемент из списка: {}", value); - // Находим запись в базе данных - var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id); - // Если запись найдена, то заполняем поля из базы данных - if (selectedRecord is not null) - { - // Заполняем простые поля - Record.Id = selectedRecord.Id; - Record.Uuid = selectedRecord.Uuid; - Record.CreateDateTime = selectedRecord.CreateDateTime; - Record.EditDateTime = selectedRecord.EditDateTime; - Record.SampleName = selectedRecord.SampleName; - Record.SampleComment = selectedRecord.SampleComment; - Record.SamplingDateTime = selectedRecord.SamplingDateTime; - Record.SamplingPlace = selectedRecord.SamplingPlace; - Record.SeparatorType = selectedRecord.SeparatorType; - Record.BrandName = selectedRecord.BrandName; - Record.MixtureName = selectedRecord.MixtureName; - Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; - Record.ParticlesCount = selectedRecord.ParticlesCount; - Record.Values = selectedRecord.ValueData.Chunk(selectedRecord.ValueCount).Select(v => v.ToArray()).ToList(); - Record.Xs = selectedRecord.XData.Chunk(selectedRecord.XCount).Select(v => v.ToArray()).ToList(); - Record.Ys = selectedRecord.YData.Chunk(selectedRecord.YCount).Select(v => v.ToArray()).ToList(); + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords)); + // Сохраняем несуществующий индекс + var nonExistingIndex = MenuSelectedIndex; + // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями + MenuSelectedIndex = -1; + // Удаляем несуществующий элемент из списка + MenuItems.RemoveAt(nonExistingIndex); + return; + } - // Ищем изображение в базе данных - var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); - // Если изображение не найдено в базе данных, то логгируем как ошибку - if (imageRecord is null) - _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); - // В любом случае заполняем изображение - Record.Image.Data = imageRecord?.Base64 ?? ""; + _logger.LogTrace("Выбран элемент из списка: {}", value); + // Находим запись в базе данных + var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id); + // Если запись найдена, то заполняем поля из базы данных + if (selectedRecord is not null) + { + // Заполняем простые поля + Record.Id = selectedRecord.Id; + Record.Uuid = selectedRecord.Uuid; + Record.CreateDateTime = selectedRecord.CreateDateTime; + Record.EditDateTime = selectedRecord.EditDateTime; + Record.SampleName = selectedRecord.SampleName; + Record.SampleComment = selectedRecord.SampleComment; + Record.SamplingDateTime = selectedRecord.SamplingDateTime; + Record.SamplingPlace = selectedRecord.SamplingPlace; + Record.SeparatorType = selectedRecord.SeparatorType; + Record.BrandName = selectedRecord.BrandName; + Record.MixtureName = selectedRecord.MixtureName; + Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; + Record.ParticlesCount = selectedRecord.ParticlesCount; + Record.Values = selectedRecord.ValueData.Chunk(selectedRecord.ValueCount).Select(v => v.ToArray()).ToList(); + Record.Xs = selectedRecord.XData.Chunk(selectedRecord.XCount).Select(v => v.ToArray()).ToList(); + Record.Ys = selectedRecord.YData.Chunk(selectedRecord.YCount).Select(v => v.ToArray()).ToList(); - // Ищем изображение маски в базе данных - var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId); - // Если изображение не найдено в базе данных, то логгируем как ошибку - if (maskImageRecord is null) - _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords)); - // В любом случае заполняем изображение - Record.MaskImage.Data = maskImageRecord?.Base64 ?? ""; + // Ищем изображение в базе данных + var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (imageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.Image.Data = imageRecord?.Base64 ?? ""; - // Ищем изображение результата в базе данных - var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId); - // Если изображение не найдено в базе данных, то логгируем как ошибку - if (resultImageRecord is null) - _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords)); - // В любом случае заполняем изображение - Record.MaskImage.Data = resultImageRecord?.Base64 ?? ""; + // Ищем изображение маски в базе данных + var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (maskImageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.MaskImage.Data = maskImageRecord?.Base64 ?? ""; - // Сбрасываем приближение изображений - ImagesPanX = 0; - ImagesPanY = 0; - ImagesZoom = 1; + // Ищем изображение результата в базе данных + var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (resultImageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.ResultImage.Data = resultImageRecord?.Base64 ?? ""; - // Устанавливаем, что запись выбрана из базы данных - IsNewRecord = false; - // Вычисляем дополнительные свойства - Record.CalculateProperties(); + // Сбрасываем приближение изображений + ImagesPanX = 0; + ImagesPanY = 0; + ImagesZoom = 1; - return; - } - // Если значение не найдено в базе данных - else - { - // Логгируем как ошибку - _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords)); - var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс - // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями - MenuSelectedIndex = -1; - // Удаляем несуществующий элемент из списка - MenuItems.RemoveAt(nonExistingIndex); - return; - } + // Устанавливаем, что запись выбрана из базы данных + IsNewRecord = false; + // Вычисляем дополнительные свойства + Record.CalculateProperties(); + + return; } // Если элемент не выбран, заполняем поля пустыми значениями @@ -1056,6 +1119,84 @@ public partial class AnalysisViewModel : ViewModelBase Record.CalculateProperties(); } + /// + /// Вызывается при изменении + /// + partial void OnBrandComboBoxSelectedIndexChanged(int value) + { + if (!IsNewRecord) + return; + // Если значение выходит за верхнюю границу списка элементов выпадающего списка марок, то логгируем как ошибку + if (value >= BrandComboBoxItems.Count()) + _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count()); + // Если значение меньше 0, то значит элемент выпадающего списка марок не выбран + if (value < 0 || value >= BrandComboBoxItems.Count()) + { + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[value].Id, nameof(_context.BrandRecords)); + // Сохраняем несуществующий индекс + var nonExistingIndex = BrandComboBoxSelectedIndex; + // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями + BrandComboBoxSelectedIndex = -1; + // Удаляем несуществующий элемент из списка + BrandComboBoxItems.RemoveAt(nonExistingIndex); + return; + } + + // Находим запись в базе данных + var selectedRecord = _context.BrandRecords.Find(BrandComboBoxItems[value].Id); + // Если запись найдена, то заполняем поля из базы данных + if (selectedRecord is not null) + { + Record.BrandName = selectedRecord.BrandName; + Record.MixtureName = selectedRecord.MixtureName; + Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; + return; + } + + // Если элемент не выбран, заполняем поля пустыми значениями + Record.BrandName = ""; + Record.MixtureName = ""; + Record.MixtureNormalRate = -1; + } + + /// + /// Вызывается при изменении + /// + partial void OnSeparatorComboBoxSelectedIndexChanged(int value) + { + if (!IsNewRecord) + return; + // Если значение выходит за верхнюю границу списка элементов выпадающего списка сепараторов, то логгируем как ошибку + if (value >= SeparatorComboBoxItems.Count()) + _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(SeparatorComboBoxSelectedIndex), SeparatorComboBoxSelectedIndex, nameof(SeparatorComboBoxItems), SeparatorComboBoxItems.Count()); + // Если значение меньше 0, то значит элемент выпадающего списка сепараторов не выбран + if (value < 0 || value >= SeparatorComboBoxItems.Count()) + { + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[value].Id, nameof(_context.SeparatorRecords)); + // Сохраняем несуществующий индекс + var nonExistingIndex = SeparatorComboBoxSelectedIndex; + // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями + SeparatorComboBoxSelectedIndex = -1; + // Удаляем несуществующий элемент из списка + SeparatorComboBoxItems.RemoveAt(nonExistingIndex); + return; + } + + // Находим запись в базе данных + var selectedRecord = _context.SeparatorRecords.Find(SeparatorComboBoxItems[value].Id); + // Если запись найдена, то заполняем поля из базы данных + if (selectedRecord is not null) + { + Record.SeparatorType = selectedRecord.Type; + return; + } + + // Если элемент не выбран, заполняем поля пустыми значениями + Record.SeparatorType = ""; + } + /// /// Обновить значение /// @@ -1386,11 +1527,11 @@ public partial class AnalysisViewModel : ViewModelBase if (attachedImageRecord is null) { // Если изображение есть - if (!string.IsNullOrEmpty(Record.Image.Data)) + if (!string.IsNullOrEmpty(Record.MaskImage.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения - var imageRecord = new ImageRecord(base64: Record.Image.Data); + var imageRecord = new ImageRecord(base64: Record.MaskImage.Data); // Добавляем запись изображения в базу данных attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; // Сохраняем изменения в базе данных @@ -1403,11 +1544,11 @@ public partial class AnalysisViewModel : ViewModelBase else { // Если изображение есть - if (!string.IsNullOrEmpty(Record.Image.Data)) + if (!string.IsNullOrEmpty(Record.MaskImage.Data)) { _logger.LogTrace("Записываем изменения в запись изображения"); // Вносим изменения - attachedImageRecord.Base64 = Record.Image.Data; + attachedImageRecord.Base64 = Record.MaskImage.Data; // Сохраняем изменения в базе данных _context.SaveChanges(); } @@ -1430,11 +1571,11 @@ public partial class AnalysisViewModel : ViewModelBase if (attachedImageRecord is null) { // Если изображение есть - if (!string.IsNullOrEmpty(Record.Image.Data)) + if (!string.IsNullOrEmpty(Record.ResultImage.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения - var imageRecord = new ImageRecord(base64: Record.Image.Data); + var imageRecord = new ImageRecord(base64: Record.ResultImage.Data); // Добавляем запись изображения в базу данных attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; // Сохраняем изменения в базе данных @@ -1447,11 +1588,11 @@ public partial class AnalysisViewModel : ViewModelBase else { // Если изображение есть - if (!string.IsNullOrEmpty(Record.Image.Data)) + if (!string.IsNullOrEmpty(Record.ResultImage.Data)) { _logger.LogTrace("Записываем изменения в запись изображения"); // Вносим изменения - attachedImageRecord.Base64 = Record.Image.Data; + attachedImageRecord.Base64 = Record.ResultImage.Data; // Сохраняем изменения в базе данных _context.SaveChanges(); } @@ -1561,7 +1702,7 @@ public partial class AnalysisViewModel : ViewModelBase // Выбрасываем исключение, если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Сохраняем PNG изображение во временную папку - var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png"); + var path = Path.Join(Path.GetTempPath(), $"gss2-analysis-{visible}-{uv365}-{uv254}.png"); image.ImWrite(path); // Освобождаем память изображения @@ -1574,6 +1715,18 @@ public partial class AnalysisViewModel : ViewModelBase imagesData.Add(imagesDataTask); }, cancellationToken); + // var captureTask = Task.Run(() => { + // foreach (var (visible, uv365, uv254) in ImageCapturerService.IlluminatorIntensities) + // { + // var imagesDataTask = Task.Run(() => + // { + // var path = Path.Join(Path.GetTempPath(), $"gss2-analysis-{visible}-{uv365}-{uv254}.png"); + // return new ImageData(path, visible, uv365, uv254); + // }, cancellationToken); + // imagesData.Add(imagesDataTask); + // } + // }); + // Создаём задачу обработки изображений, // эта задача начнётся только после того как завершится // задача съёмки изображений и все задачи сохранения изображений @@ -1628,6 +1781,11 @@ public partial class AnalysisViewModel : ViewModelBase _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[BrandComboBoxSelectedIndex].Id, nameof(_context.BrandRecords)); return; } + if (brand.MixtureNormalRate <= 0) + { + _logger.LogError("Норма расхода выбранной марки меньше или равена 0"); + return; + } if (separator is null) { _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords)); @@ -1662,13 +1820,16 @@ public partial class AnalysisViewModel : ViewModelBase await Task.Delay(3000); _lights.Disconnect(); }); + return; } // Распаковываем результат - var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value; + var (avgImage, _, mask, contours, resultImage) = result.Value; try { + Record.ParticlesCount = contours.Count(); + // Кодируем среднее изображение OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); Record.Image.Data = Convert.ToBase64String(avgData); @@ -1685,7 +1846,7 @@ public partial class AnalysisViewModel : ViewModelBase new OpenCvSharp.Scalar(0) ); - Record.Values = new List(); + var values = new List(); // Проходим по каждому контуру foreach (var contour in contours) { @@ -1704,8 +1865,9 @@ public partial class AnalysisViewModel : ViewModelBase if (contourMask.Get(i, j) > 127) contourValues.Add(resultImage.Get(i, j)); // Сохраняем полученные значения - Record.Values.Add(contourValues.ToArray()); + values.Add(contourValues.ToArray()); } + Record.Values = values; // Освобождаем память маски контура contourMask.Dispose(); @@ -1714,65 +1876,30 @@ public partial class AnalysisViewModel : ViewModelBase Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList(); // Вычисляем пороговые значения - var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * Record.MixtureNormalRate; - var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * Record.MixtureNormalRate; - + var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * brand.MixtureNormalRate; + var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * brand.MixtureNormalRate; + + // Выичсляем максимальное значение OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max); + // Если иаксмиальное значниеz слишком мало, то устанавливаем его чуть больше hotspotsThreshold, чтобы не возникло деления на ноль и отридцательных значений if (max <= hotspotsThreshold) max = hotspotsThreshold + 0.001; - var lutIndices = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC1, OpenCvSharp.Scalar.All(0)); + + // Создаём цветное изображение результата + var coloredResultImage = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC3, OpenCvSharp.Scalar.All(0)); + // Расшрашиваем диапазон 0...unprocessedThreshold -> чёрный...белый + ApplyGradientToRange(resultImage, 0, unprocessedThreshold, OpenCvSharp.Scalar.Black, OpenCvSharp.Scalar.White, coloredResultImage); + // Расшрашиваем диапазон unprocessedThreshold...hotspotsThreshold -> белый...зелёный + ApplyGradientToRange(resultImage, unprocessedThreshold, hotspotsThreshold, OpenCvSharp.Scalar.White, OpenCvSharp.Scalar.Green, coloredResultImage); + // Расшрашиваем диапазон hotspotsThreshold...max -> зелёный...крастный + ApplyGradientToRange(resultImage, hotspotsThreshold, max, OpenCvSharp.Scalar.Green, OpenCvSharp.Scalar.Red, coloredResultImage); - using (var mask1 = new OpenCvSharp.Mat()) - { - OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(0), OpenCvSharp.Scalar.All(unprocessedThreshold), mask1); - - var scale1 = 85.0 / unprocessedThreshold; - - using (var res1 = new OpenCvSharp.Mat()) - { - resultImage.ConvertTo(res1, OpenCvSharp.MatType.CV_8UC1, scale1); - res1.CopyTo(lutIndices, mask1); - } - } - using (var mask2 = new OpenCvSharp.Mat()) - { - OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(unprocessedThreshold), OpenCvSharp.Scalar.All(hotspotsThreshold), mask2); - - double scale2 = 85.0 / (hotspotsThreshold - unprocessedThreshold); - double shift2 = -unprocessedThreshold * scale2 + 85.0; - - using (var res2 = new OpenCvSharp.Mat()) - { - resultImage.ConvertTo(res2, OpenCvSharp.MatType.CV_8UC1, scale2, shift2); - res2.CopyTo(lutIndices, mask2); - } - } - using (var mask3 = new OpenCvSharp.Mat()) - { - OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(hotspotsThreshold), OpenCvSharp.Scalar.All(max), mask3); - - double scale3 = 85.0 / (max - hotspotsThreshold); - double shift3 = -hotspotsThreshold * scale3 + 170.0; - - using (var res3 = new OpenCvSharp.Mat()) - { - resultImage.ConvertTo(res3, OpenCvSharp.MatType.CV_8UC1, scale3, shift3); - res3.CopyTo(lutIndices, mask3); - } - } - - // Создаём и применяем LUT - var lut = CreateLut(); - var coloredResult = new OpenCvSharp.Mat(); - OpenCvSharp.Cv2.LUT(lutIndices, lut, coloredResult); // Устанавливаем в 0 всё что не попадает в маску var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0)); OpenCvSharp.Cv2.BitwiseNot(mask, invMask); - coloredResult.SetTo(OpenCvSharp.Scalar.All(0), invMask); + coloredResultImage.SetTo(OpenCvSharp.Scalar.All(0), invMask); // Освобождаем память, оригинальное изображение результата можно удалить - lutIndices.Dispose(); - lut.Dispose(); invMask.Dispose(); resultImage.Dispose(); @@ -1783,10 +1910,10 @@ public partial class AnalysisViewModel : ViewModelBase mask.Release(); // Кодируем изображение результата - OpenCvSharp.Cv2.ImEncode(".png", coloredResult, out var resultData); + OpenCvSharp.Cv2.ImEncode(".png", coloredResultImage, out var resultData); Record.ResultImage.Data = Convert.ToBase64String(resultData); // Освобождаем память изображения результата - coloredResult.Release(); + coloredResultImage.Release(); // Пересчитываем значения свойств Record.CalculateProperties(); @@ -1883,32 +2010,52 @@ public partial class AnalysisViewModel : ViewModelBase /// [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); - private static OpenCvSharp.Mat CreateLut() + /// + /// Раскрасить диапазон в градиент + /// + public static void ApplyGradientToRange(OpenCvSharp.Mat source, double minVal, double maxVal, OpenCvSharp.Scalar minColor, OpenCvSharp.Scalar maxColor, OpenCvSharp.Mat destination) { - var lut = new OpenCvSharp.Mat(256, 1, OpenCvSharp.MatType.CV_8UC3); - var indexer = lut.GetGenericIndexer(); + // Проверяем что все типы данных + if (source.Type() != OpenCvSharp.MatType.CV_32FC1) + throw new ArgumentException("Исходное изображение должно быть типа CV_32FC1", nameof(source)); + if (destination.Type() != OpenCvSharp.MatType.CV_8UC3) + throw new ArgumentException("Выходное изображение должно быть типа CV_8UC3", nameof(destination)); + if (source.Size() != destination.Size()) + throw new ArgumentException("Размеры исходного и выходного изображений должны совпадать"); + if (maxVal <= minVal) + throw new ArgumentException("Максимальное значение диапазона должно быть больше минимального."); - for (int i = 0; i <= 85; i++) - { - var val = (byte)Math.Round(i * (255.0 / 85.0)); - indexer[i, 0] = new OpenCvSharp.Vec3b(val, val, val); - } + // Создаем маску для целевого диапазона значений + using var mask = new OpenCvSharp.Mat(); + OpenCvSharp.Cv2.InRange(source, OpenCvSharp.Scalar.All(minVal), OpenCvSharp.Scalar.All(maxVal), mask); - for (int i = 86; i <= 170; i++) - { - var factor = (i - 85) / 85.0; // от 0 до 1 - var rbVal = (byte)Math.Round(255 * (1.0 - factor)); - indexer[i, 0] = new OpenCvSharp.Vec3b(rbVal, 255, rbVal); - } + // Нормализуем значения из диапазона minVal...maxVal в 0.0...1.0 + using var normalized = new OpenCvSharp.Mat(); + OpenCvSharp.Cv2.Subtract(source, OpenCvSharp.Scalar.All(minVal), normalized); + normalized.ConvertTo(normalized, OpenCvSharp.MatType.CV_32FC1, 1.0 / (maxVal - minVal)); - for (int i = 171; i <= 255; i++) - { - var factor = (i - 170) / 85.0; // от 0 до 1 - var gVal = (byte)Math.Round(255 * (1.0 - factor)); - var rVal = (byte)Math.Round(255 * factor); - indexer[i, 0] = new OpenCvSharp.Vec3b(0, gVal, rVal); // BGR - } + // Чтобы избежать выхода за границы, ограничиваем диапазон от 0.0...1.0 + OpenCvSharp.Cv2.Threshold(normalized, normalized, 1.0, 1.0, OpenCvSharp.ThresholdTypes.Trunc); + OpenCvSharp.Cv2.Threshold(normalized, normalized, 0.0, 0.0, OpenCvSharp.ThresholdTypes.Tozero); - return lut; + // Вычисляем градиент для каждого из трех каналов (BGR) + using var bChannel = new OpenCvSharp.Mat(); + using var gChannel = new OpenCvSharp.Mat(); + using var rChannel = new OpenCvSharp.Mat(); + + double diffB = maxColor.Val0 - minColor.Val0; + double diffG = maxColor.Val1 - minColor.Val1; + double diffR = maxColor.Val2 - minColor.Val2; + + normalized.ConvertTo(bChannel, OpenCvSharp.MatType.CV_8UC1, diffB, minColor.Val0); + normalized.ConvertTo(gChannel, OpenCvSharp.MatType.CV_8UC1, diffG, minColor.Val1); + normalized.ConvertTo(rChannel, OpenCvSharp.MatType.CV_8UC1, diffR, minColor.Val2); + + // Объеденяем каналы в одно изображение + using var gradientColor = new OpenCvSharp.Mat(); + OpenCvSharp.Cv2.Merge(new[] { bChannel, gChannel, rChannel }, gradientColor); + + // Копируем полученный градиент в итоговое изображение по маске + gradientColor.CopyTo(destination, mask); } } diff --git a/GSS2/Views/AmineContent/AnalysisView.axaml b/GSS2/Views/AmineContent/AnalysisView.axaml index f521a54..ecc9a17 100644 --- a/GSS2/Views/AmineContent/AnalysisView.axaml +++ b/GSS2/Views/AmineContent/AnalysisView.axaml @@ -185,9 +185,8 @@ IsReadOnly="True" IsVisible="{Binding !IsNewRecord}" PlaceholderText="Тип сепаратора" - > - - + Text="{Binding Record.SeparatorType}" + /> - + + + + + + + @@ -365,234 +365,238 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + + + + + + + + + - - + - - - - - - - + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + - + - + + + + + + + + + + - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - - - - + - + + + + + + + + + + - - + + + + + + + + + + - - - - - - + - - - + - + - - - - - - - - - - + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - + - + - + + + + + + + + + + - + + + + + + + + + + - + + + + + @@ -657,7 +661,7 @@ Grid.Row="1" IsReadOnly="True" PlaceholderText="Средняя норм. обр. площадь" - Text="{Binding Record.MeanProcessedArea}" + Text="{Binding Record.MeanProcessedArea, StringFormat={}{0:F4}}" /> @@ -856,30 +860,6 @@ - - - - - - - - - - - @@ -1133,192 +1113,182 @@ + + + + + + + 5 + - - + + + - + 5 - - - - - - - + + + - 5 + + - - - - - + + - 5 + + - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - + + - - - - - + + + + + + + - + + + + + + + + - - - - - - - - - - + + + + - - - - - + + - - - - - + + + + + - + - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/GSS2/Views/AmineContent/AnalysisView.axaml.cs b/GSS2/Views/AmineContent/AnalysisView.axaml.cs index ef473aa..052a225 100644 --- a/GSS2/Views/AmineContent/AnalysisView.axaml.cs +++ b/GSS2/Views/AmineContent/AnalysisView.axaml.cs @@ -17,11 +17,9 @@ public partial class AnalysisView : UserControl private void OnChartsUpdate(object? sender, EventArgs ea) { ProcessedAreasHistogram.CoreChart.Update(); - UnprocessedAreasHistogram.CoreChart.Update(); HotspotAreasHistogram.CoreChart.Update(); OpticalIntegralsHistogram.CoreChart.Update(); InnerStdDevsHistogram.CoreChart.Update(); InnerHeterogeneitiesHistogram.CoreChart.Update(); - ParticleValuesHistogram.CoreChart.Update(); } }