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
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
autosectionlabel_prefix_document = True
rst_epilog = """
.. |RVC3| replace:: `P. Corke, Robotics, Vision & Control for Python, Springer, 2023 <https://link.springer.com/book/10.1007/978-3-031-06469-2>`__
.. |dtype_aliases| replace:: The short-name aliases ``'int'``, ``'float'``, ``'double'``, ``'half'`` are also accepted and resolve to the Toolbox's own defaults (``uint8``, ``float32``, ``float64``, ``float16`` respectively) -- this matters because NumPy's own ``np.dtype('float')`` resolves to ``float64``, not ``float32``. See :data:`~machinevisiontoolbox.base.types.DTYPE_ALIASES`.
"""

# tricks to get :class:`Image` links to resolve to the correct place in the RsT docs
Expand Down
12 changes: 10 additions & 2 deletions src/machinevisiontoolbox/ImageCore.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

# from numpy.lib.arraysetops import isin
from machinevisiontoolbox.base import (
DTYPE_ALIASES,
draw_box,
draw_circle,
draw_labelbox,
Expand Down Expand Up @@ -98,8 +99,13 @@ def __init__(
:type copy: bool, optional
:param size: new size for the image, defaults to None
:type size: tuple, optional
:param dtype: data type for image, defaults to same type as ``image``
:type dtype: str or NumPy dtype, optional
:param dtype: data type for image; ``None`` [default] auto-detects (any
floating input becomes ``float32``; integer input becomes the
smallest unsigned/signed integer type that holds all its values);
``True`` keeps ``image``'s own dtype as-is; ``False`` raises
``ValueError``; otherwise a NumPy dtype string (``"uint8"``,
``"float32"``, ...) or NumPy type. |dtype_aliases|
:type dtype: str, NumPy dtype, bool, or None, optional
:param name: name of image, defaults to None
:type name: str, optional
:param id: numeric id of image, typically a sequence number, defaults to None
Expand Down Expand Up @@ -381,6 +387,8 @@ def _infer_dtype(image: np.ndarray, dtype: Dtype | bool | None) -> np.dtype | No
raise ValueError("bad dtype argument passed to Image constructor")
else:
# dtype is given, convert to a NumPy dtype
if isinstance(dtype, str):
dtype = DTYPE_ALIASES.get(dtype, dtype)
try:
dtype = np.dtype(dtype)
except TypeError:
Expand Down
1 change: 1 addition & 0 deletions src/machinevisiontoolbox/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
# types
"int_image",
"float_image",
"DTYPE_ALIASES",
# data
"mvtb_path_to_datafile",
"mvtb_load_data",
Expand Down
13 changes: 3 additions & 10 deletions src/machinevisiontoolbox/base/imageio.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from machinevisiontoolbox.base.color import colorspace_convert, gamma_decode
from machinevisiontoolbox.base.data import mvtb_path_to_datafile
from machinevisiontoolbox.base.types import float_image, int_image
from machinevisiontoolbox.base.types import DTYPE_ALIASES, float_image, int_image

try:
import pyclip
Expand Down Expand Up @@ -1176,7 +1176,7 @@ def convert(
:type grey: bool or 'ITU601' [default] or 'ITU709'
:param gray: synonym for ``grey``
:param dtype: a NumPy dtype string such as ``"uint8"``, ``"int16"``, ``"float32"`` or
a NumPy type like ``np.uint8``.
a NumPy type like ``np.uint8``. |dtype_aliases|
:type dtype: str
:param rgb: force color image to RGB order, otherwise BGR
:type rgb: bool, optional
Expand Down Expand Up @@ -1250,17 +1250,10 @@ def convert(
if mono and len(image.shape) == 3:
image = colorspace_convert(image, colororder, "grey")

dtype_alias = {
"int": "uint8",
"float": "float32",
"double": "float64",
"half": "float16",
}

if dtype is not None:
# default types
if isinstance(dtype, str):
dtype = dtype_alias.get(dtype, dtype)
dtype = DTYPE_ALIASES.get(dtype, dtype)

if "int" in str(dtype):
image = int_image(image, intclass=dtype, maxintval=maxintval)
Expand Down
14 changes: 14 additions & 0 deletions src/machinevisiontoolbox/base/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@

from machinevisiontoolbox.mvtb_types import Dtype

#: Short-name aliases for dtype strings accepted throughout the Toolbox
#: (e.g. Image(..., dtype='float'), VideoFile(..., dtype='float')).
#: NumPy's own np.dtype('float') resolves to float64, not float32 -- these
#: aliases exist specifically so 'float'/'int' mean the Toolbox's own
#: default single-precision/8-bit types, not NumPy's C-double/C-long
#: defaults. Single source of truth: resolve any dtype string through this
#: table (dtype_alias.get(dtype, dtype)) before passing it to np.dtype().
DTYPE_ALIASES: dict[str, str] = {
"int": "uint8",
"float": "float32",
"double": "float64",
"half": "float16",
}


def int_image(
image: np.ndarray, intclass: Dtype = "uint8", maxintval: int | None = None
Expand Down
56 changes: 56 additions & 0 deletions tests/test_dtype_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Consolidated dtype-resolution consistency tests.

Multiple entry points resolve a user-supplied dtype string into an actual
NumPy dtype: the Image constructor (via _infer_dtype), convert(), and (once
fixed on their own branches) Image.to()/.array_as()/.astype() and the
ImageConstantsMixin factory methods (Zeros, Constant, Random, ...). All of
them are supposed to honour the same short-name aliases (DTYPE_ALIASES:
'int'->uint8, 'float'->float32, 'double'->float64, 'half'->float16) plus
pass explicit NumPy dtype strings through unchanged.

Three independent, inconsistent implementations of this resolution existed
at once (found 2026-08 while working through RVC3-python's chap11.ipynb --
VideoFile(..., mono=True, dtype='float') produced float64 frames instead of
float32, because Image.__init__'s own dtype resolution didn't share
convert()'s alias table). This module pins every entry point against the
same shared matrix of cases so that kind of drift can't happen silently
again -- add a test method to the relevant class below rather than a new
one-off test elsewhere when another entry point is fixed.
"""

import numpy as np
import pytest

from machinevisiontoolbox import Image
from machinevisiontoolbox.base.imageio import convert

# (dtype spec passed in, expected resolved np.dtype)
DTYPE_CASES = [
# short-name aliases (DTYPE_ALIASES) -- NumPy's own np.dtype(...) would
# resolve 'float' to float64 and 'int' to platform int, not these.
("int", np.dtype("uint8")),
("float", np.dtype("float32")),
("double", np.dtype("float64")),
("half", np.dtype("float16")),
# explicit NumPy dtype strings, not in DTYPE_ALIASES -- must pass
# through unchanged, not be (mis)matched against an alias.
("uint8", np.dtype("uint8")),
("int16", np.dtype("int16")),
("float32", np.dtype("float32")),
("float64", np.dtype("float64")),
]
DTYPE_CASE_IDS = [c[0] for c in DTYPE_CASES]


@pytest.mark.parametrize("dtype_in,expected", DTYPE_CASES, ids=DTYPE_CASE_IDS)
class TestDtypeResolutionConsistency:
"""Every entry point below must resolve the same dtype_in the same way."""

def test_image_constructor(self, dtype_in, expected):
im = Image(np.ones((2, 3), dtype=np.uint8), dtype=dtype_in)
assert im.dtype == expected

Check warning on line 52 in tests/test_dtype_resolution.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_dtype_resolution.py#L52

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

def test_convert(self, dtype_in, expected):
arr = convert(np.ones((2, 3), dtype=np.uint8), dtype=dtype_in)
assert arr.dtype == expected

Check warning on line 56 in tests/test_dtype_resolution.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tests/test_dtype_resolution.py#L56

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.