changeset 2085:554bc96e7508

added DicomStructuredReport
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 07 Nov 2023 17:03:38 +0100
parents 0c0c228a3a73
children 40476d5e0cfd
files OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp OrthancStone/Sources/Toolbox/DicomStructuredReport.h
diffstat 3 files changed, 613 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Oct 11 21:12:08 2023 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Nov 07 17:03:38 2023 +0100
@@ -189,6 +189,7 @@
 if (ENABLE_DCMTK)
   list(APPEND ORTHANC_STONE_SOURCES
     ${ORTHANC_STONE_ROOT}/Oracle/ParseDicomSuccessMessage.cpp
+    ${ORTHANC_STONE_ROOT}/Toolbox/DicomStructuredReport.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/OrthancDatasets/SimplifiedOrthancDataset.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomCache.cpp
     ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomDataset.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp	Tue Nov 07 17:03:38 2023 +0100
@@ -0,0 +1,512 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomStructuredReport.h"
+
+#include "../Scene2D/ScenePoint2D.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcsequen.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
+static std::string GetStringValue(DcmItem& dataset,
+                                  const DcmTagKey& key)
+{
+  const char* value = NULL;
+  if (dataset.findAndGetString(key, value).good() &&
+      value != NULL)
+  {
+    return value;
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                    "Missing tag in DICOM-SR: " + key.toString());
+  }
+}
+
+
+static DcmSequenceOfItems& GetSequenceValue(DcmItem& dataset,
+                                            const DcmTagKey& key)
+{
+  DcmSequenceOfItems* sequence = NULL;
+  if (dataset.findAndGetSequence(key, sequence).good() &&
+      sequence != NULL)
+  {
+    return *sequence;
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                    "Missing sequence in DICOM-SR: " + key.toString());
+  }
+}
+
+
+static void CheckStringValue(DcmItem& dataset,
+                             const DcmTagKey& key,
+                             const std::string& expected)
+{
+  if (GetStringValue(dataset, key) != expected)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+}
+
+
+static bool IsDicomTemplate(DcmItem& dataset,
+                            const std::string& tid)
+{
+  DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_ContentTemplateSequence);
+
+  return (sequence.card() == 1 &&
+          GetStringValue(*sequence.getItem(0), DCM_MappingResource) == "DCMR" &&
+          GetStringValue(*sequence.getItem(0), DCM_TemplateIdentifier) == tid);
+}
+
+
+static bool IsValidConcept(DcmItem& dataset,
+                           const DcmTagKey& key,
+                           const std::string& scheme,
+                           const std::string& concept)
+{
+  DcmSequenceOfItems& sequence = GetSequenceValue(dataset, key);
+
+  return (sequence.card() == 1 &&
+          GetStringValue(*sequence.getItem(0), DCM_CodingSchemeDesignator) == scheme &&
+          GetStringValue(*sequence.getItem(0), DCM_CodeValue) == concept);
+}
+
+
+static bool IsDicomConcept(DcmItem& dataset,
+                           const std::string& concept)
+{
+  return IsValidConcept(dataset, DCM_ConceptNameCodeSequence, "DCM", concept);
+}
+
+
+namespace OrthancStone
+{
+  class DicomStructuredReport::Structure : public boost::noncopyable
+  {
+  private:
+    std::string   sopInstanceUid_;
+    bool          hasFrameNumber_;
+    unsigned int  frameNumber_;
+    bool          hasProbabilityOfCancer_;
+    float         probabilityOfCancer_;
+
+  public:
+    Structure(const std::string& sopInstanceUid) :
+      sopInstanceUid_(sopInstanceUid),
+      hasFrameNumber_(false),
+      hasProbabilityOfCancer_(false)
+    {
+    }
+
+    virtual ~Structure()
+    {
+    }
+
+    void SetFrameNumber(unsigned int frame)
+    {
+      if (frame <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        hasFrameNumber_ = true;
+        frameNumber_ = frame;
+      }
+    }
+
+    void SetProbabilityOfCancer(float probability)
+    {
+      if (probability < 0 ||
+          probability > 100)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        hasProbabilityOfCancer_ = true;
+        probabilityOfCancer_ = probability;
+      }
+    }
+
+    bool HasFrameNumber() const
+    {
+      return hasFrameNumber_;
+    }
+
+    bool HasProbabilityOfCancer() const
+    {
+      return hasProbabilityOfCancer_;
+    }
+
+    unsigned int GetFrameNumber() const
+    {
+      if (hasFrameNumber_)
+      {
+        return frameNumber_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetProbabilityOfCancer() const
+    {
+      if (hasProbabilityOfCancer_)
+      {
+        return probabilityOfCancer_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+  class DicomStructuredReport::Point : public Structure
+  {
+  private:
+    ScenePoint2D  point_;
+
+  public:
+    Point(const std::string& sopInstanceUid,
+          double x,
+          double y) :
+      Structure(sopInstanceUid),
+      point_(x, y)
+    {
+    }
+
+    const ScenePoint2D& GetPoint() const
+    {
+      return point_;
+    }
+  };
+
+
+  class DicomStructuredReport::Polyline : public Structure
+  {
+  private:
+    std::vector<ScenePoint2D>  points_;
+
+  public:
+    Polyline(const std::string& sopInstanceUid,
+             const float* points,
+             unsigned long pointsCount) :
+      Structure(sopInstanceUid)
+    {
+      if (pointsCount % 2 != 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      points_.reserve(pointsCount / 2);
+
+      for (unsigned long i = 0; i < pointsCount; i += 2)
+      {
+        points_.push_back(ScenePoint2D(points[i], points[i + 1]));
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return points_.size();
+    }
+
+    const ScenePoint2D& GetPoint(size_t i) const
+    {
+      if (i >= points_.size())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        return points_[i];
+      }
+    }
+  };
+
+
+  void DicomStructuredReport::AddStructure(const std::string& sopInstanceUid,
+                                           DcmItem& group,
+                                           bool hasFrameNumber,
+                                           unsigned int frameNumber,
+                                           bool hasProbabilityOfCancer,
+                                           float probabilityOfCancer)
+  {
+    const std::string graphicType = GetStringValue(group, DCM_GraphicType);
+
+    const Float32* coords = NULL;
+    unsigned long coordsCount = 0;
+    if (!group.findAndGetFloat32Array(DCM_GraphicData, coords, &coordsCount).good() ||
+        (coordsCount != 0 && coords == NULL))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "Cannot read coordinates for region in DICOM-SR");
+    }
+
+    std::unique_ptr<Structure> structure;
+
+    if (graphicType == "POINT")
+    {
+      if (coordsCount != 2)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        structure.reset(new Point(sopInstanceUid, coords[0], coords[1]));
+      }
+    }
+    else if (graphicType == "POLYLINE")
+    {
+      structure.reset(new Polyline(sopInstanceUid, coords, coordsCount));
+    }
+    else
+    {
+      return;  // Unsupported graphic type
+    }
+
+    assert(structure.get() != NULL);
+
+    if (hasFrameNumber)
+    {
+      structure->SetFrameNumber(frameNumber);
+    }
+
+    if (hasProbabilityOfCancer)
+    {
+      structure->SetProbabilityOfCancer(probabilityOfCancer);
+    }
+
+    structures_.push_back(structure.release());
+  }
+
+
+  DicomStructuredReport::DicomStructuredReport(Orthanc::ParsedDicomFile& dicom)
+  {
+    DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+
+    CheckStringValue(dataset, DCM_Modality, "SR");
+    CheckStringValue(dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.88.33");  // Comprehensive SR IOD
+    CheckStringValue(dataset, DCM_ValueType, "CONTAINER");
+
+    if (!IsDicomConcept(dataset, "126000") /* Imaging measurement report */ ||
+        !IsDicomTemplate(dataset, "1500"))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_CurrentRequestedProcedureEvidenceSequence);
+
+    std::list<std::string> tmp;
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      std::string studyInstanceUid = GetStringValue(*sequence.getItem(i), DCM_StudyInstanceUID);
+
+      DcmSequenceOfItems* referencedSeries = NULL;
+      if (!sequence.getItem(i)->findAndGetSequence(DCM_ReferencedSeriesSequence, referencedSeries).good() ||
+          referencedSeries == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      for (unsigned long j = 0; j < referencedSeries->card(); j++)
+      {
+        std::string seriesInstanceUid = GetStringValue(*referencedSeries->getItem(j), DCM_SeriesInstanceUID);
+
+        DcmSequenceOfItems* referencedInstances = NULL;
+        if (!referencedSeries->getItem(j)->findAndGetSequence(DCM_ReferencedSOPSequence, referencedInstances).good() ||
+            referencedInstances == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        for (unsigned int k = 0; k < referencedInstances->card(); k++)
+        {
+          std::string sopClassUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPClassUID);
+          std::string sopInstanceUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPInstanceUID);
+
+          if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end())
+          {
+            instancesInformation_[sopInstanceUid] = ReferencedInstance(studyInstanceUid, seriesInstanceUid, sopClassUid);
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                            "Multiple occurrences of the same instance in DICOM-SR: " + sopInstanceUid);
+          }
+
+          tmp.push_back(sopInstanceUid);
+        }
+      }
+    }
+
+    orderedInstances_.reserve(tmp.size());
+
+    for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it)
+    {
+      orderedInstances_.push_back(*it);
+    }
+
+    sequence = GetSequenceValue(dataset, DCM_ContentSequence);
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      DcmItem& item = *sequence.getItem(i);
+
+      if (GetStringValue(item, DCM_RelationshipType) == "CONTAINS" &&
+          GetStringValue(item, DCM_ValueType) == "CONTAINER" &&
+          IsDicomConcept(item, "126010" /* Imaging measurements */))
+      {
+        DcmSequenceOfItems& measurements = GetSequenceValue(item, DCM_ContentSequence);
+
+        for (unsigned long j = 0; j < measurements.card(); j++)
+        {
+          DcmItem& measurement = *measurements.getItem(j);
+
+          if (GetStringValue(measurement, DCM_RelationshipType) == "CONTAINS" &&
+              GetStringValue(measurement, DCM_ValueType) == "CONTAINER" &&
+              IsDicomConcept(measurement, "125007" /* Measurement group */) &&
+              IsDicomTemplate(measurement, "1410"))
+          {
+            DcmSequenceOfItems& groups = GetSequenceValue(measurement, DCM_ContentSequence);
+
+            bool hasProbabilityOfCancer = false;
+            float probabilityOfCancer = 0;
+
+            for (unsigned int k = 0; k < groups.card(); k++)
+            {
+              DcmItem& group = *groups.getItem(k);
+
+              if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
+                  GetStringValue(group, DCM_ValueType) == "NUM" &&
+                  IsDicomConcept(group, "111047" /* Probability of cancer */))
+              {
+                DcmSequenceOfItems& values = GetSequenceValue(group, DCM_MeasuredValueSequence);
+
+                if (values.card() == 1 &&
+                    IsValidConcept(*values.getItem(0), DCM_MeasurementUnitsCodeSequence, "UCUM", "%"))
+                {
+                  std::string value = GetStringValue(*values.getItem(0), DCM_NumericValue);
+                  if (Orthanc::SerializationToolbox::ParseFloat(probabilityOfCancer, value))
+                  {
+                    hasProbabilityOfCancer = true;
+                  }
+                  else
+                  {
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                    "Cannot parse float in DICOM-SR: " + value);
+                  }
+                }
+              }
+            }
+
+            for (unsigned int k = 0; k < groups.card(); k++)
+            {
+              DcmItem& group = *groups.getItem(k);
+
+              if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
+                  GetStringValue(group, DCM_ValueType) == "SCOORD" &&
+                  IsDicomConcept(group, "111030" /* Image region */))
+              {
+                DcmSequenceOfItems& regions = GetSequenceValue(group, DCM_ContentSequence);
+
+                for (unsigned int l = 0; l < regions.card(); l++)
+                {
+                  DcmItem& region = *regions.getItem(l);
+
+                  if (GetStringValue(region, DCM_RelationshipType) == "SELECTED FROM" &&
+                      GetStringValue(region, DCM_ValueType) == "IMAGE" &&
+                      IsDicomConcept(region, "111040") /* Original source */)
+                  {
+                    DcmSequenceOfItems& instances = GetSequenceValue(region, DCM_ReferencedSOPSequence);
+                    if (instances.card() != 1)
+                    {
+                      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                      "Region cannot reference multiple instances in DICOM-SR");
+                    }
+
+                    std::string sopInstanceUid = GetStringValue(*instances.getItem(0), DCM_ReferencedSOPInstanceUID);
+                    if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end())
+                    {
+                      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                                      "Referencing unknown instance in DICOM-SR: " + sopInstanceUid);
+                    }
+
+                    if (instances.getItem(0)->tagExists(DCM_ReferencedFrameNumber))
+                    {
+                      std::string frames = GetStringValue(*instances.getItem(0), DCM_ReferencedFrameNumber);
+                      std::vector<std::string> tokens;
+                      Orthanc::Toolbox::SplitString(tokens, frames, '\\');
+
+                      for (size_t m = 0; m < tokens.size(); m++)
+                      {
+                        uint32_t frame;
+                        if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frame, tokens[m]))
+                        {
+                          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+                        }
+                        else
+                        {
+                          AddStructure(sopInstanceUid, group, true, frame, hasProbabilityOfCancer, probabilityOfCancer);
+                        }
+                      }
+                    }
+                    else
+                    {
+                      AddStructure(sopInstanceUid, group, false, 0, hasProbabilityOfCancer, probabilityOfCancer);
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+
+  DicomStructuredReport::~DicomStructuredReport()
+  {
+    for (std::list<Structure*>::iterator it = structures_.begin(); it != structures_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.h	Tue Nov 07 17:03:38 2023 +0100
@@ -0,0 +1,100 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error Support for DCMTK must be enabled
+#endif
+
+#include <DicomParsing/ParsedDicomFile.h>
+
+#include <dcmtk/dcmdata/dcitem.h>
+
+namespace OrthancStone
+{
+  class DicomStructuredReport : public boost::noncopyable
+  {
+  private:
+    class Structure;
+    class Point;
+    class Polyline;
+
+    class ReferencedInstance
+    {
+    private:
+      std::string  studyInstanceUid_;
+      std::string  seriesInstanceUid_;
+      std::string  sopClassUid_;
+
+    public:
+      ReferencedInstance(const std::string& studyInstanceUid,
+                         const std::string& seriesInstanceUid,
+                         const std::string& sopClassUid) :
+        studyInstanceUid_(studyInstanceUid),
+        seriesInstanceUid_(seriesInstanceUid),
+        sopClassUid_(sopClassUid)
+      {
+      }
+
+      ReferencedInstance()
+      {
+      }
+
+      const std::string& GetStudyInstanceUid() const
+      {
+        return studyInstanceUid_;
+      }
+
+      const std::string& GetSeriesInstanceUid() const
+      {
+        return seriesInstanceUid_;
+      }
+
+      const std::string& GetSopClassUid() const
+      {
+        return sopClassUid_;
+      }
+    };
+
+    void AddStructure(const std::string& sopInstanceUid,
+                      DcmItem& group,
+                      bool hasFrameNumber,
+                      unsigned int frameNumber,
+                      bool hasProbabilityOfCancer,
+                      float probabilityOfCancer);
+
+    std::map<std::string, ReferencedInstance>  instancesInformation_;
+    std::vector<std::string>                   orderedInstances_;
+    std::list<Structure*>                      structures_;
+
+  public:
+    DicomStructuredReport(Orthanc::ParsedDicomFile& dicom);
+
+    ~DicomStructuredReport();
+  };
+}