Merge branch 'master' into PARCHG---Electron-Density-Volume-Renderer

This commit is contained in:
Matias Lavik 2021-10-18 13:55:34 +02:00 коммит произвёл GitHub
Родитель 640cf0794a 46901624a7
Коммит 1ca6548acf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 177 добавлений и 68 удалений

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

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