Skip to content

Commit 3359c5a

Browse files
authored
Merge pull request #13 from zivy/resize
resizing an image while retaining physical location.
2 parents 6dad599 + 88e0a71 commit 3359c5a

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

SimpleITK/utilities/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .make_isotropic import make_isotropic
2222
from .fft import fft_based_translation_initialization
2323
from .overlay_bounding_boxes import overlay_bounding_boxes
24+
from .resize import resize
2425

2526
try:
2627
from ._version import version as __version__
@@ -34,6 +35,7 @@
3435
"make_isotropic",
3536
"fft_based_translation_initialization",
3637
"overlay_bounding_boxes",
38+
"resize",
3739
"__version__",
3840
]
3941

SimpleITK/utilities/resize.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import SimpleITK as sitk
2+
from typing import Sequence
3+
from typing import Union
4+
from math import ceil
5+
import collections
6+
7+
8+
def _issequence(obj):
9+
if isinstance(obj, (bytes, str)):
10+
return False
11+
return isinstance(obj, collections.abc.Sequence)
12+
13+
14+
def resize(
15+
image: sitk.Image,
16+
new_size: Sequence[int],
17+
isotropic: bool = True,
18+
fill: bool = True,
19+
interpolator=sitk.sitkLinear,
20+
fill_value: float = 0.0,
21+
use_nearest_extrapolator: bool = False,
22+
anti_aliasing_sigma: Union[None, float, Sequence[float]] = None,
23+
) -> sitk.Image:
24+
"""
25+
Resize an image to an arbitrary size while retaining the original image's spatial location.
26+
27+
Allows for specification of the target image size in pixels, and whether the image pixels spacing should be
28+
isotropic. The physical extent of the image's data is retained in the new image, with the new image's spacing
29+
adjusted to achieve the desired size. The image is centered in the new image.
30+
31+
:param image: A SimpleITK image.
32+
:param new_size: The new image size in pixels.
33+
:param isotropic: If False, the original image is resized to fill the new image size by adjusting space. If True,
34+
the new image's spacing will be isotropic.
35+
:param fill: If True, the output image will be new_size, and the original image will be centered in the new image
36+
with constant or nearest values used to fill in the new image. If False and isotropic is True, the output image's
37+
new size will be calculated to fit the original image's extent such that at least one dimension is equal to
38+
new_size.
39+
:param fill_value: Value used for padding.
40+
:param interpolator: Interpolator used for resampling.
41+
:param use_nearest_extrapolator: If True, use a nearest neighbor for extrapolation when resampling, overridding the
42+
constant fill value.
43+
:param anti_aliasing_sigma: If zero no antialiasing is performed. If a scalar, it is used as the sigma value in
44+
physical units for all axes. If None or a sequence, the sigma value for each axis is calculated as
45+
$sigma = (new_spacing - old_spacing) / 2$ in physical units. Gaussian smoothing is performed prior to resampling
46+
for antialiasing.
47+
:return: A SimpleITK image with desired size.
48+
"""
49+
new_spacing = [
50+
(osz * ospc) / nsz
51+
for ospc, osz, nsz in zip(image.GetSpacing(), image.GetSize(), new_size)
52+
]
53+
54+
if isotropic:
55+
new_spacing = [max(new_spacing)] * image.GetDimension()
56+
if not fill:
57+
new_size = [
58+
ceil(osz * ospc / nspc)
59+
for ospc, osz, nspc in zip(image.GetSpacing(), image.GetSize(), new_spacing)
60+
]
61+
62+
center_cidx = [0.5 * (sz - 1) for sz in image.GetSize()]
63+
new_center_cidx = [0.5 * (sz - 1) for sz in new_size]
64+
65+
new_origin_cidx = [0] * image.GetDimension()
66+
# The continuous index of the new center of the image, in the original image's continuous index space.
67+
for i in range(image.GetDimension()):
68+
new_origin_cidx[i] = center_cidx[i] - new_center_cidx[i] * (
69+
new_spacing[i] / image.GetSpacing()[i]
70+
)
71+
72+
new_origin = image.TransformContinuousIndexToPhysicalPoint(new_origin_cidx)
73+
74+
input_pixel_type = image.GetPixelID()
75+
76+
if anti_aliasing_sigma is None:
77+
# (s-1)/2.0 is the standard deviation of the Gaussian kernel in index space, where s downsample factor defined
78+
# by nspc/ospc.
79+
anti_aliasing_sigma = [
80+
max((nspc - ospc) / 2.0, 0.0)
81+
for ospc, nspc in zip(image.GetSpacing(), new_spacing)
82+
]
83+
elif not _issequence(anti_aliasing_sigma):
84+
anti_aliasing_sigma = [anti_aliasing_sigma] * image.GetDimension()
85+
86+
if any([s < 0.0 for s in anti_aliasing_sigma]):
87+
raise ValueError("anti_aliasing_sigma must be positive, or None.")
88+
if len(anti_aliasing_sigma) != image.GetDimension():
89+
raise ValueError(
90+
"anti_aliasing_sigma must be a scalar or a sequence of length equal to the image dimension."
91+
)
92+
93+
if all([s > 0.0 for s in anti_aliasing_sigma]):
94+
image = sitk.SmoothingRecursiveGaussian(image, anti_aliasing_sigma)
95+
else:
96+
for d, s in enumerate(anti_aliasing_sigma):
97+
if s > 0.0:
98+
image = sitk.RecursiveGaussian(image, sigma=s, direction=d)
99+
100+
return sitk.Resample(
101+
image,
102+
size=new_size,
103+
outputOrigin=new_origin,
104+
outputSpacing=new_spacing,
105+
outputDirection=image.GetDirection(),
106+
defaultPixelValue=fill_value,
107+
interpolator=interpolator,
108+
useNearestNeighborExtrapolator=use_nearest_extrapolator,
109+
outputPixelType=input_pixel_type,
110+
)

test/test_utilities.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import math
2+
13
import SimpleITK as sitk
24
import SimpleITK.utilities as sitkutils
5+
from numpy.testing import assert_allclose
36

47

58
def test_Logger():
@@ -88,3 +91,201 @@ def test_overlay_bounding_boxes():
8891
scalar_hash == "d7dde3eee4c334ffe810a636dff872a6ded592fc"
8992
and rgb_hash == "d6694a394f8fcc32ea337a1f9531dda6f4884af1"
9093
)
94+
95+
96+
def test_resize():
97+
original_image = sitk.Image([128, 128], sitk.sitkUInt8) + 50
98+
resized_image = sitkutils.resize(image=original_image, new_size=[128, 128])
99+
assert resized_image.GetSize() == (128, 128)
100+
assert resized_image.GetSpacing() == (1.0, 1.0)
101+
assert resized_image.GetOrigin() == (0.0, 0.0)
102+
assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == (
103+
-0.5,
104+
-0.5,
105+
)
106+
107+
resized_image = sitkutils.resize(image=original_image, new_size=[64, 64])
108+
assert resized_image.GetSize() == (64, 64)
109+
assert resized_image.GetSpacing() == (2.0, 2.0)
110+
assert resized_image.GetOrigin() == (0.5, 0.5)
111+
assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == (
112+
-0.5,
113+
-0.5,
114+
)
115+
116+
resized_image = sitkutils.resize(
117+
image=original_image, new_size=[64, 128], fill=False
118+
)
119+
assert resized_image.GetSize() == (64, 64)
120+
assert resized_image.GetSpacing() == (2.0, 2.0)
121+
assert resized_image.GetOrigin() == (0.5, 0.5)
122+
assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == (
123+
-0.5,
124+
-0.5,
125+
)
126+
127+
resized_image = sitkutils.resize(
128+
image=original_image, new_size=[64, 128], isotropic=False
129+
)
130+
assert resized_image.GetSize() == (64, 128)
131+
assert resized_image.GetSpacing() == (2.0, 1.0)
132+
assert resized_image.GetOrigin() == (0.5, 0.0)
133+
assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == (
134+
-0.5,
135+
-0.5,
136+
)
137+
138+
139+
def test_resize_3d():
140+
original_image = sitk.Image([128, 128, 128], sitk.sitkUInt8) + 50
141+
resized_image = sitkutils.resize(image=original_image, new_size=[128, 128, 128])
142+
assert resized_image.GetSize() == (128, 128, 128)
143+
assert resized_image.GetSpacing() == (1.0, 1.0, 1.0)
144+
assert resized_image.GetOrigin() == (0.0, 0.0, 0.0)
145+
assert resized_image.TransformContinuousIndexToPhysicalPoint(
146+
(-0.5, -0.5, -0.5)
147+
) == (
148+
-0.5,
149+
-0.5,
150+
-0.5,
151+
)
152+
153+
resized_image = sitkutils.resize(image=original_image, new_size=[64, 64, 64])
154+
assert resized_image.GetSize() == (64, 64, 64)
155+
assert resized_image.GetSpacing() == (2.0, 2.0, 2.0)
156+
assert resized_image.GetOrigin() == (0.5, 0.5, 0.5)
157+
assert resized_image.TransformContinuousIndexToPhysicalPoint(
158+
(-0.5, -0.5, -0.5)
159+
) == (
160+
-0.5,
161+
-0.5,
162+
-0.5,
163+
)
164+
165+
resized_image = sitkutils.resize(image=original_image, new_size=[64, 32, 64])
166+
assert resized_image.GetSize() == (64, 32, 64)
167+
assert resized_image.GetSpacing() == (4.0, 4.0, 4.0)
168+
assert resized_image.GetOrigin() == (-62.5, 1.5, -62.5)
169+
assert resized_image.TransformContinuousIndexToPhysicalPoint(
170+
(-0.5, -0.5, -0.5)
171+
) == (
172+
-64.5,
173+
-0.5,
174+
-64.5,
175+
)
176+
177+
resized_image = sitkutils.resize(
178+
image=original_image, new_size=[64, 64, 32], fill=False
179+
)
180+
assert resized_image.GetSize() == (32, 32, 32)
181+
assert resized_image.GetSpacing() == (4.0, 4.0, 4.0)
182+
assert resized_image.GetOrigin() == (1.5, 1.5, 1.5)
183+
assert resized_image.TransformContinuousIndexToPhysicalPoint(
184+
(-0.5, -0.5, -0.5)
185+
) == (
186+
-0.5,
187+
-0.5,
188+
-0.5,
189+
)
190+
191+
resized_image = sitkutils.resize(
192+
image=original_image, new_size=[32, 64, 64], isotropic=False
193+
)
194+
assert resized_image.GetSize() == (32, 64, 64)
195+
assert resized_image.GetSpacing() == (4.0, 2.0, 2.0)
196+
assert resized_image.GetOrigin() == (1.5, 0.5, 0.5)
197+
assert resized_image.TransformContinuousIndexToPhysicalPoint(
198+
(-0.5, -0.5, -0.5)
199+
) == (
200+
-0.5,
201+
-0.5,
202+
-0.5,
203+
)
204+
205+
206+
def test_resize_fill():
207+
original_image = sitk.Image([16, 32], sitk.sitkFloat32) + 1.0
208+
209+
resized_image = sitkutils.resize(
210+
image=original_image, new_size=[32, 32], fill=True, fill_value=10.0
211+
)
212+
assert resized_image.GetSize() == (32, 32)
213+
assert resized_image.GetSpacing() == (1.0, 1.0)
214+
assert resized_image.GetOrigin() == (-8.0, 0.0)
215+
assert resized_image[0, 0] == 10.0
216+
assert resized_image[15, 15] == 1.0
217+
assert resized_image[31, 31] == 10.0
218+
219+
resized_image = sitkutils.resize(
220+
image=original_image,
221+
new_size=[32, 32],
222+
fill=True,
223+
use_nearest_extrapolator=True,
224+
)
225+
assert resized_image.GetSize() == (32, 32)
226+
assert resized_image.GetSpacing() == (1.0, 1.0)
227+
assert resized_image.GetOrigin() == (-8.0, 0.0)
228+
assert resized_image[0, 0] == 1.0
229+
assert resized_image[15, 15] == 1.0
230+
assert resized_image[31, 31] == 1.0
231+
232+
233+
def test_resize_anti_aliasing():
234+
original_image = sitk.Image([5, 5], sitk.sitkFloat32)
235+
original_image[2, 2] = 1.0
236+
237+
resized_image = sitkutils.resize(
238+
image=original_image,
239+
new_size=[3, 3],
240+
interpolator=sitk.sitkNearestNeighbor,
241+
anti_aliasing_sigma=0,
242+
)
243+
assert resized_image.GetSize() == (3, 3)
244+
assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3))
245+
assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3))
246+
assert resized_image[0, 0] == 0.0
247+
assert resized_image[1, 1] == 1.0
248+
assert resized_image[1, 0] == 0.0
249+
assert resized_image[0, 1] == 0.0
250+
251+
resized_image = sitkutils.resize(
252+
image=original_image,
253+
new_size=[3, 3],
254+
interpolator=sitk.sitkNearestNeighbor,
255+
anti_aliasing_sigma=None,
256+
)
257+
assert resized_image.GetSize() == (3, 3)
258+
assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3))
259+
assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3))
260+
assert math.isclose(resized_image[1, 1], 0.960833, abs_tol=1e-6)
261+
assert resized_image[1, 0] == resized_image[0, 1]
262+
assert resized_image[0, 0] == resized_image[2, 2]
263+
264+
resized_image = sitkutils.resize(
265+
image=original_image,
266+
new_size=[3, 3],
267+
interpolator=sitk.sitkNearestNeighbor,
268+
anti_aliasing_sigma=0.5,
269+
)
270+
assert resized_image.GetSize() == (3, 3)
271+
assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3))
272+
assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3))
273+
assert math.isclose(resized_image[1, 1], 0.621714, abs_tol=1e-6)
274+
assert math.isclose(resized_image[0, 0], 0, abs_tol=1e-6)
275+
assert math.isclose(resized_image[1, 0], resized_image[0, 1], abs_tol=1e-8)
276+
assert math.isclose(resized_image[0, 0], resized_image[2, 2], abs_tol=1e-8)
277+
278+
resized_image = sitkutils.resize(
279+
image=original_image,
280+
new_size=[3, 3],
281+
interpolator=sitk.sitkNearestNeighbor,
282+
anti_aliasing_sigma=[1.0, 0.0],
283+
)
284+
assert resized_image.GetSize() == (3, 3)
285+
assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3))
286+
assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3))
287+
assert math.isclose(resized_image[1, 1], 0.400101, abs_tol=1e-6)
288+
assert math.isclose(resized_image[0, 0], 0, abs_tol=1e-6)
289+
assert resized_image[0, 0] == resized_image[2, 2]
290+
assert resized_image[1, 0] == 0.0
291+
assert resized_image[1, 2] == 0.0

0 commit comments

Comments
 (0)