Mercurial > hg > orthanc
changeset 4196:37310bb1cd30
Fix handling of "ModalitiesInStudy" (0008,0061) in C-FIND and "/tools/find"
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 16 Sep 2020 13:22:30 +0200 |
parents | 2bc49197f806 |
children | b1d528687e25 |
files | NEWS OrthancServer/Sources/Search/DatabaseLookup.cpp OrthancServer/Sources/Search/DatabaseLookup.h OrthancServer/Sources/Search/DicomTagConstraint.cpp OrthancServer/Sources/Search/DicomTagConstraint.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp |
diffstat | 8 files changed, 307 insertions(+), 41 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Sep 16 10:22:25 2020 +0200 +++ b/NEWS Wed Sep 16 13:22:30 2020 +0200 @@ -19,7 +19,7 @@ * When checking DICOM allowed methods, if there are multiple modalities with the same AET, differentiate them from the calling IP * Enable the access to raw frames in Philips ELSCINT1 proprietary compression - +* Fix handling of "ModalitiesInStudy" (0008,0061) in C-FIND and "/tools/find" Version 1.7.3 (2020-08-24)
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Sep 16 13:22:30 2020 +0200 @@ -66,7 +66,7 @@ } - void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint) + void DatabaseLookup::AddConstraintInternal(DicomTagConstraint* constraint) { if (constraint == NULL) { @@ -163,28 +163,20 @@ if (!lower.empty()) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag)); } if (!upper.empty()) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); } } - else if (tag == DICOM_TAG_MODALITIES_IN_STUDY || - dicomQuery.find('\\') != std::string::npos) + else if (dicomQuery.find('\\') != std::string::npos) { DicomTag fixedTag(tag); - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - fixedTag = DICOM_TAG_MODALITY; - } - std::unique_ptr<DicomTagConstraint> constraint (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag)); @@ -196,7 +188,7 @@ constraint->AddValue(items[i]); } - AddConstraint(constraint.release()); + AddConstraintInternal(constraint.release()); } else if ( /** @@ -219,13 +211,13 @@ (dicomQuery.find('*') != std::string::npos || dicomQuery.find('?') != std::string::npos)) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag)); } else { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); } } @@ -313,4 +305,34 @@ return true; } + + + std::string DatabaseLookup::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + s += ("Contraint " + boost::lexical_cast<std::string>(i) + ": " + + constraints_[i]->Format() + "\n"); + } + + return s; + } + + + bool DatabaseLookup::HasTag(const DicomTag& tag) const + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (constraints_[i]->GetTag() == tag) + { + return true; + } + } + + return false; + } }
--- a/OrthancServer/Sources/Search/DatabaseLookup.h Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.h Wed Sep 16 13:22:30 2020 +0200 @@ -50,7 +50,7 @@ bool caseSensitive, bool mandatoryTag); - void AddConstraint(DicomTagConstraint* constraint); // Takes ownership + void AddConstraintInternal(DicomTagConstraint* constraint); // Takes ownership public: DatabaseLookup() @@ -87,6 +87,15 @@ bool caseSensitive, bool mandatoryTag); + void AddConstraint(const DicomTagConstraint& constraint) + { + AddConstraintInternal(new DicomTagConstraint(constraint)); + } + bool HasOnlyMainDicomTags() const; + + std::string Format() const; + + bool HasTag(const DicomTag& tag) const; }; }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Wed Sep 16 13:22:30 2020 +0200 @@ -154,6 +154,16 @@ } + DicomTagConstraint::DicomTagConstraint(const DicomTagConstraint& other) : + tag_(other.tag_), + constraintType_(other.constraintType_), + values_(other.values_), + caseSensitive_(other.caseSensitive_), + mandatory_(other.mandatory_) + { + } + + DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) : tag_(constraint.GetTag()), constraintType_(constraint.GetConstraintType()), @@ -333,7 +343,7 @@ s += *it; } - return s + "]"; + return s + " ]"; } default:
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Wed Sep 16 13:22:30 2020 +0200 @@ -70,6 +70,8 @@ bool caseSensitive, bool mandatory); + DicomTagConstraint(const DicomTagConstraint& other); + DicomTagConstraint(const DatabaseConstraint& constraint); const DicomTag& GetTag() const @@ -77,6 +79,11 @@ return tag_; } + void SetTag(const DicomTag& tag) + { + tag_ = tag; + } + ConstraintType GetConstraintType() const { return constraintType_;
--- a/OrthancServer/Sources/ServerContext.cpp Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Sep 16 13:22:30 2020 +0200 @@ -34,10 +34,10 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" -#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" #include "../../OrthancFramework/Sources/Cache/SharedArchive.h" #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h" #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h" @@ -936,12 +936,12 @@ } - void ServerContext::Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) - { + void ServerContext::ApplyInternal(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit) + { unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); @@ -994,7 +994,6 @@ continue; } -#if 1 // New in Orthanc 1.6.0: Only keep the main DICOM tags at the // level of interest for the query switch (queryLevel) @@ -1016,17 +1015,6 @@ default: throw OrthancException(ErrorCode_InternalError); } - - // Special case of the "Modality" at the study level, in order - // to deal with C-FIND on "ModalitiesInStudy" (0008,0061). - // Check out integration test "test_rest_modalities_in_study". - if (queryLevel == ResourceType_Study) - { - dicom.CopyTagIfExists(tmp, DICOM_TAG_MODALITY); - } -#else - dicom.Assign(tmp); // This emulates Orthanc <= 1.5.8 -#endif hasOnlyMainDicomTags = true; } @@ -1096,6 +1084,225 @@ } + + namespace + { + class ModalitiesInStudyVisitor : public ServerContext::ILookupVisitor + { + private: + class Study : public boost::noncopyable + { + private: + std::string orthancId_; + std::string instanceId_; + DicomMap mainDicomTags_; + Json::Value dicomAsJson_; + std::set<std::string> modalitiesInStudy_; + + public: + Study(const std::string& instanceId, + const DicomMap& seriesTags) : + instanceId_(instanceId), + dicomAsJson_(Json::nullValue) + { + { + DicomMap tmp; + tmp.Assign(seriesTags); + tmp.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "dummy", false); + DicomInstanceHasher hasher(tmp); + orthancId_ = hasher.HashStudy(); + } + + mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Study); + mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Patient); + AddModality(seriesTags); + } + + void AddModality(const DicomMap& seriesTags) + { + std::string modality; + if (seriesTags.LookupStringValue(modality, DICOM_TAG_MODALITY, false) && + !modality.empty()) + { + modalitiesInStudy_.insert(modality); + } + } + + void SetDicomAsJson(const Json::Value& dicomAsJson) + { + dicomAsJson_ = dicomAsJson; + } + + const std::string& GetOrthancId() const + { + return orthancId_; + } + + const std::string& GetInstanceId() const + { + return instanceId_; + } + + const DicomMap& GetMainDicomTags() const + { + return mainDicomTags_; + } + + const Json::Value* GetDicomAsJson() const + { + if (dicomAsJson_.type() == Json::nullValue) + { + return NULL; + } + else + { + return &dicomAsJson_; + } + } + }; + + typedef std::map<std::string, Study*> Studies; + + bool isDicomAsJsonNeeded_; + bool complete_; + Studies studies_; + + public: + ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded) : + isDicomAsJsonNeeded_(isDicomAsJsonNeeded) + { + } + + ~ModalitiesInStudyVisitor() + { + for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + studies_.clear(); + } + + virtual bool IsDicomAsJsonNeeded() const + { + return isDicomAsJsonNeeded_; + } + + virtual void MarkAsComplete() + { + complete_ = true; + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId, + const DicomMap& seriesTags, + const Json::Value* dicomAsJson) + { + std::string studyInstanceUid; + if (seriesTags.LookupStringValue(studyInstanceUid, DICOM_TAG_STUDY_INSTANCE_UID, false)) + { + Studies::iterator found = studies_.find(studyInstanceUid); + if (found == studies_.end()) + { + // New study + std::unique_ptr<Study> study(new Study(instanceId, seriesTags)); + + if (dicomAsJson != NULL) + { + study->SetDicomAsJson(*dicomAsJson); + } + + studies_[studyInstanceUid] = study.release(); + } + else + { + // Already existing study + found->second->AddModality(seriesTags); + } + } + } + + void Forward(ILookupVisitor& callerVisitor, + size_t since, + size_t limit) const + { + size_t index = 0; + size_t countForwarded = 0; + + for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it, index++) + { + if (limit == 0 || + (index >= since && + index < limit)) + { + assert(it->second != NULL); + const Study& study = *it->second; + + countForwarded++; + callerVisitor.Visit(study.GetOrthancId(), study.GetInstanceId(), + study.GetMainDicomTags(), study.GetDicomAsJson()); + } + } + + if (countForwarded == studies_.size()) + { + callerVisitor.MarkAsComplete(); + } + } + }; + } + + + void ServerContext::Apply(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit) + { + if (queryLevel == ResourceType_Study && + lookup.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + // Convert the study-level query, into a series-level query, + // where "ModalitiesInStudy" is replaced by "Modality" + DatabaseLookup seriesLookup; + + for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) + { + const DicomTagConstraint& constraint = lookup.GetConstraint(i); + if (constraint.GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + if ((constraint.GetConstraintType() == ConstraintType_Equal && constraint.GetValue().empty()) || + (constraint.GetConstraintType() == ConstraintType_List && constraint.GetValues().empty())) + { + // Ignore universal lookup on "ModalitiesInStudy" (0008,0061), + // this should have been handled by the caller + return ApplyInternal(visitor, lookup, queryLevel, since, limit); + } + else + { + DicomTagConstraint modality(constraint); + modality.SetTag(DICOM_TAG_MODALITY); + seriesLookup.AddConstraint(modality); + } + } + else + { + seriesLookup.AddConstraint(constraint); + } + } + + ModalitiesInStudyVisitor seriesVisitor(visitor.IsDicomAsJsonNeeded()); + ApplyInternal(seriesVisitor, seriesLookup, ResourceType_Series, 0, 0); + seriesVisitor.Forward(visitor, since, limit); + } + else + { + ApplyInternal(visitor, lookup, queryLevel, since, limit); + } + } + + bool ServerContext::LookupOrReconstructMetadata(std::string& target, const std::string& publicId, MetadataType metadata)
--- a/OrthancServer/Sources/ServerContext.h Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed Sep 16 13:22:30 2020 +0200 @@ -80,6 +80,8 @@ virtual void MarkAsComplete() = 0; + // NB: "dicomAsJson" must *not* be deleted, and can be NULL if + // "!IsDicomAsJsonNeeded()" virtual void Visit(const std::string& publicId, const std::string& instanceId, const DicomMap& mainDicomTags, @@ -237,6 +239,12 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); + void ApplyInternal(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit); + public: class DicomCacheLocker : public boost::noncopyable {
--- a/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp Wed Sep 16 10:22:25 2020 +0200 +++ b/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp Wed Sep 16 13:22:30 2020 +0200 @@ -154,6 +154,9 @@ ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); + + ASSERT_TRUE(lookup.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_FALSE(lookup.HasTag(DICOM_TAG_PATIENT_NAME)); } {