machinevision-toolbox-python 2.3.0__tar.gz → 2.4.0__tar.gz

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.
Files changed (82) hide show
  1. machinevision_toolbox_python-2.4.0/.release-please-manifest.json +3 -0
  2. machinevision_toolbox_python-2.4.0/AGENTS.md +30 -0
  3. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/CHANGELOG.md +36 -0
  4. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/PKG-INFO +5 -5
  5. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/pyproject.toml +13 -5
  6. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/BagOfWords.py +13 -1
  7. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/BundleAdjust.py +11 -1
  8. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Camera.py +101 -18
  9. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageConstants.py +27 -6
  10. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageCore.py +69 -8
  11. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageIO.py +8 -0
  12. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImagePointFeatures.py +13 -2
  13. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageWholeFeatures.py +18 -5
  14. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Kernel.py +29 -0
  15. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/PointCloud.py +17 -4
  16. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Sources.py +4 -5
  17. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/__init__.py +1 -0
  18. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/color.py +42 -16
  19. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/imageio.py +541 -443
  20. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/types.py +14 -0
  21. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/mvtbtool.py +2 -1
  22. machinevision_toolbox_python-2.3.0/.release-please-manifest.json +0 -3
  23. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/.githooks/pre-commit +0 -0
  24. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/.gitignore +0 -0
  25. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/LICENSE +0 -0
  26. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/Makefile +0 -0
  27. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/README.md +0 -0
  28. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/README.md +0 -0
  29. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/blobs1.py +0 -0
  30. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/blobs2.py +0 -0
  31. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/bunny.py +0 -0
  32. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/color.py +0 -0
  33. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/corners.py +0 -0
  34. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/matching.py +0 -0
  35. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/monalisa.py +0 -0
  36. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/morphdemo.py +0 -0
  37. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/randomdots.py +0 -0
  38. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/ransac_line.py +0 -0
  39. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/ros_stream.py +0 -0
  40. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/showzip.py +0 -0
  41. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/torch-yolo.py +0 -0
  42. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/examples/torch_seg.py +0 -0
  43. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/release-please-config.json +0 -0
  44. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageBlobs.py +0 -0
  45. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageColor.py +0 -0
  46. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageFiducials.py +0 -0
  47. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageLineFeatures.py +0 -0
  48. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageMorph.py +0 -0
  49. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageMultiview.py +0 -0
  50. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageProcessing.py +0 -0
  51. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageRegionFeatures.py +0 -0
  52. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageReshape.py +0 -0
  53. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageSpatial.py +0 -0
  54. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageTensor.py +0 -0
  55. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/VisualServo.py +0 -0
  56. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/__init__.py +0 -0
  57. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/_image_typing.py +0 -0
  58. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/data.py +0 -0
  59. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/findpeaks.py +0 -0
  60. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/graphics.py +0 -0
  61. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/meshgrid.py +0 -0
  62. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/shapes.py +0 -0
  63. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/version.py +0 -0
  64. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/_bintools.py +0 -0
  65. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/_iminteractive.py +0 -0
  66. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/bagtool.py +0 -0
  67. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/imtool.py +0 -0
  68. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/ocrtool.py +0 -0
  69. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/tagtool.py +0 -0
  70. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/camera.png +0 -0
  71. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/estpose_p.png +0 -0
  72. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/imageplane.png +0 -0
  73. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/visjac_p.png +0 -0
  74. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/__init__.py +0 -0
  75. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/camera.py +0 -0
  76. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/test_blocks.py +0 -0
  77. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/camera_derivatives.py +0 -0
  78. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/decorators.py +0 -0
  79. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/fiducial.py +0 -0
  80. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/mvtb_types.py +0 -0
  81. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/symbolic/README.md +0 -0
  82. {machinevision_toolbox_python-2.3.0 → machinevision_toolbox_python-2.4.0}/symbolic/bundle-adjust.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.4.0"
3
+ }
@@ -0,0 +1,30 @@
1
+ # machinevision-toolbox-python — Agent Instructions
2
+
3
+ Part of the RVC ecosystem. **Read [rvc-ecosystem/AGENTS.md](https://github.com/petercorke/rvc-ecosystem/blob/main/AGENTS.md) first** — it defines shared conventions: repo ownership, math invariants, dependency boundaries, git/PR workflow, code standards, tech-debt tracking. This file only adds what's specific to this repo.
4
+
5
+ | | |
6
+ |---|---|
7
+ | PyPI package | `machinevision-toolbox-python` |
8
+ | Nickname | MVTB |
9
+ | Owner | Peter Corke (`petercorke`) |
10
+ | Default branch | `main` |
11
+ | Contribution model | Branch → PR; direct push to `main` at Peter's discretion |
12
+
13
+ ## Notes specific to this repo
14
+
15
+ - Pure Python — no compiled extensions, builds with Hatch (`hatchling`) directly.
16
+ - Depends on `spatialmath`, `pgraph-python`, `ansitable`, plus the third-party `opencv-python`/
17
+ `opencv-contrib-python` and `open3d`.
18
+ - `open3d` is consistently a Python version or two behind — never assume it supports the
19
+ latest Python release; check compatibility before bumping this repo's Python floor/ceiling.
20
+ - OpenCV 5.x support was just added alongside the existing 4.x support (a major version jump,
21
+ not a minor bump). This is fresh work — aim to keep both working, but treat 4.x/5.x
22
+ compatibility friction as expected teething problems to fix, not a regression.
23
+ - Tech-debt tracked as GitHub Issues labelled `tech-debt` (migrated 2026-08-02) — this repo
24
+ was first to migrate off `tech-debt.md`.
25
+ - This repo's `.github/pull_request_template.md` and `.github/ISSUE_TEMPLATE/*.yml` are the
26
+ canonical source other repos' templates were copied from — check here first if updating
27
+ template content ecosystem-wide.
28
+ - Codacy is wired in (badge + automated PR comments); grade A is the aspiration. Real backlog
29
+ exists, tracked via `tech-debt`-labelled issues (including a "mypy not in CI" entry) — see
30
+ ecosystem `AGENTS.md` §5 for how that target is framed.
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.0](https://github.com/petercorke/machinevision-toolbox-python/compare/v2.3.0...v2.4.0) (2026-08-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * add __array__ protocol to Image and Kernel, ndim to Kernel ([e762f57](https://github.com/petercorke/machinevision-toolbox-python/commit/e762f578faa269655cbd06b02d48c4c30d48c516))
9
+ * relax opencv pin to support both 4.x and 5.x, add CI matrix axis ([2ec69fe](https://github.com/petercorke/machinevision-toolbox-python/commit/2ec69fe4d116bb51f717e0a4a3c8baed95a7101f)), closes [#44](https://github.com/petercorke/machinevision-toolbox-python/issues/44)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * assert hygiene -- unreachable assert, two caller-facing checks ([7afe33e](https://github.com/petercorke/machinevision-toolbox-python/commit/7afe33eb02af65d4e64d52481f668a610f63b807))
15
+ * CameraBase.plot() unbound call (self=None) crashed instead of working ([e2d4600](https://github.com/petercorke/machinevision-toolbox-python/commit/e2d460029d6ef1d5bb807168f37b37b3eb10ff1c))
16
+ * catch cv2.error in points2F(), not just F/mask=None ([c4197cb](https://github.com/petercorke/machinevision-toolbox-python/commit/c4197cbd22784b8c002df8a02bb95764dc589b50))
17
+ * correct stale open3d-python PyPI name, add py3.13 exclusion marker ([87c1e8f](https://github.com/petercorke/machinevision-toolbox-python/commit/87c1e8f5a2eb3a2b17f2a1f1c182b22fbb025483))
18
+ * esttheta() -- broken pickregion() helper and missing theta-sweep body ([50b00cc](https://github.com/petercorke/machinevision-toolbox-python/commit/50b00cc95c15b56b3598505f4cd532a0612f6ff9))
19
+ * exclude tests/ from Bandit's assert_used (B101) check ([e62b116](https://github.com/petercorke/machinevision-toolbox-python/commit/e62b116e0b57aef73882ee4689153eda326c399f))
20
+ * Histogram.plot(type='ncdf') never actually worked, docstring lied ([6d4152d](https://github.com/petercorke/machinevision-toolbox-python/commit/6d4152d58bb63970863ff0ca49593c21ec815b5b))
21
+ * idisp() crashes when reuse=True with no title given ([a05b13b](https://github.com/petercorke/machinevision-toolbox-python/commit/a05b13b93ab4ed03397548348e8de48230b284f3))
22
+ * Image.metadata() missing camera-specific Exif sub-IFD tags ([d554ef2](https://github.com/petercorke/machinevision-toolbox-python/commit/d554ef257d2d494c66daaf928114c8097fb8fb91))
23
+ * Image() drops maxintval when dtype= is also given, silently truncating ([15f1d32](https://github.com/petercorke/machinevision-toolbox-python/commit/15f1d32576183c862592dca009828ea7e2765d79))
24
+ * Image(dtype='float') resolved to float64, not float32 ([18c5d85](https://github.com/petercorke/machinevision-toolbox-python/commit/18c5d85d82ebd2c09a43adb278e4435658f0c2b7))
25
+ * ImageConstantsMixin factories silently downcast explicit dtype requests ([977dbf6](https://github.com/petercorke/machinevision-toolbox-python/commit/977dbf6402307f38f695c697d68009ddd914d237))
26
+ * mvtbtool's %precision line spuriously echoes with --showassign ([ed43ca6](https://github.com/petercorke/machinevision-toolbox-python/commit/ed43ca64652a7d64a4536ae5665e672ffcc7db4a))
27
+ * name2color() colororder reordering returns list, not ndarray ([c90e6fb](https://github.com/petercorke/machinevision-toolbox-python/commit/c90e6fb4db7814707fd1548a7d87b1057b665499))
28
+ * PointCloud.Read() double-joins a caller-supplied "data/" prefix ([fff671c](https://github.com/petercorke/machinevision-toolbox-python/commit/fff671cdd28c85a555ee9fc0b7dd6f4bad4eccc9))
29
+ * printstats() crashes if .stats hasn't been accessed yet ([3484224](https://github.com/petercorke/machinevision-toolbox-python/commit/34842247c529159364591528ec109deac182899f))
30
+ * suppress divide-by-zero RuntimeWarning in BagOfWords idf computation ([05c85e0](https://github.com/petercorke/machinevision-toolbox-python/commit/05c85e0189061019dc55888b33ddb30cc3e9d9ae))
31
+ * visual-odometry robustness -- match() metric, points2F(), BundleAdjust ([8691847](https://github.com/petercorke/machinevision-toolbox-python/commit/8691847df58cc3cf003838436f7d94c59d5ae609))
32
+
33
+
34
+ ### Documentation
35
+
36
+ * add AGENTS.md stub linking to rvc-ecosystem conventions ([0827291](https://github.com/petercorke/machinevision-toolbox-python/commit/082729184414b821a9e610f8a409f5cbeabc308c))
37
+ * points2F() kwargs -- correct which params apply to which method ([6b10d64](https://github.com/petercorke/machinevision-toolbox-python/commit/6b10d6401925564163568a09e1e504c99eec99fa))
38
+
3
39
  ## [2.3.0](https://github.com/petercorke/machinevision-toolbox-python/compare/v2.2.0...v2.3.0) (2026-08-13)
4
40
 
5
41
  **Highlight: Notebooks working in all environments** Fixed a number of problems with getting notebooks to execute in desktop Jupyter, Colab, and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: machinevision-toolbox-python
3
- Version: 2.3.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'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "machinevision-toolbox-python"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  authors = [{ name = "Peter Corke", email = "rvc@petercorke.com" }]
5
5
  description = "Object-oriented machine vision for Python."
6
6
  readme = "README.md"
@@ -39,8 +39,8 @@ dependencies = [
39
39
  "scipy",
40
40
  "matplotlib",
41
41
  "pillow",
42
- "opencv-python<5.0.0",
43
- "opencv-contrib-python<5.0.0",
42
+ "opencv-python>=4.5,<6.0.0",
43
+ "opencv-contrib-python>=4.5,<6.0.0",
44
44
  "spatialmath-python",
45
45
  "pgraph-python",
46
46
  "ansitable",
@@ -86,7 +86,7 @@ docs = [
86
86
 
87
87
  jupyter = ["ipywidgets", "ipympl"]
88
88
 
89
- open3d = ["open3d-python"]
89
+ open3d = ["open3d; python_version < '3.13'"]
90
90
 
91
91
  torch = ["torch"]
92
92
 
@@ -95,7 +95,7 @@ ocr = ["pytesseract"]
95
95
  all = [
96
96
  "ipywidgets",
97
97
  "ipympl",
98
- "open3d-python",
98
+ "open3d; python_version < '3.13'",
99
99
  "torch",
100
100
  "pytesseract",
101
101
  "rosbags",
@@ -119,6 +119,14 @@ select = ["E", "F", "I"]
119
119
  [tool.ruff.lint.isort]
120
120
  known-first-party = ["machinevisiontoolbox"]
121
121
 
122
+ [tool.bandit]
123
+ # bare `assert` is idiomatic pytest style, not a security concern -- Bandit's
124
+ # B101 ("assert used") is meant for src/, where assert can silently vanish
125
+ # under `python -O`. Excluding tests/ keeps that check meaningful without
126
+ # flagging every test assertion (Codacy runs Bandit via Prospector and
127
+ # honours this file).
128
+ exclude_dirs = ["tests"]
129
+
122
130
  [tool.pyright]
123
131
  include = ["src"]
124
132
  ignore = ["docs", "tests", "examples"]
@@ -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