Skip to content

Commit 4283212

Browse files
authored
Merge pull request #1556 from thewtex/downsample-translation
fix(downsample): shift output origin to coarse-grid pixel centers
2 parents 640d480 + fa0c29b commit 4283212

19 files changed

Lines changed: 137 additions & 30 deletions

File tree

‎packages/downsample/downsample-label-image.cxx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "itkOutputImage.h"
2222
#include "itkSupportInputImageTypes.h"
2323

24+
#include "itkContinuousIndex.h"
2425
#include "itkLabelImageGenericInterpolateImageFunction.h"
2526
#include "itkLinearInterpolateImageFunction.h"
2627
#include "itkResampleImageFilter.h"
@@ -56,21 +57,24 @@ class PipelineFunctor
5657

5758
ITK_WASM_PARSE(pipeline);
5859

59-
const auto inputOrigin = inputImage.Get()->GetOrigin();
6060
const auto inputSpacing = inputImage.Get()->GetSpacing();
6161
const auto inputSize = inputImage.Get()->GetLargestPossibleRegion().GetSize();
62+
const auto inputStartIndex = inputImage.Get()->GetLargestPossibleRegion().GetIndex();
6263

63-
typename ImageType::PointType outputOrigin;
64-
typename ImageType::SpacingType outputSpacing;
65-
typename ImageType::SizeType outputSize;
64+
typename ImageType::PointType outputOrigin;
65+
typename ImageType::SpacingType outputSpacing;
66+
typename ImageType::SizeType outputSize;
67+
itk::ContinuousIndex<double, ImageDimension> outputOriginIndex;
6668
for (unsigned int i = 0; i < ImageDimension; ++i)
6769
{
6870
const double cropRadiusValue = cropRadius.size() ? cropRadius[i] : 0.0;
6971

70-
outputOrigin[i] = inputOrigin[i] + cropRadiusValue * inputSpacing[i];
72+
// Origin lands at the center of the first bin, matching itk::BinShrinkImageFilter
73+
outputOriginIndex[i] = inputStartIndex[i] + cropRadiusValue + (shrinkFactors[i] - 1.0) / 2.0;
7174
outputSpacing[i] = inputSpacing[i] * shrinkFactors[i];
7275
outputSize[i] = std::max<itk::SizeValueType>(0, (inputSize[i] - 2 * cropRadiusValue) / shrinkFactors[i]);
7376
}
77+
inputImage.Get()->TransformContinuousIndexToPhysicalPoint(outputOriginIndex, outputOrigin);
7478

7579
using InterpolatorType =
7680
itk::LabelImageGenericInterpolateImageFunction<ImageType, itk::LinearInterpolateImageFunction>;

‎packages/downsample/downsample.cxx‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "itkOutputImage.h"
2222
#include "itkSupportInputImageTypes.h"
2323

24+
#include "itkContinuousIndex.h"
2425
#include "itkDiscreteGaussianImageFilter.h"
2526
#include "itkLinearInterpolateImageFunction.h"
2627
#include "itkResampleImageFilter.h"
@@ -94,21 +95,24 @@ DownsampleScalarImage(itk::wasm::Pipeline & pipeline, const TImage * inputImage)
9495
gaussianFilter->SetSigmaArray(sigmaArray);
9596
gaussianFilter->SetUseImageSpacingOff();
9697

97-
const auto inputOrigin = inputImage->GetOrigin();
9898
const auto inputSpacing = inputImage->GetSpacing();
9999
const auto inputSize = inputImage->GetLargestPossibleRegion().GetSize();
100+
const auto inputStartIndex = inputImage->GetLargestPossibleRegion().GetIndex();
100101

101-
typename ImageType::PointType outputOrigin;
102-
typename ImageType::SpacingType outputSpacing;
103-
typename ImageType::SizeType outputSize;
102+
typename ImageType::PointType outputOrigin;
103+
typename ImageType::SpacingType outputSpacing;
104+
typename ImageType::SizeType outputSize;
105+
itk::ContinuousIndex<double, ImageDimension> outputOriginIndex;
104106
for (unsigned int i = 0; i < ImageDimension; ++i)
105107
{
106108
const double cropRadiusValue = cropRadius.size() ? cropRadius[i] : 0.0;
107109

108-
outputOrigin[i] = inputOrigin[i] + cropRadiusValue * inputSpacing[i];
110+
// Origin lands at the center of the first bin, matching itk::BinShrinkImageFilter
111+
outputOriginIndex[i] = inputStartIndex[i] + cropRadiusValue + (shrinkFactors[i] - 1.0) / 2.0;
109112
outputSpacing[i] = inputSpacing[i] * shrinkFactors[i];
110113
outputSize[i] = std::max<itk::SizeValueType>(0, (inputSize[i] - 2 * cropRadiusValue) / shrinkFactors[i]);
111114
}
115+
inputImage->TransformContinuousIndexToPhysicalPoint(outputOriginIndex, outputOrigin);
112116

113117
using InterpolatorType = itk::LinearInterpolateImageFunction<ImageType, double>;
114118
auto interpolator = InterpolatorType::New();
@@ -187,21 +191,24 @@ class PipelineFunctor<itk::VectorImage<TPixel, VDimension>>
187191

188192
auto sigmaValues = downsampleSigma(shrinkFactors);
189193

190-
const auto inputOrigin = inputImage.Get()->GetOrigin();
191194
const auto inputSpacing = inputImage.Get()->GetSpacing();
192195
const auto inputSize = inputImage.Get()->GetLargestPossibleRegion().GetSize();
196+
const auto inputStartIndex = inputImage.Get()->GetLargestPossibleRegion().GetIndex();
193197

194-
typename VectorImageType::PointType outputOrigin;
195-
typename VectorImageType::SpacingType outputSpacing;
196-
typename VectorImageType::SizeType outputSize;
198+
typename VectorImageType::PointType outputOrigin;
199+
typename VectorImageType::SpacingType outputSpacing;
200+
typename VectorImageType::SizeType outputSize;
201+
itk::ContinuousIndex<double, Dimension> outputOriginIndex;
197202
for (unsigned int i = 0; i < Dimension; ++i)
198203
{
199204
const double cropRadiusValue = cropRadius.size() ? cropRadius[i] : 0.0;
200205

201-
outputOrigin[i] = inputOrigin[i] + cropRadiusValue * inputSpacing[i];
206+
// Origin lands at the center of the first bin, matching itk::BinShrinkImageFilter
207+
outputOriginIndex[i] = inputStartIndex[i] + cropRadiusValue + (shrinkFactors[i] - 1.0) / 2.0;
202208
outputSpacing[i] = inputSpacing[i] * shrinkFactors[i];
203209
outputSize[i] = std::max<itk::SizeValueType>(0, (inputSize[i] - 2 * cropRadiusValue) / shrinkFactors[i]);
204210
}
211+
inputImage.Get()->TransformContinuousIndexToPhysicalPoint(outputOriginIndex, outputOrigin);
205212

206213
auto composeFilter = ComposeFilterType::New();
207214

‎packages/downsample/package.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
{
22
"name": "@itk-wasm/downsample-build",
3-
"version": "1.8.1",
3+
"version": "1.9.0",
44
"private": true,
55
"description": "Pipelines for downsampling images.",
66
"type": "module",
77
"itk-wasm": {
88
"emscripten-docker-image": "docker.io/itkwasm/emscripten:latest",
99
"wasi-docker-image": "docker.io/itkwasm/wasi:latest",
10-
"test-data-hash": "bafkreiesjpg3sjqkyjb77djrffdvznjjsgvudd2y44iqjrfgnd45uyyprq",
10+
"test-data-hash": "bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi",
1111
"test-data-urls": [
12-
"https://itk.mypinata.cloud/ipfs/bafkreiesjpg3sjqkyjb77djrffdvznjjsgvudd2y44iqjrfgnd45uyyprq"
12+
"https://github.com/InsightSoftwareConsortium/ITK-Wasm/releases/download/test-data/downsample-test-data-2026-07-15-bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi.tar.gz",
13+
"https://itk.mypinata.cloud/ipfs/bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi"
1314
],
1415
"typescript-package-name": "@itk-wasm/downsample",
1516
"python-package-name": "itkwasm-downsample",
@@ -32,10 +33,10 @@
3233
"build:gen:typescript": "itk-wasm pnpm-script build:gen:typescript",
3334
"build:gen:python": "pnpm build:wasi && pnpm bindgen:python",
3435
"test": "pnpm test:data:download && pnpm build:gen:python && pnpm test:python",
35-
"test:data:download": "dam download test/data test/data.tar.gz bafkreiesjpg3sjqkyjb77djrffdvznjjsgvudd2y44iqjrfgnd45uyyprq https://itk.mypinata.cloud/ipfs/bafkreiesjpg3sjqkyjb77djrffdvznjjsgvudd2y44iqjrfgnd45uyyprq",
36+
"test:data:download": "dam download test/data test/data.tar.gz bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi https://github.com/InsightSoftwareConsortium/ITK-Wasm/releases/download/test-data/downsample-test-data-2026-07-15-bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi.tar.gz https://itk.mypinata.cloud/ipfs/bafkreie3b4xdvf4cqykvagruaivwtb3oi55hwt4egzlhppu2o3hw2eudhi",
3637
"test:data:pack": "dam pack test/data test/data.tar.gz",
3738
"test:python:wasi": "pnpm test:data:download && pixi run --manifest-path=./pixi.toml test-wasi",
38-
"test:python:emscripten": "pnpm test:data:download && pixi run --manifest-path=./pixi.toml test-emscripten",
39+
"test:python:emscripten": "pnpm build:emscripten && pnpm test:data:download && pixi run --manifest-path=./pixi.toml test-emscripten",
3940
"test:python:dispatch": "pnpm test:data:download && pixi run --manifest-path=./pixi.toml test-dispatch",
4041
"test:python": "itk-wasm pnpm-script test:python",
4142
"test:wasi": "itk-wasm pnpm-script test:wasi -- -V"

‎packages/downsample/pixi.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ description = "Download Pyodide"
104104
[feature.python.tasks.test-emscripten]
105105
cmd = '''mkdir -p dist/pyodide &&
106106
cp -r ../../pyodide dist/ &&
107+
mkdir -p dist/pyodide/pipelines &&
108+
cp ../../emscripten-build/*.js ../../emscripten-build/*.wasm.zst dist/pyodide/pipelines/ &&
107109
hatch build -t wheel ./dist/pyodide/ &&
108110
pytest --dist-dir=./dist/pyodide --rt=chrome'''
109111
cwd = "python/itkwasm-downsample-emscripten"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.8.1"
1+
__version__ = "1.9.0"

‎packages/downsample/python/itkwasm-downsample-emscripten/itkwasm_downsample_emscripten/js_package.py‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎packages/downsample/python/itkwasm-downsample-emscripten/test/test_downsample_async.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
async def test_downsample_async(selenium, package_wheel, input_data):
1111
import micropip
1212
await micropip.install([package_wheel, 'itkwasm-image-io', 'itkwasm-compare-images'])
13+
import js
14+
from itkwasm_downsample_emscripten.js_package import js_package
15+
js_package.config.pipelines_base_url = f"{js.location.origin}/pipelines"
1316
def write_input_data_to_fs(input_data, filename):
1417
with open(filename, 'wb') as fp:
1518
fp.write(input_data[filename])

‎packages/downsample/python/itkwasm-downsample-emscripten/test/test_downsample_bin_shrink_async.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
async def test_downsample_bin_shrink_async(selenium, package_wheel, input_data):
1111
import micropip
1212
await micropip.install([package_wheel, 'itkwasm-image-io', 'itkwasm-compare-images'])
13+
import js
14+
from itkwasm_downsample_emscripten.js_package import js_package
15+
js_package.config.pipelines_base_url = f"{js.location.origin}/pipelines"
1316
def write_input_data_to_fs(input_data, filename):
1417
with open(filename, 'wb') as fp:
1518
fp.write(input_data[filename])

‎packages/downsample/python/itkwasm-downsample-emscripten/test/test_downsample_label_image_async.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
async def test_downsample_label_image_async(selenium, package_wheel, input_data):
1111
import micropip
1212
await micropip.install([package_wheel, 'itkwasm-image-io', 'itkwasm-compare-images'])
13+
import js
14+
from itkwasm_downsample_emscripten.js_package import js_package
15+
js_package.config.pipelines_base_url = f"{js.location.origin}/pipelines"
1316
def write_input_data_to_fs(input_data, filename):
1417
with open(filename, 'wb') as fp:
1518
fp.write(input_data[filename])

‎packages/downsample/python/itkwasm-downsample-emscripten/test/test_downsample_sigma_async.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
async def test_downsample_sigma_async(selenium, package_wheel, input_data):
1111
import micropip
1212
await micropip.install([package_wheel,])
13+
import js
14+
from itkwasm_downsample_emscripten.js_package import js_package
15+
js_package.config.pipelines_base_url = f"{js.location.origin}/pipelines"
1316

1417
from itkwasm_downsample_emscripten import downsample_sigma_async
1518

0 commit comments

Comments
 (0)