machinevision-toolbox-python 2.2.0__py3-none-any.whl → 2.4.0__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.2.0.dist-info → machinevision_toolbox_python-2.4.0.dist-info}/METADATA +18 -14
- {machinevision_toolbox_python-2.2.0.dist-info → machinevision_toolbox_python-2.4.0.dist-info}/RECORD +21 -21
- {machinevision_toolbox_python-2.2.0.dist-info → machinevision_toolbox_python-2.4.0.dist-info}/WHEEL +1 -1
- machinevisiontoolbox/BagOfWords.py +13 -1
- machinevisiontoolbox/BundleAdjust.py +11 -1
- machinevisiontoolbox/Camera.py +101 -18
- machinevisiontoolbox/ImageConstants.py +27 -6
- machinevisiontoolbox/ImageCore.py +69 -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 +4 -5
- 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
- {machinevision_toolbox_python-2.2.0.dist-info → machinevision_toolbox_python-2.4.0.dist-info}/entry_points.txt +0 -0
- {machinevision_toolbox_python-2.2.0.dist-info → machinevision_toolbox_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
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
|
|
machinevisiontoolbox/Kernel.py
CHANGED
|
@@ -115,6 +115,35 @@ class Kernel:
|
|
|
115
115
|
def shape(self) -> tuple[int, int]:
|
|
116
116
|
return self.K.shape
|
|
117
117
|
|
|
118
|
+
@property
|
|
119
|
+
def ndim(self) -> int:
|
|
120
|
+
return 2
|
|
121
|
+
|
|
122
|
+
def __array__(self, dtype: Dtype | None = None, copy: bool | None = None) -> np.ndarray:
|
|
123
|
+
"""
|
|
124
|
+
Convert to a plain NumPy array
|
|
125
|
+
|
|
126
|
+
:param dtype: dtype of the returned array, defaults to the kernel's own dtype
|
|
127
|
+
:type dtype: numpy dtype, optional
|
|
128
|
+
:param copy: force a copy of the underlying data, defaults to None
|
|
129
|
+
:type copy: bool, optional
|
|
130
|
+
:return: kernel weighting matrix
|
|
131
|
+
:rtype: ndarray(N,M)
|
|
132
|
+
|
|
133
|
+
Implements the NumPy array protocol, used by ``np.asarray(K)`` and by
|
|
134
|
+
any NumPy/SciPy/Matplotlib function that calls ``np.asarray()``
|
|
135
|
+
internally on whatever it's given (e.g. ``scipy.signal.convolve2d``,
|
|
136
|
+
``Axes3D.plot_surface``, ``np.linalg.svd``). Without this, such a call
|
|
137
|
+
would silently coerce a :class:`Kernel` into a useless 0-d object
|
|
138
|
+
array instead of its weighting matrix.
|
|
139
|
+
"""
|
|
140
|
+
arr = self.K
|
|
141
|
+
if dtype is not None:
|
|
142
|
+
arr = arr.astype(dtype)
|
|
143
|
+
if copy:
|
|
144
|
+
arr = arr.copy()
|
|
145
|
+
return arr
|
|
146
|
+
|
|
118
147
|
def print(
|
|
119
148
|
self, fmt: str | None = None, separator: str = " ", precision: int = 2
|
|
120
149
|
) -> None:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Thin wrapper around Open3D point-cloud objects with MVTB look and feel.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from warnings import warn
|
|
6
7
|
from typing import Any, Callable
|
|
7
8
|
|
|
@@ -26,7 +27,7 @@ class PointCloud:
|
|
|
26
27
|
:type arg: :obj:`o3d.geometry.PointCloud`, :obj:`~machinevisiontoolbox.PointCloud`, ndarray(3,N)
|
|
27
28
|
:param depth_scale: depth scale factor, defaults to 1.0
|
|
28
29
|
:type depth_scale: float, optional
|
|
29
|
-
:raises RuntimeError: PointCloud class requires Open3D to be installed: pip install open3d
|
|
30
|
+
:raises RuntimeError: PointCloud class requires Open3D to be installed: pip install open3d or pip install machinevision-toolbox-python[open3d]
|
|
30
31
|
:raises ValueError: bad arguments
|
|
31
32
|
|
|
32
33
|
This object wraps an Open3D :obj:`open3d.geometry.PointCloud` object. It can be
|
|
@@ -50,7 +51,7 @@ class PointCloud:
|
|
|
50
51
|
if not _open3d:
|
|
51
52
|
raise RuntimeError(
|
|
52
53
|
"PointCloud class requires Open3D to be installed: "
|
|
53
|
-
"pip install open3d
|
|
54
|
+
"pip install open3d or "
|
|
54
55
|
"pip install machinevision-toolbox-python[open3d]\n"
|
|
55
56
|
" ** Note that Open3d typically does not run on the latest Python version(s)"
|
|
56
57
|
)
|
|
@@ -60,7 +61,7 @@ class PointCloud:
|
|
|
60
61
|
if not _open3d:
|
|
61
62
|
raise RuntimeError(
|
|
62
63
|
"PointCloud class requires Open3D to be installed: "
|
|
63
|
-
"pip install open3d
|
|
64
|
+
"pip install open3d or "
|
|
64
65
|
"pip install machinevision-toolbox-python[open3d]"
|
|
65
66
|
)
|
|
66
67
|
|
|
@@ -244,10 +245,22 @@ class PointCloud:
|
|
|
244
245
|
the ``data`` folder of the
|
|
245
246
|
`mvtb_data package <https://github.com/petercorke/machinevision-toolbox-python/tree/master/packages/mvtb-data>`_.
|
|
246
247
|
|
|
248
|
+
.. note:: ``filename`` may be given as a bare name (``'bunny.ply'``)
|
|
249
|
+
or with a redundant leading ``data/`` (``'data/bunny.ply'``, the
|
|
250
|
+
older convention some book examples still use) -- both resolve
|
|
251
|
+
to the same file.
|
|
252
|
+
|
|
247
253
|
"""
|
|
248
254
|
|
|
249
255
|
from machinevisiontoolbox import mvtb_path_to_datafile
|
|
250
256
|
|
|
257
|
+
# accept a caller-supplied redundant "data/" prefix (older
|
|
258
|
+
# convention) without double-joining it against the "data" this
|
|
259
|
+
# method already prepends
|
|
260
|
+
parts = Path(filename).parts
|
|
261
|
+
if parts and parts[0] == "data":
|
|
262
|
+
filename = str(Path(*parts[1:])) if len(parts) > 1 else ""
|
|
263
|
+
|
|
251
264
|
filename = mvtb_path_to_datafile("data", filename, string=True)
|
|
252
265
|
pcd = o3d.io.read_point_cloud(filename, *args, **kwargs)
|
|
253
266
|
return cls(pcd)
|
|
@@ -273,7 +286,7 @@ class PointCloud:
|
|
|
273
286
|
:type rgb: :class:`~machinevisiontoolbox.Image`, optional
|
|
274
287
|
:param depth_scale: depth scale factor, defaults to 1.0
|
|
275
288
|
:type depth_scale: float, optional
|
|
276
|
-
:raises RuntimeError: PointCloud class requires Open3D to be installed: pip install open3d
|
|
289
|
+
:raises RuntimeError: PointCloud class requires Open3D to be installed: pip install open3d or pip install machinevision-toolbox-python[open3d]
|
|
277
290
|
:raises ValueError: input images has wrong number of planes
|
|
278
291
|
|
|
279
292
|
The depth image is converted to a point cloud using the Open3D, and the pixel
|
machinevisiontoolbox/Sources.py
CHANGED
|
@@ -25,7 +25,6 @@ import warnings
|
|
|
25
25
|
import zipfile
|
|
26
26
|
from collections import Counter, deque
|
|
27
27
|
from collections.abc import Iterator
|
|
28
|
-
from numpy.char import array
|
|
29
28
|
from tqdm import tqdm
|
|
30
29
|
from datetime import datetime, timezone
|
|
31
30
|
from typing import TYPE_CHECKING, IO, Any, Literal, cast
|
|
@@ -2346,10 +2345,10 @@ class ROSTopic(ImageSource):
|
|
|
2346
2345
|
if not _open3d_available:
|
|
2347
2346
|
raise ImportError(
|
|
2348
2347
|
"open3d is required for PointCloud publish support. "
|
|
2349
|
-
"Install it with: pip install open3d
|
|
2348
|
+
"Install it with: pip install open3d "
|
|
2350
2349
|
"or pip install machinevision-toolbox-python[open3d]"
|
|
2351
2350
|
)
|
|
2352
|
-
|
|
2351
|
+
assert o3d is not None
|
|
2353
2352
|
|
|
2354
2353
|
points = np.asarray(pc._pcd.points)
|
|
2355
2354
|
if points.ndim != 2 or points.shape[1] != 3:
|
|
@@ -3573,7 +3572,7 @@ class ROSBag(ImageSource):
|
|
|
3573
3572
|
if not _open3d_available:
|
|
3574
3573
|
raise ImportError(
|
|
3575
3574
|
"open3d is required to read PointCloud2 messages. "
|
|
3576
|
-
"Install it with: pip install open3d
|
|
3575
|
+
"Install it with: pip install open3d "
|
|
3577
3576
|
"or pip install machinevision-toolbox-python[open3d]"
|
|
3578
3577
|
)
|
|
3579
3578
|
assert o3d is not None
|
|
@@ -3770,7 +3769,7 @@ class PointCloudSequence:
|
|
|
3770
3769
|
if not _open3d_available:
|
|
3771
3770
|
raise ImportError(
|
|
3772
3771
|
"open3d is required for point cloud display. "
|
|
3773
|
-
"Install it with: pip install open3d
|
|
3772
|
+
"Install it with: pip install open3d "
|
|
3774
3773
|
"or pip install machinevision-toolbox-python[open3d]"
|
|
3775
3774
|
)
|
|
3776
3775
|
assert o3d is not None
|
|
@@ -17,6 +17,7 @@ import matplotlib.path as mpath
|
|
|
17
17
|
import matplotlib.pyplot as plt
|
|
18
18
|
import numpy as np
|
|
19
19
|
import spatialmath.base as smb
|
|
20
|
+
from spatialmath import Polygon2
|
|
20
21
|
from scipy import interpolate
|
|
21
22
|
|
|
22
23
|
# import io as io
|
|
@@ -814,8 +815,10 @@ def name2color(
|
|
|
814
815
|
color = (color * np.iinfo(dtype).max).astype(dtype)
|
|
815
816
|
if colororder is not None:
|
|
816
817
|
# reorder the elements of the color if colororder is given
|
|
817
|
-
# colororder is a dict mapping plane name to index
|
|
818
|
-
|
|
818
|
+
# colororder is a dict mapping plane name to index -- fancy
|
|
819
|
+
# index rather than a list comprehension, to keep the
|
|
820
|
+
# ndarray return type promised by this function's contract
|
|
821
|
+
color = color[[colororder[c] for c in colorspace]]
|
|
819
822
|
return color
|
|
820
823
|
except ValueError:
|
|
821
824
|
return None
|
|
@@ -1777,8 +1780,8 @@ def shadow_invariant(
|
|
|
1777
1780
|
return gs
|
|
1778
1781
|
|
|
1779
1782
|
|
|
1780
|
-
def esttheta(im: Any, sharpen: np.ndarray | None = None) ->
|
|
1781
|
-
"""
|
|
1783
|
+
def esttheta(im: Any, sharpen: np.ndarray | None = None) -> float:
|
|
1784
|
+
r"""
|
|
1782
1785
|
Estimate theta for shadow invariance
|
|
1783
1786
|
|
|
1784
1787
|
:param im: input image
|
|
@@ -1790,24 +1793,27 @@ def esttheta(im: Any, sharpen: np.ndarray | None = None) -> None:
|
|
|
1790
1793
|
|
|
1791
1794
|
This is an interactive procedure where the image is displayed and the user
|
|
1792
1795
|
selects a region of homogeneous material (eg. grass or road) that includes
|
|
1793
|
-
areas that are directly lit by the sun and in shadow.
|
|
1796
|
+
areas that are directly lit by the sun and in shadow. The invariant angle
|
|
1797
|
+
θ is found by sweeping candidate angles and choosing the one that
|
|
1798
|
+
minimizes the variance of the resulting invariant image over the selected
|
|
1799
|
+
region -- at the true invariant angle, lit and shadowed pixels of the same
|
|
1800
|
+
material map to (nearly) the same greyscale value.
|
|
1794
1801
|
|
|
1795
1802
|
.. note:: The user selects boundary points by clicking the mouse. After the
|
|
1796
1803
|
last point, hit the Enter key and the region will be closed.
|
|
1804
|
+
|
|
1805
|
+
:seealso: :func:`shadow_invariant`
|
|
1797
1806
|
"""
|
|
1798
1807
|
|
|
1799
1808
|
def pickregion(im: Any) -> np.ndarray:
|
|
1800
|
-
im.disp()
|
|
1809
|
+
h = im.disp()
|
|
1801
1810
|
|
|
1802
|
-
clicks =
|
|
1811
|
+
clicks = h.figure.ginput(n=-1)
|
|
1803
1812
|
|
|
1804
1813
|
xy = np.array(clicks)
|
|
1805
|
-
print(xy)
|
|
1806
1814
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
polygon = smb.Polygon2(xy.T)
|
|
1810
|
-
polygon.plot("g")
|
|
1815
|
+
polygon = Polygon2(xy.T)
|
|
1816
|
+
polygon.plot(color="g")
|
|
1811
1817
|
|
|
1812
1818
|
X, Y = im.meshgrid()
|
|
1813
1819
|
inside = polygon.contains(np.c_[X.ravel(), Y.ravel()].T)
|
|
@@ -1816,13 +1822,33 @@ def esttheta(im: Any, sharpen: np.ndarray | None = None) -> None:
|
|
|
1816
1822
|
|
|
1817
1823
|
k_region = pickregion(im)
|
|
1818
1824
|
|
|
1819
|
-
imcol = im.
|
|
1820
|
-
|
|
1825
|
+
imcol = im.view1d()
|
|
1821
1826
|
z = imcol[k_region, :]
|
|
1822
|
-
print(z.shape)
|
|
1823
|
-
|
|
1827
|
+
print(f"{z.shape[0]} pixels selected in the region")
|
|
1828
|
+
|
|
1829
|
+
theta_v = np.arange(0, np.pi, 0.02)
|
|
1830
|
+
sim = np.empty_like(theta_v)
|
|
1831
|
+
for i, θ in enumerate(theta_v):
|
|
1832
|
+
gs = shadow_invariant(z.reshape(-1, 1, 3), θ, sharpen=sharpen).ravel()
|
|
1833
|
+
bad = np.isinf(gs) | np.isnan(gs)
|
|
1834
|
+
sim[i] = np.std(gs[~bad])
|
|
1835
|
+
|
|
1836
|
+
k = np.argmin(sim)
|
|
1837
|
+
theta = theta_v[k]
|
|
1838
|
+
|
|
1839
|
+
plt.figure()
|
|
1840
|
+
plt.plot(theta_v, sim)
|
|
1841
|
+
plt.plot(theta, sim[k], "o")
|
|
1842
|
+
plt.xlabel("invariant line angle (rad)")
|
|
1843
|
+
plt.ylabel("invariance image variance")
|
|
1844
|
+
plt.grid(True)
|
|
1845
|
+
|
|
1846
|
+
print(f"best angle = {theta:.4f} rad")
|
|
1847
|
+
|
|
1824
1848
|
_safe_show(block=True)
|
|
1825
1849
|
|
|
1850
|
+
return theta
|
|
1851
|
+
|
|
1826
1852
|
|
|
1827
1853
|
if __name__ == "__main__": # pragma: no cover
|
|
1828
1854
|
import os.path
|