changeset 6040:100a0371fc85 pixel-anon

pixel anon: 3D regions
author Alain Mazy <am@orthanc.team>
date Thu, 13 Mar 2025 10:09:50 +0100
parents 4742b4fe824b
children 0a60d2d482d4
files OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/Sources/DicomParsing/DicomModification.h OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h TODO
diffstat 5 files changed, 352 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Wed Mar 12 09:59:38 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Thu Mar 13 10:09:50 2025 +0100
@@ -31,6 +31,7 @@
 #include "../SerializationToolbox.h"
 #include "FromDcmtkBridge.h"
 #include "ITagVisitor.h"
+#include "DicomPixelMasker.h"
 
 #include <memory>   // For std::unique_ptr
 
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Wed Mar 12 09:59:38 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Thu Mar 13 10:09:50 2025 +0100
@@ -25,13 +25,14 @@
 #pragma once
 
 #include "ParsedDicomFile.h"
-#include "DicomPixelMasker.h"
 
 #include <list>
 
 
 namespace Orthanc
 {
+  class DicomPixelMasker;
+
   class ORTHANC_PUBLIC DicomModification : public boost::noncopyable
   {
     /**
@@ -156,7 +157,7 @@
     SequenceReplacements sequenceReplacements_;  // Must *never* be a path whose prefix is empty
 
     // New in Orthanc 1.X.X
-    std::unique_ptr<DicomPixelMasker>     pixelMasker_;    // TODO: check ownership & serialization
+    std::unique_ptr<DicomPixelMasker>     pixelMasker_;    // TODO-PIXEL-ANON: check ownership & serialization
 
     std::string MapDicomIdentifier(const std::string& original,
                                    ResourceType level);
--- a/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp	Wed Mar 12 09:59:38 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp	Thu Mar 13 10:09:50 2025 +0100
@@ -39,8 +39,8 @@
   static const char* KEY_FILL_VALUE = "FillValue";
   static const char* KEY_REGIONS = "Regions";
   static const char* KEY_REGION_TYPE = "RegionType";
-  static const char* KEY_REGION_PIXELS = "Pixels";
-  static const char* KEY_REGION_BOUNDING_BOX = "BoundingBox";
+  static const char* KEY_REGION_2D = "2D";
+  static const char* KEY_REGION_3D = "3D";
   static const char* KEY_ORIGIN = "Origin";
   static const char* KEY_END = "End";
   static const char* KEY_TARGET_SERIES = "TargetSeries";
@@ -50,38 +50,181 @@
   {
   }
 
-  void DicomPixelMasker::Apply(ParsedDicomFile& toModify)
+  DicomPixelMasker::~DicomPixelMasker()
+  {
+    for (std::list<BaseRegion*>::iterator it = regions_.begin(); it != regions_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+  DicomPixelMasker::BaseRegion::BaseRegion() :
+    mode_(DicomPixelMaskerMode_Undefined),
+    fillValue_(0),
+    filterWidth_(0)
   {
-    DicomInstanceHasher hasher = toModify.GetHasher();
+  }
+
+  DicomPixelMasker::Region2D::Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2) :
+    x1_(x1),
+    y1_(y1),
+    x2_(x2),
+    y2_(y2)
+  {
+  }
+
+  DicomPixelMasker::Region3D::Region3D(double x1, double y1, double z1, double x2, double y2, double z2) :
+    x1_(x1),
+    y1_(y1),
+    z1_(z1),
+    x2_(x2),
+    y2_(y2),
+    z2_(z2)
+  {
+  }
+
+  bool DicomPixelMasker::BaseRegion::IsTargeted(const ParsedDicomFile& file) const
+  {
+    DicomInstanceHasher hasher = file.GetHasher();
     const std::string& seriesId = hasher.HashSeries();
     const std::string& instanceId = hasher.HashInstance();
 
-    for (std::list<Region>::const_iterator r = regions_.begin(); r != regions_.end(); ++r)
+    if (targetSeries_.size() > 0 && targetSeries_.find(seriesId) == targetSeries_.end())
+    {
+      return false;
+    }
+
+    if (targetInstances_.size() > 0 && targetInstances_.find(instanceId) == targetInstances_.end())
+    {
+      return false;
+    }
+
+    return true;
+  }
+
+  bool DicomPixelMasker::Region2D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const
+  {
+    if (IsTargeted(file))
+    {
+      x1 = x1_;
+      y1 = y1_;
+      x2 = x2_;
+      y2 = y2_;
+      return true;
+    }
+
+    return false;
+  }
+
+  static void GetDoubleVector(std::vector<double>& target, const ParsedDicomFile& file, 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, '\\');
+
+    if (strVector.size() != expectedSize)
+    {
+      throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " length is invalid");
+    }
+
+    for (size_t i = 0; i < strVector.size(); ++i)
     {
-      if (r->targetSeries_.size() > 0 && r->targetSeries_.find(seriesId) == r->targetSeries_.end())
+      try
+      {
+        target.push_back(boost::lexical_cast<double>(strVector[i]));
+      }
+      catch (boost::bad_lexical_cast&)
       {
-        continue;
+        throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " contains invalid value " + strVector[i]);
+      }
+    }
+  }
+
+  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))
+    {
+      std::vector<double> imagePositionPatient;
+      std::vector<double> imageOrientationPatient;
+      std::vector<double> pixelSpacing;
+
+      GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+      GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+      GetDoubleVector(pixelSpacing, file, DICOM_TAG_PIXEL_SPACING, 2);
+
+      // 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_))
+      {
+        return false;
+      }
+      
+      double deltaX1 = x1_ - imagePositionPatient[0];
+      double deltaY1 = y1_ - imagePositionPatient[1];
+      double deltaZ1 = z1_ - imagePositionPatient[2];
+      double deltaX2 = x2_ - imagePositionPatient[0];
+      double deltaY2 = y2_ - imagePositionPatient[1];
+      double deltaZ2 = z2_ - imagePositionPatient[2];
+
+      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];
+      double ix2 = (deltaX2 * imageOrientationPatient[0] + deltaY2 * imageOrientationPatient[1] + deltaZ2 * imageOrientationPatient[2]) / pixelSpacing[0];
+      double iy2 = (deltaX2 * imageOrientationPatient[3] + deltaY2 * imageOrientationPatient[4] + deltaZ2 * imageOrientationPatient[5]) / pixelSpacing[1];
+
+      std::string strRows;
+      std::string strColumns;
+
+      if (!file.GetTagValue(strRows, DICOM_TAG_ROWS) || !file.GetTagValue(strColumns, DICOM_TAG_COLUMNS))
+      {
+        throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing ROWS or COLUMNS tag");
       }
 
-      if (r->targetInstances_.size() > 0 && r->targetInstances_.find(instanceId) == r->targetInstances_.end())
-      {
-        continue;
-      }
+      // clip on image size
+      double rows = boost::lexical_cast<double>(strRows);
+      double columns = boost::lexical_cast<double>(strColumns);
+
+      x1 = static_cast<unsigned int>(std::max(0.0, std::min(ix1, ix2)));
+      y1 = static_cast<unsigned int>(std::max(0.0, std::min(iy1, iy2)));
+      x2 = static_cast<unsigned int>(std::min(columns, std::max(ix1, ix2)));
+      y2 = static_cast<unsigned int>(std::min(rows, std::max(iy1, iy2)));
+
+      return true;
+    }
+    
+    return false;
+  }
+
+  void DicomPixelMasker::Apply(ParsedDicomFile& toModify)
+  {
+    for (std::list<BaseRegion*>::const_iterator itr = regions_.begin(); itr != regions_.end(); ++itr)
+    {
+      const BaseRegion* r = *itr;
 
       for (unsigned int i = 0; i < toModify.GetFramesCount(); ++i)
       {
-        // TODO-PIXEL-ANON: skip unwanted frames
+        unsigned int x1, y1, x2, y2;
 
-        ImageAccessor imageRegion;
-        toModify.GetRawFrame(i)->GetRegion(imageRegion, r->x_, r->y_, r->width_, r->height_);  
+        if (r->GetPixelMaskArea(x1, y1, x2, y2, toModify, i))
+        {
+          ImageAccessor imageRegion;
+          toModify.GetRawFrame(i)->GetRegion(imageRegion, x1, y1, x2 - x1, y2 - y1);
 
-        if (r->mode_ == DicomPixelMaskerMode_MeanFilter)
-        {
-          ImageProcessing::MeanFilter(imageRegion, r->filterWidth_, r->filterWidth_);
-        }
-        else if (r->mode_ == DicomPixelMaskerMode_Fill)
-        {
-          ImageProcessing::Set(imageRegion, r->fillValue_);
+          if (r->GetMode() == DicomPixelMaskerMode_MeanFilter)
+          {
+            ImageProcessing::MeanFilter(imageRegion, r->GetFilterWidth(), r->GetFilterWidth());
+          }
+          else if (r->GetMode() == DicomPixelMaskerMode_Fill)
+          {
+            ImageProcessing::Set(imageRegion, r->GetFillValue());
+          }
         }
       }
     }
@@ -100,26 +243,68 @@
         for (Json::ArrayIndex i = 0; i < regionsJson.size(); ++i)
         {
           const Json::Value& regionJson = regionsJson[i];
-          Region region;
+
+          std::unique_ptr<BaseRegion> region;
+          
+          if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].isString())
+          {
+            if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_2D)
+            {
+              if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 2 &&
+                  regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 2)
+              {
+                unsigned int x = regionJson[KEY_ORIGIN][0].asUInt();
+                unsigned int y = regionJson[KEY_ORIGIN][1].asUInt();
+                unsigned int width = regionJson[KEY_END][0].asUInt() - x;
+                unsigned int height = regionJson[KEY_END][1].asUInt() - y;
+                
+                region.reset(new Region2D(x, y, width, height));
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates");
+              }
+
+            }
+            else if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_3D)
+            {
+              if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 3 &&
+                  regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 3)
+              {
+                double x1 = regionJson[KEY_ORIGIN][0].asDouble();
+                double y1 = regionJson[KEY_ORIGIN][1].asDouble();
+                double z1 = regionJson[KEY_ORIGIN][2].asDouble();
+                double x2 = regionJson[KEY_END][0].asDouble();
+                double y2 = regionJson[KEY_END][1].asDouble();
+                double z2 = regionJson[KEY_END][2].asDouble();
+                
+                region.reset(new Region3D(x1, y1, z1, x2, y2, z2));
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates");
+              }
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadFileFormat, std::string(KEY_REGION_TYPE) + " unrecognized value '" + regionJson[KEY_REGION_TYPE].asString() +"'");
+            }
+          }
 
           if (regionJson.isMember(KEY_MASK_TYPE) && regionJson[KEY_MASK_TYPE].isString())
           {
             if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_FILL)
             {
-              region.mode_ = DicomPixelMaskerMode_Fill;
-
               if (regionJson.isMember(KEY_FILL_VALUE) && regionJson[KEY_FILL_VALUE].isInt())
               {
-                region.fillValue_ = regionJson[KEY_FILL_VALUE].asInt();
+                region->SetFillValue(regionJson[KEY_FILL_VALUE].asInt());
               }
             }
             else if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_MEAN_FILTER)
             {
-              region.mode_ = DicomPixelMaskerMode_MeanFilter;
-
               if (regionJson.isMember(KEY_FILTER_WIDTH) && regionJson[KEY_FILTER_WIDTH].isUInt())
               {
-                region.filterWidth_ = regionJson[KEY_FILTER_WIDTH].asUInt();
+                region->SetMeanFilter(regionJson[KEY_FILTER_WIDTH].asUInt());
               }
             }
             else
@@ -130,37 +315,21 @@
 
           if (regionJson.isMember(KEY_TARGET_SERIES) && regionJson[KEY_TARGET_SERIES].isArray())
           {
-            SerializationToolbox::ReadSetOfStrings(region.targetSeries_, regionJson, KEY_TARGET_SERIES);
+            std::set<std::string> targetSeries;
+            SerializationToolbox::ReadSetOfStrings(targetSeries, regionJson, KEY_TARGET_SERIES);
+            region->SetTargetSeries(targetSeries);
           }
 
           if (regionJson.isMember(KEY_TARGET_INSTANCES) && regionJson[KEY_TARGET_INSTANCES].isArray())
           {
-            SerializationToolbox::ReadSetOfStrings(region.targetInstances_, regionJson, KEY_TARGET_INSTANCES);
+            std::set<std::string> targetInstances;
+            SerializationToolbox::ReadSetOfStrings(targetInstances, regionJson, KEY_TARGET_INSTANCES);
+            region->SetTargetInstances(targetInstances);
           }
 
-          if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_PIXELS)
-          {
-            if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 2 &&
-                regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 2)
-            {
-              region.x_ = regionJson[KEY_ORIGIN][0].asUInt();
-              region.y_ = regionJson[KEY_ORIGIN][1].asUInt();
-              region.width_ = regionJson[KEY_END][0].asUInt() - region.x_;
-              region.height_ = regionJson[KEY_END][1].asUInt() - region.y_;
-              
-              regions_.push_back(region);
-            }
-          } 
-          else if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_BOUNDING_BOX)
-          {
-            // TODO
-            throw OrthancException(ErrorCode_NotImplemented);
-          }
+          regions_.push_back(region.release());
         }
-
       }
-
-      // TODO: support multiple series + move this 
     }
 
   }
--- a/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h	Wed Mar 12 09:59:38 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h	Thu Mar 13 10:09:50 2025 +0100
@@ -35,40 +35,108 @@
   enum DicomPixelMaskerMode
   {
     DicomPixelMaskerMode_Fill,
-    DicomPixelMaskerMode_MeanFilter
+    DicomPixelMaskerMode_MeanFilter,
+
+    DicomPixelMaskerMode_Undefined
   };
 
   class ORTHANC_PUBLIC DicomPixelMasker : public boost::noncopyable
   {
-    struct Region
+    class BaseRegion
     {
-      unsigned int            x_;
-      unsigned int            y_;
-      unsigned int            width_;
-      unsigned int            height_;
       DicomPixelMaskerMode    mode_;
-      int32_t                 fillValue_;  // pixel value
-      uint32_t                filterWidth_;  // filter width
-      std::set<std::string>  targetSeries_;
-      std::set<std::string>  targetInstances_;
+      int32_t                 fillValue_;     // pixel value
+      uint32_t                filterWidth_;   // filter width
+      std::set<std::string>   targetSeries_;
+      std::set<std::string>   targetInstances_;
+
+    protected:
+      bool IsTargeted(const ParsedDicomFile& file) const;
+      BaseRegion();
+
+    public:
+      
+      virtual ~BaseRegion()
+      {
+      }
+
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const = 0;
+
+      DicomPixelMaskerMode GetMode() const
+      {
+        return mode_;
+      }
 
-      Region() :
-        x_(0),
-        y_(0),
-        width_(0),
-        height_(0),
-        mode_(DicomPixelMaskerMode_Fill),
-        fillValue_(0),
-        filterWidth_(0)
+      int32_t GetFillValue() const
+      {
+        assert(mode_ == DicomPixelMaskerMode_Fill);
+        return fillValue_;
+      }
+
+      int32_t GetFilterWidth() const
+      {
+        assert(mode_ == DicomPixelMaskerMode_MeanFilter);
+        return filterWidth_;
+      }
+
+      void SetFillValue(int32_t value)
       {
+        mode_ = DicomPixelMaskerMode_Fill;
+        fillValue_ = value;
+      }
+
+      void SetMeanFilter(uint32_t value)
+      {
+        mode_ = DicomPixelMaskerMode_MeanFilter;
+        filterWidth_ = value;
+      }
+
+      void SetTargetSeries(const std::set<std::string> targetSeries)
+      {
+        targetSeries_ = targetSeries;
+      }
+
+      void SetTargetInstances(const std::set<std::string> targetInstances)
+      {
+        targetInstances_ = targetInstances;
       }
     };
 
+    class Region2D : public BaseRegion
+    {
+      unsigned int            x1_;
+      unsigned int            y1_;
+      unsigned int            x2_;
+      unsigned int            y2_;
+
+    public:
+      Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2);
+      
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE;
+    };
+
+    class Region3D : public BaseRegion
+    {
+      double             x1_;
+      double             y1_;
+      double             z1_;
+      double             x2_;
+      double             y2_;
+      double             z2_;
+
+    public:
+      Region3D(double x1, double y1, double z1, double x2, double y2, double z2);
+
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE;
+    };
+
   private:
-    std::list<Region>   regions_;
+    std::list<BaseRegion*>   regions_;
 
   public:
     DicomPixelMasker();
+    
+    ~DicomPixelMasker();
 
     void Apply(ParsedDicomFile& toModify);
 
--- a/TODO	Wed Mar 12 09:59:38 2025 +0100
+++ b/TODO	Thu Mar 13 10:09:50 2025 +0100
@@ -434,7 +434,7 @@
 			{
 				"MaskType": "MeanFilter",
 				"FilterWidth": 20,
-				"RegionType" : "Pixels",
+				"RegionType" : "2D",             // area is defined by an area in pixel coordinates
 				"Origin": [150, 100],            // X, Y in pixel coordinates
 				"End": [400, 200],               // X, Y in pixel coordinates
 				"TargetSeries" : [               // the series the pixel mask applies to.  If empty -> applies to all series
@@ -446,7 +446,7 @@
 			{                                    
 				"MaskType": "Fill",
 				"FillValue": 0,
-				"RegionType" : "BoundingBox",       // area is defined by a volume in world coordinates  (TODO-PIXEL-ANON)
+				"RegionType" : "3D",                // area is defined by a volume in world coordinates  (TODO-PIXEL-ANON)
 				"Origin": [-150.5, -250.4, -811],   // X, Y, Z in World coordinates
 				"End": [148.4, 220.7, -955],        // X, Y, Z in World coordinates
 				"TargetSeries" : [                  // in this mode, no need to list the instances since the Z coordinate shall handle that !
@@ -471,7 +471,7 @@
         "Regions": [{
             "MaskType": "Fill",
             "FillValue": 4000,
-            "RegionType" : "Pixels",
+            "RegionType" : "2D",
             "Origin": [150, 100],
             "End": [400, 200],
             "TargetSeries" : ["d5f6c1a2-d6f2f01a-a3cc4e8b-424476dc-f34b0cd1"]
@@ -491,7 +491,7 @@
         "Regions": [{
             "MaskType": "Fill",
             "FillValue": 4000,
-            "RegionType" : "Pixels",
+            "RegionType" : "2D",
             "Origin": [150, 100],
             "End": [400, 200],
             "TargetInstances" : ["11e0b13a-fb44c3d5-819193d8-314b3bb5-4da13bc4", "372d70bb-886e6cc5-a89eefcc-405b2346-f4676bb2"]
@@ -500,6 +500,41 @@
 }
 EOF
 
+# modify a few slices of one series of a study with a 3D Region
+curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \
+--data-binary @- << EOF
+{
+    "Replace" : {"StudyInstanceUID": "1.2.3", "StudyDescription": "modified-few-slices"},
+    "Force": true,
+    "MaskPixelData": {
+        "Regions": [{
+            "MaskType": "Fill",
+            "FillValue": 4000,
+            "RegionType" : "3D",
+            "Origin": [-150.0, -300, -750],
+            "End": [150.0, -250, -1000]
+      }]
+    }
+}
+EOF
+
+curl -X POST http://localhost:8043/studies/321d3848-40c81c82-49f6f235-df6b1ec7-ed52f2fa/modify \
+--data-binary @- << EOF
+{
+    "Replace" : {"StudyInstanceUID": "1.2.3", "StudyDescription": "modified-few-slices"},
+    "Force": true,
+    "MaskPixelData": {
+        "Regions": [{
+            "MaskType": "MeanFilter",
+            "FilterWidth": 30,
+            "RegionType" : "3D",
+            "Origin": [-150.0, -300, -750],
+            "End": [150.0, -250, -1000]
+      }]
+    }
+}
+EOF
+
 
 # modify all frames of a multiframe study (CARDIO)
 curl -X POST http://localhost:8043/studies/595df1a1-74fe920a-4b9e3509-826f17a3-762a2dc3/modify \
@@ -511,7 +546,7 @@
         "Regions": [{
             "MaskType": "MeanFilter",
             "FilterWidth": 30,
-            "RegionType" : "Pixels",
+            "RegionType" : "2D",
             "Origin": [150, 100],
             "End": [400, 200]
       }]
@@ -529,7 +564,7 @@
         "Regions": [{
             "MaskType": "MeanFilter",
             "FilterWidth": 30,
-            "RegionType" : "Pixels",
+            "RegionType" : "2D",
             "Origin": [0, 0],
             "End": [768, 45]
       }]