changeset 5558:c1ed59a5bdc2

new LimitToThisLevelMainDicomTags reconstruct mode + * Housekeeper plugin: Added an option LimitMainDicomTagsReconstructLevel
author Alain Mazy <am@orthanc.team>
date Fri, 19 Apr 2024 11:27:39 +0200
parents 87c0fbc8f457
children 462b8f8a619c
files NEWS OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/ServerToolbox.h
diffstat 8 files changed, 206 insertions(+), 73 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Apr 19 10:52:44 2024 +0200
+++ b/NEWS	Fri Apr 19 11:27:39 2024 +0200
@@ -19,11 +19,18 @@
 
 * API version upgraded to 24
 * Added "MaximumPatientCount" in /system
+* Added a new "LimitToThisLevelMainDicomTags" field in the payload of 
+  /patients|studies|series/instances/../reconstruct to speed up the reconstruction
+  in case you just want to update the MainDicomTags of that resource level only 
+  e.g. after you have updated the 'ExtraMainDicomTags' for this level.
 
 Plugins
 -------
 
 * Multitenant DICOM plugin: added support for locales
+* Housekeeper plugin: Added an option "LimitMainDicomTagsReconstructLevel"
+  (allowed values: "Patient", "Study", "Series", "Instance").  This can greatly speed
+  up the housekeeper process e.g. if you have only update the Study level ExtraMainDicomTags.
 
 
 Version 1.12.3 (2024-01-31)
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Apr 19 11:27:39 2024 +0200
@@ -48,6 +48,10 @@
 static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
 static bool triggerOnIngestTranscodingChange_ = true;
 static bool triggerOnDicomWebCacheChange_ = true;
+static std::string limitMainDicomTagsReconstructLevel_ = "";
+static std::string limitToChange_ = "";
+static std::string limitToUrl_ = "";
+
 
 struct RunningPeriod
 {
@@ -544,24 +548,38 @@
       const Json::Value& change = changes["Changes"][i];
       int64_t seq = change["Seq"].asInt64();
 
-      if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+      if (!limitToChange_.empty()) // if updating only maindicomtags for a single level 
       {
-        Json::Value result;
+        if (change["ChangeType"] == limitToChange_)
+        {
+          Json::Value result;
+          Json::Value request;
+          request["ReconstructFiles"] = false;
+          request["LimitToThisLevelMainDicomTags"] = true;
+          OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
+        }
+      }
+      else
+      {
+        if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+        {
+          Json::Value result;
 
-        if (needsReconstruct)
-        {
-          Json::Value request;
-          if (needsReingest)
+          if (needsReconstruct)
           {
-            request["ReconstructFiles"] = true;
+            Json::Value request;
+            if (needsReingest)
+            {
+              request["ReconstructFiles"] = true;
+            }
+            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
           }
-          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
-        }
 
-        if (needsDicomWebCaching)
-        {
-          Json::Value request;
-          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+          if (needsDicomWebCaching)
+          {
+            Json::Value request;
+            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+          }
         }
       }
 
@@ -842,7 +860,11 @@
               "MainDicomTagsChange": true,
               "UnnecessaryDicomAsJsonFiles": true,
               "DicomWebCacheChange": true   // new in 1.12.2
-            }
+            },
+
+            // When rebuilding MainDicomTags, limit to a single level of resource.
+            // Allowed values: "Patient", "Study", "Series", "Instance"
+            "LimitMainDicomTagsReconstructLevel": "Study"
 
           }
         }
@@ -865,6 +887,33 @@
         triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
       }
 
+      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
+      if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
+        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
+      {
+        OrthancPlugins::LogError("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Patient")
+      {
+        limitToChange_ = "NewPatient";
+        limitToUrl_ = "patients";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Study")
+      {
+        limitToChange_ = "NewStudy";
+        limitToUrl_ = "studies";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Series")
+      {
+        limitToChange_ = "NewSeries";
+        limitToUrl_ = "series";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Instance")
+      {
+        limitToChange_ = "NewInstance";
+        limitToUrl_ = "instances";
+      }
+
       if (housekeeper.GetJson().isMember("Schedule"))
       {
         runningPeriods_.load(housekeeper.GetJson()["Schedule"]);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 19 11:27:39 2024 +0200
@@ -2764,13 +2764,15 @@
   }
 
 
-  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom)
+  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
   {
     class Operations : public IReadWriteOperations
     {
     private:
       DicomMap                              summary_;
       std::unique_ptr<DicomInstanceHasher>  hasher_;
+      bool                                  limitToThisLevelDicomTags_;
+      ResourceType                          limitToLevel_;
       bool                                  hasTransferSyntax_;
       DicomTransferSyntax                   transferSyntax_;
 
@@ -2812,7 +2814,9 @@
       }
 
     public:
-      explicit Operations(const ParsedDicomFile& dicom)
+      explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
+      : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
+        limitToLevel_(limitToLevel)
       {
         OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
         hasher_.reset(new DicomInstanceHasher(summary_));
@@ -2840,48 +2844,76 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        transaction.ClearMainDicomTags(patient);
-        transaction.ClearMainDicomTags(study);
-        transaction.ClearMainDicomTags(series);
-        transaction.ClearMainDicomTags(instance);
-
+        if (limitToThisLevelDicomTags_)
         {
           ResourcesContent content(false /* prevent the setting of metadata */);
-          content.AddResource(patient, ResourceType_Patient, summary_);
-          content.AddResource(study, ResourceType_Study, summary_);
-          content.AddResource(series, ResourceType_Series, summary_);
-          content.AddResource(instance, ResourceType_Instance, summary_);
-
+          int64_t resource = -1;
+          if (limitToLevel_ == ResourceType_Patient)
+          {
+            resource = patient;
+          }
+          else if (limitToLevel_ == ResourceType_Study)
+          {
+            resource = study;
+          }
+          else if (limitToLevel_ == ResourceType_Series)
+          {
+            resource = series;
+          }
+          else if (limitToLevel_ == ResourceType_Instance)
+          {
+            resource = instance;
+          }
+
+          transaction.ClearMainDicomTags(resource);
+          content.AddResource(resource, limitToLevel_, summary_);
           transaction.SetResourcesContent(content);
-
-          ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
-        
-          SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
-          SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
-          SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
-          SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          ReplaceMetadata(transaction, resource, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(limitToLevel_));
         }
-
-        if (hasTransferSyntax_)
+        else
         {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          transaction.ClearMainDicomTags(patient);
+          transaction.ClearMainDicomTags(study);
+          transaction.ClearMainDicomTags(series);
+          transaction.ClearMainDicomTags(instance);
+
+          {
+            ResourcesContent content(false /* prevent the setting of metadata */);
+            content.AddResource(patient, ResourceType_Patient, summary_);
+            content.AddResource(study, ResourceType_Study, summary_);
+            content.AddResource(series, ResourceType_Series, summary_);
+            content.AddResource(instance, ResourceType_Instance, summary_);
+
+            transaction.SetResourcesContent(content);
+
+            ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
+          
+            SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
+            SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
+            SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
+            SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          }
+
+          if (hasTransferSyntax_)
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          }
+
+          const DicomValue* value;
+          if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
+          }
         }
-
-        const DicomValue* value;
-        if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
-        }
-
       }
     };
 
-    Operations operations(dicom);
+    Operations operations(dicom, limitToThisLevelDicomTags, limitToLevel);
     Apply(operations);
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Apr 19 11:27:39 2024 +0200
@@ -755,7 +755,9 @@
                    const std::string& publicId,
                    ResourceType level);
 
-    void ReconstructInstance(const ParsedDicomFile& dicom);
+    void ReconstructInstance(const ParsedDicomFile& dicom, 
+                             bool limitToThisLevelDicomTags, 
+                             ResourceType limitToLevel_);
 
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
                       const DicomMap& dicomSummary,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 19 11:27:39 2024 +0200
@@ -58,6 +58,7 @@
 
 static const char* const IGNORE_LENGTH = "ignore-length";
 static const char* const RECONSTRUCT_FILES = "ReconstructFiles";
+static const char* const LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS = "LimitToThisLevelMainDicomTags";
 
 
 namespace Orthanc
@@ -3644,12 +3645,21 @@
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
-  void DocumentReconstructFilesField(RestApiPostCall& call)
+  void DocumentReconstructFilesField(RestApiPostCall& call, bool documentLimitField)
   {
     call.GetDocumentation()
       .SetRequestField(RECONSTRUCT_FILES, RestApiCallDocumentation::Type_Boolean,
                        "Also reconstruct the files of the resources (e.g: apply IngestTranscoding, StorageCompression). "
                        "'false' by default. (New in Orthanc 1.11.0)", false);
+    if (documentLimitField)
+    {
+      call.GetDocumentation()
+        .SetRequestField(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_Boolean,
+                        "Only reconstruct this level MainDicomTags by re-reading them from a random child instance of the resource. "
+                        "This option is much faster than a full reconstruct and is usefull e.g. if you have modified the "
+                        "'ExtraMainDicomTags' at the Study level to optimize the speed of some C-Find. "
+                        "'false' by default. (New in Orthanc 1.12.4)", false);
+    }
   }
 
   bool GetReconstructFilesField(const RestApiPostCall& call)
@@ -3671,6 +3681,26 @@
     return reconstructFiles;
   }
 
+  bool GetLimitToThisLevelMainDicomTags(const RestApiPostCall& call)
+  {
+    bool limitToThisLevel = false;
+    Json::Value request;
+
+    if (call.GetBodySize() > 0 && call.ParseJsonRequest(request) && request.isMember(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS))
+    {
+      if (!request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].isBool())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The field " + std::string(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS) + " must contain a Boolean");
+      }
+
+      limitToThisLevel = request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].asBool();
+    }
+
+    return limitToThisLevel;
+  }
+
+
   template <enum ResourceType type>
   static void ReconstructResource(RestApiPostCall& call)
   {
@@ -3686,13 +3716,13 @@
                         "Beware that this is a time-consuming operation, as all the children DICOM instances will be "
                         "parsed again, and the Orthanc index will be updated accordingly.")
         .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, true);
 
       return;
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call));
+    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call), GetLimitToThisLevelMainDicomTags(call), type);
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
@@ -3710,7 +3740,7 @@
                         "as all the DICOM instances will be parsed again, and as all the Orthanc index will be regenerated. "
                         "If you have a large database to process, it is advised to use the Housekeeper plugin to perform "
                         "this action resource by resource");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, false);
 
       return;
     }
@@ -3724,7 +3754,7 @@
     for (std::list<std::string>::const_iterator 
            study = studies.begin(); study != studies.end(); ++study)
     {
-      ServerToolbox::ReconstructResource(context, *study, reconstructFiles);
+      ServerToolbox::ReconstructResource(context, *study, reconstructFiles, false, ResourceType_Study /*  dummy */);
     }
     
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Fri Apr 19 11:27:39 2024 +0200
@@ -186,7 +186,7 @@
         ServerContext::DicomCacheLocker locker(GetContext(), *it);
         ParsedDicomFile& modifiedDicom = locker.GetDicom();
 
-        GetContext().GetIndex().ReconstructInstance(modifiedDicom);
+        GetContext().GetIndex().ReconstructInstance(modifiedDicom, false, ResourceType_Instance /* dummy */);
       }
     }
     
--- a/OrthancServer/Sources/ServerToolbox.cpp	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Fri Apr 19 11:27:39 2024 +0200
@@ -280,32 +280,43 @@
     
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles)
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel)
     {
       LOG(WARNING) << "Reconstructing resource " << resource;
       
       std::list<std::string> instances;
       context.GetIndex().GetChildInstances(instances, resource);
 
-      for (std::list<std::string>::const_iterator 
-             it = instances.begin(); it != instances.end(); ++it)
+
+      if (limitToThisLevelDicomTags && instances.size() > 0) // in this case, we only need to rebuild one instance !
       {
-        ServerContext::DicomCacheLocker locker(context, *it);
+        ServerContext::DicomCacheLocker locker(context, instances.front());
+        context.GetIndex().ReconstructInstance(locker.GetDicom(), true, limitToLevel);
+      }
+      else
+      {
+        for (std::list<std::string>::const_iterator 
+              it = instances.begin(); it != instances.end(); ++it)
+        {
+          ServerContext::DicomCacheLocker locker(context, *it);
 
-        // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
-        context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
-                                            -1 /* dummy revision */, "" /* dummy MD5 */);
-        
-        context.GetIndex().ReconstructInstance(locker.GetDicom());
+          // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
+          context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
+                                              -1 /* dummy revision */, "" /* dummy MD5 */);
+          
+          context.GetIndex().ReconstructInstance(locker.GetDicom(), false, ResourceType_Instance /* dummy */);
 
-        if (reconstructFiles)
-        {
-          std::string resultPublicId;  // ignored
-          std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
+          if (reconstructFiles)
+          {
+            std::string resultPublicId;  // ignored
+            std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
 
-          // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
-          // we should very likely refactor it
-          context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
+            // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
+            // we should very likely refactor it
+            context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
+          }
         }
       }
     }
--- a/OrthancServer/Sources/ServerToolbox.h	Fri Apr 19 10:52:44 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Fri Apr 19 11:27:39 2024 +0200
@@ -55,7 +55,9 @@
 
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles);
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel);
 
     bool IsValidLabel(const std::string& label);