changeset 1783:75d3e2ab1fe1

BREAKING: SubvoxelReader using the same Z-axis ordering as ImageBuffer3D
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 14 May 2021 18:30:24 +0200
parents f053c80ea411
children 42a2880d690f
files OrthancStone/Sources/Toolbox/SubvoxelReader.h OrthancStone/Sources/Volumes/ImageBuffer3D.h UnitTestsSources/VolumeRenderingTests.cpp
diffstat 3 files changed, 184 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Sources/Toolbox/SubvoxelReader.h	Fri May 14 16:30:54 2021 +0200
+++ b/OrthancStone/Sources/Toolbox/SubvoxelReader.h	Fri May 14 18:30:24 2021 +0200
@@ -34,13 +34,6 @@
 {
   namespace Internals
   {
-    /*
-    WARNING : the slice order is different between this class and ImageBuffer3D
-
-    See the comment above ImageBuffer3D declaration.
-
-    The slices are supposed to be stored in INCREASING z-order in this class!
-    */
     class SubvoxelReaderBase : public boost::noncopyable
     {
     private:
@@ -86,31 +79,32 @@
       unsigned int ComputeRow(unsigned int y,
                               unsigned int z) const
       {
-        return z * height_ + y;
+        /**
+         * The "(depth_ - 1 - z)" comes from the fact that
+         * "ImageBuffer3D" class stores its slices in DECREASING
+         * z-order along the normal. This computation makes the
+         * "SubvoxelReader" class use the same convention as
+         * "ImageBuffer3D::GetVoxelXXX()".
+         *
+         * WARNING: Until changeset 1782:f053c80ea411, "z" was
+         * directly used, causing this class to have a slice order
+         * that was reversed between "SubvoxelReader" and
+         * "ImageBuffer3D". This notably made
+         * "DicomVolumeImageMPRSlicer" and "DicomVolumeImageReslicer"
+         * inconsistent in sagittal and coronal views (the texture was
+         * flipped along the Y-axis in the canvas).
+         **/
+        return (depth_ - 1 - z) * height_ + y;
       }
     };
   }
 
     
-  /*
-  WARNING : the slice order is different between this class and ImageBuffer3D
-
-  See the comment above ImageBuffer3D declaration.
-
-  The slices are supposed to be stored in INCREASING z-order in this class!
-  */
   template <Orthanc::PixelFormat Format,
             ImageInterpolation Interpolation>
   class SubvoxelReader;
 
     
-  /*
-  WARNING : the slice order is different between this class and ImageBuffer3D
-
-  See the comment above ImageBuffer3D declaration.
-
-  The slices are supposed to be stored in INCREASING z-order in this class!
-  */
   template <Orthanc::PixelFormat Format>
   class SubvoxelReader<Format, ImageInterpolation_Nearest> : 
     public Internals::SubvoxelReaderBase
@@ -136,13 +130,6 @@
   };
     
     
-  /*
-  WARNING : the slice order is different between this class and ImageBuffer3D
-
-  See the comment above ImageBuffer3D declaration.
-
-  The slices are supposed to be stored in INCREASING z-order in this class!
-  */
   template <Orthanc::PixelFormat Format>
   class SubvoxelReader<Format, ImageInterpolation_Bilinear> : 
     public Internals::SubvoxelReaderBase
@@ -176,13 +163,6 @@
   };
     
 
-  /*
-  WARNING : the slice order is different between this class and ImageBuffer3D
-
-  See the comment above ImageBuffer3D declaration.
-
-  The slices are supposed to be stored in INCREASING z-order in this class!
-  */
   template <Orthanc::PixelFormat Format>
   class SubvoxelReader<Format, ImageInterpolation_Trilinear> : 
     public Internals::SubvoxelReaderBase
@@ -212,10 +192,6 @@
   };
 
 
-  /*
-  See important comment above
-  */
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Nearest>::GetValue(PixelType& target,
                                                                     float x,
@@ -269,10 +245,6 @@
   }
 
 
-  /*
-  See important comment above
-  */
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Bilinear>::Sample(float& f00,
                                                                    float& f01,
@@ -326,10 +298,6 @@
   }
 
 
-  /*
-  See important comment above
-  */
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Bilinear>::GetFloatValue(float& target,
                                                                           float x,
@@ -368,10 +336,6 @@
   }
 
 
-  /*
-  See important comment above
-  */
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Bilinear>::GetValue(PixelType& target,
                                                                      float x,
@@ -392,7 +356,6 @@
   }
 
 
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Trilinear>::GetFloatValue(float& target,
                                                                            float x,
@@ -445,11 +408,6 @@
   }
 
 
-  /*
-  See important comment above
-  */
-
-
   template <Orthanc::PixelFormat Format>
   bool SubvoxelReader<Format, ImageInterpolation_Trilinear>::GetValue(PixelType& target,
                                                                       float x,
--- a/OrthancStone/Sources/Volumes/ImageBuffer3D.h	Fri May 14 16:30:54 2021 +0200
+++ b/OrthancStone/Sources/Volumes/ImageBuffer3D.h	Fri May 14 18:30:24 2021 +0200
@@ -32,7 +32,9 @@
 {
   /*
 
-  This classes stores volume images sliced across the Z axis, vertically, in the decreasing Z order :
+  This classes stores volume images sliced across the Z axis,
+  vertically, in the DECREASING Z-order along the normal (this is the
+  REVERSE of the intuitive order):
 
   +---------------+
   |               |
@@ -66,6 +68,14 @@
   - 2d width  = 3d width
   - 2d height = 3d height * 3d depth
 
+  This explains the "depth_ - 1 - z" that are used throughout this class.
+
+  EXPLANATION: This allows to have the "SliceReader" and "SliceWriter"
+  accessors for axial and coronal projections to directly access the
+  same memory buffer (no memcpy is required), while being consistent
+  with the Z-axis in coronal projection. The sagittal projection
+  nevertheless needs a memcpy.
+
   */
 
   class ImageBuffer3D : public boost::noncopyable
--- a/UnitTestsSources/VolumeRenderingTests.cpp	Fri May 14 16:30:54 2021 +0200
+++ b/UnitTestsSources/VolumeRenderingTests.cpp	Fri May 14 18:30:24 2021 +0200
@@ -22,6 +22,7 @@
 #include "../OrthancStone/Sources/Scene2D/CairoCompositor.h"
 #include "../OrthancStone/Sources/Scene2D/ColorTextureSceneLayer.h"
 #include "../OrthancStone/Sources/Scene2D/CopyStyleConfigurator.h"
+#include "../OrthancStone/Sources/Toolbox/SubvoxelReader.h"
 #include "../OrthancStone/Sources/Volumes/DicomVolumeImageMPRSlicer.h"
 #include "../OrthancStone/Sources/Volumes/DicomVolumeImageReslicer.h"
 
@@ -32,6 +33,7 @@
 #include <gtest/gtest.h>
 
 
+
 static float GetPixelValue(const Orthanc::ImageAccessor& image,
                            unsigned int x,
                            unsigned int y)
@@ -298,6 +300,161 @@
 }
 
 
+TEST(VolumeRendering, Pattern)
+{
+  {
+    // Axial
+    OrthancStone::ImageBuffer3D image(Orthanc::PixelFormat_Grayscale8, 3, 3, 1, true);
+
+    {
+      OrthancStone::ImageBuffer3D::SliceWriter writer(image, OrthancStone::VolumeProjection_Axial, 0);
+      Assign3x3Pattern(writer.GetAccessor());
+    }
+
+    float a, b;
+    ASSERT_TRUE(image.GetRange(a, b));
+    ASSERT_FLOAT_EQ(0, a);
+    ASSERT_FLOAT_EQ(200, b);
+
+    ASSERT_EQ(0, image.GetVoxelGrayscale8(0, 0, 0));
+    ASSERT_EQ(25, image.GetVoxelGrayscale8(1, 0, 0));
+    ASSERT_EQ(50, image.GetVoxelGrayscale8(2, 0, 0));
+    ASSERT_EQ(75, image.GetVoxelGrayscale8(0, 1, 0));
+    ASSERT_EQ(100, image.GetVoxelGrayscale8(1, 1, 0));
+    ASSERT_EQ(125, image.GetVoxelGrayscale8(2, 1, 0));
+    ASSERT_EQ(150, image.GetVoxelGrayscale8(0, 2, 0));
+    ASSERT_EQ(175, image.GetVoxelGrayscale8(1, 2, 0));
+    ASSERT_EQ(200, image.GetVoxelGrayscale8(2, 2, 0));
+
+    float v;
+    OrthancStone::SubvoxelReader<Orthanc::PixelFormat_Grayscale8,
+                                 OrthancStone::ImageInterpolation_Nearest> reader(image);
+    
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 1.01, 0.01));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 1.01, 0.01));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 1.01, 0.01));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 2.01, 0.01));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 2.01, 0.01));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 2.01, 0.01));  ASSERT_FLOAT_EQ(200, v);
+    
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 1.99, 0.99));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 1.99, 0.99));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 1.99, 0.99));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 2.99, 0.99));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 2.99, 0.99));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 2.99, 0.99));  ASSERT_FLOAT_EQ(200, v);
+  }
+
+  {
+    // Coronal
+    OrthancStone::ImageBuffer3D image(Orthanc::PixelFormat_Grayscale8, 3, 1, 3, true);
+
+    {
+      OrthancStone::ImageBuffer3D::SliceWriter writer(image, OrthancStone::VolumeProjection_Coronal, 0);
+      Assign3x3Pattern(writer.GetAccessor());
+    }
+
+    float a, b;
+    ASSERT_TRUE(image.GetRange(a, b));
+    ASSERT_FLOAT_EQ(0, a);
+    ASSERT_FLOAT_EQ(200, b);
+
+    // "Z" is in reverse order in "Assign3x3Pattern()", because important note in "ImageBuffer3D"
+    ASSERT_EQ(0, image.GetVoxelGrayscale8(0, 0, 2));
+    ASSERT_EQ(25, image.GetVoxelGrayscale8(1, 0, 2));
+    ASSERT_EQ(50, image.GetVoxelGrayscale8(2, 0, 2));
+    ASSERT_EQ(75, image.GetVoxelGrayscale8(0, 0, 1));
+    ASSERT_EQ(100, image.GetVoxelGrayscale8(1, 0, 1));
+    ASSERT_EQ(125, image.GetVoxelGrayscale8(2, 0, 1));
+    ASSERT_EQ(150, image.GetVoxelGrayscale8(0, 0, 0));
+    ASSERT_EQ(175, image.GetVoxelGrayscale8(1, 0, 0));
+    ASSERT_EQ(200, image.GetVoxelGrayscale8(2, 0, 0));
+
+    // Ensure that "SubvoxelReader" is consistent with "image.GetVoxelGrayscale8()"
+    float v;
+    OrthancStone::SubvoxelReader<Orthanc::PixelFormat_Grayscale8,
+                                 OrthancStone::ImageInterpolation_Nearest> reader(image);
+
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 0.01, 2.01));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 0.01, 2.01));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 0.01, 2.01));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 0.01, 1.01));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 0.01, 1.01));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 0.01, 1.01));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.01, 0.01, 0.01));  ASSERT_FLOAT_EQ(200, v);
+    
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 2.99));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 0.99, 2.99));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 0.99, 2.99));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 1.99));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 0.99, 1.99));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 0.99, 1.99));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 1.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 2.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(200, v);
+  }
+
+  {
+    // Sagittal
+    OrthancStone::ImageBuffer3D image(Orthanc::PixelFormat_Grayscale8, 1, 3, 3, true);
+
+    {
+      OrthancStone::ImageBuffer3D::SliceWriter writer(image, OrthancStone::VolumeProjection_Sagittal, 0);
+      Assign3x3Pattern(writer.GetAccessor());
+    }
+
+    float a, b;
+    ASSERT_TRUE(image.GetRange(a, b));
+    ASSERT_FLOAT_EQ(0, a);
+    ASSERT_FLOAT_EQ(200, b);
+
+    // "Z" is in reverse order in "Assign3x3Pattern()", because important note in "ImageBuffer3D"
+    ASSERT_EQ(0, image.GetVoxelGrayscale8(0, 0, 2));
+    ASSERT_EQ(25, image.GetVoxelGrayscale8(0, 1, 2));
+    ASSERT_EQ(50, image.GetVoxelGrayscale8(0, 2, 2));
+    ASSERT_EQ(75, image.GetVoxelGrayscale8(0, 0, 1));
+    ASSERT_EQ(100, image.GetVoxelGrayscale8(0, 1, 1));
+    ASSERT_EQ(125, image.GetVoxelGrayscale8(0, 2, 1));
+    ASSERT_EQ(150, image.GetVoxelGrayscale8(0, 0, 0));
+    ASSERT_EQ(175, image.GetVoxelGrayscale8(0, 1, 0));
+    ASSERT_EQ(200, image.GetVoxelGrayscale8(0, 2, 0));
+
+    // Ensure that "SubvoxelReader" is consistent with "image.GetVoxelGrayscale8()"
+    float v;
+    OrthancStone::SubvoxelReader<Orthanc::PixelFormat_Grayscale8,
+                                 OrthancStone::ImageInterpolation_Nearest> reader(image);
+
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 0.01, 2.01));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 1.01, 2.01));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 2.01, 2.01));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 0.01, 1.01));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 1.01, 1.01));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 2.01, 1.01));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 0.01, 0.01));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 1.01, 0.01));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.1, 2.01, 0.01));  ASSERT_FLOAT_EQ(200, v);
+    
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 2.99));  ASSERT_FLOAT_EQ(0, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 1.99, 2.99));  ASSERT_FLOAT_EQ(25, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 2.99, 2.99));  ASSERT_FLOAT_EQ(50, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 1.99));  ASSERT_FLOAT_EQ(75, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 1.99, 1.99));  ASSERT_FLOAT_EQ(100, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 2.99, 1.99));  ASSERT_FLOAT_EQ(125, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 0.99, 0.99));  ASSERT_FLOAT_EQ(150, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 1.99, 0.99));  ASSERT_FLOAT_EQ(175, v);
+    ASSERT_TRUE(reader.GetFloatValue(v, 0.99, 2.99, 0.99));  ASSERT_FLOAT_EQ(200, v);
+  }        
+}
+
+
 TEST(VolumeRendering, Axial)
 {
   OrthancStone::CoordinateSystem3D axial(OrthancStone::LinearAlgebra::CreateVector(-0.5, -0.5, 0),