machinevision-toolbox-python 2.3.0__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {machinevision_toolbox_python-2.3.0.dist-info → machinevision_toolbox_python-2.4.1.dist-info}/METADATA +8 -6
- {machinevision_toolbox_python-2.3.0.dist-info → machinevision_toolbox_python-2.4.1.dist-info}/RECORD +24 -22
- {machinevision_toolbox_python-2.3.0.dist-info → machinevision_toolbox_python-2.4.1.dist-info}/WHEEL +1 -1
- {machinevision_toolbox_python-2.3.0.dist-info → machinevision_toolbox_python-2.4.1.dist-info}/entry_points.txt +0 -1
- machinevisiontoolbox/BagOfWords.py +13 -1
- machinevisiontoolbox/BundleAdjust.py +983 -980
- machinevisiontoolbox/Camera.py +101 -18
- machinevisiontoolbox/ImageConstants.py +27 -6
- machinevisiontoolbox/ImageCore.py +69 -8
- machinevisiontoolbox/ImageFiducials.py +16 -8
- machinevisiontoolbox/ImageIO.py +8 -0
- machinevisiontoolbox/ImagePointFeatures.py +13 -2
- machinevisiontoolbox/ImageWholeFeatures.py +18 -5
- machinevisiontoolbox/Kernel.py +29 -0
- machinevisiontoolbox/PointCloud.py +17 -4
- machinevisiontoolbox/Sources.py +18 -9
- machinevisiontoolbox/base/__init__.py +1 -0
- machinevisiontoolbox/base/color.py +42 -16
- machinevisiontoolbox/base/imageio.py +541 -443
- machinevisiontoolbox/base/types.py +14 -0
- machinevisiontoolbox/bin/mvtbtool.py +2 -1
- machinevisiontoolbox/blocks/Icons/README.md +28 -0
- machinevisiontoolbox/blocks/Icons/make_icons.py +111 -0
- {machinevision_toolbox_python-2.3.0.dist-info → machinevision_toolbox_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
machinevisiontoolbox/Camera.py
CHANGED
|
@@ -350,7 +350,8 @@ class CameraBase(ABC):
|
|
|
350
350
|
|
|
351
351
|
:seealso: :meth:`nv` :meth:`width` :meth:`imagesize`
|
|
352
352
|
"""
|
|
353
|
-
|
|
353
|
+
if self._imagesize is None:
|
|
354
|
+
raise ValueError("imagesize not set")
|
|
354
355
|
return self._imagesize[0]
|
|
355
356
|
|
|
356
357
|
@property
|
|
@@ -370,7 +371,8 @@ class CameraBase(ABC):
|
|
|
370
371
|
|
|
371
372
|
:seealso: :meth:`nu` :meth:`height` :meth:`imagesize`
|
|
372
373
|
"""
|
|
373
|
-
|
|
374
|
+
if self._imagesize is None:
|
|
375
|
+
raise ValueError("imagesize not set")
|
|
374
376
|
return self._imagesize[1]
|
|
375
377
|
|
|
376
378
|
@property
|
|
@@ -390,7 +392,8 @@ class CameraBase(ABC):
|
|
|
390
392
|
|
|
391
393
|
:seealso: :meth:`nu` :meth:`height`
|
|
392
394
|
"""
|
|
393
|
-
|
|
395
|
+
if self._imagesize is None:
|
|
396
|
+
raise ValueError("imagesize not set")
|
|
394
397
|
return self._imagesize[0]
|
|
395
398
|
|
|
396
399
|
@property
|
|
@@ -410,7 +413,8 @@ class CameraBase(ABC):
|
|
|
410
413
|
|
|
411
414
|
:seealso: :meth:`nv` :meth:`width`
|
|
412
415
|
"""
|
|
413
|
-
|
|
416
|
+
if self._imagesize is None:
|
|
417
|
+
raise ValueError("imagesize not set")
|
|
414
418
|
return self._imagesize[1]
|
|
415
419
|
|
|
416
420
|
@property
|
|
@@ -1152,7 +1156,7 @@ class CameraBase(ABC):
|
|
|
1152
1156
|
"""
|
|
1153
1157
|
Plot 3D camera icon in world view (base method)
|
|
1154
1158
|
|
|
1155
|
-
:param pose: camera pose
|
|
1159
|
+
:param pose: camera pose, required when called unbound (see below)
|
|
1156
1160
|
:type pose: :class:`~spatialmath.pose3d.SE3`
|
|
1157
1161
|
:param scale: scale factor, defaults to 1
|
|
1158
1162
|
:type scale: float
|
|
@@ -1170,6 +1174,8 @@ class CameraBase(ABC):
|
|
|
1170
1174
|
:type projection: str, optional
|
|
1171
1175
|
:param ax: axes to draw in, defaults to current 3D axes
|
|
1172
1176
|
:type ax: :class:`~matplotlib.Axes3D`, optional
|
|
1177
|
+
:raises ValueError: called unbound (eg. ``CentralCamera.plot(...)``)
|
|
1178
|
+
without a ``pose``
|
|
1173
1179
|
:return: axes drawn into
|
|
1174
1180
|
:rtype: :class:`~matplotlib.Axes3D`
|
|
1175
1181
|
|
|
@@ -1177,8 +1183,19 @@ class CameraBase(ABC):
|
|
|
1177
1183
|
plot. Two icons are supported: the traditional frustum, and a
|
|
1178
1184
|
simplistic camera comprising a box and cylinder.
|
|
1179
1185
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1186
|
+
This method can be called two ways:
|
|
1187
|
+
|
|
1188
|
+
- **Bound**, ``camera.plot(...)`` -- draws an icon for ``camera``.
|
|
1189
|
+
``pose`` defaults to the instance's own pose if not given.
|
|
1190
|
+
- **Unbound**, ``CentralCamera.plot(pose=..., ...)`` -- called on
|
|
1191
|
+
the class itself, with no camera instance involved (``self`` is
|
|
1192
|
+
``None``). Useful for drawing icons at a set of poses that don't
|
|
1193
|
+
correspond to any single camera instance -- e.g. plotting the
|
|
1194
|
+
calibration frames returned by :meth:`images2C`. ``pose`` is
|
|
1195
|
+
required in this form.
|
|
1196
|
+
|
|
1197
|
+
.. note:: If ``pose`` is not given, it defaults to the pose of
|
|
1198
|
+
``self`` -- this only applies to the bound form.
|
|
1182
1199
|
"""
|
|
1183
1200
|
|
|
1184
1201
|
# if (fig is None) and (ax is None):
|
|
@@ -1195,6 +1212,11 @@ class CameraBase(ABC):
|
|
|
1195
1212
|
ax = smb.axes_logic(ax, 3, projection=projection)
|
|
1196
1213
|
|
|
1197
1214
|
if pose is None:
|
|
1215
|
+
if self is None:
|
|
1216
|
+
raise ValueError(
|
|
1217
|
+
"pose is required when plot() is called unbound, eg. "
|
|
1218
|
+
"CentralCamera.plot(pose=...)"
|
|
1219
|
+
)
|
|
1198
1220
|
pose = self.pose
|
|
1199
1221
|
|
|
1200
1222
|
# Matplotlib >=3.10 can raise while autoscaling ragged 3D line
|
|
@@ -1216,26 +1238,27 @@ class CameraBase(ABC):
|
|
|
1216
1238
|
try:
|
|
1217
1239
|
# draw camera-like object:
|
|
1218
1240
|
if shape == "frustum":
|
|
1219
|
-
|
|
1241
|
+
CameraBase._plot_frustum(ax, pose, scale, alpha)
|
|
1220
1242
|
|
|
1221
1243
|
elif shape == "camera":
|
|
1222
|
-
|
|
1244
|
+
CameraBase._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
|
|
1223
1245
|
|
|
1224
1246
|
if frame is True:
|
|
1225
|
-
|
|
1247
|
+
pose.plot(
|
|
1226
1248
|
length=scale * 1.5,
|
|
1227
1249
|
style="line",
|
|
1228
1250
|
color=color,
|
|
1229
1251
|
flo=(0.07, 0, -0.01),
|
|
1230
1252
|
)
|
|
1231
1253
|
elif frame is not False:
|
|
1232
|
-
|
|
1254
|
+
pose.plot(**frame)
|
|
1233
1255
|
finally:
|
|
1234
1256
|
ax.add_collection3d = _orig_add_collection3d
|
|
1235
1257
|
|
|
1236
1258
|
return ax
|
|
1237
1259
|
|
|
1238
|
-
|
|
1260
|
+
@staticmethod
|
|
1261
|
+
def _plot_frustum(ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
|
|
1239
1262
|
"""Draw the ``shape="frustum"`` camera icon into ``ax``."""
|
|
1240
1263
|
# TODO make this kwargs or optional inputs
|
|
1241
1264
|
# side colors:
|
|
@@ -1279,8 +1302,8 @@ class CameraBase(ABC):
|
|
|
1279
1302
|
poly = Poly3DCollection(points, facecolors=["r", "g", "r", "y"], alpha=alpha)
|
|
1280
1303
|
ax.add_collection3d(poly)
|
|
1281
1304
|
|
|
1305
|
+
@staticmethod
|
|
1282
1306
|
def _plot_camera_icon(
|
|
1283
|
-
self,
|
|
1284
1307
|
ax: Axes,
|
|
1285
1308
|
pose: SE3,
|
|
1286
1309
|
scale: float,
|
|
@@ -2684,8 +2707,41 @@ class CentralCamera(CameraBase):
|
|
|
2684
2707
|
:type p2: ndarray(2,N)
|
|
2685
2708
|
:param method: algorithm '7p', '8p' [default], 'ransac', 'lmeds'
|
|
2686
2709
|
:type method: str, optional
|
|
2687
|
-
:param
|
|
2688
|
-
|
|
2710
|
+
:param residual: also return the reprojection residual, defaults to True
|
|
2711
|
+
:type residual: bool, optional
|
|
2712
|
+
:param seed: seed the RNG used by 'ransac'/'lmeds' for repeatable
|
|
2713
|
+
results, defaults to None (not seeded)
|
|
2714
|
+
:type seed: int, optional
|
|
2715
|
+
:param kwargs: passed through to ``cv2.findFundamentalMat``.
|
|
2716
|
+
``ransacReprojThreshold`` (float, defaults to 3.0) applies only
|
|
2717
|
+
when ``method='ransac'`` -- max distance in pixels from a point
|
|
2718
|
+
to its epipolar line for the point to still count as an inlier;
|
|
2719
|
+
``method='lmeds'`` ignores it entirely (verified empirically --
|
|
2720
|
+
identical result regardless of value). ``confidence`` (float in
|
|
2721
|
+
(0,1), defaults to 0.99) and ``maxIters`` (int, defaults to
|
|
2722
|
+
1000) apply to either ``method='ransac'`` or ``method='lmeds'``
|
|
2723
|
+
-- ``method`` only ever selects one algorithm at a time (a
|
|
2724
|
+
combined "ransac+lmeds" method isn't a real OpenCV feature;
|
|
2725
|
+
verified empirically that OR-ing the two method flags together
|
|
2726
|
+
doesn't error, but silently reduces to plain LMedS -- an
|
|
2727
|
+
implementation accident, not a documented hybrid mode).
|
|
2728
|
+
Defaults verified empirically 2026-08-16 (identical inlier
|
|
2729
|
+
masks with/without them explicit, same RNG seed) -- not
|
|
2730
|
+
otherwise documented by the ``cv2`` Python bindings. Same
|
|
2731
|
+
parameter set and defaults on OpenCV 4.10 and 5.0.
|
|
2732
|
+
|
|
2733
|
+
.. warning:: ``maxIters`` only works when ``ransacReprojThreshold``
|
|
2734
|
+
and ``confidence`` are *also* given explicitly -- passing
|
|
2735
|
+
only ``maxIters=`` hits a ``cv2.findFundamentalMat``
|
|
2736
|
+
overload-resolution failure (a confusing low-level OpenCV
|
|
2737
|
+
error, not a clear Python one), since the 3-argument
|
|
2738
|
+
overload this method otherwise uses has no ``maxIters``
|
|
2739
|
+
parameter at all.
|
|
2740
|
+
:type kwargs: dict, optional
|
|
2741
|
+
:raises ValueError: fewer than the minimum number of point
|
|
2742
|
+
correspondences for ``method`` (7 for '7p', 8 otherwise), or
|
|
2743
|
+
the points are too degenerate (eg. coplanar, collinear) for
|
|
2744
|
+
``cv2.findFundamentalMat`` to estimate F from
|
|
2689
2745
|
:return: fundamental matrix and residual
|
|
2690
2746
|
:rtype: ndarray(3,3), float
|
|
2691
2747
|
|
|
@@ -2719,13 +2775,40 @@ class CentralCamera(CameraBase):
|
|
|
2719
2775
|
"ransac": cv2.FM_RANSAC,
|
|
2720
2776
|
"lmeds": cv2.FM_LMEDS,
|
|
2721
2777
|
}
|
|
2778
|
+
min_points = 7 if method == "7p" else 8
|
|
2779
|
+
if p1.shape[1] < min_points:
|
|
2780
|
+
raise ValueError(
|
|
2781
|
+
f"points2F() needs at least {min_points} point correspondences "
|
|
2782
|
+
f"for method={method!r}, got {p1.shape[1]}"
|
|
2783
|
+
)
|
|
2722
2784
|
|
|
2723
2785
|
if seed is not None:
|
|
2724
2786
|
cv2.setRNGSeed(seed)
|
|
2725
2787
|
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2788
|
+
try:
|
|
2789
|
+
F, mask = cv2.findFundamentalMat(
|
|
2790
|
+
points1=p1.T, points2=p2.T, method=points2F_dict[method], **kwargs
|
|
2791
|
+
)
|
|
2792
|
+
except cv2.error as e:
|
|
2793
|
+
# on some degenerate point configurations cv2.findFundamentalMat
|
|
2794
|
+
# raises a raw C++ assertion (eg. a Mat rowRange error) instead
|
|
2795
|
+
# of returning None -- same underlying cause as the F is None
|
|
2796
|
+
# case below, just surfaced differently by OpenCV
|
|
2797
|
+
raise ValueError(
|
|
2798
|
+
f"cv2.findFundamentalMat could not estimate F from "
|
|
2799
|
+
f"{p1.shape[1]} point correspondences using method={method!r} "
|
|
2800
|
+
f"-- points are likely too degenerate (eg. coplanar, collinear): {e}"
|
|
2801
|
+
) from e
|
|
2802
|
+
|
|
2803
|
+
if F is None or mask is None:
|
|
2804
|
+
# already know p1.shape[1] >= min_points (checked above), so a
|
|
2805
|
+
# failure here means the points themselves are degenerate (eg.
|
|
2806
|
+
# coplanar, collinear), not a raw-count problem
|
|
2807
|
+
raise ValueError(
|
|
2808
|
+
f"cv2.findFundamentalMat could not estimate F from "
|
|
2809
|
+
f"{p1.shape[1]} point correspondences using method={method!r} "
|
|
2810
|
+
"-- points are likely too degenerate (eg. coplanar, collinear)"
|
|
2811
|
+
)
|
|
2729
2812
|
|
|
2730
2813
|
mask = mask.ravel().astype(bool)
|
|
2731
2814
|
|
|
@@ -23,7 +23,7 @@ from spatialmath import base as smb
|
|
|
23
23
|
from spatialmath.base import islistof, isscalar
|
|
24
24
|
|
|
25
25
|
# 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
|
|
27
27
|
from machinevisiontoolbox.base.imageio import convert, idisp, iread, iwrite
|
|
28
28
|
from machinevisiontoolbox.Kernel import Kernel
|
|
29
29
|
from machinevisiontoolbox.mvtb_types import Dtype
|
|
@@ -110,6 +110,9 @@ def _resolve_pattern_options(
|
|
|
110
110
|
else:
|
|
111
111
|
dtype = default_dtype
|
|
112
112
|
|
|
113
|
+
if isinstance(dtype, str):
|
|
114
|
+
dtype = DTYPE_ALIASES.get(dtype, dtype)
|
|
115
|
+
|
|
113
116
|
if colororder is None and like is not None and like.iscolor:
|
|
114
117
|
colororder = like.colororder_str.replace(":", "")
|
|
115
118
|
|
|
@@ -121,7 +124,7 @@ def _pattern_image(cls, image: np.ndarray, colororder: str | None):
|
|
|
121
124
|
image = np.repeat(
|
|
122
125
|
image[..., np.newaxis], len(cls.colordict(colororder)), axis=2
|
|
123
126
|
)
|
|
124
|
-
return cls(image, colororder=colororder)
|
|
127
|
+
return cls(image, colororder=colororder, dtype=True)
|
|
125
128
|
|
|
126
129
|
|
|
127
130
|
class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
@@ -145,6 +148,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
145
148
|
:type colororder: str
|
|
146
149
|
:param dtype: NumPy datatype, defaults to 'uint8'
|
|
147
150
|
:type dtype: str or NumPy dtype, optional
|
|
151
|
+
|
|
152
|
+
|dtype_aliases|
|
|
148
153
|
:param like: template image supplying default ``size`` and ``colororder``
|
|
149
154
|
when those are not given explicitly
|
|
150
155
|
:type like: :class:`Image` or None, optional
|
|
@@ -179,7 +184,7 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
179
184
|
)
|
|
180
185
|
|
|
181
186
|
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)
|
|
183
188
|
|
|
184
189
|
@classmethod
|
|
185
190
|
def Constant(
|
|
@@ -202,6 +207,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
202
207
|
:type colororder: str
|
|
203
208
|
:param dtype: NumPy datatype, defaults to 'uint8'
|
|
204
209
|
:type dtype: str or NumPy dtype, optional
|
|
210
|
+
|
|
211
|
+
|dtype_aliases|
|
|
205
212
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
206
213
|
``colororder`` when those are not given explicitly
|
|
207
214
|
:type like: :class:`Image` or None, optional
|
|
@@ -258,11 +265,11 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
258
265
|
planes = []
|
|
259
266
|
for bg in value:
|
|
260
267
|
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)
|
|
262
269
|
|
|
263
270
|
else:
|
|
264
271
|
# scalar
|
|
265
|
-
return cls(np.full(shape, value, dtype=dtype))
|
|
272
|
+
return cls(np.full(shape, value, dtype=dtype), dtype=True)
|
|
266
273
|
|
|
267
274
|
@classmethod
|
|
268
275
|
def String(
|
|
@@ -406,6 +413,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
406
413
|
:type colororder: str
|
|
407
414
|
:param dtype: NumPy datatype, defaults to 'uint8'
|
|
408
415
|
:type dtype: str or NumPy dtype, optional
|
|
416
|
+
|
|
417
|
+
|dtype_aliases|
|
|
409
418
|
:param maxval: maximum value for random values, defaults to None
|
|
410
419
|
:type maxval: same as ``dtype``, optional
|
|
411
420
|
:param pdf: probability density function for pixel values, defaults to None
|
|
@@ -535,7 +544,7 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
535
544
|
else:
|
|
536
545
|
raise ValueError("pdf must be a 1D or 2D array")
|
|
537
546
|
|
|
538
|
-
return cls(im, colororder=colororder)
|
|
547
|
+
return cls(im, colororder=colororder, dtype=True)
|
|
539
548
|
|
|
540
549
|
@classmethod
|
|
541
550
|
def Squares(
|
|
@@ -562,6 +571,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
562
571
|
:type bg: int, optional
|
|
563
572
|
:param dtype: NumPy datatype, defaults to 'uint8'
|
|
564
573
|
:type dtype: str or NumPy dtype, optional
|
|
574
|
+
|
|
575
|
+
|dtype_aliases|
|
|
565
576
|
:param colororder: color plane names for the output image, defaults to None
|
|
566
577
|
:type colororder: str or None, optional
|
|
567
578
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -654,6 +665,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
654
665
|
:type bg: int, optional
|
|
655
666
|
:param dtype: NumPy datatype, defaults to 'uint8'
|
|
656
667
|
:type dtype: str or NumPy dtype, optional
|
|
668
|
+
|
|
669
|
+
|dtype_aliases|
|
|
657
670
|
:param colororder: color plane names for the output image, defaults to None
|
|
658
671
|
:type colororder: str or None, optional
|
|
659
672
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -746,6 +759,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
746
759
|
:type cycles: int, optional
|
|
747
760
|
:param dtype: NumPy datatype, defaults to 'float32'
|
|
748
761
|
:type dtype: str or NumPy dtype, optional
|
|
762
|
+
|
|
763
|
+
|dtype_aliases|
|
|
749
764
|
:param colororder: color plane names for the output image, defaults to None
|
|
750
765
|
:type colororder: str or None, optional
|
|
751
766
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -842,6 +857,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
842
857
|
:type cycles: int, optional
|
|
843
858
|
:param dtype: NumPy datatype, defaults to 'float32'
|
|
844
859
|
:type dtype: str or NumPy dtype, optional
|
|
860
|
+
|
|
861
|
+
|dtype_aliases|
|
|
845
862
|
:param colororder: color plane names for the output image, defaults to None
|
|
846
863
|
:type colororder: str or None, optional
|
|
847
864
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -922,6 +939,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
922
939
|
:type square: int, optional
|
|
923
940
|
:param dtype: image data type, defaults to "uint8"
|
|
924
941
|
:type dtype: str or NumPy dtype, optional
|
|
942
|
+
|
|
943
|
+
|dtype_aliases|
|
|
925
944
|
:param colororder: color plane names for the output image, defaults to None
|
|
926
945
|
:type colororder: str or None, optional
|
|
927
946
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -1012,6 +1031,8 @@ class ImageConstantsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
1012
1031
|
:type shift: int, optional
|
|
1013
1032
|
:param dtype: image data type, defaults to "uint8"
|
|
1014
1033
|
:type dtype: str or NumPy dtype, optional
|
|
1034
|
+
|
|
1035
|
+
|dtype_aliases|
|
|
1015
1036
|
:param colororder: color plane names for the output image, defaults to None
|
|
1016
1037
|
:type colororder: str or None, optional
|
|
1017
1038
|
:param like: template image supplying default ``size``, ``dtype`` and
|
|
@@ -20,6 +20,7 @@ from spatialmath.base import islistof, isscalar
|
|
|
20
20
|
|
|
21
21
|
# from numpy.lib.arraysetops import isin
|
|
22
22
|
from machinevisiontoolbox.base import (
|
|
23
|
+
DTYPE_ALIASES,
|
|
23
24
|
draw_box,
|
|
24
25
|
draw_circle,
|
|
25
26
|
draw_labelbox,
|
|
@@ -98,8 +99,13 @@ class Image(
|
|
|
98
99
|
:type copy: bool, optional
|
|
99
100
|
:param size: new size for the image, defaults to None
|
|
100
101
|
:type size: tuple, optional
|
|
101
|
-
:param dtype: data type for image
|
|
102
|
-
|
|
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
|
|
103
109
|
:param name: name of image, defaults to None
|
|
104
110
|
:type name: str, optional
|
|
105
111
|
:param id: numeric id of image, typically a sequence number, defaults to None
|
|
@@ -301,13 +307,22 @@ class Image(
|
|
|
301
307
|
)
|
|
302
308
|
|
|
303
309
|
# if dtype is not given, determine the appropriate type for the data
|
|
310
|
+
dtype_given = dtype is not None
|
|
304
311
|
dtype = self._infer_dtype(image, dtype)
|
|
305
312
|
|
|
306
313
|
if binary:
|
|
307
314
|
image = image > 0
|
|
308
315
|
|
|
309
|
-
#
|
|
310
|
-
|
|
316
|
+
# Change type of array to the determined dtype -- unless the caller
|
|
317
|
+
# also passed convert()-only options (eg. maxintval) alongside an
|
|
318
|
+
# explicit dtype=, in which case defer the cast to convert() below.
|
|
319
|
+
# convert()'s int_image()/float_image() know how to *scale* a value
|
|
320
|
+
# using maxintval; a bare .astype() here would instead silently
|
|
321
|
+
# truncate (eg. uint16 0..4095 -> uint8 keeps only the low byte,
|
|
322
|
+
# producing a corrupted "bottom byte" image instead of the intended
|
|
323
|
+
# rescaled 0..255).
|
|
324
|
+
defer_dtype_to_convert = dtype_given and bool(kwargs)
|
|
325
|
+
if dtype is not None and not defer_dtype_to_convert:
|
|
311
326
|
image = image.astype(dtype, copy=False)
|
|
312
327
|
|
|
313
328
|
self.name = name
|
|
@@ -324,7 +339,7 @@ class Image(
|
|
|
324
339
|
image = image[:, :, 0] # squeeze out singleton plane
|
|
325
340
|
|
|
326
341
|
if kwargs:
|
|
327
|
-
image = convert(image, **kwargs)
|
|
342
|
+
image = convert(image, dtype=dtype if defer_dtype_to_convert else None, **kwargs)
|
|
328
343
|
|
|
329
344
|
# assign the image to the object, copying if requested
|
|
330
345
|
if copy:
|
|
@@ -381,6 +396,8 @@ class Image(
|
|
|
381
396
|
raise ValueError("bad dtype argument passed to Image constructor")
|
|
382
397
|
else:
|
|
383
398
|
# dtype is given, convert to a NumPy dtype
|
|
399
|
+
if isinstance(dtype, str):
|
|
400
|
+
dtype = DTYPE_ALIASES.get(dtype, dtype)
|
|
384
401
|
try:
|
|
385
402
|
dtype = np.dtype(dtype)
|
|
386
403
|
except TypeError:
|
|
@@ -475,6 +492,37 @@ class Image(
|
|
|
475
492
|
"""Iterate over image planes."""
|
|
476
493
|
return self.planes()
|
|
477
494
|
|
|
495
|
+
def __array__(self, dtype: Dtype | None = None, copy: bool | None = None) -> np.ndarray:
|
|
496
|
+
"""
|
|
497
|
+
Convert to a plain NumPy array
|
|
498
|
+
|
|
499
|
+
:param dtype: dtype of the returned array, defaults to the image's own dtype
|
|
500
|
+
:type dtype: numpy dtype, optional
|
|
501
|
+
:param copy: force a copy of the underlying data, defaults to None
|
|
502
|
+
:type copy: bool, optional
|
|
503
|
+
:return: image pixel data
|
|
504
|
+
:rtype: ndarray(H,W) or ndarray(H,W,3)
|
|
505
|
+
|
|
506
|
+
Implements the NumPy array protocol, used by ``np.asarray(img)`` and by
|
|
507
|
+
any NumPy/SciPy/Matplotlib function that isn't ufunc- or array-function-
|
|
508
|
+
aware (those instead go through :meth:`__array_ufunc__` /
|
|
509
|
+
:meth:`__array_function__`) and calls ``np.asarray()`` internally on
|
|
510
|
+
whatever it's given. Without this, such a call would silently coerce an
|
|
511
|
+
:class:`Image` into a useless 0-d object array instead of its pixel data.
|
|
512
|
+
|
|
513
|
+
Returns the same read-only view as the :attr:`array` property unless a
|
|
514
|
+
dtype conversion or an explicit copy is requested, either of which
|
|
515
|
+
produces a new, independent (writeable) array.
|
|
516
|
+
|
|
517
|
+
:seealso: :attr:`array`
|
|
518
|
+
"""
|
|
519
|
+
arr = self.array
|
|
520
|
+
if dtype is not None:
|
|
521
|
+
arr = arr.astype(dtype)
|
|
522
|
+
if copy:
|
|
523
|
+
arr = arr.copy()
|
|
524
|
+
return arr
|
|
525
|
+
|
|
478
526
|
def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
|
|
479
527
|
"""
|
|
480
528
|
Support NumPy ufunc calls on image data.
|
|
@@ -1927,6 +1975,9 @@ class Image(
|
|
|
1927
1975
|
|
|
1928
1976
|
:param dtype: Numpy data type
|
|
1929
1977
|
:type dtype: str or NumPy dtype
|
|
1978
|
+
|
|
1979
|
+
|dtype_aliases|
|
|
1980
|
+
|
|
1930
1981
|
:return: image
|
|
1931
1982
|
:rtype: :class:`Image`
|
|
1932
1983
|
|
|
@@ -1949,8 +2000,9 @@ class Image(
|
|
|
1949
2000
|
:seealso: :meth:`astype` :meth:`to_int` :meth:`to_float`
|
|
1950
2001
|
"""
|
|
1951
2002
|
# convert image to different type, does rescaling
|
|
1952
|
-
# as just changes type
|
|
1953
|
-
|
|
2003
|
+
# as just changes type. array_as() and the constructor both
|
|
2004
|
+
# resolve dtype (including the short-name aliases) themselves,
|
|
2005
|
+
# so just pass it through unchanged rather than resolving twice.
|
|
1954
2006
|
return self.__class__(self.array_as(dtype), dtype=dtype)
|
|
1955
2007
|
|
|
1956
2008
|
def astype(self, dtype: Dtype) -> "Image":
|
|
@@ -1959,6 +2011,9 @@ class Image(
|
|
|
1959
2011
|
|
|
1960
2012
|
:param dtype: Numpy data type
|
|
1961
2013
|
:type dtype: str or NumPy dtype
|
|
2014
|
+
|
|
2015
|
+
|dtype_aliases|
|
|
2016
|
+
|
|
1962
2017
|
:return: image
|
|
1963
2018
|
:rtype: :class:`Image`
|
|
1964
2019
|
|
|
@@ -1979,6 +2034,8 @@ class Image(
|
|
|
1979
2034
|
|
|
1980
2035
|
:seealso: :meth:`to`
|
|
1981
2036
|
"""
|
|
2037
|
+
if isinstance(dtype, str):
|
|
2038
|
+
dtype = DTYPE_ALIASES.get(dtype, dtype)
|
|
1982
2039
|
return self.__class__(self._A.astype(dtype), dtype=dtype)
|
|
1983
2040
|
|
|
1984
2041
|
# ---- NumPy array access ---- #
|
|
@@ -2254,7 +2311,9 @@ class Image(
|
|
|
2254
2311
|
Convert Image to NumPy array of specified type
|
|
2255
2312
|
|
|
2256
2313
|
:param dtype: data type of the output array
|
|
2257
|
-
:type dtype: str or np.dtype
|
|
2314
|
+
:type dtype: str or np.dtype, optional
|
|
2315
|
+
|
|
2316
|
+
|dtype_aliases|
|
|
2258
2317
|
:return: NumPy array with specified type
|
|
2259
2318
|
:rtype: ndarray(H,W) or ndarray(H,W,P)
|
|
2260
2319
|
|
|
@@ -2296,6 +2355,8 @@ class Image(
|
|
|
2296
2355
|
if dtype is None:
|
|
2297
2356
|
return self._A
|
|
2298
2357
|
else:
|
|
2358
|
+
if isinstance(dtype, str):
|
|
2359
|
+
dtype = DTYPE_ALIASES.get(dtype, dtype)
|
|
2299
2360
|
dtype = np.dtype(dtype) # convert to dtype if it's a string
|
|
2300
2361
|
if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.bool_):
|
|
2301
2362
|
return int_image(self._A, dtype)
|
|
@@ -429,14 +429,22 @@ class FiducialCollection:
|
|
|
429
429
|
# -------------------------------------------------------------------
|
|
430
430
|
ids = ids.reshape(-1) if ids is not None else ids
|
|
431
431
|
|
|
432
|
-
# filter tags by ID
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
432
|
+
# filter tags by ID, keeping each marker's corners and id together in
|
|
433
|
+
# a single pass so the two lists cannot get out of step (detectMarkers
|
|
434
|
+
# returns ids=None when it finds nothing)
|
|
435
|
+
matches = (
|
|
436
|
+
[]
|
|
437
|
+
if ids is None
|
|
438
|
+
else [
|
|
439
|
+
(corners.T.squeeze(), id)
|
|
440
|
+
for corners, id in zip(cornerss, ids)
|
|
441
|
+
if id in self._ids
|
|
442
|
+
]
|
|
443
|
+
)
|
|
444
|
+
if not matches:
|
|
445
|
+
raise ValueError("no markers belonging to this board were detected")
|
|
446
|
+
cornerss = [corners for corners, _ in matches]
|
|
447
|
+
ids = [id for _, id in matches]
|
|
440
448
|
|
|
441
449
|
# match the markers to the board
|
|
442
450
|
# print(f"{len(ids)} markers found")
|
machinevisiontoolbox/ImageIO.py
CHANGED
|
@@ -267,6 +267,14 @@ class ImageIOMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
267
267
|
# map tag number to tag name
|
|
268
268
|
exif[TAGS[tag]] = value
|
|
269
269
|
|
|
270
|
+
# Camera-specific tags (FocalLength, ExposureTime, FNumber,
|
|
271
|
+
# ISOSpeedRatings, LensModel, ...) live in a separate "Exif" sub-IFD
|
|
272
|
+
# that Image.getexif() does not flatten into the top-level dict --
|
|
273
|
+
# 0x8769 is the fixed EXIF-spec tag id for the pointer to it.
|
|
274
|
+
for tag, value in meta.get_ifd(0x8769).items():
|
|
275
|
+
if tag in TAGS:
|
|
276
|
+
exif[TAGS[tag]] = value
|
|
277
|
+
|
|
270
278
|
if key is None:
|
|
271
279
|
return exif
|
|
272
280
|
else:
|
|
@@ -669,7 +669,7 @@ class BaseFeature2D:
|
|
|
669
669
|
other: "BaseFeature2D",
|
|
670
670
|
ratio: float = 0.75,
|
|
671
671
|
crosscheck: bool = False,
|
|
672
|
-
metric: str =
|
|
672
|
+
metric: str | None = None,
|
|
673
673
|
sort: bool = True,
|
|
674
674
|
top: int | None = None,
|
|
675
675
|
thresh: float | None = None,
|
|
@@ -683,7 +683,9 @@ class BaseFeature2D:
|
|
|
683
683
|
:type ratio: float, optional
|
|
684
684
|
:param crosscheck: perform left-right cross check, defaults to False
|
|
685
685
|
:type crosscheck: bool, optional
|
|
686
|
-
:param metric: distance metric, one of: 'L1', 'L2'
|
|
686
|
+
:param metric: distance metric, one of: 'L1', 'L2', 'hamming', 'hamming2'.
|
|
687
|
+
Defaults to ``None``, which auto-selects 'hamming' for binary
|
|
688
|
+
descriptors (eg. ORB, BRISK, AKAZE) and 'L2' otherwise (eg. SIFT).
|
|
687
689
|
:type metric: str, optional
|
|
688
690
|
:param sort: sort features by strength, defaults to True
|
|
689
691
|
:type sort: bool, optional
|
|
@@ -694,6 +696,12 @@ class BaseFeature2D:
|
|
|
694
696
|
Return a match object that contains pairs of putative corresponding points.
|
|
695
697
|
If ``crosscheck`` is True the ratio test is disabled
|
|
696
698
|
|
|
699
|
+
.. note:: Binary descriptors (eg. ORB, BRISK, AKAZE) are ``uint8``
|
|
700
|
+
arrays, for which 'L2' distance is close to meaningless -- it
|
|
701
|
+
will silently return very few matches rather than raising an
|
|
702
|
+
error, which is why this is auto-selected rather than left for
|
|
703
|
+
the caller to always remember.
|
|
704
|
+
|
|
697
705
|
Example:
|
|
698
706
|
|
|
699
707
|
.. runblock:: pycon
|
|
@@ -720,6 +728,9 @@ class BaseFeature2D:
|
|
|
720
728
|
# sorting
|
|
721
729
|
# return
|
|
722
730
|
|
|
731
|
+
if metric is None:
|
|
732
|
+
metric = "hamming" if self.descriptor.dtype == np.uint8 else "L2"
|
|
733
|
+
|
|
723
734
|
metricdict = {
|
|
724
735
|
"L1": cv2.NORM_L1,
|
|
725
736
|
"L2": cv2.NORM_L2,
|
|
@@ -285,7 +285,7 @@ class ImageWholeFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
285
285
|
:seealso: :meth:`stats`
|
|
286
286
|
"""
|
|
287
287
|
|
|
288
|
-
stats = self.
|
|
288
|
+
stats = self.stats
|
|
289
289
|
if self.iscolor and self.colororder is not None:
|
|
290
290
|
colororder = self.colororder
|
|
291
291
|
for k in sorted(stats.keys(), key=lambda x: colororder[x]):
|
|
@@ -1210,7 +1210,9 @@ class Histogram:
|
|
|
1210
1210
|
|
|
1211
1211
|
def plot(
|
|
1212
1212
|
self,
|
|
1213
|
-
type:
|
|
1213
|
+
type: Literal[
|
|
1214
|
+
"frequency", "pdf", "probability", "cf", "cumulative", "cdf", "normalized", "ncdf"
|
|
1215
|
+
] = "frequency",
|
|
1214
1216
|
block: bool = False,
|
|
1215
1217
|
filled: bool | None = None,
|
|
1216
1218
|
stats: bool = True,
|
|
@@ -1227,8 +1229,9 @@ class Histogram:
|
|
|
1227
1229
|
"""
|
|
1228
1230
|
Plot histogram
|
|
1229
1231
|
|
|
1230
|
-
:param type: histogram type, one of: 'frequency' [default], '
|
|
1231
|
-
|
|
1232
|
+
:param type: histogram type, one of: 'frequency' [default], 'pdf'/'probability',
|
|
1233
|
+
'cf'/'cumulative', 'cdf'/'normalized'; 'ncdf' is accepted as a deprecated
|
|
1234
|
+
alias for 'cdf'
|
|
1232
1235
|
:param block: hold plot, defaults to False
|
|
1233
1236
|
:type block: bool, optional
|
|
1234
1237
|
:param filled: use a filled stairs plot, defaults to True for frequency plot, False for
|
|
@@ -1258,7 +1261,9 @@ class Histogram:
|
|
|
1258
1261
|
|
|
1259
1262
|
Plots the histogram using Matplotlib. For a color image, the histograms of each
|
|
1260
1263
|
plane are plotted separately. The ``type`` option selects the type of histogram
|
|
1261
|
-
to plot: ``frequency``, ``
|
|
1264
|
+
to plot: ``frequency``, ``pdf``/``probability``, ``cf``/``cumulative``, or
|
|
1265
|
+
``cdf``/``normalized`` (normalized cumulative, range 0 to 1). ``ncdf`` is
|
|
1266
|
+
accepted as a deprecated alias for ``cdf``.
|
|
1262
1267
|
|
|
1263
1268
|
The ``style`` option selects the style for plotting multiple planes:
|
|
1264
1269
|
|
|
@@ -1342,6 +1347,14 @@ class Histogram:
|
|
|
1342
1347
|
if filled is None:
|
|
1343
1348
|
filled = bar
|
|
1344
1349
|
|
|
1350
|
+
if type == "ncdf":
|
|
1351
|
+
warnings.warn(
|
|
1352
|
+
"Deprecated in 2.0.0: use type='cdf' instead of type='ncdf'.",
|
|
1353
|
+
DeprecationWarning,
|
|
1354
|
+
stacklevel=2,
|
|
1355
|
+
)
|
|
1356
|
+
type = "cdf"
|
|
1357
|
+
|
|
1345
1358
|
if type not in ("frequency", "pdf"):
|
|
1346
1359
|
stats = False # only show stats for frequency and pdf plot
|
|
1347
1360
|
|