From 54f6ff1754db4718be6cab99e3cc65b2a47901f3 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Fri, 23 Feb 2024 16:43:24 +0100 Subject: [PATCH] Use SimpleITK for image sequence datasets (#226) * Use SimpleITK's image sequence importer when available * SimpleITKDicomImporter * Use image file importer for tiff --- .../Editor/VolumeRendererEditorFunctions.cs | 57 ++++++ .../Interface/IImageFileImporter.cs | 3 +- .../SimpleITK/SimpleITKDICOMImporter.cs | 186 ++++++++++++++++++ .../SimpleITKImageSequenceImporter.cs | 64 +++--- Assets/Scripts/Importing/ImporterFactory.cs | 14 +- 5 files changed, 282 insertions(+), 42 deletions(-) create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKDICOMImporter.cs diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index 66168dbf..90396a3f 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -244,6 +244,58 @@ private static async void ImportNIFTIDatasetAsync(bool spawnInScene) } } + [MenuItem("Volume Rendering/Load dataset/Load image file")] + private static void ShowImageFileImporter() + { + ImporImageFileDatasetAsync(true); + } + + [MenuItem("Assets/Volume Rendering/Import dataset/Import image file")] + private static void ImportImageFileAsset() + { + ImporImageFileDatasetAsync(false); + } + + private static async void ImporImageFileDatasetAsync(bool spawnInScene) + { + string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", ""); + if (File.Exists(file)) + { + Debug.Log("Async dataset load. Hold on."); + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView(), "Image file import")) + { + progressHandler.ReportProgress(0.0f, "Importing image file dataset"); + + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.Unknown); + VolumeDataset dataset = await importer.ImportAsync(file); + + progressHandler.ReportProgress(0.0f, "Creating object"); + + if (dataset != null) + { + await OptionallyDownscale(dataset); + if (spawnInScene) + { + await VolumeObjectFactory.CreateObjectAsync(dataset); + } + else + { + ProjectWindowUtil.CreateAsset(dataset, $"{dataset.datasetName}.asset"); + AssetDatabase.SaveAssets(); + } + } + else + { + Debug.LogError("Failed to import datset"); + } + } + } + else + { + Debug.LogError("File doesn't exist: " + file); + } + } + [MenuItem("Volume Rendering/Load dataset/Load PARCHG dataset")] private static void ShowParDatasetImporter() { @@ -315,6 +367,11 @@ private static async void ImportSequenceAsync() IEnumerable seriesList = await importer.LoadSeriesAsync(filePaths); + if (seriesList.Count() == 0) + { + Debug.LogWarning("Found no series to import."); + } + foreach (IImageSequenceSeries series in seriesList) { VolumeDataset dataset = await importer.ImportSeriesAsync(series); diff --git a/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs index 27639513..62ce4dac 100644 --- a/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs +++ b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs @@ -7,7 +7,8 @@ public enum ImageFileFormat { VASP, NRRD, - NIFTI + NIFTI, + Unknown } /// diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKDICOMImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKDICOMImporter.cs new file mode 100644 index 00000000..94406c45 --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKDICOMImporter.cs @@ -0,0 +1,186 @@ +#if UVR_USE_SIMPLEITK +using UnityEngine; +using System; +using itk.simple; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace UnityVolumeRendering +{ + /// + /// SimpleITK-based DICOM importer. + /// Has support for JPEG2000 and more. + /// + public class SimpleITKDICOMImporter : IImageSequenceImporter + { + public class ImageSequenceSlice : IImageSequenceFile + { + public string filePath; + + public string GetFilePath() + { + return filePath; + } + } + + public class ImageSequenceSeries : IImageSequenceSeries + { + public List files = new List(); + + public IEnumerable GetFiles() + { + return files; + } + } + + public IEnumerable LoadSeries(IEnumerable files, ImageSequenceImportSettings settings) + { + List seriesList= LoadSeriesInternal(files); + + return seriesList; + } + + public async Task> LoadSeriesAsync(IEnumerable files, ImageSequenceImportSettings settings) + { + List seriesList = null; + await Task.Run(() => seriesList=LoadSeriesInternal(files)); + + return seriesList; + } + + private List LoadSeriesInternal(IEnumerable files) + { + HashSet directories = new HashSet(); + + foreach (string file in files) + { + string dir = Path.GetDirectoryName(file); + if (!directories.Contains(dir)) + directories.Add(dir); + } + + List seriesList = new List(); + Dictionary directorySeries = new Dictionary(); + foreach (string directory in directories) + { + VectorString seriesIDs = ImageSeriesReader.GetGDCMSeriesIDs(directory); + directorySeries.Add(directory, seriesIDs); + + } + + foreach (var dirSeries in directorySeries) + { + foreach (string seriesID in dirSeries.Value) + { + VectorString dicom_names = ImageSeriesReader.GetGDCMSeriesFileNames(dirSeries.Key, seriesID); + ImageSequenceSeries series = new ImageSequenceSeries(); + foreach (string file in dicom_names) + { + ImageSequenceSlice sliceFile = new ImageSequenceSlice(); + sliceFile.filePath = file; + series.files.Add(sliceFile); + } + seriesList.Add(series); + } + } + return seriesList; + } + + public VolumeDataset ImportSeries(IImageSequenceSeries series, ImageSequenceImportSettings settings) + { + Image image = null; + float[] pixelData = null; + VectorUInt32 size = null; + VectorString dicomNames = null; + + // Create dataset + VolumeDataset volumeDataset = ScriptableObject.CreateInstance(); + + ImageSequenceSeries sequenceSeries = (ImageSequenceSeries)series; + if (sequenceSeries.files.Count == 0) + { + Debug.LogError("Empty series. No files to load."); + return null; + } + + ImportSeriesInternal(dicomNames, sequenceSeries, image, size, pixelData, volumeDataset); + + return volumeDataset; + } + + public async Task ImportSeriesAsync(IImageSequenceSeries series, ImageSequenceImportSettings settings) + { + Image image = null; + float[] pixelData = null; + VectorUInt32 size = null; + VectorString dicomNames = null; + + // Create dataset + VolumeDataset volumeDataset = ScriptableObject.CreateInstance(); + + ImageSequenceSeries sequenceSeries = (ImageSequenceSeries)series; + if (sequenceSeries.files.Count == 0) + { + Debug.LogError("Empty series. No files to load."); + settings.progressHandler.Fail(); + return null; + } + + await Task.Run(() => ImportSeriesInternal(dicomNames, sequenceSeries, image, size, pixelData, volumeDataset)); + + return volumeDataset; + } + + private void ImportSeriesInternal(VectorString dicomNames, ImageSequenceSeries sequenceSeries, Image image, VectorUInt32 size, float[] pixelData, VolumeDataset volumeDataset) + { + ImageSeriesReader reader = new ImageSeriesReader(); + + dicomNames = new VectorString(); + + foreach (var dicomFile in sequenceSeries.files) + dicomNames.Add(dicomFile.filePath); + reader.SetFileNames(dicomNames); + + image = reader.Execute(); + + // Cast to 32-bit float + image = SimpleITK.Cast(image, PixelIDValueEnum.sitkFloat32); + + size = image.GetSize(); + + int numPixels = 1; + for (int dim = 0; dim < image.GetDimension(); dim++) + numPixels *= (int)size[dim]; + + // Read pixel data + pixelData = new float[numPixels]; + IntPtr imgBuffer = image.GetBufferAsFloat(); + Marshal.Copy(imgBuffer, pixelData, 0, numPixels); + + for (int i = 0; i < pixelData.Length; i++) + pixelData[i] = Mathf.Clamp(pixelData[i], -1024, 3071); + + VectorDouble spacing = image.GetSpacing(); + + volumeDataset.data = pixelData; + volumeDataset.dimX = (int)size[0]; + volumeDataset.dimY = (int)size[1]; + volumeDataset.dimZ = (int)size[2]; + volumeDataset.datasetName = Path.GetFileName(dicomNames[0]); + volumeDataset.filePath = dicomNames[0]; + volumeDataset.scale = new Vector3( + (float)(spacing[0] * size[0]) / 1000.0f, // mm to m + (float)(spacing[1] * size[1]) / 1000.0f, // mm to m + (float)(spacing[2] * size[2]) / 1000.0f // mm to m + ); + + // Convert from LPS to Unity's coordinate system + ImporterUtilsInternal.ConvertLPSToUnityCoordinateSpace(volumeDataset); + + volumeDataset.FixDimensions(); + } + } +} +#endif diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs index 29f323f5..4009777e 100644 --- a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs +++ b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs @@ -10,8 +10,8 @@ namespace UnityVolumeRendering { /// - /// SimpleITK-based DICOM importer. - /// Has support for JPEG2000 and more. + /// SimpleITK-based image sequence importer. + /// Has support for TIFF and more. /// public class SimpleITKImageSequenceImporter : IImageSequenceImporter { @@ -52,39 +52,20 @@ public async Task> LoadSeriesAsync(IEnumerable private List LoadSeriesInternal(IEnumerable files) { - HashSet directories = new HashSet(); + ImageSequenceSeries series = new ImageSequenceSeries(); foreach (string file in files) { - string dir = Path.GetDirectoryName(file); - if (!directories.Contains(dir)) - directories.Add(dir); - } - - List seriesList = new List(); - Dictionary directorySeries = new Dictionary(); - foreach (string directory in directories) - { - VectorString seriesIDs = ImageSeriesReader.GetGDCMSeriesIDs(directory); - directorySeries.Add(directory, seriesIDs); - - } - - foreach (var dirSeries in directorySeries) - { - foreach (string seriesID in dirSeries.Value) + if (File.Exists(file)) { - VectorString dicom_names = ImageSeriesReader.GetGDCMSeriesFileNames(dirSeries.Key, seriesID); - ImageSequenceSeries series = new ImageSequenceSeries(); - foreach (string file in dicom_names) - { - ImageSequenceSlice sliceFile = new ImageSequenceSlice(); - sliceFile.filePath = file; - series.files.Add(sliceFile); - } - seriesList.Add(series); + ImageSequenceSlice sliceFile = new ImageSequenceSlice(); + sliceFile.filePath = file; + series.files.Add(sliceFile); } } + + List seriesList = new List(); + seriesList.Add(series); return seriesList; } @@ -93,7 +74,6 @@ public VolumeDataset ImportSeries(IImageSequenceSeries series, ImageSequenceImpo Image image = null; float[] pixelData = null; VectorUInt32 size = null; - VectorString dicomNames = null; // Create dataset VolumeDataset volumeDataset = ScriptableObject.CreateInstance(); @@ -105,7 +85,7 @@ public VolumeDataset ImportSeries(IImageSequenceSeries series, ImageSequenceImpo return null; } - ImportSeriesInternal(dicomNames, sequenceSeries, image, size, pixelData, volumeDataset); + ImportSeriesInternal(sequenceSeries, image, size, pixelData, volumeDataset); return volumeDataset; } @@ -115,7 +95,6 @@ public async Task ImportSeriesAsync(IImageSequenceSeries series, Image image = null; float[] pixelData = null; VectorUInt32 size = null; - VectorString dicomNames = null; // Create dataset VolumeDataset volumeDataset = ScriptableObject.CreateInstance(); @@ -128,23 +107,28 @@ public async Task ImportSeriesAsync(IImageSequenceSeries series, return null; } - await Task.Run(() => ImportSeriesInternal(dicomNames, sequenceSeries, image, size, pixelData, volumeDataset)); + await Task.Run(() => ImportSeriesInternal(sequenceSeries, image, size, pixelData, volumeDataset)); return volumeDataset; } - private void ImportSeriesInternal(VectorString dicomNames, ImageSequenceSeries sequenceSeries, Image image, VectorUInt32 size, float[] pixelData, VolumeDataset volumeDataset) + private void ImportSeriesInternal(ImageSequenceSeries sequenceSeries, Image image, VectorUInt32 size, float[] pixelData, VolumeDataset volumeDataset) { ImageSeriesReader reader = new ImageSeriesReader(); - dicomNames = new VectorString(); + VectorString fileNames = new VectorString(); - foreach (var dicomFile in sequenceSeries.files) - dicomNames.Add(dicomFile.filePath); - reader.SetFileNames(dicomNames); + foreach (var file in sequenceSeries.files) + fileNames.Add(file.filePath); + reader.SetFileNames(fileNames); image = reader.Execute(); + if (image.GetDimension() > 3) + { + Debug.LogWarning("Dataset has more than 3 dimensions. Time-series are not supported. If this fails, please try import one of the files as an image file"); + } + // Cast to 32-bit float image = SimpleITK.Cast(image, PixelIDValueEnum.sitkFloat32); @@ -168,8 +152,8 @@ private void ImportSeriesInternal(VectorString dicomNames, ImageSequenceSeries s volumeDataset.dimX = (int)size[0]; volumeDataset.dimY = (int)size[1]; volumeDataset.dimZ = (int)size[2]; - volumeDataset.datasetName = Path.GetFileName(dicomNames[0]); - volumeDataset.filePath = dicomNames[0]; + volumeDataset.datasetName = Path.GetFileName(fileNames[0]); + volumeDataset.filePath = fileNames[0]; volumeDataset.scale = new Vector3( (float)(spacing[0] * size[0]) / 1000.0f, // mm to m (float)(spacing[1] * size[1]) / 1000.0f, // mm to m diff --git a/Assets/Scripts/Importing/ImporterFactory.cs b/Assets/Scripts/Importing/ImporterFactory.cs index b68afa2b..58414910 100644 --- a/Assets/Scripts/Importing/ImporterFactory.cs +++ b/Assets/Scripts/Importing/ImporterFactory.cs @@ -56,12 +56,16 @@ private static Type GetImageSequenceImporterType(ImageSequenceFormat format) { case ImageSequenceFormat.ImageSequence: { + #if UVR_USE_SIMPLEITK + return typeof(SimpleITKImageSequenceImporter); + #else return typeof(ImageSequenceImporter); + #endif } case ImageSequenceFormat.DICOM: { #if UVR_USE_SIMPLEITK - return typeof(SimpleITKImageSequenceImporter); + return typeof(SimpleITKDICOMImporter); #else return typeof(DICOMImporter); #endif @@ -95,6 +99,14 @@ private static Type GetImageFileImporterType(ImageFileFormat format) return typeof(NiftiImporter); #endif } + case ImageFileFormat.Unknown: + { +#if UVR_USE_SIMPLEITK + return typeof(SimpleITKImageFileImporter); +#else + return null; +#endif + } default: return null; }