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