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