Mercurial > hg > orthanc-dicomweb
changeset 392:c467391b3585
"Extrapolate" mode for WADO-RS Retrieve Metadata
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 21 Feb 2020 17:14:21 +0100 |
parents | 27001924c456 |
children | 5af041432a60 |
files | NEWS Plugin/Configuration.cpp Plugin/Configuration.h Plugin/WadoRs.cpp Resources/Graveyard/MinorityReport.cpp |
diffstat | 5 files changed, 306 insertions(+), 88 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Feb 17 16:33:32 2020 +0100 +++ b/NEWS Fri Feb 21 17:14:21 2020 +0100 @@ -7,9 +7,11 @@ * Support of "window", "viewport" and "quality" parameters in "Retrieve Rendered Transaction" * Support of "/studies/.../series/.../rendered" * QIDO-RS: Allow to query against a list of multiple values separated by commas -* WADO-RS "Retrieve Metadata": Configuration options "StudiesMetadata" and "SeriesMetadata", - whose value can be "Full" (read all DICOM instances from the storage area) or - "MainDicomTags" (only report the main DICOM tags from the Orthanc database) +* WADO-RS "Retrieve Metadata": Configuration options "StudiesMetadata" + and "SeriesMetadata", whose value can be "Full" (read all DICOM + instances from the storage area), "MainDicomTags" (only report the + main DICOM tags from the Orthanc database), or "Extrapolate" (main + DICOM tags + user-specified tags extrapolated from a few random instances) Maintenance -----------
--- a/Plugin/Configuration.cpp Mon Feb 17 16:33:32 2020 +0100 +++ b/Plugin/Configuration.cpp Fri Feb 21 17:14:21 2020 +0100 @@ -311,6 +311,10 @@ // Check configuration during initialization GetMetadataMode(Orthanc::ResourceType_Study); GetMetadataMode(Orthanc::ResourceType_Series); + + std::set<Orthanc::DicomTag> tags; + GetExtrapolatedMetadataTags(tags, Orthanc::ResourceType_Study); + GetExtrapolatedMetadataTags(tags, Orthanc::ResourceType_Series); } @@ -615,6 +619,7 @@ { static const std::string FULL = "Full"; static const std::string MAIN_DICOM_TAGS = "MainDicomTags"; + static const std::string EXTRAPOLATE = "Extrapolate"; std::string key; switch (level) @@ -641,12 +646,61 @@ { return MetadataMode_MainDicomTags; } + else if (value == EXTRAPOLATE) + { + return MetadataMode_Extrapolate; + } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Bad value for option \"" + key + "\": Should be either \"" + FULL + "\" or \"" + - MAIN_DICOM_TAGS + "\""); + MAIN_DICOM_TAGS + "\" or \"" + EXTRAPOLATE + "\""); + } + } + + + void GetSetOfTags(std::set<Orthanc::DicomTag>& tags, + const std::string& key) + { + tags.clear(); + + std::list<std::string> s; + + if (configuration_->LookupListOfStrings(s, key, false)) + { + for (std::list<std::string>::const_iterator it = s.begin(); it != s.end(); ++it) + { + OrthancPluginDictionaryEntry entry; + if (OrthancPluginLookupDictionary(GetGlobalContext(), &entry, it->c_str()) == OrthancPluginErrorCode_Success) + { + tags.insert(Orthanc::DicomTag(entry.group, entry.element)); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Unknown DICOM tag in option \"" + key + "\" of DICOMweb: " + *it); + } + } + } + } + + + void GetExtrapolatedMetadataTags(std::set<Orthanc::DicomTag>& tags, + Orthanc::ResourceType level) + { + switch (level) + { + case Orthanc::ResourceType_Study: + GetSetOfTags(tags, "StudiesMetadataExtrapolatedTags"); + break; + + case Orthanc::ResourceType_Series: + GetSetOfTags(tags, "SeriesMetadataExtrapolatedTags"); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } }
--- a/Plugin/Configuration.h Mon Feb 17 16:33:32 2020 +0100 +++ b/Plugin/Configuration.h Fri Feb 21 17:14:21 2020 +0100 @@ -49,7 +49,7 @@ { MetadataMode_Full, // Read all the DICOM instances from the storage area MetadataMode_MainDicomTags, // Only use the Orthanc database (main DICOM tags only) - MetadataMode_Interpolate // Unused so far + MetadataMode_Extrapolate // Extrapolate user-specified tags from a few DICOM instances }; @@ -125,5 +125,11 @@ bool IsXmlExpected(const OrthancPluginHttpRequest* request); MetadataMode GetMetadataMode(Orthanc::ResourceType level); + + void GetSetOfTags(std::set<Orthanc::DicomTag>& tags, + const std::string& key); + + void GetExtrapolatedMetadataTags(std::set<Orthanc::DicomTag>& tags, + Orthanc::ResourceType level); } }
--- a/Plugin/WadoRs.cpp Mon Feb 17 16:33:32 2020 +0100 +++ b/Plugin/WadoRs.cpp Fri Feb 21 17:14:21 2020 +0100 @@ -262,19 +262,104 @@ -static void CopyTagIfMissing(Orthanc::DicomMap& target, - const Orthanc::DicomMap& source, - const Orthanc::DicomTag& tag) -{ - if (!target.HasTag(tag)) - { - target.CopyTagIfExists(source, tag); - } -} - - namespace { + class SetOfDicomInstances : public boost::noncopyable + { + private: + std::vector<Orthanc::DicomMap*> instances_; + + public: + ~SetOfDicomInstances() + { + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + delete instances_[i]; + } + } + + size_t GetSize() const + { + return instances_.size(); + } + + bool ReadInstance(const std::string& publicId) + { + Json::Value dicomAsJson; + + if (OrthancPlugins::RestApiGet(dicomAsJson, "/instances/" + publicId + "/tags", false)) + { + std::auto_ptr<Orthanc::DicomMap> instance(new Orthanc::DicomMap); + instance->FromDicomAsJson(dicomAsJson); + instances_.push_back(instance.release()); + + return true; + } + else + { + return false; + } + } + + + void MinorityReport(Orthanc::DicomMap& target, + const Orthanc::DicomTag& tag) const + { + typedef std::map<std::string, unsigned int> Counters; + + Counters counters; + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + + std::string value; + if (instances_[i]->LookupStringValue(value, tag, false)) + { + Counters::iterator found = counters.find(value); + if (found == counters.end()) + { + counters[value] = 1; + } + else + { + found->second ++; + } + } + } + + if (!counters.empty()) + { + Counters::const_iterator current = counters.begin(); + + std::string maxValue = current->first; + size_t maxCount = current->second; + + current++; + + while (current != counters.end()) + { + if (maxCount < current->second) + { + maxValue = current->first; + maxCount = current->second; + } + + current++; + } + + // Take the ceiling of the number of available instances + const size_t threshold = instances_.size() / 2 + 1; + if (maxCount >= threshold) + { + target.SetValue(tag, maxValue, false); + } + } + } + }; + + class MainDicomTagsCache : public boost::noncopyable { private: @@ -289,7 +374,6 @@ Content content_; - static bool ReadResource(Orthanc::DicomMap& dicom, std::string& parent, OrthancPlugins::MetadataMode mode, @@ -362,84 +446,105 @@ } } - - if (mode == OrthancPlugins::MetadataMode_Interpolate && - level == Orthanc::ResourceType_Series) + + if (mode == OrthancPlugins::MetadataMode_Extrapolate && + (level == Orthanc::ResourceType_Series || + level == Orthanc::ResourceType_Study)) { - /** - * Complete the series-level tags, with instance-level tags - * that are not considered as "main DICOM tags" in Orthanc, - * but that are necessary for Web viewers, and that should be - * constant throughout all the instances of the series. To - * this end, we read 1 DICOM instance of this series from - * disk, and extract the subset of tags of - * interest. Obviously, this is an approximation to improve - * performance. - * - * TODO - Decide which tags are safe (i.e. what is supposed to - * be constant?) - **/ - if (!value.isMember(INSTANCES) || - value[INSTANCES].type() != Json::arrayValue || - value[INSTANCES].size() == 0 || - value[INSTANCES][0].type() != Json::stringValue) + std::set<Orthanc::DicomTag> tags; + OrthancPlugins::Configuration::GetExtrapolatedMetadataTags(tags, level); + + if (!tags.empty()) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - else - { - const std::string& someInstance = value[INSTANCES][0].asString(); + /** + * Complete the series/study-level tags, with instance-level + * tags that are not considered as "main DICOM tags" in + * Orthanc, but that are necessary for Web viewers, and that + * are expected to be constant throughout all the instances of + * the study/series. To this end, we read up to "N" DICOM + * instances of this study/series from disk, and for the tags + * of interest, we look at whether there is a consensus in the + * value among these instances. Obviously, this is an + * approximation to improve performance. + **/ + + std::set<std::string> allInstances; - Json::Value dicomAsJson; - if (OrthancPlugins::RestApiGet(dicomAsJson, "/instances/" + someInstance + "/tags", false)) + switch (level) { - Orthanc::DicomMap instance; - instance.FromDicomAsJson(dicomAsJson); + case Orthanc::ResourceType_Series: + if (!value.isMember(INSTANCES) || + value[INSTANCES].type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + for (Json::Value::ArrayIndex i = 0; i < value[INSTANCES].size(); i++) + { + if (value[INSTANCES][i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + allInstances.insert(value[INSTANCES][i].asString()); + } + } + } + + break; - // Those tags are necessary for "DicomImageInformation" in - // the Orthanc core (for Stone) - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_BITS_ALLOCATED); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_BITS_STORED); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_COLUMNS); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_HIGH_BIT); - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES); // => Already in main DICOM tags - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_PIXEL_REPRESENTATION); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_PLANAR_CONFIGURATION); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_ROWS); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SAMPLES_PER_PIXEL); + case Orthanc::ResourceType_Study: + { + Json::Value tmp; + if (OrthancPlugins::RestApiGet(tmp, "/studies/" + orthancId + "/instances", false)) + { + if (tmp.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } - // Those tags are necessary for "DicomInstanceParameters" in Stone - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SOP_CLASS_UID); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_WINDOW_CENTER); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_WINDOW_WIDTH); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR); // TODO => probably unsafe! - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER); // TODO => probably unsafe! - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SLICE_THICKNESS); - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT); // => Already in main DICOM tags - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_RESCALE_SLOPE); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_DOSE_GRID_SCALING); // TODO => probably unsafe! + for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++) + { + if (tmp[i].type() != Json::objectValue || + !tmp[i].isMember("ID") || + tmp[i]["ID"].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + allInstances.insert(tmp[i]["ID"].asString()); + } + } + } + + break; + } - // SeriesMetadataLoader - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SOP_INSTANCE_UID); // => Already in main DICOM tags - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID); // => Already in main DICOM tags - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID); // => Already in main DICOM tags - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE); // => Meaningless at series level + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + // Select up to N random instances. The instances are + // implicitly selected randomly, as the public ID of an + // instance is a SHA-1 hash (whose domain is uniformly distributed) - // SeriesOrderedFrames - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_INSTANCE_NUMBER); // => Already in main DICOM tags - //CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_IMAGE_INDEX); // => Already in main DICOM tags + static const size_t N = 3; + SetOfDicomInstances selectedInstances; - // SeriesFramesLoader - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_REFERENCED_FILE_ID); - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_PATIENT_ID); + for (std::set<std::string>::const_iterator it = allInstances.begin(); + selectedInstances.GetSize() < N && it != allInstances.end(); ++it) + { + selectedInstances.ReadInstance(*it); + } - // GeometryToolbox - CopyTagIfMissing(dicom, instance, Orthanc::DICOM_TAG_PIXEL_SPACING); + for (std::set<Orthanc::DicomTag>::const_iterator + it = tags.begin(); it != tags.end(); ++it) + { + selectedInstances.MinorityReport(dicom, *it); } } } @@ -525,7 +630,7 @@ switch (mode) { case OrthancPlugins::MetadataMode_MainDicomTags: - case OrthancPlugins::MetadataMode_Interpolate: + case OrthancPlugins::MetadataMode_Extrapolate: { Orthanc::DicomMap dicom; if (cache.GetInstance(dicom, mode, orthancId)) @@ -556,8 +661,8 @@ #if 0 - /** - **/ + /** + **/ // TODO - Have a global setting to enable/disable caching of DICOMweb
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/MinorityReport.cpp Fri Feb 21 17:14:21 2020 +0100 @@ -0,0 +1,51 @@ +#if 0 + /** + * TODO - Decide which tags are safe (i.e. what is supposed to + * be constant?) + **/ + + // Those tags are necessary for "DicomImageInformation" in + // the Orthanc core (for Stone) + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_BITS_ALLOCATED); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_BITS_STORED); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_COLUMNS); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_HIGH_BIT); + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES); // => Already in main DICOM tags + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_PIXEL_REPRESENTATION); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_PLANAR_CONFIGURATION); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_ROWS); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SAMPLES_PER_PIXEL); + + // Those tags are necessary for "DicomInstanceParameters" in Stone + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SOP_CLASS_UID); + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_WINDOW_CENTER); // varies over each instance + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH); // varies over each instance + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR); // TODO => probably unsafe! + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER); // TODO => probably unsafe! + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SLICE_THICKNESS); + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT); // => Already in main DICOM tags + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_RESCALE_SLOPE); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_DOSE_GRID_SCALING); // TODO => probably unsafe! + + // SeriesMetadataLoader + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SOP_INSTANCE_UID); // => Already in main DICOM tags + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID); // => Already in main DICOM tags + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID); // => Already in main DICOM tags + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE); // => Meaningless at series level + + // SeriesOrderedFrames + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_INSTANCE_NUMBER); // => Already in main DICOM tags + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_IMAGE_INDEX); // => Already in main DICOM tags + + // SeriesFramesLoader + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE); => throws "Exception while invoking plugin service 23: Internal error" in Orthanc + //instances.MinorityReport(dicom, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE); // varies over each instance + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_REFERENCED_FILE_ID); + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_PATIENT_ID); + + // GeometryToolbox + instances.MinorityReport(dicom, Orthanc::DICOM_TAG_PIXEL_SPACING); +#endif