Repository: InsightSoftwareConsortium/ITK-Wasm (language: python)

Issue:
Handling negative indices on WebAssembly memory IO
Hi 👋

I've noticed a difference while working with padding filters, particularly when using WebAssembly memory IO versus the filesystem.

When employing ITK filters with padding, the image's origin is preserved, and negative indices handle padding. However, when saving images via the filesystem, metadata updates smoothly, while WebAssembly memory IO seems to miss the mark.

I've set up a quick example [here](https://github.com/ramonemiliani93/pad-bug) to illustrate. When running the pipeline with the WASI build, the image is correct, but in the browser, the origin behaves oddly.

I'm curious: should WebAssembly memory IO handle this, or is it the pipeline's job to manage negative indices and update the image origin?

Looking forward to your insights!


Discussion / hints:
Hello! :wave: :sun_with_face: 

> I've set up a quick example here to illustrate. When running the pipeline with the WASI build, the image is correct, but in the browser, the origin behaves oddly.

This is an outstanding example to illustrate the issue. Well done.

Yes, you have discovered a bug! :bug: The ITK-Wasm `Image` type does not include support for the `Index` that is adjusted by the ConstantPadImageFilter. Adjusting your demo to print the output image:

```
Image (0x2e048)
  RTTI typeinfo:   N3itk5ImageIhLj3EEE
  Reference Count: 1
  Modified Time: 30
  Debug: Off
  Object Name: 
  Observers: 
    none
  Source: (0x2de00) 
  Source output name: Primary
  Release Data: Off
  Data Released: False
  Global Release Data: Off
  PipelineMTime: 23
  UpdateMTime: 31
  RealTimeStamp: 0 seconds 
  LargestPossibleRegion: 
    Dimension: 3
    Index: [-3, -3, -3]
    Size: [9, 9, 9]
  BufferedRegion: 
    Dimension: 3
    Index: [-3, -3, -3]
    Size: [9, 9, 9]
  RequestedRegion: 
    Dimension: 3
    Index: [-3, -3, -3]
    Size: [9, 9, 9]
  Spacing: [1, 1, 1]
  Origin: [0, 0, 0]
  Direction: 
1 0 0
0 1 0
0 0 1

  IndexToPointMatrix: 
1 0 0
0 1 0
0 0 1

  PointToIndexMatrix: 
1 0 0
0 1 0
0 0 1

  Inverse Direction: 
1 0 0
0 1 0
0 0 1

  PixelContainer: 
    ImportImageContainer (0x2e258)
      RTTI typeinfo:   N3itk20ImportImageContainerImhEE
      Reference Count: 1
      Modified Time: 28
      Debug: Off
      Object Name: 
      Observers: 
        none
      Pointer: 0x2e2d8
      Container manages memory: true
      Size: 729
      Capacity: 729
```

The solution is to fix the `ImageToWasmImageFilter` to adjust the output origin when the LargestPossibleRegion's Index is not all zeros. The output origin can be determined by calling [`TransformIndexToPhysicalPoint`](https://itk.org/Doxygen/html/classitk_1_1ImageBase.html#a8360b9623d685997725c81c85716c4d2) with the input image's LargestPossibleRegion Index:

https://github.com/InsightSoftwareConsortium/itk-wasm/blob/3bd777e1225671090edb6e6a45a5a73eab07f5c8/include/itkImageToWasmImageFilter.hxx#L167

Could you please [submit a patch](https://wasm.itk.org/en/latest/development/hacking_itk_wasm.html)?

Fix patch:
diff --git a/include/itkImageToWasmImageFilter.hxx b/include/itkImageToWasmImageFilter.hxx
index 135af74f4..9516366bb 100644
--- a/include/itkImageToWasmImageFilter.hxx
+++ b/include/itkImageToWasmImageFilter.hxx
@@ -137,6 +137,7 @@ ImageToWasmImageFilter<TImage>
 
   imageJSON->SetImage(image);
 
+  using PointType = typename TImage::PointType;
   using PixelType = typename TImage::IOPixelType;
   using ConvertPixelTraits = DefaultConvertPixelTraits<PixelType>;
   using ComponentType = typename ConvertPixelTraits::ComponentType;
@@ -164,7 +165,10 @@ ImageToWasmImageFilter<TImage>
   document.AddMember( "imageType", imageType.Move(), allocator );
 
   rapidjson::Value origin(rapidjson::kArrayType);
-  const auto imageOrigin = image->GetOrigin();
+
+  const auto largestRegion = image->GetLargestPossibleRegion();
+  PointType imageOrigin;
+  image->TransformIndexToPhysicalPoint(largestRegion.GetIndex(), imageOrigin);
   for( unsigned int ii = 0; ii < dimension; ++ii )
     {
     origin.PushBack(rapidjson::Value().SetDouble(imageOrigin[ii]), allocator);
diff --git a/itk-module.cmake b/itk-module.cmake
index 1dd150c26..5d1597191 100644
--- a/itk-module.cmake
+++ b/itk-module.cmake
@@ -15,6 +15,7 @@ itk_module(WebAssemblyInterface
   TEST_DEPENDS
     ITKTestKernel
     ITKMesh
+    ITKImageGrid
   FACTORY_NAMES
     ImageIO::Wasm
     MeshIO::Wasm


Test patch:
diff --git a/test/Baseline/negative_idx_padded.mha.cid b/test/Baseline/negative_idx_padded.mha.cid
new file mode 100644
index 000000000..aa5aab7c4
--- /dev/null
+++ b/test/Baseline/negative_idx_padded.mha.cid
@@ -0,0 +1,1 @@
+bafkreifzpwttltw372hen6ouewhpqwodxs4zo4kjk7ekyclzcoshfnxq3u
\ No newline at end of file
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index b96e765b8..31897f735 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -30,6 +30,7 @@ ExternalData_Expand_Arguments(
   DATA{Input/sphere.fsa}
   DATA{Input/sphere.fsb}
   DATA{Input/box.obj}
+  DATA{Input/negative_idx_base.mha}
   DATA{Input/octa.off}
   DATA{Input/sphere.stl}
   DATA{Input/cthead1.iwi.cbor}
@@ -50,10 +51,12 @@ ExternalData_Expand_Arguments(
   DATA{Input/PNGSeries/,REGEX:mri3D_.[0-9]+.png}
   DATA{Input/11706c2.CNG.swc}
   DATA{Baseline/web_worker_pool.cy.png}
-  )
+  DATA{Baseline/negative_idx_padded.mha}
+)
 
 set(WebAssemblyInterfaceTests
   itkWasmImageInterfaceTest.cxx
+  itkWasmImageInterfaceWithNegativeIndexTest.cxx
   itkWasmMeshInterfaceTest.cxx
   itkWasmPolyDataInterfaceTest.cxx
   itkWasmImageIOTest.cxx
@@ -84,6 +87,15 @@ itk_add_test(NAME itkWasmImageInterfaceTest
       ${ITK_TEST_OUTPUT_DIR}/itkWasmImageInterfaceTest.mha
 )
 
+itk_add_test(NAME itkWasmImageInterfaceWithNegativeIndexTest
+        COMMAND WebAssemblyInterfaceTestDriver
+        --compare DATA{Baseline/negative_idx_padded.mha}
+        ${ITK_TEST_OUTPUT_DIR}/itkWasmImageInterfaceWithNegativeIndexTest.mha
+        itkWasmImageInterfaceWithNegativeIndexTest
+        DATA{Input/negative_idx_base.mha}
+        ${ITK_TEST_OUTPUT_DIR}/itkWasmImageInterfaceWithNegativeIndexTest.mha
+)
+
 itk_add_test(NAME itkWasmMeshInterfaceTest
     COMMAND WebAssemblyInterfaceTestDriver
     itkWasmMeshInterfaceTest
diff --git a/test/Input/negative_idx_base.mha.cid b/test/Input/negative_idx_base.mha.cid
new file mode 100644
index 000000000..eb282ba21
--- /dev/null
+++ b/test/Input/negative_idx_base.mha.cid
@@ -0,0 +1,1 @@
+bafkreiec2npcdz52d6gjmod7f4cfckgqcmizch3dpujxhjpxxqggxlkemy
\ No newline at end of file
diff --git a/test/itkWasmImageInterfaceWithNegativeIndexTest.cxx b/test/itkWasmImageInterfaceWithNegativeIndexTest.cxx
new file mode 100644
index 000000000..f0be6cbc0
--- /dev/null
+++ b/test/itkWasmImageInterfaceWithNegativeIndexTest.cxx
@@ -0,0 +1,78 @@
+/*=========================================================================
+ *
+ *  Copyright NumFOCUS
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *         https://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *=========================================================================*/
+#include "itkImageToWasmImageFilter.h"
+#include "itkWasmImageToImageFilter.h"
+
+#include "itkConstantPadImageFilter.h"
+#include "itkImageFileReader.h"
+#include "itkImageFileWriter.h"
+#include "itkTestingMacros.h"
+
+int
+itkWasmImageInterfaceWithNegativeIndexTest(int argc, char * argv[])
+{
+  if (argc < 3)
+  {
+    std::cerr << "Missing parameters" << std::endl;
+    std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << " InputImage OutputImage" << std::endl;
+    return EXIT_FAILURE;
+  }
+  const char * inputImageFile = argv[1];
+  const char * outputImageFile = argv[2];
+
+  constexpr unsigned int Dimension = 3;
+  using PixelType = unsigned char;
+  using ImageType = itk::Image<PixelType, Dimension>;
+  using ImagePointer = ImageType::Pointer;
+
+  ImagePointer inputImage = nullptr;
+  ITK_TRY_EXPECT_NO_EXCEPTION(inputImage = itk::ReadImage<ImageType>(inputImageFile));
+  std::cout << "inputImage: " << inputImage << std::endl;
+
+  ImageType::SizeType lowerExtendRegion;
+  lowerExtendRegion.Fill(1);
+
+  ImageType::SizeType upperExtendRegion;
+  upperExtendRegion.Fill(1);
+
+  using ConstantPadImageFilterType = itk::ConstantPadImageFilter<ImageType,ImageType>;
+  auto constantPad = ConstantPadImageFilterType::New();
+  constantPad->SetInput(inputImage);
+  constantPad->SetPadLowerBound(lowerExtendRegion);
+  constantPad->SetPadUpperBound(upperExtendRegion);
+  constantPad->SetConstant(0);
+
+  using ImageToWasmImageFilterType = itk::ImageToWasmImageFilter<ImageType>;
+  auto imageToJSON = ImageToWasmImageFilterType::New();
+  imageToJSON->SetInput(constantPad->GetOutput());
+  imageToJSON->Update();
+  auto imageJSON = imageToJSON->GetOutput();
+  std::cout << "Image JSON: " << imageJSON->GetJSON() << std::endl;
+
+  using WasmImageToImageFilterType = itk::WasmImageToImageFilter<ImageType>;
+  auto jsonToImage = WasmImageToImageFilterType::New();
+  jsonToImage->SetInput(imageToJSON->GetOutput());
+  jsonToImage->Update();
+  ImageType::Pointer convertedImage = jsonToImage->GetOutput();
+
+  std::cout << "convertedImage: " << convertedImage << std::endl;
+
+  ITK_TRY_EXPECT_NO_EXCEPTION(itk::WriteImage(convertedImage, outputImageFile));
+
+  return EXIT_SUCCESS;
+}