From 45e2a0d2bc649a4ac232b64c3572ccc8ad715673 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Sat, 24 Aug 2024 23:11:32 +0200 Subject: [PATCH 01/13] Support for secondary volume (PET/CT or segmentation) --- .../Editor/VolumeRendererEditorFunctions.cs | 43 +++++++++++++++++++ .../VolumeObject/VolumeRenderedObject.cs | 39 ++++++++++++++--- .../DirectVolumeRenderingShader.shader | 29 +++++++++++++ 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index 90396a3f..f9001009 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -36,6 +36,12 @@ private static void ShowDICOMImporter() DicomImportAsync(true); } + [MenuItem("Volume Rendering/Load dataset/Load PET-CT DICOM")] + private static void ShowPETCTDICOMImporter() + { + PETCTDicomImportAsync(); + } + [MenuItem("Assets/Volume Rendering/Import dataset/Import DICOM")] private static void ImportDICOMAsset() { @@ -79,6 +85,43 @@ private static async void DicomImportAsync(bool spawnInScene) } } + private static async void PETCTDicomImportAsync() + { + string dirCT = EditorUtility.OpenFolderPanel("Select a CT DICOM folder to load", "", ""); + string dirPET = EditorUtility.OpenFolderPanel("Select a PET DICOM folder to load", "", ""); + if (Directory.Exists(dirCT) && Directory.Exists(dirPET)) + { + Debug.Log("Async dataset load. Hold on."); + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) + { + progressHandler.StartStage(0.35f, "Importing CT dataset"); + Task importTaskCT = DicomImportDirectoryAsync(dirCT, progressHandler); + await importTaskCT; + progressHandler.EndStage(); + Debug.Assert(importTaskCT.Result.Length > 0); + progressHandler.StartStage(0.35f, "Importing PET dataset"); + Task importTaskPET = DicomImportDirectoryAsync(dirPET, progressHandler); + await importTaskPET; + progressHandler.EndStage(); + Debug.Assert(importTaskPET.Result.Length > 0); + progressHandler.StartStage(0.3f, "Spawning dataset"); + VolumeDataset datasetCT = importTaskCT.Result[0]; + VolumeDataset datasetPET = importTaskPET.Result[0]; + VolumeRenderedObject objCT = await VolumeObjectFactory.CreateObjectAsync(datasetCT); + VolumeRenderedObject objPET = await VolumeObjectFactory.CreateObjectAsync(datasetPET); + objPET.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + objPET.transferFunction.GenerateTexture(); + progressHandler.EndStage(); + objCT.SetSecondaryVolume(objPET); + objPET.gameObject.SetActive(false); + } + } + else + { + Debug.LogError("Directory doesn't exist"); + } + } + private static async Task DicomImportDirectoryAsync(string dir, ProgressHandler progressHandler) { Debug.Log("Async dataset load. Hold on."); diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index 707f0b48..d7c6b4ba 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -34,6 +34,9 @@ public class VolumeRenderedObject : MonoBehaviour [SerializeField, HideInInspector] private LightSource lightSource; + [SerializeField, HideInInspector] + private VolumeRenderedObject secondaryVolume; + // Minimum and maximum gradient threshold for lighting contribution. Values below min will be unlit, and between min and max will be partly shaded. [SerializeField, HideInInspector] private Vector2 gradientLightingThreshold = new Vector2(0.02f, 0.15f); @@ -82,6 +85,12 @@ public SlicingPlane CreateSlicingPlane() return slicingPlaneComp; } + public void SetSecondaryVolume(VolumeRenderedObject volumeObject) + { + this.secondaryVolume = volumeObject; + UpdateMaterialProperties(); + } + public void SetRenderMode(RenderMode mode) { Task task = SetRenderModeAsync(mode); @@ -317,11 +326,10 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle try { bool useGradientTexture = tfRenderMode == TFRenderMode.TF2D || renderMode == RenderMode.IsosurfaceRendering || lightingEnabled; - Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; Texture3D dataTexture = await dataset.GetDataTextureAsync(progressHandler); - meshRenderer.sharedMaterial.SetTexture("_DataTex", dataTexture); - meshRenderer.sharedMaterial.SetTexture("_GradientTex", gradientTexture); - UpdateMatInternal(); + Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; + Texture3D secondaryDataTexture = await secondaryVolume?.dataset?.GetDataTextureAsync(progressHandler); + UpdateMatInternal(dataTexture, gradientTexture, secondaryDataTexture); } finally { @@ -329,11 +337,28 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle } } - private void UpdateMatInternal() + private void UpdateMatInternal(Texture3D dataTexture, Texture3D gradientTexture, Texture3D secondaryDataTexture) { - if (meshRenderer.sharedMaterial.GetTexture("_DataTex") == null) + if (dataTexture != null) { - meshRenderer.sharedMaterial.SetTexture("_DataTex", dataset.GetDataTexture()); + meshRenderer.sharedMaterial.SetTexture("_DataTex", dataTexture); + } + + if (gradientTexture != null) + { + meshRenderer.sharedMaterial.SetTexture("_GradientTex", gradientTexture); + } + + if (secondaryDataTexture != null) + { + Texture2D secondaryTF = secondaryVolume.transferFunction.GetTexture(); + meshRenderer.sharedMaterial.SetTexture("_SecondaryDataTex", secondaryDataTexture); + meshRenderer.sharedMaterial.SetTexture("_SecondaryTFTex", secondaryTF); + meshRenderer.sharedMaterial.EnableKeyword("SECONDARY_VOLUME_ON"); + } + else + { + meshRenderer.sharedMaterial.DisableKeyword("SECONDARY_VOLUME_ON"); } if (meshRenderer.sharedMaterial.GetTexture("_NoiseTex") == null) diff --git a/Assets/Shaders/DirectVolumeRenderingShader.shader b/Assets/Shaders/DirectVolumeRenderingShader.shader index 37413038..cf99b886 100644 --- a/Assets/Shaders/DirectVolumeRenderingShader.shader +++ b/Assets/Shaders/DirectVolumeRenderingShader.shader @@ -13,6 +13,8 @@ _MinGradient("Gradient visibility threshold", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdStart("Gradient threshold for lighting (end)", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdEnd("Gradient threshold for lighting (start)", Range(0.0, 1.0)) = 0.0 + _SecondaryDataTex ("Secondary Data Texture (Generated)", 3D) = "" {} + _SecondaryTFTex("Transfer Function Texture for secondary volume", 2D) = "" {} [HideInInspector] _ShadowVolumeTextureSize("Shadow volume dimensions", Vector) = (1, 1, 1) [HideInInspector] _TextureSize("Dataset dimensions", Vector) = (1, 1, 1) } @@ -37,6 +39,7 @@ #pragma multi_compile __ RAY_TERMINATE_ON #pragma multi_compile __ USE_MAIN_LIGHT #pragma multi_compile __ CUBIC_INTERPOLATION_ON + #pragma multi_compile __ SECONDARY_VOLUME_ON #pragma vertex vert #pragma fragment frag @@ -76,6 +79,8 @@ sampler2D _NoiseTex; sampler2D _TFTex; sampler3D _ShadowVolume; + sampler3D _SecondaryDataTex; + sampler2D _SecondaryTFTex; float _MinVal; float _MaxVal; @@ -191,6 +196,12 @@ return tex2Dlod(_TFTex, float4(density, gradientMagnitude, 0.0f, 0.0f)); } + // Gets the colour from a secondary 1D Transfer Function (x = density) + float4 getSecondaryTF1DColour(float density) + { + return tex2Dlod(_SecondaryTFTex, float4(density, 0.0f, 0.0f, 0.0f)); + } + // Gets the density at the specified position float getDensity(float3 pos) { @@ -201,6 +212,16 @@ #endif } + // Gets the density of the secondary volume at the specified position + float getSecondaryDensity(float3 pos) + { +#if CUBIC_INTERPOLATION_ON + return interpolateTricubicFast(_SecondaryDataTex, float3(pos.x, pos.y, pos.z), _TextureSize); +#else + return tex3Dlod(_SecondaryDataTex, float4(pos.x, pos.y, pos.z, 0.0f)); +#endif + } + // Gets the density at the specified position, without tricubic interpolation float getDensityNoTricubic(float3 pos) { @@ -323,6 +344,14 @@ continue; #endif +#if SECONDARY_VOLUME_ON + const float density2 = getSecondaryDensity(currPos); + float4 src2 = getSecondaryTF1DColour(density2); + //src.rgb = src.rgb * (1.0 - src2.a) + src2.rgb * src2.a; + src = src2.a > 0.0 ? src2 : src; + //src.a = src2.a > 0.0 ? src.a : 0.0; +#endif + // Calculate gradient (needed for lighting and 2D transfer functions) #if defined(TF2D_ON) || defined(LIGHTING_ON) float3 gradient = getGradient(currPos); From 3b441437d1a5b1c1034243e46f3b39d95170ae24 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Sun, 25 Aug 2024 18:43:39 +0200 Subject: [PATCH 02/13] VolumeRenderedObject inspector: Load PET scan --- .../VolumeRenderedObjectCustomInspector.cs | 34 +++++++++++++++++++ .../Editor/VolumeRendererEditorFunctions.cs | 10 +++--- .../VolumeObject/VolumeRenderedObject.cs | 5 +++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index bd343c52..f83488c5 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -2,6 +2,7 @@ using UnityEditor; using System.Collections.Generic; using System.Threading.Tasks; +using System.IO; namespace UnityVolumeRendering { @@ -11,6 +12,7 @@ public class VolumeRenderedObjectCustomInspector : Editor, IProgressView private bool tfSettings = true; private bool lightSettings = true; private bool otherSettings = true; + private bool secondaryVolumeSettings = true; private float currentProgress = 1.0f; private string currentProgressDescrition = ""; private bool progressDirty = false; @@ -137,6 +139,30 @@ public override void OnInspectorGUI() } } + // Secondary volume + secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "Overlay volume"); + VolumeRenderedObject secondaryObject = volrendObj.GetSecondaryVolume(); + if (secondaryObject == null) + { + if (GUILayout.Button("Load PET data")) + { + ImportPetScan(volrendObj); + } + } + else + { + if (GUILayout.Button("Edit secondary transfer function")) + { + TransferFunctionEditorWindow.ShowWindow(secondaryObject); + } + + if (GUILayout.Button("Remove secondary volume")) + { + volrendObj.SetSecondaryVolume(null); + GameObject.Destroy(secondaryObject.gameObject); + } + } + // Other settings GUILayout.Space(10); otherSettings = EditorGUILayout.Foldout(otherSettings, "Other Settings"); @@ -152,5 +178,13 @@ public override void OnInspectorGUI() volrendObj.SetSamplingRateMultiplier(EditorGUILayout.Slider("Sampling rate multiplier", volrendObj.GetSamplingRateMultiplier(), 0.2f, 2.0f)); } } + private static async void ImportPetScan(VolumeRenderedObject targetObject) + { + VolumeRenderedObject petObject = await VolumeRendererEditorFunctions.DicomImportAsync(true); + petObject.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + petObject.transferFunction.GenerateTexture(); + targetObject.SetSecondaryVolume(petObject); + petObject.gameObject.SetActive(false); + } } } diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index f9001009..f845ab0b 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -33,7 +33,7 @@ private static void ShowDatasetImporter() [MenuItem("Volume Rendering/Load dataset/Load DICOM")] private static void ShowDICOMImporter() { - DicomImportAsync(true); + _ = DicomImportAsync(true); } [MenuItem("Volume Rendering/Load dataset/Load PET-CT DICOM")] @@ -45,11 +45,12 @@ private static void ShowPETCTDICOMImporter() [MenuItem("Assets/Volume Rendering/Import dataset/Import DICOM")] private static void ImportDICOMAsset() { - DicomImportAsync(false); + _ = DicomImportAsync(false); } - private static async void DicomImportAsync(bool spawnInScene) + public static async Task DicomImportAsync(bool spawnInScene) { + VolumeRenderedObject obj = null; string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); if (Directory.Exists(dir)) { @@ -66,7 +67,7 @@ private static async void DicomImportAsync(bool spawnInScene) if (spawnInScene) { VolumeDataset dataset = importTask.Result[i]; - VolumeRenderedObject obj = await VolumeObjectFactory.CreateObjectAsync(dataset); + obj = await VolumeObjectFactory.CreateObjectAsync(dataset); obj.transform.position = new Vector3(i, 0, 0); } else @@ -83,6 +84,7 @@ private static async void DicomImportAsync(bool spawnInScene) { Debug.LogError("Directory doesn't exist: " + dir); } + return obj; } private static async void PETCTDicomImportAsync() diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index d7c6b4ba..45351805 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -85,6 +85,11 @@ public SlicingPlane CreateSlicingPlane() return slicingPlaneComp; } + public VolumeRenderedObject GetSecondaryVolume() + { + return this.secondaryVolume; + } + public void SetSecondaryVolume(VolumeRenderedObject volumeObject) { this.secondaryVolume = volumeObject; From bba1900b29a6f8fc358fd6fb271b6c3b3f77e27e Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Sun, 25 Aug 2024 23:18:22 +0200 Subject: [PATCH 03/13] cleanup --- .../Editor/Utils/EditorDatasetImportUtils.cs | 78 +++++++++++ .../VolumeRenderedObjectCustomInspector.cs | 26 +++- .../Editor/VolumeRendererEditorFunctions.cs | 130 ++---------------- 3 files changed, 109 insertions(+), 125 deletions(-) create mode 100644 Assets/Editor/Utils/EditorDatasetImportUtils.cs diff --git a/Assets/Editor/Utils/EditorDatasetImportUtils.cs b/Assets/Editor/Utils/EditorDatasetImportUtils.cs new file mode 100644 index 00000000..81a085f1 --- /dev/null +++ b/Assets/Editor/Utils/EditorDatasetImportUtils.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace UnityVolumeRendering +{ + public class EditorDatasetImportUtils + { + public static async Task ImportDicomDirectoryAsync(string dir, ProgressHandler progressHandler) + { + Debug.Log("Async dataset load. Hold on."); + + List importedDatasets = new List(); + bool recursive = true; + + // Read all files + IEnumerable fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase)); + + if (!fileCandidates.Any()) + { + if (UnityEditor.EditorUtility.DisplayDialog("Could not find any DICOM files", + $"Failed to find any files with DICOM file extension.{Environment.NewLine}Do you want to include files without DICOM file extension?", "Yes", "No")) + { + fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + } + } + + if (fileCandidates.Any()) + { + progressHandler.StartStage(0.2f, "Loading DICOM series"); + + IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM); + IEnumerable seriesList = await importer.LoadSeriesAsync(fileCandidates, new ImageSequenceImportSettings { progressHandler = progressHandler }); + + progressHandler.EndStage(); + progressHandler.StartStage(0.8f); + + int seriesIndex = 0, numSeries = seriesList.Count(); + foreach (IImageSequenceSeries series in seriesList) + { + progressHandler.StartStage(1.0f / numSeries, $"Importing series {seriesIndex + 1} of {numSeries}"); + VolumeDataset dataset = await importer.ImportSeriesAsync(series, new ImageSequenceImportSettings { progressHandler = progressHandler }); + if (dataset != null) + { + await OptionallyDownscale(dataset); + importedDatasets.Add(dataset); + } + seriesIndex++; + progressHandler.EndStage(); + } + + progressHandler.EndStage(); + } + else + Debug.LogError("Could not find any DICOM files to import."); + + return importedDatasets.ToArray(); + } + + public static async Task OptionallyDownscale(VolumeDataset dataset) + { + if (EditorPrefs.GetBool("DownscaleDatasetPrompt")) + { + 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")) + { + Debug.Log("Async dataset downscale. Hold on."); + await Task.Run(() => dataset.DownScaleData()); + } + } + } + } +} diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index f83488c5..a7cf600d 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -180,11 +180,27 @@ public override void OnInspectorGUI() } private static async void ImportPetScan(VolumeRenderedObject targetObject) { - VolumeRenderedObject petObject = await VolumeRendererEditorFunctions.DicomImportAsync(true); - petObject.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - petObject.transferFunction.GenerateTexture(); - targetObject.SetSecondaryVolume(petObject); - petObject.gameObject.SetActive(false); + string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); + if (Directory.Exists(dir)) + { + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) + { + progressHandler.StartStage(0.7f, "Importing PET dataset"); + Task importTask = EditorDatasetImportUtils.ImportDicomDirectoryAsync(dir, progressHandler); + await importTask; + progressHandler.EndStage(); + + progressHandler.StartStage(0.3f, "Spawning PET dataset"); + Debug.Assert(importTask.Result.Length > 0); + VolumeRenderedObject petObject = await VolumeObjectFactory.CreateObjectAsync(importTask.Result[0]); + petObject.transform.position = targetObject.transform.position; + petObject.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + petObject.transferFunction.GenerateTexture(); + targetObject.SetSecondaryVolume(petObject); + petObject.gameObject.SetActive(false); + progressHandler.EndStage(); + } + } } } } diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index f845ab0b..cd564580 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -33,24 +33,17 @@ private static void ShowDatasetImporter() [MenuItem("Volume Rendering/Load dataset/Load DICOM")] private static void ShowDICOMImporter() { - _ = DicomImportAsync(true); - } - - [MenuItem("Volume Rendering/Load dataset/Load PET-CT DICOM")] - private static void ShowPETCTDICOMImporter() - { - PETCTDicomImportAsync(); + DicomImportAsync(true); } [MenuItem("Assets/Volume Rendering/Import dataset/Import DICOM")] private static void ImportDICOMAsset() { - _ = DicomImportAsync(false); + DicomImportAsync(false); } - public static async Task DicomImportAsync(bool spawnInScene) + private static async void DicomImportAsync(bool spawnInScene) { - VolumeRenderedObject obj = null; string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); if (Directory.Exists(dir)) { @@ -58,7 +51,7 @@ public static async Task DicomImportAsync(bool spawnInScen using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) { progressHandler.StartStage(0.7f, "Importing dataset"); - Task importTask = DicomImportDirectoryAsync(dir, progressHandler); + Task importTask = EditorDatasetImportUtils.ImportDicomDirectoryAsync(dir, progressHandler); await importTask; progressHandler.EndStage(); progressHandler.StartStage(0.3f, "Spawning dataset"); @@ -67,7 +60,7 @@ public static async Task DicomImportAsync(bool spawnInScen if (spawnInScene) { VolumeDataset dataset = importTask.Result[i]; - obj = await VolumeObjectFactory.CreateObjectAsync(dataset); + VolumeRenderedObject obj = await VolumeObjectFactory.CreateObjectAsync(dataset); obj.transform.position = new Vector3(i, 0, 0); } else @@ -84,96 +77,6 @@ public static async Task DicomImportAsync(bool spawnInScen { Debug.LogError("Directory doesn't exist: " + dir); } - return obj; - } - - private static async void PETCTDicomImportAsync() - { - string dirCT = EditorUtility.OpenFolderPanel("Select a CT DICOM folder to load", "", ""); - string dirPET = EditorUtility.OpenFolderPanel("Select a PET DICOM folder to load", "", ""); - if (Directory.Exists(dirCT) && Directory.Exists(dirPET)) - { - Debug.Log("Async dataset load. Hold on."); - using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) - { - progressHandler.StartStage(0.35f, "Importing CT dataset"); - Task importTaskCT = DicomImportDirectoryAsync(dirCT, progressHandler); - await importTaskCT; - progressHandler.EndStage(); - Debug.Assert(importTaskCT.Result.Length > 0); - progressHandler.StartStage(0.35f, "Importing PET dataset"); - Task importTaskPET = DicomImportDirectoryAsync(dirPET, progressHandler); - await importTaskPET; - progressHandler.EndStage(); - Debug.Assert(importTaskPET.Result.Length > 0); - progressHandler.StartStage(0.3f, "Spawning dataset"); - VolumeDataset datasetCT = importTaskCT.Result[0]; - VolumeDataset datasetPET = importTaskPET.Result[0]; - VolumeRenderedObject objCT = await VolumeObjectFactory.CreateObjectAsync(datasetCT); - VolumeRenderedObject objPET = await VolumeObjectFactory.CreateObjectAsync(datasetPET); - objPET.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - objPET.transferFunction.GenerateTexture(); - progressHandler.EndStage(); - objCT.SetSecondaryVolume(objPET); - objPET.gameObject.SetActive(false); - } - } - else - { - Debug.LogError("Directory doesn't exist"); - } - } - - private static async Task DicomImportDirectoryAsync(string dir, ProgressHandler progressHandler) - { - Debug.Log("Async dataset load. Hold on."); - - List importedDatasets = new List(); - bool recursive = true; - - // Read all files - IEnumerable fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) - .Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase)); - - if (!fileCandidates.Any()) - { - if (UnityEditor.EditorUtility.DisplayDialog("Could not find any DICOM files", - $"Failed to find any files with DICOM file extension.{Environment.NewLine}Do you want to include files without DICOM file extension?", "Yes", "No")) - { - fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - } - } - - if (fileCandidates.Any()) - { - progressHandler.StartStage(0.2f, "Loading DICOM series"); - - IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM); - IEnumerable seriesList = await importer.LoadSeriesAsync(fileCandidates, new ImageSequenceImportSettings { progressHandler = progressHandler }); - - progressHandler.EndStage(); - progressHandler.StartStage(0.8f); - - int seriesIndex = 0, numSeries = seriesList.Count(); - foreach (IImageSequenceSeries series in seriesList) - { - progressHandler.StartStage(1.0f / numSeries, $"Importing series {seriesIndex + 1} of {numSeries}"); - VolumeDataset dataset = await importer.ImportSeriesAsync(series, new ImageSequenceImportSettings { progressHandler = progressHandler }); - if (dataset != null) - { - await OptionallyDownscale(dataset); - importedDatasets.Add(dataset); - } - seriesIndex++; - progressHandler.EndStage(); - } - - progressHandler.EndStage(); - } - else - Debug.LogError("Could not find any DICOM files to import."); - - return importedDatasets.ToArray(); } [MenuItem("Volume Rendering/Load dataset/Load NRRD dataset")] @@ -214,7 +117,7 @@ private static async void ImportNRRDDatasetAsync(bool spawnInScene) progressHandler.ReportProgress(0.8f, "Creating object"); if (dataset != null) { - await OptionallyDownscale(dataset); + await EditorDatasetImportUtils.OptionallyDownscale(dataset); if (spawnInScene) { await VolumeObjectFactory.CreateObjectAsync(dataset); @@ -266,7 +169,7 @@ private static async void ImportNIFTIDatasetAsync(bool spawnInScene) if (dataset != null) { - await OptionallyDownscale(dataset); + await EditorDatasetImportUtils.OptionallyDownscale(dataset); if (spawnInScene) { await VolumeObjectFactory.CreateObjectAsync(dataset); @@ -318,7 +221,7 @@ private static async void ImporImageFileDatasetAsync(bool spawnInScene) if (dataset != null) { - await OptionallyDownscale(dataset); + await EditorDatasetImportUtils.OptionallyDownscale(dataset); if (spawnInScene) { await VolumeObjectFactory.CreateObjectAsync(dataset); @@ -370,7 +273,7 @@ private static async void ImportParDatasetAsync(bool spawnInScene) if (dataset != null) { - await OptionallyDownscale(dataset); + await EditorDatasetImportUtils.OptionallyDownscale(dataset); if (spawnInScene) { await VolumeObjectFactory.CreateObjectAsync(dataset); @@ -422,7 +325,7 @@ private static async void ImportSequenceAsync() VolumeDataset dataset = await importer.ImportSeriesAsync(series); if (dataset != null) { - await OptionallyDownscale(dataset); + await EditorDatasetImportUtils.OptionallyDownscale(dataset); await VolumeObjectFactory.CreateObjectAsync(dataset); } } @@ -433,19 +336,6 @@ private static async void ImportSequenceAsync() } } - private static async Task OptionallyDownscale(VolumeDataset dataset) - { - if (EditorPrefs.GetBool("DownscaleDatasetPrompt")) - { - 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")) - { - Debug.Log("Async dataset downscale. Hold on."); - await Task.Run(() => dataset.DownScaleData()); - } - } - } - [MenuItem("Volume Rendering/Cross section/Cross section plane")] private static void OnMenuItemClick() { From 26c4491857e2037c48726ab237dc7cf4ec39c650 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Mon, 26 Aug 2024 20:44:23 +0200 Subject: [PATCH 04/13] improvements --- Assets/Editor/TransferFunctionEditorWindow.cs | 34 +++++++--- .../VolumeRenderedObjectCustomInspector.cs | 62 ++++++++++++++----- .../GUI/IMGUI/TransferFunctionEditor.cs | 52 ++++++++++++---- .../Utilities/DatasetFormatUtilities.cs | 21 +++++++ .../VolumeObject/VolumeRenderedObject.cs | 28 ++++++--- 5 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs diff --git a/Assets/Editor/TransferFunctionEditorWindow.cs b/Assets/Editor/TransferFunctionEditorWindow.cs index e1bd274e..d35be9db 100644 --- a/Assets/Editor/TransferFunctionEditorWindow.cs +++ b/Assets/Editor/TransferFunctionEditorWindow.cs @@ -11,6 +11,8 @@ public class TransferFunctionEditorWindow : EditorWindow private TransferFunctionEditor tfEditor = new TransferFunctionEditor(); + private bool keepTf = false; + public static void ShowWindow(VolumeRenderedObject volRendObj) { // Close all (if any) 2D TF editor windows @@ -25,6 +27,21 @@ public static void ShowWindow(VolumeRenderedObject volRendObj) wnd.SetInitialPosition(); } + public static void ShowWindow(VolumeRenderedObject volRendObj, TransferFunction transferFunction) + { + // Close all (if any) 2D TF editor windows + TransferFunction2DEditorWindow[] tf2dWnds = Resources.FindObjectsOfTypeAll(); + foreach (TransferFunction2DEditorWindow tf2dWnd in tf2dWnds) + tf2dWnd.Close(); + + TransferFunctionEditorWindow wnd = (TransferFunctionEditorWindow)EditorWindow.GetWindow(typeof(TransferFunctionEditorWindow)); + wnd.volRendObject = volRendObj; + wnd.tf = transferFunction; + wnd.keepTf = true; + wnd.Show(); + wnd.SetInitialPosition(); + } + private void SetInitialPosition() { Rect rect = this.position; @@ -48,8 +65,9 @@ private void OnGUI() if (volRendObject == null) return; - - tf = volRendObject.transferFunction; + + if (!keepTf) + tf = volRendObject.transferFunction; Event currentEvent = new Event(Event.current); @@ -62,7 +80,7 @@ private void OnGUI() Rect outerRect = new Rect(0.0f, 0.0f, contentWidth, contentHeight); Rect tfEditorRect = new Rect(outerRect.x + 20.0f, outerRect.y + 20.0f, outerRect.width - 40.0f, outerRect.height - 50.0f); - tfEditor.SetVolumeObject(volRendObject); + tfEditor.SetTarget(volRendObject.dataset, tf); tfEditor.DrawOnGUI(tfEditorRect); // Draw horizontal zoom slider @@ -99,8 +117,9 @@ private void OnGUI() TransferFunction newTF = TransferFunctionDatabase.LoadTransferFunction(filepath); if(newTF != null) { - tf = newTF; - volRendObject.SetTransferFunction(tf); + tf.alphaControlPoints = newTF.alphaControlPoints; + tf.colourControlPoints = newTF.colourControlPoints; + tf.GenerateTexture(); tfEditor.ClearSelection(); } } @@ -108,11 +127,12 @@ private void OnGUI() // Clear TF if(GUI.Button(new Rect(tfEditorRect.x + 150.0f, tfEditorRect.y + tfEditorRect.height + 20.0f, 70.0f, 30.0f), "Clear")) { - tf = ScriptableObject.CreateInstance(); + tf.alphaControlPoints.Clear(); + tf.colourControlPoints.Clear(); tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.2f, 0.0f)); tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.8f, 1.0f)); tf.colourControlPoints.Add(new TFColourControlPoint(0.5f, new Color(0.469f, 0.354f, 0.223f, 1.0f))); - volRendObject.SetTransferFunction(tf); + tf.GenerateTexture(); tfEditor.ClearSelection(); } diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index a7cf600d..ae52b99d 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -141,25 +141,29 @@ public override void OnInspectorGUI() // Secondary volume secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "Overlay volume"); - VolumeRenderedObject secondaryObject = volrendObj.GetSecondaryVolume(); - if (secondaryObject == null) + VolumeDataset secondaryDataset = volrendObj.GetSecondaryDataset(); + TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction(); + if (secondaryDataset == null) { - if (GUILayout.Button("Load PET data")) + if (GUILayout.Button("Load PET (NRRD, NIFTI)")) { ImportPetScan(volrendObj); } + if (GUILayout.Button("Load PET (DICOM)")) + { + ImportPetScanDicom(volrendObj); + } } else { if (GUILayout.Button("Edit secondary transfer function")) { - TransferFunctionEditorWindow.ShowWindow(secondaryObject); + TransferFunctionEditorWindow.ShowWindow(volrendObj, secondaryTransferFunction); } if (GUILayout.Button("Remove secondary volume")) { - volrendObj.SetSecondaryVolume(null); - GameObject.Destroy(secondaryObject.gameObject); + volrendObj.SetSecondaryDataset(null); } } @@ -179,26 +183,54 @@ public override void OnInspectorGUI() } } private static async void ImportPetScan(VolumeRenderedObject targetObject) + { + string filePath = EditorUtility.OpenFilePanel("Select a folder to load", "", ""); + ImageFileFormat imageFileFormat = DatasetFormatUtilities.GetImageFileFormat(filePath); + if (!File.Exists(filePath)) + { + Debug.LogError($"File doesn't exist: {filePath}"); + return; + } + if (imageFileFormat == ImageFileFormat.Unknown) + { + Debug.LogError($"Invalid file format: {Path.GetExtension(filePath)}"); + return; + } + + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) + { + progressHandler.StartStage(1.0f, "Importing PET dataset"); + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imageFileFormat); + Task importTask = importer.ImportAsync(filePath); + await importTask; + progressHandler.EndStage(); + + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + targetObject.SetSecondaryDataset(importTask.Result); + targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); + } + } + + private static async void ImportPetScanDicom(VolumeRenderedObject targetObject) { string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); if (Directory.Exists(dir)) { using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) { - progressHandler.StartStage(0.7f, "Importing PET dataset"); + progressHandler.StartStage(1.0f, "Importing PET dataset"); Task importTask = EditorDatasetImportUtils.ImportDicomDirectoryAsync(dir, progressHandler); await importTask; progressHandler.EndStage(); - progressHandler.StartStage(0.3f, "Spawning PET dataset"); Debug.Assert(importTask.Result.Length > 0); - VolumeRenderedObject petObject = await VolumeObjectFactory.CreateObjectAsync(importTask.Result[0]); - petObject.transform.position = targetObject.transform.position; - petObject.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - petObject.transferFunction.GenerateTexture(); - targetObject.SetSecondaryVolume(petObject); - petObject.gameObject.SetActive(false); - progressHandler.EndStage(); + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + targetObject.SetSecondaryDataset(importTask.Result[0]); + targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); } } } diff --git a/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs b/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs index 9ca12f53..44f9418c 100644 --- a/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs +++ b/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs @@ -1,3 +1,4 @@ +using System; using UnityEngine; namespace UnityVolumeRendering @@ -8,7 +9,9 @@ public class TransferFunctionEditor private int movingAlphaPointIndex = -1; private int selectedColPointIndex = -1; - private VolumeRenderedObject volRendObject = null; + private VolumeRenderedObject targetObject = null; + private VolumeDataset dataset = null; + private TransferFunction transferFunction = null; private Texture2D histTex = null; private Material tfGUIMat = null; @@ -28,19 +31,42 @@ public void Initialise() tfPaletteGUIMat = Resources.Load("TransferFunctionPaletteGUIMat"); } + [Obsolete("Use SetTarget instead")] public void SetVolumeObject(VolumeRenderedObject volRendObject) { - this.volRendObject = volRendObject; + SetTarget(volRendObject); + } + + public void SetTarget(VolumeRenderedObject volRendObject) + { + this.targetObject = volRendObject; + this.dataset = volRendObject.dataset; + this.transferFunction = volRendObject.transferFunction; + } + + public void SetTarget(VolumeDataset dataset, TransferFunction transferFunction) + { + this.targetObject = null; + this.dataset = dataset; + this.transferFunction = transferFunction; } public void DrawOnGUI(Rect rect) { GUI.skin.button.alignment = TextAnchor.MiddleCenter; - if (volRendObject == null) + if (targetObject != null) + { + dataset = targetObject.dataset; + transferFunction = targetObject.transferFunction; + } + + if (dataset == null || transferFunction == null) + { return; + } - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = this.transferFunction; Event currentEvent = Event.current; @@ -67,9 +93,9 @@ public void DrawOnGUI(Rect rect) if(histTex == null) { if(SystemInfo.supportsComputeShaders) - histTex = HistogramTextureGenerator.GenerateHistogramTextureOnGPU(volRendObject.dataset); + histTex = HistogramTextureGenerator.GenerateHistogramTextureOnGPU(dataset); else - histTex = HistogramTextureGenerator.GenerateHistogramTexture(volRendObject.dataset); + histTex = HistogramTextureGenerator.GenerateHistogramTexture(dataset); } // Draw histogram @@ -209,7 +235,7 @@ public void DrawOnGUI(Rect rect) } else { - float hue = Random.Range(0.0f, 1.0f); + float hue = UnityEngine.Random.Range(0.0f, 1.0f); Color newColour = Color.HSVToRGB(hue, 1.0f, 1.0f); tf.colourControlPoints.Add(new TFColourControlPoint(Mathf.Clamp(mousePos.x, 0.0f, 1.0f), newColour)); } @@ -232,7 +258,7 @@ public void ClearSelection() public Color? GetSelectedColour() { if (selectedColPointIndex != -1) - return volRendObject.transferFunction.colourControlPoints[selectedColPointIndex].colourValue; + return transferFunction.colourControlPoints[selectedColPointIndex].colourValue; else return null; } @@ -241,9 +267,9 @@ public void SetSelectedColour(Color colour) { if (selectedColPointIndex != -1) { - TFColourControlPoint colPoint = volRendObject.transferFunction.colourControlPoints[selectedColPointIndex]; + TFColourControlPoint colPoint = transferFunction.colourControlPoints[selectedColPointIndex]; colPoint.colourValue = colour; - volRendObject.transferFunction.colourControlPoints[selectedColPointIndex] = colPoint; + transferFunction.colourControlPoints[selectedColPointIndex] = colPoint; } } @@ -251,7 +277,7 @@ public void RemoveSelectedColour() { if (selectedColPointIndex != -1) { - volRendObject.transferFunction.colourControlPoints.RemoveAt(selectedColPointIndex); + transferFunction.colourControlPoints.RemoveAt(selectedColPointIndex); selectedColPointIndex = -1; } } @@ -289,7 +315,7 @@ private void HandleZoom(float zoomDelta, Vector2 zoomTarget) /// Threshold for maximum distance. Points further away than this won't get picked. private int PickColourControlPoint(float position, float maxDistance = 0.03f) { - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.colourControlPoints.Count; i++) @@ -312,7 +338,7 @@ private int PickColourControlPoint(float position, float maxDistance = 0.03f) private int PickAlphaControlPoint(Vector2 position, float maxDistance = 0.05f) { Vector2 distMultiplier = new Vector2(1.0f / zoomRect.width, 1.0f / zoomRect.height); - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.alphaControlPoints.Count; i++) diff --git a/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs b/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs new file mode 100644 index 00000000..b4d7798b --- /dev/null +++ b/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs @@ -0,0 +1,21 @@ +namespace UnityVolumeRendering +{ + public class DatasetFormatUtilities + { + public static ImageFileFormat GetImageFileFormat(string filePath) + { + string extension = System.IO.Path.GetExtension(filePath); + switch (extension) + { + case ".nrrd": + return ImageFileFormat.NRRD; + case ".vasp": + return ImageFileFormat.VASP; + case ".nii": + return ImageFileFormat.NIFTI; + default: + return ImageFileFormat.Unknown; + } + } + } +} diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index 45351805..b38101cb 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -35,7 +35,10 @@ public class VolumeRenderedObject : MonoBehaviour private LightSource lightSource; [SerializeField, HideInInspector] - private VolumeRenderedObject secondaryVolume; + private VolumeDataset secondaryDataset; + + [SerializeField, HideInInspector] + private TransferFunction secondaryTransferFunction; // Minimum and maximum gradient threshold for lighting contribution. Values below min will be unlit, and between min and max will be partly shaded. [SerializeField, HideInInspector] @@ -85,14 +88,25 @@ public SlicingPlane CreateSlicingPlane() return slicingPlaneComp; } - public VolumeRenderedObject GetSecondaryVolume() + public VolumeDataset GetSecondaryDataset() + { + return this.secondaryDataset; + } + + public TransferFunction GetSecondaryTransferFunction() { - return this.secondaryVolume; + return this.secondaryTransferFunction; + } + + public void SetSecondaryDataset(VolumeDataset dataset) + { + this.secondaryDataset = dataset; + UpdateMaterialProperties(); } - public void SetSecondaryVolume(VolumeRenderedObject volumeObject) + public void SetSecondaryTransferFunction(TransferFunction tf) { - this.secondaryVolume = volumeObject; + this.secondaryTransferFunction = tf; UpdateMaterialProperties(); } @@ -333,7 +347,7 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle bool useGradientTexture = tfRenderMode == TFRenderMode.TF2D || renderMode == RenderMode.IsosurfaceRendering || lightingEnabled; Texture3D dataTexture = await dataset.GetDataTextureAsync(progressHandler); Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; - Texture3D secondaryDataTexture = await secondaryVolume?.dataset?.GetDataTextureAsync(progressHandler); + Texture3D secondaryDataTexture = await secondaryDataset?.GetDataTextureAsync(progressHandler); UpdateMatInternal(dataTexture, gradientTexture, secondaryDataTexture); } finally @@ -356,7 +370,7 @@ private void UpdateMatInternal(Texture3D dataTexture, Texture3D gradientTexture, if (secondaryDataTexture != null) { - Texture2D secondaryTF = secondaryVolume.transferFunction.GetTexture(); + Texture2D secondaryTF = secondaryTransferFunction.GetTexture(); meshRenderer.sharedMaterial.SetTexture("_SecondaryDataTex", secondaryDataTexture); meshRenderer.sharedMaterial.SetTexture("_SecondaryTFTex", secondaryTF); meshRenderer.sharedMaterial.EnableKeyword("SECONDARY_VOLUME_ON"); From ac33fa6fffe99fb7ed10b78f53affbfeb8fc346c Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Mon, 26 Aug 2024 20:46:45 +0200 Subject: [PATCH 05/13] fix --- Assets/Scripts/VolumeObject/VolumeRenderedObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index b38101cb..f0bb535f 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -347,7 +347,7 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle bool useGradientTexture = tfRenderMode == TFRenderMode.TF2D || renderMode == RenderMode.IsosurfaceRendering || lightingEnabled; Texture3D dataTexture = await dataset.GetDataTextureAsync(progressHandler); Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; - Texture3D secondaryDataTexture = await secondaryDataset?.GetDataTextureAsync(progressHandler); + Texture3D secondaryDataTexture = secondaryDataset ? await secondaryDataset?.GetDataTextureAsync(progressHandler) : null; UpdateMatInternal(dataTexture, gradientTexture, secondaryDataTexture); } finally From cb9c92df04058067766e263bd549d82ad802ffbf Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Mon, 26 Aug 2024 21:50:29 +0200 Subject: [PATCH 06/13] segmentations WIP --- .../VolumeRenderedObjectCustomInspector.cs | 63 +++++++++++- .../Utilities/DatasetFormatUtilities.cs | 3 +- Assets/Scripts/VolumeData/VolumeDataset.cs | 10 ++ .../VolumeObject/VolumeRenderedObject.cs | 96 ++++++++++++++++++- 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index ae52b99d..0a659ebb 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -13,6 +13,7 @@ public class VolumeRenderedObjectCustomInspector : Editor, IProgressView private bool lightSettings = true; private bool otherSettings = true; private bool secondaryVolumeSettings = true; + private bool segmentationSettings = true; private float currentProgress = 1.0f; private string currentProgressDescrition = ""; private bool progressDirty = false; @@ -140,7 +141,7 @@ public override void OnInspectorGUI() } // Secondary volume - secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "Overlay volume"); + secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "PET/overlay volume"); VolumeDataset secondaryDataset = volrendObj.GetSecondaryDataset(); TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction(); if (secondaryDataset == null) @@ -167,6 +168,39 @@ public override void OnInspectorGUI() } } + // Segmentations + segmentationSettings = EditorGUILayout.Foldout(segmentationSettings, "Segmentations"); + List segmentationLabels = volrendObj.GetSegmentationLabels(); + if (segmentationLabels != null && segmentationLabels.Count > 0) + { + for (int i = 0; i < segmentationLabels.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + SegmentationLabel segmentationlabel = segmentationLabels[i]; + segmentationlabel.name = EditorGUILayout.TextField(segmentationlabel.name); + segmentationlabel.colour = EditorGUILayout.ColorField(segmentationlabel.colour); + segmentationLabels[i] = segmentationlabel; + if (GUILayout.Button("delete")) + { + segmentationLabels.RemoveAt(i); + volrendObj.UpdateSegmentationLabels(); + } + if (GUILayout.Button("test")) + { + volrendObj.UpdateSegmentationLabels(); + } + EditorGUILayout.EndHorizontal(); + } + } + if (GUILayout.Button("Add segmentation (NRRD, NIFTI)")) + { + ImportSegmentation(volrendObj); + } + /*if (GUILayout.Button("Add segmentation (DICOM)")) + { + ImportSegmentationDicom(volrendObj); + }*/ + // Other settings GUILayout.Space(10); otherSettings = EditorGUILayout.Foldout(otherSettings, "Other Settings"); @@ -234,5 +268,32 @@ private static async void ImportPetScanDicom(VolumeRenderedObject targetObject) } } } + + private static async void ImportSegmentation(VolumeRenderedObject targetObject) + { + string filePath = EditorUtility.OpenFilePanel("Select a folder to load", "", ""); + ImageFileFormat imageFileFormat = DatasetFormatUtilities.GetImageFileFormat(filePath); + if (!File.Exists(filePath)) + { + Debug.LogError($"File doesn't exist: {filePath}"); + return; + } + if (imageFileFormat == ImageFileFormat.Unknown) + { + Debug.LogError($"Invalid file format: {Path.GetExtension(filePath)}"); + return; + } + + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) + { + progressHandler.StartStage(1.0f, "Importing segmentation dataset"); + IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imageFileFormat); + Task importTask = importer.ImportAsync(filePath); + await importTask; + progressHandler.EndStage(); + + targetObject.AddSegmentation(importTask.Result); + } + } } } diff --git a/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs b/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs index b4d7798b..2c1f8c1e 100644 --- a/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs +++ b/Assets/Scripts/Importing/Utilities/DatasetFormatUtilities.cs @@ -12,7 +12,8 @@ public static ImageFileFormat GetImageFileFormat(string filePath) case ".vasp": return ImageFileFormat.VASP; case ".nii": - return ImageFileFormat.NIFTI; + case ".gz": + return filePath.ToLower().EndsWith(".nii.gz") ? ImageFileFormat.NIFTI : ImageFileFormat.Unknown; default: return ImageFileFormat.Unknown; } diff --git a/Assets/Scripts/VolumeData/VolumeDataset.cs b/Assets/Scripts/VolumeData/VolumeDataset.cs index 8b9a8fcb..9fcbc2bb 100644 --- a/Assets/Scripts/VolumeData/VolumeDataset.cs +++ b/Assets/Scripts/VolumeData/VolumeDataset.cs @@ -74,6 +74,11 @@ public Texture3D GetDataTexture() } } + public void RecreateDataTexture() + { + dataTexture = AsyncHelper.RunSync(() => CreateTextureInternalAsync(NullProgressHandler.instance)); + } + /// /// Gets the 3D data texture, containing the density values of the dataset. /// Will create the data texture if it does not exist, without blocking the main thread. @@ -156,6 +161,11 @@ public float GetMaxDataValue() return maxDataValue; } + public void RecalculateBounds() + { + CalculateValueBounds(new NullProgressHandler()); + } + /// /// Ensures that the dataset is not too large. /// This is automatically called during import, diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index f0bb535f..d31d9d02 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -1,9 +1,20 @@ -using System.Threading; +using openDicom.Encoding; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using UnityEngine; namespace UnityVolumeRendering { + [System.Serializable] + public struct SegmentationLabel + { + public int id; + public string name; + public Color colour; + } + [ExecuteInEditMode] public class VolumeRenderedObject : MonoBehaviour { @@ -40,6 +51,9 @@ public class VolumeRenderedObject : MonoBehaviour [SerializeField, HideInInspector] private TransferFunction secondaryTransferFunction; + [SerializeField, HideInInspector] + private List segmentationLabels = new List(); + // Minimum and maximum gradient threshold for lighting contribution. Values below min will be unlit, and between min and max will be partly shaded. [SerializeField, HideInInspector] private Vector2 gradientLightingThreshold = new Vector2(0.02f, 0.15f); @@ -93,20 +107,92 @@ public VolumeDataset GetSecondaryDataset() return this.secondaryDataset; } + public void SetSecondaryDataset(VolumeDataset dataset) + { + this.secondaryDataset = dataset; + UpdateMaterialProperties(); + } + public TransferFunction GetSecondaryTransferFunction() { return this.secondaryTransferFunction; } - public void SetSecondaryDataset(VolumeDataset dataset) + public void SetSecondaryTransferFunction(TransferFunction tf) { - this.secondaryDataset = dataset; + this.secondaryTransferFunction = tf; UpdateMaterialProperties(); } - public void SetSecondaryTransferFunction(TransferFunction tf) + public List GetSegmentationLabels() { - this.secondaryTransferFunction = tf; + return segmentationLabels; + } + + public void AddSegmentation(VolumeDataset dataset) + { + if (dataset.data.Length != secondaryDataset.data.Length) + { + Debug.LogError("Can't add segmentation with different dimension than original dataset."); + return; + } + + int segmentationId = segmentationLabels.Count > 0 ? segmentationLabels.Max(l => l.id) + 1 : 1; + + if (segmentationLabels.Count == 0) + { + secondaryDataset = dataset; + } + else + { + for (int i = 0; i < secondaryDataset.data.Length; i++) + { + secondaryDataset.data[i] = dataset.data[i] > 0.0f ? (float)segmentationId : secondaryDataset.data[i]; + } + secondaryDataset.RecalculateBounds(); + secondaryDataset.RecreateDataTexture(); + secondaryDataset.GetDataTexture().filterMode = FilterMode.Point; + } + SegmentationLabel segmentationLabel = new SegmentationLabel(); + segmentationLabel.id = segmentationId; + segmentationLabel.name = dataset.name; + segmentationLabel.colour = Random.ColorHSV(); + segmentationLabels.Add(segmentationLabel); + UpdateSegmentationLabels(); + } + + public void UpdateSegmentationLabels() + { + if (segmentationLabels.Count == 0) + { + return; + } + + segmentationLabels.OrderBy(l => l.id); + if (secondaryTransferFunction == null) + { + secondaryTransferFunction = ScriptableObject.CreateInstance(); + } + secondaryTransferFunction.alphaControlPoints.Clear(); + secondaryTransferFunction.colourControlPoints.Clear(); + int maxSegmentationId = segmentationLabels[segmentationLabels.Count - 1].id; + float minDataValue = secondaryDataset.GetMinDataValue(); + float maxDataValue = secondaryDataset.GetMaxDataValue(); + secondaryTransferFunction.alphaControlPoints.Add(new TFAlphaControlPoint(0.0f, 0.0f)); + secondaryTransferFunction.alphaControlPoints.Add(new TFAlphaControlPoint(1.0f, 1.0f)); + for (int i = 0; i < segmentationLabels.Count; i++) + { + SegmentationLabel segmentationLabel = segmentationLabels[i]; + float t = segmentationLabel.id / maxDataValue; + secondaryTransferFunction.colourControlPoints.Add(new TFColourControlPoint(t, segmentationLabel.colour)); + if (i == 0) + { + secondaryTransferFunction.alphaControlPoints.Add(new TFAlphaControlPoint(t - 0.01f, 0.0f)); + secondaryTransferFunction.alphaControlPoints.Add(new TFAlphaControlPoint(t, 1.0f)); + } + } + secondaryTransferFunction.GenerateTexture(); + secondaryTransferFunction.GetTexture().filterMode = FilterMode.Point; UpdateMaterialProperties(); } From 66c6e2ccfe3a40df08ca0ea444d24e1cd017d63c Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Mon, 26 Aug 2024 22:10:34 +0200 Subject: [PATCH 07/13] fixes --- Assets/Scripts/VolumeObject/VolumeRenderedObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index d31d9d02..338b3980 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -131,7 +131,7 @@ public List GetSegmentationLabels() public void AddSegmentation(VolumeDataset dataset) { - if (dataset.data.Length != secondaryDataset.data.Length) + if (secondaryDataset != null && dataset.data.Length != secondaryDataset.data.Length) { Debug.LogError("Can't add segmentation with different dimension than original dataset."); return; From 3b32b6037a10b57bcd4afe3ae63800e39a4a4e5e Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Tue, 27 Aug 2024 20:36:46 +0200 Subject: [PATCH 08/13] Segmentation: Isolated rendre mode --- .../VolumeRenderedObjectCustomInspector.cs | 26 +++-- Assets/Scripts/Segmentation/OverlayType.cs | 9 ++ .../Scripts/Segmentation/SegmentationLabel.cs | 12 ++ .../Segmentation/SegmentationRenderMode.cs | 9 ++ .../VolumeObject/VolumeRenderedObject.cs | 104 ++++++++++++++---- 5 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 Assets/Scripts/Segmentation/OverlayType.cs create mode 100644 Assets/Scripts/Segmentation/SegmentationLabel.cs create mode 100644 Assets/Scripts/Segmentation/SegmentationRenderMode.cs diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index 0a659ebb..09e45b37 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -142,9 +142,9 @@ public override void OnInspectorGUI() // Secondary volume secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "PET/overlay volume"); - VolumeDataset secondaryDataset = volrendObj.GetSecondaryDataset(); + OverlayType overlayType = volrendObj.GetOverlayType(); TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction(); - if (secondaryDataset == null) + if (overlayType != OverlayType.Overlay) { if (GUILayout.Button("Load PET (NRRD, NIFTI)")) { @@ -164,7 +164,7 @@ public override void OnInspectorGUI() if (GUILayout.Button("Remove secondary volume")) { - volrendObj.SetSecondaryDataset(null); + volrendObj.SetOverlayDataset(null); } } @@ -177,20 +177,24 @@ public override void OnInspectorGUI() { EditorGUILayout.BeginHorizontal(); SegmentationLabel segmentationlabel = segmentationLabels[i]; + EditorGUI.BeginChangeCheck(); segmentationlabel.name = EditorGUILayout.TextField(segmentationlabel.name); segmentationlabel.colour = EditorGUILayout.ColorField(segmentationlabel.colour); + bool changed = EditorGUI.EndChangeCheck(); segmentationLabels[i] = segmentationlabel; if (GUILayout.Button("delete")) { - segmentationLabels.RemoveAt(i); - volrendObj.UpdateSegmentationLabels(); + volrendObj.RemoveSegmentation(segmentationlabel.id); } - if (GUILayout.Button("test")) + EditorGUILayout.EndHorizontal(); + if (changed) { volrendObj.UpdateSegmentationLabels(); } - EditorGUILayout.EndHorizontal(); } + + SegmentationRenderMode segmentationRendreMode = (SegmentationRenderMode)EditorGUILayout.EnumPopup("Render mode", volrendObj.GetSegmentationRenderMode()); + volrendObj.SetSegmentationRenderMode(segmentationRendreMode); } if (GUILayout.Button("Add segmentation (NRRD, NIFTI)")) { @@ -200,6 +204,10 @@ public override void OnInspectorGUI() { ImportSegmentationDicom(volrendObj); }*/ + if (GUILayout.Button("Clear segmentations")) + { + volrendObj.ClearSegmentations(); + } // Other settings GUILayout.Space(10); @@ -242,7 +250,7 @@ private static async void ImportPetScan(VolumeRenderedObject targetObject) TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; secondaryTransferFunction.GenerateTexture(); - targetObject.SetSecondaryDataset(importTask.Result); + targetObject.SetOverlayDataset(importTask.Result); targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); } } @@ -263,7 +271,7 @@ private static async void ImportPetScanDicom(VolumeRenderedObject targetObject) TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; secondaryTransferFunction.GenerateTexture(); - targetObject.SetSecondaryDataset(importTask.Result[0]); + targetObject.SetOverlayDataset(importTask.Result[0]); targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); } } diff --git a/Assets/Scripts/Segmentation/OverlayType.cs b/Assets/Scripts/Segmentation/OverlayType.cs new file mode 100644 index 00000000..77c20850 --- /dev/null +++ b/Assets/Scripts/Segmentation/OverlayType.cs @@ -0,0 +1,9 @@ +namespace UnityVolumeRendering +{ + public enum OverlayType + { + None, + Overlay, + Segmentation + } +} diff --git a/Assets/Scripts/Segmentation/SegmentationLabel.cs b/Assets/Scripts/Segmentation/SegmentationLabel.cs new file mode 100644 index 00000000..bc5f0363 --- /dev/null +++ b/Assets/Scripts/Segmentation/SegmentationLabel.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace UnityVolumeRendering +{ + [System.Serializable] + public struct SegmentationLabel + { + public int id; + public string name; + public Color colour; + } +} diff --git a/Assets/Scripts/Segmentation/SegmentationRenderMode.cs b/Assets/Scripts/Segmentation/SegmentationRenderMode.cs new file mode 100644 index 00000000..d59068cf --- /dev/null +++ b/Assets/Scripts/Segmentation/SegmentationRenderMode.cs @@ -0,0 +1,9 @@ +namespace UnityVolumeRendering +{ + [System.Serializable] + public enum SegmentationRenderMode + { + OverlayColour, + Isolate + } +} diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index 338b3980..f87e18b9 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -7,14 +7,6 @@ namespace UnityVolumeRendering { - [System.Serializable] - public struct SegmentationLabel - { - public int id; - public string name; - public Color colour; - } - [ExecuteInEditMode] public class VolumeRenderedObject : MonoBehaviour { @@ -54,6 +46,12 @@ public class VolumeRenderedObject : MonoBehaviour [SerializeField, HideInInspector] private List segmentationLabels = new List(); + [SerializeField, HideInInspector] + private OverlayType overlayType = OverlayType.None; + + [SerializeField, HideInInspector] + private SegmentationRenderMode segmentationRenderMode = SegmentationRenderMode.OverlayColour; + // Minimum and maximum gradient threshold for lighting contribution. Values below min will be unlit, and between min and max will be partly shaded. [SerializeField, HideInInspector] private Vector2 gradientLightingThreshold = new Vector2(0.02f, 0.15f); @@ -102,15 +100,9 @@ public SlicingPlane CreateSlicingPlane() return slicingPlaneComp; } - public VolumeDataset GetSecondaryDataset() + public OverlayType GetOverlayType() { - return this.secondaryDataset; - } - - public void SetSecondaryDataset(VolumeDataset dataset) - { - this.secondaryDataset = dataset; - UpdateMaterialProperties(); + return this.overlayType; } public TransferFunction GetSecondaryTransferFunction() @@ -124,6 +116,34 @@ public void SetSecondaryTransferFunction(TransferFunction tf) UpdateMaterialProperties(); } + public void SetOverlayDataset(VolumeDataset dataset) + { + if (dataset != null) + { + this.overlayType = OverlayType.Overlay; + } + else if(this.overlayType == OverlayType.Overlay) + { + this.overlayType = OverlayType.None; + } + this.secondaryDataset = dataset; + UpdateMaterialProperties(); + } + + public SegmentationRenderMode GetSegmentationRenderMode() + { + return segmentationRenderMode; + } + + public void SetSegmentationRenderMode(SegmentationRenderMode mode) + { + if (mode != segmentationRenderMode) + { + segmentationRenderMode = mode; + UpdateMaterialProperties(); + } + } + public List GetSegmentationLabels() { return segmentationLabels; @@ -137,6 +157,8 @@ public void AddSegmentation(VolumeDataset dataset) return; } + overlayType = OverlayType.Segmentation; + int segmentationId = segmentationLabels.Count > 0 ? segmentationLabels.Max(l => l.id) + 1 : 1; if (segmentationLabels.Count == 0) @@ -161,10 +183,44 @@ public void AddSegmentation(VolumeDataset dataset) UpdateSegmentationLabels(); } + public void RemoveSegmentation(int id) + { + int segmentationIndex = segmentationLabels.FindIndex(s => s.id == id); + if (segmentationIndex != -1) + { + segmentationLabels.RemoveAt(segmentationIndex); + } + else + { + Debug.LogError($"Segmentation not found: {id}"); + } + for (int i = 0; i < secondaryDataset.data.Length; i++) + { + secondaryDataset.data[i] = secondaryDataset.data[i] == id ? 0 : secondaryDataset.data[i]; + } + secondaryDataset.RecalculateBounds(); + secondaryDataset.RecreateDataTexture(); + secondaryDataset.GetDataTexture().filterMode = FilterMode.Point; + UpdateSegmentationLabels(); + } + + public void ClearSegmentations() + { + if (overlayType == OverlayType.Segmentation) + { + secondaryDataset = null; + secondaryTransferFunction = null; + overlayType = OverlayType.None; + } + segmentationLabels.Clear(); + UpdateMaterialProperties(); + } + public void UpdateSegmentationLabels() { if (segmentationLabels.Count == 0) { + UpdateMaterialProperties(); return; } @@ -454,16 +510,26 @@ private void UpdateMatInternal(Texture3D dataTexture, Texture3D gradientTexture, meshRenderer.sharedMaterial.SetTexture("_GradientTex", gradientTexture); } - if (secondaryDataTexture != null) + if (overlayType != OverlayType.None && secondaryDataTexture != null) { Texture2D secondaryTF = secondaryTransferFunction.GetTexture(); meshRenderer.sharedMaterial.SetTexture("_SecondaryDataTex", secondaryDataTexture); meshRenderer.sharedMaterial.SetTexture("_SecondaryTFTex", secondaryTF); - meshRenderer.sharedMaterial.EnableKeyword("SECONDARY_VOLUME_ON"); + if (overlayType == OverlayType.Segmentation && segmentationRenderMode == SegmentationRenderMode.Isolate) + { + meshRenderer.sharedMaterial.EnableKeyword("MULTIVOLUME_ISOLATE"); + meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_OVERLAY"); + } + else if(overlayType == OverlayType.Overlay) + { + meshRenderer.sharedMaterial.EnableKeyword("MULTIVOLUME_OVERLAY"); + meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_ISOLATE"); + } } else { - meshRenderer.sharedMaterial.DisableKeyword("SECONDARY_VOLUME_ON"); + meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_OVERLAY"); + meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_ISOLATE"); } if (meshRenderer.sharedMaterial.GetTexture("_NoiseTex") == null) From 30dc9f791e28feb36554412c77cc43d4444b52cf Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Tue, 27 Aug 2024 20:45:03 +0200 Subject: [PATCH 09/13] fix --- Assets/Scripts/VolumeObject/VolumeRenderedObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index f87e18b9..2890064e 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -520,7 +520,7 @@ private void UpdateMatInternal(Texture3D dataTexture, Texture3D gradientTexture, meshRenderer.sharedMaterial.EnableKeyword("MULTIVOLUME_ISOLATE"); meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_OVERLAY"); } - else if(overlayType == OverlayType.Overlay) + else { meshRenderer.sharedMaterial.EnableKeyword("MULTIVOLUME_OVERLAY"); meshRenderer.sharedMaterial.DisableKeyword("MULTIVOLUME_ISOLATE"); From 5d80d8f036230cdb260009d6e780483ea01a8f2a Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Wed, 28 Aug 2024 19:28:58 +0200 Subject: [PATCH 10/13] cleanup --- .../VolumeRenderedObjectCustomInspector.cs | 87 +++++++++---------- .../DirectVolumeRenderingShader.shader | 36 +++++--- 2 files changed, 65 insertions(+), 58 deletions(-) diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index 09e45b37..4aef2bdf 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.IO; +using UnityEngine.Events; namespace UnityVolumeRendering { @@ -148,11 +149,25 @@ public override void OnInspectorGUI() { if (GUILayout.Button("Load PET (NRRD, NIFTI)")) { - ImportPetScan(volrendObj); + ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => + { + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + volrendObj.SetOverlayDataset(dataset); + volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); + }); } if (GUILayout.Button("Load PET (DICOM)")) { - ImportPetScanDicom(volrendObj); + ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + { + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + volrendObj.SetOverlayDataset(dataset); + volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); + }); } } else @@ -198,12 +213,18 @@ public override void OnInspectorGUI() } if (GUILayout.Button("Add segmentation (NRRD, NIFTI)")) { - ImportSegmentation(volrendObj); + ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => + { + volrendObj.AddSegmentation(dataset); + }); } - /*if (GUILayout.Button("Add segmentation (DICOM)")) + if (GUILayout.Button("Add segmentation (DICOM)")) { - ImportSegmentationDicom(volrendObj); - }*/ + ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + { + volrendObj.AddSegmentation(dataset); + }); + } if (GUILayout.Button("Clear segmentations")) { volrendObj.ClearSegmentations(); @@ -224,7 +245,7 @@ public override void OnInspectorGUI() volrendObj.SetSamplingRateMultiplier(EditorGUILayout.Slider("Sampling rate multiplier", volrendObj.GetSamplingRateMultiplier(), 0.2f, 2.0f)); } } - private static async void ImportPetScan(VolumeRenderedObject targetObject) + private static async void ImportImageFileDataset(VolumeRenderedObject targetObject, UnityAction onLoad) { string filePath = EditorUtility.OpenFilePanel("Select a folder to load", "", ""); ImageFileFormat imageFileFormat = DatasetFormatUtilities.GetImageFileFormat(filePath); @@ -241,67 +262,37 @@ private static async void ImportPetScan(VolumeRenderedObject targetObject) using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) { - progressHandler.StartStage(1.0f, "Importing PET dataset"); + progressHandler.StartStage(1.0f, "Importing dataset"); IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imageFileFormat); Task importTask = importer.ImportAsync(filePath); await importTask; progressHandler.EndStage(); - TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); - secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - secondaryTransferFunction.GenerateTexture(); - targetObject.SetOverlayDataset(importTask.Result); - targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); + if (importTask.Result != null) + { + onLoad.Invoke(importTask.Result); + } } } - private static async void ImportPetScanDicom(VolumeRenderedObject targetObject) + private static async void ImportDicomDataset(VolumeRenderedObject targetObject, UnityAction onLoad) { string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); if (Directory.Exists(dir)) { using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) { - progressHandler.StartStage(1.0f, "Importing PET dataset"); + progressHandler.StartStage(1.0f, "Importing dataset"); Task importTask = EditorDatasetImportUtils.ImportDicomDirectoryAsync(dir, progressHandler); await importTask; progressHandler.EndStage(); - Debug.Assert(importTask.Result.Length > 0); - TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); - secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - secondaryTransferFunction.GenerateTexture(); - targetObject.SetOverlayDataset(importTask.Result[0]); - targetObject.SetSecondaryTransferFunction(secondaryTransferFunction); + if (importTask.Result.Length > 0) + { + onLoad.Invoke(importTask.Result[0]); + } } } } - - private static async void ImportSegmentation(VolumeRenderedObject targetObject) - { - string filePath = EditorUtility.OpenFilePanel("Select a folder to load", "", ""); - ImageFileFormat imageFileFormat = DatasetFormatUtilities.GetImageFileFormat(filePath); - if (!File.Exists(filePath)) - { - Debug.LogError($"File doesn't exist: {filePath}"); - return; - } - if (imageFileFormat == ImageFileFormat.Unknown) - { - Debug.LogError($"Invalid file format: {Path.GetExtension(filePath)}"); - return; - } - - using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) - { - progressHandler.StartStage(1.0f, "Importing segmentation dataset"); - IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imageFileFormat); - Task importTask = importer.ImportAsync(filePath); - await importTask; - progressHandler.EndStage(); - - targetObject.AddSegmentation(importTask.Result); - } - } } } diff --git a/Assets/Shaders/DirectVolumeRenderingShader.shader b/Assets/Shaders/DirectVolumeRenderingShader.shader index cf99b886..279b2b20 100644 --- a/Assets/Shaders/DirectVolumeRenderingShader.shader +++ b/Assets/Shaders/DirectVolumeRenderingShader.shader @@ -40,6 +40,7 @@ #pragma multi_compile __ USE_MAIN_LIGHT #pragma multi_compile __ CUBIC_INTERPOLATION_ON #pragma multi_compile __ SECONDARY_VOLUME_ON + #pragma multi_compile MULTIVOLUME_NONE MULTIVOLUME_OVERLAY MULTIVOLUME_ISOLATE #pragma vertex vert #pragma fragment frag @@ -215,11 +216,7 @@ // Gets the density of the secondary volume at the specified position float getSecondaryDensity(float3 pos) { -#if CUBIC_INTERPOLATION_ON - return interpolateTricubicFast(_SecondaryDataTex, float3(pos.x, pos.y, pos.z), _TextureSize); -#else return tex3Dlod(_SecondaryDataTex, float4(pos.x, pos.y, pos.z, 0.0f)); -#endif } // Gets the density at the specified position, without tricubic interpolation @@ -344,12 +341,14 @@ continue; #endif -#if SECONDARY_VOLUME_ON - const float density2 = getSecondaryDensity(currPos); - float4 src2 = getSecondaryTF1DColour(density2); - //src.rgb = src.rgb * (1.0 - src2.a) + src2.rgb * src2.a; - src = src2.a > 0.0 ? src2 : src; - //src.a = src2.a > 0.0 ? src.a : 0.0; +#if defined(MULTIVOLUME_OVERLAY) || defined(MULTIVOLUME_ISOLATE) + const float secondaryDensity = getSecondaryDensity(currPos); + float4 secondaryColour = getSecondaryTF1DColour(secondaryDensity); +#if MULTIVOLUME_OVERLAY + src = secondaryColour.a > 0.0 ? secondaryColour : src; +#elif MULTIVOLUME_ISOLATE + src.a = secondaryColour.a > 0.0 ? src.a : 0.0; +#endif #endif // Calculate gradient (needed for lighting and 2D transfer functions) @@ -469,6 +468,23 @@ #endif const float density = getDensity(currPos); +#if MULTIVOLUME_ISOLATE + const float secondaryDensity = getSecondaryDensity(currPos); + if (secondaryDensity <= 0.0) + continue; +#elif MULTIVOLUME_OVERLAY + const float secondaryDensity = getSecondaryDensity(currPos); + if (secondaryDensity > 0.0) + { + col = getSecondaryTF1DColour(secondaryDensity); + float3 gradient = getGradient(currPos); + float gradMag = length(gradient); + float3 normal = gradient / gradMag; + col.rgb = calculateLighting(col.rgb, normal, getLightDirection(-ray.direction), -ray.direction, 0.15); + col.a = 1.0; + break; + } +#endif if (density > _MinVal && density < _MaxVal) { float3 gradient = getGradient(currPos); From 38d2b4cce8791848e69fe0f445a89e6884899911 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Wed, 28 Aug 2024 19:55:48 +0200 Subject: [PATCH 11/13] cleanup --- .../VolumeRenderedObjectCustomInspector.cs | 144 +++++++++--------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index 4aef2bdf..5ccb1e6e 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -13,8 +13,8 @@ public class VolumeRenderedObjectCustomInspector : Editor, IProgressView private bool tfSettings = true; private bool lightSettings = true; private bool otherSettings = true; - private bool secondaryVolumeSettings = true; - private bool segmentationSettings = true; + private bool overlayVolumeSettings = false; + private bool segmentationSettings = false; private float currentProgress = 1.0f; private string currentProgressDescrition = ""; private bool progressDirty = false; @@ -141,93 +141,99 @@ public override void OnInspectorGUI() } } - // Secondary volume - secondaryVolumeSettings = EditorGUILayout.Foldout(secondaryVolumeSettings, "PET/overlay volume"); - OverlayType overlayType = volrendObj.GetOverlayType(); - TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction(); - if (overlayType != OverlayType.Overlay) + // Overlay volume + overlayVolumeSettings = EditorGUILayout.Foldout(overlayVolumeSettings, "PET/overlay volume"); + if (overlayVolumeSettings) { - if (GUILayout.Button("Load PET (NRRD, NIFTI)")) + OverlayType overlayType = volrendObj.GetOverlayType(); + TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction(); + if (overlayType != OverlayType.Overlay) { - ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => + if (GUILayout.Button("Load PET (NRRD, NIFTI)")) { - TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); - secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - secondaryTransferFunction.GenerateTexture(); - volrendObj.SetOverlayDataset(dataset); - volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); - }); - } - if (GUILayout.Button("Load PET (DICOM)")) - { - ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => + { + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + volrendObj.SetOverlayDataset(dataset); + volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); + }); + } + if (GUILayout.Button("Load PET (DICOM)")) { - TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); - secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; - secondaryTransferFunction.GenerateTexture(); - volrendObj.SetOverlayDataset(dataset); - volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); - }); + ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + { + TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance(); + secondaryTransferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + secondaryTransferFunction.GenerateTexture(); + volrendObj.SetOverlayDataset(dataset); + volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction); + }); + } } - } - else - { - if (GUILayout.Button("Edit secondary transfer function")) + else { - TransferFunctionEditorWindow.ShowWindow(volrendObj, secondaryTransferFunction); - } + if (GUILayout.Button("Edit overlay transfer function")) + { + TransferFunctionEditorWindow.ShowWindow(volrendObj, secondaryTransferFunction); + } - if (GUILayout.Button("Remove secondary volume")) - { - volrendObj.SetOverlayDataset(null); + if (GUILayout.Button("Remove secondary volume")) + { + volrendObj.SetOverlayDataset(null); + } } } // Segmentations segmentationSettings = EditorGUILayout.Foldout(segmentationSettings, "Segmentations"); - List segmentationLabels = volrendObj.GetSegmentationLabels(); - if (segmentationLabels != null && segmentationLabels.Count > 0) + if (segmentationSettings) { - for (int i = 0; i < segmentationLabels.Count; i++) + List segmentationLabels = volrendObj.GetSegmentationLabels(); + if (segmentationLabels != null && segmentationLabels.Count > 0) { - EditorGUILayout.BeginHorizontal(); - SegmentationLabel segmentationlabel = segmentationLabels[i]; - EditorGUI.BeginChangeCheck(); - segmentationlabel.name = EditorGUILayout.TextField(segmentationlabel.name); - segmentationlabel.colour = EditorGUILayout.ColorField(segmentationlabel.colour); - bool changed = EditorGUI.EndChangeCheck(); - segmentationLabels[i] = segmentationlabel; - if (GUILayout.Button("delete")) + for (int i = 0; i < segmentationLabels.Count; i++) { - volrendObj.RemoveSegmentation(segmentationlabel.id); + EditorGUILayout.BeginHorizontal(); + SegmentationLabel segmentationlabel = segmentationLabels[i]; + EditorGUI.BeginChangeCheck(); + segmentationlabel.name = EditorGUILayout.TextField(segmentationlabel.name); + segmentationlabel.colour = EditorGUILayout.ColorField(segmentationlabel.colour); + bool changed = EditorGUI.EndChangeCheck(); + segmentationLabels[i] = segmentationlabel; + if (GUILayout.Button("delete")) + { + volrendObj.RemoveSegmentation(segmentationlabel.id); + } + EditorGUILayout.EndHorizontal(); + if (changed) + { + volrendObj.UpdateSegmentationLabels(); + } } - EditorGUILayout.EndHorizontal(); - if (changed) + + SegmentationRenderMode segmentationRendreMode = (SegmentationRenderMode)EditorGUILayout.EnumPopup("Render mode", volrendObj.GetSegmentationRenderMode()); + volrendObj.SetSegmentationRenderMode(segmentationRendreMode); + } + if (GUILayout.Button("Add segmentation (NRRD, NIFTI)")) + { + ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => { - volrendObj.UpdateSegmentationLabels(); - } + volrendObj.AddSegmentation(dataset); + }); } - - SegmentationRenderMode segmentationRendreMode = (SegmentationRenderMode)EditorGUILayout.EnumPopup("Render mode", volrendObj.GetSegmentationRenderMode()); - volrendObj.SetSegmentationRenderMode(segmentationRendreMode); - } - if (GUILayout.Button("Add segmentation (NRRD, NIFTI)")) - { - ImportImageFileDataset(volrendObj, (VolumeDataset dataset) => + if (GUILayout.Button("Add segmentation (DICOM)")) { - volrendObj.AddSegmentation(dataset); - }); - } - if (GUILayout.Button("Add segmentation (DICOM)")) - { - ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + ImportDicomDataset(volrendObj, (VolumeDataset dataset) => + { + volrendObj.AddSegmentation(dataset); + }); + } + if (GUILayout.Button("Clear segmentations")) { - volrendObj.AddSegmentation(dataset); - }); - } - if (GUILayout.Button("Clear segmentations")) - { - volrendObj.ClearSegmentations(); + volrendObj.ClearSegmentations(); + } } // Other settings From 21b7dbececa261c3f4e1dbbfef1da7d1d64bf2a2 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Wed, 28 Aug 2024 21:20:39 +0200 Subject: [PATCH 12/13] CREDITS --- CREDITS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index 56a11822..20c6ae7c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -18,5 +18,7 @@ Texture downscaling, optimisation. Async loading - [Riccardo Lops](https://github.com/riccardolops) Modified shader to handle stereo rendering. +- [Juan Pablo Montoya](https://github.com/JuanPabloMontoya271) +Overlay Segmentations - initial implementation Feel free to add yourself to this list when contributing to this project. From 720dcabe933e6a2ad74a02734f327907d7c1d043 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Thu, 29 Aug 2024 18:49:17 +0200 Subject: [PATCH 13/13] fixed built-in NIFTI importer: Handle byte arrays + handle import failure --- Assets/3rdparty/Nifti.NET/Nifti.cs | 2 ++ .../Nifti.NET/NiftiImporter.cs | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Assets/3rdparty/Nifti.NET/Nifti.cs b/Assets/3rdparty/Nifti.NET/Nifti.cs index 47cc2ff5..3d7a8371 100644 --- a/Assets/3rdparty/Nifti.NET/Nifti.cs +++ b/Assets/3rdparty/Nifti.NET/Nifti.cs @@ -135,6 +135,8 @@ public float[] ToSingleArray() return Array.ConvertAll(this.Data as short[], Convert.ToSingle); else if(type == typeof(ushort)) return Array.ConvertAll(this.Data as ushort[], Convert.ToSingle); + else if (type == typeof(byte)) + return Array.ConvertAll(this.Data as byte[], Convert.ToSingle); else return null; } diff --git a/Assets/Scripts/Importing/ImageFileImporter/Nifti.NET/NiftiImporter.cs b/Assets/Scripts/Importing/ImageFileImporter/Nifti.NET/NiftiImporter.cs index 3cee3776..3bbff7f0 100644 --- a/Assets/Scripts/Importing/ImageFileImporter/Nifti.NET/NiftiImporter.cs +++ b/Assets/Scripts/Importing/ImageFileImporter/Nifti.NET/NiftiImporter.cs @@ -30,7 +30,10 @@ public VolumeDataset Import(string filePath) // Create dataset VolumeDataset volumeDataset = ScriptableObject.CreateInstance(); - ImportInternal(volumeDataset, niftiFile, filePath); + bool succeeded = ImportInternal(volumeDataset, niftiFile, filePath); + + if (!succeeded) + volumeDataset = null; return volumeDataset; } @@ -55,17 +58,26 @@ public async Task ImportAsync(string filePath) return null; } - await Task.Run(() => ImportInternal(volumeDataset,niftiFile,filePath)); + bool succeeded = await Task.Run(() => ImportInternal(volumeDataset,niftiFile,filePath)); + + if (!succeeded) + volumeDataset = null; return volumeDataset; } - private void ImportInternal(VolumeDataset volumeDataset,Nifti.NET.Nifti niftiFile,string filePath) + private bool ImportInternal(VolumeDataset volumeDataset,Nifti.NET.Nifti niftiFile,string filePath) { int dimX = niftiFile.Header.dim[1]; int dimY = niftiFile.Header.dim[2]; int dimZ = niftiFile.Header.dim[3]; float[] pixelData = niftiFile.ToSingleArray(); + if (pixelData == null) + { + Debug.LogError($"Failed to read data, of type: {niftiFile.Data?.GetType()}"); + return false; + } + Vector3 pixdim = new Vector3(niftiFile.Header.pixdim[1], niftiFile.Header.pixdim[2], niftiFile.Header.pixdim[3]); Vector3 size = new Vector3(dimX * pixdim.x, dimY * pixdim.y, dimZ * pixdim.z); @@ -80,6 +92,8 @@ private void ImportInternal(VolumeDataset volumeDataset,Nifti.NET.Nifti niftiFil volumeDataset.FixDimensions(); volumeDataset.rotation = Quaternion.Euler(90.0f, 0.0f, 0.0f); + + return true; } } }