changeset 6056:7902d830c9a5 pixel-anon

pixel masking: handling of multiframe images
author Alain Mazy <am@orthanc.team>
date Mon, 24 Mar 2025 16:34:15 +0100
parents 0a60d2d482d4
children 035a13b295a7
files OrthancFramework/Sources/DicomFormat/DicomTag.h OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp TODO
diffstat 3 files changed, 107 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Mar 18 10:08:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Mar 24 16:34:15 2025 +0100
@@ -102,6 +102,8 @@
   static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
   static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
 
+  static const DicomTag DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE(0x0054, 0x0022);
+
   // The following is used for "modify/anonymize" operations
   static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
   static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
@@ -194,6 +196,7 @@
   static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052);
   static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053);
   static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050);
+  static const DicomTag DICOM_TAG_SPACING_BETWEEN_SLICES(0x0018, 0x0088);
   static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050);
   static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051);
   static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e);
--- a/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp	Tue Mar 18 10:08:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp	Mon Mar 24 16:34:15 2025 +0100
@@ -116,18 +116,12 @@
     return false;
   }
 
-  static void GetDoubleVector(std::vector<double>& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize)
+  static void GetDoubleVector(std::vector<double>& target, const std::string& strValue, const DicomTag& tag, size_t expectedSize)
   {
     target.clear();
 
-    std::string str;
-    if (!file.GetTagValue(str, tag))
-    {
-      throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format());
-    }
-
     std::vector<std::string> strVector;
-    Toolbox::SplitString(strVector, str, '\\');
+    Toolbox::SplitString(strVector, strValue, '\\');
 
     if (strVector.size() != expectedSize)
     {
@@ -147,32 +141,88 @@
     }
   }
 
+  static void GetDoubleVector(std::vector<double>& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize)
+  {
+    std::string str;
+    if (!file.GetTagValue(str, tag))
+    {
+      throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format());
+    }
+
+    GetDoubleVector(target, str, tag, expectedSize);
+  }
+
   bool DicomPixelMasker::Region3D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const
   {
     if (IsTargeted(file))
     {
+      DicomMap tags;
+      file.ExtractDicomSummary(tags, 256);
+
       std::vector<double> imagePositionPatient;
       std::vector<double> imageOrientationPatient;
       std::vector<double> pixelSpacing;
+      double sliceSpacing = 0.0;
 
-      GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
-      GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+      if (file.HasTag(DICOM_TAG_IMAGE_POSITION_PATIENT) && file.HasTag(DICOM_TAG_IMAGE_ORIENTATION_PATIENT))
+      {
+        GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+        GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+      }
+      else if (file.HasTag(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE)) // find it in the detector info sequence (for some multi frame instances like NM or scintigraphy) TODO-PIXEL-ANON: to validate
+      {
+        const Json::Value& jsonSequence = tags.GetValue(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE).GetSequenceContent();
+        if (jsonSequence.size() == 1)
+        {
+          std::string strImagePositionPatient = jsonSequence[0]["0020,0032"]["Value"].asString();
+          std::string strImageOrientationPatient = jsonSequence[0]["0020,0037"]["Value"].asString();
+
+          GetDoubleVector(imagePositionPatient, strImagePositionPatient, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+          GetDoubleVector(imageOrientationPatient, strImageOrientationPatient, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient in DetectorInformationSequence, invalid sequence size");
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient or ImageOrientationPatient");
+      }
+
       GetDoubleVector(pixelSpacing, file, DICOM_TAG_PIXEL_SPACING, 2);
 
+      if (file.HasTag(DICOM_TAG_SPACING_BETWEEN_SLICES))
+      {
+        std::string strSliceSpacing;
+        if (file.GetTagValue(strSliceSpacing, DICOM_TAG_SPACING_BETWEEN_SLICES))
+        {
+          sliceSpacing = boost::lexical_cast<double>(strSliceSpacing);
+        }
+      }
+
+      double z = imagePositionPatient[2];
+      
+      if (sliceSpacing != 0.0)
+      {
+        z = z - frameIndex * sliceSpacing;
+      }
+
       // note: To simplify, for the z, we only check that imagePositionPatient is between the authorized z values.
       //       This won't be perfectly true for weird images with slices that are not parallel but let's wait for someone to complain ...
-      if (imagePositionPatient[2] < std::min(z1_, z2_) || 
-          imagePositionPatient[2] > std::max(z1_, z2_))
+      if (z < std::min(z1_, z2_) || 
+          z > std::max(z1_, z2_))
       {
         return false;
       }
       
+
       double deltaX1 = x1_ - imagePositionPatient[0];
       double deltaY1 = y1_ - imagePositionPatient[1];
-      double deltaZ1 = z1_ - imagePositionPatient[2];
+      double deltaZ1 = z1_ - z;
       double deltaX2 = x2_ - imagePositionPatient[0];
       double deltaY2 = y2_ - imagePositionPatient[1];
-      double deltaZ2 = z2_ - imagePositionPatient[2];
+      double deltaZ2 = z2_ - z;
 
       double ix1 = (deltaX1 * imageOrientationPatient[0] + deltaY1 * imageOrientationPatient[1] + deltaZ1 * imageOrientationPatient[2]) / pixelSpacing[0];
       double iy1 = (deltaX1 * imageOrientationPatient[3] + deltaY1 * imageOrientationPatient[4] + deltaZ1 * imageOrientationPatient[5]) / pixelSpacing[1];
--- a/TODO	Tue Mar 18 10:08:43 2025 +0100
+++ b/TODO	Mon Mar 24 16:34:15 2025 +0100
@@ -570,4 +570,43 @@
       }]
     }
 }
-EOF
\ No newline at end of file
+EOF
+
+
+# modify some frames of a multiframe scintigraphy study
+curl -X POST http://localhost:8043/studies/ab67d5f8-95865506-8fb83c8b-93610651-ddce6e77/modify \
+--data-binary @- << EOF
+{
+    "Replace" : {"StudyInstanceUID": "1.2.6", "StudyDescription": "modified-scinti"},
+    "Force": true,
+    "MaskPixelData": {
+        "Regions": [{
+            "MaskType": "Fill",
+            "FillValue": 3000,
+            "RegionType" : "3D",
+            "Origin": [-150.0, -200, 200],
+            "End": [150.0, -150, 100]
+      }]
+    }
+}
+EOF
+
+
+# modify some frames of a multiframe PET-CT study
+curl -X POST http://localhost:8043/studies/890e1167-55ad171a-7721ffec-db91e2c1-700778c0/modify \
+--data-binary @- << EOF
+{
+    "Replace" : {"StudyInstanceUID": "1.2.7", "StudyDescription": "modified-pet-ct"},
+    "Force": true,
+    "MaskPixelData": {
+        "Regions": [{
+            "MaskType": "Fill",
+            "FillValue": 3000,
+            "RegionType" : "3D",
+            "Origin": [-150.0, -100, -85],
+            "End": [150.0, 100, -250]
+      }]
+    }
+}
+EOF
+