using System.Windows.Media; using FellowOakDicom.Imaging; using System.Windows.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using FellowOakDicom; using Livia.Utility; using Microsoft.Extensions.Logging; using Livia.Utility.DependencyInjection; using Microsoft.Extensions.DependencyInjection; namespace Livia.Models; public interface IImageSeries : IDisposable { ImageSource? CurrentBitmapImage { get; } double WindowCenterModifier { get; set; } double WindowWidthModifier { get; set; } bool WindowCenterModifiable { get; } bool IsMask { set; } bool ShowBoundary { get; set; } //An index of the series where you can find the mask, used to locate ROI int? MaskJumpToIndex { get; } int Count { get; } public void SetImageIndex(int n); Task LoadData(string loadPath); } internal class PlainImageSeries : ObservableObject, IImageSeries { public ImageSource? CurrentBitmapImage { get => _currentBitmapImage; private set => SetProperty(ref _currentBitmapImage, value); } public double WindowCenterModifier { get; set; } public double WindowWidthModifier { get; set; } public bool WindowCenterModifiable => false; public bool IsMask { set => throw new NotImplementedException(); } public bool ShowBoundary { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public int? MaskJumpToIndex => -1; public int Count => _bitmapImages.Count; private readonly List _bitmapImages = []; private ImageSource? _currentBitmapImage; private BitmapImage? GetImage(int n) { if (n < 0 || n >= Count) return null; return _bitmapImages[n]; } public void SetImageIndex(int n) { CurrentBitmapImage = GetImage(n); } public async Task LoadData(string loadPath) { _bitmapImages.Clear(); foreach (string file in LiviaUtility.ReadFilesFromDir(loadPath)) { _bitmapImages.Add(await LiviaUtility.LoadBitmapAsync(file)); } } public void Dispose() { CurrentBitmapImage = null; _bitmapImages.Clear(); } } internal class Dicom2DImageSeries : ObservableObject, IImageSeries { public ImageSource? CurrentBitmapImage { get => _currentBitmapImage; private set => SetProperty(ref _currentBitmapImage, value); } public double WindowCenterModifier { get; set; } = 1; public double WindowWidthModifier { get; set; } = 1; public bool WindowCenterModifiable => Count > 0 && _defaultWindowCenter > 0 && _defaultWindowWidth > 0; public bool IsMask { set => throw new NotImplementedException(); } public bool ShowBoundary { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public int? MaskJumpToIndex => -1; public int Count => _dicomImages.Count; private readonly List _dicomImages = []; private double _defaultWindowCenter; private double _defaultWindowWidth; private readonly SemaphoreSlim _lock = new(1, 1); private string _loadedPath = string.Empty; private readonly ILogger _logger = ActivatorUtilities.GetServiceOrCreateInstance(ServiceProviderFactory.ServiceProvider); private ImageSource? _currentBitmapImage; private BitmapImage? GetImage(int n) { if (n < 0 || n >= Count) return null; DicomImage dicomImage = _dicomImages[n]; dicomImage.WindowCenter = _defaultWindowCenter * WindowCenterModifier; dicomImage.WindowWidth = Math.Max(_defaultWindowWidth * WindowWidthModifier, 1); BitmapImage bitmapImage = dicomImage.RenderImage().AsClonedBitmap().ToBitmapImage(); bitmapImage.Freeze(); return bitmapImage; } public void SetImageIndex(int n) { CurrentBitmapImage = GetImage(n); } public async Task LoadData(string loadPath) { await _lock.WaitAsync(); try { if (loadPath == _loadedPath) { _logger.LogInformation("{path} is already loaded, move on", loadPath); return; } _dicomImages.Clear(); double min = double.MaxValue; double max = double.MinValue; foreach (string file in LiviaUtility.ReadFilesFromDir(loadPath)) { DicomFile dicomFile = await DicomFile.OpenAsync(file); _dicomImages.Add(new DicomImage(dicomFile.Dataset)); int windowCenter = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, 0); double windowWidthHalf = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, 0) / 2.0; min = Math.Min(min, windowCenter - windowWidthHalf); max = Math.Max(max, windowCenter + windowWidthHalf); } _defaultWindowCenter = (max + min) / 2; _defaultWindowWidth = max - min; _loadedPath = loadPath; } finally { _lock.Release(); } } public void Dispose() { CurrentBitmapImage = null; _dicomImages.Clear(); } } internal class Dicom3DImageSeries : ObservableObject, IImageSeries { public ImageSource? CurrentBitmapImage { get => _currentBitmapImage; private set => SetProperty(ref _currentBitmapImage, value); } public double WindowCenterModifier { get; set; } = 1; public double WindowWidthModifier { get; set; } = 1; public bool WindowCenterModifiable => Count > 0 && _defaultWindowCenter > 0 && _defaultWindowWidth > 0; public bool IsMask { get; set; } public bool ShowBoundary { get => _showBoundary; set { _showBoundary = value; //reload stuff SetImageIndex(_currentImageIndex); } } public int? MaskJumpToIndex { get => _maskJumpToIndex; private set => SetProperty(ref _maskJumpToIndex, value); } public int Count => _dicomImage?.NumberOfFrames ?? 0; private DicomImage? _dicomImage; private ImageSource? _currentBitmapImage; private int? _maskJumpToIndex; private bool _showBoundary = true; private int _currentImageIndex; private int _defaultWindowCenter; private int _defaultWindowWidth; public void SetImageIndex(int n) { if (_dicomImage == null || n < 0 || n >= Count) return; _currentImageIndex = n; if (_defaultWindowCenter != 0 && _defaultWindowWidth != 0) { _dicomImage.WindowCenter = _defaultWindowCenter * WindowCenterModifier; _dicomImage.WindowWidth = Math.Max(_defaultWindowWidth * WindowWidthModifier, 1); } BitmapImage image = _dicomImage.RenderImage(n).AsClonedBitmap().ToBitmapImage(); image.Freeze(); if (!IsMask) { CurrentBitmapImage = image; return; } BitmapSource transparencyImage = LiviaUtility.CreateTransparency(image, ShowBoundary); CurrentBitmapImage = transparencyImage; } public async Task LoadData(string loadPath) { DicomFile dcmFile = await DicomFile.OpenAsync(loadPath); _dicomImage = new DicomImage(dcmFile.Dataset); _defaultWindowCenter = dcmFile.Dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, null) ?? 0; _defaultWindowWidth = dcmFile.Dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, null) ?? 0; if (!IsMask) return; //just run it. Do not care how long it takes, as long as it is not blocking ui thread. _ = Task.Run(UpdateMaskJumpToIndex); } private void UpdateMaskJumpToIndex() { if (_dicomImage == null) return; //find am index with max pixel count. When locate this roi, jump here. int[] result = new int[Count]; Parallel.For(0, Count, i => { BitmapImage image = _dicomImage.RenderImage(i).AsClonedBitmap().ToBitmapImage(); image.Freeze(); int bytesPerPixel = (image.Format.BitsPerPixel + 7) / 8; int stride = bytesPerPixel * image.PixelWidth; byte[] buffer = new byte[stride * image.PixelHeight]; int alphaCount = buffer.Length / 4; image.CopyPixels(buffer, stride, 0); int count = buffer.Count(b => b > 0) - alphaCount; result[i] = count; }); int maxCount = result.Max(); MaskJumpToIndex = maxCount == 0 ? null : Array.IndexOf(result, maxCount); } public void Dispose() { CurrentBitmapImage = null; _dicomImage = null; } }