changeset 4827:4cfd96732076

Support decoding of black-and-white images (with 1 bit per pixel), notably DICOM SEG
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 25 Nov 2021 15:52:54 +0100
parents 381c2ca04860
children 2425fa7bd50d
files NEWS OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp
diffstat 4 files changed, 145 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Nov 25 13:58:10 2021 +0100
+++ b/NEWS	Thu Nov 25 15:52:54 2021 +0100
@@ -11,7 +11,7 @@
   the new storage cache.
 * New configuration option "ZipLoaderThreads" to configure the number of threads used
   to read instances from storage when createing a Zip archive/media.
-
+* Support decoding of black-and-white images (with 1 bit per pixel), notably DICOM SEG
 
 Maintenance
 -----------
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Thu Nov 25 13:58:10 2021 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Thu Nov 25 15:52:54 2021 +0100
@@ -124,6 +124,11 @@
         bitsStored_ = bitsAllocated_;
       }
 
+      if (bitsStored_ > bitsAllocated_)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
       if (!values.ParseUnsignedInteger32(highBit_, DICOM_TAG_HIGH_BIT))
       {
         highBit_ = bitsStored_ - 1;
@@ -168,7 +173,8 @@
     }
 
     if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
-        bitsAllocated_ != 24 && bitsAllocated_ != 32)
+        bitsAllocated_ != 24 && bitsAllocated_ != 32 &&
+        bitsAllocated_ != 1 /* new in Orthanc 1.9.8 */)
     {
       throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
     }
@@ -186,7 +192,26 @@
       throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
     }
 
-    bytesPerValue_ = bitsAllocated_ / 8;
+    if (bitsStored_ == 1)
+    {
+      // This is the case of DICOM SEG, new in Orthanc 1.9.8
+      if (bitsAllocated_ != 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else if (width_ % 8 != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Bad number of columns for a black-and-white image");
+      }
+      else
+      {
+        bytesPerValue_ = 0;  // Arbitrary initialization
+      }
+    }
+    else
+    {
+      bytesPerValue_ = bitsAllocated_ / 8;
+    }
 
     isPlanar_ = (planarConfiguration != 0 ? true : false);
     isSigned_ = (pixelRepresentation != 0 ? true : false);
@@ -237,7 +262,16 @@
 
   size_t DicomImageInformation::GetBytesPerValue() const
   {
-    return bytesPerValue_;
+    if (bitsStored_ == 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "This call is incompatible with black-and-white images");
+    }
+    else
+    {
+      assert(bitsAllocated_ >= 8);
+      return bytesPerValue_;
+    }
   }
 
   bool DicomImageInformation::IsSigned() const
@@ -315,6 +349,13 @@
         format = PixelFormat_Grayscale32;
         return true;
       }
+
+      if (GetBitsStored() == 1 && GetChannelCount() == 1 && !IsSigned())
+      {
+        // This is the case of DICOM SEG, new in Orthanc 1.9.8
+        format = PixelFormat_Grayscale8;
+        return true;
+      }
     }
 
     if (GetBitsStored() == 8 &&
@@ -332,10 +373,27 @@
 
   size_t DicomImageInformation::GetFrameSize() const
   {
-    return (GetHeight() *
-            GetWidth() *
-            GetBytesPerValue() *
-            GetChannelCount());
+    if (bitsStored_ == 1)
+    {
+      assert(GetWidth() % 8 == 0);
+      
+      if (GetChannelCount() == 1)
+      {
+        return GetHeight() * GetWidth() / 8;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_IncompatibleImageFormat,
+                               "Image not supported (multi-channel black-and-image image)");
+      }
+    }
+    else
+    {
+      return (GetHeight() *
+              GetWidth() *
+              GetBytesPerValue() *
+              GetChannelCount());
+    }
   }
 
 
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Thu Nov 25 13:58:10 2021 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Thu Nov 25 15:52:54 2021 +0100
@@ -88,7 +88,24 @@
        * means the order of the pixel values sent shall be R1, G1, B1,
        * R2, G2, B2, ..., etc.
        **/
-      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue() * information_.GetChannelCount();
+      if (information_.GetBitsStored() == 1)
+      {
+        if (information_.GetChannelCount() == 1 &&
+            information_.GetBitsAllocated() == 1)
+        {
+          assert(information_.GetWidth() % 8 == 0);  // Tested by DicomImageInformation
+          rowOffset_ = information_.GetWidth() / 8;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_IncompatibleImageFormat,
+                                 "Image not supported (multi-channel black-and-image image)");
+        }
+      }
+      else
+      {
+        rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue() * information_.GetChannelCount();
+      }
     }
   }
 
@@ -133,53 +150,74 @@
     assert(x < information_.GetWidth() && 
            y < information_.GetHeight() && 
            channel < information_.GetChannelCount());
+
+    const uint8_t* pixel = (reinterpret_cast<const uint8_t*>(pixelData_) + 
+                            y * rowOffset_ + frame_ * frameOffset_);
     
-    const uint8_t* pixel = reinterpret_cast<const uint8_t*>(pixelData_) + 
-      y * rowOffset_ + frame_ * frameOffset_;
-
-    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
-    if (information_.IsPlanar())
+    if (information_.GetBitsStored() == 1)
     {
-      /**
-       * Each color plane shall be sent contiguously. For RGB images,
-       * this means the order of the pixel values sent is R1, R2, R3,
-       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
-       **/
-      assert(frameOffset_ % information_.GetChannelCount() == 0);
-      pixel += channel * frameOffset_ / information_.GetChannelCount() + x * information_.GetBytesPerValue();
+      // New in Orthanc 1.9.8, notably for DICOM SEG
+      assert(information_.GetBitsAllocated() == 1 &&
+             information_.GetChannelCount() == 1 &&
+             !information_.IsPlanar());
+      
+      uint8_t b = pixel[x / 8];
+
+      if (b & (1 << (x % 8)))
+      {
+        return 255;
+      }
+      else
+      {
+        return 0;
+      }
     }
     else
     {
-      /**
-       * The sample values for the first pixel are followed by the
-       * sample values for the second pixel, etc. For RGB images, this
-       * means the order of the pixel values sent shall be R1, G1, B1,
-       * R2, G2, B2, ..., etc.
-       **/
-      pixel += channel * information_.GetBytesPerValue() + x * information_.GetChannelCount() * information_.GetBytesPerValue();
-    }
+      // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
+      if (information_.IsPlanar())
+      {
+        /**
+         * Each color plane shall be sent contiguously. For RGB images,
+         * this means the order of the pixel values sent is R1, R2, R3,
+         * ..., G1, G2, G3, ..., B1, B2, B3, etc.
+         **/
+        assert(frameOffset_ % information_.GetChannelCount() == 0);
+        pixel += channel * frameOffset_ / information_.GetChannelCount() + x * information_.GetBytesPerValue();
+      }
+      else
+      {
+        /**
+         * The sample values for the first pixel are followed by the
+         * sample values for the second pixel, etc. For RGB images, this
+         * means the order of the pixel values sent shall be R1, G1, B1,
+         * R2, G2, B2, ..., etc.
+         **/
+        pixel += channel * information_.GetBytesPerValue() + x * information_.GetChannelCount() * information_.GetBytesPerValue();
+      }
 
-    uint32_t v;
-    v = pixel[0];
-    if (information_.GetBytesPerValue() >= 2)
-      v = v + (static_cast<uint32_t>(pixel[1]) << 8);
-    if (information_.GetBytesPerValue() >= 3)
-      v = v + (static_cast<uint32_t>(pixel[2]) << 16);
-    if (information_.GetBytesPerValue() >= 4)
-      v = v + (static_cast<uint32_t>(pixel[3]) << 24);
+      uint32_t v;
+      v = pixel[0];
+      if (information_.GetBytesPerValue() >= 2)
+        v = v + (static_cast<uint32_t>(pixel[1]) << 8);
+      if (information_.GetBytesPerValue() >= 3)
+        v = v + (static_cast<uint32_t>(pixel[2]) << 16);
+      if (information_.GetBytesPerValue() >= 4)
+        v = v + (static_cast<uint32_t>(pixel[3]) << 24);
 
-    v = v >> information_.GetShift();
+      v = v >> information_.GetShift();
 
-    if (v & signMask_)
-    {
-      // Signed value
-      // http://en.wikipedia.org/wiki/Two%27s_complement#Subtraction_from_2N
-      return -static_cast<int32_t>(mask_) + static_cast<int32_t>(v & mask_) - 1;
-    }
-    else
-    {
-      // Unsigned value
-      return static_cast<int32_t>(v & mask_);
+      if (v & signMask_)
+      {
+        // Signed value
+        // http://en.wikipedia.org/wiki/Two%27s_complement#Subtraction_from_2N
+        return -static_cast<int32_t>(mask_) + static_cast<int32_t>(v & mask_) - 1;
+      }
+      else
+      {
+        // Unsigned value
+        return static_cast<int32_t>(v & mask_);
+      }
     }
   }
 
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Nov 25 13:58:10 2021 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Nov 25 15:52:54 2021 +0100
@@ -542,6 +542,7 @@
     bool fastVersionSuccess = false;
     PixelFormat sourceFormat;
     if (!info.IsPlanar() &&
+        info.GetBitsStored() != 1 &&  // Black-and-white image, notably DICOM SEG (new in Orthanc 1.9.8)
         info.ExtractPixelFormat(sourceFormat, false))
     {
       try