Skip to content

Commit

Permalink
Use SimpleITK for image sequence datasets (#226)
Browse files Browse the repository at this point in the history
* Use SimpleITK's image sequence importer when available
* SimpleITKDicomImporter
* Use image file importer for tiff
  • Loading branch information
mlavik1 committed Feb 23, 2024
1 parent 186d6cd commit 54f6ff1
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 42 deletions.
57 changes: 57 additions & 0 deletions Assets/Editor/VolumeRendererEditorFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -315,6 +367,11 @@ private static async void ImportSequenceAsync()

IEnumerable<IImageSequenceSeries> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public enum ImageFileFormat
{
VASP,
NRRD,
NIFTI
NIFTI,
Unknown
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// SimpleITK-based DICOM importer.
/// Has support for JPEG2000 and more.
/// </summary>
public class SimpleITKDICOMImporter : IImageSequenceImporter
{
public class ImageSequenceSlice : IImageSequenceFile
{
public string filePath;

public string GetFilePath()
{
return filePath;
}
}

public class ImageSequenceSeries : IImageSequenceSeries
{
public List<ImageSequenceSlice> files = new List<ImageSequenceSlice>();

public IEnumerable<IImageSequenceFile> GetFiles()
{
return files;
}
}

public IEnumerable<IImageSequenceSeries> LoadSeries(IEnumerable<string> files, ImageSequenceImportSettings settings)
{
List<ImageSequenceSeries> seriesList= LoadSeriesInternal(files);

return seriesList;
}

public async Task<IEnumerable<IImageSequenceSeries>> LoadSeriesAsync(IEnumerable<string> files, ImageSequenceImportSettings settings)
{
List<ImageSequenceSeries> seriesList = null;
await Task.Run(() => seriesList=LoadSeriesInternal(files));

return seriesList;
}

private List<ImageSequenceSeries> LoadSeriesInternal(IEnumerable<string> files)
{
HashSet<string> directories = new HashSet<string>();

foreach (string file in files)
{
string dir = Path.GetDirectoryName(file);
if (!directories.Contains(dir))
directories.Add(dir);
}

List<ImageSequenceSeries> seriesList = new List<ImageSequenceSeries>();
Dictionary<string, VectorString> directorySeries = new Dictionary<string, VectorString>();
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<VolumeDataset>();

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<VolumeDataset> ImportSeriesAsync(IImageSequenceSeries series, ImageSequenceImportSettings settings)
{
Image image = null;
float[] pixelData = null;
VectorUInt32 size = null;
VectorString dicomNames = null;

// Create dataset
VolumeDataset volumeDataset = ScriptableObject.CreateInstance<VolumeDataset>();

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
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
namespace UnityVolumeRendering
{
/// <summary>
/// SimpleITK-based DICOM importer.
/// Has support for JPEG2000 and more.
/// SimpleITK-based image sequence importer.
/// Has support for TIFF and more.
/// </summary>
public class SimpleITKImageSequenceImporter : IImageSequenceImporter
{
Expand Down Expand Up @@ -52,39 +52,20 @@ public async Task<IEnumerable<IImageSequenceSeries>> LoadSeriesAsync(IEnumerable

private List<ImageSequenceSeries> LoadSeriesInternal(IEnumerable<string> files)
{
HashSet<string> directories = new HashSet<string>();
ImageSequenceSeries series = new ImageSequenceSeries();

foreach (string file in files)
{
string dir = Path.GetDirectoryName(file);
if (!directories.Contains(dir))
directories.Add(dir);
}

List<ImageSequenceSeries> seriesList = new List<ImageSequenceSeries>();
Dictionary<string, VectorString> directorySeries = new Dictionary<string, VectorString>();
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<ImageSequenceSeries> seriesList = new List<ImageSequenceSeries>();
seriesList.Add(series);
return seriesList;
}

Expand All @@ -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<VolumeDataset>();
Expand All @@ -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;
}
Expand All @@ -115,7 +95,6 @@ public async Task<VolumeDataset> ImportSeriesAsync(IImageSequenceSeries series,
Image image = null;
float[] pixelData = null;
VectorUInt32 size = null;
VectorString dicomNames = null;

// Create dataset
VolumeDataset volumeDataset = ScriptableObject.CreateInstance<VolumeDataset>();
Expand All @@ -128,23 +107,28 @@ public async Task<VolumeDataset> 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);

Expand All @@ -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
Expand Down
Loading

0 comments on commit 54f6ff1

Please sign in to comment.