# HG changeset patch # User Alain Mazy # Date 1713518859 -7200 # Node ID c1ed59a5bdc24703955be92eefb58f38ecc6f931 # Parent 87c0fbc8f4579aea2df3e5b1d7902a72e9e89c70 new LimitToThisLevelMainDicomTags reconstruct mode + * Housekeeper plugin: Added an option LimitMainDicomTagsReconstructLevel diff -r 87c0fbc8f457 -r c1ed59a5bdc2 NEWS --- 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) diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp --- 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"]); diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- 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 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); } diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- 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& instanceMetadata, const DicomMap& dicomSummary, diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- 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 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::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); diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp --- 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 */); } } diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/ServerToolbox.cpp --- 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 instances; context.GetIndex().GetChildInstances(instances, resource); - for (std::list::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::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::CreateFromParsedDicomFile(locker.GetDicom())); + if (reconstructFiles) + { + std::string resultPublicId; // ignored + std::unique_ptr 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); + } } } } diff -r 87c0fbc8f457 -r c1ed59a5bdc2 OrthancServer/Sources/ServerToolbox.h --- 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);