Mercurial > hg > orthanc
changeset 6421:d0920767534e pixel-anon
integration mainline->pixel-anon
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Sat, 15 Nov 2025 12:27:24 +0100 |
| parents | ded8a2be0d46 (diff) c557f6bdcbfd (current diff) |
| children | |
| files | OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Sources/Enumerations.h OrthancFramework/Sources/Images/ImageProcessing.cpp OrthancServer/Sources/ServerContext.cpp |
| diffstat | 23 files changed, 1420 insertions(+), 104 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Sat Nov 15 11:49:39 2025 +0100 +++ b/.hgignore Sat Nov 15 12:27:24 2025 +0100 @@ -9,6 +9,7 @@ *~ *.cmake.orig .idea/ +OrthancFramework/Resources/CodeGeneration/.venv/ # when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore: .settings/
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Sat Nov 15 12:27:24 2025 +0100 @@ -549,6 +549,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/Resources/CodeGeneration/DicomTransferSyntaxes.json Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json Sat Nov 15 12:27:24 2025 +0100 @@ -5,7 +5,8 @@ "Value" : "LittleEndianImplicit", "Retired" : false, "DCMTK" : "EXS_LittleEndianImplicit", - "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian" + "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian", + "Raw": true, "Lossless": true }, { @@ -14,7 +15,8 @@ "Value" : "LittleEndianExplicit", "Retired" : false, "DCMTK" : "EXS_LittleEndianExplicit", - "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian" + "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian", + "Raw": true, "Lossless": true }, { @@ -22,7 +24,8 @@ "Name" : "Deflated Explicit VR Little Endian", "Value" : "DeflatedLittleEndianExplicit", "Retired" : false, - "DCMTK" : "EXS_DeflatedLittleEndianExplicit" + "DCMTK" : "EXS_DeflatedLittleEndianExplicit", + "Raw": false, "Lossless": true }, { @@ -30,7 +33,8 @@ "Name" : "Explicit VR Big Endian", "Value" : "BigEndianExplicit", "Retired" : false, - "DCMTK" : "EXS_BigEndianExplicit" + "DCMTK" : "EXS_BigEndianExplicit", + "Raw": true, "Lossless": true }, { @@ -41,7 +45,8 @@ "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression", "DCMTK" : "EXS_JPEGProcess1", "DCMTK360" : "EXS_JPEGProcess1TransferSyntax", - "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1" + "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1", + "Raw": false, "Lossless": false }, { @@ -52,7 +57,8 @@ "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)", "DCMTK" : "EXS_JPEGProcess2_4", "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax", - "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4" + "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4", + "Raw": false, "Lossless": false }, { @@ -61,7 +67,8 @@ "Value" : "JPEGProcess3_5", "Retired" : true, "DCMTK" : "EXS_JPEGProcess3_5", - "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -70,7 +77,8 @@ "Value" : "JPEGProcess6_8", "Retired" : true, "DCMTK" : "EXS_JPEGProcess6_8", - "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -79,7 +87,8 @@ "Value" : "JPEGProcess7_9", "Retired" : true, "DCMTK" : "EXS_JPEGProcess7_9", - "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -88,7 +97,8 @@ "Value" : "JPEGProcess10_12", "Retired" : true, "DCMTK" : "EXS_JPEGProcess10_12", - "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -97,7 +107,8 @@ "Value" : "JPEGProcess11_13", "Retired" : true, "DCMTK" : "EXS_JPEGProcess11_13", - "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -107,7 +118,8 @@ "Retired" : false, "DCMTK" : "EXS_JPEGProcess14", "DCMTK360" : "EXS_JPEGProcess14TransferSyntax", - "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14" + "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14", + "Raw": false, "Lossless": true }, { @@ -116,7 +128,8 @@ "Value" : "JPEGProcess15", "Retired" : true, "DCMTK" : "EXS_JPEGProcess15", - "DCMTK360" : "EXS_JPEGProcess15TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess15TransferSyntax", + "Raw": false, "Lossless": true }, { @@ -125,7 +138,8 @@ "Value" : "JPEGProcess16_18", "Retired" : true, "DCMTK" : "EXS_JPEGProcess16_18", - "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -134,7 +148,8 @@ "Value" : "JPEGProcess17_19", "Retired" : true, "DCMTK" : "EXS_JPEGProcess17_19", - "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -143,7 +158,8 @@ "Value" : "JPEGProcess20_22", "Retired" : true, "DCMTK" : "EXS_JPEGProcess20_22", - "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -152,7 +168,8 @@ "Value" : "JPEGProcess21_23", "Retired" : true, "DCMTK" : "EXS_JPEGProcess21_23", - "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -161,7 +178,8 @@ "Value" : "JPEGProcess24_26", "Retired" : true, "DCMTK" : "EXS_JPEGProcess24_26", - "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -170,7 +188,8 @@ "Value" : "JPEGProcess25_27", "Retired" : true, "DCMTK" : "EXS_JPEGProcess25_27", - "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax", + "Raw": false, "Lossless": false }, { @@ -179,7 +198,8 @@ "Value" : "JPEGProcess28", "Retired" : true, "DCMTK" : "EXS_JPEGProcess28", - "DCMTK360" : "EXS_JPEGProcess28TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess28TransferSyntax", + "Raw": false, "Lossless": true }, { @@ -188,7 +208,8 @@ "Value" : "JPEGProcess29", "Retired" : true, "DCMTK" : "EXS_JPEGProcess29", - "DCMTK360" : "EXS_JPEGProcess29TransferSyntax" + "DCMTK360" : "EXS_JPEGProcess29TransferSyntax", + "Raw": false, "Lossless": true }, { @@ -199,7 +220,8 @@ "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression", "DCMTK" : "EXS_JPEGProcess14SV1", "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax", - "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1" + "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1", + "Raw": false, "Lossless": true }, { @@ -208,7 +230,8 @@ "Value" : "JPEGLSLossless", "Retired" : false, "DCMTK" : "EXS_JPEGLSLossless", - "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless" + "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless", + "Raw": false, "Lossless": true }, { @@ -217,7 +240,8 @@ "Value" : "JPEGLSLossy", "Retired" : false, "DCMTK" : "EXS_JPEGLSLossy", - "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless" + "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless", + "Raw": false, "Lossless": false }, { @@ -226,7 +250,8 @@ "Value" : "JPEG2000LosslessOnly", "Retired" : false, "DCMTK" : "EXS_JPEG2000LosslessOnly", - "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless" + "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless", + "Raw": false, "Lossless": true }, { @@ -235,7 +260,8 @@ "Value" : "JPEG2000", "Retired" : false, "DCMTK" : "EXS_JPEG2000", - "GDCM" : "gdcm::TransferSyntax::JPEG2000" + "GDCM" : "gdcm::TransferSyntax::JPEG2000", + "Raw": false, "Lossless": false }, { @@ -244,7 +270,8 @@ "Value" : "JPEG2000MulticomponentLosslessOnly", "Retired" : false, "DCMTK" : "EXS_JPEG2000MulticomponentLosslessOnly", - "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless" + "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless", + "Raw": false, "Lossless": true }, { @@ -253,7 +280,8 @@ "Value" : "JPEG2000Multicomponent", "Retired" : false, "DCMTK" : "EXS_JPEG2000Multicomponent", - "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2" + "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2", + "Raw": false, "Lossless": false }, { @@ -261,7 +289,8 @@ "Name" : "JPIP Referenced", "Value" : "JPIPReferenced", "Retired" : false, - "DCMTK" : "EXS_JPIPReferenced" + "DCMTK" : "EXS_JPIPReferenced", + "Raw": false, "Lossless": true }, { @@ -269,7 +298,8 @@ "Name" : "JPIP Referenced Deflate", "Value" : "JPIPReferencedDeflate", "Retired" : false, - "DCMTK" : "EXS_JPIPReferencedDeflate" + "DCMTK" : "EXS_JPIPReferencedDeflate", + "Raw": false, "Lossless": true }, { @@ -277,7 +307,8 @@ "Name" : "MPEG2 Main Profile / Main Level", "Value" : "MPEG2MainProfileAtMainLevel", "Retired" : false, - "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel" + "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel", + "Raw": false, "Lossless": false }, { @@ -285,7 +316,8 @@ "Name" : "MPEG2 Main Profile / High Level", "Value" : "MPEG2MainProfileAtHighLevel", "Retired" : false, - "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel" + "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel", + "Raw": false, "Lossless": false }, { @@ -294,7 +326,8 @@ "Value" : "MPEG4HighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_1", - "SinceDCMTK" : "361" + "SinceDCMTK" : "361", + "Raw": false, "Lossless": false }, { @@ -303,7 +336,8 @@ "Value" : "MPEG4BDcompatibleHighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1", - "SinceDCMTK" : "361" + "SinceDCMTK" : "361", + "Raw": false, "Lossless": false }, { @@ -312,7 +346,8 @@ "Value" : "MPEG4HighProfileLevel4_2_For2DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo", - "SinceDCMTK" : "361" + "SinceDCMTK" : "361", + "Raw": false, "Lossless": false }, { @@ -321,7 +356,8 @@ "Value" : "MPEG4HighProfileLevel4_2_For3DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo", - "SinceDCMTK" : "361" + "SinceDCMTK" : "361", + "Raw": false, "Lossless": false }, { @@ -330,7 +366,8 @@ "Value" : "MPEG4StereoHighProfileLevel4_2", "Retired" : false, "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2", - "SinceDCMTK" : "361" + "SinceDCMTK" : "361", + "Raw": false, "Lossless": false }, { @@ -339,7 +376,8 @@ "Value" : "HEVCMainProfileLevel5_1", "Retired" : false, "DCMTK" : "EXS_HEVCMainProfileLevel5_1", - "SinceDCMTK" : "362" + "SinceDCMTK" : "362", + "Raw": false, "Lossless": false }, { @@ -348,7 +386,8 @@ "Value" : "HEVCMain10ProfileLevel5_1", "Retired" : false, "DCMTK" : "EXS_HEVCMain10ProfileLevel5_1", - "SinceDCMTK" : "362" + "SinceDCMTK" : "362", + "Raw": false, "Lossless": false }, { @@ -357,7 +396,8 @@ "Value" : "RLELossless", "Retired" : false, "DCMTK" : "EXS_RLELossless", - "GDCM" : "gdcm::TransferSyntax::RLELossless" + "GDCM" : "gdcm::TransferSyntax::RLELossless", + "Raw": false, "Lossless": true }, {
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache Sat Nov 15 12:27:24 2025 +0100 @@ -82,4 +82,44 @@ target.insert(DicomTransferSyntax_{{Value}}); {{/Syntaxes}} } + + + bool IsLossyTransferSyntax(DicomTransferSyntax syntax) + { + switch (syntax) + { + {{#Syntaxes}} + case DicomTransferSyntax_{{Value}}: + {{#Lossless}} + return false; + {{/Lossless}} + {{^Lossless}} + return true; + {{/Lossless}} + + {{/Syntaxes}} + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool IsRawTransferSyntax(DicomTransferSyntax syntax) + { + switch (syntax) + { + {{#Syntaxes}} + case DicomTransferSyntax_{{Value}}: + {{#Raw}} + return true; + {{/Raw}} + {{^Raw}} + return false; + {{/Raw}} + + {{/Syntaxes}} + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } }
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Sat Nov 15 12:27:24 2025 +0100 @@ -102,6 +102,8 @@ static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e); static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060); + static const DicomTag DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE(0x0054, 0x0022); + // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002); @@ -196,6 +198,7 @@ static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052); static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053); static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050); + static const DicomTag DICOM_TAG_SPACING_BETWEEN_SLICES(0x0018, 0x0088); static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050); static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051); static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e);
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Sat Nov 15 12:27:24 2025 +0100 @@ -31,6 +31,7 @@ #include "../SerializationToolbox.h" #include "FromDcmtkBridge.h" #include "ITagVisitor.h" +#include "DicomPixelMasker.h" #include <memory> // For std::unique_ptr @@ -1015,8 +1016,16 @@ } } + // (0.1) New in Orthanc 1.X.X: Apply pixel modifications + // This is done before modifying any tags because the pixelMasker has filters on the Orthanc ids -> + // the DICOM UID tags must not be modified before. + if (pixelMasker_ != NULL) + { + pixelMasker_->Apply(toModify); + } - // (0) Create a summary of the source file, if a custom generator + + // (0.2) Create a summary of the source file, if a custom generator // is provided if (identifierGenerator_ != NULL) { @@ -1153,6 +1162,7 @@ toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent, privateCreator_); } + } void DicomModification::SetAllowManualIdentifiers(bool check) @@ -1324,6 +1334,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); + } + if (!force) { /** @@ -1387,7 +1404,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); @@ -1401,7 +1418,7 @@ { if (request["DicomVersion"].type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadFileFormat); + throw OrthancException(ErrorCode_BadFileFormat, "DicomVersion should be a string"); } else { @@ -1443,6 +1460,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) @@ -1904,4 +1928,9 @@ target.insert(it->first); } } + + bool DicomModification::RequiresUncompressedTransferSyntax() const + { + return pixelMasker_ != NULL; + } }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h Sat Nov 15 12:27:24 2025 +0100 @@ -31,6 +31,8 @@ namespace Orthanc { + class DicomPixelMasker; + class ORTHANC_PUBLIC DicomModification : public boost::noncopyable { /** @@ -154,6 +156,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-PIXEL-ANON: check ownership & serialization + std::string MapDicomIdentifier(const std::string& original, ResourceType level); @@ -268,5 +273,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 Sat Nov 15 12:27:24 2025 +0100 @@ -0,0 +1,386 @@ +/** + * 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" +#include "../Logging.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_2D = "2D"; + static const char* KEY_REGION_3D = "3D"; + static const char* KEY_ORIGIN = "Origin"; + static const char* KEY_END = "End"; + static const char* KEY_TARGET_SERIES = "TargetSeries"; + static const char* KEY_TARGET_INSTANCES = "TargetInstances"; + + DicomPixelMasker::DicomPixelMasker() + { + } + + DicomPixelMasker::~DicomPixelMasker() + { + for (std::list<BaseRegion*>::iterator it = regions_.begin(); it != regions_.end(); ++it) + { + delete *it; + } + } + + DicomPixelMasker::BaseRegion::BaseRegion() : + mode_(DicomPixelMaskerMode_Undefined), + fillValue_(0), + filterWidth_(0) + { + } + + DicomPixelMasker::Region2D::Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2) : + x1_(x1), + y1_(y1), + x2_(x2), + y2_(y2) + { + } + + DicomPixelMasker::Region3D::Region3D(double x1, double y1, double z1, double x2, double y2, double z2) : + x1_(x1), + y1_(y1), + z1_(z1), + x2_(x2), + y2_(y2), + z2_(z2) + { + } + + bool DicomPixelMasker::BaseRegion::IsTargeted(const ParsedDicomFile& file) const + { + DicomInstanceHasher hasher = file.GetHasher(); + const std::string& seriesId = hasher.HashSeries(); + const std::string& instanceId = hasher.HashInstance(); + + if (targetSeries_.size() > 0 && targetSeries_.find(seriesId) == targetSeries_.end()) + { + return false; + } + + if (targetInstances_.size() > 0 && targetInstances_.find(instanceId) == targetInstances_.end()) + { + return false; + } + + return true; + } + + bool DicomPixelMasker::Region2D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const + { + if (IsTargeted(file)) + { + x1 = x1_; + y1 = y1_; + x2 = x2_; + y2 = y2_; + return true; + } + + return false; + } + + static void GetDoubleVector(std::vector<double>& target, const std::string& strValue, const DicomTag& tag, size_t expectedSize) + { + target.clear(); + + std::vector<std::string> strVector; + Toolbox::SplitString(strVector, strValue, '\\'); + + if (strVector.size() != expectedSize) + { + throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " length is invalid"); + } + + for (size_t i = 0; i < strVector.size(); ++i) + { + try + { + target.push_back(boost::lexical_cast<double>(strVector[i])); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " contains invalid value " + strVector[i]); + } + } + } + + static void GetDoubleVector(std::vector<double>& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize) + { + std::string str; + if (!file.GetTagValue(str, tag)) + { + throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format()); + } + + GetDoubleVector(target, str, tag, expectedSize); + } + + bool DicomPixelMasker::Region3D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const + { + if (IsTargeted(file)) + { + DicomMap tags; + file.ExtractDicomSummary(tags, 256); + + std::vector<double> imagePositionPatient; + std::vector<double> imageOrientationPatient; + std::vector<double> pixelSpacing; + double sliceSpacing = 0.0; + + if (file.HasTag(DICOM_TAG_IMAGE_POSITION_PATIENT) && file.HasTag(DICOM_TAG_IMAGE_ORIENTATION_PATIENT)) + { + GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); + GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + } + else if (file.HasTag(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE)) // find it in the detector info sequence (for some multi frame instances like NM or scintigraphy) TODO-PIXEL-ANON: to validate + { + const Json::Value& jsonSequence = tags.GetValue(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE).GetSequenceContent(); + if (jsonSequence.size() == 1) + { + std::string strImagePositionPatient = jsonSequence[0]["0020,0032"]["Value"].asString(); + std::string strImageOrientationPatient = jsonSequence[0]["0020,0037"]["Value"].asString(); + + GetDoubleVector(imagePositionPatient, strImagePositionPatient, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); + GetDoubleVector(imageOrientationPatient, strImageOrientationPatient, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + } + else + { + throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient in DetectorInformationSequence, invalid sequence size"); + } + } + else + { + throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient or ImageOrientationPatient"); + } + + GetDoubleVector(pixelSpacing, file, DICOM_TAG_PIXEL_SPACING, 2); + + if (file.HasTag(DICOM_TAG_SPACING_BETWEEN_SLICES)) + { + std::string strSliceSpacing; + if (file.GetTagValue(strSliceSpacing, DICOM_TAG_SPACING_BETWEEN_SLICES)) + { + sliceSpacing = boost::lexical_cast<double>(strSliceSpacing); + } + } + + double z = imagePositionPatient[2]; + + if (sliceSpacing != 0.0) + { + z = z - frameIndex * sliceSpacing; + } + + // note: To simplify, for the z, we only check that imagePositionPatient is between the authorized z values. + // This won't be perfectly true for weird images with slices that are not parallel but let's wait for someone to complain ... + if (z < std::min(z1_, z2_) || + z > std::max(z1_, z2_)) + { + return false; + } + + + double deltaX1 = x1_ - imagePositionPatient[0]; + double deltaY1 = y1_ - imagePositionPatient[1]; + double deltaZ1 = z1_ - z; + double deltaX2 = x2_ - imagePositionPatient[0]; + double deltaY2 = y2_ - imagePositionPatient[1]; + double deltaZ2 = z2_ - z; + + double ix1 = (deltaX1 * imageOrientationPatient[0] + deltaY1 * imageOrientationPatient[1] + deltaZ1 * imageOrientationPatient[2]) / pixelSpacing[0]; + double iy1 = (deltaX1 * imageOrientationPatient[3] + deltaY1 * imageOrientationPatient[4] + deltaZ1 * imageOrientationPatient[5]) / pixelSpacing[1]; + double ix2 = (deltaX2 * imageOrientationPatient[0] + deltaY2 * imageOrientationPatient[1] + deltaZ2 * imageOrientationPatient[2]) / pixelSpacing[0]; + double iy2 = (deltaX2 * imageOrientationPatient[3] + deltaY2 * imageOrientationPatient[4] + deltaZ2 * imageOrientationPatient[5]) / pixelSpacing[1]; + + std::string strRows; + std::string strColumns; + + if (!file.GetTagValue(strRows, DICOM_TAG_ROWS) || !file.GetTagValue(strColumns, DICOM_TAG_COLUMNS)) + { + throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing ROWS or COLUMNS tag"); + } + + // clip on image size + double rows = boost::lexical_cast<double>(strRows); + double columns = boost::lexical_cast<double>(strColumns); + + x1 = static_cast<unsigned int>(std::max(0.0, std::min(ix1, ix2))); + y1 = static_cast<unsigned int>(std::max(0.0, std::min(iy1, iy2))); + x2 = static_cast<unsigned int>(std::min(columns, std::max(ix1, ix2))); + y2 = static_cast<unsigned int>(std::min(rows, std::max(iy1, iy2))); + + return true; + } + + return false; + } + + void DicomPixelMasker::Apply(ParsedDicomFile& toModify) + { + for (std::list<BaseRegion*>::const_iterator itr = regions_.begin(); itr != regions_.end(); ++itr) + { + const BaseRegion* r = *itr; + + for (unsigned int i = 0; i < toModify.GetFramesCount(); ++i) + { + unsigned int x1, y1, x2, y2; + + if (r->GetPixelMaskArea(x1, y1, x2, y2, toModify, i)) + { + ImageAccessor imageRegion; + toModify.GetRawFrame(i)->GetRegion(imageRegion, x1, y1, x2 - x1, y2 - y1); + + if (r->GetMode() == DicomPixelMaskerMode_MeanFilter) + { + ImageProcessing::MeanFilter(imageRegion, r->GetFilterWidth(), r->GetFilterWidth()); + } + else if (r->GetMode() == DicomPixelMaskerMode_Fill) + { + ImageProcessing::Set(imageRegion, r->GetFillValue()); + } + } + } + } + } + + 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]; + + std::unique_ptr<BaseRegion> region; + + if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].isString()) + { + if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_2D) + { + 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) + { + unsigned int x = regionJson[KEY_ORIGIN][0].asUInt(); + unsigned int y = regionJson[KEY_ORIGIN][1].asUInt(); + unsigned int width = regionJson[KEY_END][0].asUInt() - x; + unsigned int height = regionJson[KEY_END][1].asUInt() - y; + + region.reset(new Region2D(x, y, width, height)); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates"); + } + + } + else if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_3D) + { + if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 3 && + regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 3) + { + double x1 = regionJson[KEY_ORIGIN][0].asDouble(); + double y1 = regionJson[KEY_ORIGIN][1].asDouble(); + double z1 = regionJson[KEY_ORIGIN][2].asDouble(); + double x2 = regionJson[KEY_END][0].asDouble(); + double y2 = regionJson[KEY_END][1].asDouble(); + double z2 = regionJson[KEY_END][2].asDouble(); + + region.reset(new Region3D(x1, y1, z1, x2, y2, z2)); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates"); + } + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, std::string(KEY_REGION_TYPE) + " unrecognized value '" + regionJson[KEY_REGION_TYPE].asString() +"'"); + } + } + + if (regionJson.isMember(KEY_MASK_TYPE) && regionJson[KEY_MASK_TYPE].isString()) + { + if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_FILL) + { + if (regionJson.isMember(KEY_FILL_VALUE) && regionJson[KEY_FILL_VALUE].isInt()) + { + region->SetFillValue(regionJson[KEY_FILL_VALUE].asInt()); + } + } + else if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_MEAN_FILTER) + { + if (regionJson.isMember(KEY_FILTER_WIDTH) && regionJson[KEY_FILTER_WIDTH].isUInt()) + { + region->SetMeanFilter(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()) + { + std::set<std::string> targetSeries; + SerializationToolbox::ReadSetOfStrings(targetSeries, regionJson, KEY_TARGET_SERIES); + region->SetTargetSeries(targetSeries); + } + + if (regionJson.isMember(KEY_TARGET_INSTANCES) && regionJson[KEY_TARGET_INSTANCES].isArray()) + { + std::set<std::string> targetInstances; + SerializationToolbox::ReadSetOfStrings(targetInstances, regionJson, KEY_TARGET_INSTANCES); + region->SetTargetInstances(targetInstances); + } + + regions_.push_back(region.release()); + } + } + } + + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h Sat Nov 15 12:27:24 2025 +0100 @@ -0,0 +1,145 @@ +/** + * 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 <set> + + +namespace Orthanc +{ + enum DicomPixelMaskerMode + { + DicomPixelMaskerMode_Fill, + DicomPixelMaskerMode_MeanFilter, + + DicomPixelMaskerMode_Undefined + }; + + class ORTHANC_PUBLIC DicomPixelMasker : public boost::noncopyable + { + class BaseRegion + { + DicomPixelMaskerMode mode_; + int32_t fillValue_; // pixel value + uint32_t filterWidth_; // filter width + std::set<std::string> targetSeries_; + std::set<std::string> targetInstances_; + + protected: + bool IsTargeted(const ParsedDicomFile& file) const; + BaseRegion(); + + public: + + virtual ~BaseRegion() + { + } + + virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const = 0; + + DicomPixelMaskerMode GetMode() const + { + return mode_; + } + + int32_t GetFillValue() const + { + assert(mode_ == DicomPixelMaskerMode_Fill); + return fillValue_; + } + + int32_t GetFilterWidth() const + { + assert(mode_ == DicomPixelMaskerMode_MeanFilter); + return filterWidth_; + } + + void SetFillValue(int32_t value) + { + mode_ = DicomPixelMaskerMode_Fill; + fillValue_ = value; + } + + void SetMeanFilter(uint32_t value) + { + mode_ = DicomPixelMaskerMode_MeanFilter; + filterWidth_ = value; + } + + void SetTargetSeries(const std::set<std::string> targetSeries) + { + targetSeries_ = targetSeries; + } + + void SetTargetInstances(const std::set<std::string> targetInstances) + { + targetInstances_ = targetInstances; + } + }; + + class Region2D : public BaseRegion + { + unsigned int x1_; + unsigned int y1_; + unsigned int x2_; + unsigned int y2_; + + public: + Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2); + + virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE; + }; + + class Region3D : public BaseRegion + { + double x1_; + double y1_; + double z1_; + double x2_; + double y2_; + double z2_; + + public: + Region3D(double x1, double y1, double z1, double x2, double y2, double z2); + + virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE; + }; + + private: + std::list<BaseRegion*> regions_; + + public: + DicomPixelMasker(); + + ~DicomPixelMasker(); + + void Apply(ParsedDicomFile& toModify); + + void ParseRequest(const Json::Value& request); + }; +}
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp Sat Nov 15 12:27:24 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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h Sat Nov 15 12:27:24 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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Sat Nov 15 12:27:24 2025 +0100 @@ -1819,6 +1819,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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Sat Nov 15 12:27:24 2025 +0100 @@ -316,6 +316,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/Enumerations.h Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Sat Nov 15 12:27:24 2025 +0100 @@ -965,6 +965,12 @@ DicomTransferSyntax GetTransferSyntax(const std::string& uid); ORTHANC_PUBLIC + bool IsRawTransferSyntax(DicomTransferSyntax syntax); + + ORTHANC_PUBLIC + bool IsLossyTransferSyntax(DicomTransferSyntax syntax); + + ORTHANC_PUBLIC const char* GetResourceTypeText(ResourceType type, bool isPlural, bool isUpperCase);
--- a/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h Sat Nov 15 12:27:24 2025 +0100 @@ -602,4 +602,276 @@ target.insert(DicomTransferSyntax_RFC2557MimeEncapsulation); target.insert(DicomTransferSyntax_XML); } + + + bool IsLossyTransferSyntax(DicomTransferSyntax syntax) + { + switch (syntax) + { + case DicomTransferSyntax_LittleEndianImplicit: + return false; + + case DicomTransferSyntax_LittleEndianExplicit: + return false; + + case DicomTransferSyntax_DeflatedLittleEndianExplicit: + return false; + + case DicomTransferSyntax_BigEndianExplicit: + return false; + + case DicomTransferSyntax_JPEGProcess1: + return true; + + case DicomTransferSyntax_JPEGProcess2_4: + return true; + + case DicomTransferSyntax_JPEGProcess3_5: + return true; + + case DicomTransferSyntax_JPEGProcess6_8: + return true; + + case DicomTransferSyntax_JPEGProcess7_9: + return true; + + case DicomTransferSyntax_JPEGProcess10_12: + return true; + + case DicomTransferSyntax_JPEGProcess11_13: + return true; + + case DicomTransferSyntax_JPEGProcess14: + return false; + + case DicomTransferSyntax_JPEGProcess15: + return false; + + case DicomTransferSyntax_JPEGProcess16_18: + return true; + + case DicomTransferSyntax_JPEGProcess17_19: + return true; + + case DicomTransferSyntax_JPEGProcess20_22: + return true; + + case DicomTransferSyntax_JPEGProcess21_23: + return true; + + case DicomTransferSyntax_JPEGProcess24_26: + return true; + + case DicomTransferSyntax_JPEGProcess25_27: + return true; + + case DicomTransferSyntax_JPEGProcess28: + return false; + + case DicomTransferSyntax_JPEGProcess29: + return false; + + case DicomTransferSyntax_JPEGProcess14SV1: + return false; + + case DicomTransferSyntax_JPEGLSLossless: + return false; + + case DicomTransferSyntax_JPEGLSLossy: + return true; + + case DicomTransferSyntax_JPEG2000LosslessOnly: + return false; + + case DicomTransferSyntax_JPEG2000: + return true; + + case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly: + return false; + + case DicomTransferSyntax_JPEG2000Multicomponent: + return true; + + case DicomTransferSyntax_JPIPReferenced: + return false; + + case DicomTransferSyntax_JPIPReferencedDeflate: + return false; + + case DicomTransferSyntax_MPEG2MainProfileAtMainLevel: + return true; + + case DicomTransferSyntax_MPEG2MainProfileAtHighLevel: + return true; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_1: + return true; + + case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1: + return true; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo: + return true; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo: + return true; + + case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2: + return true; + + case DicomTransferSyntax_HEVCMainProfileLevel5_1: + return true; + + case DicomTransferSyntax_HEVCMain10ProfileLevel5_1: + return true; + + case DicomTransferSyntax_RLELossless: + return false; + + case DicomTransferSyntax_RFC2557MimeEncapsulation: + return true; + + case DicomTransferSyntax_XML: + return true; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool IsRawTransferSyntax(DicomTransferSyntax syntax) + { + switch (syntax) + { + case DicomTransferSyntax_LittleEndianImplicit: + return true; + + case DicomTransferSyntax_LittleEndianExplicit: + return true; + + case DicomTransferSyntax_DeflatedLittleEndianExplicit: + return false; + + case DicomTransferSyntax_BigEndianExplicit: + return true; + + case DicomTransferSyntax_JPEGProcess1: + return false; + + case DicomTransferSyntax_JPEGProcess2_4: + return false; + + case DicomTransferSyntax_JPEGProcess3_5: + return false; + + case DicomTransferSyntax_JPEGProcess6_8: + return false; + + case DicomTransferSyntax_JPEGProcess7_9: + return false; + + case DicomTransferSyntax_JPEGProcess10_12: + return false; + + case DicomTransferSyntax_JPEGProcess11_13: + return false; + + case DicomTransferSyntax_JPEGProcess14: + return false; + + case DicomTransferSyntax_JPEGProcess15: + return false; + + case DicomTransferSyntax_JPEGProcess16_18: + return false; + + case DicomTransferSyntax_JPEGProcess17_19: + return false; + + case DicomTransferSyntax_JPEGProcess20_22: + return false; + + case DicomTransferSyntax_JPEGProcess21_23: + return false; + + case DicomTransferSyntax_JPEGProcess24_26: + return false; + + case DicomTransferSyntax_JPEGProcess25_27: + return false; + + case DicomTransferSyntax_JPEGProcess28: + return false; + + case DicomTransferSyntax_JPEGProcess29: + return false; + + case DicomTransferSyntax_JPEGProcess14SV1: + return false; + + case DicomTransferSyntax_JPEGLSLossless: + return false; + + case DicomTransferSyntax_JPEGLSLossy: + return false; + + case DicomTransferSyntax_JPEG2000LosslessOnly: + return false; + + case DicomTransferSyntax_JPEG2000: + return false; + + case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly: + return false; + + case DicomTransferSyntax_JPEG2000Multicomponent: + return false; + + case DicomTransferSyntax_JPIPReferenced: + return false; + + case DicomTransferSyntax_JPIPReferencedDeflate: + return false; + + case DicomTransferSyntax_MPEG2MainProfileAtMainLevel: + return false; + + case DicomTransferSyntax_MPEG2MainProfileAtHighLevel: + return false; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_1: + return false; + + case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1: + return false; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo: + return false; + + case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo: + return false; + + case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2: + return false; + + case DicomTransferSyntax_HEVCMainProfileLevel5_1: + return false; + + case DicomTransferSyntax_HEVCMain10ProfileLevel5_1: + return false; + + case DicomTransferSyntax_RLELossless: + return false; + + case DicomTransferSyntax_RFC2557MimeEncapsulation: + return false; + + case DicomTransferSyntax_XML: + return false; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } }
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp Sat Nov 15 12:27:24 2025 +0100 @@ -2789,6 +2789,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) { @@ -2822,6 +2835,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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.h Sat Nov 15 12:27:24 2025 +0100 @@ -201,6 +201,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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Sat Nov 15 12:27:24 2025 +0100 @@ -219,35 +219,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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Sat Nov 15 12:27:24 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> @@ -2085,6 +2086,106 @@ } } + void ServerContext::Modify(std::unique_ptr<ParsedDicomFile>& dicomFile, + DicomModification& modification, + bool transcode, + DicomTransferSyntax targetSyntax, + unsigned int lossyQuality, + bool keepSOPInstanceUidDuringLossyTranscoding) + { + const std::string originalSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicomFile->GetDcmtkObject()); + std::string forceModifiedSopInstanceUid; + + // do we need to transcode before ? + DicomTransferSyntax currentTransferSyntax; + if (modification.RequiresUncompressedTransferSyntax() && + dicomFile->LookupTransferSyntax(currentTransferSyntax) && + !IsRawTransferSyntax(currentTransferSyntax)) + { + 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); + + dicomFile.reset(transcoded.ReleaseAsParsedDicomFile()); + + if (IsLossyTransferSyntax(currentTransferSyntax)) + { + // TODO-PIXEL-ANON: Test this path with IngestTranscoding = lossy syntax + // TODO-PIXEL-ANON: + Test with source = lossy + modification requires a transcoding to a raw TS -> the transcoding shall not be performed after pixel modification since it is already being done now but the SOPInstance UID must be changed afterwards + + // this means we have moved from lossy to raw -> the SOPInstanceUID should have changed here but, + // let's keep the SOPInstanceUID unchanged during this pre-transcoding to make sure the orthanc ids are + // still the original ones when the pixelMasker is applied (since the pixelMasker has a filter on Orthanc ids). + // however, after the modification, we must make sure that we change the SOPInstanceUID. + if (dicomFile.get() && dicomFile->GetDcmtkObject().getDataset()) + { + const char* sopInstanceUid; + if (dicomFile->GetDcmtkObject().getDataset()->findAndGetString(DCM_SOPInstanceUID, sopInstanceUid, OFFalse).good()) + { + forceModifiedSopInstanceUid = sopInstanceUid; + dicomFile->GetDcmtkObject().getDataset()->putAndInsertString(DCM_SOPInstanceUID, originalSopInstanceUid.c_str(), OFTrue /* replace */); + } + } + } + + if (!transcode) // if we had to change the TS for the modification, we need to restore the original TS afterwards + { + transcode = true; + targetSyntax = currentTransferSyntax; + } + + } + + 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))); + } + } + // TODO-PIXEL-ANON: set forceModifiedSopInstanceUid if required + } + + + const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const { static const std::string redactedContent = "*** POTENTIAL PHI ***";
--- a/OrthancServer/Sources/ServerContext.h Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Sat Nov 15 12:27:24 2025 +0100 @@ -604,6 +604,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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp Sat Nov 15 12:27:24 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 Sat Nov 15 11:49:39 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Sat Nov 15 12:27:24 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()));
--- a/TODO Sat Nov 15 11:49:39 2025 +0100 +++ b/TODO Sat Nov 15 12:27:24 2025 +0100 @@ -462,3 +462,216 @@ * Create REST bindings with Slicer * Create REST bindings with Horos/OsiriX + + + + + + + +WORK IN PROGRESS: pixel-anon API +================================ + +Rest API proposal: + +{ + "MaskPixelData" : { + "Regions": [ + { + "MaskType": "MeanFilter", + "FilterWidth": 20, + "RegionType" : "2D", // area is defined by an area in pixel coordinates + "Origin": [150, 100], // X, Y in pixel coordinates + "End": [400, 200], // X, Y in pixel coordinates + "TargetSeries" : [ // the series the pixel mask applies to. If empty -> applies to all series + "cd589a09-6e705e06-57997219-7812eb49-709873a9" + ], + "TargetInstances" : [ // the instances the pixel mask applies to. If empty -> applies to all instances + ] + }, + { + "MaskType": "Fill", + "FillValue": 0, + "RegionType" : "3D", // area is defined by a volume in world coordinates + "Origin": [-150.5, -250.4, -811], // X, Y, Z in World coordinates + "End": [148.4, 220.7, -955], // X, Y, Z in World coordinates + "TargetSeries" : [ // in this mode, no need to list the instances since the Z coordinate shall handle that ! + "94df9100-3b476f5b-f4e8c381-d78c327f-a387bc7e" + ] + } + ] + } +} + +# anonymize a single instance +curl http://localhost:8043/instances/19565ed8-6bd8f20e-efbd6c34-36688133-bc91329e/anonymize --data-binary '{"MaskPixelData" : {"Regions": [{"MaskType": "MeanFilter", "FilterWidth": 20, "RegionType" : "Pixels", "Origin": [150, 100], "End": [400, 200]} ]} }' --output out.dcm +curl http://localhost:8043/instances/19565ed8-6bd8f20e-efbd6c34-36688133-bc91329e/anonymize --data-binary '{"MaskPixelData" : {"Regions": [{"MaskType": "Fill", "FillValue": 0, "RegionType" : "Pixels", "Origin": [150, 100], "End": [400, 200]} ]} }' --output out.dcm + +# modify all slices of one series of a study +curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.2", "StudyDescription": "modified-all-slices"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 4000, + "RegionType" : "2D", + "Origin": [150, 100], + "End": [400, 200], + "TargetSeries" : ["d5f6c1a2-d6f2f01a-a3cc4e8b-424476dc-f34b0cd1"] + }] + } +} +EOF + + +# modify a few slices of one series of a study +curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.3", "StudyDescription": "modified-few-slices"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 4000, + "RegionType" : "2D", + "Origin": [150, 100], + "End": [400, 200], + "TargetInstances" : ["11e0b13a-fb44c3d5-819193d8-314b3bb5-4da13bc4", "372d70bb-886e6cc5-a89eefcc-405b2346-f4676bb2"] + }] + } +} +EOF + +# modify a few slices of one series of a study with a 3D Region +curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.3", "StudyDescription": "modified-few-slices"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 4000, + "RegionType" : "3D", + "Origin": [-150.0, -300, -750], + "End": [150.0, -250, -1000] + }] + } +} +EOF + +curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.3", "StudyDescription": "modified-few-slices"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "MeanFilter", + "FilterWidth": 30, + "RegionType" : "3D", + "Origin": [-150.0, -300, -750], + "End": [150.0, -250, -1000] + }] + } +} +EOF + + +# modify all frames of a multiframe study (CARDIO) +curl -X POST http://localhost:8043/studies/595df1a1-74fe920a-4b9e3509-826f17a3-762a2dc3/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.4", "StudyDescription": "modified-multi-frame"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "MeanFilter", + "FilterWidth": 30, + "RegionType" : "2D", + "Origin": [150, 100], + "End": [400, 200] + }] + } +} +EOF + +# modify all frames of a multiframe US study (Cine US, TS = .50) +curl -X POST http://localhost:8043/studies/50f2961c-c35755eb-8762b05b-0e01cd97-dd8a294c/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.5", "StudyDescription": "modified-multi-frame-us"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "MeanFilter", + "FilterWidth": 30, + "RegionType" : "2D", + "Origin": [0, 0], + "End": [768, 45] + }] + } +} +EOF + + +# modify some frames of a multiframe scintigraphy study +curl -X POST http://localhost:8043/studies/ab67d5f8-95865506-8fb83c8b-93610651-ddce6e77/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.6", "StudyDescription": "modified-scinti"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 3000, + "RegionType" : "3D", + "Origin": [-150.0, -200, 200], + "End": [150.0, -150, 100] + }] + } +} +EOF + +# modify some frames of a multiframe scintigraphy series +curl -X POST http://localhost:8043/series/08a23232-a61c3cb9-5cdb4518-b725cd5d-820ee1f6/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.6", "StudyDescription": "modified-scinti"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 3000, + "RegionType" : "3D", + "Origin": [-150.0, -200, 200], + "End": [150.0, -150, 100] + }] + } +} +EOF + + + +# modify some frames of a multiframe PET-CT study +curl -X POST http://localhost:8043/studies/890e1167-55ad171a-7721ffec-db91e2c1-700778c0/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.7", "StudyDescription": "modified-pet-ct"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 3000, + "RegionType" : "3D", + "Origin": [-150.0, -100, -85], + "End": [150.0, 100, -250] + }] + } +} +EOF +
