Skip to content

Commit f3cfa2b

Browse files
authored
Merge pull request #88 from petercorke/fix/image-constants-dtype-downcast
2 parents f7dfed7 + f7a0dd7 commit f3cfa2b

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

‎src/machinevisiontoolbox/ImageConstants.py‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from spatialmath.base import islistof, isscalar
2424

2525
# from numpy.lib.arraysetops import isin
26-
from machinevisiontoolbox.base import float_image, int_image, name2color
26+
from machinevisiontoolbox.base import DTYPE_ALIASES, float_image, int_image, name2color
2727
from machinevisiontoolbox.base.imageio import convert, idisp, iread, iwrite
2828
from machinevisiontoolbox.Kernel import Kernel
2929
from machinevisiontoolbox.mvtb_types import Dtype
@@ -110,6 +110,9 @@ def _resolve_pattern_options(
110110
else:
111111
dtype = default_dtype
112112

113+
if isinstance(dtype, str):
114+
dtype = DTYPE_ALIASES.get(dtype, dtype)
115+
113116
if colororder is None and like is not None and like.iscolor:
114117
colororder = like.colororder_str.replace(":", "")
115118

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

126129

127130
class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
@@ -145,6 +148,8 @@ def Zeros(
145148
:type colororder: str
146149
:param dtype: NumPy datatype, defaults to 'uint8'
147150
:type dtype: str or NumPy dtype, optional
151+
152+
|dtype_aliases|
148153
:param like: template image supplying default ``size`` and ``colororder``
149154
when those are not given explicitly
150155
:type like: :class:`Image` or None, optional
@@ -179,7 +184,7 @@ def Zeros(
179184
)
180185

181186
shape = _getshape(cls, None, None, colororder, size)
182-
return cls(np.zeros(shape, dtype=dtype), colororder=colororder)
187+
return cls(np.zeros(shape, dtype=dtype), colororder=colororder, dtype=True)
183188

184189
@classmethod
185190
def Constant(
@@ -202,6 +207,8 @@ def Constant(
202207
:type colororder: str
203208
:param dtype: NumPy datatype, defaults to 'uint8'
204209
:type dtype: str or NumPy dtype, optional
210+
211+
|dtype_aliases|
205212
:param like: template image supplying default ``size``, ``dtype`` and
206213
``colororder`` when those are not given explicitly
207214
:type like: :class:`Image` or None, optional
@@ -258,11 +265,11 @@ def Constant(
258265
planes = []
259266
for bg in value:
260267
planes.append(np.full(shape[:2], bg, dtype=dtype))
261-
return cls(np.stack(planes, axis=2), colororder=colororder)
268+
return cls(np.stack(planes, axis=2), colororder=colororder, dtype=True)
262269

263270
else:
264271
# scalar
265-
return cls(np.full(shape, value, dtype=dtype))
272+
return cls(np.full(shape, value, dtype=dtype), dtype=True)
266273

267274
@classmethod
268275
def String(
@@ -406,6 +413,8 @@ def Random(
406413
:type colororder: str
407414
:param dtype: NumPy datatype, defaults to 'uint8'
408415
:type dtype: str or NumPy dtype, optional
416+
417+
|dtype_aliases|
409418
:param maxval: maximum value for random values, defaults to None
410419
:type maxval: same as ``dtype``, optional
411420
:param pdf: probability density function for pixel values, defaults to None
@@ -535,7 +544,7 @@ def Random(
535544
else:
536545
raise ValueError("pdf must be a 1D or 2D array")
537546

538-
return cls(im, colororder=colororder)
547+
return cls(im, colororder=colororder, dtype=True)
539548

540549
@classmethod
541550
def Squares(
@@ -562,6 +571,8 @@ def Squares(
562571
:type bg: int, optional
563572
:param dtype: NumPy datatype, defaults to 'uint8'
564573
:type dtype: str or NumPy dtype, optional
574+
575+
|dtype_aliases|
565576
:param colororder: color plane names for the output image, defaults to None
566577
:type colororder: str or None, optional
567578
:param like: template image supplying default ``size``, ``dtype`` and
@@ -654,6 +665,8 @@ def Circles(
654665
:type bg: int, optional
655666
:param dtype: NumPy datatype, defaults to 'uint8'
656667
:type dtype: str or NumPy dtype, optional
668+
669+
|dtype_aliases|
657670
:param colororder: color plane names for the output image, defaults to None
658671
:type colororder: str or None, optional
659672
:param like: template image supplying default ``size``, ``dtype`` and
@@ -746,6 +759,8 @@ def Ramp(
746759
:type cycles: int, optional
747760
:param dtype: NumPy datatype, defaults to 'float32'
748761
:type dtype: str or NumPy dtype, optional
762+
763+
|dtype_aliases|
749764
:param colororder: color plane names for the output image, defaults to None
750765
:type colororder: str or None, optional
751766
:param like: template image supplying default ``size``, ``dtype`` and
@@ -842,6 +857,8 @@ def Sin(
842857
:type cycles: int, optional
843858
:param dtype: NumPy datatype, defaults to 'float32'
844859
:type dtype: str or NumPy dtype, optional
860+
861+
|dtype_aliases|
845862
:param colororder: color plane names for the output image, defaults to None
846863
:type colororder: str or None, optional
847864
:param like: template image supplying default ``size``, ``dtype`` and
@@ -922,6 +939,8 @@ def Chequerboard(
922939
:type square: int, optional
923940
:param dtype: image data type, defaults to "uint8"
924941
:type dtype: str or NumPy dtype, optional
942+
943+
|dtype_aliases|
925944
:param colororder: color plane names for the output image, defaults to None
926945
:type colororder: str or None, optional
927946
:param like: template image supplying default ``size``, ``dtype`` and
@@ -1012,6 +1031,8 @@ def Polygons(
10121031
:type shift: int, optional
10131032
:param dtype: image data type, defaults to "uint8"
10141033
:type dtype: str or NumPy dtype, optional
1034+
1035+
|dtype_aliases|
10151036
:param colororder: color plane names for the output image, defaults to None
10161037
:type colororder: str or None, optional
10171038
:param like: template image supplying default ``size``, ``dtype`` and

‎tests/test_dtype_resolution.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import numpy as np
2323
import pytest
24+
from spatialmath import Polygon2
2425

2526
from machinevisiontoolbox import Image
2627
from machinevisiontoolbox.base.imageio import convert
@@ -54,3 +55,41 @@ def test_image_constructor(self, dtype_in, expected):
5455
def test_convert(self, dtype_in, expected):
5556
arr = convert(np.ones((2, 3), dtype=np.uint8), dtype=dtype_in)
5657
assert arr.dtype == expected
58+
59+
60+
# ImageConstantsMixin factory methods: a *different* bug from the above --
61+
# they build their raw pixel array with the (now alias-resolved)
62+
# dtype, but historically never forwarded dtype= to the Image
63+
# constructor call, so the constructor's own "no dtype given" auto-detect
64+
# (any float input becomes float32) silently downcast even an explicit,
65+
# already-correct dtype='float64' request. Fixed by passing dtype=True
66+
# ("trust the array I already built") through _pattern_image() and each
67+
# factory's own final constructor call.
68+
_SQUARE = Polygon2([(2, 2), (8, 2), (8, 8), (2, 8)])
69+
FACTORY_CASES = [
70+
("Zeros", lambda dtype: Image.Zeros(size=8, dtype=dtype)),
71+
("Constant_scalar", lambda dtype: Image.Constant(1.0, size=8, dtype=dtype)),
72+
(
73+
"Constant_iterable",
74+
lambda dtype: Image.Constant(
75+
[1.0, 0.5, 0.2], size=8, colororder="RGB", dtype=dtype
76+
),
77+
),
78+
("Random", lambda dtype: Image.Random(size=8, dtype=dtype)),
79+
("Squares", lambda dtype: Image.Squares(1, size=20, dtype=dtype)),
80+
("Circles", lambda dtype: Image.Circles(1, size=20, dtype=dtype)),
81+
("Ramp", lambda dtype: Image.Ramp(size=20, dtype=dtype)),
82+
("Sin", lambda dtype: Image.Sin(size=20, dtype=dtype)),
83+
("Chequerboard", lambda dtype: Image.Chequerboard(size=20, dtype=dtype)),
84+
("Polygons", lambda dtype: Image.Polygons(_SQUARE, size=10, dtype=dtype)),
85+
]
86+
FACTORY_CASE_IDS = [c[0] for c in FACTORY_CASES]
87+
88+
89+
@pytest.mark.parametrize("dtype_in,expected", DTYPE_CASES, ids=DTYPE_CASE_IDS)
90+
@pytest.mark.parametrize("factory_name,factory_fn", FACTORY_CASES, ids=FACTORY_CASE_IDS)
91+
def test_image_constants_factory_respects_explicit_dtype(
92+
factory_name, factory_fn, dtype_in, expected
93+
):
94+
im = factory_fn(dtype_in)
95+
assert im.dtype == expected

0 commit comments

Comments
 (0)