Skip to content

Commit 4c4efa1

Browse files
authored
Segmentations + PET overlay (#264)
* Segmentations and PET/CT rendering * Fixed built-in NIFTI importer: Handle byte arrays + handle import failure
1 parent 59606fb commit 4c4efa1

File tree

15 files changed

+630
-102
lines changed

15 files changed

+630
-102
lines changed

Assets/3rdparty/Nifti.NET/Nifti.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public float[] ToSingleArray()
135135
return Array.ConvertAll<short, float>(this.Data as short[], Convert.ToSingle);
136136
else if(type == typeof(ushort))
137137
return Array.ConvertAll<ushort, float>(this.Data as ushort[], Convert.ToSingle);
138+
else if (type == typeof(byte))
139+
return Array.ConvertAll<byte, float>(this.Data as byte[], Convert.ToSingle);
138140
else
139141
return null;
140142
}

Assets/Editor/TransferFunctionEditorWindow.cs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class TransferFunctionEditorWindow : EditorWindow
1111

1212
private TransferFunctionEditor tfEditor = new TransferFunctionEditor();
1313

14+
private bool keepTf = false;
15+
1416
public static void ShowWindow(VolumeRenderedObject volRendObj)
1517
{
1618
// Close all (if any) 2D TF editor windows
@@ -25,6 +27,21 @@ public static void ShowWindow(VolumeRenderedObject volRendObj)
2527
wnd.SetInitialPosition();
2628
}
2729

30+
public static void ShowWindow(VolumeRenderedObject volRendObj, TransferFunction transferFunction)
31+
{
32+
// Close all (if any) 2D TF editor windows
33+
TransferFunction2DEditorWindow[] tf2dWnds = Resources.FindObjectsOfTypeAll<TransferFunction2DEditorWindow>();
34+
foreach (TransferFunction2DEditorWindow tf2dWnd in tf2dWnds)
35+
tf2dWnd.Close();
36+
37+
TransferFunctionEditorWindow wnd = (TransferFunctionEditorWindow)EditorWindow.GetWindow(typeof(TransferFunctionEditorWindow));
38+
wnd.volRendObject = volRendObj;
39+
wnd.tf = transferFunction;
40+
wnd.keepTf = true;
41+
wnd.Show();
42+
wnd.SetInitialPosition();
43+
}
44+
2845
private void SetInitialPosition()
2946
{
3047
Rect rect = this.position;
@@ -48,8 +65,9 @@ private void OnGUI()
4865

4966
if (volRendObject == null)
5067
return;
51-
52-
tf = volRendObject.transferFunction;
68+
69+
if (!keepTf)
70+
tf = volRendObject.transferFunction;
5371

5472
Event currentEvent = new Event(Event.current);
5573

@@ -62,7 +80,7 @@ private void OnGUI()
6280
Rect outerRect = new Rect(0.0f, 0.0f, contentWidth, contentHeight);
6381
Rect tfEditorRect = new Rect(outerRect.x + 20.0f, outerRect.y + 20.0f, outerRect.width - 40.0f, outerRect.height - 50.0f);
6482

65-
tfEditor.SetVolumeObject(volRendObject);
83+
tfEditor.SetTarget(volRendObject.dataset, tf);
6684
tfEditor.DrawOnGUI(tfEditorRect);
6785

6886
// Draw horizontal zoom slider
@@ -99,20 +117,22 @@ private void OnGUI()
99117
TransferFunction newTF = TransferFunctionDatabase.LoadTransferFunction(filepath);
100118
if(newTF != null)
101119
{
102-
tf = newTF;
103-
volRendObject.SetTransferFunction(tf);
120+
tf.alphaControlPoints = newTF.alphaControlPoints;
121+
tf.colourControlPoints = newTF.colourControlPoints;
122+
tf.GenerateTexture();
104123
tfEditor.ClearSelection();
105124
}
106125
}
107126
}
108127
// Clear TF
109128
if(GUI.Button(new Rect(tfEditorRect.x + 150.0f, tfEditorRect.y + tfEditorRect.height + 20.0f, 70.0f, 30.0f), "Clear"))
110129
{
111-
tf = ScriptableObject.CreateInstance<TransferFunction>();
130+
tf.alphaControlPoints.Clear();
131+
tf.colourControlPoints.Clear();
112132
tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.2f, 0.0f));
113133
tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.8f, 1.0f));
114134
tf.colourControlPoints.Add(new TFColourControlPoint(0.5f, new Color(0.469f, 0.354f, 0.223f, 1.0f)));
115-
volRendObject.SetTransferFunction(tf);
135+
tf.GenerateTexture();
116136
tfEditor.ClearSelection();
117137
}
118138

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using System;
5+
using System.Linq;
6+
using UnityEditor;
7+
using UnityEngine;
8+
9+
namespace UnityVolumeRendering
10+
{
11+
public class EditorDatasetImportUtils
12+
{
13+
public static async Task<VolumeDataset[]> ImportDicomDirectoryAsync(string dir, ProgressHandler progressHandler)
14+
{
15+
Debug.Log("Async dataset load. Hold on.");
16+
17+
List<VolumeDataset> importedDatasets = new List<VolumeDataset>();
18+
bool recursive = true;
19+
20+
// Read all files
21+
IEnumerable<string> fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
22+
.Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase));
23+
24+
if (!fileCandidates.Any())
25+
{
26+
if (UnityEditor.EditorUtility.DisplayDialog("Could not find any DICOM files",
27+
$"Failed to find any files with DICOM file extension.{Environment.NewLine}Do you want to include files without DICOM file extension?", "Yes", "No"))
28+
{
29+
fileCandidates = Directory.EnumerateFiles(dir, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
30+
}
31+
}
32+
33+
if (fileCandidates.Any())
34+
{
35+
progressHandler.StartStage(0.2f, "Loading DICOM series");
36+
37+
IImageSequenceImporter importer = ImporterFactory.CreateImageSequenceImporter(ImageSequenceFormat.DICOM);
38+
IEnumerable<IImageSequenceSeries> seriesList = await importer.LoadSeriesAsync(fileCandidates, new ImageSequenceImportSettings { progressHandler = progressHandler });
39+
40+
progressHandler.EndStage();
41+
progressHandler.StartStage(0.8f);
42+
43+
int seriesIndex = 0, numSeries = seriesList.Count();
44+
foreach (IImageSequenceSeries series in seriesList)
45+
{
46+
progressHandler.StartStage(1.0f / numSeries, $"Importing series {seriesIndex + 1} of {numSeries}");
47+
VolumeDataset dataset = await importer.ImportSeriesAsync(series, new ImageSequenceImportSettings { progressHandler = progressHandler });
48+
if (dataset != null)
49+
{
50+
await OptionallyDownscale(dataset);
51+
importedDatasets.Add(dataset);
52+
}
53+
seriesIndex++;
54+
progressHandler.EndStage();
55+
}
56+
57+
progressHandler.EndStage();
58+
}
59+
else
60+
Debug.LogError("Could not find any DICOM files to import.");
61+
62+
return importedDatasets.ToArray();
63+
}
64+
65+
public static async Task OptionallyDownscale(VolumeDataset dataset)
66+
{
67+
if (EditorPrefs.GetBool("DownscaleDatasetPrompt"))
68+
{
69+
if (EditorUtility.DisplayDialog("Optional DownScaling",
70+
$"Do you want to downscale the dataset? The dataset's dimension is: {dataset.dimX} x {dataset.dimY} x {dataset.dimZ}", "Yes", "No"))
71+
{
72+
Debug.Log("Async dataset downscale. Hold on.");
73+
await Task.Run(() => dataset.DownScaleData());
74+
}
75+
}
76+
}
77+
}
78+
}

Assets/Editor/VolumeRenderedObjectCustomInspector.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using UnityEditor;
33
using System.Collections.Generic;
44
using System.Threading.Tasks;
5+
using System.IO;
6+
using UnityEngine.Events;
57

68
namespace UnityVolumeRendering
79
{
@@ -11,6 +13,8 @@ public class VolumeRenderedObjectCustomInspector : Editor, IProgressView
1113
private bool tfSettings = true;
1214
private bool lightSettings = true;
1315
private bool otherSettings = true;
16+
private bool overlayVolumeSettings = false;
17+
private bool segmentationSettings = false;
1418
private float currentProgress = 1.0f;
1519
private string currentProgressDescrition = "";
1620
private bool progressDirty = false;
@@ -137,6 +141,101 @@ public override void OnInspectorGUI()
137141
}
138142
}
139143

144+
// Overlay volume
145+
overlayVolumeSettings = EditorGUILayout.Foldout(overlayVolumeSettings, "PET/overlay volume");
146+
if (overlayVolumeSettings)
147+
{
148+
OverlayType overlayType = volrendObj.GetOverlayType();
149+
TransferFunction secondaryTransferFunction = volrendObj.GetSecondaryTransferFunction();
150+
if (overlayType != OverlayType.Overlay)
151+
{
152+
if (GUILayout.Button("Load PET (NRRD, NIFTI)"))
153+
{
154+
ImportImageFileDataset(volrendObj, (VolumeDataset dataset) =>
155+
{
156+
TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance<TransferFunction>();
157+
secondaryTransferFunction.colourControlPoints = new List<TFColourControlPoint>() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) };
158+
secondaryTransferFunction.GenerateTexture();
159+
volrendObj.SetOverlayDataset(dataset);
160+
volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction);
161+
});
162+
}
163+
if (GUILayout.Button("Load PET (DICOM)"))
164+
{
165+
ImportDicomDataset(volrendObj, (VolumeDataset dataset) =>
166+
{
167+
TransferFunction secondaryTransferFunction = ScriptableObject.CreateInstance<TransferFunction>();
168+
secondaryTransferFunction.colourControlPoints = new List<TFColourControlPoint>() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) };
169+
secondaryTransferFunction.GenerateTexture();
170+
volrendObj.SetOverlayDataset(dataset);
171+
volrendObj.SetSecondaryTransferFunction(secondaryTransferFunction);
172+
});
173+
}
174+
}
175+
else
176+
{
177+
if (GUILayout.Button("Edit overlay transfer function"))
178+
{
179+
TransferFunctionEditorWindow.ShowWindow(volrendObj, secondaryTransferFunction);
180+
}
181+
182+
if (GUILayout.Button("Remove secondary volume"))
183+
{
184+
volrendObj.SetOverlayDataset(null);
185+
}
186+
}
187+
}
188+
189+
// Segmentations
190+
segmentationSettings = EditorGUILayout.Foldout(segmentationSettings, "Segmentations");
191+
if (segmentationSettings)
192+
{
193+
List<SegmentationLabel> segmentationLabels = volrendObj.GetSegmentationLabels();
194+
if (segmentationLabels != null && segmentationLabels.Count > 0)
195+
{
196+
for (int i = 0; i < segmentationLabels.Count; i++)
197+
{
198+
EditorGUILayout.BeginHorizontal();
199+
SegmentationLabel segmentationlabel = segmentationLabels[i];
200+
EditorGUI.BeginChangeCheck();
201+
segmentationlabel.name = EditorGUILayout.TextField(segmentationlabel.name);
202+
segmentationlabel.colour = EditorGUILayout.ColorField(segmentationlabel.colour);
203+
bool changed = EditorGUI.EndChangeCheck();
204+
segmentationLabels[i] = segmentationlabel;
205+
if (GUILayout.Button("delete"))
206+
{
207+
volrendObj.RemoveSegmentation(segmentationlabel.id);
208+
}
209+
EditorGUILayout.EndHorizontal();
210+
if (changed)
211+
{
212+
volrendObj.UpdateSegmentationLabels();
213+
}
214+
}
215+
216+
SegmentationRenderMode segmentationRendreMode = (SegmentationRenderMode)EditorGUILayout.EnumPopup("Render mode", volrendObj.GetSegmentationRenderMode());
217+
volrendObj.SetSegmentationRenderMode(segmentationRendreMode);
218+
}
219+
if (GUILayout.Button("Add segmentation (NRRD, NIFTI)"))
220+
{
221+
ImportImageFileDataset(volrendObj, (VolumeDataset dataset) =>
222+
{
223+
volrendObj.AddSegmentation(dataset);
224+
});
225+
}
226+
if (GUILayout.Button("Add segmentation (DICOM)"))
227+
{
228+
ImportDicomDataset(volrendObj, (VolumeDataset dataset) =>
229+
{
230+
volrendObj.AddSegmentation(dataset);
231+
});
232+
}
233+
if (GUILayout.Button("Clear segmentations"))
234+
{
235+
volrendObj.ClearSegmentations();
236+
}
237+
}
238+
140239
// Other settings
141240
GUILayout.Space(10);
142241
otherSettings = EditorGUILayout.Foldout(otherSettings, "Other Settings");
@@ -152,5 +251,54 @@ public override void OnInspectorGUI()
152251
volrendObj.SetSamplingRateMultiplier(EditorGUILayout.Slider("Sampling rate multiplier", volrendObj.GetSamplingRateMultiplier(), 0.2f, 2.0f));
153252
}
154253
}
254+
private static async void ImportImageFileDataset(VolumeRenderedObject targetObject, UnityAction<VolumeDataset> onLoad)
255+
{
256+
string filePath = EditorUtility.OpenFilePanel("Select a folder to load", "", "");
257+
ImageFileFormat imageFileFormat = DatasetFormatUtilities.GetImageFileFormat(filePath);
258+
if (!File.Exists(filePath))
259+
{
260+
Debug.LogError($"File doesn't exist: {filePath}");
261+
return;
262+
}
263+
if (imageFileFormat == ImageFileFormat.Unknown)
264+
{
265+
Debug.LogError($"Invalid file format: {Path.GetExtension(filePath)}");
266+
return;
267+
}
268+
269+
using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView()))
270+
{
271+
progressHandler.StartStage(1.0f, "Importing dataset");
272+
IImageFileImporter importer = ImporterFactory.CreateImageFileImporter(imageFileFormat);
273+
Task<VolumeDataset> importTask = importer.ImportAsync(filePath);
274+
await importTask;
275+
progressHandler.EndStage();
276+
277+
if (importTask.Result != null)
278+
{
279+
onLoad.Invoke(importTask.Result);
280+
}
281+
}
282+
}
283+
284+
private static async void ImportDicomDataset(VolumeRenderedObject targetObject, UnityAction<VolumeDataset> onLoad)
285+
{
286+
string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", "");
287+
if (Directory.Exists(dir))
288+
{
289+
using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView()))
290+
{
291+
progressHandler.StartStage(1.0f, "Importing dataset");
292+
Task<VolumeDataset[]> importTask = EditorDatasetImportUtils.ImportDicomDirectoryAsync(dir, progressHandler);
293+
await importTask;
294+
progressHandler.EndStage();
295+
296+
if (importTask.Result.Length > 0)
297+
{
298+
onLoad.Invoke(importTask.Result[0]);
299+
}
300+
}
301+
}
302+
}
155303
}
156304
}

0 commit comments

Comments
 (0)