From 3d514653defe918090523ede43fdef442bcce77c Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Thu, 7 Apr 2022 18:32:50 +0200 Subject: [PATCH] SimpleITK: DICOM JPEG, NRRD, NIFTI (Windows only) (#95) * SimpleITK-based DICOM importer. * Cleanup: Importer interfaces and factory. * NRRD and NIFTI import (using SimpleITK) * Added script for exporting .unitypackage * Updated documentation. * Don't use EditorDatasetImporter for RAW import. * .gitattributes --- ACKNOWLEDGEMENTS.txt | 10 + Assets/DONOTREMOVE-PathSearchFile.txt | 2 + Assets/DONOTREMOVE-PathSearchFile.txt.meta | 7 + Assets/Editor/EditorDatasetImporter.cs | 35 +- Assets/Editor/ImportSettingsEditorWindow.cs | 31 ++ Assets/Editor/SimpleITK.meta | 8 + Assets/Editor/SimpleITK/SimpleITKManager.cs | 158 ++++++++ .../Editor/SimpleITK/SimpleITKManager.cs.meta | 11 + .../Editor/VolumeRendererEditorFunctions.cs | 145 +++++-- Assets/Scripts/GUI/Components/RuntimeGUI.cs | 12 +- .../Importing/DatasetImporterUtility.cs | 19 +- .../Scripts/Importing/ImageFileImporter.meta | 8 + .../ImageFileImporter/Interface.meta | 8 + .../Interface/IImageFileImporter.cs | 20 + .../Interface/IImageFileImporter.cs.meta | 11 + .../Interface/SimpleITK.meta | 8 + .../ImageFileImporter/SimpleITK.meta | 8 + .../SimpleITK/SimpleITKImageFileImporter.cs | 58 +++ .../SimpleITKImageFileImporter.cs.meta | 11 + .../Importing/ImageFileImporter/VASP.meta | 8 + .../VASP}/ParDatasetImporter.cs | 9 +- .../VASP}/ParDatasetImporter.cs.meta | 0 .../Importing/ImageSequenceImporter.meta | 8 + .../ImageSequenceImporter.meta | 8 + .../ImageSequenceImporter}/DensityHelper.cs | 0 .../DensityHelper.cs.meta | 2 +- .../ImageSequenceImporter}/DensitySource.cs | 0 .../DensitySource.cs.meta | 2 +- .../ImageSequenceImporter.cs | 78 ++-- .../ImageSequenceImporter.cs.meta | 2 +- .../ImageSequenceImporter/Interface.meta | 8 + .../Interface/IImageSequenceImporter.cs | 44 +++ .../Interface/IImageSequenceImporter.cs.meta | 11 + .../ImageSequenceImporter/OpenDICOM.meta | 8 + .../OpenDICOM}/DICOMImporter.cs | 42 +- .../OpenDICOM}/DICOMImporter.cs.meta | 0 .../ImageSequenceImporter/SimpleITK.meta | 8 + .../SimpleITKImageSequenceImporter.cs | 131 +++++++ .../SimpleITKImageSequenceImporter.cs.meta | 11 + Assets/Scripts/Importing/ImporterFactory.cs | 103 +++++ .../Scripts/Importing/ImporterFactory.cs.meta | 11 + Assets/Scripts/Importing/Ini.meta | 8 + .../Importing/{ => Ini}/DatasetIniReader.cs | 0 .../{ => Ini}/DatasetIniReader.cs.meta | 0 Assets/Scripts/Importing/RawImporter.meta | 8 + .../{ => RawImporter}/RawDatasetImporter.cs | 364 +++++++++--------- .../RawDatasetImporter.cs.meta | 0 Documentation/Importing.md | 95 +++++ Documentation/SimpleITK.md | 25 ++ Documentation/img/settings-toolbar.jpg | Bin 0 -> 15373 bytes Documentation/img/settings.jpg | Bin 0 -> 32617 bytes README.md | 25 +- gitattributes | 5 + scripts/ExportUnityPackage.py | 29 ++ 54 files changed, 1320 insertions(+), 303 deletions(-) create mode 100644 Assets/DONOTREMOVE-PathSearchFile.txt create mode 100644 Assets/DONOTREMOVE-PathSearchFile.txt.meta create mode 100644 Assets/Editor/SimpleITK.meta create mode 100644 Assets/Editor/SimpleITK/SimpleITKManager.cs create mode 100644 Assets/Editor/SimpleITK/SimpleITKManager.cs.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/Interface.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs create mode 100644 Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/Interface/SimpleITK.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/SimpleITK.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs create mode 100644 Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs.meta create mode 100644 Assets/Scripts/Importing/ImageFileImporter/VASP.meta rename Assets/Scripts/Importing/{ => ImageFileImporter/VASP}/ParDatasetImporter.cs (98%) rename Assets/Scripts/Importing/{ => ImageFileImporter/VASP}/ParDatasetImporter.cs.meta (100%) create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter.meta create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter.meta rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/DensityHelper.cs (100%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/DensityHelper.cs.meta (83%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/DensitySource.cs (100%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/DensitySource.cs.meta (83%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/ImageSequenceImporter.cs (70%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/ImageSequenceImporter}/ImageSequenceImporter.cs.meta (83%) create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/Interface.meta create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs.meta create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM.meta rename Assets/Scripts/Importing/{ => ImageSequenceImporter/OpenDICOM}/DICOMImporter.cs (92%) rename Assets/Scripts/Importing/{ => ImageSequenceImporter/OpenDICOM}/DICOMImporter.cs.meta (100%) create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK.meta create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs create mode 100644 Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs.meta create mode 100644 Assets/Scripts/Importing/ImporterFactory.cs create mode 100644 Assets/Scripts/Importing/ImporterFactory.cs.meta create mode 100644 Assets/Scripts/Importing/Ini.meta rename Assets/Scripts/Importing/{ => Ini}/DatasetIniReader.cs (100%) rename Assets/Scripts/Importing/{ => Ini}/DatasetIniReader.cs.meta (100%) create mode 100644 Assets/Scripts/Importing/RawImporter.meta rename Assets/Scripts/Importing/{ => RawImporter}/RawDatasetImporter.cs (97%) rename Assets/Scripts/Importing/{ => RawImporter}/RawDatasetImporter.cs.meta (100%) create mode 100644 Documentation/Importing.md create mode 100644 Documentation/SimpleITK.md create mode 100644 Documentation/img/settings-toolbar.jpg create mode 100644 Documentation/img/settings.jpg create mode 100644 gitattributes create mode 100644 scripts/ExportUnityPackage.py diff --git a/ACKNOWLEDGEMENTS.txt b/ACKNOWLEDGEMENTS.txt index a8ed7db7..bd3314c0 100644 --- a/ACKNOWLEDGEMENTS.txt +++ b/ACKNOWLEDGEMENTS.txt @@ -6,3 +6,13 @@ http://opendicom.sourceforge.net/index.html Copyright (C) 2006-2007 Albert Gnandt + +SimpleITK +released under Apache License 2.0 +https://github.com/SimpleITK/SimpleITK/blob/master/LICENSE + +Copyright 2010-2019 Insight Software Consortium +Copyright 2020 NumFOCUS + +SimpleITK is not included in this repository, but will optionally be downloaded if the user chooser enable it. +The license file will be stored together with the library in the Assets/3rdparty/SimpleITK directory. diff --git a/Assets/DONOTREMOVE-PathSearchFile.txt b/Assets/DONOTREMOVE-PathSearchFile.txt new file mode 100644 index 00000000..3d076264 --- /dev/null +++ b/Assets/DONOTREMOVE-PathSearchFile.txt @@ -0,0 +1,2 @@ +DO NOT REMOVE. This file simply exists so we can get the path of the UnityVolumeRendering project folder (maybe be added as a subfolder to another project). + \ No newline at end of file diff --git a/Assets/DONOTREMOVE-PathSearchFile.txt.meta b/Assets/DONOTREMOVE-PathSearchFile.txt.meta new file mode 100644 index 00000000..46878dbb --- /dev/null +++ b/Assets/DONOTREMOVE-PathSearchFile.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49eccd9c87b5fde45810b63c2f167df3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/EditorDatasetImporter.cs b/Assets/Editor/EditorDatasetImporter.cs index 07c8b323..276592ef 100644 --- a/Assets/Editor/EditorDatasetImporter.cs +++ b/Assets/Editor/EditorDatasetImporter.cs @@ -25,22 +25,31 @@ public static void ImportDataset(string filePath) break; } case DatasetType.DICOM: + case DatasetType.ImageSequence: { + ImageSequenceFormat imgSeqFormat; + if (datasetType == DatasetType.DICOM) + imgSeqFormat = ImageSequenceFormat.DICOM; + else if (datasetType == DatasetType.ImageSequence) + imgSeqFormat = ImageSequenceFormat.ImageSequence; + else + throw new NotImplementedException(); + string directoryPath = new FileInfo(filePath).Directory.FullName; // Find all DICOM files in directory IEnumerable fileCandidates = Directory.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly) .Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase)); - DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(directoryPath)); + IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(imgSeqFormat); - List seriesList = importer.LoadDICOMSeries(); - foreach (DICOMImporter.DICOMSeries series in seriesList) + IEnumerable seriesList = importer.LoadSeries(fileCandidates); + foreach (IImageSequenceSeries series in seriesList) { // Only import the series that contains the selected file - if(series.dicomFiles.Any(f => Path.GetFileName(f.filePath) == Path.GetFileName(filePath))) + if(series.GetFiles().Any(f => Path.GetFileName(f.GetFilePath()) == Path.GetFileName(filePath))) { - VolumeDataset dataset = importer.ImportDICOMSeries(series); + VolumeDataset dataset = importer.ImportSeries(series); if (dataset != null) { @@ -51,9 +60,21 @@ public static void ImportDataset(string filePath) break; } case DatasetType.PARCHG: + case DatasetType.NRRD: + case DatasetType.NIFTI: { - ParDatasetImporter importer = new ParDatasetImporter(filePath); - VolumeDataset dataset = importer.Import(); + ImageFileFormat imgFileFormat; + if (datasetType == DatasetType.PARCHG) + imgFileFormat = ImageFileFormat.VASP; + else if (datasetType == DatasetType.NRRD) + imgFileFormat = ImageFileFormat.NRRD; + else if (datasetType == DatasetType.NIFTI) + imgFileFormat = ImageFileFormat.NIFTI; + else + throw new NotImplementedException(); + + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imgFileFormat); + VolumeDataset dataset = importer.Import(filePath); if (dataset != null) { diff --git a/Assets/Editor/ImportSettingsEditorWindow.cs b/Assets/Editor/ImportSettingsEditorWindow.cs index 6b01099b..2ede6f7c 100644 --- a/Assets/Editor/ImportSettingsEditorWindow.cs +++ b/Assets/Editor/ImportSettingsEditorWindow.cs @@ -13,9 +13,40 @@ public static void ShowWindow() private void OnGUI() { + GUIStyle headerStyle = new GUIStyle(EditorStyles.label); + headerStyle.fontSize = 20; + + EditorGUILayout.LabelField("Volume rendering import settings", headerStyle); + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Show promt asking if you want to downscale the dataset on import?"); bool showDownscalePrompt = EditorGUILayout.Toggle("Show downscale prompt", EditorPrefs.GetBool("DownscaleDatasetPrompt")); EditorPrefs.SetBool("DownscaleDatasetPrompt", showDownscalePrompt); + +#if UNITY_EDITOR_WIN + EditorGUILayout.Space(); + EditorGUILayout.Space(); + EditorGUILayout.LabelField("SimpleITK", headerStyle); + EditorGUILayout.Space(); + EditorGUILayout.LabelField("SimpleITK is a library that adds support for JPEG-compressed DICOM, as well as NRRD and NIFTI formats.\n" + + "Enabling it will start a download of ca 100MBs of binaries. It currently only works on Windows (Linux is WIP)", EditorStyles.wordWrappedLabel); + + if (!SimpleITKManager.IsSITKEnabled()) + { + if (GUILayout.Button("Enable SimpleITK")) + { + SimpleITKManager.DownloadBinaries(); + SimpleITKManager.EnableSITK(true); + } + } + else + { + if (GUILayout.Button("Disable SimpleITK")) + { + SimpleITKManager.EnableSITK(false); + } + } +#endif } } } diff --git a/Assets/Editor/SimpleITK.meta b/Assets/Editor/SimpleITK.meta new file mode 100644 index 00000000..67f0431f --- /dev/null +++ b/Assets/Editor/SimpleITK.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72b202778592e63429e05350d02fd176 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/SimpleITK/SimpleITKManager.cs b/Assets/Editor/SimpleITK/SimpleITKManager.cs new file mode 100644 index 00000000..31a5b6e1 --- /dev/null +++ b/Assets/Editor/SimpleITK/SimpleITKManager.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using UnityEditor; +using UnityEngine; +using System.IO.Compression; + +namespace UnityVolumeRendering +{ + /// + /// Manager for the SimpleITK integration. + /// Since SimpleITK is a native library that requires binaries to be built for your target platform, + /// SimpleITK will be disabled by default and can be enabled through this class. + /// The binaries will be downloaded automatically. + /// + public class SimpleITKManager + { + private static string SimpleITKDefinition = "UVR_USE_SIMPLEITK"; + + public static bool IsSITKEnabled() + { + BuildTarget target = EditorUserBuildSettings.activeBuildTarget; + BuildTargetGroup group = BuildPipeline.GetBuildTargetGroup(target); + + HashSet defines = new HashSet(PlayerSettings.GetScriptingDefineSymbolsForGroup(group).Split(';')); + return defines.Contains(SimpleITKDefinition); + } + + public static void EnableSITK(bool enable) + { + if (!HasDownloadedBinaries()) + { + EditorUtility.DisplayDialog("Missing SimpleITK binaries", "You need to download the SimpleITK binaries before you can enable SimpleITK.", "Ok"); + return; + } + + // Enable the UVR_USE_SIMPLEITK preprocessor definition for standalone target + List buildTargetGroups = new List (){ BuildTargetGroup.Standalone }; + foreach (BuildTargetGroup group in buildTargetGroups) + { + List defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group).Split(';').ToList(); + defines.Remove(SimpleITKDefinition); + if (enable) + defines.Add(SimpleITKDefinition); + PlayerSettings.SetScriptingDefineSymbolsForGroup(group, String.Join(";", defines)); + } + + // Save project and recompile scripts + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); +#if UNITY_2019_3_OR_NEWER + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); +#endif + } + + public static bool HasDownloadedBinaries() + { + string binDir = GetBinaryDirectoryPath(); + return Directory.Exists(binDir) && Directory.GetFiles(binDir).Length > 0; // TODO: Check actual files? + } + + public static void DownloadBinaries() + { + string extractDirPath = GetBinaryDirectoryPath(); + string zipPath = Path.Combine(Directory.GetParent(extractDirPath).FullName, "SimpleITK.zip"); + if (HasDownloadedBinaries()) + { + if (!EditorUtility.DisplayDialog("Download SimpleITK binaries", "SimpleITK has already been downloaded. Do you want to delete it and download again?", "Yes", "No")) + { + return; + } + } + + EditorUtility.DisplayProgressBar("Downloading SimpleITK", "Downloading SimpleITK binaries.", 0); + + // Downlaod binaries zip + using (var client = new WebClient()) + { + string downloadURL = "https://sourceforge.net/projects/simpleitk/files/SimpleITK/1.2.4/CSharp/SimpleITK-1.2.4-CSharp-win64-x64.zip/download"; + client.DownloadFile(downloadURL, zipPath); + + EditorUtility.DisplayProgressBar("Downloading SimpleITK", "Downloading SimpleITK binaries.", 70); + + if (!File.Exists(zipPath)) + { + Debug.Log(zipPath); + EditorUtility.DisplayDialog("Error downloadig SimpleITK binaries.", "Failed to download SimpleITK binaries. Please check your internet connection.", "Close"); + Debug.Log($"Failed to download SimpleITK binaries. You can also try to manually download from {downloadURL} and extract it to some folder inside the Assets folder."); + return; + } + + try + { + ExtractZip(zipPath, extractDirPath); + } + catch (Exception ex) + { + string errorString = $"Extracting binaries failed with error: {ex.Message}\n" + + $"Please try downloading the zip from: {downloadURL}\nAnd extract it somewhere in the Assets folder.\n\n" + + "The download URL can be copied from the error log (console)."; + Debug.LogError(ex.ToString()); + Debug.LogError(errorString); + EditorUtility.DisplayDialog("Failed to extract binaries.", errorString, "Close"); + } + } + + File.Delete(zipPath); + + EditorUtility.ClearProgressBar(); + } + + private static void ExtractZip(string zipPath, string extractDirPath) + { + // Extract zip + using (FileStream zipStream = new FileStream(zipPath, FileMode.Open)) + { + using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Update)) + { + if (!Directory.Exists(extractDirPath)) + Directory.CreateDirectory(extractDirPath); + + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (entry.Name != "" && !entry.Name.EndsWith("/")) + { + string destFilePath = Path.Combine(extractDirPath, entry.Name); + //TextAsset destAsset = new TextAsset("abc"); + //AssetDatabase.CreateAsset(destAsset, extractDirRelPath + "/" + entry.Name); + Stream inStream = entry.Open(); + + using (Stream outStream = File.OpenWrite(destFilePath)) + { + inStream.CopyTo(outStream); + } + } + } + } + } + } + + private static string GetBinaryDirectoryPath() + { + string dataPath = Application.dataPath; + foreach (string file in Directory.EnumerateFiles(Application.dataPath, "*.*", SearchOption.AllDirectories)) + { + // Search for magic file stored in Assets directory. + // This is necessary for cases where the UVR plugin is stored in a subfolder (thatæs the case for the asset store version) + if (Path.GetFileName(file) == "DONOTREMOVE-PathSearchFile.txt") + { + dataPath = Path.GetDirectoryName(file); + } + } + return Path.Combine(dataPath, "3rdparty", "SimpleITK"); // TODO: What is UVR is in a subfolder? + } + } +} diff --git a/Assets/Editor/SimpleITK/SimpleITKManager.cs.meta b/Assets/Editor/SimpleITK/SimpleITKManager.cs.meta new file mode 100644 index 00000000..77ba85d3 --- /dev/null +++ b/Assets/Editor/SimpleITK/SimpleITKManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b1dcf5d0f378e04e9ae4aa3552f5667 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index ac288e29..5ea824a9 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -15,21 +15,12 @@ static void ShowDatasetImporter() string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", ""); if (File.Exists(file)) { - EditorDatasetImporter.ImportDataset(file); - } - else - { - Debug.LogError("File doesn't exist: " + file); - } - } + RAWDatasetImporterEditorWindow wnd = (RAWDatasetImporterEditorWindow)EditorWindow.GetWindow(typeof(RAWDatasetImporterEditorWindow)); + if (wnd != null) + wnd.Close(); - [MenuItem("Volume Rendering/Load dataset/Load PARCHG dataset")] - static void ShowParDatasetImporter() - { - string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", ""); - if (File.Exists(file)) - { - EditorDatasetImporter.ImportDataset(file); + wnd = new RAWDatasetImporterEditorWindow(file); + wnd.Show(); } else { @@ -62,13 +53,13 @@ static void ShowDICOMImporter() if (fileCandidates.Any()) { - DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(dir)); - List seriesList = importer.LoadDICOMSeries(); + IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM); + IEnumerable seriesList = importer.LoadSeries(fileCandidates); float numVolumesCreated = 0; - foreach (DICOMImporter.DICOMSeries series in seriesList) + foreach (IImageSequenceSeries series in seriesList) { - VolumeDataset dataset = importer.ImportDICOMSeries(series); + VolumeDataset dataset = importer.ImportSeries(series); if (dataset != null) { if (EditorPrefs.GetBool("DownscaleDatasetPrompt")) @@ -97,6 +88,102 @@ static void ShowDICOMImporter() } } +#if UNITY_EDITOR_WIN + [MenuItem("Volume Rendering/Load dataset/Load NRRD dataset")] + static void ShowNRRDDatasetImporter() + { + if (!SimpleITKManager.IsSITKEnabled()) + { + if (EditorUtility.DisplayDialog("Missing SimpleITK", "You need to download SimpleITK to load NRRD datasets from the import settings menu.\n" + + "Do you want to open the import settings menu?", "Yes", "No")) + { + ImportSettingsEditorWindow.ShowWindow(); + } + return; + } + + string file = EditorUtility.OpenFilePanel("Select a dataset to load (.nrrd)", "DataFiles", ""); + if (File.Exists(file)) + { + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.NRRD); + VolumeDataset dataset = importer.Import(file); + + if (dataset != null) + { + VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset); + } + else + { + Debug.LogError("Failed to import datset"); + } + } + else + { + Debug.LogError("File doesn't exist: " + file); + } + } +#endif + +#if UNITY_EDITOR_WIN + [MenuItem("Volume Rendering/Load dataset/Load NIFTI dataset")] + static void ShowNIFTIDatasetImporter() + { + if (!SimpleITKManager.IsSITKEnabled()) + { + if (EditorUtility.DisplayDialog("Missing SimpleITK", "You need to download SimpleITK to load NRRD datasets from the import settings menu.\n" + + "Do you want to open the import settings menu?", "Yes", "No")) + { + ImportSettingsEditorWindow.ShowWindow(); + } + return; + } + + string file = EditorUtility.OpenFilePanel("Select a dataset to load (.nii)", "DataFiles", ""); + if (File.Exists(file)) + { + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.NIFTI); + VolumeDataset dataset = importer.Import(file); + + if (dataset != null) + { + VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset); + } + else + { + Debug.LogError("Failed to import datset"); + } + } + else + { + Debug.LogError("File doesn't exist: " + file); + } + } +#endif + + [MenuItem("Volume Rendering/Load dataset/Load PARCHG dataset")] + static void ShowParDatasetImporter() + { + string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", ""); + if (File.Exists(file)) + { + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.VASP); + VolumeDataset dataset = importer.Import(file); + + if (dataset != null) + { + VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset); + } + else + { + Debug.LogError("Failed to import datset"); + } + } + else + { + Debug.LogError("File doesn't exist: " + file); + } + } + [MenuItem("Volume Rendering/Load dataset/Load image sequence")] static void ShowSequenceImporter() { @@ -104,20 +191,26 @@ static void ShowSequenceImporter() if (Directory.Exists(dir)) { - ImageSequenceImporter importer = new ImageSequenceImporter(dir); + List filePaths = Directory.GetFiles(dir).ToList(); + IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.ImageSequence); - VolumeDataset dataset = importer.Import(); - if (dataset != null) + IEnumerable seriesList = importer.LoadSeries(filePaths); + + foreach(IImageSequenceSeries series in seriesList) { - if (EditorPrefs.GetBool("DownscaleDatasetPrompt")) + VolumeDataset dataset = importer.ImportSeries(series); + if (dataset != null) { - if (EditorUtility.DisplayDialog("Optional DownScaling", - $"Do you want to downscale the dataset? The dataset's dimension is: {dataset.dimX} x {dataset.dimY} x {dataset.dimZ}", "Yes", "No")) + if (EditorPrefs.GetBool("DownscaleDatasetPrompt")) { - dataset.DownScaleData(); + if (EditorUtility.DisplayDialog("Optional DownScaling", + $"Do you want to downscale the dataset? The dataset's dimension is: {dataset.dimX} x {dataset.dimY} x {dataset.dimZ}", "Yes", "No")) + { + dataset.DownScaleData(); + } } + VolumeObjectFactory.CreateObject(dataset); } - VolumeObjectFactory.CreateObject(dataset); } } else diff --git a/Assets/Scripts/GUI/Components/RuntimeGUI.cs b/Assets/Scripts/GUI/Components/RuntimeGUI.cs index 228b1c5b..12d548c6 100644 --- a/Assets/Scripts/GUI/Components/RuntimeGUI.cs +++ b/Assets/Scripts/GUI/Components/RuntimeGUI.cs @@ -55,8 +55,8 @@ private void OnOpenPARDatasetResult(RuntimeFileBrowser.DialogResult result) { DespawnAllDatasets(); string filePath = result.path; - ParDatasetImporter parimporter = new ParDatasetImporter(filePath); - VolumeDataset dataset = parimporter.Import(); //overriden somewhere + IImageFileImporter parimporter = ImporterFactory.CreateImageFileImporter(ImageFileFormat.VASP); + VolumeDataset dataset = parimporter.Import(filePath); if (dataset != null) { VolumeObjectFactory.CreateObject(dataset); @@ -107,12 +107,12 @@ private void OnOpenDICOMDatasetResult(RuntimeFileBrowser.DialogResult result) .Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase)); // Import the dataset - DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(result.path)); - List seriesList = importer.LoadDICOMSeries(); + IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM); + IEnumerable seriesList = importer.LoadSeries(fileCandidates); float numVolumesCreated = 0; - foreach (DICOMImporter.DICOMSeries series in seriesList) + foreach (IImageSequenceSeries series in seriesList) { - VolumeDataset dataset = importer.ImportDICOMSeries(series); + VolumeDataset dataset = importer.ImportSeries(series); // Spawn the object if (dataset != null) { diff --git a/Assets/Scripts/Importing/DatasetImporterUtility.cs b/Assets/Scripts/Importing/DatasetImporterUtility.cs index 0bafcec6..cb5db5a3 100644 --- a/Assets/Scripts/Importing/DatasetImporterUtility.cs +++ b/Assets/Scripts/Importing/DatasetImporterUtility.cs @@ -10,12 +10,14 @@ public enum DatasetType Unknown, Raw, DICOM, - PARCHG + PARCHG, + NRRD, + NIFTI, + ImageSequence } public class DatasetImporterUtility { - public static DatasetType GetDatasetType(string filePath) { DatasetType datasetType; @@ -40,7 +42,18 @@ public static DatasetType GetDatasetType(string filePath) { datasetType = DatasetType.DICOM; } - + else if(extension == ".nrrd") + { + datasetType = DatasetType.NRRD; + } + else if(extension == ".nii") + { + datasetType = DatasetType.NIFTI; + } + else if(extension == ".jpg" || extension == ".jpeg" || extension == ".png") + { + datasetType = DatasetType.ImageSequence; + } else { datasetType = DatasetType.Unknown; diff --git a/Assets/Scripts/Importing/ImageFileImporter.meta b/Assets/Scripts/Importing/ImageFileImporter.meta new file mode 100644 index 00000000..84855533 --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0bc4bfc7f2f25c24393bda6d37c5d2d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/Interface.meta b/Assets/Scripts/Importing/ImageFileImporter/Interface.meta new file mode 100644 index 00000000..0ff7e897 --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/Interface.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 07ab175bed68a71409ea15e6dbd1a1c8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs new file mode 100644 index 00000000..24f7e02d --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs @@ -0,0 +1,20 @@ +using System; + +namespace UnityVolumeRendering +{ + public enum ImageFileFormat + { + VASP, + NRRD, + NIFTI + } + + /// + /// Interface for single file dataset importers (NRRD, NIFTI, etc.). + /// These datasets contain only one single file. + /// + public interface IImageFileImporter + { + VolumeDataset Import(String filePath); + } +} diff --git a/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs.meta b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs.meta new file mode 100644 index 00000000..cbcd00de --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/Interface/IImageFileImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3a3d1be37fcc3c4893fa3b9bb747e51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/Interface/SimpleITK.meta b/Assets/Scripts/Importing/ImageFileImporter/Interface/SimpleITK.meta new file mode 100644 index 00000000..78570c8c --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/Interface/SimpleITK.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2a823b11bfaf79741bf3312984c336fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/SimpleITK.meta b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK.meta new file mode 100644 index 00000000..dca2d65a --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 35cb10b1230dc924db4f6edc326656dd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs new file mode 100644 index 00000000..75cff61f --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs @@ -0,0 +1,58 @@ +#if UVR_USE_SIMPLEITK +using UnityEngine; +using System; +using itk.simple; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.IO; + +namespace UnityVolumeRendering +{ + /// + /// SimpleITK-based DICOM importer. + /// + public class SimpleITKImageFileImporter : IImageFileImporter + { + public VolumeDataset Import(string filePath) + { + ImageFileReader reader = new ImageFileReader(); + + reader.SetFileName(filePath); + + Image image = reader.Execute(); + + // Cast to 32-bit float + image = SimpleITK.Cast(image, PixelIDValueEnum.sitkFloat32); + + VectorUInt32 size = image.GetSize(); + + int numPixels = 1; + for (int dim = 0; dim < image.GetDimension(); dim++) + numPixels *= (int)size[dim]; + + // Read pixel data + float[] pixelData = new float[numPixels]; + IntPtr imgBuffer = image.GetBufferAsFloat(); + Marshal.Copy(imgBuffer, pixelData, 0, numPixels); + + VectorDouble spacing = image.GetSpacing(); + + // Create dataset + VolumeDataset volumeDataset = new VolumeDataset(); + volumeDataset.data = pixelData; + volumeDataset.dimX = (int)size[0]; + volumeDataset.dimY = (int)size[1]; + volumeDataset.dimZ = (int)size[2]; + volumeDataset.datasetName = "test"; + volumeDataset.filePath = filePath; + volumeDataset.scaleX = (float)(spacing[0] * size[0]); + volumeDataset.scaleY = (float)(spacing[1] * size[1]); + volumeDataset.scaleZ = (float)(spacing[2] * size[2]); + + volumeDataset.FixDimensions(); + + return volumeDataset; + } + } +} +#endif diff --git a/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs.meta b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs.meta new file mode 100644 index 00000000..2598a0ee --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/SimpleITK/SimpleITKImageFileImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abda27d3474f8d74eaa8ab9502de54e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageFileImporter/VASP.meta b/Assets/Scripts/Importing/ImageFileImporter/VASP.meta new file mode 100644 index 00000000..3e11d124 --- /dev/null +++ b/Assets/Scripts/Importing/ImageFileImporter/VASP.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f8aa6a88cf66650458a44d6339aad472 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ParDatasetImporter.cs b/Assets/Scripts/Importing/ImageFileImporter/VASP/ParDatasetImporter.cs similarity index 98% rename from Assets/Scripts/Importing/ParDatasetImporter.cs rename to Assets/Scripts/Importing/ImageFileImporter/VASP/ParDatasetImporter.cs index bda14c86..a7c1bd78 100644 --- a/Assets/Scripts/Importing/ParDatasetImporter.cs +++ b/Assets/Scripts/Importing/ImageFileImporter/VASP/ParDatasetImporter.cs @@ -21,7 +21,7 @@ namespace UnityVolumeRendering { - public class ParDatasetImporter + public class ParDatasetImporter : IImageFileImporter { string filePath; string fileName; @@ -54,13 +54,10 @@ public class ParDatasetImporter string[] fileContentLines; int fileContentIndex; - public ParDatasetImporter(string filePath) + public VolumeDataset Import(string filePath) { this.filePath = filePath; - } - - public VolumeDataset Import() //fills VolumeDataset object - { + var extension = Path.GetExtension(filePath); if (!File.Exists(filePath)) { diff --git a/Assets/Scripts/Importing/ParDatasetImporter.cs.meta b/Assets/Scripts/Importing/ImageFileImporter/VASP/ParDatasetImporter.cs.meta similarity index 100% rename from Assets/Scripts/Importing/ParDatasetImporter.cs.meta rename to Assets/Scripts/Importing/ImageFileImporter/VASP/ParDatasetImporter.cs.meta diff --git a/Assets/Scripts/Importing/ImageSequenceImporter.meta b/Assets/Scripts/Importing/ImageSequenceImporter.meta new file mode 100644 index 00000000..0afe33da --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ec1386093d269948b40d7bc47e9de07 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter.meta b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter.meta new file mode 100644 index 00000000..41c1f3b9 --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4b91e6acf81adf47a1dc652f7e1b5dc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/DensityHelper.cs b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensityHelper.cs similarity index 100% rename from Assets/Scripts/Importing/DensityHelper.cs rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensityHelper.cs diff --git a/Assets/Scripts/Importing/DensityHelper.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensityHelper.cs.meta similarity index 83% rename from Assets/Scripts/Importing/DensityHelper.cs.meta rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensityHelper.cs.meta index 9f52c86c..08b7ad4e 100644 --- a/Assets/Scripts/Importing/DensityHelper.cs.meta +++ b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensityHelper.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 49be4e9169bf3414c813c8a82ce22908 +guid: 4b22f148aa28d6e408926877a42139d2 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Importing/DensitySource.cs b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensitySource.cs similarity index 100% rename from Assets/Scripts/Importing/DensitySource.cs rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensitySource.cs diff --git a/Assets/Scripts/Importing/DensitySource.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensitySource.cs.meta similarity index 83% rename from Assets/Scripts/Importing/DensitySource.cs.meta rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensitySource.cs.meta index 9d6d36c3..dff889e3 100644 --- a/Assets/Scripts/Importing/DensitySource.cs.meta +++ b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/DensitySource.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e9041472fdada6742af93a331fcbc3ea +guid: cf44cb487e46042489ffc46377a5bfb9 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Importing/ImageSequenceImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs similarity index 70% rename from Assets/Scripts/Importing/ImageSequenceImporter.cs rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs index 662156f6..b68739cc 100644 --- a/Assets/Scripts/Importing/ImageSequenceImporter.cs +++ b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs @@ -2,59 +2,77 @@ using System.Collections.Generic; using System.IO; using UnityEngine; +using System.Linq; namespace UnityVolumeRendering { /// /// Converts a directory of image slices into a VolumeDataset for volumetric rendering. /// - public class ImageSequenceImporter + public class ImageSequenceImporter : IImageSequenceImporter { - private string directoryPath; - private string[] supportedImageTypes = new string[] + public class ImageSequenceFile : IImageSequenceFile { - "*.png", - "*.jpg", - "*.jpeg" - }; + public string filePath; + + public string GetFilePath() + { + return filePath; + } + } - public ImageSequenceImporter(string directoryPath) + public class ImageSequenceSeries : IImageSequenceSeries { - this.directoryPath = directoryPath; + public List files = new List(); + + public IEnumerable GetFiles() + { + return files; + } } - public VolumeDataset Import() + private string directoryPath; + private HashSet supportedImageTypes = new HashSet { - if (!Directory.Exists(directoryPath)) - throw new NullReferenceException("No directory found: " + directoryPath); + ".png", + ".jpg", + ".jpeg" + }; - List imagePaths = GetSortedImagePaths(); + public IEnumerable LoadSeries(IEnumerable files) + { + Dictionary sequenceByFiletype = new Dictionary(); + foreach(string filePath in files) + { + string fileExt = Path.GetExtension(filePath).ToLower(); + if (supportedImageTypes.Contains(fileExt)) + { + if (!sequenceByFiletype.ContainsKey(fileExt)) + sequenceByFiletype[fileExt] = new ImageSequenceSeries(); - Vector3Int dimensions = GetVolumeDimensions(imagePaths); - int[] data = FillSequentialData(dimensions, imagePaths); - VolumeDataset dataset = FillVolumeDataset(data, dimensions); + ImageSequenceFile imgSeqFile = new ImageSequenceFile(); + imgSeqFile.filePath = filePath; + sequenceByFiletype[fileExt].files.Add(imgSeqFile); + } + } - dataset.FixDimensions(); + if (sequenceByFiletype.Count == 0) + Debug.LogError("Found no image files of supported formats. Currently supported formats are: " + supportedImageTypes.ToString()); - return dataset; + return sequenceByFiletype.Select(f => f.Value).ToList(); } - /// - /// Gets every file path in the directory with a supported suffix. - /// - /// /// A sorted list of image file paths. - private List GetSortedImagePaths() + public VolumeDataset ImportSeries(IImageSequenceSeries series) { - var imagePaths = new List(); + List imagePaths = series.GetFiles().Select(f => f.GetFilePath()).ToList(); - foreach (var type in supportedImageTypes) - { - imagePaths.AddRange(Directory.GetFiles(directoryPath, type)); - } + Vector3Int dimensions = GetVolumeDimensions(imagePaths); + int[] data = FillSequentialData(dimensions, imagePaths); + VolumeDataset dataset = FillVolumeDataset(data, dimensions); - imagePaths.Sort(); + dataset.FixDimensions(); - return imagePaths; + return dataset; } /// diff --git a/Assets/Scripts/Importing/ImageSequenceImporter.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs.meta similarity index 83% rename from Assets/Scripts/Importing/ImageSequenceImporter.cs.meta rename to Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs.meta index b59f9072..15989c19 100644 --- a/Assets/Scripts/Importing/ImageSequenceImporter.cs.meta +++ b/Assets/Scripts/Importing/ImageSequenceImporter/ImageSequenceImporter/ImageSequenceImporter.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6d0af196f3f99ae469267b17c08ad729 +guid: a09b0193701f64f43b73c5762f1edd61 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/Interface.meta b/Assets/Scripts/Importing/ImageSequenceImporter/Interface.meta new file mode 100644 index 00000000..75b8ec32 --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/Interface.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f0a32cb726a32244a9345137d6d65b56 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs new file mode 100644 index 00000000..a62b31db --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace UnityVolumeRendering +{ + public enum ImageSequenceFormat + { + ImageSequence, + DICOM + } + + public interface IImageSequenceFile + { + string GetFilePath(); + } + + public interface IImageSequenceSeries + { + IEnumerable GetFiles(); + } + + /// + /// Importer for image sequence datasets, such as DICOM and image sequences. + /// These datasets usually contain one file per slice. + /// + public interface IImageSequenceImporter + { + /// + /// Read a list of files, and return all image sequence series. + /// Normally a directory will only contain a single series, + /// but if a folder contains multiple series/studies than this function will return all of them. + /// Each series should be imported separately, resulting in one dataset per series. (mostly relevant for DICOM) + /// + /// Files to load. Typically all the files stored in a specific (DICOM) directory. + /// List of image sequence series. + IEnumerable LoadSeries(IEnumerable files); + + /// + /// Import a single image sequence series. + /// + /// The series to import + /// Imported 3D volume dataset. + VolumeDataset ImportSeries(IImageSequenceSeries series); + } +} diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs.meta new file mode 100644 index 00000000..e3a2dbca --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/Interface/IImageSequenceImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34051e0115d64a448baf58b9926ac5e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM.meta b/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM.meta new file mode 100644 index 00000000..4ae231da --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 76f7b47449337ae49998aac9e4649089 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/DICOMImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM/DICOMImporter.cs similarity index 92% rename from Assets/Scripts/Importing/DICOMImporter.cs rename to Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM/DICOMImporter.cs index 53a16d4d..18646240 100644 --- a/Assets/Scripts/Importing/DICOMImporter.cs +++ b/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM/DICOMImporter.cs @@ -14,12 +14,11 @@ namespace UnityVolumeRendering { /// /// DICOM importer. - /// Reads a 3D DICOM dataset from a folder. - /// The folder needs to contain several .dcm/.dicom files, where each file is a slice of the same dataset. + /// Reads a 3D DICOM dataset from a list of DICOM files. /// - public class DICOMImporter + public class DICOMImporter : IImageSequenceImporter { - public class DICOMSliceFile + public class DICOMSliceFile : IImageSequenceFile { public AcrNemaFile file; public string filePath; @@ -30,25 +29,26 @@ public class DICOMSliceFile public float pixelSpacing = 0.0f; public string seriesUID = ""; public bool missingLocation = false; + + public string GetFilePath() + { + return filePath; + } } - public class DICOMSeries + public class DICOMSeries : IImageSequenceSeries { public List dicomFiles = new List(); - } - private IEnumerable fileCandidates; - private string datasetName; + public IEnumerable GetFiles() + { + return dicomFiles; + } + } private int iFallbackLoc = 0; - public DICOMImporter(IEnumerable files, string name = "DICOM_Dataset") - { - this.fileCandidates = files; - datasetName = name; - } - - public List LoadDICOMSeries() + public IEnumerable LoadSeries(IEnumerable fileCandidates) { DataElementDictionary dataElementDictionary = new DataElementDictionary(); UidDictionary uidDictionary = new UidDictionary(); @@ -79,7 +79,10 @@ public List LoadDICOMSeries() DICOMSliceFile sliceFile = ReadDICOMFile(filePath); if(sliceFile != null) { - files.Add(sliceFile); + if (sliceFile.file.PixelData.IsJpeg) + Debug.LogError("DICOM with JPEG not supported by importer. Please enable SimpleITK from volume rendering import settings."); + else + files.Add(sliceFile); } } @@ -99,9 +102,10 @@ public List LoadDICOMSeries() return new List(seriesByUID.Values); } - public VolumeDataset ImportDICOMSeries(DICOMSeries series) + public VolumeDataset ImportSeries(IImageSequenceSeries series) { - List files = series.dicomFiles; + DICOMSeries dicomSeries = (DICOMSeries)series; + List files = dicomSeries.dicomFiles; // Check if the series is missing the slice location tag bool needsCalcLoc = false; @@ -127,7 +131,7 @@ public VolumeDataset ImportDICOMSeries(DICOMSeries series) // Create dataset VolumeDataset dataset = new VolumeDataset(); - dataset.datasetName = Path.GetFileName(datasetName); + dataset.datasetName = Path.GetFileName(files[0].filePath); dataset.dimX = files[0].file.PixelData.Columns; dataset.dimY = files[0].file.PixelData.Rows; dataset.dimZ = files.Count; diff --git a/Assets/Scripts/Importing/DICOMImporter.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM/DICOMImporter.cs.meta similarity index 100% rename from Assets/Scripts/Importing/DICOMImporter.cs.meta rename to Assets/Scripts/Importing/ImageSequenceImporter/OpenDICOM/DICOMImporter.cs.meta diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK.meta b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK.meta new file mode 100644 index 00000000..285d020a --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 54d52e69554607d468e663d1444a05a9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs new file mode 100644 index 00000000..4c616ee9 --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs @@ -0,0 +1,131 @@ +#if UVR_USE_SIMPLEITK +using UnityEngine; +using System; +using itk.simple; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.IO; + +namespace UnityVolumeRendering +{ + /// + /// SimpleITK-based DICOM importer. + /// Has support for JPEG2000 and more. + /// + public class SimpleITKImageSequenceImporter : 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) + { + 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) + { + ImageSequenceSeries sequenceSeries = (ImageSequenceSeries)series; + if (sequenceSeries.files.Count == 0) + { + Debug.LogError("Empty series. No files to load."); + return null; + } + + ImageSeriesReader reader = new ImageSeriesReader(); + + VectorString dicomNames = new VectorString(); + foreach (var dicomFile in sequenceSeries.files) + dicomNames.Add(dicomFile.filePath); + reader.SetFileNames(dicomNames); + + Image image = reader.Execute(); + + // Cast to 32-bit float + image = SimpleITK.Cast(image, PixelIDValueEnum.sitkFloat32); + + VectorUInt32 size = image.GetSize(); + + int numPixels = 1; + for (int dim = 0; dim < image.GetDimension(); dim++) + numPixels *= (int)size[dim]; + + // Read pixel data + float[] 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(); + + // Create dataset + VolumeDataset volumeDataset = new VolumeDataset(); + volumeDataset.data = pixelData; + volumeDataset.dimX = (int)size[0]; + volumeDataset.dimY = (int)size[1]; + volumeDataset.dimZ = (int)size[2]; + volumeDataset.datasetName = "test"; + volumeDataset.filePath = dicomNames[0]; + volumeDataset.scaleX = (float)(spacing[0] * size[0]); + volumeDataset.scaleY = (float)(spacing[1] * size[1]); + volumeDataset.scaleZ = (float)(spacing[2] * size[2]); + + volumeDataset.FixDimensions(); + + return volumeDataset; + } + } +} +#endif diff --git a/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs.meta b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs.meta new file mode 100644 index 00000000..bb733b67 --- /dev/null +++ b/Assets/Scripts/Importing/ImageSequenceImporter/SimpleITK/SimpleITKImageSequenceImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adbf511b47c12914e980501f59b75e55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/ImporterFactory.cs b/Assets/Scripts/Importing/ImporterFactory.cs new file mode 100644 index 00000000..45f53ecc --- /dev/null +++ b/Assets/Scripts/Importing/ImporterFactory.cs @@ -0,0 +1,103 @@ +using System; +using UnityEngine; + +namespace UnityVolumeRendering +{ + /// + /// Factory for creating importers, for each format. + /// Use this if you only want to import a dataset, without deciding which importer to use. + /// Some dataset formats can be imported using several different importers, in which case this factory will return the best alternative. + /// + public class ImporterFactory + { + /// + /// Create an importer for an image sequence dataset (multiple files) of the specified format. + /// Use this for DICOM and image sequences. + /// + /// Format of the dataset. + /// + public static IImageSequenceImporter CreateImageSequenceImporter(ImageSequenceFormat format) + { + Type importerType = GetImageSequenceImporterType(format); + if (importerType != null) + { + return (IImageSequenceImporter)Activator.CreateInstance(importerType); + } + else + { + Debug.LogError("No supported importer for format: " + format); + return null; + } + } + + /// + /// Create an importer for an image file dataset (single file) of the specified format. + /// Use this for NRRD, NIFTI and VASP/PARCHG. + /// + /// Format of the dataset. + /// + public static IImageFileImporter CreateImageFileImporter(ImageFileFormat format) + { + Type importerType = GetImageFileImporterType(format); + if (importerType != null) + { + return (IImageFileImporter)Activator.CreateInstance(importerType); + } + else + { + Debug.LogError("No supported importer for format: " + format); + return null; + } + } + + private static Type GetImageSequenceImporterType(ImageSequenceFormat format) + { + switch (format) + { + case ImageSequenceFormat.ImageSequence: + { + return typeof(ImageSequenceImporter); + } + case ImageSequenceFormat.DICOM: + { + #if UVR_USE_SIMPLEITK + return typeof(SimpleITKImageSequenceImporter); + #else + return typeof(DICOMImporter); + #endif + } + default: + return null; + } + } + + private static Type GetImageFileImporterType(ImageFileFormat format) + { + switch (format) + { + case ImageFileFormat.VASP: + { + return typeof(ParDatasetImporter); + } + case ImageFileFormat.NRRD: + { + #if UVR_USE_SIMPLEITK + return typeof(SimpleITKImageFileImporter); + #else + return null; + #endif + } + case ImageFileFormat.NIFTI: + { + #if UVR_USE_SIMPLEITK + return typeof(SimpleITKImageFileImporter); + #else + return null; + #endif + } + default: + return null; + } + } + } +} diff --git a/Assets/Scripts/Importing/ImporterFactory.cs.meta b/Assets/Scripts/Importing/ImporterFactory.cs.meta new file mode 100644 index 00000000..cddc3740 --- /dev/null +++ b/Assets/Scripts/Importing/ImporterFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31dab3c456c78ea47bfe755467eaf615 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/Ini.meta b/Assets/Scripts/Importing/Ini.meta new file mode 100644 index 00000000..0e5520a9 --- /dev/null +++ b/Assets/Scripts/Importing/Ini.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0a12b119cb6fa2345b6aecbfa713ffa6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/DatasetIniReader.cs b/Assets/Scripts/Importing/Ini/DatasetIniReader.cs similarity index 100% rename from Assets/Scripts/Importing/DatasetIniReader.cs rename to Assets/Scripts/Importing/Ini/DatasetIniReader.cs diff --git a/Assets/Scripts/Importing/DatasetIniReader.cs.meta b/Assets/Scripts/Importing/Ini/DatasetIniReader.cs.meta similarity index 100% rename from Assets/Scripts/Importing/DatasetIniReader.cs.meta rename to Assets/Scripts/Importing/Ini/DatasetIniReader.cs.meta diff --git a/Assets/Scripts/Importing/RawImporter.meta b/Assets/Scripts/Importing/RawImporter.meta new file mode 100644 index 00000000..e05dab06 --- /dev/null +++ b/Assets/Scripts/Importing/RawImporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a3bdbf5c6d9e2642a32c45e5b4cafe1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Importing/RawDatasetImporter.cs b/Assets/Scripts/Importing/RawImporter/RawDatasetImporter.cs similarity index 97% rename from Assets/Scripts/Importing/RawDatasetImporter.cs rename to Assets/Scripts/Importing/RawImporter/RawDatasetImporter.cs index f895af9f..5f3b1cab 100644 --- a/Assets/Scripts/Importing/RawDatasetImporter.cs +++ b/Assets/Scripts/Importing/RawImporter/RawDatasetImporter.cs @@ -1,182 +1,182 @@ -using System; -using System.IO; -using UnityEngine; - -namespace UnityVolumeRendering -{ - public enum DataContentFormat - { - Int8, - Uint8, - Int16, - Uint16, - Int32, - Uint32 - } - - public enum Endianness - { - LittleEndian, - BigEndian - } - - public class RawDatasetImporter - { - string filePath; - private int dimX; - private int dimY; - private int dimZ; - private DataContentFormat contentFormat; - private Endianness endianness; - private int skipBytes; - public RawDatasetImporter(string filePath, int dimX, int dimY, int dimZ, DataContentFormat contentFormat, Endianness endianness, int skipBytes) - { - this.filePath = filePath; - this.dimX = dimX; - this.dimY = dimY; - this.dimZ = dimZ; - this.contentFormat = contentFormat; - this.endianness = endianness; - this.skipBytes = skipBytes; - } - - public VolumeDataset Import() - { - // Check that the file exists - if (!File.Exists(filePath)) - { - Debug.LogError("The file does not exist: " + filePath); - return null; - } - - FileStream fs = new FileStream(filePath, FileMode.Open); - BinaryReader reader = new BinaryReader(fs); - - // Check that the dimension does not exceed the file size - long expectedFileSize = (long)(dimX * dimY * dimZ) * GetSampleFormatSize(contentFormat) + skipBytes; - if (fs.Length < expectedFileSize) - { - Debug.LogError($"The dimension({dimX}, {dimY}, {dimZ}) exceeds the file size. Expected file size is {expectedFileSize} bytes, while the actual file size is {fs.Length} bytes"); - reader.Close(); - fs.Close(); - return null; - } - - VolumeDataset dataset = new VolumeDataset(); - dataset.datasetName = Path.GetFileName(filePath); - dataset.filePath = filePath; - dataset.dimX = dimX; - dataset.dimY = dimY; - dataset.dimZ = dimZ; - - // Skip header (if any) - if (skipBytes > 0) - reader.ReadBytes(skipBytes); - - int uDimension = dimX * dimY * dimZ; - dataset.data = new float[uDimension]; - - // Read the data/sample values - for (int i = 0; i < uDimension; i++) - { - dataset.data[i] = (float)ReadDataValue(reader); - } - Debug.Log("Loaded dataset in range: " + dataset.GetMinDataValue() + " - " + dataset.GetMaxDataValue()); - - reader.Close(); - fs.Close(); - - dataset.FixDimensions(); - - return dataset; - } - - private int ReadDataValue(BinaryReader reader) - { - switch (contentFormat) - { - case DataContentFormat.Int8: - { - sbyte dataval = reader.ReadSByte(); - return (int)dataval; - } - case DataContentFormat.Int16: - { - short dataval = reader.ReadInt16(); - if (endianness == Endianness.BigEndian) - { - byte[] bytes = BitConverter.GetBytes(dataval); - Array.Reverse(bytes, 0, bytes.Length); - dataval = BitConverter.ToInt16(bytes, 0); - } - return (int)dataval; - } - case DataContentFormat.Int32: - { - int dataval = reader.ReadInt32(); - if (endianness == Endianness.BigEndian) - { - byte[] bytes = BitConverter.GetBytes(dataval); - Array.Reverse(bytes, 0, bytes.Length); - dataval = BitConverter.ToInt32(bytes, 0); - } - return (int)dataval; - } - case DataContentFormat.Uint8: - { - return (int)reader.ReadByte(); - } - case DataContentFormat.Uint16: - { - ushort dataval = reader.ReadUInt16(); - if (endianness == Endianness.BigEndian) - { - byte[] bytes = BitConverter.GetBytes(dataval); - Array.Reverse(bytes, 0, bytes.Length); - dataval = BitConverter.ToUInt16(bytes, 0); - } - return (int)dataval; - } - case DataContentFormat.Uint32: - { - uint dataval = reader.ReadUInt32(); - if (endianness == Endianness.BigEndian) - { - byte[] bytes = BitConverter.GetBytes(dataval); - Array.Reverse(bytes, 0, bytes.Length); - dataval = BitConverter.ToUInt32(bytes, 0); - } - return (int)dataval; - } - default: - throw new NotImplementedException("Unimplemented data content format"); - } - } - - private int GetSampleFormatSize(DataContentFormat format) - { - switch (format) - { - case DataContentFormat.Int8: - return 1; - break; - case DataContentFormat.Uint8: - return 1; - break; - case DataContentFormat.Int16: - return 2; - break; - case DataContentFormat.Uint16: - return 2; - break; - case DataContentFormat.Int32: - return 4; - break; - case DataContentFormat.Uint32: - return 4; - break; - } - throw new NotImplementedException(); - } - } -} +using System; +using System.IO; +using UnityEngine; + +namespace UnityVolumeRendering +{ + public enum DataContentFormat + { + Int8, + Uint8, + Int16, + Uint16, + Int32, + Uint32 + } + + public enum Endianness + { + LittleEndian, + BigEndian + } + + public class RawDatasetImporter + { + string filePath; + private int dimX; + private int dimY; + private int dimZ; + private DataContentFormat contentFormat; + private Endianness endianness; + private int skipBytes; + public RawDatasetImporter(string filePath, int dimX, int dimY, int dimZ, DataContentFormat contentFormat, Endianness endianness, int skipBytes) + { + this.filePath = filePath; + this.dimX = dimX; + this.dimY = dimY; + this.dimZ = dimZ; + this.contentFormat = contentFormat; + this.endianness = endianness; + this.skipBytes = skipBytes; + } + + public VolumeDataset Import() + { + // Check that the file exists + if (!File.Exists(filePath)) + { + Debug.LogError("The file does not exist: " + filePath); + return null; + } + + FileStream fs = new FileStream(filePath, FileMode.Open); + BinaryReader reader = new BinaryReader(fs); + + // Check that the dimension does not exceed the file size + long expectedFileSize = (long)(dimX * dimY * dimZ) * GetSampleFormatSize(contentFormat) + skipBytes; + if (fs.Length < expectedFileSize) + { + Debug.LogError($"The dimension({dimX}, {dimY}, {dimZ}) exceeds the file size. Expected file size is {expectedFileSize} bytes, while the actual file size is {fs.Length} bytes"); + reader.Close(); + fs.Close(); + return null; + } + + VolumeDataset dataset = new VolumeDataset(); + dataset.datasetName = Path.GetFileName(filePath); + dataset.filePath = filePath; + dataset.dimX = dimX; + dataset.dimY = dimY; + dataset.dimZ = dimZ; + + // Skip header (if any) + if (skipBytes > 0) + reader.ReadBytes(skipBytes); + + int uDimension = dimX * dimY * dimZ; + dataset.data = new float[uDimension]; + + // Read the data/sample values + for (int i = 0; i < uDimension; i++) + { + dataset.data[i] = (float)ReadDataValue(reader); + } + Debug.Log("Loaded dataset in range: " + dataset.GetMinDataValue() + " - " + dataset.GetMaxDataValue()); + + reader.Close(); + fs.Close(); + + dataset.FixDimensions(); + + return dataset; + } + + private int ReadDataValue(BinaryReader reader) + { + switch (contentFormat) + { + case DataContentFormat.Int8: + { + sbyte dataval = reader.ReadSByte(); + return (int)dataval; + } + case DataContentFormat.Int16: + { + short dataval = reader.ReadInt16(); + if (endianness == Endianness.BigEndian) + { + byte[] bytes = BitConverter.GetBytes(dataval); + Array.Reverse(bytes, 0, bytes.Length); + dataval = BitConverter.ToInt16(bytes, 0); + } + return (int)dataval; + } + case DataContentFormat.Int32: + { + int dataval = reader.ReadInt32(); + if (endianness == Endianness.BigEndian) + { + byte[] bytes = BitConverter.GetBytes(dataval); + Array.Reverse(bytes, 0, bytes.Length); + dataval = BitConverter.ToInt32(bytes, 0); + } + return (int)dataval; + } + case DataContentFormat.Uint8: + { + return (int)reader.ReadByte(); + } + case DataContentFormat.Uint16: + { + ushort dataval = reader.ReadUInt16(); + if (endianness == Endianness.BigEndian) + { + byte[] bytes = BitConverter.GetBytes(dataval); + Array.Reverse(bytes, 0, bytes.Length); + dataval = BitConverter.ToUInt16(bytes, 0); + } + return (int)dataval; + } + case DataContentFormat.Uint32: + { + uint dataval = reader.ReadUInt32(); + if (endianness == Endianness.BigEndian) + { + byte[] bytes = BitConverter.GetBytes(dataval); + Array.Reverse(bytes, 0, bytes.Length); + dataval = BitConverter.ToUInt32(bytes, 0); + } + return (int)dataval; + } + default: + throw new NotImplementedException("Unimplemented data content format"); + } + } + + private int GetSampleFormatSize(DataContentFormat format) + { + switch (format) + { + case DataContentFormat.Int8: + return 1; + break; + case DataContentFormat.Uint8: + return 1; + break; + case DataContentFormat.Int16: + return 2; + break; + case DataContentFormat.Uint16: + return 2; + break; + case DataContentFormat.Int32: + return 4; + break; + case DataContentFormat.Uint32: + return 4; + break; + } + throw new NotImplementedException(); + } + } +} diff --git a/Assets/Scripts/Importing/RawDatasetImporter.cs.meta b/Assets/Scripts/Importing/RawImporter/RawDatasetImporter.cs.meta similarity index 100% rename from Assets/Scripts/Importing/RawDatasetImporter.cs.meta rename to Assets/Scripts/Importing/RawImporter/RawDatasetImporter.cs.meta diff --git a/Documentation/Importing.md b/Documentation/Importing.md new file mode 100644 index 00000000..f5ab2914 --- /dev/null +++ b/Documentation/Importing.md @@ -0,0 +1,95 @@ +# Importing datasets + +There are 3 types of importers: +- Raw importer + - Used for importing raw binary datasets. + - These datasets can optionally have a header, followed by raw 3D data in various formats (int, uint, etc.). +- Image file importer + - Used for importing a single file dataset. + - Supported formats: VASP, NRRD; NIFTI +- Image sequence importer + - Used for importing sequences datasets, where each slice maybe be stored in a separate file (multiple files per dataset). + +## Raw importer + +The _RawDatasetImporter_ imports raw datasets, where the data is stored sequentially. Some raw datasets contain a header where you can read information about how the data is stored (content format, dimension, etc.), while some datasets expect you to know the layout and format. + +To import a RAW dataset, do the following: + +```csharp +// Create the importer +RawDatasetImporter importer = new RawDatasetImporter(filePath, initData.dimX, initData.dimY, initData.dimZ, initData.format, initData.endianness, initData.bytesToSkip); +// Import the dataset +VolumeDataset dataset = importer.Import(); +// Spawn the object +VolumeObjectFactory.CreateObject(dataset); +``` + +The _RawDatasetImporter_ constructor takes the following parameters: +- filePath: File path to the dataset. +- dimX, dimY, dimZ: The dimension of the dataset. +- contentFormat: The format of the content. Possible values: Int8, Uint8, Int16, Uint16, Int32, Uint32. +- endianness: The byte [endianness](https://en.wikipedia.org/wiki/Endianness) of the dataset. +- skipBytes: Number of bytes to skip before reading the content. This is used in cases where the dataset has a header. Some raw datasets formats store information about the dimension, format and endianness in a header. To import these datasets you can read the header yourself and pass this info to the _RawDatasetImporter_ constructor. The skipBytes parameter should then be equal to the header size. + +All this info can be added to a ".ini"-file, which the importer will use (if it finds any). See the sample files (in the "DataFiles" folder for an example). + +## Image file importer + +To import single-file datasets, such as VASP/PARCHG, NRRD and NIFTI, you can use one of the image file importers. You can manually create an instance of your desired importer, or you can simply use the `ImporterFactory` class, which will select one for your desired file format. + +Example: + +```csharp +IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.NRRD); +VolumeDataset dataset = importer.Import(file); +VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset); +``` + +Possible parameters to _ImporterFactory.CreateImageFileImporter_: +- ImageFileFormat.NRRD (requires [SimpleITK](SimpleITK.md)) +- ImageFileFormat.NIFTI (requires [SimpleITK](SimpleITK.md)) +- ImageFileFormat.VASP + +The available importer implementations are: +- _ParDatasetImporter_: For VASP/PARCHG. +- SimpleITKImageFileImporter: For NRRD and NIFTI. Currently only works on Windows. + +For more information about NRRD and NIFTI support, see the page about [SimpleITK](SimpleITK.md). + +## Image sequence importer + +To import an image sequence dataset, such as DICOM, you can manually create an instance of one of the image sequence importers or simply use the `ImporterFactory` class, which will select one for you. + +Example: + +```csharp +// Get all files in DICOM directory +List filePaths = Directory.GetFiles(dir).ToList(); +// Create importer +IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM); +// Load list of DICOM series (normally just one series) +IEnumerable seriesList = importer.LoadSeries(filePaths); +// There will usually just be one series +foreach(IImageSequenceSeries series in seriesList) +{ + // Import single DICOm series + VolumeDataset dataset = importer.ImportSeries(series); + VolumeObjectFactory.CreateObject(dataset); +} +``` + +These importers can import one or several _series_. In most cases there will only be one series. However, in DICOM each DICOM slice can be associated with a "series". This allows you to store several datasets in the same folder. + +Supported formats: +- ImageSequenceFormat.DICOM +- ImageSequenceFormat.ImageSequence + +The available importer implementations are: +- SimpleITKImageSequenceImporter: For DICOM (see [SimpleITK.md](SimpleITK.md) for more info.) +- DICOMImporter: For DICOM. Uses OpenDICOM library, and works on all platforms. This is the default when SimpleITK is disabled. +- ImageSequenceImporter: For image sequences (directory containing multiple image files, typically JPEG or PNG) + +### Notes about DICOM support + +The SimpleITK-based importer is the recommended way to import DICOM datasets, as it supports JPEG compression. See the [SimpleITK documentation](SimpleITK.md) for information about how to enable it. Once enabled, _ImporterFactory.CreateImageSequenceImporter_ will automatically return an importer of type `SimpleITKImageSequenceImporter`. diff --git a/Documentation/SimpleITK.md b/Documentation/SimpleITK.md new file mode 100644 index 00000000..bdacaf09 --- /dev/null +++ b/Documentation/SimpleITK.md @@ -0,0 +1,25 @@ +# SimpleITK + +**NOTE: The SimpleITK importers currently only work on Windows!** + +SimpleITK is a library that supports a wide range for formats, such as: +- DICOM (with JPEG compression) +- NRRD +- NIFTI + +This project optionally uses SimpleITK for the above formats. There is another fallback DICOM importer, but SimpleITK is a requirement for NRRD and NIFTI. + +Since SimpleITK is a native library, that requires you to download some large binaries for each target platform, it has been disabled by default. + +To enable SimpleITK, you simply have to do the following: +1. In Unity's top toolbar, click "Volume rendering" and then "Settings", to open the settings menu. +2. In the settings menu, click "Enable SimpleITK" + + + + +This will automatically download the SimpleITK binaries, and enable support for SimpleITK in the code. The `ImporterFactory` class will then return the SimpleITK-based importer implementations. + +## Supported platforms + +Currently the SimpleITK integration only works on Windows, because the SimpleITK C# wrapper only has distributed binaries for Windows and no other platforms. However, SimpleITK is a cross platform library. To use it on other platforms you could probably try building [the official C# wrapper](https://github.com/SimpleITK/SimpleITK/tree/master/Wrapping/CSharp) for that platform, or manually download the SimpleITK binaries for that platform and create your own C# wrapper. However, I'll look into distributing binaries for at least Linux (which is what I use as a daily driver). diff --git a/Documentation/img/settings-toolbar.jpg b/Documentation/img/settings-toolbar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5cc53307ec9a19f934969db09bc85f745857ecd GIT binary patch literal 15373 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 literal 0 HcmV?d00001 diff --git a/Documentation/img/settings.jpg b/Documentation/img/settings.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1db4ee8c6fe7f0c7153b11f377a76e05c69b2df GIT binary patch literal 32617 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 1712005c..817c9c8c 100644 --- a/README.md +++ b/README.md @@ -85,32 +85,15 @@ Since VR requires two cameras to render each frame, you can expect worse perform - Disable the DEPTHWRITE_ON shader variant. You can do this from code, or just remove the line "#pragma multi_compile DEPTHWRITE_ON DEPTHWRITE_OFF" in _DirectVolumeRenderingShader.shader_. Note: this will remove depth writing, so you won't be able to intersect multiple datasets. # How to use in your own project -- Create an instance of an importer (for example _RawDatasetImporter_):
-`DatasetImporterBase importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, DataContentFormat.Int16, 6);` +- Create an instance of an importer (Directly, or indirectly using the `ImporterFactory`):
+`IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(ImageFileFormat.NRRD);` (alternatively, use the _DICOMImporter_) - Call the Import()-function, which returns a Dataset:
-`VolumeDataset dataset = importer.Import();` +`VolumeDataset dataset = importer.Import(file);` - Use _VolumeObjectFactory_ to create an object from the dataset:
`VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset);` -See "DatasetImporterEditorWindow.cs" for an example. - -# Explanation of the raw dataset importer: -The _RawDatasetImporter_ imports raw datasets, where the data is stored sequentially. Some raw datasets contain a header where you can read information about how the data is stored (content format, dimension, etc.), while some datasets expect you to know the layout and format. -The importer takes the following parameters: -- filePath: Filepath of the dataset -- dimX: X-dimension (number of samples in the X-axis) -- dimY: Y-dimension -- dimZ: Z-dimension -- contentFormat: Value type of the data (Int8, Uint8, Int16, Uint16, etc..) -- skipBytes: Number of bytes to skip (offset to where the data begins). This is usually the same as the header size, and will be 0 if there is no header. - -All this info can be added to a ".ini"-file, which the importer will use (if it finds any). See the sample files (in the "DataFiles" folder for an example). - -# Todo: -- Improve 2D Transfer Function editor: Better GUI, more shapes (triangles) -- Optimise histogram generation -- Support very large datasets (currently we naively try to create 3D textures with the same dimension as the data) +See the [importer documentation](Documentation/Importing.md) for more detailed information. ![alt tag](Screenshots/slices.gif) ![alt tag](Screenshots/1.png) diff --git a/gitattributes b/gitattributes new file mode 100644 index 00000000..a648fc70 --- /dev/null +++ b/gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.cs text +*.shader text +*.cs text +*.md text diff --git a/scripts/ExportUnityPackage.py b/scripts/ExportUnityPackage.py new file mode 100644 index 00000000..c38fea78 --- /dev/null +++ b/scripts/ExportUnityPackage.py @@ -0,0 +1,29 @@ +import os, shutil, errno + +def copy_filedir(src, dst): + try: + shutil.copytree(src, dst) + except OSError as exc: # python >2.5 + if exc.errno in (errno.ENOTDIR, errno.EINVAL): + shutil.copy(src, dst) + else: raise + +unity_path = "D:/Program Files/UnityEditors/2019.4.35f1/Editor/Unity.exe" # TODO +uvr_project_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir) +export_project_path = "tmp-package-export" + +if os.path.exists(export_project_path): + shutil.rmtree(export_project_path) +os.mkdir(export_project_path) + +assets = ["Assets", "DataFiles", "ACKNOWLEDGEMENTS.txt", "CREDITS.md", "LICENSE", "README.md"] + +for asset in assets: + dest_asset = os.path.join(export_project_path, "Assets", "UnityVolumeRendering", asset) + copy_filedir(asset, dest_asset) + +command_string = "\"{unity_path}\" -batchmode -nographics -projectPath -silent-crashes {project_path} -exportPackage {assets} UnityVolumeRendering.unitypackage -quit".format(unity_path=unity_path, project_path=export_project_path, assets="Assets") +print(command_string) +os.system(command_string) + +shutil.rmtree(export_project_path)