Mercurial > hg > orthanc
changeset 6032:c76b1b2ee57e pixel-anon
very first proto of pixel-anon: only tested on /instances/{id}/anonymize
line wrap: on
line diff
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Thu Mar 06 19:10:32 2025 +0100 @@ -540,6 +540,7 @@ set(ORTHANC_DICOM_SOURCES_INTERNAL ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomFindAnswers.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomModification.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomPixelMasker.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomWebJsonVisitor.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/FromDcmtkBridge.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/ParsedDicomCache.cpp
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -1145,6 +1145,12 @@ toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent, privateCreator_); } + + // (9) New in Orthanc 1.X.X: Apply pixel modifications + if (pixelMasker_ != NULL) + { + pixelMasker_->Apply(toModify); + } } void DicomModification::SetAllowManualIdentifiers(bool check) @@ -1379,7 +1385,7 @@ { if (!request.isObject()) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "The payload should be a JSON object."); } bool force = GetBooleanValue("Force", request, false); @@ -1393,7 +1399,7 @@ { if (request["DicomVersion"].type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "DicomVersion should be a string"); } else { @@ -1435,6 +1441,13 @@ { privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator"); } + + // New in Orthanc 1.X.X + if (request.isMember("MaskPixelData") && request["MaskPixelData"].isObject()) + { + pixelMasker_.reset(new DicomPixelMasker()); + pixelMasker_->ParseRequest(request); + } } void DicomModification::SetDicomIdentifierGenerator(DicomModification::IDicomIdentifierGenerator &generator) @@ -1896,4 +1909,9 @@ target.insert(it->first); } } + + bool DicomModification::RequiresUncompressedTransferSyntax() const + { + return pixelMasker_ != NULL; + } }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h Thu Mar 06 19:10:32 2025 +0100 @@ -25,6 +25,7 @@ #pragma once #include "ParsedDicomFile.h" +#include "DicomPixelMasker.h" #include <list> @@ -154,6 +155,9 @@ ListOfPaths removeSequences_; // Must *never* be a path whose prefix is empty SequenceReplacements sequenceReplacements_; // Must *never* be a path whose prefix is empty + // New in Orthanc 1.X.X + std::unique_ptr<DicomPixelMasker> pixelMasker_; // TODO: check ownership & serialization + std::string MapDicomIdentifier(const std::string& original, ResourceType level); @@ -268,5 +272,7 @@ bool safeForAnonymization); bool IsAlteredTag(const DicomTag& tag) const; + + bool RequiresUncompressedTransferSyntax() const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -0,0 +1,142 @@ +/** + * 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 "DicomPixelMasker.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + + +namespace Orthanc +{ + static const char* KEY_MASK_PIXELS = "MaskPixelData"; + static const char* KEY_MASK_TYPE = "MaskType"; + static const char* KEY_MASK_TYPE_FILL = "Fill"; + static const char* KEY_MASK_TYPE_MEAN_FILTER = "MeanFilter"; + static const char* KEY_FILTER_WIDTH = "FilterWidth"; + static const char* KEY_FILL_VALUE = "FillValue"; + static const char* KEY_REGIONS = "Regions"; + static const char* KEY_REGION_TYPE = "RegionType"; + static const char* KEY_REGION_PIXELS = "Pixels"; + static const char* KEY_REGION_BOUNDING_BOX = "BoundingBox"; + static const char* KEY_ORIGIN = "Origin"; + static const char* KEY_END = "End"; + static const char* KEY_TARGET_SERIES = "TargetSeries"; + + DicomPixelMasker::DicomPixelMasker() + { + } + + void DicomPixelMasker::Apply(ParsedDicomFile& toModify) + { + for (std::list<Region>::const_iterator r = regions_.begin(); r != regions_.end(); ++r) + { + ImageAccessor imageRegion; + toModify.GetRawFrame(0)->GetRegion(imageRegion, r->x_, r->y_, r->width_, r->height_); + + if (r->mode_ == DicomPixelMaskerMode_MeanFilter) + { + ImageProcessing::MeanFilter(imageRegion, r->filterWidth_, r->filterWidth_); + } + else if (r->mode_ == DicomPixelMaskerMode_Fill) + { + ImageProcessing::Set(imageRegion, r->fillValue_); + } + } + } + + void DicomPixelMasker::ParseRequest(const Json::Value& request) + { + if (request.isMember(KEY_MASK_PIXELS) && request[KEY_MASK_PIXELS].isObject()) + { + const Json::Value& maskPixelsJson = request[KEY_MASK_PIXELS]; + + if (maskPixelsJson.isMember(KEY_REGIONS) && maskPixelsJson[KEY_REGIONS].isArray()) + { + const Json::Value& regionsJson = maskPixelsJson[KEY_REGIONS]; + + for (Json::ArrayIndex i = 0; i < regionsJson.size(); ++i) + { + const Json::Value& regionJson = regionsJson[i]; + Region region; + + if (regionJson.isMember(KEY_MASK_TYPE) && regionJson[KEY_MASK_TYPE].isString()) + { + if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_FILL) + { + region.mode_ = DicomPixelMaskerMode_Fill; + + if (regionJson.isMember(KEY_FILL_VALUE) && regionJson[KEY_FILL_VALUE].isInt()) + { + region.fillValue_ = regionJson[KEY_FILL_VALUE].asInt(); + } + } + else if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_MEAN_FILTER) + { + region.mode_ = DicomPixelMaskerMode_MeanFilter; + + if (regionJson.isMember(KEY_FILTER_WIDTH) && regionJson[KEY_FILTER_WIDTH].isUInt()) + { + region.filterWidth_ = regionJson[KEY_FILTER_WIDTH].asUInt(); + } + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, std::string(KEY_MASK_TYPE) + " should be '" + KEY_MASK_TYPE_FILL +"' or '" + KEY_MASK_TYPE_MEAN_FILTER + "'."); + } + } + + if (regionJson.isMember(KEY_TARGET_SERIES) && regionJson[KEY_TARGET_SERIES].isArray()) + { + SerializationToolbox::ReadListOfStrings(region.targetSeries_, regionJson, KEY_TARGET_SERIES); + } + + if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_PIXELS) + { + if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 2 && + regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 2) + { + region.x_ = regionJson[KEY_ORIGIN][0].asUInt(); + region.y_ = regionJson[KEY_ORIGIN][1].asUInt(); + region.width_ = regionJson[KEY_END][0].asUInt() - region.x_; + region.height_ = regionJson[KEY_END][1].asUInt() - region.y_; + + regions_.push_back(region); + } + } + else if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_BOUNDING_BOX) + { + // TODO + throw OrthancException(ErrorCode_NotImplemented); + } + } + + } + + // TODO: support multiple series + move this + } + + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h Thu Mar 06 19:10:32 2025 +0100 @@ -0,0 +1,76 @@ +/** + * 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/>. + **/ + + +#pragma once + +#include "ParsedDicomFile.h" +#include "../Images/ImageProcessing.h" + +#include <list> + + +namespace Orthanc +{ + enum DicomPixelMaskerMode + { + DicomPixelMaskerMode_Fill, + DicomPixelMaskerMode_MeanFilter + }; + + class ORTHANC_PUBLIC DicomPixelMasker : public boost::noncopyable + { + struct Region + { + unsigned int x_; + unsigned int y_; + unsigned int width_; + unsigned int height_; + DicomPixelMaskerMode mode_; + int32_t fillValue_; // pixel value + uint32_t filterWidth_; // filter width + std::list<std::string> targetSeries_; + + Region() : + x_(0), + y_(0), + width_(0), + height_(0), + mode_(DicomPixelMaskerMode_Fill), + fillValue_(0), + filterWidth_(0) + { + } + }; + + private: + std::list<Region> regions_; + + public: + DicomPixelMasker(); + + void Apply(ParsedDicomFile& toModify); + + void ParseRequest(const Json::Value& request); + }; +}
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -226,6 +226,28 @@ fragment = dynamic_cast<DcmPixelItem*>(pixelSequence_->nextInContainer(fragment)); } } + + virtual uint8_t* GetRawFrameBuffer(unsigned int index) + { + if (index >= startFragment_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (countFragments_[index] != 1) + { + throw OrthancException(ErrorCode_NotImplemented, "GetRawFrameBuffer is currently not implemented if there are more fragments than frames."); + } + + DcmPixelItem* fragment = startFragment_[index]; + uint8_t* content = NULL; + if (!fragment->getUint8Array(content).good() || + content == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + return content; + } }; @@ -277,6 +299,12 @@ memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_); } } + + virtual uint8_t* GetRawFrameBuffer(unsigned int index) + { + return pixelData_ + index * frameSize_; + } + }; @@ -308,6 +336,12 @@ memcpy(&frame[0], reinterpret_cast<const uint8_t*>(&pixelData_[0]) + index * frameSize_, frameSize_); } } + + virtual uint8_t* GetRawFrameBuffer(unsigned int index) + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; @@ -415,4 +449,20 @@ throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame"); } } + + uint8_t* DicomFrameIndex::GetRawFrameBuffer(unsigned int index) + { + if (index >= countFrames_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else if (index_.get() != NULL) + { + return index_->GetRawFrameBuffer(index); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame"); + } + } }
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h Thu Mar 06 19:10:32 2025 +0100 @@ -48,6 +48,8 @@ virtual void GetRawFrame(std::string& frame, unsigned int index) const = 0; + + virtual uint8_t* GetRawFrameBuffer(unsigned int index) = 0; }; class FragmentIndex; @@ -69,5 +71,7 @@ unsigned int index) const; static unsigned int GetFramesCount(DcmDataset& dicom); + + uint8_t* GetRawFrameBuffer(unsigned int index); }; }
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -1805,6 +1805,46 @@ } } + ImageAccessor* ParsedDicomFile::GetRawFrame(unsigned int frame) + { + E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer(); + if (transferSyntax != EXS_LittleEndianImplicit && + transferSyntax != EXS_BigEndianImplicit && + transferSyntax != EXS_LittleEndianExplicit && + transferSyntax != EXS_BigEndianExplicit) + { + throw OrthancException(ErrorCode_NotImplemented, "ParseDicomFile::GetRawFrame only works with uncompressed transfer syntaxes"); + } + + if (pimpl_->frameIndex_.get() == NULL) + { + assert(pimpl_->file_ != NULL && + GetDcmtkObjectConst().getDataset() != NULL); + pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObjectConst().getDataset())); + } + + DicomMap m; + std::set<DicomTag> ignoreTagLength; + FromDcmtkBridge::ExtractDicomSummary(m, *GetDcmtkObjectConst().getDataset(), DicomImageInformation::GetUsefulTagLength(), ignoreTagLength); + + DicomImageInformation info(m); + PixelFormat format; + + if (!info.ExtractPixelFormat(format, false)) + { + LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() + << "bpp, " << info.GetChannelCount() << " channels, " + << (info.IsSigned() ? "signed" : "unsigned") + << (info.IsPlanar() ? ", planar, " : ", non-planar, ") + << EnumerationToString(info.GetPhotometricInterpretation()) + << " photometric interpretation"; + throw OrthancException(ErrorCode_NotImplemented); + } + + std::unique_ptr<ImageAccessor> img(new ImageAccessor()); + img->AssignWritable(format, info.GetWidth(), info.GetHeight(), info.GetWidth() * GetBytesPerPixel(format), pimpl_->frameIndex_->GetRawFrameBuffer(frame)); + return img.release(); + } static bool HasGenericGroupLength(const DicomPath& path) {
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Thu Mar 06 19:10:32 2025 +0100 @@ -313,6 +313,10 @@ ImageAccessor* DecodeAllOverlays(int& originX, int& originY) const; + // Returns an image accessor to the raw frame only if the DicomFile is in an uncompressed TS. + // This enables modification of pixels data in place. + ImageAccessor* GetRawFrame(unsigned int frame); + void InjectEmptyPixelData(ValueRepresentation vr); // Remove all the tags after pixel data
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -2756,6 +2756,19 @@ } break; + case PixelFormat_Grayscale16: + if (useRound) + { + SeparableConvolutionFloat<uint16_t, 1u, true> + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + else + { + SeparableConvolutionFloat<uint16_t, 1u, false> + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + break; + case PixelFormat_RGB24: if (useRound) { @@ -2789,6 +2802,14 @@ } + void ImageProcessing::MeanFilter(ImageAccessor& image, size_t horizontalKernelWidth, size_t verticalKernelWidth) + { + std::vector<float> hKernel(horizontalKernelWidth, 1.0f); + std::vector<float> vKernel(verticalKernelWidth, 1.0f); + + SeparableConvolution(image, hKernel, horizontalKernelWidth / 2, vKernel, verticalKernelWidth / 2, false); + } + void ImageProcessing::FitSize(ImageAccessor& target, const ImageAccessor& source) {
--- a/OrthancFramework/Sources/Images/ImageProcessing.h Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.h Thu Mar 06 19:10:32 2025 +0100 @@ -200,6 +200,8 @@ static void SmoothGaussian5x5(ImageAccessor& image, bool useRound /* this is expensive */); + static void MeanFilter(ImageAccessor& image, size_t horizontalAverageWidth, size_t verticalAverageWidth); + static void FitSize(ImageAccessor& target, const ImageAccessor& source);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -218,35 +218,9 @@ ServerContext::DicomCacheLocker locker(context, id); modified.reset(locker.GetDicom().Clone(true)); } - - modification.Apply(*modified); - - if (transcode) - { - IDicomTranscoder::DicomImage source; - source.AcquireParsed(*modified); // "modified" is invalid below this point - - IDicomTranscoder::DicomImage transcoded; - - std::set<DicomTransferSyntax> s; - s.insert(targetSyntax); - - if (context.Transcode(transcoded, source, s, true, lossyQuality)) - { - call.GetOutput().AnswerBuffer(transcoded.GetBufferData(), - transcoded.GetBufferSize(), MimeType_Dicom); - } - else - { - throw OrthancException(ErrorCode_InternalError, - "Cannot transcode to transfer syntax: " + - std::string(GetTransferSyntaxUid(targetSyntax))); - } - } - else - { - modified->Answer(call.GetOutput()); - } + + context.Modify(modified, modification, transcode, targetSyntax, lossyQuality, false /* keepSOPInstanceUidDuringLossyTranscoding*/); + modified->Answer(call.GetOutput()); }
--- a/OrthancServer/Sources/ServerContext.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -50,6 +50,7 @@ #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmnet/dimse.h> +#include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmdata/dcuid.h> /* for variable dcmAllStorageSOPClassUIDs */ #include <boost/regex.hpp> @@ -2012,6 +2013,80 @@ } } + void ServerContext::Modify(std::unique_ptr<ParsedDicomFile>& dicomFile, + DicomModification& modification, + bool transcode, + DicomTransferSyntax targetSyntax, + unsigned int lossyQuality, + bool keepSOPInstanceUidDuringLossyTranscoding) + { + // do we need to transcode before ? + DicomTransferSyntax currentTransferSyntax; + if (modification.RequiresUncompressedTransferSyntax() && + dicomFile->LookupTransferSyntax(currentTransferSyntax) && + currentTransferSyntax > DicomTransferSyntax_BigEndianExplicit) + { + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*dicomFile); // "dicomFile" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; + + std::set<DicomTransferSyntax> uncompressedTransferSyntax; + uncompressedTransferSyntax.insert(DicomTransferSyntax_LittleEndianExplicit); + Transcode(transcoded, source, uncompressedTransferSyntax, true); + + if (!transcode) // if we had to change the TS for the modification, we need to restore the original TS afterwards + { + transcode = true; + targetSyntax = currentTransferSyntax; + } + dicomFile.reset(transcoded.ReleaseAsParsedDicomFile()); + } + + modification.Apply(*dicomFile); + + if (transcode) + { + const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(dicomFile->GetDcmtkObject()); + + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*dicomFile); // "dicomFile" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; + + std::set<DicomTransferSyntax> s; + s.insert(targetSyntax); + + if (Transcode(transcoded, source, s, true, lossyQuality)) + { + dicomFile.reset(transcoded.ReleaseAsParsedDicomFile()); + + if (keepSOPInstanceUidDuringLossyTranscoding) + { + // Fix the SOP instance UID in order the preserve the + // references between instance UIDs in the DICOM hierarchy + // (the UID might have changed during this last transcoding step in the case of lossy transcoding) + if (dicomFile.get() == NULL || + dicomFile->GetDcmtkObject().getDataset() == NULL || + !dicomFile->GetDcmtkObject().getDataset()->putAndInsertString( + DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + return; + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot transcode to transfer syntax " + + std::string(GetTransferSyntaxUid(targetSyntax))); + } + } + } + + + const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const { static const std::string redactedContent = "*** POTENTIAL PHI ***";
--- a/OrthancServer/Sources/ServerContext.h Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Thu Mar 06 19:10:32 2025 +0100 @@ -590,6 +590,13 @@ const std::string& attachmentId, // for the storage cache DicomTransferSyntax targetSyntax); + void Modify(std::unique_ptr<ParsedDicomFile>& toModify, + DicomModification& modification, + bool transcode, + DicomTransferSyntax targetSyntax, + unsigned int lossyQuality, + bool keepSOPInstanceUidDuringLossyTranscoding); + bool IsTranscodeDicomProtocol() const { return transcodeDicomProtocol_;
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -92,7 +92,7 @@ try { - modification_->Apply(*modified); + context_.Modify(modified, *modification_, false, DicomTransferSyntax_LittleEndianExplicit /* not used */, 100 /* not used */, false /*keepSOPInstanceUidDuringLossyTranscoding*/); std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*modified)); assert(origin_ == RequestOrigin_Lua);
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue Feb 25 19:19:40 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Mar 06 19:10:32 2025 +0100 @@ -238,7 +238,7 @@ { boost::recursive_mutex::scoped_lock lock(mutex_); // DicomModification object is not thread safe, we must protect it from here - modification_->Apply(*modified); + GetContext().Modify(modified, *modification_, transcode_, transferSyntax_, 100 /* not used */, true /* keepSOPInstanceUidDuringLossyTranscoding*/); // TODO-PIXEL-ANON: get lossy quality from ??? if (modification_->AreLabelsKept()) { @@ -252,36 +252,6 @@ const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()); - if (transcode_) - { - std::set<DicomTransferSyntax> syntaxes; - syntaxes.insert(transferSyntax_); - - IDicomTranscoder::DicomImage source; - source.AcquireParsed(*modified); // "modified" is invalid below this point - - IDicomTranscoder::DicomImage transcoded; - if (GetContext().Transcode(transcoded, source, syntaxes, true)) - { - modified.reset(transcoded.ReleaseAsParsedDicomFile()); - - // Fix the SOP instance UID in order the preserve the - // references between instance UIDs in the DICOM hierarchy - // (the UID might have changed in the case of lossy transcoding) - if (modified.get() == NULL || - modified->GetDcmtkObject().getDataset() == NULL || - !modified->GetDcmtkObject().getDataset()->putAndInsertString( - DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - else - { - LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance; - modified.reset(source.ReleaseAsParsedDicomFile()); - } - } assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));