From c6dcc731a757d53b245a84fc5deb36214b6d7ad4 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Sun, 24 May 2020 20:42:15 +0200 Subject: [PATCH] Added "remove plane" button to slicing renderer window. Renamed SlicingPlaneAnyDirection -> CrossSectionPlane. Added more comments. --- Assets/Editor/DatasetImporterEditorWIndow.cs | 9 +- Assets/Editor/SliceRenderingEditorWindow.cs | 13 ++- Assets/Scripts/Importing/DICOMImporter.cs | 93 +++++++++++-------- .../Scripts/Importing/DatasetImporterBase.cs | 4 + Assets/Scripts/Importing/DatasetIniReader.cs | 11 +++ .../Scripts/VolumeObject/CrossSectionPlane.cs | 35 +++++++ ...tion.cs.meta => CrossSectionPlane.cs.meta} | 0 .../VolumeObject/SlicingPlaneAnyDirection.cs | 27 ------ .../VolumeObject/VolumeObjectFactory.cs | 5 +- 9 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 Assets/Scripts/VolumeObject/CrossSectionPlane.cs rename Assets/Scripts/VolumeObject/{SlicingPlaneAnyDirection.cs.meta => CrossSectionPlane.cs.meta} (100%) delete mode 100644 Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs diff --git a/Assets/Editor/DatasetImporterEditorWIndow.cs b/Assets/Editor/DatasetImporterEditorWIndow.cs index efbc363..eaa6bdf 100644 --- a/Assets/Editor/DatasetImporterEditorWIndow.cs +++ b/Assets/Editor/DatasetImporterEditorWIndow.cs @@ -5,6 +5,9 @@ using System; namespace UnityVolumeRendering { + /// + /// Editor window for importing datasets. + /// public class DatasetImporterEditorWindow : EditorWindow { private enum DatasetType @@ -17,9 +20,9 @@ namespace UnityVolumeRendering private string fileToImport; private DatasetType datasetType; - private int dimX; // TODO: set good default value - private int dimY; // TODO: set good default value - private int dimZ; // TODO: set good default value + private int dimX; + private int dimY; + private int dimZ; private int bytesToSkip = 0; private DataContentFormat dataFormat = DataContentFormat.Int16; private Endianness endianness = Endianness.LittleEndian; diff --git a/Assets/Editor/SliceRenderingEditorWindow.cs b/Assets/Editor/SliceRenderingEditorWindow.cs index eced0b0..ec7f233 100644 --- a/Assets/Editor/SliceRenderingEditorWindow.cs +++ b/Assets/Editor/SliceRenderingEditorWindow.cs @@ -76,12 +76,12 @@ namespace UnityVolumeRendering // Show buttons for changing the active plane if (spawnedPlanes.Length > 0) { - if (GUI.Button(new Rect(0.0f, bgRect.y + bgRect.height + 20.0f, 100.0f, 30.0f), "previous\nplane")) + if (GUI.Button(new Rect(0.0f, bgRect.y + bgRect.height + 20.0f, 70.0f, 30.0f), "previous\nplane")) { selectedPlaneIndex = (selectedPlaneIndex - 1) % spawnedPlanes.Length; Selection.activeGameObject = spawnedPlanes[selectedPlaneIndex].gameObject; } - if (GUI.Button(new Rect(120.0f, bgRect.y + bgRect.height + 20.0f, 100.0f, 30.0f), "next\nplane")) + if (GUI.Button(new Rect(90.0f, bgRect.y + bgRect.height + 20.0f, 70.0f, 30.0f), "next\nplane")) { selectedPlaneIndex = (selectedPlaneIndex + 1) % spawnedPlanes.Length; Selection.activeGameObject = spawnedPlanes[selectedPlaneIndex].gameObject; @@ -89,7 +89,7 @@ namespace UnityVolumeRendering } // Show button for adding new plane - if (GUI.Button(new Rect(240.0f, bgRect.y + bgRect.height + 20.0f, 100.0f, 30.0f), "add\nplane")) + if (GUI.Button(new Rect(180.0f, bgRect.y + bgRect.height + 20.0f, 70.0f, 30.0f), "add\nplane")) { VolumeRenderedObject volRend = FindObjectOfType(); if (volRend != null) @@ -99,6 +99,13 @@ namespace UnityVolumeRendering } } + // Show button for removing + if (spawnedPlanes.Length > 0 && GUI.Button(new Rect(270.0f, bgRect.y + bgRect.height + 20.0f, 70.0f, 30.0f), "remove\nplane")) + { + SlicingPlane planeToRemove = spawnedPlanes[selectedPlaneIndex]; + GameObject.DestroyImmediate(planeToRemove.gameObject); + } + // Show hint if (spawnedPlanes.Length > 0) GUI.Label(new Rect(0.0f, bgRect.y + bgRect.height + 60.0f, 450.0f, 30.0f), "Move plane by left clicking in the above view and dragging the mouse,\n or simply move it in the object hierarchy."); diff --git a/Assets/Scripts/Importing/DICOMImporter.cs b/Assets/Scripts/Importing/DICOMImporter.cs index 48a6888..4d58745 100644 --- a/Assets/Scripts/Importing/DICOMImporter.cs +++ b/Assets/Scripts/Importing/DICOMImporter.cs @@ -11,6 +11,11 @@ using System.Linq; 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. + /// public class DICOMImporter : DatasetImporterBase { private class DICOMSliceFile @@ -45,50 +50,17 @@ namespace UnityVolumeRendering return null; } + // Read all files IEnumerable fileCandidates = Directory.EnumerateFiles(diroctoryPath, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Where(p => p.EndsWith(".dcm") || p.EndsWith(".dicom") || p.EndsWith(".dicm")); List files = new List(); foreach (string filePath in fileCandidates) { - AcrNemaFile file = LoadFile(filePath); - if(file != null && file.HasPixelData) - { - DICOMSliceFile slice = new DICOMSliceFile(); - slice.file = file; - // Read location - Tag locTag = new Tag("(0020,1041)"); - if(file.DataSet.Contains(locTag)) - { - DataElement elemLoc = file.DataSet[locTag]; - slice.location = (float)Convert.ToDouble(elemLoc.Value[0]); - } - else - { - Debug.LogError($"Missing location tag in file: {filePath}.\n The file will not be imported"); - continue; - } - // Read intercept - Tag interceptTag = new Tag("(0028,1052)"); - if (file.DataSet.Contains(interceptTag)) - { - DataElement elemIntercept = file.DataSet[interceptTag]; - slice.intercept = (float)Convert.ToDouble(elemIntercept.Value[0]); - } - else - Debug.LogWarning($"The file {filePath} is missing the intercept element. As a result, the default transfer function might not look good."); - // Read slope - Tag slopeTag = new Tag("(0028,1053)"); - if (file.DataSet.Contains(slopeTag)) - { - DataElement elemSlope = file.DataSet[slopeTag]; - slice.slope = (float)Convert.ToDouble(elemSlope.Value[0]); - } - else - Debug.LogWarning($"The file {filePath} is missing the intercept element. As a result, the default transfer function might not look good."); - - files.Add(slice); - } + DICOMSliceFile sliceFile = ReadDICOMFile(filePath); + if(sliceFile != null) + files.Add(sliceFile); } + // Sort files by slice location files.Sort((DICOMSliceFile a, DICOMSliceFile b) => { return a.location.CompareTo(b.location); }); Debug.Log($"Imported {files.Count} datasets"); @@ -103,6 +75,7 @@ namespace UnityVolumeRendering float maxLoc = (float)files[files.Count - 1].location; float locRange = maxLoc - minLoc; + // Create dataset VolumeDataset dataset = new VolumeDataset(); dataset.dimX = files[0].file.PixelData.Columns; dataset.dimY = files[0].file.PixelData.Rows; @@ -137,6 +110,50 @@ namespace UnityVolumeRendering return dataset; } + private DICOMSliceFile ReadDICOMFile(string filePath) + { + AcrNemaFile file = LoadFile(filePath); + + if (file != null && file.HasPixelData) + { + DICOMSliceFile slice = new DICOMSliceFile(); + slice.file = file; + // Read location + Tag locTag = new Tag("(0020,1041)"); + if (file.DataSet.Contains(locTag)) + { + DataElement elemLoc = file.DataSet[locTag]; + slice.location = (float)Convert.ToDouble(elemLoc.Value[0]); + } + else + { + Debug.LogError($"Missing location tag in file: {filePath}.\n The file will not be imported"); + return null; + } + // Read intercept + Tag interceptTag = new Tag("(0028,1052)"); + if (file.DataSet.Contains(interceptTag)) + { + DataElement elemIntercept = file.DataSet[interceptTag]; + slice.intercept = (float)Convert.ToDouble(elemIntercept.Value[0]); + } + else + Debug.LogWarning($"The file {filePath} is missing the intercept element. As a result, the default transfer function might not look good."); + // Read slope + Tag slopeTag = new Tag("(0028,1053)"); + if (file.DataSet.Contains(slopeTag)) + { + DataElement elemSlope = file.DataSet[slopeTag]; + slice.slope = (float)Convert.ToDouble(elemSlope.Value[0]); + } + else + Debug.LogWarning($"The file {filePath} is missing the intercept element. As a result, the default transfer function might not look good."); + + return slice; + } + return null; + } + private AcrNemaFile LoadFile(string filePath) { AcrNemaFile file = null; diff --git a/Assets/Scripts/Importing/DatasetImporterBase.cs b/Assets/Scripts/Importing/DatasetImporterBase.cs index bb08d66..1c28e71 100644 --- a/Assets/Scripts/Importing/DatasetImporterBase.cs +++ b/Assets/Scripts/Importing/DatasetImporterBase.cs @@ -1,5 +1,9 @@ namespace UnityVolumeRendering { + /// + /// Base class for all dataset imports. + /// If you want to add support for a new format, create a sublcass of this. + /// public abstract class DatasetImporterBase { public abstract VolumeDataset Import(); diff --git a/Assets/Scripts/Importing/DatasetIniReader.cs b/Assets/Scripts/Importing/DatasetIniReader.cs index 6690672..c74c630 100644 --- a/Assets/Scripts/Importing/DatasetIniReader.cs +++ b/Assets/Scripts/Importing/DatasetIniReader.cs @@ -13,6 +13,17 @@ namespace UnityVolumeRendering public Endianness endianness = Endianness.LittleEndian; } + /// + /// .ini-file reader for raw datasets. + /// .ini files contains information about how to import a raw dataset file. + /// Example file: + /// dimx:256 + /// dimy:256 + /// dimz:68 + /// skip:28 + /// format:uint8 + /// "skip" defines how many bytes to skip (file header) - it should be 0 if the file has no header, which is often the case. + /// public class DatasetIniReader { public static DatasetIniData ParseIniFile(string filePath) diff --git a/Assets/Scripts/VolumeObject/CrossSectionPlane.cs b/Assets/Scripts/VolumeObject/CrossSectionPlane.cs new file mode 100644 index 0000000..ed43231 --- /dev/null +++ b/Assets/Scripts/VolumeObject/CrossSectionPlane.cs @@ -0,0 +1,35 @@ +using UnityEngine; + +namespace UnityVolumeRendering +{ + /// + /// Cross section plane. + /// Used for cutting a model (cross section view). + /// + [ExecuteInEditMode] + public class CrossSectionPlane : MonoBehaviour + { + /// + /// Volume dataset to cross section. + /// + public VolumeRenderedObject targetObject; + + private void OnDisable() + { + if (targetObject != null) + targetObject.meshRenderer.sharedMaterial.DisableKeyword("SLICEPLANE_ON"); + } + + private void Update() + { + if (targetObject == null) + return; + + Material mat = targetObject.meshRenderer.sharedMaterial; + + mat.EnableKeyword("SLICEPLANE_ON"); + mat.SetVector("_PlanePos", targetObject.transform.position - transform.position); + mat.SetVector("_PlaneNormal", transform.forward); + } + } +} diff --git a/Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs.meta b/Assets/Scripts/VolumeObject/CrossSectionPlane.cs.meta similarity index 100% rename from Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs.meta rename to Assets/Scripts/VolumeObject/CrossSectionPlane.cs.meta diff --git a/Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs b/Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs deleted file mode 100644 index 49ac541..0000000 --- a/Assets/Scripts/VolumeObject/SlicingPlaneAnyDirection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using UnityEngine; - -namespace UnityVolumeRendering -{ - [ExecuteInEditMode] - public class SlicingPlaneAnyDirection : MonoBehaviour - { - public Material mat; - public Transform volumeTransform; - - private void OnDisable() - { - if (mat != null) - mat.DisableKeyword("SLICEPLANE_ON"); - } - - private void Update() - { - if (mat == null || volumeTransform == null) - return; - - mat.EnableKeyword("SLICEPLANE_ON"); - mat.SetVector("_PlanePos", volumeTransform.position - transform.position); - mat.SetVector("_PlaneNormal", transform.forward); - } - } -} diff --git a/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs b/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs index cd5880a..961b8fb 100644 --- a/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs +++ b/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs @@ -49,9 +49,8 @@ namespace UnityVolumeRendering { GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad); quad.transform.rotation = Quaternion.Euler(270.0f, 0.0f, 0.0f); - SlicingPlaneAnyDirection csplane = quad.gameObject.AddComponent(); - csplane.mat = volobj.meshRenderer.sharedMaterial; - csplane.volumeTransform = volobj.transform; + CrossSectionPlane csplane = quad.gameObject.AddComponent(); + csplane.targetObject = volobj; quad.transform.position = volobj.transform.position; #if UNITY_EDITOR