# HG changeset patch # User Sebastien Jodogne # Date 1621009824 -7200 # Node ID 75d3e2ab1fe165f4c5de7e23a78cf09ca8bafd4a # Parent f053c80ea41129aa9daa5488d543307ba1b7de8c BREAKING: SubvoxelReader using the same Z-axis ordering as ImageBuffer3D diff -r f053c80ea411 -r 75d3e2ab1fe1 OrthancStone/Sources/Toolbox/SubvoxelReader.h --- 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 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 class SubvoxelReader : 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 class SubvoxelReader : 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 class SubvoxelReader : public Internals::SubvoxelReaderBase @@ -212,10 +192,6 @@ }; - /* - See important comment above - */ - template bool SubvoxelReader::GetValue(PixelType& target, float x, @@ -269,10 +245,6 @@ } - /* - See important comment above - */ - template bool SubvoxelReader::Sample(float& f00, float& f01, @@ -326,10 +298,6 @@ } - /* - See important comment above - */ - template bool SubvoxelReader::GetFloatValue(float& target, float x, @@ -368,10 +336,6 @@ } - /* - See important comment above - */ - template bool SubvoxelReader::GetValue(PixelType& target, float x, @@ -392,7 +356,6 @@ } - template bool SubvoxelReader::GetFloatValue(float& target, float x, @@ -445,11 +408,6 @@ } - /* - See important comment above - */ - - template bool SubvoxelReader::GetValue(PixelType& target, float x, diff -r f053c80ea411 -r 75d3e2ab1fe1 OrthancStone/Sources/Volumes/ImageBuffer3D.h --- 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 diff -r f053c80ea411 -r 75d3e2ab1fe1 UnitTestsSources/VolumeRenderingTests.cpp --- 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 + 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 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 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 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),