using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using GSS2.Core; using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent.Database.Results; using GSS2.Core.Extensions; using GSS2.UI.Core.ViewModels; using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp; using LiveChartsCore.SkiaSharpView.Painting.Effects; using GSS2.UI.Core.Services; using GSS2.Core.Hardware; namespace GSS2.ViewModels.AmineContent; public partial class AnalysisViewModel : ViewModelBase { const double QUALITY_MIN_MEAN = 2.0; const double QUALITY_ALPHA = 0.7; const double QUALITY_BETA = 0.3; public event EventHandler? UpdateCharts; private readonly ILogger _logger; private readonly ResultsContext _context; private readonly ImageCapturerService _imageCapturer; private readonly ImageStorageService _imageStorage; private readonly AnalyzerService _analyzer; private readonly DependencyInjectionViewLocator _viewLocator; private readonly NavigationService _navigation; private readonly LightsService _lights; private CancellationTokenSource? _captureImagesTaskCts = null; [ObservableProperty] public partial bool MenuOpened { get; set; } = false; [ObservableProperty] public partial ObservableCollection Records { get; set; } [ObservableProperty] public partial ResultRecord? SelectedRecord { get; set; } [ObservableProperty] public partial ResultRecord EditingRecord { get; set; } [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true; [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true; [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true; [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true; [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null; [ObservableProperty] public partial AsyncImageRecordViewModel? MaskImage { get; set; } = null; [ObservableProperty] public partial AsyncImageRecordViewModel? ResultImage { get; set; } = null; [ObservableProperty] public partial bool ImageIsVisible { get; set; } = true; [ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false; [ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false; [ObservableProperty] public partial float ImagesZoom { get; set; } = 1; [ObservableProperty] public partial float ImagesPanX { get; set; } = 0; [ObservableProperty] public partial float ImagesPanY { get; set; } = 0; [ObservableProperty] public partial SolidColorPaint LegendTextPaint { get; set; } = new SolidColorPaint(SKColors.White); [ObservableProperty] public partial ISeries[] MeanValuesHistogram { get; set; } = []; [ObservableProperty] public partial Axis[] MeanValuesXAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial Axis[] MeanValuesYAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial ISeries[] VariancesHistogram { get; set; } = []; [ObservableProperty] public partial Axis[] VariancesXAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial Axis[] VariancesYAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial ISeries[] QualitiesHistogram { get; set; } = []; [ObservableProperty] public partial Axis[] QualitiesXAxis { get; set; } = [new Axis()]; [ObservableProperty] public partial Axis[] QualitiesYAxis { get; set; } = [new Axis()]; public AnalysisViewModel( ILogger logger, ResultsContext context, ImageCapturerService imageCapturer, ImageStorageService imageStorage, AnalyzerService analyzer, DependencyInjectionViewLocator viewLocator, NavigationService navigation, LightsService lights ) { _logger = logger; _context = context; _imageCapturer = imageCapturer; _imageStorage = imageStorage; _analyzer = analyzer; _viewLocator = viewLocator; _navigation = navigation; _lights = lights; _logger.LogInformation("Инициализация"); // Получаем записи из базы данных Records = new ObservableCollection(_context.ResultRecords); // Создаём пустые записи SelectedRecord = null; // На самом деле заполнять EditingRecord излишне, потому что при изменении SelectedRecord // вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений EditingRecord = new() { SampleName = "", SampleComment = "", SamplingDateTime = DateTime.UtcNow, SamplingPlace = "", Image = "", SeparatorType = "", SampleBrand = "", MaskImage = "", ResultImage = "", MinValue = -1, MaxValue = -1, ParticlesCount = -1, MeanValue = -1, MeanVariance = -1, VarianceOfMean = -1, MeanValues = [], Variances = [], Qualities = [], MeanQuality = -1 }; _logger.LogInformation("Инициализировано"); } /// /// Вызывается при изменении .
/// Копирует в или издаёт пустую . ///
partial void OnSelectedRecordChanged(ResultRecord? value) { // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) // то создаём пустую запись if (SelectedRecord is null) { EditingRecord = new() { SampleName = "", SampleComment = "", SamplingDateTime = DateTime.UtcNow, SamplingPlace = "", Image = "", SeparatorType = "", SampleBrand = "", MaskImage = "", ResultImage = "", MinValue = -1, MaxValue = -1, ParticlesCount = -1, MeanValue = -1, MeanVariance = -1, VarianceOfMean = -1, MeanValues = [], Variances = [], Qualities = [], MeanQuality = -1 }; } else { // Иначе копируем запись, отвязывая её от базы данных EditingRecord = CopyRecord(SelectedRecord); } } /// /// Вызывается при изменении .
/// Изменяет изображением из . ///
partial void OnEditingRecordChanged(ResultRecord value) { var avgImagePath = _imageStorage.GetFullPath(value.Image, "result-avg"); if (File.Exists(avgImagePath)) { if (Image is null) Image = new AsyncImageRecordViewModel(avgImagePath); else Image.Path = avgImagePath; } else Image = null; var maskImagePath = _imageStorage.GetFullPath(value.MaskImage, "result-mask"); if (File.Exists(maskImagePath)) { if (MaskImage is null) MaskImage = new AsyncImageRecordViewModel(maskImagePath); else MaskImage.Path = maskImagePath; } else MaskImage = null; var resultImagePath = _imageStorage.GetFullPath(value.ResultImage, "result"); if (File.Exists(resultImagePath)) { if (ResultImage is null) ResultImage = new AsyncImageRecordViewModel(resultImagePath); else ResultImage.Path = resultImagePath; } else ResultImage = null; try { (MeanValuesHistogram, MeanValuesXAxis, MeanValuesYAxis) = BuildHistogram( value.MeanValues, 0, Math.Max(value.MeanValues.Count() > 0 ? value.MeanValues.Max() : 0, 1), leftText: "Количество частиц", bottomText: "Среднее знечение" ); (VariancesHistogram, VariancesXAxis, VariancesYAxis) = BuildHistogram( value.Variances, 0, Math.Max(value.Variances.Count() > 0 ? value.Variances.Max() : 0, 1), leftText: "Количество частиц", bottomText: "Дисперсия" ); (QualitiesHistogram, QualitiesXAxis, QualitiesYAxis) = BuildHistogram( value.Qualities ?? [], 0, 100, leftText: "Количество частиц", bottomText: "Качество обработки" ); } catch (Exception ex) { _logger.LogError(ex, ""); } } // Просто команды управления меню [RelayCommand] private void OpenMenu() => MenuOpened = true; [RelayCommand] private void CloseMenu() => MenuOpened = false; [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; // Команда создания новой записи // вызывает цепочку действий по изменению EditingRecord и Images [RelayCommand] private void CreateRecord() { // Если уже создана новая запись, то SelectedRecord == null и // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged if (SelectedRecord is not null) SelectedRecord = null; else OnSelectedRecordChanged(null); } // Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных [RelayCommand] private void DeleteRecord() { var confirmViewModel = new DeleteConfirmViewModel(_navigation) { Question = "Вы уверены, что хотиите удаить запись?" }; if (SelectedRecord is null) { CreateRecord(); return; } confirmViewModel.Closes += (_, ea) => { if (ea != true) return; // Сохраняем текущую запись var deletedRecord = SelectedRecord; // Создаём новую запись CreateRecord(); // Если запись на была сохранена в базу данных, то ничего не делаем if (deletedRecord is null) return; // Если запись была в без данных, то // она была и в Records, удаляем её из Records Records.Remove(deletedRecord); // Удаляем запись из базы данных и сохраняем изменения // TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления, // но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, // тогда удалённые записи будут заполнять диск // TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища _context.ResultRecords.Remove(deletedRecord); _context.SaveChanges(); }; var convfirmView = _viewLocator.Build(confirmViewModel); if (convfirmView is not null) _navigation.NavigateTo(convfirmView); } // // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной [RelayCommand] private void SaveRecord() { // Если это новая запись if (SelectedRecord is null) { // Если это новая запись, то копируем её var newRecord = CopyRecord(EditingRecord); // Если записи нет в базе данных if (_context.Entry(newRecord).State is EntityState.Detached) { // То добавляем её в базу var attachedRecord = _context.ResultRecords.Add(newRecord); // Заменяем текущую запись на привязанную к базе данных newRecord = attachedRecord.Entity; } // Сохраняем изменения в базе данных _context.SaveChanges(); // Добавляем запись в список Records.Add(newRecord); // Выбираем запись SelectedRecord = newRecord; } // Если эта запись есть в базе else { // Переносим данные из редактируемой записи в выбранную SelectedRecord.SampleName = EditingRecord.SampleName; SelectedRecord.SampleComment = EditingRecord.SampleComment; SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime; SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace; SelectedRecord.Image = EditingRecord.Image; SelectedRecord.SeparatorType = EditingRecord.SeparatorType; SelectedRecord.SampleBrand = EditingRecord.SampleBrand; SelectedRecord.MaskImage = EditingRecord.MaskImage; SelectedRecord.ResultImage = EditingRecord.ResultImage; SelectedRecord.MeanValue = EditingRecord.MeanValue; SelectedRecord.MeanVariance = EditingRecord.MeanVariance; SelectedRecord.VarianceOfMean = EditingRecord.VarianceOfMean; SelectedRecord.Qualities = EditingRecord.Qualities; SelectedRecord.MeanQuality = EditingRecord.MeanQuality; // Если записи нет в базе данных if (_context.Entry(SelectedRecord).State is EntityState.Detached) { // То добавляем её в базу var attachedRecord = _context.ResultRecords.Add(SelectedRecord); // Находим текущую запись в списке var index = Records.IndexOf(SelectedRecord); // Заменяем её в списке на привязанную к базе данных Records[index] = attachedRecord.Entity; // Заменяем выбранную запись на привязанную SelectedRecord = attachedRecord.Entity; } // Сохраняем изменения в базе данных _context.SaveChanges(); } } // Команда изменения основной секции (информация <-> изображения) [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; // Команда навигации по информации [RelayCommand] private void InfoSectionPrevious() { InfoSectionCurrentIndex--; UpdateCharts?.Invoke(this, new EventArgs()); } [RelayCommand] private void InfoSectionNext() { InfoSectionCurrentIndex++; UpdateCharts?.Invoke(this, new EventArgs()); } // Команда съёмки изображений [RelayCommand] private void CaptureImage() { // Если съёмка уже начата, то ничего не делаем if (CaptureImagesTask is not null) return; // Создаём токен отмены задачи _captureImagesTaskCts = new CancellationTokenSource(); var cancellationToken = _captureImagesTaskCts.Token; // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}"); // Запускаем получение изображений асинхронно CaptureImagesTask = Task.Run(async () => { // Список полученных изображений var imagesData = new List>(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); // Получаем изображения var captureTask = _imageCapturer.CaptureImages(async (data) => { // Распаковываем данные var (i, visible, uv365, uv254, image) = data; // Выбрасываем исключение если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); var imagesDataTask = Task.Run(() => { // Сохраняем png изображение во временную папку var path = Path.Join(Path.GetTempPath(), $"gss2-result-{visible}-{uv365}-{uv254}.png"); image.ImWrite(path); // Освобождаем память изображения image.Release(); // Добавляем данные изображения в список return new ImageData(path, visible, uv365, uv254); }); imagesData.Add(imagesDataTask); }, cancellationToken); // Обрабатываем изображения var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => { // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); // Проверяем исключения задачи получения изображений if (t.Exception is not null) { _logger.LogError(t.Exception, "Исключение во время получения изображений"); foreach (var ex in t.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время получения изображений"); return; } // Проверяем исключения задач сохранения изображений foreach (var imageData in imagesData) if (imageData.Exception is not null) { _logger.LogError(imageData.Exception, "Исключение во время получения изображения"); foreach (var ex in imageData.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время получения изображения"); return; } // Инициализируем анализатор if (!_analyzer.Initialized) { _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей"); await _analyzer.Initialize(); } // Инициализируем анализатор if (!_analyzer.Initialized) { _logger.LogError("Ошибка при инициализации анализатора"); return; } // Обновляем данные в редактируемой записи // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Анализ"); // Проводим анализ var result = await _analyzer.Analyze(imagesData.Select(i => i.Result)); // Если анализ проведён if (result is not null) { _logger.LogInformation("Анализ завершён"); // Распаковываем результат var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value; try { _logger.LogInformation("Анализ завершён [2]"); // Вычисляем минимальное и максимальное значение OpenCvSharp.Cv2.MinMaxLoc(resultImage, out double minValue, out double maxValue, out _, out _, mask); // Вычисляем среднее значение var meanValue = OpenCvSharp.Cv2.Mean(resultImage, mask).Val0; _logger.LogInformation("Миниальное значение: {}, Максимальное значние: {}", minValue, maxValue); _logger.LogInformation("Колличество конутров: {}, Среднее значние: {}", contours.Count(), meanValue); _logger.LogInformation("Анализ завершён [3]"); // Вычисляем среднее и дисперсию каждого контура var means = new List(); var variances = new List(); var qualities = new List(); // Создаём временную маску для каждого контура var contourMask = new OpenCvSharp.Mat( new OpenCvSharp.Size( AnalyzerService.ANALYSIS_IMAGE_SIZE, AnalyzerService.ANALYSIS_IMAGE_SIZE ), OpenCvSharp.MatType.CV_8UC1, new OpenCvSharp.Scalar(0) ); // Проходим по каждому контуру foreach (var contour in contours) { // Обнуляем маску contourMask.SetTo(new OpenCvSharp.Scalar(0)); // Заполняем контур на маске OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1); // Вычисляем среднее и среднеквадратическое отклонение OpenCvSharp.Cv2.MeanStdDev(resultImage, out var mean, out var stdDev, contourMask); // Добавляем значения в лист means.Add(mean.Val0); variances.Add(Math.Pow(stdDev.Val0, 2)); qualities.Add(CalculateQuality(mean.Val0, Math.Pow(stdDev.Val0, 2))); } contourMask.Release(); _logger.LogInformation("Анализ завершён [4]"); // Вычисляем среднюю дисперсию и дисперсию среднего double meanVariance = variances.Average(); double varianceOfMean = means.Variance(); _logger.LogInformation("Средняя дивергенция: {}, Дивергенция среднего: {}", meanVariance, varianceOfMean); _logger.LogInformation("Анализ завершён [5]"); // Кодируем и сохраняем среднее изображение OpenCvSharp.Cv2.DrawContours(avgImage, contours, -1, OpenCvSharp.Scalar.Red, 1, OpenCvSharp.LineTypes.Link8); OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); string avgPath; using (var stream = new MemoryStream(avgData)) avgPath = await _imageStorage.SaveAsync(stream, ".png", "result-avg"); // Освобождаем память среднего изображения avgImage.Release(); avgData = null; _logger.LogInformation("Анализ завершён [6]"); // Кодируем и сохраняем маску OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData); string maskPath; using (var stream = new MemoryStream(maskData)) maskPath = await _imageStorage.SaveAsync(stream, ".png", "result-mask"); // Освобождаем память маски mask.Release(); maskData = null; _logger.LogInformation("Анализ завершён [7]"); // Преобразуем изображение (0...maxValue) -> (0...255) resultImage.ConvertTo(resultImage, OpenCvSharp.MatType.CV_8UC1, 255 / (maxValue - 0), 255 * 0 / (maxValue - 0)); // Кодируем и сохраняем изображение результата OpenCvSharp.Cv2.ImEncode(".png", resultImage, out var resultData); string resultPath; using (var stream = new MemoryStream(resultData)) resultPath = await _imageStorage.SaveAsync(stream, ".png", "result"); // Освобождаем память изображения результата resultImage.Release(); resultData = null; _logger.LogInformation("Анализ завершён [8]"); // Заполняем поля в редактируемой записи EditingRecord.Image = avgPath; EditingRecord.SeparatorType = separatorType; EditingRecord.SampleBrand = sampleBrand; EditingRecord.MaskImage = maskPath; EditingRecord.ResultImage = resultPath; EditingRecord.MinValue = minValue; EditingRecord.MaxValue = maxValue; EditingRecord.ParticlesCount = contours.Count(); EditingRecord.MeanValue = meanValue; EditingRecord.MeanVariance = meanVariance; EditingRecord.VarianceOfMean = varianceOfMean; EditingRecord.MeanValues = means.ToArray(); EditingRecord.Variances = variances.ToArray(); EditingRecord.MeanQuality = qualities.Average(); EditingRecord.Qualities = qualities.ToArray(); // Обновляем данные в редактируемой записи // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord)); // Если ошибок не было, то мигаем светодиодами зелёным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Green); await Task.Delay(3000); _lights.Disconnect(); }); } catch (Exception ex) { _logger.LogError(ex, "Исключение во время анализа"); // Если была ошибка, то мигаем светодиодами красным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Red); await Task.Delay(3000); _lights.Disconnect(); }); } } else { _logger.LogError("Ошибка при выполнении анализа"); // Если была ошибка, то мигаем светодиодами красным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Red); await Task.Delay(3000); _lights.Disconnect(); }); } }, cancellationToken); // Сбрасываем состояние задачи съёмки изображений _ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t => { // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { ImagesIsCapturing = false; ImagesCapturingProgress = ""; CaptureImagesTask = null; }); }); }); } // Команда отмены задачи съёмки изображений [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); // Команды переключения изображений [RelayCommand] private void SwitchToImage() { ImageIsVisible = true; MaskImageIsVisible = false; ResultImageIsVisible = false; } [RelayCommand] private void SwitchToMaskImage() { ImageIsVisible = false; MaskImageIsVisible = true; ResultImageIsVisible = false; } [RelayCommand] private void SwitchToResultImage() { ImageIsVisible = false; MaskImageIsVisible = false; ResultImageIsVisible = true; } /// /// Копирует , отвязывая её от базы данных /// /// Скопированная private static ResultRecord CopyRecord(ResultRecord source) { return new ResultRecord { Id = source.Id, SampleName = source.SampleName, SampleComment = source.SampleComment, SamplingDateTime = source.SamplingDateTime, SamplingPlace = source.SamplingPlace, Image = source.Image, SeparatorType = source.SeparatorType, SampleBrand = source.SampleBrand, MaskImage = source.MaskImage, ResultImage = source.ResultImage, MinValue = source.MinValue, MaxValue = source.MaxValue, ParticlesCount = source.ParticlesCount, MeanValue = source.MeanValue, MeanVariance = source.MeanVariance, VarianceOfMean = source.VarianceOfMean, MeanValues = source.MeanValues, Variances = source.Variances, Qualities = source.Qualities ?? Enumerable.Zip(source.MeanValues, source.Variances) .Select((pair) => CalculateQuality(pair.First, pair.Second)) .ToArray(), MeanQuality = source.MeanQuality ?? Enumerable.Zip(source.MeanValues, source.Variances) .Select((pair) => CalculateQuality(pair.First, pair.Second)) .Average(), }; } private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) EmptyHistogram() { return (Array.Empty(), [new Axis()], [new Axis()]); } private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") { if (values == null || values.Count() == 0) return EmptyHistogram(); var vals = values.Select(v => (double)v).ToArray(); double minVal = min ?? vals.Min(); double maxVal = max ?? vals.Max(); int binCount = 20; double binSize = (maxVal - minVal) / binCount; var binsPDF = new int[binCount]; var binsCDF = new float[binCount]; foreach (var v in vals) { int index = (int)((v - minVal) / binSize); if (index >= binCount) index = binCount - 1; if (index < 0) index = 0; binsPDF[index]++; for (int i = index; i < binCount; i++) binsCDF[i] += 100.0f / values.Count(); } // Подписи для оси X var labels = new string[binCount]; for (int i = 0; i < binCount; i++) { double from = minVal + i * binSize; double to = from + binSize; labels[i] = $"{from:F2}"; } return ( new ISeries[] { new ColumnSeries { Values = binsPDF, Name = barLegend, Fill = new SolidColorPaint(SKColors.White), ScalesYAt = 0 }, new LineSeries { Values = binsCDF, Name = lineLegend, Fill = new SolidColorPaint(SKColors.Transparent), Stroke = new SolidColorPaint(SKColors.Red), GeometryFill = new SolidColorPaint(SKColors.Red), GeometryStroke = new SolidColorPaint(SKColors.Red), GeometrySize = 8, ScalesYAt = 1 } }, new Axis[] { new Axis { Labels = labels, LabelsRotation = 45, TextSize = 14, Name = bottomText, NameTextSize = 14, NamePaint=new SolidColorPaint(SKColors.White), SeparatorsPaint = new SolidColorPaint(SKColors.White), LabelsPaint = new SolidColorPaint(SKColors.White), TicksPaint = new SolidColorPaint(SKColors.White), SubticksPaint = new SolidColorPaint(SKColors.White) } }, new Axis[] { new Axis { TextSize = 14, MinLimit = 0, Name=leftText, NameTextSize = 14, NamePaint=new SolidColorPaint(SKColors.White), Position = LiveChartsCore.Measure.AxisPosition.Start, SeparatorsPaint = new SolidColorPaint(SKColors.White), LabelsPaint = new SolidColorPaint(SKColors.White), TicksPaint = new SolidColorPaint(SKColors.White), SubticksPaint = new SolidColorPaint(SKColors.White) }, new Axis { TextSize = 14, MinLimit = 0f, MaxLimit = 100f, Name=rightText, NameTextSize = 14, NamePaint=new SolidColorPaint(SKColors.White), Position = LiveChartsCore.Measure.AxisPosition.End, SeparatorsPaint = new SolidColorPaint(SKColors.White) { PathEffect = new DashEffect([4, 4]) }, LabelsPaint = new SolidColorPaint(SKColors.White), TicksPaint = new SolidColorPaint(SKColors.White), SubticksPaint = new SolidColorPaint(SKColors.White) } } ); } private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") => BuildHistogram(values.Select(v => (float)v), min, max, leftText, rightText, bottomText, barLegend, lineLegend); private static double CalculateQuality(double mean, double variance) => 100.0 * Math.Exp(-1 * (QUALITY_ALPHA * Math.Max(0, QUALITY_MIN_MEAN - mean) + QUALITY_BETA * Math.Pow(variance, 2))); }