Skip to content

Commit 87bc23f

Browse files
committed
BUG: Keep itk.Image reference in NumPy array views
Prevent the view'ed image from going out of scope by holding a reference in a new class, NDArrayITKBase. The object is kept in the `itk_base` attributed -- the typical `base` attribute NumPy uses for this purpose is not writable. The class is created according to the documentation here: https://numpy.org/doc/stable/user/basics.subclassing.html
1 parent 1f5f9b8 commit 87bc23f

File tree

4 files changed

+53
-48
lines changed

4 files changed

+53
-48
lines changed

Modules/Bridge/NumPy/wrapping/PyBuffer.i.in

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
indexing. This is the reverse of how indices are specified in ITK,
99
i.e. k,j,i versus i,j,k. However C-order indexing is expected by most
1010
algorithms in NumPy / SciPy.
11-
12-
Warning: No copy of the data is performed. Using an array
13-
view after its source image has been deleted can results in corrupt values
14-
or a segfault.
1511
"""
1612

1713
if not HAVE_NUMPY:
@@ -37,11 +33,12 @@
3733
shape = shape[::-1]
3834

3935
pixelType = "@PixelType@"
40-
numpydatatype = _get_numpy_pixelid(pixelType)
36+
numpy_dtype = _get_numpy_pixelid(pixelType)
4137
memview = itkPyBuffer@PyBufferTypes@._GetArrayViewFromImage(image)
42-
ndarrview = numpy.asarray(memview).view(dtype = numpydatatype).reshape(shape).view(numpy.ndarray)
38+
ndarr_view = np.asarray(memview).view(dtype = numpy_dtype).reshape(shape).view(np.ndarray)
39+
itk_view = NDArrayITKBase(ndarr_view, image)
4340

44-
return ndarrview
41+
return itk_view
4542

4643
GetArrayViewFromImage = staticmethod(GetArrayViewFromImage)
4744

@@ -59,7 +56,7 @@
5956
arrayView = itkPyBuffer@PyBufferTypes@.GetArrayViewFromImage(image, keep_axes, update)
6057

6158
# perform deep copy of the image buffer
62-
arr = numpy.array(arrayView, copy=True)
59+
arr = np.array(arrayView, copy=True)
6360

6461
return arr
6562

@@ -77,10 +74,10 @@
7774
C-order indexing, i.e. k,j,i, the image Size will have the dimensions
7875
reversed from the array shape.
7976

80-
Therefore, since the *numpy.transpose* operator on a 2D array simply
77+
Therefore, since the *np.transpose* operator on a 2D array simply
8178
inverts the indexing scheme, the Image representation will be the
8279
same for an array and its transpose. If flipping is desired, see
83-
*numpy.reshape*.
80+
*np.reshape*.
8481
"""
8582

8683
if not HAVE_NUMPY:
@@ -89,7 +86,7 @@
8986
assert ndarr.ndim in ( 2, 3, 4 ), \
9087
"Only arrays of 2, 3 or 4 dimensions are supported."
9188
if not ndarr.flags['C_CONTIGUOUS'] and not ndarr.flags['F_CONTIGUOUS']:
92-
ndarr = numpy.ascontiguousarray(ndarr)
89+
ndarr = np.ascontiguousarray(ndarr)
9390

9491
if ( ndarr.ndim == 3 and is_vector ) or (ndarr.ndim == 4):
9592
if( ndarr.flags['C_CONTIGUOUS'] ):
@@ -120,10 +117,10 @@
120117
C-order indexing, i.e. k,j,i, the image Size will have the dimensions
121118
reversed from the array shape.
122119

123-
Therefore, since the *numpy.transpose* operator on a 2D array simply
120+
Therefore, since the *np.transpose* operator on a 2D array simply
124121
inverts the indexing scheme, the Image representation will be the
125122
same for an array and its transpose. If flipping is desired, see
126-
*numpy.reshape*.
123+
*np.reshape*.
127124
"""
128125

129126
# Create a temporary image view of the array

Modules/Bridge/NumPy/wrapping/PyBuffer.i.init

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@
22

33
HAVE_NUMPY = True
44
try:
5-
import numpy
5+
import numpy as np
6+
class NDArrayITKBase(np.ndarray):
7+
"""A numpy array that provides a view on the data associated with an optional itk "base" object."""
8+
9+
def __new__(cls, input_array, itk_base=None):
10+
obj = np.asarray(input_array).view(cls)
11+
obj.itk_base = itk_base
12+
return obj
13+
14+
def __array_finalize__(self, obj):
15+
if obj is None: return
16+
self.itk_base = getattr(obj, 'itk_base', None)
17+
618
except ImportError:
719
HAVE_NUMPY = False
820

@@ -12,21 +24,21 @@ def _get_numpy_pixelid(itk_Image_type):
1224
if not HAVE_NUMPY:
1325
raise ImportError('Numpy not available.')
1426
# This is a Mapping from numpy array types to itk pixel types.
15-
_np_itk = {"UC":numpy.uint8,
16-
"US":numpy.uint16,
17-
"UI":numpy.uint32,
18-
"UL":numpy.uint64,
19-
"SC":numpy.int8,
20-
"SS":numpy.int16,
21-
"SI":numpy.int32,
22-
"SL":numpy.int64,
23-
"F":numpy.float32,
24-
"D":numpy.float64,
27+
_np_itk = {"UC":np.uint8,
28+
"US":np.uint16,
29+
"UI":np.uint32,
30+
"UL":np.uint64,
31+
"SC":np.int8,
32+
"SS":np.int16,
33+
"SI":np.int32,
34+
"SL":np.int64,
35+
"F":np.float32,
36+
"D":np.float64,
2537
}
2638
import os
2739
if os.name == 'nt':
28-
_np_itk['UL'] = numpy.uint32
29-
_np_itk['SL'] = numpy.int32
40+
_np_itk['UL'] = np.uint32
41+
_np_itk['SL'] = np.int32
3042
try:
3143
return _np_itk[itk_Image_type]
3244
except KeyError as e:

Modules/Bridge/NumPy/wrapping/PyVnlVectorBuffer.i.in

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22
%pythoncode %{
33

44
def GetArrayViewFromVnlVector(vnl_vector):
5-
""" Get a NumPy array view of a VNL vector.
6-
7-
Warning: No copy of the data is performed. Using an array
8-
view after its source vector has been deleted can results in corrupt values
9-
or a segfault.
10-
"""
5+
"""Get a NumPy array view of a VNL vector."""
116

127
if not HAVE_NUMPY:
138
raise ImportError('Numpy not available.')
@@ -16,11 +11,12 @@
1611
shape = [itksize]
1712

1813
pixelType = "@PixelType@"
19-
numpydatatype = _get_numpy_pixelid(pixelType)
14+
numpy_dtype = _get_numpy_pixelid(pixelType)
2015
memview = itkPyVnl@PyVnlTypes@._GetArrayViewFromVnlVector(vnl_vector)
21-
ndarrview = numpy.asarray(memview).view(dtype = numpydatatype).reshape(shape).view(numpy.ndarray)
16+
ndarr_view = np.asarray(memview).view(dtype = numpy_dtype).reshape(shape).view(np.ndarray)
17+
itk_view = NDArrayITKBase(ndarr_view, vnl_vector)
2218

23-
return ndarrview
19+
return itk_view
2420

2521
GetArrayViewFromVnlVector = staticmethod(GetArrayViewFromVnlVector)
2622

@@ -33,7 +29,7 @@
3329
arrayView = itkPyVnl@PyVnlTypes@.GetArrayViewFromVnlVector(vnl_vector)
3430

3531
# perform deep copy of the buffer
36-
return numpy.array(arrayView, copy=True)
32+
return np.array(arrayView, copy=True)
3733

3834
GetArrayFromVnlVector = staticmethod(GetArrayFromVnlVector)
3935

@@ -51,20 +47,15 @@
5147
assert ndarr.ndim == 1 , \
5248
"Only arrays of 1 dimension are supported."
5349

54-
ndarr = numpy.ascontiguousarray(ndarr)
50+
ndarr = np.ascontiguousarray(ndarr)
5551
vec = itkPyVnl@PyVnlTypes@._GetVnlVectorFromArray( ndarr, ndarr.shape)
5652

5753
return vec
5854

5955
GetVnlVectorFromArray = staticmethod(GetVnlVectorFromArray)
6056

6157
def GetArrayViewFromVnlMatrix(vnl_matrix):
62-
""" Get a NumPy array view of a VNL matrix.
63-
64-
Warning: No copy of the data is performed. Using an array
65-
view after its source matrix has been deleted can results in corrupt values
66-
or a segfault.
67-
"""
58+
"""Get a NumPy array view of a VNL matrix."""
6859

6960
if not HAVE_NUMPY:
7061
raise ImportError('Numpy not available.')
@@ -75,11 +66,13 @@
7566
shape = [rows,cols]
7667

7768
pixelType = "@PixelType@"
78-
numpydatatype = _get_numpy_pixelid(pixelType)
69+
numpy_dtype = _get_numpy_pixelid(pixelType)
7970
memview = itkPyVnl@PyVnlTypes@._GetArrayViewFromVnlMatrix(vnl_matrix)
80-
ndarrview = numpy.asarray(memview).view(dtype = numpydatatype).reshape(shape).view(numpy.ndarray)
71+
ndarr_view = np.asarray(memview).view(dtype = numpy_dtype).reshape(shape).view(np.ndarray)
72+
73+
itk_view = NDArrayITKBase(ndarr_view, vnl_matrix)
8174

82-
return ndarrview
75+
return itk_view
8376

8477
GetArrayViewFromVnlMatrix = staticmethod(GetArrayViewFromVnlMatrix)
8578

@@ -92,7 +85,7 @@
9285
arrayView = itkPyVnl@PyVnlTypes@.GetArrayViewFromVnlMatrix(vnl_matrix)
9386

9487
# perform deep copy of the buffer
95-
return numpy.array(arrayView, copy=True)
88+
return np.array(arrayView, copy=True)
9689

9790
GetArrayFromVnlMatrix = staticmethod(GetArrayFromVnlMatrix)
9891

@@ -111,7 +104,7 @@
111104
assert ndarr.ndim == 2 , \
112105
"Only arrays of 2 dimensions are supported."
113106

114-
ndarr = numpy.ascontiguousarray(ndarr)
107+
ndarr = np.ascontiguousarray(ndarr)
115108
mat = itkPyVnl@PyVnlTypes@._GetVnlMatrixFromArray( ndarr, ndarr.shape)
116109

117110
return mat

Modules/Bridge/NumPy/wrapping/test/itkPyBufferTest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def test_NumPyBridge_itkScalarImage(self):
4646
scalarImage.Allocate();
4747

4848
scalarndarr = itk.PyBuffer[ScalarImageType].GetArrayViewFromImage(scalarImage)
49+
self.assertTrue(hasattr(scalarndarr, 'itk_base'))
50+
self.assertTrue(scalarndarr.itk_base, scalarImage)
51+
self.assertTrue(isinstance(scalarndarr, np.ndarray))
4952
convertedscalarImage = itk.PyBuffer[ScalarImageType].GetImageViewFromArray(scalarndarr)
5053

5154
ScalarDiffFilterType = itk.ComparisonImageFilter[ScalarImageType, ScalarImageType]

0 commit comments

Comments
 (0)