changeset 167:605247fc8758

Fix issue #144 (OrthancWSIDicomizer PhotometricInterpretation)
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 12 Jul 2019 12:00:31 +0200
parents f0dac1e8f736
children d3aea0af03e1
files Applications/DicomToTiff.cpp Applications/Dicomizer.cpp Framework/DicomizerParameters.cpp Framework/ImageToolbox.cpp Framework/ImageToolbox.h Framework/Inputs/PyramidWithRawTiles.cpp Framework/Outputs/DicomPyramidWriter.cpp Framework/Outputs/DicomPyramidWriter.h Framework/Outputs/HierarchicalTiffWriter.cpp Framework/Outputs/HierarchicalTiffWriter.h Framework/Outputs/MultiframeDicomWriter.cpp Framework/Outputs/MultiframeDicomWriter.h NEWS
diffstat 13 files changed, 171 insertions(+), 111 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/DicomToTiff.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Applications/DicomToTiff.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -183,20 +183,36 @@
 static void Run(OrthancWSI::ITiledPyramid& source,
                 const boost::program_options::variables_map& options)
 {
+  bool reencode = (options.count(OPTION_REENCODE) &&
+                   options[OPTION_REENCODE].as<bool>());
+
+  Orthanc::PhotometricInterpretation targetPhotometric;
+
+  if (reencode)
+  {
+    // The DicomToTiff tool only creates TIFF images with JPEG tiles (*)
+    targetPhotometric = Orthanc::PhotometricInterpretation_YBRFull422;
+  }
+  else
+  {
+    targetPhotometric = source.GetPhotometricInterpretation();
+  }
+
   OrthancWSI::HierarchicalTiffWriter target(options[OPTION_OUTPUT].as<std::string>(),
                                             source.GetPixelFormat(), 
-                                            OrthancWSI::ImageCompression_Jpeg,
+                                            OrthancWSI::ImageCompression_Jpeg,  // (*)
                                             source.GetTileWidth(), 
-                                            source.GetTileHeight());
-
-  bool reencode = (options.count(OPTION_REENCODE) &&
-                   options[OPTION_REENCODE].as<bool>());
+                                            source.GetTileHeight(),
+                                            targetPhotometric);
 
   if (options.count(OPTION_JPEG_QUALITY))
   {
     target.SetJpegQuality(options[OPTION_JPEG_QUALITY].as<int>());
   }
 
+  LOG(WARNING) << "Source photometric interpretation: " << EnumerationToString(source.GetPhotometricInterpretation());
+  LOG(WARNING) << "Target photometric interpretation: " << EnumerationToString(targetPhotometric);
+
   std::auto_ptr<Orthanc::ImageAccessor> empty(CreateEmptyTile(target, options));
 
   for (unsigned int level = 0; level < source.GetLevelCount(); level++)
@@ -226,15 +242,31 @@
         bool missing = false;
         bool success = true;
 
-        // Give a first try to get the raw tile
         std::string tile;
         OrthancWSI::ImageCompression compression;
+
         if (source.ReadRawTile(tile, compression, level, tileX, tileY))
         {
-          if (reencode ||
-              compression == OrthancWSI::ImageCompression_Jpeg)
+          if (compression == OrthancWSI::ImageCompression_Jpeg)
+          {
+            // Transcoding of JPEG tiles
+            target.WriteRawTile(tile, compression, level, tileX, tileY);
+          }
+          else if (reencode)
           {
-            target.WriteRawTile(tile, compression, level, tileX, tileY);
+            std::auto_ptr<Orthanc::ImageAccessor> decoded;
+
+            if (compression == OrthancWSI::ImageCompression_None)
+            {
+              decoded.reset(OrthancWSI::ImageToolbox::DecodeRawTile(tile, source.GetPixelFormat(),
+                                                                    source.GetTileWidth(), source.GetTileHeight()));
+            }
+            else
+            {
+              decoded.reset(OrthancWSI::ImageToolbox::DecodeTile(tile, compression));
+            }
+                
+            target.EncodeTile(*decoded, level, tileX, tileY);
           }
           else
           {
@@ -243,23 +275,8 @@
         }
         else
         {
-          // Give a second try to get the decoded tile
-          compression = OrthancWSI::ImageCompression_Unknown;
-
-          std::auto_ptr<Orthanc::ImageAccessor> tile(source.DecodeTile(level, tileX, tileY));
-          if (tile.get() == NULL)
-          {
-            // Unable to read the raw tile or to decode it: The tile is missing (sparse tiling)
-            missing = true;
-          }
-          else if (reencode)
-          {
-            target.EncodeTile(*empty, level, tileX, tileY);
-          }
-          else
-          {
-            success = false;  // Re-encoding is mandatory
-          }
+          // Unable to read the raw tile: The tile is missing (sparse tiling)
+          missing = true;
         }
 
         if (!success)
--- a/Applications/Dicomizer.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Applications/Dicomizer.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -84,6 +84,8 @@
                              OrthancWSI::ITiledPyramid& source,
                              const OrthancWSI::DicomizerParameters& parameters)
 {
+  LOG(WARNING) << "Transcoding the source pyramid (not re-encoding)";
+
   Orthanc::BagOfTasks tasks;
 
   for (unsigned int i = 0; i < source.GetLevelCount(); i++)
@@ -93,8 +95,6 @@
     target.AddLevel(source.GetLevelWidth(i), source.GetLevelHeight(i));
   }
 
-  LOG(WARNING) << "Transcoding the source pyramid";
-
   OrthancWSI::TranscodeTileCommand::PrepareBagOfTasks(tasks, target, source, parameters);
   OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
 }
@@ -104,6 +104,8 @@
                                OrthancWSI::ITiledPyramid& source,
                                const OrthancWSI::DicomizerParameters& parameters)
 {
+  LOG(WARNING) << "Re-encoding the source pyramid (not transcoding, slower process)";
+
   Orthanc::BagOfTasks tasks;
 
   unsigned int levelsCount = parameters.GetPyramidLevelsCount(target, source);
@@ -160,12 +162,15 @@
                        OrthancWSI::ITiledPyramid& source,
                        const DcmDataset& dataset,
                        const OrthancWSI::DicomizerParameters& parameters,
-                       const OrthancWSI::ImagedVolumeParameters& volume)
+                       const OrthancWSI::ImagedVolumeParameters& volume,
+                       OrthancWSI::ImageCompression sourceCompression)
 {
   OrthancWSI::TiledPyramidStatistics stats(source);
 
   LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth() << "x" << stats.GetTileHeight();
-  LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(stats.GetPixelFormat());
+  LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(source.GetPixelFormat());
+  LOG(WARNING) << "Source photometric interpretation: " << Orthanc::EnumerationToString(source.GetPhotometricInterpretation());
+  LOG(WARNING) << "Source compression: " << EnumerationToString(sourceCompression);
   LOG(WARNING) << "Smoothing is " << (parameters.IsSmoothEnabled() ? "enabled" : "disabled");
 
   if (parameters.IsRepaintBackground())
@@ -180,26 +185,60 @@
     LOG(WARNING) << "No repainting of the background";
   }
 
+  Orthanc::PhotometricInterpretation targetPhotometric;
+  bool transcoding;
+
+  if (parameters.IsForceReencode() ||
+      parameters.IsReconstructPyramid() ||
+      sourceCompression != parameters.GetTargetCompression())
+  {
+    // The tiles of the source image will be re-encoded
+    transcoding = false;
+    
+    switch (parameters.GetTargetCompression())
+    {
+      case OrthancWSI::ImageCompression_Jpeg:
+      case OrthancWSI::ImageCompression_Jpeg2000:
+        targetPhotometric = Orthanc::PhotometricInterpretation_YBRFull422;
+        break;
+
+      case OrthancWSI::ImageCompression_None:
+        targetPhotometric = Orthanc::PhotometricInterpretation_RGB;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+  else
+  {
+    // Transcoding: The tiles are copied (no re-encoding)
+    transcoding = true;
+    targetPhotometric = source.GetPhotometricInterpretation();
+  }
+  
   OrthancWSI::DicomPyramidWriter target(output, dataset, 
                                         source.GetPixelFormat(), 
                                         parameters.GetTargetCompression(),
                                         parameters.GetTargetTileWidth(source),
                                         parameters.GetTargetTileHeight(source),
                                         parameters.GetDicomMaxFileSize(),
-                                        volume);
+                                        volume, targetPhotometric);
   target.SetJpegQuality(parameters.GetJpegQuality());
 
   LOG(WARNING) << "Size of target tiles: " << target.GetTileWidth() << "x" << target.GetTileHeight();
+  LOG(WARNING) << "Target photometric interpretation: " << Orthanc::EnumerationToString(targetPhotometric);
 
-  if (target.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg)
+  if (!transcoding &&
+      target.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg)
   {
-    LOG(WARNING) << "Target image compression: Jpeg with quality "
+    LOG(WARNING) << "Target compression: Jpeg with quality "
                  << static_cast<int>(target.GetJpegQuality());
     target.SetJpegQuality(target.GetJpegQuality());
   }
   else
   {
-    LOG(WARNING) << "Target image compression: "
+    LOG(WARNING) << "Target compression: "
                  << OrthancWSI::EnumerationToString(target.GetImageCompression());
   }
 
@@ -219,13 +258,13 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
   }
 
-  if (parameters.IsReconstructPyramid())
+  if (transcoding)
   {
-    ReconstructPyramid(target, stats, parameters);
+    TranscodePyramid(target, stats, parameters);
   }
   else
   {
-    TranscodePyramid(target, stats, parameters);
+    ReconstructPyramid(target, stats, parameters);
   }
 
   target.Flush();
@@ -863,6 +902,7 @@
 }
 
 
+
 OrthancWSI::ITiledPyramid* OpenInputPyramid(OrthancWSI::ImageCompression& sourceCompression,
                                             const std::string& path,
                                             const OrthancWSI::DicomizerParameters& parameters)
@@ -954,7 +994,7 @@
       EnrichDataset(*dataset, *source, sourceCompression, parameters, volume);
 
       std::auto_ptr<OrthancWSI::IFileTarget> output(parameters.CreateTarget());
-      Recompress(*output, *source, *dataset, parameters, volume);
+      Recompress(*output, *source, *dataset, parameters, volume, sourceCompression);
     }
   }
   catch (Orthanc::OrthancException& e)
--- a/Framework/DicomizerParameters.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/DicomizerParameters.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -156,12 +156,6 @@
   unsigned int DicomizerParameters::GetPyramidLevelsCount(const IPyramidWriter& target,
                                                           const ITiledPyramid& source) const
   {
-    if (!reconstructPyramid_)
-    {
-      // Only makes sense if reconstructing the pyramid
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-
     if (pyramidLevelsCount_ != 0)
     {
       return pyramidLevelsCount_;
@@ -205,12 +199,6 @@
   unsigned int DicomizerParameters::GetPyramidLowerLevelsCount(const IPyramidWriter& target,
                                                                const ITiledPyramid& source) const
   {
-    if (!reconstructPyramid_)
-    {
-      // Only makes sense if reconstructing the pyramid
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-
     if (pyramidLowerLevelsCount_ != 0)
     {
       return pyramidLowerLevelsCount_;
--- a/Framework/ImageToolbox.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/ImageToolbox.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -158,6 +158,25 @@
     }
 
 
+    Orthanc::ImageAccessor* DecodeRawTile(const std::string& source,
+                                          Orthanc::PixelFormat format,
+                                          unsigned int width,
+                                          unsigned int height)
+    {
+      unsigned int bpp = GetBytesPerPixel(format);
+
+      if (bpp * width * height != source.size())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+
+      Orthanc::ImageAccessor accessor;
+      accessor.AssignReadOnly(format, width, height, bpp * width, source.empty() ? NULL : source.c_str());
+
+      return Orthanc::Image::Clone(accessor);
+    }
+
+
     void EncodeTile(std::string& target,
                     const Orthanc::ImageAccessor& source,
                     ImageCompression compression,
--- a/Framework/ImageToolbox.h	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/ImageToolbox.h	Fri Jul 12 12:00:31 2019 +0200
@@ -54,6 +54,11 @@
     Orthanc::ImageAccessor* DecodeTile(const std::string& source,
                                        ImageCompression compression);
     
+    Orthanc::ImageAccessor* DecodeRawTile(const std::string& source,
+                                          Orthanc::PixelFormat format,
+                                          unsigned int width,
+                                          unsigned int height);
+    
     void EncodeTile(std::string& target,
                     const Orthanc::ImageAccessor& source,
                     ImageCompression compression,
--- a/Framework/Inputs/PyramidWithRawTiles.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Inputs/PyramidWithRawTiles.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -22,10 +22,7 @@
 #include "../PrecompiledHeadersWSI.h"
 #include "PyramidWithRawTiles.h"
 
-#include <Core/Images/PngReader.h>
-#include <Core/Images/JpegReader.h>
-#include <Core/OrthancException.h>
-#include "../Jpeg2000Reader.h"
+#include "../ImageToolbox.h"
 
 namespace OrthancWSI
 {
@@ -40,39 +37,13 @@
     {
       return NULL;
     }
-
-    std::auto_ptr<Orthanc::ImageAccessor> result;
-
-    switch (compression)
+    else if (compression == ImageCompression_None)
     {
-      case ImageCompression_None:
-        result.reset(new Orthanc::ImageAccessor);
-        result->AssignReadOnly(GetPixelFormat(), 
-                               GetTileWidth(),
-                               GetTileHeight(), 
-                               GetBytesPerPixel(GetPixelFormat()) * GetTileWidth(),
-                               tile.c_str());
-        break;
-
-      case ImageCompression_Jpeg:
-        result.reset(new Orthanc::JpegReader);
-        dynamic_cast<Orthanc::JpegReader&>(*result).ReadFromMemory(tile);
-        break;
-
-      case ImageCompression_Png:
-        result.reset(new Orthanc::PngReader);
-        dynamic_cast<Orthanc::PngReader&>(*result).ReadFromMemory(tile);
-        break;
-
-      case ImageCompression_Jpeg2000:
-        result.reset(new Jpeg2000Reader);
-        dynamic_cast<Jpeg2000Reader&>(*result).ReadFromMemory(tile);
-        break;
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      return ImageToolbox::DecodeRawTile(tile, GetPixelFormat(), GetTileWidth(), GetTileHeight());
     }
-
-    return result.release();
+    else
+    {
+      return ImageToolbox::DecodeTile(tile, compression);
+    }
   }
 }
--- a/Framework/Outputs/DicomPyramidWriter.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/DicomPyramidWriter.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -140,7 +140,7 @@
       {
         writer = new MultiframeDicomWriter
           (dataset_, GetImageCompression(), GetPixelFormat(), level.width_, level.height_, 
-           GetTileWidth(), GetTileHeight());
+           GetTileWidth(), GetTileHeight(), photometric_);
         writers_[z] = writer;
       }
 
@@ -166,14 +166,16 @@
                                          unsigned int tileWidth,
                                          unsigned int tileHeight,
                                          size_t maxSize,   // If "0", no automatic flushing
-                                         const ImagedVolumeParameters&  volume) :
+                                         const ImagedVolumeParameters&  volume,
+                                         Orthanc::PhotometricInterpretation photometric) :
     PyramidWriterBase(pixelFormat, compression, tileWidth, tileHeight),
     target_(target),
     dataset_(dataset),
     maxSize_(maxSize),
     countTiles_(0),
     countInstances_(0),
-    volume_(volume)
+    volume_(volume),
+    photometric_(photometric)
   {
   }
 
--- a/Framework/Outputs/DicomPyramidWriter.h	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/DicomPyramidWriter.h	Fri Jul 12 12:00:31 2019 +0200
@@ -39,8 +39,9 @@
     size_t             maxSize_;
     size_t             countTiles_;
     unsigned int       countInstances_;
-
-    const ImagedVolumeParameters&  volume_;
+      
+    const ImagedVolumeParameters&       volume_;
+    Orthanc::PhotometricInterpretation  photometric_;
 
     void FlushInternal(MultiframeDicomWriter& writer,
                        bool force);
@@ -70,7 +71,8 @@
                        unsigned int tileWidth,
                        unsigned int tileHeight,
                        size_t maxSize,   // If "0", no automatic flushing
-                       const ImagedVolumeParameters&  volume);
+                       const ImagedVolumeParameters&  volume,
+                       Orthanc::PhotometricInterpretation photometric);
 
     virtual ~DicomPyramidWriter();
 
--- a/Framework/Outputs/HierarchicalTiffWriter.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/HierarchicalTiffWriter.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -266,12 +266,29 @@
       case Orthanc::PixelFormat_RGB24:
       {
         uint16_t samplesPerPixel = 3;
-        uint16_t photometric = PHOTOMETRIC_YCBCR;
+        uint16_t photometric;
         uint16_t planar = PLANARCONFIG_CONTIG;   // Interleaved RGB
         uint16_t bpp = 8;
         uint16_t subsampleHorizontal = 2;
         uint16_t subsampleVertical = 2;
 
+        switch (photometric_)
+        {
+          case Orthanc::PhotometricInterpretation_YBRFull422:
+            photometric = PHOTOMETRIC_YCBCR;
+            break;
+
+          case Orthanc::PhotometricInterpretation_RGB:
+            photometric = PHOTOMETRIC_RGB;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(
+              Orthanc::ErrorCode_ParameterOutOfRange,
+              "Unsupported photometric interpreation: " +
+              std::string(Orthanc::EnumerationToString(photometric_)));
+        }
+  
         if (TIFFSetField(tiff_, TIFFTAG_SAMPLESPERPIXEL, samplesPerPixel) != 1 ||
             TIFFSetField(tiff_, TIFFTAG_PHOTOMETRIC, photometric) != 1 ||
             TIFFSetField(tiff_, TIFFTAG_BITSPERSAMPLE, bpp) != 1 ||
@@ -383,12 +400,14 @@
                                                  Orthanc::PixelFormat pixelFormat, 
                                                  ImageCompression compression,
                                                  unsigned int tileWidth,
-                                                 unsigned int tileHeight) :
+                                                 unsigned int tileHeight,
+                                                 Orthanc::PhotometricInterpretation photometric) :
     PyramidWriterBase(pixelFormat, compression, tileWidth, tileHeight),
     currentLevel_(0),
     nextX_(0),
     nextY_(0),
-    isFirst_(true)
+    isFirst_(true),
+    photometric_(photometric)
   {
     tiff_ = TIFFOpen(path.c_str(), "w");
     if (tiff_ == NULL)
--- a/Framework/Outputs/HierarchicalTiffWriter.h	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/HierarchicalTiffWriter.h	Fri Jul 12 12:00:31 2019 +0200
@@ -46,7 +46,8 @@
     unsigned int              nextX_;
     unsigned int              nextY_;
     bool                      isFirst_;
- 
+    Orthanc::PhotometricInterpretation  photometric_;
+    
     void Close()
     {
       TIFFClose(tiff_);
@@ -78,7 +79,8 @@
                            Orthanc::PixelFormat pixelFormat, 
                            ImageCompression compression,
                            unsigned int tileWidth,
-                           unsigned int tileHeight);
+                           unsigned int tileHeight,
+                           Orthanc::PhotometricInterpretation photometric);
 
     virtual ~HierarchicalTiffWriter();
 
--- a/Framework/Outputs/MultiframeDicomWriter.cpp	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/MultiframeDicomWriter.cpp	Fri Jul 12 12:00:31 2019 +0200
@@ -147,7 +147,8 @@
                                                unsigned int width,
                                                unsigned int height,
                                                unsigned int tileWidth,
-                                               unsigned int tileHeight) :
+                                               unsigned int tileHeight,
+                                               Orthanc::PhotometricInterpretation photometric) :
     compression_(compression),
     width_(width),
     height_(height)
@@ -187,28 +188,18 @@
     DicomToolbox::SetUint16Tag(sharedTags_, DCM_BitsStored, 8);
     DicomToolbox::SetUint16Tag(sharedTags_, DCM_HighBit, 7);
     DicomToolbox::SetUint16Tag(sharedTags_, DCM_PixelRepresentation, 0);   // Unsigned values
+    DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, Orthanc::EnumerationToString(photometric));
 
     switch (pixelFormat)
     {
       case Orthanc::PixelFormat_RGB24:
         uncompressedFrameSize_ = 3 * tileWidth * tileHeight;
         DicomToolbox::SetUint16Tag(sharedTags_, DCM_SamplesPerPixel, 3);
-
-        if (compression_ == ImageCompression_Jpeg)
-        {
-          DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "YBR_FULL_422");
-        }
-        else
-        {
-          DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "RGB");
-        }
-
         break;
 
       case Orthanc::PixelFormat_Grayscale8:
         uncompressedFrameSize_ = tileWidth * tileHeight;
         DicomToolbox::SetUint16Tag(sharedTags_, DCM_SamplesPerPixel, 1);
-        DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "MONOCHROME2");
         break;
 
       default:
--- a/Framework/Outputs/MultiframeDicomWriter.h	Fri Jul 12 09:06:54 2019 +0200
+++ b/Framework/Outputs/MultiframeDicomWriter.h	Fri Jul 12 12:00:31 2019 +0200
@@ -63,7 +63,8 @@
                           unsigned int width,
                           unsigned int height,
                           unsigned int tileWidth,
-                          unsigned int tileHeight);
+                          unsigned int tileHeight,
+                          Orthanc::PhotometricInterpretation photometric);
 
     void AddFrame(const std::string& frame,
                   DcmItem* functionalGroup);   // This takes the ownership
--- a/NEWS	Fri Jul 12 09:06:54 2019 +0200
+++ b/NEWS	Fri Jul 12 12:00:31 2019 +0200
@@ -1,6 +1,9 @@
 Pending changes in the mainline
 ===============================
 
+* Improved consistency when transcoding/re-encoding is applied
+* Fix issue #144 (OrthancWSIDicomizer PhotometricInterpretation)
+
 
 Version 0.6 (2019-01-26)
 ========================
@@ -23,7 +26,7 @@
 Version 0.4 (2017-03-01)
 ========================
 
-* Fix issue #30: Bad colorspace if using OpenSlide
+* Fix issue #30 (Bad colorspace if using OpenSlide)
 
 
 Version 0.3 (2016-12-23)