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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: machinevision-toolbox-python
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Object-oriented machine vision for Python.
5
5
  Project-URL: Homepage, https://github.com/petercorke/machinevision-toolbox-python
6
6
  Project-URL: Bug Tracker, https://github.com/pypa/sampleproject/issues
@@ -23,8 +23,8 @@ Requires-Dist: ansitable
23
23
  Requires-Dist: matplotlib
24
24
  Requires-Dist: mvtb-data
25
25
  Requires-Dist: numpy
26
- Requires-Dist: opencv-contrib-python<5.0.0
27
- Requires-Dist: opencv-python<5.0.0
26
+ Requires-Dist: opencv-contrib-python<6.0.0,>=4.5
27
+ Requires-Dist: opencv-python<6.0.0,>=4.5
28
28
  Requires-Dist: pgraph-python
29
29
  Requires-Dist: pillow
30
30
  Requires-Dist: requests
@@ -35,7 +35,7 @@ Provides-Extra: all
35
35
  Requires-Dist: ipympl; extra == 'all'
36
36
  Requires-Dist: ipython; extra == 'all'
37
37
  Requires-Dist: ipywidgets; extra == 'all'
38
- Requires-Dist: open3d-python; extra == 'all'
38
+ Requires-Dist: open3d; (python_version < '3.13') and extra == 'all'
39
39
  Requires-Dist: pygments; extra == 'all'
40
40
  Requires-Dist: pytesseract; extra == 'all'
41
41
  Requires-Dist: rosbags; extra == 'all'
@@ -64,7 +64,7 @@ Requires-Dist: ipywidgets; extra == 'jupyter'
64
64
  Provides-Extra: ocr
65
65
  Requires-Dist: pytesseract; extra == 'ocr'
66
66
  Provides-Extra: open3d
67
- Requires-Dist: open3d-python; extra == 'open3d'
67
+ Requires-Dist: open3d; (python_version < '3.13') and extra == 'open3d'
68
68
  Provides-Extra: ros
69
69
  Requires-Dist: rosbags; extra == 'ros'
70
70
  Requires-Dist: roslibpy; extra == 'ros'
@@ -99,20 +99,24 @@ Description-Content-Type: text/markdown
99
99
 
100
100
  ---
101
101
 
102
- ### Status & Ecosystem
103
- [![A Python Robotics Package](https://raw.githubusercontent.com/petercorke/robotics-toolbox-python/master/.github/svg/py_collection.min.svg)](https://github.com/petercorke/robotics-toolbox-python)
104
- [![QUT Centre for Robotics Open Source](https://github.com/qcr/qcr.github.io/raw/master/misc/badge.svg)](https://qcr.github.io)
102
+ ### Status & Project Health
103
+
105
104
  [![Build Status](https://github.com/petercorke/machinevision-toolbox-python/actions/workflows/ci.yml/badge.svg)](https://github.com/petercorke/machinevision-toolbox-python/actions/workflows/ci.yml)
106
- [![Coverage](https://codecov.io/gh/petercorke/machinevision-toolbox-python/branch/main/graph/badge.svg)](https://codecov.io/gh/petercorke/machinevision-toolbox-python)
105
+ [![Downloads](https://static.pepy.tech/badge/machinevision-toolbox-python/month)](https://pepy.tech/projects/machinevision-toolbox-python)
107
106
  ![Python Version](https://img.shields.io/pypi/pyversions/machinevision-toolbox-python.svg)
107
+ [![Coverage](https://codecov.io/gh/petercorke/machinevision-toolbox-python/branch/main/graph/badge.svg)](https://codecov.io/gh/petercorke/machinevision-toolbox-python)
108
108
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
109
- [![PyPI - Downloads](https://img.shields.io/pypi/dw/machinevision-toolbox-python)](https://pypistats.org/packages/machinevision-toolbox-python)
110
109
 
111
- ### Powered by
112
- [![Powered by Spatial Maths](https://raw.githubusercontent.com/petercorke/spatialmath-python/master/.github/svg/sm_powered.min.svg)](https://github.com/petercorke/spatialmath-python)
113
- [![Powered by NumPy](https://img.shields.io/badge/powered_by-NumPy-013243?logo=numpy&logoColor=white)](https://numpy.org)
110
+ ### Ecosystem & Dependencies
111
+ [![A Python Robotics Package](https://raw.githubusercontent.com/petercorke/robotics-toolbox-python/master/.github/svg/py_collection.min.svg)](https://github.com/petercorke/robotics-toolbox-python)
112
+ [![QUT Centre for Robotics Open Source](https://github.com/qcr/qcr.github.io/raw/master/misc/badge.svg)](https://qcr.github.io)
113
+
114
+ [![powered by NumPy](https://img.shields.io/badge/powered_by-NumPy-013243?logo=numpy&logoColor=white)](https://numpy.org)
115
+ [![powered by SciPy](https://img.shields.io/badge/powered_by-SciPy-0054a6?logo=scipy&logoColor=white)](https://scipy.org)
116
+ [![powered by Matplotlib](https://img.shields.io/badge/powered_by-Matplotlib-11557c?logo=matplotlib&logoColor=white)](https://matplotlib.org)
114
117
  [![Powered by OpenCV](https://raw.githubusercontent.com/petercorke/machinevision-toolbox-python/main/.github/svg/opencv_powered.svg)](https://opencv.org)
115
118
  [![Powered by Open3D](https://raw.githubusercontent.com/petercorke/machinevision-toolbox-python/main/.github/svg/open3d_powered.svg)](https://open3d.org)
119
+ [![Powered by Spatial Maths](https://raw.githubusercontent.com/petercorke/spatialmath-python/master/.github/svg/sm_powered.min.svg)](https://github.com/petercorke/spatialmath-python)
116
120
 
117
121
  ## Synopsis
118
122
 
@@ -1,25 +1,25 @@
1
- machinevisiontoolbox/BagOfWords.py,sha256=CYNvYkVhE_aDijV7v3gIxXHkN60QD3MNu9-Xh97q5Y4,19036
2
- machinevisiontoolbox/BundleAdjust.py,sha256=Fjp1eWe78DwZfvu9tuiuMYlCGmv8fqwF2kMFT1_GW4w,37076
3
- machinevisiontoolbox/Camera.py,sha256=YRG5s6rvlI9vJX2C3X2mawcTBofknjU8MiRc7atJv3o,139142
1
+ machinevisiontoolbox/BagOfWords.py,sha256=dfl-9v_CJmXyf2LUQnGUQBw7QWiQSw1qe8jN3fjE_30,19808
2
+ machinevisiontoolbox/BundleAdjust.py,sha256=07i0dxuKiLHthUWsLw0DU6hxvRXpMTZMSbipP9YVsYc,37498
3
+ machinevisiontoolbox/Camera.py,sha256=fHqCIyyF_QchRv-luZaz5nocxkcL_7c-n4rQSAh4qOk,143841
4
4
  machinevisiontoolbox/ImageBlobs.py,sha256=TsXaJpqUN1WrPg0zHRmLQvP3wUBxcHHUBt9qDggjVnE,89447
5
5
  machinevisiontoolbox/ImageColor.py,sha256=fueFYDNlvqSJPgcKsG5v2wBxaW7mYNa6OA-_rK-UJK0,17577
6
- machinevisiontoolbox/ImageConstants.py,sha256=R0sc9z8sbraruI6qkixLso6fTgxUAC5cQMSw4ms9m2k,38774
7
- machinevisiontoolbox/ImageCore.py,sha256=ufDI7ynOGRwM8TSSDEdM4LMQxBhclc0ve976tsprBW8,140594
6
+ machinevisiontoolbox/ImageConstants.py,sha256=TGzo62YR2_CFozena3ReZdTnuRHY_spQlgsMJoDa8RE,39154
7
+ machinevisiontoolbox/ImageCore.py,sha256=pQSJ3x-hmsTWvar0FpkGn4ZHi6fncy9d4We4fOr_ktA,143486
8
8
  machinevisiontoolbox/ImageFiducials.py,sha256=wrCevhrkgdHzBMMRoDqADZE9OOD7L6nQCd3UDA_e3rU,26903
9
- machinevisiontoolbox/ImageIO.py,sha256=MFUwjxdQ8oGttGMgzon9xOom-IGsVUFkiEWDrVEEgMs,27019
9
+ machinevisiontoolbox/ImageIO.py,sha256=BA-szxEJy6LSeq4DdBF845wmy3Nq2s_YjRko_dteWXA,27434
10
10
  machinevisiontoolbox/ImageLineFeatures.py,sha256=Kq0rCzPER2eB9MhEBNn6hAHpGY-G_WPZd657RmuRryA,10855
11
11
  machinevisiontoolbox/ImageMorph.py,sha256=WioUD2Jr9Ejnk7Let4Vf3iTFz5DhMkZ7l_ZBCbvgl4U,30237
12
12
  machinevisiontoolbox/ImageMultiview.py,sha256=swzNsxX39dijPdPa-a4LFOl7T7liTcQeP7NJf5ODGQU,15605
13
- machinevisiontoolbox/ImagePointFeatures.py,sha256=HYgBUs931OOYsM9kx4p_zFU5vAc9dOJsNPuUyTKxoY4,78209
13
+ machinevisiontoolbox/ImagePointFeatures.py,sha256=mf1GN37ZOvZRMhX25WAP9Twrymgk8yn1e7XXHkoq5-k,78807
14
14
  machinevisiontoolbox/ImageProcessing.py,sha256=HBR6cbTlgfAdOiqXU8DPSFKZQXWTBIVufCyRaifky7g,51669
15
15
  machinevisiontoolbox/ImageRegionFeatures.py,sha256=nZAdKFSpxmMZKwlbF13MOxBIUwXvPvDtIxNJUyp7pvg,16092
16
16
  machinevisiontoolbox/ImageReshape.py,sha256=F8eNrJ1to6d5jrIJeT1D2RqX-7EnARrQ03UGUCvljXg,50204
17
17
  machinevisiontoolbox/ImageSpatial.py,sha256=nzgScDprv7UQXhKcOps80gKjx4Ve6y498lCRm21sEtw,55622
18
18
  machinevisiontoolbox/ImageTensor.py,sha256=ohNbThdsQid2AQbDIHaCoxj00tpRggFWv0TTpIHAJQk,9006
19
- machinevisiontoolbox/ImageWholeFeatures.py,sha256=wpD03wsGu314Ff-nkDqrbCVGgPGobuQItnCZ04Xkwfo,62901
20
- machinevisiontoolbox/Kernel.py,sha256=VWobkVDlgMvQDo1eoC0fSLxAn2SGu2pQG13QaUIrRNE,21501
21
- machinevisiontoolbox/PointCloud.py,sha256=FJP-gFmd16ePFfBQ2Y-Tdu6SSw_HyEhtDjNeLyknJFY,25239
22
- machinevisiontoolbox/Sources.py,sha256=ZWH4o4ffT-qaJuRJOYT7YFLQV4WmzwzuH5J7ZWwzHvQ,152710
19
+ machinevisiontoolbox/ImageWholeFeatures.py,sha256=eLOZdG2OTyHUjkk6W2Wmp9mYOwqEFvfxQcFmGZgwX1s,63457
20
+ machinevisiontoolbox/Kernel.py,sha256=erTDpPsB38bNHvtBfijjf-9zIW57YM-Bg2ltlFCu6Mg,22607
21
+ machinevisiontoolbox/PointCloud.py,sha256=uwa-Y6aIV3_c6iIrfR60VkiCfPst6cSZntEXPhe2z2s,25819
22
+ machinevisiontoolbox/Sources.py,sha256=Odr3ome26j306JNYdak86RcutWT46chQwnSPsHNTScU,152656
23
23
  machinevisiontoolbox/VisualServo.py,sha256=2q_szKZo51McduLUMnN8-O6foLyLAR-o0h10-UDWKPY,60947
24
24
  machinevisiontoolbox/__init__.py,sha256=ocn82vw6o7pvUKSfebBwPKyKTiK-jMR8FK3SEtpDFVM,3871
25
25
  machinevisiontoolbox/_image_typing.py,sha256=FjdzZpuxEH9nBrj8gb-tkwUZDjpCNH92sQS3meemCVE,7286
@@ -27,21 +27,21 @@ machinevisiontoolbox/camera_derivatives.py,sha256=ysaP8PvgeBpFf8e37AmYQTW28SiFBK
27
27
  machinevisiontoolbox/decorators.py,sha256=6wEoLFozK2RFcGb9QrLy2bAw4sjkbm_2q3PpnIS9Rtk,2623
28
28
  machinevisiontoolbox/fiducial.py,sha256=syC7iIz95nYBTr_njo4ZzLXOyAdAIqoAZq_wRVhyUHQ,1374
29
29
  machinevisiontoolbox/mvtb_types.py,sha256=YD2P9mDXhz75GgC7Cyeer8luBpSHckkrLZ0GDVvGOnE,728
30
- machinevisiontoolbox/base/__init__.py,sha256=DxtRJJF6vnY2S1n33xJmXTBf504RCkctmoyKwBW4pc4,1645
31
- machinevisiontoolbox/base/color.py,sha256=F1eBcjyQPFV1uiTkeOe6UtBf_2V4s5wkQH-0egh7Ru0,58432
30
+ machinevisiontoolbox/base/__init__.py,sha256=RvRL9e_FMqBordxwHDqX44YGN38AD4m4fHfkEXslNK4,1666
31
+ machinevisiontoolbox/base/color.py,sha256=-8bjXwkgfRamNs-jZnFsbqpGKR_dN4rCrLqxrsUYjN8,59492
32
32
  machinevisiontoolbox/base/data.py,sha256=PF1HyrYQ-EDR-0heHMG1oDxs63AtQshNWbUvN8GOKsY,6130
33
33
  machinevisiontoolbox/base/findpeaks.py,sha256=Ag0UIw2CPVioB356Pvj-_k_wjxMODGpsW7qdkt6UvEs,11251
34
34
  machinevisiontoolbox/base/graphics.py,sha256=R8hbf_jK3mvq_dEoATxkjcbxUJvoSMovNbrk3W4BQe8,36974
35
- machinevisiontoolbox/base/imageio.py,sha256=QwiYa0XS2E1ZyOIPqGU72RFaPZQZAzKO47w_FBouKV8,54053
35
+ machinevisiontoolbox/base/imageio.py,sha256=uQceV4CDDhXrWMKgB4hhu3DxeJjEOKE2QJ8fatd4-MA,55592
36
36
  machinevisiontoolbox/base/meshgrid.py,sha256=Bo2GIlk0tRT2CV5sP5I9i-mZy-6c3BPozLOuOiEVHT0,3162
37
37
  machinevisiontoolbox/base/shapes.py,sha256=d-DmB_9iEsHyC6AT6lv2LMHvb-PMqHTXtehqMKk3aRQ,11430
38
- machinevisiontoolbox/base/types.py,sha256=AEiyEsmX5ljifC_PldaOcJnZRDyUSF3kwRbAihNDslQ,5055
38
+ machinevisiontoolbox/base/types.py,sha256=AkeDiJOh1nUmfhcfE-ZS_cISIPYIcSEJxzzWKkvkgTw,5687
39
39
  machinevisiontoolbox/base/version.py,sha256=uSKKfk9fOu5XF_21Kzfh3N111U-39mobNwiRyelOqcU,976
40
40
  machinevisiontoolbox/bin/_bintools.py,sha256=WwzyOqXEv4T-TB2GMBym4pGU2NACM5ACDCg7exGRl0I,1918
41
41
  machinevisiontoolbox/bin/_iminteractive.py,sha256=9fh2QcuO9RFfO9uGA05MePbe6cUtlh-F19z2zbG4pLA,17063
42
42
  machinevisiontoolbox/bin/bagtool.py,sha256=b-Ap2AynpXe0FtcydyfkXIgHDaumAkGBJgDZ6lgdZBU,12359
43
43
  machinevisiontoolbox/bin/imtool.py,sha256=la9m9LuMnthJzXxjMRWqGY_l7rJUZS5wiVgZsw84SO4,7897
44
- machinevisiontoolbox/bin/mvtbtool.py,sha256=NuVm2lLCJSUT4_RvlSplqeuUb0duDxAoupL1UIrjjuQ,13785
44
+ machinevisiontoolbox/bin/mvtbtool.py,sha256=7-fhz11-TIUCTPeDlpCxnpM7rONWcHRBTi80b537-nY,13871
45
45
  machinevisiontoolbox/bin/ocrtool.py,sha256=HtkbLnjij40oy83nBtZlbJSFJ2pn7lcQAmOifvCLw0I,4719
46
46
  machinevisiontoolbox/bin/tagtool.py,sha256=io-NCIMdMc5zIFYMa6U2ITTv2T-Q8Y0bwh_pB1qZ4IQ,8523
47
47
  machinevisiontoolbox/blocks/__init__.py,sha256=EsdfK2WxYLSo0SA5HSmlgsivLT1kDnoH9v-RTBDnWwc,142
@@ -51,8 +51,8 @@ machinevisiontoolbox/blocks/Icons/camera.png,sha256=CGcTB_llMoucN-rfnP6ZP_7AfE_9
51
51
  machinevisiontoolbox/blocks/Icons/estpose_p.png,sha256=1XrwL4OkotGWMwKmhnar3tt8cQWUdZiO7PxXD354T-E,4320
52
52
  machinevisiontoolbox/blocks/Icons/imageplane.png,sha256=34FYQMzkq6foxvRU8pn0KkcHxpHRvsJRJSYKfNu13Eg,3902
53
53
  machinevisiontoolbox/blocks/Icons/visjac_p.png,sha256=8X023l-C9Y5DfAN8wCdWYjSjq6wp1vRzuZ85sUbwxv8,3819
54
- machinevision_toolbox_python-2.2.0.dist-info/METADATA,sha256=djNu7vAO1sHkOR2lI9mGurJqY7889oMByoOCDl8M-no,21453
55
- machinevision_toolbox_python-2.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
56
- machinevision_toolbox_python-2.2.0.dist-info/entry_points.txt,sha256=R0sRa7JufKCG1VPkgo5KcL7j7pW4-E0Ch2Og4AqmXg4,310
57
- machinevision_toolbox_python-2.2.0.dist-info/licenses/LICENSE,sha256=niCzY_CzbIMKuUZlUkGimjrKp0vkLAtcwFmoSuSKrdw,1068
58
- machinevision_toolbox_python-2.2.0.dist-info/RECORD,,
54
+ machinevision_toolbox_python-2.4.0.dist-info/METADATA,sha256=LQ2UVCMI5WioCEYdVK_NQnCgjhjI-4JBSChJ2e6cZDE,21791
55
+ machinevision_toolbox_python-2.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
56
+ machinevision_toolbox_python-2.4.0.dist-info/entry_points.txt,sha256=R0sRa7JufKCG1VPkgo5KcL7j7pW4-E0Ch2Og4AqmXg4,310
57
+ machinevision_toolbox_python-2.4.0.dist-info/licenses/LICENSE,sha256=niCzY_CzbIMKuUZlUkGimjrKp0vkLAtcwFmoSuSKrdw,1068
58
+ machinevision_toolbox_python-2.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -58,6 +58,13 @@ class BagOfWords:
58
58
  of image features is assigned labels from the ``.centroids`` any with a
59
59
  label greater that ``.nstopwords`` is a stop word and can be discarded.
60
60
 
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
+
61
68
  :reference:
62
69
  - Video Google: a text retrieval approach to object matching in videos
63
70
  J.Sivic and A.Zisserman,
@@ -139,7 +146,12 @@ class BagOfWords:
139
146
  # total number of occurrences of word i
140
147
  # multiple occurrences in the one image count only as one
141
148
  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)
143
155
 
144
156
  M = []
145
157
 
@@ -405,7 +405,8 @@ if pgraph_installed:
405
405
 
406
406
  :seealso: :meth:`add_view` :meth:`add_landmark`
407
407
  """
408
- assert len(uv) == 2, "uv must be a 2-vector"
408
+ if len(uv) != 2:
409
+ raise ValueError("uv must be a 2-vector")
409
410
 
410
411
  edge = Observation(viewpoint, landmark, uv.flatten()) # create edge object
411
412
  e = viewpoint.connect(landmark, edge=edge) # connect nodes with it
@@ -562,6 +563,8 @@ if pgraph_installed:
562
563
  :type iterations: int, optional
563
564
  :param verbose: show Levenberg-Marquadt status, defaults to False
564
565
  :type verbose: bool, optional
566
+ :raises ValueError: no landmark projections have been added to
567
+ the graph
565
568
  :return: optimized state vector
566
569
  :rtype: ndarray(N)
567
570
 
@@ -578,6 +581,13 @@ if pgraph_installed:
578
581
 
579
582
  self.update_index()
580
583
 
584
+ if self.g.ne == 0:
585
+ raise ValueError(
586
+ "no landmark projections in the graph -- nothing to "
587
+ "optimize (add_projection() was never called, or every "
588
+ "candidate landmark was discarded)"
589
+ )
590
+
581
591
  if x is None:
582
592
  x = self.getstate()
583
593
  x0 = x
@@ -350,7 +350,8 @@ class CameraBase(ABC):
350
350
 
351
351
  :seealso: :meth:`nv` :meth:`width` :meth:`imagesize`
352
352
  """
353
- assert self._imagesize is not None, "imagesize not set"
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
- assert self._imagesize is not None, "imagesize not set"
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
- assert self._imagesize is not None, "imagesize not set"
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
- assert self._imagesize is not None, "imagesize not set"
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
- .. note:: If ``pose`` is not given it defaults to the pose of the
1181
- instance.
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
- self._plot_frustum(ax, pose, scale, alpha)
1241
+ CameraBase._plot_frustum(ax, pose, scale, alpha)
1220
1242
 
1221
1243
  elif shape == "camera":
1222
- self._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
1244
+ CameraBase._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
1223
1245
 
1224
1246
  if frame is True:
1225
- self.pose.plot(
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
- self.pose.plot(**frame)
1254
+ pose.plot(**frame)
1233
1255
  finally:
1234
1256
  ax.add_collection3d = _orig_add_collection3d
1235
1257
 
1236
1258
  return ax
1237
1259
 
1238
- def _plot_frustum(self, ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
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 kwargs: optional arguments as required for ransac', 'lmeds'
2688
- methods
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
- F, mask = cv2.findFundamentalMat(
2727
- points1=p1.T, points2=p2.T, method=points2F_dict[method], **kwargs
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