Added "remove plane" button to slicing renderer window.

Renamed SlicingPlaneAnyDirection -> CrossSectionPlane.
Added more comments.
This commit is contained in:
Matias Lavik 2020-05-24 20:42:15 +02:00
Родитель d3a6049884
Коммит c6dcc731a7
9 изменённых файлов: 123 добавлений и 74 удалений

Просмотреть файл

@ -5,6 +5,9 @@ using System;
namespace UnityVolumeRendering
{
/// <summary>
/// Editor window for importing datasets.
/// </summary>
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;

Просмотреть файл

@ -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<VolumeRenderedObject>();
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.");

Просмотреть файл

@ -11,6 +11,11 @@ using System.Linq;
namespace UnityVolumeRendering
{
/// <summary>
/// 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.
/// </summary>
public class DICOMImporter : DatasetImporterBase
{
private class DICOMSliceFile
@ -45,50 +50,17 @@ namespace UnityVolumeRendering
return null;
}
// Read all files
IEnumerable<string> fileCandidates = Directory.EnumerateFiles(diroctoryPath, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.Where(p => p.EndsWith(".dcm") || p.EndsWith(".dicom") || p.EndsWith(".dicm"));
List<DICOMSliceFile> files = new List<DICOMSliceFile>();
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;

Просмотреть файл

@ -1,5 +1,9 @@
namespace UnityVolumeRendering
{
/// <summary>
/// Base class for all dataset imports.
/// If you want to add support for a new format, create a sublcass of this.
/// </summary>
public abstract class DatasetImporterBase
{
public abstract VolumeDataset Import();

Просмотреть файл

@ -13,6 +13,17 @@ namespace UnityVolumeRendering
public Endianness endianness = Endianness.LittleEndian;
}
/// <summary>
/// .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.
/// </summary>
public class DatasetIniReader
{
public static DatasetIniData ParseIniFile(string filePath)

Просмотреть файл

@ -0,0 +1,35 @@
using UnityEngine;
namespace UnityVolumeRendering
{
/// <summary>
/// Cross section plane.
/// Used for cutting a model (cross section view).
/// </summary>
[ExecuteInEditMode]
public class CrossSectionPlane : MonoBehaviour
{
/// <summary>
/// Volume dataset to cross section.
/// </summary>
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);
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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<SlicingPlaneAnyDirection>();
csplane.mat = volobj.meshRenderer.sharedMaterial;
csplane.volumeTransform = volobj.transform;
CrossSectionPlane csplane = quad.gameObject.AddComponent<CrossSectionPlane>();
csplane.targetObject = volobj;
quad.transform.position = volobj.transform.position;
#if UNITY_EDITOR