Files
GSS2Rework/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs
T

789 lines
39 KiB
C#

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;
namespace GSS2.ViewModels.AmineContent;
public partial class AnalysisViewModel : ViewModelBase
{
public event EventHandler? UpdateCharts;
private readonly ILogger<AnalysisViewModel> _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 CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
[ObservableProperty] public partial ObservableCollection<ResultRecord> 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()];
public AnalysisViewModel(
ILogger<AnalysisViewModel> logger,
ResultsContext context,
ImageCapturerService imageCapturer,
ImageStorageService imageStorage,
AnalyzerService analyzer,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
)
{
_logger = logger;
_context = context;
_imageCapturer = imageCapturer;
_imageStorage = imageStorage;
_analyzer = analyzer;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация");
// Получаем записи из базы данных
Records = new ObservableCollection<ResultRecord>(_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 = []
};
_logger.LogInformation("Инициализировано");
}
/// <summary>
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
/// </summary>
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 = []
};
}
else
{
// Иначе копируем запись, отвязывая её от базы данных
EditingRecord = CopyRecord(SelectedRecord);
}
}
/// <summary>
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
/// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
/// </summary>
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: "Дисперсия"
);
}
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;
// Если записи нет в базе данных
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<Task<ImageData>>();
// Обновляем данные в интерфейсе
// 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<double>();
var variances = new List<double>();
// Создаём временную маску для каждого контура
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));
}
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();
// Обновляем данные в редактируемой записи
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord));
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение во время анализа");
}
}
else
{
_logger.LogError("Ошибка при выполнении анализа");
}
}, 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;
}
/// <summary>
/// Копирует <see cref="ResultRecord"/>, отвязывая её от базы данных
/// </summary>
/// <returns>Скопированная <see cref="ResultRecord"/></returns>
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
};
}
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) EmptyHistogram()
{
return (Array.Empty<ISeries>(), [new Axis()], [new Axis()]);
}
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable<float> 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<int>
{
Values = binsPDF,
Name = barLegend,
Fill = new SolidColorPaint(SKColors.White),
ScalesYAt = 0
},
new LineSeries<float>
{
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<double> 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);
}