forked from amkovkov/GranuSightSoftware2
feat: testing and fixes
This commit is contained in:
@@ -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)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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, то ничего не делаем
|
||||||
|
|||||||
@@ -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,9 +1011,20 @@ 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.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
|
||||||
|
// Сохраняем несуществующий индекс
|
||||||
|
var nonExistingIndex = MenuSelectedIndex;
|
||||||
|
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
|
||||||
|
MenuSelectedIndex = -1;
|
||||||
|
// Удаляем несуществующий элемент из списка
|
||||||
|
MenuItems.RemoveAt(nonExistingIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogTrace("Выбран элемент из списка: {}", value);
|
_logger.LogTrace("Выбран элемент из списка: {}", value);
|
||||||
// Находим запись в базе данных
|
// Находим запись в базе данных
|
||||||
var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id);
|
var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id);
|
||||||
@@ -995,7 +1071,7 @@ public partial class AnalysisViewModel : ViewModelBase
|
|||||||
if (resultImageRecord is null)
|
if (resultImageRecord is null)
|
||||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords));
|
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords));
|
||||||
// В любом случае заполняем изображение
|
// В любом случае заполняем изображение
|
||||||
Record.MaskImage.Data = resultImageRecord?.Base64 ?? "";
|
Record.ResultImage.Data = resultImageRecord?.Base64 ?? "";
|
||||||
|
|
||||||
// Сбрасываем приближение изображений
|
// Сбрасываем приближение изображений
|
||||||
ImagesPanX = 0;
|
ImagesPanX = 0;
|
||||||
@@ -1009,19 +1085,6 @@ public partial class AnalysisViewModel : ViewModelBase
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Если значение не найдено в базе данных
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Логгируем как ошибку
|
|
||||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
|
|
||||||
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
|
|
||||||
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
|
|
||||||
MenuSelectedIndex = -1;
|
|
||||||
// Удаляем несуществующий элемент из списка
|
|
||||||
MenuItems.RemoveAt(nonExistingIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если элемент не выбран, заполняем поля пустыми значениями
|
// Если элемент не выбран, заполняем поля пустыми значениями
|
||||||
_logger.LogTrace("Выбран новый элемент");
|
_logger.LogTrace("Выбран новый элемент");
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (подпись) -->
|
||||||
@@ -368,19 +368,19 @@
|
|||||||
<!-- Изображения -->
|
<!-- Изображения -->
|
||||||
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
|
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
|
||||||
|
|
||||||
<!-- Изображения -->
|
|
||||||
<Grid>
|
|
||||||
|
|
||||||
<!-- Изображения -->
|
<!-- Изображения -->
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<Grid IsVisible="{Binding ImageIsVisible}">
|
<Grid IsVisible="{Binding ImageIsVisible}" IsEnabled="{Binding ImageIsVisible}">
|
||||||
|
|
||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.Image.Image}"
|
ImageSource="{Binding Record.Image.Image}"
|
||||||
|
PanX="{Binding ImagesPanX, Mode=TwoWay}"
|
||||||
|
PanY="{Binding ImagesPanY, Mode=TwoWay}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
@@ -447,14 +447,17 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- MaskImage -->
|
<!-- MaskImage -->
|
||||||
<Grid IsVisible="{Binding MaskImageIsVisible}">
|
<Grid IsVisible="{Binding MaskImageIsVisible}" IsEnabled="{Binding MaskImageIsVisible}">
|
||||||
|
|
||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.MaskImage.Image}"
|
ImageSource="{Binding Record.MaskImage.Image}"
|
||||||
|
PanX="{Binding ImagesPanX, Mode=TwoWay}"
|
||||||
|
PanY="{Binding ImagesPanY, Mode=TwoWay}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
@@ -521,14 +524,17 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- ResultImage -->
|
<!-- ResultImage -->
|
||||||
<Grid IsVisible="{Binding ResultImageIsVisible}">
|
<Grid IsVisible="{Binding ResultImageIsVisible}" IsEnabled="{Binding ResultImageIsVisible}">
|
||||||
|
|
||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.ResultImage.Image}"
|
ImageSource="{Binding Record.ResultImage.Image}"
|
||||||
|
PanX="{Binding ImagesPanX, Mode=TwoWay}"
|
||||||
|
PanY="{Binding ImagesPanY, Mode=TwoWay}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
@@ -598,8 +604,6 @@
|
|||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</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,17 +1113,8 @@
|
|||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="1">
|
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="*" /> <!-- Изображения -->
|
|
||||||
<RowDefinition Height="*" /> <!-- Гистограмма -->
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
|
||||||
|
|
||||||
<!-- Изображения -->
|
<!-- Изображения -->
|
||||||
<Grid Grid.Row="0">
|
<Grid Grid.Column="1">
|
||||||
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/> <!-- Изображения -->
|
<ColumnDefinition Width="*"/> <!-- Изображения -->
|
||||||
@@ -1198,7 +1169,7 @@
|
|||||||
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
|
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
|
||||||
|
|
||||||
<!-- Изображения -->
|
<!-- Изображения -->
|
||||||
<Grid>
|
<Grid x:Name="PraticleImagesGrid" Bounds="{Binding Record.ParticleImagesBounds, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
|
||||||
<!-- Изображения -->
|
<!-- Изображения -->
|
||||||
<!-- ParticleImage -->
|
<!-- ParticleImage -->
|
||||||
@@ -1207,14 +1178,18 @@
|
|||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
|
CanUserZoomPan="False"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.ParticleImage.Image}"
|
ImageSource="{Binding Record.Image.Image}"
|
||||||
|
PanX="{Binding Record.ParticleImagesPanX}"
|
||||||
|
PanY="{Binding Record.ParticleImagesPanY}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding Record.ParticleImagesZoom}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</core:ZoomPanImage.IsVisible>
|
</core:ZoomPanImage.IsVisible>
|
||||||
|
|
||||||
@@ -1228,7 +1203,7 @@
|
|||||||
>
|
>
|
||||||
<TextBlock.IsVisible>
|
<TextBlock.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</TextBlock.IsVisible>
|
</TextBlock.IsVisible>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -1241,14 +1216,18 @@
|
|||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
|
CanUserZoomPan="False"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.ParticleMaskImage.Image}"
|
ImageSource="{Binding Record.MaskImage.Image}"
|
||||||
|
PanX="{Binding Record.ParticleImagesPanX}"
|
||||||
|
PanY="{Binding Record.ParticleImagesPanY}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding Record.ParticleImagesZoom}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleMaskImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.MaskImage.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</core:ZoomPanImage.IsVisible>
|
</core:ZoomPanImage.IsVisible>
|
||||||
|
|
||||||
@@ -1262,7 +1241,7 @@
|
|||||||
>
|
>
|
||||||
<TextBlock.IsVisible>
|
<TextBlock.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleMaskImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.MaskImage.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</TextBlock.IsVisible>
|
</TextBlock.IsVisible>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -1275,14 +1254,18 @@
|
|||||||
<!-- Изображение -->
|
<!-- Изображение -->
|
||||||
<core:ZoomPanImage
|
<core:ZoomPanImage
|
||||||
Background="Black"
|
Background="Black"
|
||||||
|
CanUserZoomPan="False"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ImageSource="{Binding Record.ParticleResultImage.Image}"
|
ImageSource="{Binding Record.ResultImage.Image}"
|
||||||
|
PanX="{Binding Record.ParticleImagesPanX}"
|
||||||
|
PanY="{Binding Record.ParticleImagesPanY}"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
|
Zoom="{Binding Record.ParticleImagesZoom}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<core:ZoomPanImage.IsVisible>
|
<core:ZoomPanImage.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ParticleResultImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.ResultImage.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</core:ZoomPanImage.IsVisible>
|
</core:ZoomPanImage.IsVisible>
|
||||||
|
|
||||||
@@ -1296,7 +1279,7 @@
|
|||||||
>
|
>
|
||||||
<TextBlock.IsVisible>
|
<TextBlock.IsVisible>
|
||||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ParticleResultImage.Data" />
|
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.ResultImage.Data" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</TextBlock.IsVisible>
|
</TextBlock.IsVisible>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -1309,19 +1292,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>
|
||||||
|
|
||||||
<!-- Оверлей "Нет информации" -->
|
<!-- Оверлей "Нет информации" -->
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user