livia-test/Livia/Models/ImageSeries.cs
2025-03-28 14:31:53 +08:00

263 lines
8.6 KiB
C#

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<BitmapImage> _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<DicomImage> _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<ILogger>(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<int?>(DicomTag.WindowCenter, null) ?? 0;
_defaultWindowWidth = dcmFile.Dataset.GetSingleValueOrDefault<int?>(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;
}
}