machinevision-toolbox-python 2.2.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/.githooks/pre-commit +83 -0
  2. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/.gitignore +4 -0
  3. machinevision_toolbox_python-2.4.0/.release-please-manifest.json +3 -0
  4. machinevision_toolbox_python-2.4.0/AGENTS.md +30 -0
  5. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/CHANGELOG.md +64 -0
  6. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/PKG-INFO +18 -14
  7. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/README.md +12 -8
  8. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/pyproject.toml +13 -5
  9. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/BagOfWords.py +13 -1
  10. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/BundleAdjust.py +11 -1
  11. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Camera.py +101 -18
  12. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageConstants.py +27 -6
  13. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageCore.py +69 -8
  14. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageIO.py +8 -0
  15. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImagePointFeatures.py +13 -2
  16. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageWholeFeatures.py +18 -5
  17. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Kernel.py +29 -0
  18. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/PointCloud.py +17 -4
  19. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/Sources.py +4 -5
  20. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/__init__.py +1 -0
  21. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/color.py +42 -16
  22. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/imageio.py +541 -443
  23. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/types.py +14 -0
  24. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/mvtbtool.py +2 -1
  25. machinevision_toolbox_python-2.2.0/.release-please-manifest.json +0 -3
  26. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/LICENSE +0 -0
  27. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/Makefile +0 -0
  28. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/README.md +0 -0
  29. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/blobs1.py +0 -0
  30. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/blobs2.py +0 -0
  31. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/bunny.py +0 -0
  32. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/color.py +0 -0
  33. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/corners.py +0 -0
  34. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/matching.py +0 -0
  35. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/monalisa.py +0 -0
  36. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/morphdemo.py +0 -0
  37. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/randomdots.py +0 -0
  38. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/ransac_line.py +0 -0
  39. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/ros_stream.py +0 -0
  40. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/showzip.py +0 -0
  41. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/torch-yolo.py +0 -0
  42. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/examples/torch_seg.py +0 -0
  43. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/release-please-config.json +0 -0
  44. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageBlobs.py +0 -0
  45. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageColor.py +0 -0
  46. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageFiducials.py +0 -0
  47. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageLineFeatures.py +0 -0
  48. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageMorph.py +0 -0
  49. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageMultiview.py +0 -0
  50. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageProcessing.py +0 -0
  51. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageRegionFeatures.py +0 -0
  52. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageReshape.py +0 -0
  53. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageSpatial.py +0 -0
  54. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/ImageTensor.py +0 -0
  55. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/VisualServo.py +0 -0
  56. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/__init__.py +0 -0
  57. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/_image_typing.py +0 -0
  58. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/data.py +0 -0
  59. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/findpeaks.py +0 -0
  60. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/graphics.py +0 -0
  61. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/meshgrid.py +0 -0
  62. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/shapes.py +0 -0
  63. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/base/version.py +0 -0
  64. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/_bintools.py +0 -0
  65. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/_iminteractive.py +0 -0
  66. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/bagtool.py +0 -0
  67. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/imtool.py +0 -0
  68. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/ocrtool.py +0 -0
  69. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/bin/tagtool.py +0 -0
  70. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/camera.png +0 -0
  71. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/estpose_p.png +0 -0
  72. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/imageplane.png +0 -0
  73. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/Icons/visjac_p.png +0 -0
  74. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/__init__.py +0 -0
  75. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/camera.py +0 -0
  76. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/blocks/test_blocks.py +0 -0
  77. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/camera_derivatives.py +0 -0
  78. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/decorators.py +0 -0
  79. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/fiducial.py +0 -0
  80. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/src/machinevisiontoolbox/mvtb_types.py +0 -0
  81. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/symbolic/README.md +0 -0
  82. {machinevision_toolbox_python-2.2.0 → machinevision_toolbox_python-2.4.0}/symbolic/bundle-adjust.py +0 -0
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Pre-commit hook: keep notebook outputs clear and bootstrap cells in sync.
3
+
4
+ Activated once per machine via `git config core.hooksPath .githooks`
5
+ (see docs/notebooks/README.md for the full explanation). Fixes any affected
6
+ notebook in place and re-stages it, so the commit proceeds with the fix already
7
+ applied -- no interruption, no separate re-commit step.
8
+ """
9
+
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ REPO_ROOT = Path(__file__).resolve().parent.parent
15
+ NOTEBOOKS_DIR = REPO_ROOT / "docs" / "notebooks"
16
+
17
+
18
+ def staged_files(pattern: str) -> list[str]:
19
+ result = subprocess.run(
20
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR", "--", pattern],
21
+ cwd=REPO_ROOT,
22
+ capture_output=True,
23
+ text=True,
24
+ check=True,
25
+ )
26
+ return [line for line in result.stdout.splitlines() if line]
27
+
28
+
29
+ def main() -> int:
30
+ if not NOTEBOOKS_DIR.exists():
31
+ return 0
32
+
33
+ staged_notebooks = staged_files("docs/notebooks/*.ipynb")
34
+ template_staged = bool(staged_files("docs/notebooks/_mvtb_nb_bootstrap.py"))
35
+
36
+ # Output-clearing only ever touches notebooks actually staged for this commit.
37
+ output_targets = staged_notebooks
38
+
39
+ # Bootstrap-cell regeneration also has to cover every notebook when the
40
+ # template itself changed -- otherwise the other notebooks go stale and CI
41
+ # fails on files this commit never touched.
42
+ if template_staged:
43
+ bootstrap_targets = sorted(
44
+ str(p.relative_to(REPO_ROOT)) for p in NOTEBOOKS_DIR.glob("*.ipynb")
45
+ )
46
+ else:
47
+ bootstrap_targets = staged_notebooks
48
+
49
+ if not output_targets and not bootstrap_targets:
50
+ return 0
51
+
52
+ changed_paths: set[str] = set()
53
+
54
+ try:
55
+ if output_targets:
56
+ paths = [str(REPO_ROOT / t) for t in output_targets]
57
+ subprocess.run(
58
+ [sys.executable, str(NOTEBOOKS_DIR / "clear_outputs.py"), *paths],
59
+ cwd=REPO_ROOT,
60
+ check=True,
61
+ )
62
+ changed_paths.update(paths)
63
+
64
+ if bootstrap_targets:
65
+ paths = [str(REPO_ROOT / t) for t in bootstrap_targets]
66
+ subprocess.run(
67
+ [sys.executable, str(NOTEBOOKS_DIR / "sync_bootstrap.py"), *paths],
68
+ cwd=REPO_ROOT,
69
+ check=True,
70
+ )
71
+ changed_paths.update(paths)
72
+ except subprocess.CalledProcessError:
73
+ print("pre-commit hook: notebook cleanup failed unexpectedly", file=sys.stderr)
74
+ return 1
75
+
76
+ if changed_paths:
77
+ subprocess.run(["git", "add", "--", *sorted(changed_paths)], cwd=REPO_ROOT, check=True)
78
+
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ sys.exit(main())
@@ -9,6 +9,7 @@ dependencies*
9
9
  *.bag
10
10
  # but keep test fixtures
11
11
  !mvtb-data/mvtbdata/data/*.bag
12
+ bags
12
13
 
13
14
  # old notebooks
14
15
  notebooks/old/*
@@ -94,3 +95,6 @@ docs/source/stubs/*
94
95
 
95
96
  # LLVM profiling data
96
97
  *.profraw
98
+
99
+ ### Node ###
100
+ node_modules/
@@ -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,69 @@
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
+
39
+ ## [2.3.0](https://github.com/petercorke/machinevision-toolbox-python/compare/v2.2.0...v2.3.0) (2026-08-13)
40
+
41
+ **Highlight: Notebooks working in all environments** Fixed a number of problems with getting notebooks to execute in desktop Jupyter, Colab, and
42
+ JupyterLite in browser. Created nice framework to keep the environment-specific import logic in each notebook consistent at commit time. Added
43
+ CI tests and also CI check that notebooks runs in the pyodide environment. Currently pinning to pyodide 0.6.1 to sidestep the lack of JSPI support
44
+ in Safari and Firefox required by later pyodide kernels.
45
+
46
+
47
+ ### Features
48
+
49
+ * **ci:** add a real-Pyodide dependency-completeness check for JupyterLite ([9072da6](https://github.com/petercorke/machinevision-toolbox-python/commit/9072da6e945b45bf2f04a5994f9dbc9c6d99622d))
50
+ * **notebooks:** add shared environment-bootstrap module and drift-check tooling ([44242e8](https://github.com/petercorke/machinevision-toolbox-python/commit/44242e81420e05d2d5656c81c53a33f1071ddac7))
51
+ * **notebooks:** migrate all notebooks to the generated bootstrap cell ([75f230c](https://github.com/petercorke/machinevision-toolbox-python/commit/75f230ce7d789e25edcd06694e886e0f4db8a83d))
52
+
53
+
54
+ ### Bug Fixes
55
+
56
+ * **ci:** guard release-please's PyPI-publish trigger against the truthy-string bug ([#74](https://github.com/petercorke/machinevision-toolbox-python/issues/74)) ([0d74276](https://github.com/petercorke/machinevision-toolbox-python/commit/0d742769d2553125a5b2a08a4a8a16f1e9b9f69c))
57
+ * **ci:** pass --repo to gh calls in release-please's PyPI-publish trigger ([#73](https://github.com/petercorke/machinevision-toolbox-python/issues/73)) ([6ff01a3](https://github.com/petercorke/machinevision-toolbox-python/commit/6ff01a395402d6363ade8b4f4e07952237089666))
58
+ * **docs:** pin jupyterlite-pyodide-kernel==0.6.1, matching RTB and bdsim ([76bd7e1](https://github.com/petercorke/machinevision-toolbox-python/commit/76bd7e130dab5562bb947be9a6b6c1ef3a5e2140))
59
+ * **notebooks:** fix JupyterLite bugs found via real-browser testing ([3979404](https://github.com/petercorke/machinevision-toolbox-python/commit/3979404b317a79f2769007a02ff7a24091e1e4b7))
60
+ * release-please never triggered the actual PyPI publish ([18cc08b](https://github.com/petercorke/machinevision-toolbox-python/commit/18cc08bc21e38bef2f5ed56e31daee4c9404732a))
61
+
62
+
63
+ ### Documentation
64
+
65
+ * **readme:** move to standard badge layout ([0722f9d](https://github.com/petercorke/machinevision-toolbox-python/commit/0722f9da96c97e2d49e791fd68237063df8d38cf))
66
+
3
67
  ## [2.2.0](https://github.com/petercorke/machinevision-toolbox-python/compare/v2.1.0...v2.2.0) (2026-08-10)
4
68
 
5
69
  **Highlight: OpenCV 5 compatibility.** MVTB now works correctly under both OpenCV 4 and the
@@ -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
 
@@ -21,20 +21,24 @@
21
21
 
22
22
  ---
23
23
 
24
- ### Status & Ecosystem
25
- [![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)
26
- [![QUT Centre for Robotics Open Source](https://github.com/qcr/qcr.github.io/raw/master/misc/badge.svg)](https://qcr.github.io)
24
+ ### Status & Project Health
25
+
27
26
  [![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)
28
- [![Coverage](https://codecov.io/gh/petercorke/machinevision-toolbox-python/branch/main/graph/badge.svg)](https://codecov.io/gh/petercorke/machinevision-toolbox-python)
27
+ [![Downloads](https://static.pepy.tech/badge/machinevision-toolbox-python/month)](https://pepy.tech/projects/machinevision-toolbox-python)
29
28
  ![Python Version](https://img.shields.io/pypi/pyversions/machinevision-toolbox-python.svg)
29
+ [![Coverage](https://codecov.io/gh/petercorke/machinevision-toolbox-python/branch/main/graph/badge.svg)](https://codecov.io/gh/petercorke/machinevision-toolbox-python)
30
30
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
- [![PyPI - Downloads](https://img.shields.io/pypi/dw/machinevision-toolbox-python)](https://pypistats.org/packages/machinevision-toolbox-python)
32
31
 
33
- ### Powered by
34
- [![Powered by Spatial Maths](https://raw.githubusercontent.com/petercorke/spatialmath-python/master/.github/svg/sm_powered.min.svg)](https://github.com/petercorke/spatialmath-python)
35
- [![Powered by NumPy](https://img.shields.io/badge/powered_by-NumPy-013243?logo=numpy&logoColor=white)](https://numpy.org)
32
+ ### Ecosystem & Dependencies
33
+ [![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)
34
+ [![QUT Centre for Robotics Open Source](https://github.com/qcr/qcr.github.io/raw/master/misc/badge.svg)](https://qcr.github.io)
35
+
36
+ [![powered by NumPy](https://img.shields.io/badge/powered_by-NumPy-013243?logo=numpy&logoColor=white)](https://numpy.org)
37
+ [![powered by SciPy](https://img.shields.io/badge/powered_by-SciPy-0054a6?logo=scipy&logoColor=white)](https://scipy.org)
38
+ [![powered by Matplotlib](https://img.shields.io/badge/powered_by-Matplotlib-11557c?logo=matplotlib&logoColor=white)](https://matplotlib.org)
36
39
  [![Powered by OpenCV](https://raw.githubusercontent.com/petercorke/machinevision-toolbox-python/main/.github/svg/opencv_powered.svg)](https://opencv.org)
37
40
  [![Powered by Open3D](https://raw.githubusercontent.com/petercorke/machinevision-toolbox-python/main/.github/svg/open3d_powered.svg)](https://open3d.org)
41
+ [![Powered by Spatial Maths](https://raw.githubusercontent.com/petercorke/spatialmath-python/master/.github/svg/sm_powered.min.svg)](https://github.com/petercorke/spatialmath-python)
38
42
 
39
43
  ## Synopsis
40
44
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "machinevision-toolbox-python"
3
- version = "2.2.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