Orthanc/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp
2025-06-23 19:07:37 +05:30

419 lines
12 KiB
C++

/**
* 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-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 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"
#include "DicomFrameIndex.h"
#include "../../OrthancException.h"
#include "../../DicomFormat/DicomImageInformation.h"
#include "../FromDcmtkBridge.h"
#include "../../Endianness.h"
#include "DicomImageDecoder.h"
#include <boost/lexical_cast.hpp>
#include <dcmtk/dcmdata/dcdeftag.h>
#include <dcmtk/dcmdata/dcpxitem.h>
#include <dcmtk/dcmdata/dcpixseq.h>
namespace Orthanc
{
class DicomFrameIndex::FragmentIndex : public DicomFrameIndex::IIndex
{
private:
DcmPixelSequence* pixelSequence_;
std::vector<DcmPixelItem*> startFragment_;
std::vector<unsigned int> countFragments_;
std::vector<unsigned int> frameSize_;
void GetOffsetTable(std::vector<uint32_t>& table)
{
DcmPixelItem* item = NULL;
if (!pixelSequence_->getItem(item, 0).good() ||
item == NULL)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
uint32_t length = item->getLength();
if (length == 0)
{
// Degenerate case: Empty offset table means only one frame
// that overlaps all the fragments
table.resize(1);
table[0] = 0;
return;
}
if (length % 4 != 0)
{
// Error: Each fragment is index with 4 bytes (uint32_t)
throw OrthancException(ErrorCode_BadFileFormat);
}
uint8_t* content = NULL;
if (!item->getUint8Array(content).good() ||
content == NULL)
{
throw OrthancException(ErrorCode_InternalError);
}
table.resize(length / 4);
// The offset table is always in little endian in the DICOM
// file. Swap it to host endianness if needed.
const uint32_t* offset = reinterpret_cast<const uint32_t*>(content);
for (size_t i = 0; i < table.size(); i++, offset++)
{
table[i] = le32toh(*offset);
}
}
public:
FragmentIndex(DcmPixelSequence* pixelSequence,
unsigned int countFrames) :
pixelSequence_(pixelSequence)
{
assert(pixelSequence != NULL);
startFragment_.resize(countFrames);
countFragments_.resize(countFrames);
frameSize_.resize(countFrames);
// The first fragment corresponds to the offset table
unsigned int countFragments = static_cast<unsigned int>(pixelSequence_->card());
if (countFragments < countFrames + 1)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
if (countFragments == countFrames + 1)
{
// Simple case: There is one fragment per frame.
DcmObject* fragment = pixelSequence_->nextInContainer(NULL); // Skip the offset table
if (fragment == NULL)
{
throw OrthancException(ErrorCode_InternalError);
}
for (unsigned int i = 0; i < countFrames; i++)
{
fragment = pixelSequence_->nextInContainer(fragment);
startFragment_[i] = dynamic_cast<DcmPixelItem*>(fragment);
frameSize_[i] = fragment->getLength();
countFragments_[i] = 1;
}
return;
}
// Parse the offset table
std::vector<uint32_t> offsetOfFrame;
GetOffsetTable(offsetOfFrame);
if (offsetOfFrame.size() != countFrames ||
offsetOfFrame[0] != 0)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
// Loop over the fragments (ignoring the offset table). This is
// an alternative, faster implementation to DCMTK's
// "DcmCodec::determineStartFragment()".
DcmObject* fragment = pixelSequence_->nextInContainer(NULL);
if (fragment == NULL)
{
throw OrthancException(ErrorCode_InternalError);
}
fragment = pixelSequence_->nextInContainer(fragment); // Skip the offset table
if (fragment == NULL)
{
throw OrthancException(ErrorCode_InternalError);
}
uint32_t offset = 0;
unsigned int currentFrame = 0;
startFragment_[0] = dynamic_cast<DcmPixelItem*>(fragment);
unsigned int currentFragment = 1;
while (fragment != NULL)
{
if (currentFrame + 1 < countFrames &&
offset == offsetOfFrame[currentFrame + 1])
{
currentFrame += 1;
startFragment_[currentFrame] = dynamic_cast<DcmPixelItem*>(fragment);
}
frameSize_[currentFrame] += fragment->getLength();
countFragments_[currentFrame]++;
// 8 bytes = overhead for the item tag and length field
offset += fragment->getLength() + 8;
currentFragment++;
fragment = pixelSequence_->nextInContainer(fragment);
}
if (currentFragment != countFragments ||
currentFrame + 1 != countFrames ||
fragment != NULL)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
assert(startFragment_.size() == countFragments_.size() &&
startFragment_.size() == frameSize_.size());
}
virtual void GetRawFrame(std::string& frame,
unsigned int index) const ORTHANC_OVERRIDE
{
if (index >= startFragment_.size())
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
frame.resize(frameSize_[index]);
if (frame.size() == 0)
{
return;
}
uint8_t* target = reinterpret_cast<uint8_t*>(&frame[0]);
size_t offset = 0;
DcmPixelItem* fragment = startFragment_[index];
for (unsigned int i = 0; i < countFragments_[index]; i++)
{
uint8_t* content = NULL;
if (!fragment->getUint8Array(content).good() ||
content == NULL)
{
throw OrthancException(ErrorCode_InternalError);
}
assert(offset + fragment->getLength() <= frame.size());
memcpy(target + offset, content, fragment->getLength());
offset += fragment->getLength();
fragment = dynamic_cast<DcmPixelItem*>(pixelSequence_->nextInContainer(fragment));
}
}
};
class DicomFrameIndex::UncompressedIndex : public DicomFrameIndex::IIndex
{
private:
uint8_t* pixelData_;
size_t frameSize_;
public:
UncompressedIndex(DcmDataset& dataset,
unsigned int countFrames,
size_t frameSize) :
pixelData_(NULL),
frameSize_(frameSize)
{
size_t size = 0;
DcmElement* e;
if (dataset.findAndGetElement(DCM_PixelData, e).good() &&
e != NULL)
{
size = e->getLength();
if (size > 0)
{
pixelData_ = NULL;
if (!e->getUint8Array(pixelData_).good() ||
pixelData_ == NULL)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
}
if (size < frameSize_ * countFrames)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
virtual void GetRawFrame(std::string& frame,
unsigned int index) const ORTHANC_OVERRIDE
{
frame.resize(frameSize_);
if (frameSize_ > 0)
{
memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_);
}
}
};
class DicomFrameIndex::PsmctRle1Index : public DicomFrameIndex::IIndex
{
private:
std::string pixelData_;
size_t frameSize_;
public:
PsmctRle1Index(DcmDataset& dataset,
unsigned int countFrames,
size_t frameSize) :
frameSize_(frameSize)
{
if (!DicomImageDecoder::DecodePsmctRle1(pixelData_, dataset) ||
pixelData_.size() < frameSize * countFrames)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
virtual void GetRawFrame(std::string& frame,
unsigned int index) const ORTHANC_OVERRIDE
{
frame.resize(frameSize_);
if (frameSize_ > 0)
{
memcpy(&frame[0], reinterpret_cast<const uint8_t*>(&pixelData_[0]) + index * frameSize_, frameSize_);
}
}
};
unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom)
{
DicomTransferSyntax transferSyntax;
if (FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom) &&
(transferSyntax == DicomTransferSyntax_MPEG2MainProfileAtMainLevel ||
transferSyntax == DicomTransferSyntax_MPEG2MainProfileAtHighLevel ||
transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_1 ||
transferSyntax == DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 ||
transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo ||
transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo ||
transferSyntax == DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 ||
transferSyntax == DicomTransferSyntax_HEVCMainProfileLevel5_1 ||
transferSyntax == DicomTransferSyntax_HEVCMain10ProfileLevel5_1))
{
/**
* Fixes an issue that was present from Orthanc 1.6.0 until
* 1.8.0 for the special case of the videos: In a video, the
* number of frames doesn't correspond to the number of
* fragments. We consider that there is one single frame (the
* video itself).
**/
return 1;
}
const char* tmp = NULL;
if (!dicom.findAndGetString(DCM_NumberOfFrames, tmp).good() ||
tmp == NULL)
{
return 1;
}
int count = -1;
try
{
count = boost::lexical_cast<int>(tmp);
}
catch (boost::bad_lexical_cast&)
{
}
if (count < 0)
{
throw OrthancException(ErrorCode_BadFileFormat);
}
else
{
return static_cast<unsigned int>(count);
}
}
DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom)
{
countFrames_ = GetFramesCount(dicom);
if (countFrames_ == 0)
{
// The image has no frame. No index is to be built.
return;
}
// Extract information about the image structure
DicomMap tags;
std::set<DicomTag> ignoreTagLength;
FromDcmtkBridge::ExtractDicomSummary(tags, dicom, DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
DicomImageInformation information(tags);
// Test whether this image is composed of a sequence of fragments
if (dicom.tagExists(DCM_PixelData))
{
DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom);
if (pixelSequence != NULL)
{
index_.reset(new FragmentIndex(pixelSequence, countFrames_));
}
else
{
// Access to the raw pixel data
index_.reset(new UncompressedIndex(dicom, countFrames_, information.GetFrameSize()));
}
}
else if (DicomImageDecoder::IsPsmctRle1(dicom))
{
index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize()));
}
}
void DicomFrameIndex::GetRawFrame(std::string& frame,
unsigned int index) const
{
if (index >= countFrames_)
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
else if (index_.get() != NULL)
{
return index_->GetRawFrame(frame, index);
}
else
{
throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame");
}
}
}