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 00000000..c5cc5330 Binary files /dev/null and b/Documentation/img/settings-toolbar.jpg differ diff --git a/Documentation/img/settings.jpg b/Documentation/img/settings.jpg new file mode 100644 index 00000000..e1db4ee8 Binary files /dev/null and b/Documentation/img/settings.jpg differ 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)