# HG changeset patch # User Sebastien Jodogne # Date 1623169737 -7200 # Node ID 693f049729bac81f578647f6c93c907b1ab327d4 # Parent e3810750dc9dba3de04d116a9d9937a9dcec04c7 New versions of Keep(), Remove() and Replace() in DicomModification that use DicomPath diff -r e3810750dc9d -r 693f049729ba OrthancFramework/Sources/DicomFormat/DicomPath.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp Tue Jun 08 14:42:09 2021 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp Tue Jun 08 18:28:57 2021 +0200 @@ -154,7 +154,7 @@ { prefix_.reserve(parentTags.size()); - for (size_t i = 0; i < prefix_.size(); i++) + for (size_t i = 0; i < parentTags.size(); i++) { prefix_.push_back(PrefixItem::CreateIndexed(parentTags[i], parentIndexes[i])); } diff -r e3810750dc9d -r 693f049729ba OrthancFramework/Sources/DicomParsing/DicomModification.cpp --- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Jun 08 14:42:09 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Jun 08 18:28:57 2021 +0200 @@ -169,6 +169,22 @@ else { // We are within a sequence + + if (!that_.keepSequences_.empty()) + { + // New in Orthanc 1.9.4 - Solves issue LSD-629 + DicomPath path(parentTags, parentIndexes, tag); + + for (ListOfPaths::const_iterator it = that_.keepSequences_.begin(); + it != that_.keepSequences_.end(); ++it) + { + if (DicomPath::IsMatch(*it, path)) + { + return Action_None; + } + } + } + if (tag == DICOM_TAG_STUDY_INSTANCE_UID) { newValue = that_.MapDicomIdentifier(value, ResourceType_Study); @@ -247,19 +263,15 @@ }; - bool DicomModification::CancelReplacement(const DicomTag& tag) + void DicomModification::CancelReplacement(const DicomTag& tag) { Replacements::iterator it = replacements_.find(tag); if (it != replacements_.end()) { + assert(it->second != NULL); delete it->second; replacements_.erase(it); - return true; - } - else - { - return false; } } @@ -271,6 +283,7 @@ if (it != replacements_.end()) { + assert(it->second != NULL); delete it->second; it->second = NULL; // In the case of an exception during the clone it->second = new Json::Value(value); // Clone @@ -287,10 +300,21 @@ for (Replacements::iterator it = replacements_.begin(); it != replacements_.end(); ++it) { + assert(it->second != NULL); delete it->second; } replacements_.clear(); + + for (SequenceReplacements::iterator it = sequenceReplacements_.begin(); + it != sequenceReplacements_.end(); ++it) + { + assert(*it != NULL); + assert((*it)->GetPath().GetPrefixLength() > 0); + delete *it; + } + + sequenceReplacements_.clear(); } @@ -298,13 +322,17 @@ { Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD); - if (it != replacements_.end() && - (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 || - it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c || - it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b)) + if (it != replacements_.end()) { - delete it->second; - replacements_.erase(it); + assert(it->second != NULL); + + if (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 || + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c || + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b) + { + delete it->second; + replacements_.erase(it); + } } } @@ -413,14 +441,11 @@ void DicomModification::Keep(const DicomTag& tag) { - bool wasRemoved = IsRemoved(tag); - bool wasCleared = IsCleared(tag); - removals_.erase(tag); clearings_.erase(tag); uids_.erase(tag); - bool wasReplaced = CancelReplacement(tag); + CancelReplacement(tag); if (tag == DICOM_TAG_STUDY_INSTANCE_UID) { @@ -438,12 +463,6 @@ { privateTagsToKeep_.insert(tag); } - else if (!wasRemoved && - !wasReplaced && - !wasCleared) - { - LOG(WARNING) << "Marking this tag as to be kept has no effect: " << tag.Format(); - } MarkNotOrthancAnonymization(); } @@ -528,6 +547,7 @@ } else { + assert(it->second != NULL); return *it->second; } } @@ -727,6 +747,8 @@ level_ = ResourceType_Patient; uidMap_.clear(); privateTagsToKeep_.clear(); + keepSequences_.clear(); + removeSequences_.clear(); switch (version) { @@ -937,11 +959,29 @@ for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it) { + assert(it->second != NULL); toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent, privateCreator_); } - // (6) Update the DICOM identifiers + // (6) New in Orthanc 1.9.4: Apply modifications to subsequences + for (ListOfPaths::const_iterator it = removeSequences_.begin(); + it != removeSequences_.end(); ++it) + { + assert(it->GetPrefixLength() > 0); + toModify.RemovePath(*it); + } + + for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin(); + it != sequenceReplacements_.end(); ++it) + { + assert(*it != NULL); + assert((*it)->GetPath().GetPrefixLength() > 0); + toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */, + DicomReplaceMode_InsertIfAbsent, privateCreator_); + } + + // (7) Update the DICOM identifiers if (level_ <= ResourceType_Study && !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { @@ -981,7 +1021,7 @@ } } - // (7) Update the "referenced" relationships in the case of an anonymization + // (8) Update the "referenced" relationships in the case of an anonymization if (isAnonymization_) { RelationshipsVisitor visitor(*this); @@ -1089,7 +1129,7 @@ "requires the \"Force\" option to be set to true"); } - target.Replace(tag, value, false); + target.Replace(tag, value, false /* not safe for anonymization */); LOG(TRACE) << "Replace: " << name << " (" << tag.Format() << ") == " << value.toStyledString(); @@ -1483,4 +1523,50 @@ { return privateCreator_; } + + + void DicomModification::Keep(const DicomPath& path) + { + if (path.GetPrefixLength() == 0) + { + Keep(path.GetFinalTag()); + } + + keepSequences_.push_back(path); + MarkNotOrthancAnonymization(); + } + + + void DicomModification::Remove(const DicomPath& path) + { + if (path.GetPrefixLength() == 0) + { + Remove(path.GetFinalTag()); + } + else + { + removeSequences_.push_back(path); + MarkNotOrthancAnonymization(); + } + } + + + void DicomModification::Replace(const DicomPath& path, + const Json::Value& value, + bool safeForAnonymization) + { + if (path.GetPrefixLength() == 0) + { + Replace(path.GetFinalTag(), value, safeForAnonymization); + } + else + { + sequenceReplacements_.push_back(new SequenceReplacement(path, value)); + + if (!safeForAnonymization) + { + MarkNotOrthancAnonymization(); + } + } + } } diff -r e3810750dc9d -r 693f049729ba OrthancFramework/Sources/DicomParsing/DicomModification.h --- a/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Jun 08 14:42:09 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Jun 08 18:28:57 2021 +0200 @@ -97,12 +97,40 @@ bool Contains(const DicomTag& tag) const; }; + + class SequenceReplacement : public boost::noncopyable + { + private: + DicomPath path_; + Json::Value value_; + + public: + SequenceReplacement(const DicomPath& path, + const Json::Value& value) : + path_(path), + value_(value) + { + } + + const DicomPath& GetPath() const + { + return path_; + } + + const Json::Value& GetValue() const + { + return value_; + } + }; - typedef std::set SetOfTags; - typedef std::map Replacements; + typedef std::set SetOfTags; + typedef std::map Replacements; + typedef std::list RemovedRanges; + typedef std::list ListOfPaths; + typedef std::list SequenceReplacements; + typedef std::map< std::pair, std::string> UidMap; - typedef std::list RemovedRanges; - + SetOfTags removals_; SetOfTags clearings_; Replacements replacements_; @@ -122,8 +150,11 @@ IDicomIdentifierGenerator* identifierGenerator_; // New in Orthanc 1.9.4 - SetOfTags uids_; - RemovedRanges removedRanges_; + SetOfTags uids_; + RemovedRanges removedRanges_; + ListOfPaths keepSequences_; // Can *possibly* be a path whose prefix is empty + ListOfPaths removeSequences_; // Must *never* be a path whose prefix is empty + SequenceReplacements sequenceReplacements_; // Must *never* be a path whose prefix is empty std::string MapDicomIdentifier(const std::string& original, ResourceType level); @@ -139,7 +170,7 @@ void ClearReplacements(); - bool CancelReplacement(const DicomTag& tag); + void CancelReplacement(const DicomTag& tag); void ReplaceInternal(const DicomTag& tag, const Json::Value& value); @@ -214,5 +245,16 @@ void SetPrivateCreator(const std::string& privateCreator); const std::string& GetPrivateCreator() const; + + // New in Orthanc 1.9.4 + void Keep(const DicomPath& path); + + // New in Orthanc 1.9.4 + void Remove(const DicomPath& path); + + // New in Orthanc 1.9.4 + void Replace(const DicomPath& path, + const Json::Value& value, // Encoded using UTF-8 + bool safeForAnonymization); }; } diff -r e3810750dc9d -r 693f049729ba OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Jun 08 14:42:09 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Jun 08 18:28:57 2021 +0200 @@ -2447,6 +2447,7 @@ static const char* REF_SOP_CLASS = "0008,1150"; static const char* REF_SOP_INSTANCE = "0008,1155"; static const char* REL_SERIES_SEQ = "0008,1250"; + static const char* STUDY_INSTANCE_UID = "0020,000d"; { std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); @@ -2613,6 +2614,94 @@ ASSERT_EQ("", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.Replace(DicomPath(DICOM_TAG_PATIENT_NAME), "Hello1", false); + modif.Replace(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"), "Hello2", false); + modif.Replace(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue"), "Hello3", false); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_TRUE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ("Hello1", vv[PATIENT_NAME].asString()); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello2", vv[REF_IM_SEQ][1][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello3", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); + ASSERT_EQ(2u, vv[REL_SERIES_SEQ][0].size()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.Remove(DicomPath(DICOM_TAG_PATIENT_NAME)); + modif.Remove(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID")); + modif.Remove(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence")); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_FALSE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ(2u, vv[REF_IM_SEQ][0].size()); + ASSERT_TRUE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_EQ(1u, vv[REF_IM_SEQ][1].size()); + ASSERT_FALSE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + ASSERT_EQ(1u, vv[REL_SERIES_SEQ][0].size()); + } + + { + std::unique_ptr dicom1(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + std::unique_ptr dicom2(dicom1->Clone(true)); + + { + DicomModification modif; + modif.SetupAnonymization(DicomVersion_2021b); + modif.Apply(*dicom1); + modif.Apply(*dicom2); + } + + // Same anonymization context and same input DICOM => hence, same output DICOM + Json::Value vv1, vv2; + dicom1->DatasetToJson(vv1, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + dicom2->DatasetToJson(vv2, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(vv1.toStyledString(), vv2.toStyledString()); + + ASSERT_TRUE(Toolbox::IsUuid(vv1[PATIENT_NAME].asString())); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv1[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv1[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv1[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); + ASSERT_NE("1.2.840.113704.1.111.7016.1342451220.40", vv1[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); + ASSERT_EQ("WORLD", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][PATIENT_NAME].asString()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.SetupAnonymization(DicomVersion_2021b); + modif.Keep(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPInstanceUID")); + modif.Keep(DicomPath::Parse("RelatedSeriesSequence")); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); // kept + ASSERT_EQ("1.2.840.113704.1.111.7016.1342451220.40", vv[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); // kept + } }