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
33 changes: 27 additions & 6 deletions src/machinevisiontoolbox/ImageConstants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from spatialmath.base import islistof, isscalar

# from numpy.lib.arraysetops import isin
from machinevisiontoolbox.base import float_image, int_image, name2color
from machinevisiontoolbox.base import DTYPE_ALIASES, float_image, int_image, name2color
from machinevisiontoolbox.base.imageio import convert, idisp, iread, iwrite
from machinevisiontoolbox.Kernel import Kernel
from machinevisiontoolbox.mvtb_types import Dtype
Expand Down Expand Up @@ -110,6 +110,9 @@ def _resolve_pattern_options(
else:
dtype = default_dtype

if isinstance(dtype, str):
dtype = DTYPE_ALIASES.get(dtype, dtype)

if colororder is None and like is not None and like.iscolor:
colororder = like.colororder_str.replace(":", "")

Expand All @@ -121,7 +124,7 @@ def _pattern_image(cls, image: np.ndarray, colororder: str | None):
image = np.repeat(
image[..., np.newaxis], len(cls.colordict(colororder)), axis=2
)
return cls(image, colororder=colororder)
return cls(image, colororder=colororder, dtype=True)


class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
Expand All @@ -145,6 +148,8 @@ def Zeros(
:type colororder: str
:param dtype: NumPy datatype, defaults to 'uint8'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param like: template image supplying default ``size`` and ``colororder``
when those are not given explicitly
:type like: :class:`Image` or None, optional
Expand Down Expand Up @@ -179,7 +184,7 @@ def Zeros(
)

shape = _getshape(cls, None, None, colororder, size)
return cls(np.zeros(shape, dtype=dtype), colororder=colororder)
return cls(np.zeros(shape, dtype=dtype), colororder=colororder, dtype=True)

@classmethod
def Constant(
Expand All @@ -202,6 +207,8 @@ def Constant(
:type colororder: str
:param dtype: NumPy datatype, defaults to 'uint8'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param like: template image supplying default ``size``, ``dtype`` and
``colororder`` when those are not given explicitly
:type like: :class:`Image` or None, optional
Expand Down Expand Up @@ -258,11 +265,11 @@ def Constant(
planes = []
for bg in value:
planes.append(np.full(shape[:2], bg, dtype=dtype))
return cls(np.stack(planes, axis=2), colororder=colororder)
return cls(np.stack(planes, axis=2), colororder=colororder, dtype=True)

else:
# scalar
return cls(np.full(shape, value, dtype=dtype))
return cls(np.full(shape, value, dtype=dtype), dtype=True)

@classmethod
def String(
Expand Down Expand Up @@ -406,6 +413,8 @@ def Random(
:type colororder: str
:param dtype: NumPy datatype, defaults to 'uint8'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param maxval: maximum value for random values, defaults to None
:type maxval: same as ``dtype``, optional
:param pdf: probability density function for pixel values, defaults to None
Expand Down Expand Up @@ -535,7 +544,7 @@ def Random(
else:
raise ValueError("pdf must be a 1D or 2D array")

return cls(im, colororder=colororder)
return cls(im, colororder=colororder, dtype=True)

@classmethod
def Squares(
Expand All @@ -562,6 +571,8 @@ def Squares(
:type bg: int, optional
:param dtype: NumPy datatype, defaults to 'uint8'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down Expand Up @@ -654,6 +665,8 @@ def Circles(
:type bg: int, optional
:param dtype: NumPy datatype, defaults to 'uint8'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down Expand Up @@ -746,6 +759,8 @@ def Ramp(
:type cycles: int, optional
:param dtype: NumPy datatype, defaults to 'float32'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down Expand Up @@ -842,6 +857,8 @@ def Sin(
:type cycles: int, optional
:param dtype: NumPy datatype, defaults to 'float32'
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down Expand Up @@ -922,6 +939,8 @@ def Chequerboard(
:type square: int, optional
:param dtype: image data type, defaults to "uint8"
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down Expand Up @@ -1012,6 +1031,8 @@ def Polygons(
:type shift: int, optional
:param dtype: image data type, defaults to "uint8"
:type dtype: str or NumPy dtype, optional

|dtype_aliases|
:param colororder: color plane names for the output image, defaults to None
:type colororder: str or None, optional
:param like: template image supplying default ``size``, ``dtype`` and
Expand Down
39 changes: 39 additions & 0 deletions tests/test_dtype_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import numpy as np
import pytest
from spatialmath import Polygon2

from machinevisiontoolbox import Image
from machinevisiontoolbox.base.imageio import convert
Expand Down Expand Up @@ -54,3 +55,41 @@ def test_image_constructor(self, dtype_in, expected):
def test_convert(self, dtype_in, expected):
arr = convert(np.ones((2, 3), dtype=np.uint8), dtype=dtype_in)
assert arr.dtype == expected


# ImageConstantsMixin factory methods: a *different* bug from the above --
# they build their raw pixel array with the (now alias-resolved)
# dtype, but historically never forwarded dtype= to the Image
# constructor call, so the constructor's own "no dtype given" auto-detect
# (any float input becomes float32) silently downcast even an explicit,
# already-correct dtype='float64' request. Fixed by passing dtype=True
# ("trust the array I already built") through _pattern_image() and each
# factory's own final constructor call.
_SQUARE = Polygon2([(2, 2), (8, 2), (8, 8), (2, 8)])
FACTORY_CASES = [
("Zeros", lambda dtype: Image.Zeros(size=8, dtype=dtype)),
("Constant_scalar", lambda dtype: Image.Constant(1.0, size=8, dtype=dtype)),
(
"Constant_iterable",
lambda dtype: Image.Constant(
[1.0, 0.5, 0.2], size=8, colororder="RGB", dtype=dtype
),
),
("Random", lambda dtype: Image.Random(size=8, dtype=dtype)),
("Squares", lambda dtype: Image.Squares(1, size=20, dtype=dtype)),
("Circles", lambda dtype: Image.Circles(1, size=20, dtype=dtype)),
("Ramp", lambda dtype: Image.Ramp(size=20, dtype=dtype)),
("Sin", lambda dtype: Image.Sin(size=20, dtype=dtype)),
("Chequerboard", lambda dtype: Image.Chequerboard(size=20, dtype=dtype)),
("Polygons", lambda dtype: Image.Polygons(_SQUARE, size=10, dtype=dtype)),
]
FACTORY_CASE_IDS = [c[0] for c in FACTORY_CASES]


@pytest.mark.parametrize("dtype_in,expected", DTYPE_CASES, ids=DTYPE_CASE_IDS)
@pytest.mark.parametrize("factory_name,factory_fn", FACTORY_CASES, ids=FACTORY_CASE_IDS)
def test_image_constants_factory_respects_explicit_dtype(
factory_name, factory_fn, dtype_in, expected
):
im = factory_fn(dtype_in)
assert im.dtype == expected