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.
@@ -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-python or pip install machinevision-toolbox-python[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-python or "
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-python or "
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-python or pip install machinevision-toolbox-python[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
@@ -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
@@ -603,7 +602,8 @@ class VideoCamera(ImageSource):
603
602
  attached video camera. On a Mac running 13.0 (Ventura) or later and
604
603
  an iPhone with iOS 16 or later, the Continuity Camera feature allows
605
604
  the phone camera to be used as a local video camera, and it will
606
- appear as a separate camera with its own ID.
605
+ appear as a separate camera with its own ID. Other phones, and cameras
606
+ on the network, can be used through :class:`WebCam`.
607
607
 
608
608
  OpenCV does not expose a portable API for mapping integer ``id``
609
609
  values to human-readable camera names. Use :meth:`list` to
@@ -613,7 +613,7 @@ class VideoCamera(ImageSource):
613
613
  :references:
614
614
  - |RVC3|, Section 11.1.3.
615
615
 
616
- :seealso: :meth:`list` :func:`~machinevisiontoolbox.base.imageio.convert`
616
+ :seealso: :meth:`list` :class:`WebCam` :func:`~machinevisiontoolbox.base.imageio.convert`
617
617
  `cv2.VideoCapture <https://docs.opencv.org/4.x/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80>`_
618
618
  """
619
619
 
@@ -1730,15 +1730,24 @@ class WebCam(ImageSource):
1730
1730
  pass
1731
1731
 
1732
1732
 
1733
- .. note:: Manu webcameras accept a query string in the URL to specify
1733
+ .. note:: Many webcameras accept a query string in the URL to specify
1734
1734
  image resolution, image format, codec and other parameters. There
1735
1735
  is no common standard for this, see the manufacturer's datasheet
1736
1736
  for details.
1737
1737
 
1738
+ .. note:: The ``url`` is passed to ``cv2.VideoCapture``, so it can be
1739
+ anything that OpenCV can open, for example a still-image URL, a
1740
+ live HTTP (MJPEG) stream or an RTSP stream. A phone can be used as a
1741
+ networked camera in this way: many IP-camera apps for iOS and Android
1742
+ publish a live stream at a URL. Which stream types and URL formats are
1743
+ supported depends on the app and on how your OpenCV was built. On a Mac
1744
+ with an iPhone, Continuity Camera makes the phone appear as a local
1745
+ camera instead, see :class:`VideoCamera`.
1746
+
1738
1747
  :references:
1739
1748
  - |RVC3|, Section 11.1.5.
1740
1749
 
1741
- :seealso: :func:`~machinevisiontoolbox.base.imageio.convert`
1750
+ :seealso: :class:`VideoCamera` :func:`~machinevisiontoolbox.base.imageio.convert`
1742
1751
  `cv2.VideoCapture <https://docs.opencv.org/4.x/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80>`_
1743
1752
  """
1744
1753
 
@@ -2346,10 +2355,10 @@ class ROSTopic(ImageSource):
2346
2355
  if not _open3d_available:
2347
2356
  raise ImportError(
2348
2357
  "open3d is required for PointCloud publish support. "
2349
- "Install it with: pip install open3d-python "
2358
+ "Install it with: pip install open3d "
2350
2359
  "or pip install machinevision-toolbox-python[open3d]"
2351
2360
  )
2352
- assert o3d is not None
2361
+ assert o3d is not None
2353
2362
 
2354
2363
  points = np.asarray(pc._pcd.points)
2355
2364
  if points.ndim != 2 or points.shape[1] != 3:
@@ -3573,7 +3582,7 @@ class ROSBag(ImageSource):
3573
3582
  if not _open3d_available:
3574
3583
  raise ImportError(
3575
3584
  "open3d is required to read PointCloud2 messages. "
3576
- "Install it with: pip install open3d-python "
3585
+ "Install it with: pip install open3d "
3577
3586
  "or pip install machinevision-toolbox-python[open3d]"
3578
3587
  )
3579
3588
  assert o3d is not None
@@ -3770,7 +3779,7 @@ class PointCloudSequence:
3770
3779
  if not _open3d_available:
3771
3780
  raise ImportError(
3772
3781
  "open3d is required for point cloud display. "
3773
- "Install it with: pip install open3d-python "
3782
+ "Install it with: pip install open3d "
3774
3783
  "or pip install machinevision-toolbox-python[open3d]"
3775
3784
  )
3776
3785
  assert o3d is not None
@@ -56,6 +56,7 @@ __all__ = [
56
56
  # types
57
57
  "int_image",
58
58
  "float_image",
59
+ "DTYPE_ALIASES",
59
60
  # data
60
61
  "mvtb_path_to_datafile",
61
62
  "mvtb_load_data",
@@ -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
- color = [color[colororder[c]] for c in colorspace]
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) -> 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 = plt.ginput(n=-1)
1811
+ clicks = h.figure.ginput(n=-1)
1803
1812
 
1804
1813
  xy = np.array(clicks)
1805
- print(xy)
1806
1814
 
1807
- smb.plot_poly(xy.T, "g", close=True)
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.column()
1820
-
1825
+ imcol = im.view1d()
1821
1826
  z = imcol[k_region, :]
1822
- print(z.shape)
1823
- # k = find(in);
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