Skip to content

Commit 358ba44

Browse files
authored
Merge branch 'main' into fix/metadata-exif-subifd
2 parents c6513e5 + 0371b81 commit 358ba44

12 files changed

Lines changed: 265 additions & 36 deletions

File tree

‎docs/source/conf.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
autosectionlabel_prefix_document = True
109109
rst_epilog = """
110110
.. |RVC3| replace:: `P. Corke, Robotics, Vision & Control for Python, Springer, 2023 <https://link.springer.com/book/10.1007/978-3-031-06469-2>`__
111+
.. |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`.
111112
"""
112113

113114
# tricks to get :class:`Image` links to resolve to the correct place in the RsT docs

‎src/machinevisiontoolbox/BagOfWords.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def __init__(
5858
of image features is assigned labels from the ``.centroids`` any with a
5959
label greater that ``.nstopwords`` is a stop word and can be discarded.
6060
61+
.. note:: Removing stop words can leave a remaining word that never
62+
occurs in any image, giving it an inverse document frequency (idf)
63+
of ``inf`` (``log(N / 0)``). This is harmless -- every place idf is
64+
used to compute a word-frequency vector, the corresponding word
65+
count is also always zero there, so the ``0 * inf`` result is
66+
``nan`` and gets replaced with 0 before the vector is returned.
67+
6168
:reference:
6269
- Video Google: a text retrieval approach to object matching in videos
6370
J.Sivic and A.Zisserman,
@@ -139,7 +146,12 @@ def __init__(
139146
# total number of occurrences of word i
140147
# multiple occurrences in the one image count only as one
141148
ni = (W > 0).sum(axis=1)
142-
idf = np.log(N / ni)
149+
# a word that never occurs in any image (ni=0, possible after
150+
# stopword removal) gives idf=inf here; harmless since nid is
151+
# always 0 at that same position below, so 0*inf->nan, already
152+
# suppressed by the invalid="ignore" there.
153+
with np.errstate(divide="ignore"):
154+
idf = np.log(N / ni)
143155

144156
M = []
145157

‎src/machinevisiontoolbox/Camera.py‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ def plot(
11521152
"""
11531153
Plot 3D camera icon in world view (base method)
11541154
1155-
:param pose: camera pose
1155+
:param pose: camera pose, required when called unbound (see below)
11561156
:type pose: :class:`~spatialmath.pose3d.SE3`
11571157
:param scale: scale factor, defaults to 1
11581158
:type scale: float
@@ -1170,15 +1170,28 @@ def plot(
11701170
:type projection: str, optional
11711171
:param ax: axes to draw in, defaults to current 3D axes
11721172
:type ax: :class:`~matplotlib.Axes3D`, optional
1173+
:raises ValueError: called unbound (eg. ``CentralCamera.plot(...)``)
1174+
without a ``pose``
11731175
:return: axes drawn into
11741176
:rtype: :class:`~matplotlib.Axes3D`
11751177
11761178
Plot a 3D icon representing the pose of a camera into a 3D Matplotlib
11771179
plot. Two icons are supported: the traditional frustum, and a
11781180
simplistic camera comprising a box and cylinder.
11791181
1180-
.. note:: If ``pose`` is not given it defaults to the pose of the
1181-
instance.
1182+
This method can be called two ways:
1183+
1184+
- **Bound**, ``camera.plot(...)`` -- draws an icon for ``camera``.
1185+
``pose`` defaults to the instance's own pose if not given.
1186+
- **Unbound**, ``CentralCamera.plot(pose=..., ...)`` -- called on
1187+
the class itself, with no camera instance involved (``self`` is
1188+
``None``). Useful for drawing icons at a set of poses that don't
1189+
correspond to any single camera instance -- e.g. plotting the
1190+
calibration frames returned by :meth:`images2C`. ``pose`` is
1191+
required in this form.
1192+
1193+
.. note:: If ``pose`` is not given, it defaults to the pose of
1194+
``self`` -- this only applies to the bound form.
11821195
"""
11831196

11841197
# if (fig is None) and (ax is None):
@@ -1195,6 +1208,11 @@ def plot(
11951208
ax = smb.axes_logic(ax, 3, projection=projection)
11961209

11971210
if pose is None:
1211+
if self is None:
1212+
raise ValueError(
1213+
"pose is required when plot() is called unbound, eg. "
1214+
"CentralCamera.plot(pose=...)"
1215+
)
11981216
pose = self.pose
11991217

12001218
# Matplotlib >=3.10 can raise while autoscaling ragged 3D line
@@ -1216,26 +1234,27 @@ def _add_collection3d_compat(collection, *args, **kwargs):
12161234
try:
12171235
# draw camera-like object:
12181236
if shape == "frustum":
1219-
self._plot_frustum(ax, pose, scale, alpha)
1237+
CameraBase._plot_frustum(ax, pose, scale, alpha)
12201238

12211239
elif shape == "camera":
1222-
self._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
1240+
CameraBase._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
12231241

12241242
if frame is True:
1225-
self.pose.plot(
1243+
pose.plot(
12261244
length=scale * 1.5,
12271245
style="line",
12281246
color=color,
12291247
flo=(0.07, 0, -0.01),
12301248
)
12311249
elif frame is not False:
1232-
self.pose.plot(**frame)
1250+
pose.plot(**frame)
12331251
finally:
12341252
ax.add_collection3d = _orig_add_collection3d
12351253

12361254
return ax
12371255

1238-
def _plot_frustum(self, ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
1256+
@staticmethod
1257+
def _plot_frustum(ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
12391258
"""Draw the ``shape="frustum"`` camera icon into ``ax``."""
12401259
# TODO make this kwargs or optional inputs
12411260
# side colors:
@@ -1279,8 +1298,8 @@ def _plot_frustum(self, ax: Axes, pose: SE3, scale: float, alpha: float) -> None
12791298
poly = Poly3DCollection(points, facecolors=["r", "g", "r", "y"], alpha=alpha)
12801299
ax.add_collection3d(poly)
12811300

1301+
@staticmethod
12821302
def _plot_camera_icon(
1283-
self,
12841303
ax: Axes,
12851304
pose: SE3,
12861305
scale: float,

‎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

‎src/machinevisiontoolbox/ImageCore.py‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
# from numpy.lib.arraysetops import isin
2222
from machinevisiontoolbox.base import (
23+
DTYPE_ALIASES,
2324
draw_box,
2425
draw_circle,
2526
draw_labelbox,
@@ -98,8 +99,13 @@ def __init__(
9899
:type copy: bool, optional
99100
:param size: new size for the image, defaults to None
100101
:type size: tuple, optional
101-
:param dtype: data type for image, defaults to same type as ``image``
102-
:type dtype: str or NumPy dtype, optional
102+
:param dtype: data type for image; ``None`` [default] auto-detects (any
103+
floating input becomes ``float32``; integer input becomes the
104+
smallest unsigned/signed integer type that holds all its values);
105+
``True`` keeps ``image``'s own dtype as-is; ``False`` raises
106+
``ValueError``; otherwise a NumPy dtype string (``"uint8"``,
107+
``"float32"``, ...) or NumPy type. |dtype_aliases|
108+
:type dtype: str, NumPy dtype, bool, or None, optional
103109
:param name: name of image, defaults to None
104110
:type name: str, optional
105111
:param id: numeric id of image, typically a sequence number, defaults to None
@@ -381,6 +387,8 @@ def _infer_dtype(image: np.ndarray, dtype: Dtype | bool | None) -> np.dtype | No
381387
raise ValueError("bad dtype argument passed to Image constructor")
382388
else:
383389
# dtype is given, convert to a NumPy dtype
390+
if isinstance(dtype, str):
391+
dtype = DTYPE_ALIASES.get(dtype, dtype)
384392
try:
385393
dtype = np.dtype(dtype)
386394
except TypeError:
@@ -1958,6 +1966,9 @@ def to(self, dtype: Dtype) -> "Image":
19581966
19591967
:param dtype: Numpy data type
19601968
:type dtype: str or NumPy dtype
1969+
1970+
|dtype_aliases|
1971+
19611972
:return: image
19621973
:rtype: :class:`Image`
19631974
@@ -1980,8 +1991,9 @@ def to(self, dtype: Dtype) -> "Image":
19801991
:seealso: :meth:`astype` :meth:`to_int` :meth:`to_float`
19811992
"""
19821993
# convert image to different type, does rescaling
1983-
# as just changes type
1984-
dtype = np.dtype(dtype) # convert to dtype if it's a string
1994+
# as just changes type. array_as() and the constructor both
1995+
# resolve dtype (including the short-name aliases) themselves,
1996+
# so just pass it through unchanged rather than resolving twice.
19851997
return self.__class__(self.array_as(dtype), dtype=dtype)
19861998

19871999
def astype(self, dtype: Dtype) -> "Image":
@@ -1990,6 +2002,9 @@ def astype(self, dtype: Dtype) -> "Image":
19902002
19912003
:param dtype: Numpy data type
19922004
:type dtype: str or NumPy dtype
2005+
2006+
|dtype_aliases|
2007+
19932008
:return: image
19942009
:rtype: :class:`Image`
19952010
@@ -2010,6 +2025,8 @@ def astype(self, dtype: Dtype) -> "Image":
20102025
20112026
:seealso: :meth:`to`
20122027
"""
2028+
if isinstance(dtype, str):
2029+
dtype = DTYPE_ALIASES.get(dtype, dtype)
20132030
return self.__class__(self._A.astype(dtype), dtype=dtype)
20142031

20152032
# ---- NumPy array access ---- #
@@ -2285,7 +2302,9 @@ def array_as(self, dtype: Dtype | None = None) -> np.ndarray:
22852302
Convert Image to NumPy array of specified type
22862303
22872304
:param dtype: data type of the output array
2288-
:type dtype: str or np.dtype
2305+
:type dtype: str or np.dtype, optional
2306+
2307+
|dtype_aliases|
22892308
:return: NumPy array with specified type
22902309
:rtype: ndarray(H,W) or ndarray(H,W,P)
22912310
@@ -2327,6 +2346,8 @@ def array_as(self, dtype: Dtype | None = None) -> np.ndarray:
23272346
if dtype is None:
23282347
return self._A
23292348
else:
2349+
if isinstance(dtype, str):
2350+
dtype = DTYPE_ALIASES.get(dtype, dtype)
23302351
dtype = np.dtype(dtype) # convert to dtype if it's a string
23312352
if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.bool_):
23322353
return int_image(self._A, dtype)

‎src/machinevisiontoolbox/base/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
# types
5757
"int_image",
5858
"float_image",
59+
"DTYPE_ALIASES",
5960
# data
6061
"mvtb_path_to_datafile",
6162
"mvtb_load_data",

0 commit comments

Comments
 (0)