Skip to content

fix: OpenCV 5 compatibility fixes (aruco, HoughLinesP, BRISK/AKAZE, MSER) - #61

Merged
petercorke merged 7 commits into
mainfrom
feat/opencv5
Aug 10, 2026
Merged

petercorke merged 7 commits into
mainfrom
feat/opencv5

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

Applies the OpenCV 5 compatibility fixes identified in #44's audit:

  • ImagePointFeatures.py: BRISK/AKAZE relocated to cv2.xfeatures2d on OpenCV 5 — resolve via version-aware lookup
  • ImageFiducials.py: cv2.aruco.estimatePoseSingleMarkers removed outright on OpenCV 5 — replaced with cv2.solvePnP(..., flags=cv2.SOLVEPNP_IPPE_SQUARE), works unchanged on both versions
  • ImageFiducials.py: normalize aruco ids/matchImagePoints shapes — OpenCV 5 drops the singleton middle dimension ((N,1) → (N,))
  • ImageLineFeatures.py: same singleton-dimension fix for cv2.HoughLinesP's (N,1,4) → (N,4) shape change
  • ImageRegionFeatures.py: handle MSER's empty-tuple bboxes on OpenCV 5, resolve the discrepancy flagged in OpenCV 5's MSER finds fewer/zero regions than OpenCV 4 on identical input #54
  • test_graphics.py: relax draw_point pixel-count assertions — OpenCV 5 renders FONT_HERSHEY_* via a different embedded font
  • test_camera.py: add missing end-to-end coverage for CameraBase.images2C (checkerboard calibration) — verified the findChessboardCorners/cornerSubPix shape change doesn't break this pipeline, so no source fix needed there, just test coverage

Progresses #44 (does not close it — relaxing pyproject.toml's <5.0.0 pin and adding a CI version-matrix axis is tracked separately as future work in that issue).

Test plan

petercorke and others added 7 commits August 10, 2026 15:42
OpenCV 5's FONT_HERSHEY_* glyphs render via a different embedded font
than OpenCV 4, so an exact rendered-pixel-count assertion isn't stable
across versions -- affects both the default '+' marker (itself drawn
via cv2.putText) and explicit text= labels. Assert shape (some pixels
drawn, roughly centered) instead of an exact count.

Verified passing under real OpenCV 4.13.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OpenCV 5 dropped BRISK_create/AKAZE_create from the main cv2 namespace;
they now live under cv2.xfeatures2d (verified empirically:
opencv-contrib-python 5.0.0.93 vs 4.13.0.92, no OPENCV_ENABLE_NONFREE
build flag needed for these, unlike SURF). A prior code review flagged
the BRISK relocation as a "likely copy-paste slip" -- that's disproven:
cv2.BRISK_create genuinely does not exist in OpenCV 5.

Resolve by attribute existence (cv2 first, then cv2.xfeatures2d) rather
than a version-string check, so this keeps working unchanged if a
future OpenCV release moves things back or a build has both. SIFT/ORB
are unaffected, unchanged.

Added test_brisk/test_akaze -- there was no prior coverage at all for
either detector. Verified the regression tests actually catch the bug:
reverting the fix under a real OpenCV 5.0.0.93 install makes both fail
with AttributeError: module 'cv2' has no attribute 'BRISK_create',
confirming these aren't tests that would pass regardless.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OpenCV 5's HoughLinesP returns a true (N,4) array; OpenCV 4 returns
(N,1,4) (the old std::vector<T>-wrapping convention). lines_p()
unconditionally did lines[:, 0, :], which raises IndexError: too many
indices for array on OpenCV 5.

Guards on ndim/shape before indexing, rather than np.squeeze(): once
the shape is already fully confirmed, a direct index is simpler than
squeeze's extra validation, and squeeze has its own failure modes here
anyway -- axis-less squeeze would incorrectly collapse the N=1 case
(OpenCV 4's (1,1,4) loses its row dimension, becoming (4,) instead of
(1,4)), and a fixed squeeze(axis=1) crashes outright on OpenCV 5's
(N,4) output since axis 1 has size 4, not 1. Checks ndim before
shape[1] so an unexpected 1D/0D array short-circuits safely instead of
raising on the shape index.

Added a real test_lines_p (previously a TODO stub with no assertions
at all -- draws a line, exercises the actual detector). Verified the
test catches the regression: reverting the fix under a real OpenCV
5.0.0.93 install fails with the exact IndexError described above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two singleton-dim breaks in ImageFiducials.py, both verified
empirically (opencv-contrib-python 5.0.0.93 vs 4.13.0.92):

- ArucoDetector.detectMarkers()'s ids array is (N,1) on OpenCV 4 but a
  flat (N,) on OpenCV 5. Code doing id[0] to extract each tag's scalar
  ID broke on OpenCV 5 (id is already 0-d there) -- fixed by flattening
  ids once right after detectMarkers() in both fiducial() and
  FiducialCollection.estimatePose(), then using the scalar directly.

- Board.matchImagePoints() returns (M,1,3)/(M,1,2) on OpenCV 4 but true
  (M,3)/(M,2) on OpenCV 5 -- not previously identified. Found by
  running the existing test_matchImagePoints under a real OpenCV 5
  install (it already asserts the documented (M,1,3)/(M,1,2) shape).
  Normalized the return value back to that documented shape so the
  method's contract is stable across both versions.

Also spotted, logged separately (not fixed here, unrelated to opencv5
-- pre-existing on both versions): FiducialCollection.estimatePose's
ID-filtering step zips an already-filtered cornerss against the
original unfiltered ids, silently misaligning when any detected marker
isn't part of this board. See #52.

Full tests/test_image_fiducials.py passes under real OpenCV 4.13.
Under real OpenCV 5.0.0.93, 5/7 pass; the remaining 2
(test_draw, test_estimatePose) fail on cv2.aruco.estimatePoseSingleMarkers
being fully removed, which is Step 3c's fix, not this one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CentralCamera.images2C (checkerboard calibration) had zero test
coverage at all -- only referenced from an example notebook. Verified
empirically whether OpenCV 5's findChessboardCorners/cornerSubPix
returning (N,2) instead of OpenCV 4's (N,1,2) breaks this pipeline: it
does not. cv2.calibrateCamera and cv2.drawChessboardCorners both
accept either shape natively, and the raw corner arrays aren't part of
images2C's public return value (C, distortion, list of
CalibrationFrame namedtuples) -- so no source change was needed here,
unlike the other Step 3b fixes.

Added a real regression test instead: synthetic checkerboard views at
three different scales/positions, exercising the full
find-corners -> refine -> calibrate pipeline. Passes under both real
OpenCV 4.13.0 and 5.0.0.93, confirming the pipeline is genuinely
shape-agnostic rather than untested-and-hoping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cv2.aruco.estimatePoseSingleMarkers is completely removed in OpenCV 5
(not moved/renamed -- hasattr(cv2.aruco, "estimatePoseSingleMarkers")
is False in a real 5.0.0 install). Replaced with OpenCV's own
documented replacement for planar square markers: per-marker
cv2.solvePnP with SOLVEPNP_IPPE_SQUARE, using the same object-point
convention (clockwise from top-left, Z out of the marker) the removed
function used internally. This works unchanged on OpenCV 4 too, so no
version branching is needed -- it's a straight replacement, not a
compat shim.

Verified beyond shape compatibility: reprojecting the estimated pose's
object points back through the camera model reproduces the detected
corners exactly (0.0 px RMSE) on a synthetic fronto-parallel marker
scene, and the recovered distance matches the expected geometry.

Full tests/test_image_fiducials.py (7 tests, including the two that
were previously failing under OpenCV 5 -- test_draw, test_estimatePose)
now passes under both real OpenCV 4.13.0 and 5.0.0.93.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…epancy

Resolves the discrepancy flagged in tech-debt.md's original opencv5
incident writeup ("MSER indexing return shape changed") that an
earlier synthetic-image probe this session had disproven -- the probe
just didn't happen to hit the empty-results case.

Found by running the full test suite under a real OpenCV 5.0.0.93
install: test_mser_indexing crashed with TypeError: tuple indices must
be integers or slices, not tuple. Root cause: when detectRegions()
finds no regions, OpenCV 5 returns bboxes as an empty tuple () instead
of an empty ndarray(0,4) -- the unconditional bboxes[:, 2:] = ...
slice assignment in MSERFeature.__init__ only works on an ndarray.
Fixed by normalizing to np.zeros((0, 4)) first.

Separately, and not fixable by this shape-compat change: verified
empirically that OpenCV 5's MSER finds fewer/zero regions than OpenCV
4 for identical default parameters and identical grayscale input
(tests/data/shark2.png: 2 regions on 4.13.0, 0 on 5.0.0.93) -- a real
upstream algorithm-behavior difference. Logged as its own issue (#54)
rather than silently worked around. test_mser_indexing now skips (with
a clear reason and issue link) specifically when this exact zero-result
case occurs, rather than weakening the assertion for OpenCV 4 too or
gating on version alone -- it self-heals if the upstream behavior is
ever fixed.

tests/test_image_region_features.py: 3 passed on real OpenCV 4.13.0;
2 passed + 1 skipped (not failed) on real OpenCV 5.0.0.93.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
ErrorProne 1 high

View in Codacy

🟢 Metrics 10 complexity · 0 duplication

Metric Results
Complexity 10
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@petercorke
petercorke merged commit 5eb6bbf into main Aug 10, 2026
18 of 19 checks passed
@petercorke
petercorke deleted the feat/opencv5 branch August 10, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant