feat: testing and fixes

This commit is contained in:
2026-07-08 08:25:09 +03:00
parent 72ae26eee7
commit 5b6abc045b
6 changed files with 730 additions and 606 deletions
@@ -172,6 +172,7 @@ public partial class AnalyzerService
} }
var sampleDataSet = sampleRecords var sampleDataSet = sampleRecords
.AsEnumerable()
.Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0)
.SelectMany(r => r.Features.Chunk(FEATURES_LENGTH)) .SelectMany(r => r.Features.Chunk(FEATURES_LENGTH))
.Select(f => new ClusterizationData .Select(f => new ClusterizationData
@@ -231,6 +232,7 @@ public partial class AnalyzerService
_logger.LogInformation("Подготовка данных"); _logger.LogInformation("Подготовка данных");
var trainDataSet = sampleRecords var trainDataSet = sampleRecords
.AsEnumerable()
.Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0)
.Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0) .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0)
.SelectMany(r => .SelectMany(r =>
@@ -290,7 +292,7 @@ public partial class AnalyzerService
model.Dispose(); model.Dispose();
} }
public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, Mat result, string separatorType, string sampleBrand)?> Analyze(IEnumerable<ImageData> imagesData) public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, Mat result)?> Analyze(IEnumerable<ImageData> imagesData)
{ {
if (!Initialized) if (!Initialized)
return null; return null;
@@ -304,7 +306,7 @@ public partial class AnalyzerService
if (segmentationResult is null) if (segmentationResult is null)
return 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)>(); var images = new List<(Mat mat, double visible, double uv365, double uv254)>();
foreach (var imageData in imagesData 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.Erode(mask, mask, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)));
Cv2.FindContours(mask, out var newContours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple); 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) catch (Exception ex)
{ {
@@ -418,10 +420,10 @@ public partial class AnalyzerService
return null; return null;
} }
} }
private async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, string separatorType, string sampleBrand)?> Segment(IEnumerable<ImageData> imagesData) private async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours)?> Segment(IEnumerable<ImageData> imagesData)
{ {
var separatorPrefix = "SEPARATOR"; var separatorPrefix = "separator";
var samplePrefix = "SAMPLE"; var samplePrefix = "sample";
if (imagesData.Count() != IMAGES_COUNT) if (imagesData.Count() != IMAGES_COUNT)
return null; 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)); 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); 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}"))); _logger.LogInformation("Детекторованные обекты:\n\t{}", string.Join("\n\t", frequency.Select(f => $"{f.Key}: {f.Value}")));
return ( return (
avgImage, avgImage,
roi.Value, roi.Value,
result, result,
contours.Where(ContourIsValid), 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)
); );
} }
+1 -2
View File
@@ -82,7 +82,6 @@ public class CameraService : IDisposable
private FrameBufferAllocator? _imageCaptureFrameBufferAllocator; private FrameBufferAllocator? _imageCaptureFrameBufferAllocator;
private Stream? _imageCaptureStream; private Stream? _imageCaptureStream;
private List<MappedBuffer>? _imageCaptureMappedBuffers; private List<MappedBuffer>? _imageCaptureMappedBuffers;
private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null;
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
private readonly Dictionary<string, object> _cameraHardSettings = new Dictionary<string, object>(); private readonly Dictionary<string, object> _cameraHardSettings = new Dictionary<string, object>();
@@ -238,7 +237,7 @@ public class CameraService : IDisposable
_cameraConfiguration = _camera.GenerateConfiguration(StreamRoleEnum.Raw); _cameraConfiguration = _camera.GenerateConfiguration(StreamRoleEnum.Raw);
_imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First(); _imageCaptureStreamConfiguration = _cameraConfiguration.First();
_imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount); _imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount);
_imageCaptureStreamConfiguration.Size = new Size( _imageCaptureStreamConfiguration.Size = new Size(
checked((uint)_configuration.ImageCaptureWidth), checked((uint)_configuration.ImageCaptureWidth),
+18
View File
@@ -54,6 +54,12 @@ public class ZoomPanImage : Control
get => GetValue(PanYProperty); get => GetValue(PanYProperty);
set => SetValue(PanYProperty, value); set => SetValue(PanYProperty, value);
} }
public static readonly StyledProperty<bool> CanUserZoomPanProperty = AvaloniaProperty.Register<ZoomPanImage, bool>(nameof(CanUserZoomPan), defaultValue: true);
public bool CanUserZoomPan
{
get => GetValue(CanUserZoomPanProperty);
set => SetValue(CanUserZoomPanProperty, value);
}
public ZoomPanImage() public ZoomPanImage()
: base() : base()
@@ -64,6 +70,10 @@ public class ZoomPanImage : Control
PanX = 0f; PanX = 0f;
PanY = 0f; PanY = 0f;
}; };
AffectsRender<ZoomPanImage>(ZoomProperty);
AffectsRender<ZoomPanImage>(PanXProperty);
AffectsRender<ZoomPanImage>(PanYProperty);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -75,6 +85,8 @@ public class ZoomPanImage : Control
protected override void OnPointerWheelChanged(PointerWheelEventArgs ea) protected override void OnPointerWheelChanged(PointerWheelEventArgs ea)
{ {
if (!CanUserZoomPan)
return;
if (ea.Pointer.Type == PointerType.Mouse) if (ea.Pointer.Type == PointerType.Mouse)
{ {
var position = ea.GetPosition(this); var position = ea.GetPosition(this);
@@ -107,6 +119,8 @@ public class ZoomPanImage : Control
} }
protected override void OnPointerPressed(PointerPressedEventArgs ea) protected override void OnPointerPressed(PointerPressedEventArgs ea)
{ {
if (!CanUserZoomPan)
return;
if (ea.Pointer.Type == PointerType.Mouse) if (ea.Pointer.Type == PointerType.Mouse)
{ {
if (ea.Properties.IsLeftButtonPressed) if (ea.Properties.IsLeftButtonPressed)
@@ -136,6 +150,8 @@ public class ZoomPanImage : Control
} }
protected override void OnPointerReleased(PointerReleasedEventArgs ea) protected override void OnPointerReleased(PointerReleasedEventArgs ea)
{ {
if (!CanUserZoomPan)
return;
if (ea.Pointer.Type == PointerType.Mouse) if (ea.Pointer.Type == PointerType.Mouse)
{ {
if (!ea.Properties.IsLeftButtonPressed) if (!ea.Properties.IsLeftButtonPressed)
@@ -151,6 +167,8 @@ public class ZoomPanImage : Control
} }
protected override void OnPointerMoved(PointerEventArgs ea) protected override void OnPointerMoved(PointerEventArgs ea)
{ {
if (!CanUserZoomPan)
return;
if (ea.Pointer.Type == PointerType.Mouse) if (ea.Pointer.Type == PointerType.Mouse)
{ {
if (_lastMousePosition is null || Zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем if (_lastMousePosition is null || Zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем
+325 -178
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Avalonia;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -16,6 +17,10 @@ using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.Painting.Effects; using LiveChartsCore.SkiaSharpView.Painting.Effects;
using UnitsNet;
using MathNet.Numerics.Statistics;
using GSS2.Core.Hardware; using GSS2.Core.Hardware;
using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent;
using GSS2.Core.Analysis.AmineContent.Database; using GSS2.Core.Analysis.AmineContent.Database;
@@ -24,10 +29,6 @@ using GSS2.Core.Extensions;
using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.ViewModels;
using GSS2.UI.Core.Services; using GSS2.UI.Core.Services;
using UnitsNet;
using MathNet.Numerics.Statistics;
namespace GSS2.ViewModels.AmineContent; namespace GSS2.ViewModels.AmineContent;
public partial class AnalysisViewModel : ViewModelBase public partial class AnalysisViewModel : ViewModelBase
@@ -85,8 +86,8 @@ public partial class AnalysisViewModel : ViewModelBase
/// </summary> /// </summary>
public partial class RecordViewModel : ViewModelBase public partial class RecordViewModel : ViewModelBase
{ {
public const double UNPROCESSED_THRESHOLD = 0.5; public const double UNPROCESSED_THRESHOLD = 0.6;
public const double HOTSPOT_THRESHOLD = 1.2; public const double HOTSPOT_THRESHOLD = 1.1;
public const double EPSILON = 1e-10; public const double EPSILON = 1e-10;
public const double UNPROCESSED_FRACTION_THRESHOLD = 10; public const double UNPROCESSED_FRACTION_THRESHOLD = 10;
@@ -415,19 +416,6 @@ public partial class AnalysisViewModel : ViewModelBase
/// </summary> /// </summary>
[ObservableProperty] public partial int ParticleIndex { get; set; } = 0; [ObservableProperty] public partial int ParticleIndex { get; set; } = 0;
/// <summary>
/// Представление <see cref="ResultRecord.ImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleImage { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.MaskImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleMaskImage { get; set; } = new();
/// <summary>
/// Представление <see cref="ResultRecord.ResultImageId"/> выбранной гранулы
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel ParticleResultImage { get; set; } = new();
/// <summary> /// <summary>
/// Выбрано ли изображение <see cref="ParticleImage"/> /// Выбрано ли изображение <see cref="ParticleImage"/>
/// </summary> /// </summary>
@@ -452,7 +440,12 @@ public partial class AnalysisViewModel : ViewModelBase
/// <summary> /// <summary>
/// Смещение по Y изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/> /// Смещение по Y изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary> /// </summary>
[ObservableProperty] public partial float ImagesPanY { get; set; } = 0; [ObservableProperty] public partial float ParticleImagesPanY { get; set; } = 0;
/// <summary>
/// Ширина изображений <see cref="ParticleImage"/>, <see cref="ParticleMaskImage"/> и <see cref="ParticleResultImage"/>
/// </summary>
[ObservableProperty] public partial Rect ParticleImagesBounds { get; set; } = new Rect();
/// <summary>
/// <summary> /// <summary>
/// Нормированная обработанная площадь выбранной гранулы /// Нормированная обработанная площадь выбранной гранулы
@@ -501,6 +494,33 @@ public partial class AnalysisViewModel : ViewModelBase
InnerStdDevs.Clear(); InnerStdDevs.Clear();
InnerHeterogeneities.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()); var max = Values.Max(vs => vs.Max());
foreach (var value in Values) foreach (var value in Values)
@@ -517,10 +537,10 @@ public partial class AnalysisViewModel : ViewModelBase
// Вычисляем значения // Вычисляем значения
var area = normalized.Count(); var area = normalized.Count();
var processedArea = normalized.Count(v => v > 1) / area * 100; var processedArea = normalized.Count(v => v > 1) * 100.0 / area;
var unprocessedArea = normalized.Count(v => v <= 1) / area * 100; var unprocessedArea = normalized.Count(v => v <= 1) * 100.0 / area;
var hotspotArea = normalized.Count(v => v > 2) / area * 100; var hotspotArea = normalized.Count(v => v > 2) * 100.0 / area;
var opticalIntegral = normalized.Sum() / area * 100; var opticalIntegral = normalized.Sum() * 100.0 / area;
var innerStdDev = normalized.StdDev(); var innerStdDev = normalized.StdDev();
var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON); var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON);
@@ -558,6 +578,7 @@ public partial class AnalysisViewModel : ViewModelBase
// Сбрасываем индекс выбранной гранулы // Сбрасываем индекс выбранной гранулы
ParticleIndex = 0; ParticleIndex = 0;
ZoomPanOnParticle();
} }
/// <summary> /// <summary>
@@ -578,7 +599,7 @@ public partial class AnalysisViewModel : ViewModelBase
{ {
if (ParticlesCount == 0) if (ParticlesCount == 0)
return; return;
ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount; // ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount;
} }
/// <summary> /// <summary>
/// Переключить на изображение <see cref="RecordViewModel.Image"/> /// Переключить на изображение <see cref="RecordViewModel.Image"/>
@@ -638,8 +659,35 @@ public partial class AnalysisViewModel : ViewModelBase
ParticleInnerHeterogeneity = InnerHeterogeneities[value]; ParticleInnerHeterogeneity = InnerHeterogeneities[value];
// Обновляем гистограмму // Обновляем гистограмму
ParticleValuesHistogram.Set(NormalizedValues[value], 0, 3, "Количество, шт", "Значение"); 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; public event EventHandler? UpdateCharts;
@@ -868,6 +916,11 @@ public partial class AnalysisViewModel : ViewModelBase
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems));
return; return;
} }
// Берём индекс изменённого элемента в выпадающем списке марок
var editedComboBoxIndex = SeparatorComboBoxItems.IndexOf(editedComboBoxItem);
// Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи
if (SeparatorComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord)
Record.SeparatorType = separatorRecord.Type;
// Обновляем элемент из выпадающего списка марок // Обновляем элемент из выпадающего списка марок
editedComboBoxItem.Type = separatorRecord.Type; editedComboBoxItem.Type = separatorRecord.Type;
} }
@@ -911,6 +964,15 @@ public partial class AnalysisViewModel : ViewModelBase
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems));
return; 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.BrandName = brandRecord.BrandName;
editedComboBoxItem.MixtureName = brandRecord.MixtureName; editedComboBoxItem.MixtureName = brandRecord.MixtureName;
@@ -926,13 +988,16 @@ public partial class AnalysisViewModel : ViewModelBase
else // Если записей нет, else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanScroll // Подписываемся на изменения свойств для обновления CanInteract и обновления графиков
PropertyChanged += (_, ea) => PropertyChanged += (_, ea) =>
{ {
if (ea.PropertyName == nameof(MenuOpened) || if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling) || ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling)) ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract(); UpdateCanInteract();
if (ea.PropertyName == nameof(MenuSelectedIndex) ||
ea.PropertyName == nameof(InfoSectionCurrentIndex) )
UpdateCharts?.Invoke(this, EventArgs.Empty);
}; };
_logger.LogInformation("Инициализировано"); _logger.LogInformation("Инициализировано");
@@ -946,81 +1011,79 @@ public partial class AnalysisViewModel : ViewModelBase
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
if (value >= MenuItems.Count()) if (value >= MenuItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count()); _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count());
// Если значение больше или равно 0, то значит выбран один из элементов меню // Если значение меньше 0, то значит элемент меню не выбран
if (value >= 0 && value < MenuItems.Count()) if (value < 0 || value > MenuItems.Count())
{ {
_logger.LogTrace("Выбран элемент из списка: {}", value); // Логгируем как ошибку
// Находим запись в базе данных _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id); // Сохраняем несуществующий индекс
// Если запись найдена, то заполняем поля из базы данных var nonExistingIndex = MenuSelectedIndex;
if (selectedRecord is not null) // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
{ MenuSelectedIndex = -1;
// Заполняем простые поля // Удаляем несуществующий элемент из списка
Record.Id = selectedRecord.Id; MenuItems.RemoveAt(nonExistingIndex);
Record.Uuid = selectedRecord.Uuid; return;
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.LogTrace("Выбран элемент из списка: {}", value);
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); // Находим запись в базе данных
// Если изображение не найдено в базе данных, то логгируем как ошибку var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id);
if (imageRecord is null) // Если запись найдена, то заполняем поля из базы данных
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); if (selectedRecord is not null)
// В любом случае заполняем изображение {
Record.Image.Data = imageRecord?.Base64 ?? ""; // Заполняем простые поля
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); var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку // Если изображение не найдено в базе данных, то логгируем как ошибку
if (maskImageRecord is null) if (imageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение // В любом случае заполняем изображение
Record.MaskImage.Data = maskImageRecord?.Base64 ?? ""; Record.Image.Data = imageRecord?.Base64 ?? "";
// Ищем изображение результата в базе данных // Ищем изображение маски в базе данных
var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId); var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку // Если изображение не найдено в базе данных, то логгируем как ошибку
if (resultImageRecord is null) if (maskImageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение // В любом случае заполняем изображение
Record.MaskImage.Data = resultImageRecord?.Base64 ?? ""; Record.MaskImage.Data = maskImageRecord?.Base64 ?? "";
// Сбрасываем приближение изображений // Ищем изображение результата в базе данных
ImagesPanX = 0; var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId);
ImagesPanY = 0; // Если изображение не найдено в базе данных, то логгируем как ошибку
ImagesZoom = 1; if (resultImageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение
Record.ResultImage.Data = resultImageRecord?.Base64 ?? "";
// Устанавливаем, что запись выбрана из базы данных // Сбрасываем приближение изображений
IsNewRecord = false; ImagesPanX = 0;
// Вычисляем дополнительные свойства ImagesPanY = 0;
Record.CalculateProperties(); ImagesZoom = 1;
return; // Устанавливаем, что запись выбрана из базы данных
} IsNewRecord = false;
// Если значение не найдено в базе данных // Вычисляем дополнительные свойства
else Record.CalculateProperties();
{
// Логгируем как ошибку return;
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
MenuSelectedIndex = -1;
// Удаляем несуществующий элемент из списка
MenuItems.RemoveAt(nonExistingIndex);
return;
}
} }
// Если элемент не выбран, заполняем поля пустыми значениями // Если элемент не выбран, заполняем поля пустыми значениями
@@ -1056,6 +1119,84 @@ public partial class AnalysisViewModel : ViewModelBase
Record.CalculateProperties(); Record.CalculateProperties();
} }
/// <summary>
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndex"/>
/// </summary>
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;
}
/// <summary>
/// Вызывается при изменении <see cref="SeparatorComboBoxSelectedIndex"/>
/// </summary>
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 = "";
}
/// <summary> /// <summary>
/// Обновить значение <see cref="CanInteract"/> /// Обновить значение <see cref="CanInteract"/>
/// </summary> /// </summary>
@@ -1386,11 +1527,11 @@ public partial class AnalysisViewModel : ViewModelBase
if (attachedImageRecord is null) if (attachedImageRecord is null)
{ {
// Если изображение есть // Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data)) if (!string.IsNullOrEmpty(Record.MaskImage.Data))
{ {
_logger.LogTrace("Создаём новую запись изображения"); _logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения // Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.Image.Data); var imageRecord = new ImageRecord(base64: Record.MaskImage.Data);
// Добавляем запись изображения в базу данных // Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
@@ -1403,11 +1544,11 @@ public partial class AnalysisViewModel : ViewModelBase
else else
{ {
// Если изображение есть // Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data)) if (!string.IsNullOrEmpty(Record.MaskImage.Data))
{ {
_logger.LogTrace("Записываем изменения в запись изображения"); _logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения // Вносим изменения
attachedImageRecord.Base64 = Record.Image.Data; attachedImageRecord.Base64 = Record.MaskImage.Data;
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
_context.SaveChanges(); _context.SaveChanges();
} }
@@ -1430,11 +1571,11 @@ public partial class AnalysisViewModel : ViewModelBase
if (attachedImageRecord is null) if (attachedImageRecord is null)
{ {
// Если изображение есть // Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data)) if (!string.IsNullOrEmpty(Record.ResultImage.Data))
{ {
_logger.LogTrace("Создаём новую запись изображения"); _logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения // Заполняем запись изображения
var imageRecord = new ImageRecord(base64: Record.Image.Data); var imageRecord = new ImageRecord(base64: Record.ResultImage.Data);
// Добавляем запись изображения в базу данных // Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
@@ -1447,11 +1588,11 @@ public partial class AnalysisViewModel : ViewModelBase
else else
{ {
// Если изображение есть // Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data)) if (!string.IsNullOrEmpty(Record.ResultImage.Data))
{ {
_logger.LogTrace("Записываем изменения в запись изображения"); _logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения // Вносим изменения
attachedImageRecord.Base64 = Record.Image.Data; attachedImageRecord.Base64 = Record.ResultImage.Data;
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
_context.SaveChanges(); _context.SaveChanges();
} }
@@ -1561,7 +1702,7 @@ public partial class AnalysisViewModel : ViewModelBase
// Выбрасываем исключение, если задача отменена // Выбрасываем исключение, если задача отменена
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Сохраняем PNG изображение во временную папку // Сохраняем 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); image.ImWrite(path);
// Освобождаем память изображения // Освобождаем память изображения
@@ -1574,6 +1715,18 @@ public partial class AnalysisViewModel : ViewModelBase
imagesData.Add(imagesDataTask); imagesData.Add(imagesDataTask);
}, cancellationToken); }, 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)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[BrandComboBoxSelectedIndex].Id, nameof(_context.BrandRecords));
return; return;
} }
if (brand.MixtureNormalRate <= 0)
{
_logger.LogError("Норма расхода выбранной марки меньше или равена 0");
return;
}
if (separator is null) if (separator is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords));
@@ -1662,13 +1820,16 @@ public partial class AnalysisViewModel : ViewModelBase
await Task.Delay(3000); await Task.Delay(3000);
_lights.Disconnect(); _lights.Disconnect();
}); });
return;
} }
// Распаковываем результат // Распаковываем результат
var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value; var (avgImage, _, mask, contours, resultImage) = result.Value;
try try
{ {
Record.ParticlesCount = contours.Count();
// Кодируем среднее изображение // Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
Record.Image.Data = Convert.ToBase64String(avgData); Record.Image.Data = Convert.ToBase64String(avgData);
@@ -1685,7 +1846,7 @@ public partial class AnalysisViewModel : ViewModelBase
new OpenCvSharp.Scalar(0) new OpenCvSharp.Scalar(0)
); );
Record.Values = new List<float[]>(); var values = new List<float[]>();
// Проходим по каждому контуру // Проходим по каждому контуру
foreach (var contour in contours) foreach (var contour in contours)
{ {
@@ -1704,8 +1865,9 @@ public partial class AnalysisViewModel : ViewModelBase
if (contourMask.Get<byte>(i, j) > 127) if (contourMask.Get<byte>(i, j) > 127)
contourValues.Add(resultImage.Get<float>(i, j)); contourValues.Add(resultImage.Get<float>(i, j));
// Сохраняем полученные значения // Сохраняем полученные значения
Record.Values.Add(contourValues.ToArray()); values.Add(contourValues.ToArray());
} }
Record.Values = values;
// Освобождаем память маски контура // Освобождаем память маски контура
contourMask.Dispose(); contourMask.Dispose();
@@ -1714,65 +1876,30 @@ public partial class AnalysisViewModel : ViewModelBase
Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList(); Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList();
// Вычисляем пороговые значения // Вычисляем пороговые значения
var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * Record.MixtureNormalRate; var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * brand.MixtureNormalRate;
var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * Record.MixtureNormalRate; var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * brand.MixtureNormalRate;
// Выичсляем максимальное значение
OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max); OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max);
// Если иаксмиальное значниеz слишком мало, то устанавливаем его чуть больше hotspotsThreshold, чтобы не возникло деления на ноль и отридцательных значений
if (max <= hotspotsThreshold) if (max <= hotspotsThreshold)
max = hotspotsThreshold + 0.001; max = hotspotsThreshold + 0.001;
var lutIndices = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC1, OpenCvSharp.Scalar.All(0));
using (var mask1 = new OpenCvSharp.Mat()) // Создаём цветное изображение результата
{ var coloredResultImage = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC3, OpenCvSharp.Scalar.All(0));
OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(0), OpenCvSharp.Scalar.All(unprocessedThreshold), mask1); // Расшрашиваем диапазон 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);
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 всё что не попадает в маску // Устанавливаем в 0 всё что не попадает в маску
var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0)); var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0));
OpenCvSharp.Cv2.BitwiseNot(mask, invMask); 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(); invMask.Dispose();
resultImage.Dispose(); resultImage.Dispose();
@@ -1783,10 +1910,10 @@ public partial class AnalysisViewModel : ViewModelBase
mask.Release(); mask.Release();
// Кодируем изображение результата // Кодируем изображение результата
OpenCvSharp.Cv2.ImEncode(".png", coloredResult, out var resultData); OpenCvSharp.Cv2.ImEncode(".png", coloredResultImage, out var resultData);
Record.ResultImage.Data = Convert.ToBase64String(resultData); Record.ResultImage.Data = Convert.ToBase64String(resultData);
// Освобождаем память изображения результата // Освобождаем память изображения результата
coloredResult.Release(); coloredResultImage.Release();
// Пересчитываем значения свойств // Пересчитываем значения свойств
Record.CalculateProperties(); Record.CalculateProperties();
@@ -1883,32 +2010,52 @@ public partial class AnalysisViewModel : ViewModelBase
/// </summary> /// </summary>
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
private static OpenCvSharp.Mat CreateLut() /// <summary>
/// Раскрасить диапазон в градиент
/// </summary>
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<OpenCvSharp.Vec3b>(); 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++) // Создаем маску для целевого диапазона значений
{ using var mask = new OpenCvSharp.Mat();
var val = (byte)Math.Round(i * (255.0 / 85.0)); OpenCvSharp.Cv2.InRange(source, OpenCvSharp.Scalar.All(minVal), OpenCvSharp.Scalar.All(maxVal), mask);
indexer[i, 0] = new OpenCvSharp.Vec3b(val, val, val);
}
for (int i = 86; i <= 170; i++) // Нормализуем значения из диапазона minVal...maxVal в 0.0...1.0
{ using var normalized = new OpenCvSharp.Mat();
var factor = (i - 85) / 85.0; // от 0 до 1 OpenCvSharp.Cv2.Subtract(source, OpenCvSharp.Scalar.All(minVal), normalized);
var rbVal = (byte)Math.Round(255 * (1.0 - factor)); normalized.ConvertTo(normalized, OpenCvSharp.MatType.CV_32FC1, 1.0 / (maxVal - minVal));
indexer[i, 0] = new OpenCvSharp.Vec3b(rbVal, 255, rbVal);
}
for (int i = 171; i <= 255; i++) // Чтобы избежать выхода за границы, ограничиваем диапазон от 0.0...1.0
{ OpenCvSharp.Cv2.Threshold(normalized, normalized, 1.0, 1.0, OpenCvSharp.ThresholdTypes.Trunc);
var factor = (i - 170) / 85.0; // от 0 до 1 OpenCvSharp.Cv2.Threshold(normalized, normalized, 0.0, 0.0, OpenCvSharp.ThresholdTypes.Tozero);
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
}
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);
} }
} }
+344 -374
View File
@@ -185,9 +185,8 @@
IsReadOnly="True" IsReadOnly="True"
IsVisible="{Binding !IsNewRecord}" IsVisible="{Binding !IsNewRecord}"
PlaceholderText="Тип сепаратора" PlaceholderText="Тип сепаратора"
> Text="{Binding Record.SeparatorType}"
<!-- <Run Text="{Binding Record.SeparatorType}" /> --> />
</TextBox>
<!-- BrandComboBox (подпись) --> <!-- BrandComboBox (подпись) -->
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
@@ -229,12 +228,13 @@
IsVisible="{Binding !IsNewRecord}" IsVisible="{Binding !IsNewRecord}"
PlaceholderText="Марка пробы" PlaceholderText="Марка пробы"
> >
<!-- <Run Text="{Binding Record.BrandName}" /> <TextBox.Text>
<Run Text=" - " /> <MultiBinding StringFormat="{}{0} - {1} ({2} кг/т)" Mode="OneWay">
<Run Text="{Binding Record.MixtureName}" /> <Binding Path="Record.BrandName" />
<Run Text=" (" /> <Binding Path="Record.MixtureName" />
<Run Text="{Binding Record.MixtureNormalRate}" /> <Binding Path="Record.MixtureNormalRate" />
<Run Text=" кг/т)" /> --> </MultiBinding>
</TextBox.Text>
</TextBox> </TextBox>
<!-- SampleName (подпись) --> <!-- SampleName (подпись) -->
@@ -365,234 +365,238 @@
<TextBlock Text="3"/> <TextBlock Text="3"/>
</Button> </Button>
<!-- Изображения --> <!-- Изображения -->
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3"> <Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
<!-- Изображения --> <!-- Изображения -->
<Grid> <!-- Image -->
<Grid IsVisible="{Binding ImageIsVisible}" IsEnabled="{Binding ImageIsVisible}">
<!-- Изображения --> <!-- Изображение -->
<!-- Image --> <core:ZoomPanImage
<Grid IsVisible="{Binding ImageIsVisible}"> Background="Black"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.Image.Image}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
HorizontalAlignment="Stretch" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
ImageSource="{Binding Record.Image.Image}" <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
VerticalAlignment="Stretch" </MultiBinding>
> </core:ZoomPanImage.IsVisible>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение загружается" -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<!-- Оверлей "Изображение загружается" --> <StackPanel.IsVisible>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" > <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" /> </StackPanel>
</StackPanel> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей "Изображение отсутствует" --> <!-- Оверлей прогресс -->
<TextBlock <StackPanel
HorizontalAlignment="Center" HorizontalAlignment="Stretch"
Text="Изображение отсутствует" VerticalAlignment="Center"
VerticalAlignment="Center" >
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей прогресс --> <StackPanel.IsVisible>
<StackPanel <MultiBinding Converter="{x:Static BoolConverters.And}">
HorizontalAlignment="Stretch" <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
VerticalAlignment="Center" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
> <Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" /> </StackPanel>
</StackPanel> </Grid>
</Grid> <!-- MaskImage -->
<Grid IsVisible="{Binding MaskImageIsVisible}" IsEnabled="{Binding MaskImageIsVisible}">
<!-- MaskImage --> <!-- Изображение -->
<Grid IsVisible="{Binding MaskImageIsVisible}"> <core:ZoomPanImage
Background="Black"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.MaskImage.Image}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.MaskImage.Data" />
HorizontalAlignment="Stretch" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
ImageSource="{Binding Record.MaskImage.Image}" <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
VerticalAlignment="Stretch" </MultiBinding>
> </core:ZoomPanImage.IsVisible>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.MaskImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение загружается" -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<!-- Оверлей "Изображение загружается" --> <StackPanel.IsVisible>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" > <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.MaskImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.MaskImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" /> </StackPanel>
</StackPanel> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей "Изображение отсутствует" --> <!-- Оверлей прогресс -->
<TextBlock <StackPanel
HorizontalAlignment="Center" HorizontalAlignment="Stretch"
Text="Изображение отсутствует" VerticalAlignment="Center"
VerticalAlignment="Center" >
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей прогресс --> <StackPanel.IsVisible>
<StackPanel <MultiBinding Converter="{x:Static BoolConverters.And}">
HorizontalAlignment="Stretch" <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
VerticalAlignment="Center" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
> <Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.MaskImage.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" /> </StackPanel>
</StackPanel> </Grid>
</Grid> <!-- ResultImage -->
<Grid IsVisible="{Binding ResultImageIsVisible}" IsEnabled="{Binding ResultImageIsVisible}">
<!-- ResultImage --> <!-- Изображение -->
<Grid IsVisible="{Binding ResultImageIsVisible}"> <core:ZoomPanImage
Background="Black"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.ResultImage.Image}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ResultImage.Data" />
HorizontalAlignment="Stretch" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
ImageSource="{Binding Record.ResultImage.Image}" <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
VerticalAlignment="Stretch" </MultiBinding>
> </core:ZoomPanImage.IsVisible>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ResultImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение загружается" -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<!-- Оверлей "Изображение загружается" --> <StackPanel.IsVisible>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" > <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.ResultImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="Record.ResultImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" /> </StackPanel>
</StackPanel> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей "Изображение отсутствует" --> <!-- Оверлей прогресс -->
<TextBlock <StackPanel
HorizontalAlignment="Center" HorizontalAlignment="Stretch"
Text="Изображение отсутствует" VerticalAlignment="Center"
VerticalAlignment="Center" >
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей прогресс --> <StackPanel.IsVisible>
<StackPanel <MultiBinding Converter="{x:Static BoolConverters.And}">
HorizontalAlignment="Stretch" <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
VerticalAlignment="Center" <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
> <Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<StackPanel.IsVisible> <ProgressBar IsIndeterminate="True" />
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.ResultImage.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" /> <TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" /> </StackPanel>
</StackPanel>
</Grid>
</Grid> </Grid>
@@ -657,7 +661,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Средняя норм. обр. площадь" PlaceholderText="Средняя норм. обр. площадь"
Text="{Binding Record.MeanProcessedArea}" Text="{Binding Record.MeanProcessedArea, StringFormat={}{0:F4}}"
/> />
<!-- ProcessedAreaP50 (подпись) --> <!-- ProcessedAreaP50 (подпись) -->
<TextBlock <TextBlock
@@ -673,7 +677,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="XXX" PlaceholderText="XXX"
Text="{Binding Record.ProcessedAreaP50}" Text="{Binding Record.ProcessedAreaP50, StringFormat={}{0:F4}}"
/> />
<!-- ProcessedAreaP10 (подпись) --> <!-- ProcessedAreaP10 (подпись) -->
<TextBlock <TextBlock
@@ -689,7 +693,7 @@
Grid.Row="3" Grid.Row="3"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="10-й процентиль" PlaceholderText="10-й процентиль"
Text="{Binding Record.ProcessedAreaP10}" Text="{Binding Record.ProcessedAreaP10, StringFormat={}{0:F4}}"
/> />
<!-- ProcessedAreaP90 (подпись) --> <!-- ProcessedAreaP90 (подпись) -->
<TextBlock <TextBlock
@@ -705,7 +709,7 @@
Grid.Row="3" Grid.Row="3"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="90-й процентиль" PlaceholderText="90-й процентиль"
Text="{Binding Record.ProcessedAreaP90}" Text="{Binding Record.ProcessedAreaP90, StringFormat={}{0:F4}}"
/> />
<!-- ProcessedAreaDeltaP90P10 (подпись) --> <!-- ProcessedAreaDeltaP90P10 (подпись) -->
<TextBlock <TextBlock
@@ -721,7 +725,7 @@
Grid.Row="3" Grid.Row="3"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Ширина центрального диапазона" PlaceholderText="Ширина центрального диапазона"
Text="{Binding Record.ProcessedAreaDeltaP90P10}" Text="{Binding Record.ProcessedAreaDeltaP90P10, StringFormat={}{0:F4}}"
/> />
<!-- ProcessedAreaStdDev (подпись) --> <!-- ProcessedAreaStdDev (подпись) -->
<TextBlock <TextBlock
@@ -737,7 +741,7 @@
Grid.Row="5" Grid.Row="5"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Межгранульное СКО" PlaceholderText="Межгранульное СКО"
Text="{Binding Record.ProcessedAreaStdDev}" Text="{Binding Record.ProcessedAreaStdDev, StringFormat={}{0:F4}}"
/> />
<!-- InterHeterogeneity (подпись) --> <!-- InterHeterogeneity (подпись) -->
<TextBlock <TextBlock
@@ -753,7 +757,7 @@
Grid.Row="5" Grid.Row="5"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Межгранульная неоднородность" PlaceholderText="Межгранульная неоднородность"
Text="{Binding Record.InterHeterogeneity}" Text="{Binding Record.InterHeterogeneity, StringFormat={}{0:F4}}"
/> />
<!-- UnprocessedFraction (подпись) --> <!-- UnprocessedFraction (подпись) -->
<TextBlock <TextBlock
@@ -769,7 +773,7 @@
Grid.Row="5" Grid.Row="5"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Доля необработанных гранул" PlaceholderText="Доля необработанных гранул"
Text="{Binding Record.UnprocessedFraction}" Text="{Binding Record.UnprocessedFraction, StringFormat={}{0:F4}}"
/> />
<!-- MeanInnerHeterogeneity (подпись) --> <!-- MeanInnerHeterogeneity (подпись) -->
<TextBlock <TextBlock
@@ -785,7 +789,7 @@
Grid.Row="7" Grid.Row="7"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Ср. внутригранульная однородность" PlaceholderText="Ср. внутригранульная однородность"
Text="{Binding Record.MeanInnerHeterogeneity}" Text="{Binding Record.MeanInnerHeterogeneity, StringFormat={}{0:F4}}"
/> />
<!-- MeanHotspotArea (подпись) --> <!-- MeanHotspotArea (подпись) -->
<TextBlock <TextBlock
@@ -801,7 +805,7 @@
Grid.Row="7" Grid.Row="7"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Ср. горячая площадь" PlaceholderText="Ср. горячая площадь"
Text="{Binding Record.MeanHotspotArea}" Text="{Binding Record.MeanHotspotArea, StringFormat={}{0:F4}}"
/> />
<!-- MeanOpticalIntegral (подпись) --> <!-- MeanOpticalIntegral (подпись) -->
<TextBlock <TextBlock
@@ -817,7 +821,7 @@
Grid.Row="7" Grid.Row="7"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Ср. инт. опт. показатель" PlaceholderText="Ср. инт. опт. показатель"
Text="{Binding Record.MeanOpticalIntegral}" Text="{Binding Record.MeanOpticalIntegral, StringFormat={}{0:F4}}"
/> />
</Grid> </Grid>
@@ -856,30 +860,6 @@
</Grid> </Grid>
<!-- Гистограмма нормированных необработанных площадей -->
<Grid>
<!-- Гистограмма -->
<lvc:CartesianChart
HorizontalAlignment="Stretch"
IsVisible="{Binding Path=Record.ParticlesCount, Converter={StaticResource GreaterThanConverter}, ConverterParameter=0}"
Series="{Binding Record.UnprocessedAreasHistogram.Series, Mode=OneWay}"
VerticalAlignment="Stretch"
x:Name="UnprocessedAreasHistogram"
XAxes="{Binding Record.UnprocessedAreasHistogram.XAxis, Mode=OneWay}"
YAxes="{Binding Record.UnprocessedAreasHistogram.YAxis, Mode=OneWay}"
/>
<!-- Оверлей "Нет информации" -->
<TextBlock
HorizontalAlignment="Center"
IsVisible="{Binding Path=Record.ParticlesCount, Converter={StaticResource LessThanOrEqualConverter}, ConverterParameter=0}"
Text="Нет информации"
VerticalAlignment="Center"
/>
</Grid>
<!-- Гистограмма нормированных площадей хотспотов --> <!-- Гистограмма нормированных площадей хотспотов -->
<Grid> <Grid>
@@ -1133,175 +1113,176 @@
</Grid> </Grid>
<!-- Изображения -->
<Grid Grid.Column="1"> <Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- Изображения -->
<ColumnDefinition Width="Auto"/> <!-- Кнопки -->
</Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Изображения --> <RowDefinition Height="*"/>
<RowDefinition Height="*" /> <!-- Гистограмма --> <RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing> <Grid.RowSpacing>5</Grid.RowSpacing>
<!-- Изображения --> <!-- Кнопки переключения -->
<Grid Grid.Row="0"> <!-- ParticleImage -->
<Button
Classes="vertical"
Command="{Binding Record.SwitchToImageCommand}"
Grid.Column="1"
Grid.Row="0"
VerticalAlignment="Stretch"
>
<TextBlock Text="1"/>
</Button>
<Grid.ColumnDefinitions> <!-- ParticleMaskImage -->
<ColumnDefinition Width="*"/> <!-- Изображения --> <Button
<ColumnDefinition Width="Auto"/> <!-- Кнопки --> Classes="vertical"
</Grid.ColumnDefinitions> Command="{Binding Record.SwitchToMaskImageCommand}"
Grid.Column="1"
Grid.Row="1"
VerticalAlignment="Stretch"
>
<TextBlock Text="2"/>
</Button>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing> <!-- ParticleResultImage -->
<Button
Classes="vertical"
Command="{Binding Record.SwitchToResultImageCommand}"
Grid.Column="1"
Grid.Row="2"
VerticalAlignment="Stretch"
>
<TextBlock Text="3"/>
</Button>
<Grid.RowDefinitions> <!-- Изображения -->
<RowDefinition Height="*"/> <Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing> <!-- Изображения -->
<Grid x:Name="PraticleImagesGrid" Bounds="{Binding Record.ParticleImagesBounds, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}">
<!-- Кнопки переключения -->
<!-- ParticleImage -->
<Button
Classes="vertical"
Command="{Binding Record.SwitchToImageCommand}"
Grid.Column="1"
Grid.Row="0"
VerticalAlignment="Stretch"
>
<TextBlock Text="1"/>
</Button>
<!-- ParticleMaskImage -->
<Button
Classes="vertical"
Command="{Binding Record.SwitchToMaskImageCommand}"
Grid.Column="1"
Grid.Row="1"
VerticalAlignment="Stretch"
>
<TextBlock Text="2"/>
</Button>
<!-- ParticleResultImage -->
<Button
Classes="vertical"
Command="{Binding Record.SwitchToResultImageCommand}"
Grid.Column="1"
Grid.Row="2"
VerticalAlignment="Stretch"
>
<TextBlock Text="3"/>
</Button>
<!-- Изображения -->
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
<!-- Изображения --> <!-- Изображения -->
<Grid> <!-- ParticleImage -->
<Grid IsVisible="{Binding Record.ParticleImageIsVisible}">
<!-- Изображения --> <!-- Изображение -->
<!-- ParticleImage --> <core:ZoomPanImage
<Grid IsVisible="{Binding Record.ParticleImageIsVisible}"> Background="Black"
CanUserZoomPan="False"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.Image.Image}"
PanX="{Binding Record.ParticleImagesPanX}"
PanY="{Binding Record.ParticleImagesPanY}"
VerticalAlignment="Stretch"
Zoom="{Binding Record.ParticleImagesZoom}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
HorizontalAlignment="Stretch" </MultiBinding>
ImageSource="{Binding Record.ParticleImage.Image}" </core:ZoomPanImage.IsVisible>
VerticalAlignment="Stretch"
>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleImage.Data" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей "Изображение отсутствует" --> </Grid>
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleImage.Data" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
</Grid> <!-- ParticleMaskImage -->
<Grid IsVisible="{Binding Record.ParticleMaskImageIsVisible}">
<!-- ParticleMaskImage --> <!-- Изображение -->
<Grid IsVisible="{Binding Record.ParticleMaskImageIsVisible}"> <core:ZoomPanImage
Background="Black"
CanUserZoomPan="False"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.MaskImage.Image}"
PanX="{Binding Record.ParticleImagesPanX}"
PanY="{Binding Record.ParticleImagesPanY}"
VerticalAlignment="Stretch"
Zoom="{Binding Record.ParticleImagesZoom}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.MaskImage.Data" />
HorizontalAlignment="Stretch" </MultiBinding>
ImageSource="{Binding Record.ParticleMaskImage.Image}" </core:ZoomPanImage.IsVisible>
VerticalAlignment="Stretch"
>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleMaskImage.Data" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей "Изображение отсутствует" --> </Grid>
<TextBlock
HorizontalAlignment="Center"
Text="Изображение отсутствует"
VerticalAlignment="Center"
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleMaskImage.Data" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
</Grid> <!-- ParticleResultImage -->
<Grid IsVisible="{Binding Record.ParticleResultImageIsVisible}">
<!-- ParticleResultImage --> <!-- Изображение -->
<Grid IsVisible="{Binding Record.ParticleResultImageIsVisible}"> <core:ZoomPanImage
Background="Black"
CanUserZoomPan="False"
HorizontalAlignment="Stretch"
ImageSource="{Binding Record.ResultImage.Image}"
PanX="{Binding Record.ParticleImagesPanX}"
PanY="{Binding Record.ParticleImagesPanY}"
VerticalAlignment="Stretch"
Zoom="{Binding Record.ParticleImagesZoom}"
>
<!-- Изображение --> <core:ZoomPanImage.IsVisible>
<core:ZoomPanImage <MultiBinding Converter="{x:Static BoolConverters.And}">
Background="Black" <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ResultImage.Data" />
HorizontalAlignment="Stretch" </MultiBinding>
ImageSource="{Binding Record.ParticleResultImage.Image}" </core:ZoomPanImage.IsVisible>
VerticalAlignment="Stretch"
>
<core:ZoomPanImage.IsVisible> </core:ZoomPanImage>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleResultImage.Data" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
</core:ZoomPanImage> <!-- Оверлей "Изображение отсутствует" -->
<TextBlock
<!-- Оверлей "Изображение отсутствует" --> HorizontalAlignment="Center"
<TextBlock Text="Изображение отсутствует"
HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Изображение отсутствует" >
VerticalAlignment="Center" <TextBlock.IsVisible>
> <MultiBinding Converter="{x:Static BoolConverters.And}">
<TextBlock.IsVisible> <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
<MultiBinding Converter="{x:Static BoolConverters.And}"> </MultiBinding>
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleResultImage.Data" /> </TextBlock.IsVisible>
</MultiBinding> </TextBlock>
</TextBlock.IsVisible>
</TextBlock>
</Grid>
</Grid> </Grid>
@@ -1309,17 +1290,6 @@
</Grid> </Grid>
<lvc:CartesianChart
Grid.Row="1"
HorizontalAlignment="Stretch"
IsVisible="{Binding Path=Record.ParticlesCount, Converter={StaticResource GreaterThanConverter}, ConverterParameter=0}"
Series="{Binding Record.ParticleValuesHistogram.Series, Mode=OneWay}"
VerticalAlignment="Stretch"
x:Name="ParticleValuesHistogram"
XAxes="{Binding Record.ParticleValuesHistogram.XAxis, Mode=OneWay}"
YAxes="{Binding Record.ParticleValuesHistogram.YAxis, Mode=OneWay}"
/>
</Grid> </Grid>
</Grid> </Grid>
@@ -17,11 +17,9 @@ public partial class AnalysisView : UserControl
private void OnChartsUpdate(object? sender, EventArgs ea) private void OnChartsUpdate(object? sender, EventArgs ea)
{ {
ProcessedAreasHistogram.CoreChart.Update(); ProcessedAreasHistogram.CoreChart.Update();
UnprocessedAreasHistogram.CoreChart.Update();
HotspotAreasHistogram.CoreChart.Update(); HotspotAreasHistogram.CoreChart.Update();
OpticalIntegralsHistogram.CoreChart.Update(); OpticalIntegralsHistogram.CoreChart.Update();
InnerStdDevsHistogram.CoreChart.Update(); InnerStdDevsHistogram.CoreChart.Update();
InnerHeterogeneitiesHistogram.CoreChart.Update(); InnerHeterogeneitiesHistogram.CoreChart.Update();
ParticleValuesHistogram.CoreChart.Update();
} }
} }