Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/machinevisiontoolbox/ImageCore.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,37 @@
"""Iterate over image planes."""
return self.planes()

def __array__(self, dtype: Dtype | None = None, copy: bool | None = None) -> np.ndarray:

Check warning on line 478 in src/machinevisiontoolbox/ImageCore.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/machinevisiontoolbox/ImageCore.py#L478

'Dtype' may be undefined, or defined from star imports: machinevisiontoolbox.mvtb_types (F405)
"""
Convert to a plain NumPy array

:param dtype: dtype of the returned array, defaults to the image's own dtype
:type dtype: numpy dtype, optional
:param copy: force a copy of the underlying data, defaults to None
:type copy: bool, optional
:return: image pixel data
:rtype: ndarray(H,W) or ndarray(H,W,3)

Implements the NumPy array protocol, used by ``np.asarray(img)`` and by
any NumPy/SciPy/Matplotlib function that isn't ufunc- or array-function-
aware (those instead go through :meth:`__array_ufunc__` /
:meth:`__array_function__`) and calls ``np.asarray()`` internally on
whatever it's given. Without this, such a call would silently coerce an
:class:`Image` into a useless 0-d object array instead of its pixel data.

Returns the same read-only view as the :attr:`array` property unless a
dtype conversion or an explicit copy is requested, either of which
produces a new, independent (writeable) array.

:seealso: :attr:`array`
"""
arr = self.array
if dtype is not None:
arr = arr.astype(dtype)
if copy:
arr = arr.copy()
return arr

def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
"""
Support NumPy ufunc calls on image data.
Expand Down
29 changes: 29 additions & 0 deletions src/machinevisiontoolbox/Kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,35 @@ def T(self) -> "Kernel":
def shape(self) -> tuple[int, int]:
return self.K.shape

@property
def ndim(self) -> int:
return 2

def __array__(self, dtype: Dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""
Convert to a plain NumPy array

:param dtype: dtype of the returned array, defaults to the kernel's own dtype
:type dtype: numpy dtype, optional
:param copy: force a copy of the underlying data, defaults to None
:type copy: bool, optional
:return: kernel weighting matrix
:rtype: ndarray(N,M)

Implements the NumPy array protocol, used by ``np.asarray(K)`` and by
any NumPy/SciPy/Matplotlib function that calls ``np.asarray()``
internally on whatever it's given (e.g. ``scipy.signal.convolve2d``,
``Axes3D.plot_surface``, ``np.linalg.svd``). Without this, such a call
would silently coerce a :class:`Kernel` into a useless 0-d object
array instead of its weighting matrix.
"""
arr = self.K
if dtype is not None:
arr = arr.astype(dtype)
if copy:
arr = arr.copy()
return arr

def print(
self, fmt: str | None = None, separator: str = " ", precision: int = 2
) -> None:
Expand Down
45 changes: 45 additions & 0 deletions tests/test_image_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,51 @@ def test_array_ufunc_out_rejected(self):
):
np.ceil(im, out=im)

def test_array_dunder_asarray(self):
"""Regression test: np.asarray(img) used to silently coerce to a
useless 0-d object array (no __array__ implemented) instead of
exposing the pixel data -- same bug class found in Kernel's
missing __array__."""
im = Image([[1, 2], [3, 4]], dtype="uint8")

arr = np.asarray(im)

self.assertEqual(arr.shape, im.array.shape)
nt.assert_array_equal(arr, im.array)

def test_array_dunder_scipy_interop(self):
"""Regression test: functions that aren't ufunc-/array-function-
aware (e.g. scipy.signal.convolve2d) call np.asarray() internally
and used to receive a useless 0-d object array."""
from scipy.signal import convolve2d

im = Image.Random(size=(10, 10), dtype="float64")

out = convolve2d(im, np.ones((3, 3)))

self.assertEqual(out.ndim, 2)

def test_array_dunder_dtype_conversion(self):
im = Image([[1, 2], [3, 4]], dtype="uint8")

arr = np.asarray(im, dtype="float32")

self.assertEqual(arr.dtype, np.float32)

def test_array_dunder_returns_readonly_view_by_default(self):
im = Image([[1, 2], [3, 4]], dtype="uint8")

arr = np.asarray(im)

self.assertFalse(arr.flags.writeable)

def test_array_dunder_copy_true_returns_writeable(self):
im = Image([[1, 2], [3, 4]], dtype="uint8")

arr = np.array(im, copy=True)

self.assertTrue(arr.flags.writeable)

def test_pixel(self):
im = Image(np.arange(80).reshape((10, 8)), dtype="int64") # 8x10 image
pix = im.pixel(5, 6)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_image_spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ def test_kernel_shape_property(self):
K = self.Kernel.Gauss(sigma=2, h=3)
self.assertEqual(K.shape, (7, 7))

def test_kernel_ndim_property(self):
K = self.Kernel.Gauss(sigma=2, h=3)
self.assertEqual(K.ndim, 2)

def test_kernel_array_protocol(self):
"""Regression test: Kernel objects used to silently coerce to a
useless 0-d object array under np.asarray()/SciPy/Matplotlib calls
that aren't ufunc-aware, instead of exposing the real weight
matrix -- e.g. scipy.signal.convolve2d raised "inputs must both be
2-D arrays" and np.linalg.svd raised "0-dimensional array given"."""
K = self.Kernel.Box(2, normalize=False)
arr = np.asarray(K)
self.assertEqual(arr.shape, K.shape)
nt.assert_array_equal(arr, K.K)

U, s, Vh = np.linalg.svd(K, full_matrices=True)
self.assertEqual(U.shape[0], K.shape[0])

from scipy.signal import convolve2d

out = convolve2d(K, self.Kernel.Box(1, normalize=False))
self.assertEqual(out.ndim, 2)

def test_kernel_box(self):
K = self.Kernel.Box(2)
self.assertEqual(K.shape, (5, 5))
Expand Down