view OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp @ 5934:892ee42584bb

merge
author Alain Mazy <am@orthanc.team>
date Mon, 16 Dec 2024 18:09:11 +0100
parents f7adfb22e20e
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"

#ifndef NOMINMAX
#define NOMINMAX
#endif

#include "DicomImageInformation.h"

#include "../Compatibility.h"
#include "../Logging.h"
#include "../OrthancException.h"
#include "../Toolbox.h"

#include <boost/lexical_cast.hpp>
#include <limits>
#include <cassert>
#include <stdio.h>
#include <memory>

namespace Orthanc
{
  DicomImageInformation::DicomImageInformation(const DicomMap& values)
  {
    std::string sopClassUid;
    if (values.LookupStringValue(sopClassUid, DICOM_TAG_SOP_CLASS_UID, false))
    {
      sopClassUid = Toolbox::StripSpaces(sopClassUid);
      if (sopClassUid == "1.2.840.10008.5.1.4.1.1.481.3" /* RT-STRUCT */)
      {
        LOG(WARNING) << "Orthanc::DicomImageInformation() should not be applied to SOP Class UID: " << sopClassUid;
      }
    }

    uint32_t pixelRepresentation = 0;
    uint32_t planarConfiguration = 0;

    try
    {
      std::string p;
      if (values.LookupStringValue(p, DICOM_TAG_PHOTOMETRIC_INTERPRETATION, false)) {
        Toolbox::ToUpperCase(p);

        if (p == "RGB")
        {
          photometric_ = PhotometricInterpretation_RGB;
        }
        else if (p == "MONOCHROME1")
        {
          photometric_ = PhotometricInterpretation_Monochrome1;
        }
        else if (p == "MONOCHROME2")
        {
          photometric_ = PhotometricInterpretation_Monochrome2;
        }
        else if (p == "PALETTE COLOR")
        {
          photometric_ = PhotometricInterpretation_Palette;
        }
        else if (p == "HSV")
        {
          photometric_ = PhotometricInterpretation_HSV;
        }
        else if (p == "ARGB")
        {
          photometric_ = PhotometricInterpretation_ARGB;
        }
        else if (p == "CMYK")
        {
          photometric_ = PhotometricInterpretation_CMYK;
        }
        else if (p == "YBR_FULL")
        {
          photometric_ = PhotometricInterpretation_YBRFull;
        }
        else if (p == "YBR_FULL_422")
        {
          photometric_ = PhotometricInterpretation_YBRFull422;
        }
        else if (p == "YBR_PARTIAL_420")
        {
          photometric_ = PhotometricInterpretation_YBRPartial420;
        }
        else if (p == "YBR_PARTIAL_422")
        {
          photometric_ = PhotometricInterpretation_YBRPartial422;
        }
        else if (p == "YBR_ICT")
        {
          photometric_ = PhotometricInterpretation_YBR_ICT;
        }
        else if (p == "YBR_RCT")
        {
          photometric_ = PhotometricInterpretation_YBR_RCT;
        }
        else
        {
          photometric_ = PhotometricInterpretation_Unknown;
        }
      }
      else
      {
        photometric_ = PhotometricInterpretation_Unknown;
      }

      values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
      values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);

      if (!values.ParseUnsignedInteger32(bitsAllocated_, DICOM_TAG_BITS_ALLOCATED))
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      if (!values.ParseUnsignedInteger32(samplesPerPixel_, DICOM_TAG_SAMPLES_PER_PIXEL))
      {
        samplesPerPixel_ = 1;  // Assume 1 color channel
      }

      if (!values.ParseUnsignedInteger32(bitsStored_, DICOM_TAG_BITS_STORED))
      {
        bitsStored_ = bitsAllocated_;
      }

      if (bitsStored_ > bitsAllocated_)
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      if (!values.ParseUnsignedInteger32(highBit_, DICOM_TAG_HIGH_BIT))
      {
        highBit_ = bitsStored_ - 1;
      }

      if (!values.ParseUnsignedInteger32(pixelRepresentation, DICOM_TAG_PIXEL_REPRESENTATION))
      {
        pixelRepresentation = 0;  // Assume unsigned pixels
      }

      if (samplesPerPixel_ > 1)
      {
        // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
        // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3

        if (!values.ParseUnsignedInteger32(planarConfiguration, DICOM_TAG_PLANAR_CONFIGURATION))
        {
          planarConfiguration = 0;  // Assume interleaved color channels
        }
      }
    }
    catch (boost::bad_lexical_cast&)
    {
      throw OrthancException(ErrorCode_NotImplemented);
    }
    catch (OrthancException&)
    {
      throw OrthancException(ErrorCode_NotImplemented);
    }

    
    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
    {
      if (!values.ParseUnsignedInteger32(numberOfFrames_, DICOM_TAG_NUMBER_OF_FRAMES))
      {
        throw OrthancException(ErrorCode_NotImplemented);
      }
    }
    else
    {
      numberOfFrames_ = 1;
    }

    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
        bitsAllocated_ != 24 && bitsAllocated_ != 32 &&
        bitsAllocated_ != 1 /* new in Orthanc 1.10.0 */)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
    }
    else if (numberOfFrames_ == 0)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
    }
    else if (planarConfiguration != 0 && planarConfiguration != 1)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
    }

    if (samplesPerPixel_ == 0)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
    }

    if (bitsStored_ == 1)
    {
      // This is the case of DICOM SEG, new in Orthanc 1.10.0
      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);
  }

  DicomImageInformation* DicomImageInformation::Clone() const
  {
    std::unique_ptr<DicomImageInformation> target(new DicomImageInformation);
    target->width_ = width_;
    target->height_ = height_;
    target->samplesPerPixel_ = samplesPerPixel_;
    target->numberOfFrames_ = numberOfFrames_;
    target->isPlanar_ = isPlanar_;
    target->isSigned_ = isSigned_;
    target->bytesPerValue_ = bytesPerValue_;
    target->bitsAllocated_ = bitsAllocated_;
    target->bitsStored_ = bitsStored_;
    target->highBit_ = highBit_;
    target->photometric_ = photometric_;

    return target.release();
  }

  unsigned int DicomImageInformation::GetWidth() const
  {
    return width_;
  }

  unsigned int DicomImageInformation::GetHeight() const
  {
    return height_;
  }

  unsigned int DicomImageInformation::GetNumberOfFrames() const
  {
    return numberOfFrames_;
  }

  unsigned int DicomImageInformation::GetChannelCount() const
  {
    return samplesPerPixel_;
  }

  unsigned int DicomImageInformation::GetBitsStored() const
  {
    return bitsStored_;
  }

  size_t DicomImageInformation::GetBytesPerValue() const
  {
    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
  {
    return isSigned_;
  }

  unsigned int DicomImageInformation::GetBitsAllocated() const
  {
    return bitsAllocated_;
  }

  unsigned int DicomImageInformation::GetHighBit() const
  {
    return highBit_;
  }

  bool DicomImageInformation::IsPlanar() const
  {
    return isPlanar_;
  }

  unsigned int DicomImageInformation::GetShift() const
  {
    return highBit_ + 1 - bitsStored_;
  }

  PhotometricInterpretation DicomImageInformation::GetPhotometricInterpretation() const
  {
    return photometric_;
  }

  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
                                                 bool ignorePhotometricInterpretation) const
  {
    if (photometric_ == PhotometricInterpretation_Palette)
    {
      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_RGB24;
        return true;
      }

      if (GetBitsStored() == 16 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_RGB48;
        return true;
      }
    }
    
    if (ignorePhotometricInterpretation ||
        photometric_ == PhotometricInterpretation_Monochrome1 ||
        photometric_ == PhotometricInterpretation_Monochrome2)
    {
      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale8;
        return true;
      }
      
      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale16;
        return true;
      }

      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
      {
        format = PixelFormat_SignedGrayscale16;
        return true;
      }
      
      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale32;
        return true;
      }

      if (GetBitsStored() == 1 && GetChannelCount() == 1 && !IsSigned())
      {
        // This is the case of DICOM SEG, new in Orthanc 1.10.0
        format = PixelFormat_Grayscale8;
        return true;
      }
    }

    if (GetBitsStored() == 8 &&
        GetChannelCount() == 3 &&
        !IsSigned() &&
        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
    {
      format = PixelFormat_RGB24;
      return true;
    }

    if (GetBitsStored() == 16 &&
        GetChannelCount() == 3 &&
        !IsSigned() &&
        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
    {
      format = PixelFormat_RGB48;
      return true;
    }

    return false;
  }


  size_t DicomImageInformation::GetFrameSize() const
  {
    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());
    }
  }


  unsigned int DicomImageInformation::GetUsefulTagLength()
  {
    return 256;
  }


  ValueRepresentation DicomImageInformation::GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
                                                                               unsigned int bitsAllocated)
  {
    /**
     * This approach is validated in "Tests/GuessPixelDataVR.py":
     * https://orthanc.uclouvain.be/hg/orthanc-tests/file/default/Tests/GuessPixelDataVR.py
     **/

    if (transferSyntax == DicomTransferSyntax_LittleEndianExplicit ||
        transferSyntax == DicomTransferSyntax_BigEndianExplicit)
    {
      /**
       * Same rules apply to Little Endian Explicit and Big Endian
       * Explicit (now retired). The VR of the pixel data directly
       * depends upon the "Bits Allocated (0028,0100)" tag:
       * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.2.html
       * https://dicom.nema.org/medical/dicom/2016b/output/chtml/part05/sect_A.3.html
       **/
      if (bitsAllocated > 8)
      {
        return ValueRepresentation_OtherWord;
      }
      else
      {
        return ValueRepresentation_OtherByte;
      }
    }
    else if (transferSyntax == DicomTransferSyntax_LittleEndianImplicit)
    {
      // Assume "OW" for DICOM Implicit VR Little Endian Transfer Syntax
      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html#sect_A.1
      return ValueRepresentation_OtherWord;
    }
    else
    {
      // Assume "OB" for all the compressed transfer syntaxes
      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html
      return ValueRepresentation_OtherByte;
    }
  }
}