From 6dfab009ba5c79766b4e259896402519828d4d40 Mon Sep 17 00:00:00 2001 From: Vahid <92767644+vahpy@users.noreply.github.com> Date: Sat, 23 Oct 2021 22:14:47 +1100 Subject: [PATCH 1/3] New implementation for texture downscaling Less code and less memory usage --- Assets/Scripts/VolumeData/VolumeDataset.cs | 181 ++++----------------- 1 file changed, 29 insertions(+), 152 deletions(-) diff --git a/Assets/Scripts/VolumeData/VolumeDataset.cs b/Assets/Scripts/VolumeData/VolumeDataset.cs index 81ad180..b6fa9ac 100644 --- a/Assets/Scripts/VolumeData/VolumeDataset.cs +++ b/Assets/Scripts/VolumeData/VolumeDataset.cs @@ -13,12 +13,8 @@ namespace UnityVolumeRendering [SerializeField] public int[] data = null; - // Flattened 3D array of downscaled data sample values. [SerializeField] - public float[] downScaledData = null; - - [SerializeField] - public int dimX, dimY, dimZ, halfDimX, halfDimY, halfDimZ; + public int dimX, dimY, dimZ; [SerializeField] public float scaleX = 0.0f, scaleY = 0.0f, scaleZ = 0.0f; @@ -30,23 +26,12 @@ namespace UnityVolumeRendering private int maxDataValue = int.MinValue; private Texture3D dataTexture = null; private Texture3D gradientTexture = null; - private bool downScaled = false; public Texture3D GetDataTexture() - { - return GetDataTexture(false); - } - - public Texture3D GetDataTexture(bool forceDownscaling) { if (dataTexture == null) { - if (forceDownscaling /* || some conditions */) - { - dataTexture = CreateDownScaledTextureInternal(); - downScaled = true; - } - else dataTexture = CreateTextureInternal(); + dataTexture = CreateTextureInternal(); } return dataTexture; } @@ -55,8 +40,7 @@ namespace UnityVolumeRendering { if (gradientTexture == null) { - if (downScaled) gradientTexture = CreateDownScaledGradientTextureInternal(); - else gradientTexture = CreateGradientTextureInternal(); + gradientTexture = CreateGradientTextureInternal(); } return gradientTexture; } @@ -82,32 +66,40 @@ namespace UnityVolumeRendering { 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) + while (Mathf.Max(dimX, dimY, dimZ) > MAX_DIM) { - Debug.LogWarning("Dimension exceeds limits. Cropping dataset. This might result in an incomplete dataset."); + Debug.LogWarning("Dimension exceeds limits (maximum: "+MAX_DIM+"). Dataset is downscaled by 2 on each axis!"); + DownScaleData(); + } + } - 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]; + /// + /// Downscales the data by averaging 8 voxels per each new voxel, + /// and replaces downscaled data with the original data + /// + public void DownScaleData() + { + int halfDimX = dimX / 2 + dimX % 2; + int halfDimY = dimY / 2 + dimY % 2; + int halfDimZ = dimZ / 2 + dimZ % 2; + int[] downScaledData = new int[halfDimX * halfDimY * halfDimZ]; - for (int z = 0; z < newDimZ; z++) + for (int x = 0; x < halfDimX; x++) + { + for (int y = 0; y < halfDimY; y++) { - for (int y = 0; y < newDimY; y++) + for (int z = 0; z < halfDimZ; z++) { - 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]; - } + downScaledData[x + y * halfDimX + z * (halfDimX * halfDimY)] = Mathf.RoundToInt(GetAvgerageVoxelValues(x * 2, y * 2, z * 2)); } } - data = newData; - dimX = newDimX; - dimY = newDimY; - dimZ = newDimZ; } + + //Update data & data dimensions + data = downScaledData; + dimX = halfDimX; + dimY = halfDimY; + dimZ = halfDimZ; } private void CalculateValueBounds() @@ -214,117 +206,6 @@ namespace UnityVolumeRendering return texture; } - - private Texture3D CreateDownScaledTextureInternal() - { - halfDimX = dimX / 2 + dimX % 2; - halfDimY = dimY / 2 + dimY % 2; - halfDimZ = dimZ / 2 + dimZ % 2; - - TextureFormat texformat = SystemInfo.SupportsTextureFormat(TextureFormat.RHalf) ? TextureFormat.RHalf : TextureFormat.RFloat; - - Texture3D texture = new Texture3D(halfDimX, halfDimY, halfDimZ, texformat, false); - texture.wrapMode = TextureWrapMode.Clamp; - - downScaledData = new float[halfDimX * halfDimY * halfDimZ]; - - int minValue = GetMinDataValue(); - int maxValue = GetMaxDataValue(); - int maxRange = maxValue - minValue; - - bool isHalfFloat = texformat == TextureFormat.RHalf; - int sampleSize = isHalfFloat ? 2 : 4; - byte[] bytes; - try - { - // Create a byte array for filling the texture. Store has half (16 bit) or single (32 bit) float values. - bytes = new byte[halfDimX * halfDimY * halfDimZ * sampleSize]; // This can cause OutOfMemoryException - } - catch (OutOfMemoryException ex) - { - Debug.LogWarning("Out of memory when creating texture. Using fallback method."); - bytes = null; - } - - for (int x = 0; x < halfDimX; x++) - { - for (int y = 0; y < halfDimY; y++) - { - for (int z = 0; z < halfDimZ; z++) - { - float avg = GetAvgerageVoxelValues(x * 2, y * 2, z * 2); - int index = x + y * halfDimX + z * (halfDimX * halfDimY); - downScaledData[index] = (avg - minValue) / maxRange; //Store downscaled data for gradient texture generation, etc. - - if (bytes == null) - { - texture.SetPixel(x, y, z, new Color(downScaledData[index], 0.0f, 0.0f, 0.0f)); - } - else - { - byte[] pixelBytes = isHalfFloat ? BitConverter.GetBytes(Mathf.FloatToHalf(downScaledData[index])) : BitConverter.GetBytes(downScaledData[index]); - Array.Copy(pixelBytes, 0, bytes, index * sampleSize, sampleSize); - } - } - } - } - - if (bytes != null) texture.SetPixelData(bytes, 0); - - texture.Apply(); - return texture; - } - - private Texture3D CreateDownScaledGradientTextureInternal() - { - if (!downScaled || downScaledData == null) throw new InvalidOperationException("Downscaled gradient texture must be generated after downscaled main texture!"); - - TextureFormat texformat = SystemInfo.SupportsTextureFormat(TextureFormat.RGBAHalf) ? TextureFormat.RGBAHalf : TextureFormat.RGBAFloat; - Texture3D texture = new Texture3D(halfDimX, halfDimY, halfDimZ, texformat, false); - texture.wrapMode = TextureWrapMode.Clamp; - - Color[] cols; - try - { - cols = new Color[downScaledData.Length]; - } - catch (OutOfMemoryException ex) - { - cols = null; - } - for (int x = 0; x < halfDimX; x++) - { - for (int y = 0; y < halfDimY; y++) - { - for (int z = 0; z < halfDimZ; z++) - { - int iData = x + y * halfDimX + z * (halfDimX * halfDimY); - - float x1 = downScaledData[Math.Min(x + 1, halfDimX - 1) + y * halfDimX + z * (halfDimX * halfDimY)]; - float x2 = downScaledData[Math.Max(x - 1, 0) + y * halfDimX + z * (halfDimX * halfDimY)]; - float y1 = downScaledData[x + Math.Min(y + 1, halfDimY - 1) * halfDimX + z * (halfDimX * halfDimY)]; - float y2 = downScaledData[x + Math.Max(y - 1, 0) * halfDimX + z * (halfDimX * halfDimY)]; - float z1 = downScaledData[x + y * halfDimX + Math.Min(z + 1, halfDimZ - 1) * (halfDimX * halfDimY)]; - float z2 = downScaledData[x + y * halfDimX + Math.Max(z - 1, 0) * (halfDimX * halfDimY)]; - - Vector3 grad = new Vector3(x2 - x1, y2 - y1, z2 - z1); - - if (cols == null) - { - texture.SetPixel(x, y, z, new Color(grad.x, grad.y, grad.z, downScaledData[iData])); - } - else - { - cols[iData] = new Color(grad.x, grad.y, grad.z, downScaledData[iData]); - } - } - } - } - if (cols != null) texture.SetPixels(cols); - texture.Apply(); - return texture; - } - public float GetAvgerageVoxelValues(int x, int y, int z) { // if a dimension length is not an even number @@ -351,9 +232,5 @@ namespace UnityVolumeRendering { return data[x + y * dimX + z * (dimX * dimY)]; } - public float GetDownScaledData(int x, int y, int z) - { - return downScaledData[x + y * halfDimX + z * (halfDimX * halfDimY)]; - } } } From 5f0d380920d5c5d793d50fe1776bc7b4b627ef06 Mon Sep 17 00:00:00 2001 From: Vahid <92767644+vahpy@users.noreply.github.com> Date: Sat, 23 Oct 2021 22:20:08 +1100 Subject: [PATCH 2/3] Added optional downscaled option --- Assets/Scripts/Importing/DICOMImporter.cs | 18 +++++++++++++----- .../Scripts/Importing/ImageSequenceImporter.cs | 10 +++++++++- Assets/Scripts/Importing/RawDatasetImporter.cs | 14 ++++++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Assets/Scripts/Importing/DICOMImporter.cs b/Assets/Scripts/Importing/DICOMImporter.cs index 84ff334..61bacff 100644 --- a/Assets/Scripts/Importing/DICOMImporter.cs +++ b/Assets/Scripts/Importing/DICOMImporter.cs @@ -65,7 +65,10 @@ namespace UnityVolumeRendering // Load all DICOM files List files = new List(); - foreach (string filePath in fileCandidates) + + IEnumerable sortedFiles = fileCandidates.OrderBy(s => s); + + foreach (string filePath in sortedFiles) { DICOMSliceFile sliceFile = ReadDICOMFile(filePath); if(sliceFile != null) @@ -90,13 +93,10 @@ namespace UnityVolumeRendering return new List(seriesByUID.Values); } - public VolumeDataset ImportDICOMSeries(DICOMSeries series) + public VolumeDataset ImportDICOMSeries(DICOMSeries series, bool forceDownScaling = false) { List 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) @@ -107,6 +107,9 @@ namespace UnityVolumeRendering // 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"); @@ -156,6 +159,11 @@ namespace UnityVolumeRendering dataset.scaleZ = Mathf.Abs(files[files.Count - 1].location - files[0].location); } + if (forceDownScaling) + { + dataset.DownScaleData(); + } + dataset.FixDimensions(); return dataset; diff --git a/Assets/Scripts/Importing/ImageSequenceImporter.cs b/Assets/Scripts/Importing/ImageSequenceImporter.cs index 604c08e..887a445 100644 --- a/Assets/Scripts/Importing/ImageSequenceImporter.cs +++ b/Assets/Scripts/Importing/ImageSequenceImporter.cs @@ -11,15 +11,18 @@ namespace UnityVolumeRendering public class ImageSequenceImporter { private string directoryPath; + private bool forceDownScaling; private string[] supportedImageTypes = new string[] { "*.png", "*.jpg", + "*.jpeg" }; - public ImageSequenceImporter(string directoryPath) + public ImageSequenceImporter(string directoryPath, bool forceDownScaling = false) { this.directoryPath = directoryPath; + this.forceDownScaling = forceDownScaling; } public VolumeDataset Import() @@ -36,6 +39,11 @@ namespace UnityVolumeRendering int[] data = FillSequentialData(dimensions, imagePaths); VolumeDataset dataset = FillVolumeDataset(data, dimensions); + if (forceDownScaling) + { + dataset.DownScaleData(); + } + dataset.FixDimensions(); return dataset; diff --git a/Assets/Scripts/Importing/RawDatasetImporter.cs b/Assets/Scripts/Importing/RawDatasetImporter.cs index 49ea545..54a58e6 100644 --- a/Assets/Scripts/Importing/RawDatasetImporter.cs +++ b/Assets/Scripts/Importing/RawDatasetImporter.cs @@ -29,8 +29,8 @@ namespace UnityVolumeRendering private DataContentFormat contentFormat; private Endianness endianness; private int skipBytes; - - public RawDatasetImporter(string filePath, int dimX, int dimY, int dimZ, DataContentFormat contentFormat, Endianness endianness, int skipBytes) + private bool forceDownScaling; + public RawDatasetImporter(string filePath, int dimX, int dimY, int dimZ, DataContentFormat contentFormat, Endianness endianness, int skipBytes, bool forceDownScaling = false) { this.filePath = filePath; this.dimX = dimX; @@ -39,12 +39,13 @@ namespace UnityVolumeRendering this.contentFormat = contentFormat; this.endianness = endianness; this.skipBytes = skipBytes; + this.forceDownScaling = forceDownScaling; } public VolumeDataset Import() { // Check that the file exists - if(!File.Exists(filePath)) + if (!File.Exists(filePath)) { Debug.LogError("The file does not exist: " + filePath); return null; @@ -86,8 +87,13 @@ namespace UnityVolumeRendering reader.Close(); fs.Close(); + if (forceDownScaling) + { + dataset.DownScaleData(); + } + dataset.FixDimensions(); - + return dataset; } From a36f01e819c4c0415bc5b40a27db4905d56d58ac Mon Sep 17 00:00:00 2001 From: Vahid <92767644+vahpy@users.noreply.github.com> Date: Sat, 23 Oct 2021 22:35:41 +1100 Subject: [PATCH 3/3] Added popup dialog for optional downscaling --- .../Editor/RAWDatasetImporterEditorWIndow.cs | 13 +++++++++-- .../Editor/VolumeRendererEditorFunctions.cs | 22 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Assets/Editor/RAWDatasetImporterEditorWIndow.cs b/Assets/Editor/RAWDatasetImporterEditorWIndow.cs index 6398d1a..9916864 100644 --- a/Assets/Editor/RAWDatasetImporterEditorWIndow.cs +++ b/Assets/Editor/RAWDatasetImporterEditorWIndow.cs @@ -43,8 +43,17 @@ namespace UnityVolumeRendering private void ImportDataset() { - RawDatasetImporter importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, dataFormat, endianness, bytesToSkip); - + RawDatasetImporter importer; + if (EditorUtility.DisplayDialog("Optional DownScaling", + "Do you want to downscale texture even if the dimensions are within the limits?", "Yes", "No")) + { + importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, dataFormat, endianness, bytesToSkip, true); + } + else + { + importer = new RawDatasetImporter(fileToImport, dimX, dimY, dimZ, dataFormat, endianness, bytesToSkip, false); + } + VolumeDataset dataset = importer.Import(); if (dataset != null) diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index 3305308..b36c4d5 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -51,9 +51,17 @@ namespace UnityVolumeRendering DICOMImporter importer = new DICOMImporter(fileCandidates, Path.GetFileName(dir)); List seriesList = importer.LoadDICOMSeries(); float numVolumesCreated = 0; + + bool forceDownScaling = false; + if (EditorUtility.DisplayDialog("Optional DownScaling", + "Do you want to downscale texture even if the dimensions are within the limits?", "Yes", "No")) + { + forceDownScaling = true; + } + foreach (DICOMImporter.DICOMSeries series in seriesList) { - VolumeDataset dataset = importer.ImportDICOMSeries(series); + VolumeDataset dataset = importer.ImportDICOMSeries(series, forceDownScaling); if (dataset != null) { VolumeRenderedObject obj = VolumeObjectFactory.CreateObject(dataset); @@ -77,9 +85,19 @@ namespace UnityVolumeRendering static void ShowSequenceImporter() { string dir = EditorUtility.OpenFolderPanel("Select a folder to load", "", ""); + if (Directory.Exists(dir)) { - ImageSequenceImporter importer = new ImageSequenceImporter(dir); + ImageSequenceImporter importer; + if (EditorUtility.DisplayDialog("Optional DownScaling", + "Do you want to downscale texture even if the dimensions are within the limits?", "Yes", "No")) + { + importer = new ImageSequenceImporter(dir,true); + } + else + { + importer = new ImageSequenceImporter(dir,false); + } VolumeDataset dataset = importer.Import(); if (dataset != null) VolumeObjectFactory.CreateObject(dataset);