Merge branch 'master' into PARCHG---Electron-Density-Volume-Renderer
This commit is contained in:
Коммит
1ca6548acf
|
@ -31,12 +31,21 @@ namespace UnityVolumeRendering
|
|||
IEnumerable<string> fileCandidates = Directory.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
|
||||
.Where(p => p.EndsWith(".dcm", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicom", StringComparison.InvariantCultureIgnoreCase) || p.EndsWith(".dicm", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
DatasetImporterBase importer = new DICOMImporter(fileCandidates, Path.GetFileName(directoryPath));
|
||||
VolumeDataset dataset = importer.Import();
|
||||
DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(directoryPath));
|
||||
|
||||
if (dataset != null)
|
||||
List<DICOMImporter.DICOMSeries> seriesList = importer.LoadDICOMSeries();
|
||||
foreach (DICOMImporter.DICOMSeries series in seriesList)
|
||||
{
|
||||
VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset);
|
||||
// Only import the series that contains the selected file
|
||||
if(series.dicomFiles.Any(f => Path.GetFileName(f.filePath) == Path.GetFileName(filePath)))
|
||||
{
|
||||
VolumeDataset dataset = importer.ImportDICOMSeries(series);
|
||||
|
||||
if (dataset != null)
|
||||
{
|
||||
VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace UnityVolumeRendering
|
|||
|
||||
private void ImportDataset()
|
||||
{
|
||||
DatasetImporterBase importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, dataFormat, endianness, bytesToSkip);
|
||||
RawDatasetImporter importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, dataFormat, endianness, bytesToSkip);
|
||||
|
||||
VolumeDataset dataset = importer.Import();
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ namespace UnityVolumeRendering
|
|||
private bool handleMouseMovement = false;
|
||||
private Vector2 prevMousePos;
|
||||
|
||||
[MenuItem("Volume Rendering/Slice renderer")]
|
||||
static void ShowWindow()
|
||||
public static void ShowWindow()
|
||||
{
|
||||
SliceRenderingEditorWindow wnd = new SliceRenderingEditorWindow();
|
||||
wnd.Show();
|
||||
|
|
|
@ -14,7 +14,6 @@ namespace UnityVolumeRendering
|
|||
|
||||
private VolumeRenderedObject volRendObject = null;
|
||||
|
||||
[MenuItem("Volume Rendering/2D Transfer Function")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
// Close all (if any) 1D TF editor windows
|
||||
|
|
|
@ -15,7 +15,6 @@ namespace UnityVolumeRendering
|
|||
private VolumeRenderedObject volRendObject = null;
|
||||
private Texture2D histTex = null;
|
||||
|
||||
[MenuItem("Volume Rendering/1D Transfer Function")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
// Close all (if any) 2D TF editor windows
|
||||
|
|
|
@ -5,8 +5,7 @@ namespace UnityVolumeRendering
|
|||
{
|
||||
public class ValueRangeEditorWindow : EditorWindow
|
||||
{
|
||||
[MenuItem("Volume Rendering/Value range")]
|
||||
static void ShowWindow()
|
||||
public static void ShowWindow()
|
||||
{
|
||||
ValueRangeEditorWindow wnd = new ValueRangeEditorWindow();
|
||||
wnd.Show();
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace UnityVolumeRendering
|
|||
{
|
||||
public class VolumeRendererEditorFunctions
|
||||
{
|
||||
[MenuItem("Volume Rendering/Load raw dataset")]
|
||||
[MenuItem("Volume Rendering/Load dataset/Load raw dataset")]
|
||||
static void ShowDatasetImporter()
|
||||
{
|
||||
string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", "");
|
||||
|
@ -23,7 +23,7 @@ namespace UnityVolumeRendering
|
|||
}
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/Load PARCHG dataset")]
|
||||
[MenuItem("Volume Rendering/Load dataset/Load PARCHG dataset")]
|
||||
static void ShowParDatasetImporter()
|
||||
{
|
||||
string file = EditorUtility.OpenFilePanel("Select a dataset to load", "DataFiles", "");
|
||||
|
@ -37,7 +37,7 @@ namespace UnityVolumeRendering
|
|||
}
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/Load DICOM")]
|
||||
[MenuItem("Volume Rendering/Load dataset/Load DICOM")]
|
||||
static void ShowDICOMImporter()
|
||||
{
|
||||
string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", "");
|
||||
|
@ -63,9 +63,18 @@ namespace UnityVolumeRendering
|
|||
if (fileCandidates.Any())
|
||||
{
|
||||
DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(dir));
|
||||
VolumeDataset dataset = importer.Import();
|
||||
if (dataset != null)
|
||||
VolumeObjectFactory.CreateObject(dataset);
|
||||
List<DICOMImporter.DICOMSeries> seriesList = importer.LoadDICOMSeries();
|
||||
float numVolumesCreated = 0;
|
||||
foreach (DICOMImporter.DICOMSeries series in seriesList)
|
||||
{
|
||||
VolumeDataset dataset = importer.ImportDICOMSeries(series);
|
||||
if (dataset != null)
|
||||
{
|
||||
VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset);
|
||||
obj.transform.position = new Vector3(numVolumesCreated, 0, 0);
|
||||
numVolumesCreated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
Debug.LogError("Could not find any DICOM files to import.");
|
||||
|
@ -78,7 +87,7 @@ namespace UnityVolumeRendering
|
|||
}
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/Load image sequence")]
|
||||
[MenuItem("Volume Rendering/Load dataset/Load image sequence")]
|
||||
static void ShowSequenceImporter()
|
||||
{
|
||||
string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", "");
|
||||
|
@ -115,5 +124,29 @@ namespace UnityVolumeRendering
|
|||
if (objects.Length == 1)
|
||||
VolumeObjectFactory.SpawnCutoutBox(objects[0]);
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/1D Transfer Function")]
|
||||
public static void Show1DTFWindow()
|
||||
{
|
||||
TransferFunction2DEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/2D Transfer Function")]
|
||||
public static void Show2DTFWindow()
|
||||
{
|
||||
TransferFunctionEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/Slice renderer")]
|
||||
static void ShowSliceRenderer()
|
||||
{
|
||||
SliceRenderingEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
[MenuItem("Volume Rendering/Value range")]
|
||||
static void ShowValueRangeWindow()
|
||||
{
|
||||
ValueRangeEditorWindow.ShowWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,11 +111,18 @@ namespace UnityVolumeRendering
|
|||
|
||||
// Import the dataset
|
||||
DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(result.path));
|
||||
VolumeDataset dataset = importer.Import();
|
||||
// Spawn the object
|
||||
if (dataset != null)
|
||||
List<DICOMImporter.DICOMSeries> seriesList = importer.LoadDICOMSeries();
|
||||
float numVolumesCreated = 0;
|
||||
foreach (DICOMImporter.DICOMSeries series in seriesList)
|
||||
{
|
||||
VolumeObjectFactory.CreateObject(dataset);
|
||||
VolumeDataset dataset = importer.ImportDICOMSeries(series);
|
||||
// Spawn the object
|
||||
if (dataset != null)
|
||||
{
|
||||
VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset);
|
||||
obj.transform.position = new Vector3(numVolumesCreated, 0, 0);
|
||||
numVolumesCreated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,19 +17,26 @@ namespace UnityVolumeRendering
|
|||
/// 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
|
||||
public class DICOMImporter
|
||||
{
|
||||
private class DICOMSliceFile
|
||||
public class DICOMSliceFile
|
||||
{
|
||||
public AcrNemaFile file;
|
||||
public string filePath;
|
||||
public float location = 0;
|
||||
public Vector3 position = Vector3.zero;
|
||||
public float intercept = 0.0f;
|
||||
public float slope = 1.0f;
|
||||
public float pixelSpacing = 0.0f;
|
||||
public string seriesUID = "";
|
||||
public bool missingLocation = false;
|
||||
}
|
||||
|
||||
public class DICOMSeries
|
||||
{
|
||||
public List<DICOMSliceFile> dicomFiles = new List<DICOMSliceFile>();
|
||||
}
|
||||
|
||||
private IEnumerable<string> fileCandidates;
|
||||
private string datasetName;
|
||||
|
||||
|
@ -41,7 +48,7 @@ namespace UnityVolumeRendering
|
|||
datasetName = name;
|
||||
}
|
||||
|
||||
public override VolumeDataset Import()
|
||||
public List<DICOMSeries> LoadDICOMSeries()
|
||||
{
|
||||
DataElementDictionary dataElementDictionary = new DataElementDictionary();
|
||||
UidDictionary uidDictionary = new UidDictionary();
|
||||
|
@ -56,37 +63,59 @@ namespace UnityVolumeRendering
|
|||
return null;
|
||||
}
|
||||
|
||||
// Load all DICOM files
|
||||
List<DICOMSliceFile> files = new List<DICOMSliceFile>();
|
||||
bool needsCalcLoc = false;
|
||||
foreach (string filePath in fileCandidates)
|
||||
{
|
||||
DICOMSliceFile sliceFile = ReadDICOMFile(filePath);
|
||||
if(sliceFile != null)
|
||||
{
|
||||
needsCalcLoc |= sliceFile.missingLocation;
|
||||
files.Add(sliceFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Split parsed DICOM files into series (by DICOM series UID)
|
||||
Dictionary<string, DICOMSeries> seriesByUID = new Dictionary<string, DICOMSeries>();
|
||||
foreach(DICOMSliceFile file in files)
|
||||
{
|
||||
if(!seriesByUID.ContainsKey(file.seriesUID))
|
||||
{
|
||||
seriesByUID.Add(file.seriesUID, new DICOMSeries());
|
||||
}
|
||||
seriesByUID[file.seriesUID].dicomFiles.Add(file);
|
||||
}
|
||||
|
||||
Debug.Log($"Loaded {seriesByUID.Count} DICOM series");
|
||||
|
||||
return new List<DICOMSeries>(seriesByUID.Values);
|
||||
}
|
||||
|
||||
public VolumeDataset ImportDICOMSeries(DICOMSeries series)
|
||||
{
|
||||
List<DICOMSliceFile> files = series.dicomFiles;
|
||||
|
||||
// Sort files by slice location
|
||||
files.Sort((DICOMSliceFile a, DICOMSliceFile b) => { return a.location.CompareTo(b.location); });
|
||||
|
||||
// Check if the series is missing the slice location tag
|
||||
bool needsCalcLoc = false;
|
||||
foreach (DICOMSliceFile file in files)
|
||||
{
|
||||
needsCalcLoc |= file.missingLocation;
|
||||
}
|
||||
|
||||
// Calculate slice location from "Image Position" (0020,0032)
|
||||
if (needsCalcLoc)
|
||||
CalcSliceLocFromPos(files);
|
||||
|
||||
// Sort files by slice location
|
||||
files.Sort((DICOMSliceFile a, DICOMSliceFile b) => { return a.location.CompareTo(b.location); });
|
||||
Debug.Log($"Importing {files.Count} DICOM slices");
|
||||
|
||||
Debug.Log($"Imported {files.Count} datasets");
|
||||
|
||||
if(files.Count <= 1)
|
||||
if (files.Count <= 1)
|
||||
{
|
||||
Debug.LogError("Insufficient number of slices.");
|
||||
return null;
|
||||
}
|
||||
|
||||
float minLoc = (float)files[0].location;
|
||||
float maxLoc = (float)files[files.Count - 1].location;
|
||||
float locRange = maxLoc - minLoc;
|
||||
|
||||
// Create dataset
|
||||
VolumeDataset dataset = new VolumeDataset();
|
||||
dataset.datasetName = Path.GetFileName(datasetName);
|
||||
|
@ -104,10 +133,10 @@ namespace UnityVolumeRendering
|
|||
int[] pixelArr = ToPixelArray(pixelData);
|
||||
if (pixelArr == null) // This should not happen
|
||||
pixelArr = new int[pixelData.Rows * pixelData.Columns];
|
||||
|
||||
for(int iRow = 0; iRow < pixelData.Rows; iRow++)
|
||||
|
||||
for (int iRow = 0; iRow < pixelData.Rows; iRow++)
|
||||
{
|
||||
for(int iCol = 0; iCol < pixelData.Columns; iCol++)
|
||||
for (int iCol = 0; iCol < pixelData.Columns; iCol++)
|
||||
{
|
||||
int pixelIndex = (iRow * pixelData.Columns) + iCol;
|
||||
int dataIndex = (iSlice * pixelData.Columns * pixelData.Rows) + (iRow * pixelData.Columns) + iCol;
|
||||
|
@ -127,6 +156,8 @@ namespace UnityVolumeRendering
|
|||
dataset.scaleZ = Mathf.Abs(files[files.Count - 1].location - files[0].location);
|
||||
}
|
||||
|
||||
dataset.FixDimensions();
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
|
@ -138,12 +169,14 @@ namespace UnityVolumeRendering
|
|||
{
|
||||
DICOMSliceFile slice = new DICOMSliceFile();
|
||||
slice.file = file;
|
||||
|
||||
slice.filePath = filePath;
|
||||
|
||||
Tag locTag = new Tag("(0020,1041)");
|
||||
Tag posTag = new Tag("(0020,0032)");
|
||||
Tag interceptTag = new Tag("(0028,1052)");
|
||||
Tag slopeTag = new Tag("(0028,1053)");
|
||||
Tag pixelSpacingTag = new Tag("(0028,0030)");
|
||||
Tag seriesUIDTag = new Tag("(0020,000E)");
|
||||
|
||||
// Read location (optional)
|
||||
if (file.DataSet.Contains(locTag))
|
||||
|
@ -194,6 +227,13 @@ namespace UnityVolumeRendering
|
|||
slice.pixelSpacing = (float)Convert.ToDouble(elemPixelSpacing.Value[0]);
|
||||
}
|
||||
|
||||
// Read series UID
|
||||
if (file.DataSet.Contains(seriesUIDTag))
|
||||
{
|
||||
DataElement elemSeriesUID = file.DataSet[seriesUIDTag];
|
||||
slice.seriesUID = Convert.ToString(elemSeriesUID.Value[0]);
|
||||
}
|
||||
|
||||
return slice;
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 693790aa0dad1104b8eca0e740092fd9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -8,7 +8,7 @@ namespace UnityVolumeRendering
|
|||
/// <summary>
|
||||
/// Converts a directory of image slices into a VolumeDataset for volumetric rendering.
|
||||
/// </summary>
|
||||
public class ImageSequenceImporter : DatasetImporterBase
|
||||
public class ImageSequenceImporter
|
||||
{
|
||||
private string directoryPath;
|
||||
private string[] supportedImageTypes = new string[]
|
||||
|
@ -22,7 +22,7 @@ namespace UnityVolumeRendering
|
|||
this.directoryPath = directoryPath;
|
||||
}
|
||||
|
||||
public override VolumeDataset Import()
|
||||
public VolumeDataset Import()
|
||||
{
|
||||
if (!Directory.Exists(directoryPath))
|
||||
throw new NullReferenceException("No directory found: " + directoryPath);
|
||||
|
@ -36,6 +36,8 @@ namespace UnityVolumeRendering
|
|||
int[] data = FillSequentialData(dimensions, imagePaths);
|
||||
VolumeDataset dataset = FillVolumeDataset(data, dimensions);
|
||||
|
||||
dataset.FixDimensions();
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace UnityVolumeRendering
|
|||
BigEndian
|
||||
}
|
||||
|
||||
public class RawDatasetImporter : DatasetImporterBase
|
||||
public class RawDatasetImporter
|
||||
{
|
||||
string filePath;
|
||||
private int dimX;
|
||||
|
@ -41,7 +41,7 @@ namespace UnityVolumeRendering
|
|||
this.skipBytes = skipBytes;
|
||||
}
|
||||
|
||||
public override VolumeDataset Import()
|
||||
public VolumeDataset Import()
|
||||
{
|
||||
// Check that the file exists
|
||||
if(!File.Exists(filePath))
|
||||
|
@ -86,6 +86,9 @@ namespace UnityVolumeRendering
|
|||
|
||||
reader.Close();
|
||||
fs.Close();
|
||||
|
||||
dataset.FixDimensions();
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,17 +4,23 @@ using UnityEngine;
|
|||
|
||||
namespace UnityVolumeRendering
|
||||
{
|
||||
/// <summary>
|
||||
/// An imported dataset. Has a dimension and a 3D pixel array.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VolumeDataset : ScriptableObject
|
||||
{
|
||||
[SerializeField]
|
||||
public string filePath;
|
||||
|
||||
// Flattened 3D array of data sample values.
|
||||
[SerializeField]
|
||||
public int[] data;
|
||||
public double[] dataGrid;
|
||||
|
||||
[SerializeField]
|
||||
public int dimX, dimY, dimZ;
|
||||
public int nx, ny, nz;
|
||||
|
||||
[SerializeField]
|
||||
public float scaleX = 0.0f, scaleY = 0.0f, scaleZ = 0.0f;
|
||||
public float volumeScale;
|
||||
|
@ -127,7 +133,42 @@ namespace UnityVolumeRendering
|
|||
return maxDataValueDouble;
|
||||
}
|
||||
|
||||
private void CalculateValueBounds()
|
||||
/// <summary>
|
||||
/// Ensures that the dataset is not too large.
|
||||
/// </summary>
|
||||
public void FixDimensions()
|
||||
{
|
||||
int MAX_DIM = 2048; // 3D texture max size. See: https://docs.unity3d.com/Manual/class-Texture3D.html
|
||||
|
||||
if (Mathf.Max(dimX, dimY, dimZ) > MAX_DIM)
|
||||
{
|
||||
Debug.LogWarning("Dimension exceeds limits. Cropping dataset. This might result in an incomplete dataset.");
|
||||
|
||||
int newDimX = Mathf.Min(dimX, MAX_DIM);
|
||||
int newDimY = Mathf.Min(dimY, MAX_DIM);
|
||||
int newDimZ = Mathf.Min(dimZ, MAX_DIM);
|
||||
int[] newData = new int[dimX * dimY * dimZ];
|
||||
|
||||
for (int z = 0; z < newDimZ; z++)
|
||||
{
|
||||
for (int y = 0; y < newDimY; y++)
|
||||
{
|
||||
for (int x = 0; x < newDimX; x++)
|
||||
{
|
||||
int oldIndex = (z * dimX * dimY) + (y * dimX) + x;
|
||||
int newIndex = (z * newDimX * newDimY) + (y * newDimX) + x;
|
||||
newData[newIndex] = data[oldIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
data = newData;
|
||||
dimX = newDimX;
|
||||
dimY = newDimY;
|
||||
dimZ = newDimZ;
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateValueBounds()
|
||||
{
|
||||
minDataValue = int.MaxValue;
|
||||
maxDataValue = int.MinValue;
|
||||
|
|
Загрузка…
Ссылка в новой задаче