changeset 4685:693f049729ba

New versions of Keep(), Remove() and Replace() in DicomModification that use DicomPath
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 08 Jun 2021 18:28:57 +0200
parents e3810750dc9d
children fcd2dc7c8f31
files OrthancFramework/Sources/DicomFormat/DicomPath.cpp OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/Sources/DicomParsing/DicomModification.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
diffstat 4 files changed, 250 insertions(+), 33 deletions(-) [+]
line wrap: on
line diff
--- 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]));        
       }
--- 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();
+      }
+    }
+  }
 }
--- 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<DicomTag> SetOfTags;
-    typedef std::map<DicomTag, Json::Value*> Replacements;
+    typedef std::set<DicomTag>                SetOfTags;
+    typedef std::map<DicomTag, Json::Value*>  Replacements;
+    typedef std::list<DicomTagRange>          RemovedRanges;
+    typedef std::list<DicomPath>              ListOfPaths;
+    typedef std::list<SequenceReplacement*>   SequenceReplacements;
+
     typedef std::map< std::pair<ResourceType, std::string>, std::string>  UidMap;
-    typedef std::list<DicomTagRange>  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);
   };
 }
--- 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<ParsedDicomFile> 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<ParsedDicomFile> 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<ParsedDicomFile> 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<ParsedDicomFile> dicom1(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+    std::unique_ptr<ParsedDicomFile> 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<ParsedDicomFile> 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
+  }
 }