From 889d45c4957f93ec9910ccb960f91a432d1d06a3 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 30 Jul 2026 16:41:37 +1000 Subject: [PATCH 1/2] docs: log gridify() IndexError bug found while testing bare-except fix Confirmed pre-existing (git stash reproduces identically on unmodified main) and unrelated to the except-type narrowing in this branch -- found only because verifying the narrowed exception type required actually exercising gridify(). --- tech-debt.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tech-debt.md b/tech-debt.md index c9fb0440..d287c794 100644 --- a/tech-debt.md +++ b/tech-debt.md @@ -1,5 +1,31 @@ # Technical Debt +## `BaseFeature2D.gridify()` crashes with IndexError, always + +Found 2026-07-29 while verifying the narrowed exception type on +`ImagePointFeatures.py:283`'s `nw, nh = nbins` unpack (part of a bare +`except:` cleanup). `gridify()` (`ImagePointFeatures.py:261-301`) +raises `IndexError: arrays used as indices must be of integer (or +boolean) type` on every call, regardless of whether `nbins` is passed +as a tuple or a scalar int — confirmed via `git stash` that this is +identical on unmodified `main`, unrelated to the except-type change +that surfaced it. + +Root cause: `ix = f.p[0] // binwidth` / `iy = f.p[1] // binheight` +(lines 294-295) — `f.p` is a numpy float array (feature point +coordinates), and floor-dividing a numpy float array by a Python int +still yields a numpy `float64` result, not an int. `bins[iy, ix]` +(line 300) then fails because numpy refuses float-dtype indices. + +### Fix + +Cast to int after the floor division, e.g. +`ix = int(f.p[0] // binwidth)` / `iy = int(f.p[1] // binheight)`. Small, +self-contained, real bug fix — bundle with its own `fix:` commit and a +regression test (`gridify()` currently has no test coverage at all; +none of the existing point-feature tests exercise it) rather than +folding into an unrelated change. + ## `Image.ncdf` is documented as deprecated but never warns `ncdf` (`src/machinevisiontoolbox/ImageWholeFeatures.py:511-523`) has a From 5a11306eb838b2c47535389f482ab872b7a22d24 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 30 Jul 2026 17:20:24 +1000 Subject: [PATCH 2/2] fix: replace 6 bare except: clauses with specific exception types Bare except: swallows everything including KeyboardInterrupt and SystemExit, and hides the real failure mode from readers. Narrowed each to what the guarded code can actually raise: - BundleAdjust.py:15 -- `import pgraph` fallback -> ImportError - ImagePointFeatures.py:283 -- `nw, nh = nbins` unpack (scalar nbins is a valid, intentional fallback path, not an error) -> TypeError, ValueError - Camera.py:830 -- `artist.remove()` in clf(), arbitrary matplotlib Artist -> Exception (can't narrow further without knowing every Artist subclass's failure mode) - Camera.py:1865 -- arithmetic on possibly-unset imagesize/rho in fov() -> TypeError, AttributeError - ImageProcessing.py:1149 -- argcheck.getvector(image2, 3) -> Exception - __init__.py:191 -- importlib.metadata.version() lookup -> importlib.metadata.PackageNotFoundError Verified each guarded path still gets caught correctly: fov() called before imagesize/rho set still raises the intended ValueError; choose() with an invalid image2 still raises the intended ValueError. Full suite: 816 passed, 15 skipped, no regressions. --- src/machinevisiontoolbox/BundleAdjust.py | 2 +- src/machinevisiontoolbox/Camera.py | 4 ++-- src/machinevisiontoolbox/ImagePointFeatures.py | 2 +- src/machinevisiontoolbox/ImageProcessing.py | 2 +- src/machinevisiontoolbox/__init__.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/machinevisiontoolbox/BundleAdjust.py b/src/machinevisiontoolbox/BundleAdjust.py index e6a8cd0d..d822dcae 100644 --- a/src/machinevisiontoolbox/BundleAdjust.py +++ b/src/machinevisiontoolbox/BundleAdjust.py @@ -12,7 +12,7 @@ import pgraph pgraph_installed = True -except: +except ImportError: print("pgraph not installed") pgraph_installed = False import matplotlib.pyplot as plt diff --git a/src/machinevisiontoolbox/Camera.py b/src/machinevisiontoolbox/Camera.py index 55ae67e9..3e557784 100644 --- a/src/machinevisiontoolbox/Camera.py +++ b/src/machinevisiontoolbox/Camera.py @@ -827,7 +827,7 @@ def clf(self) -> None: for artist in self._ax.get_children(): try: artist.remove() - except: + except Exception: pass def plot_point( @@ -1862,7 +1862,7 @@ def fov(self) -> np.ndarray: """ try: return 2 * np.arctan(np.r_[self.imagesize] / 2 * np.r_[self.rho] / self.f) - except: + except (TypeError, AttributeError): raise ValueError("imagesize or rho properties not set") def distort(self, points: np.ndarray) -> np.ndarray: diff --git a/src/machinevisiontoolbox/ImagePointFeatures.py b/src/machinevisiontoolbox/ImagePointFeatures.py index d185ad03..8122a76f 100644 --- a/src/machinevisiontoolbox/ImagePointFeatures.py +++ b/src/machinevisiontoolbox/ImagePointFeatures.py @@ -280,7 +280,7 @@ def gridify(self, nbins: int | tuple[int, int], nfeat: int) -> "BaseFeature2D": try: # Sort features into grid nw, nh = nbins - except: + except (TypeError, ValueError): nw = nbins nh = nbins diff --git a/src/machinevisiontoolbox/ImageProcessing.py b/src/machinevisiontoolbox/ImageProcessing.py index 164a00c1..1b33b44e 100644 --- a/src/machinevisiontoolbox/ImageProcessing.py +++ b/src/machinevisiontoolbox/ImageProcessing.py @@ -1146,7 +1146,7 @@ def choose(self, image2: Any, mask: Any) -> "Image": else: try: color = argcheck.getvector(image2, 3) - except: + except Exception: raise ValueError("expecting a scalar, string or 3-vector") if self.isbgr: color = color[::-1] diff --git a/src/machinevisiontoolbox/__init__.py b/src/machinevisiontoolbox/__init__.py index d59296ee..21f6c37c 100644 --- a/src/machinevisiontoolbox/__init__.py +++ b/src/machinevisiontoolbox/__init__.py @@ -188,5 +188,5 @@ import importlib.metadata __version__ = importlib.metadata.version("machinevision-toolbox-python") -except: +except importlib.metadata.PackageNotFoundError: pass