Skip to content

v1.4.2: RANSAC threshold space, reporting of unrecognized arguments - #9

Merged
rfonod merged 4 commits into
mainfrom
v1.4.2-dev
Sep 17, 2026
Merged

rfonod merged 4 commits into
mainfrom
v1.4.2-dev

Conversation

@rfonod

@rfonod rfonod commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Two additions that came out of benchmarking stabilo, both with no change to the default pipeline.

The RANSAC threshold's units are now explicit

Keypoints detected on a downsampled frame are rescaled to full resolution before cv2.findHomography sees them, so ransac_epipolar_threshold is a full-resolution pixel distance whatever downsample_ratio is. The keypoint localization error it has to separate from real outliers is not: that arises in the processed image and is magnified by 1 / downsample_ratio on the way back. The same threshold value is therefore a stricter filter at a low ratio than at a high one, by exactly the ratio between them, and a value tuned at one ratio does not transfer to another. Going from 1.0 to 0.18 makes it about 5.6x stricter relative to that noise.

This was never wrong at a fixed ratio, and no result computed at one is affected. It is a confound only when the ratio is swept, which is what a compute-budget axis does.

ransac_threshold_space names the convention: 'full' (the existing behaviour, and the default) or 'processed', which passes ransac_epipolar_threshold / downsample_ratio to the estimator and so holds the threshold fixed in the units the noise is generated in. The two coincide at downsample_ratio: 1.0. get_ransac_reproj_threshold() reports the value actually handed to the estimator, and --ransac-threshold-space / -rts exposes it on the CLI.

Neither convention is right in general: the residuals a threshold separates are part localization noise, which scales with 1 / downsample_ratio, and part model error, which does not. Which dominates is a property of the scenes, so this supplies the mechanism and leaves the choice to whoever tunes it.

Unrecognized Stabilizer arguments are reported

_load_configuration honours only the keys of cfg/default.yaml, so a misspelled parameter had no effect and said nothing: Stabilizer(ransac_epipolar_treshold=1.0) ran with the default threshold of 2.0. Unknown keys are now collected and logged with the nearest valid name.

They are warned about rather than raised on, which is the load-bearing part. Callers that forward a user's configuration block verbatim may carry keys stabilo does not know, so an exception would turn a typo in a downstream configuration into a crash inside this constructor. Behaviour is otherwise unchanged: the unknown key is ignored exactly as before, and every known key keeps its value.

Stabilizer.configurable_keys() exposes the valid set, so a caller can filter or check a configuration against stabilo rather than maintaining its own copy of the list, which drifts as soon as stabilo gains an option.

The CLI is the first user of it. separate_cli_arguments handed the whole argparse namespace to Stabilizer(**kwargs), including the subcommand's own options (the input path, --save, the visualization flags, argparse's func), which the Stabilizer silently dropped; they would now be reported on every invocation. They are filtered against configurable_keys() instead. A --custom-config file is deliberately not filtered, since a typo there is what the report exists to surface.

Documentation

docs/usage.md section 11 gains a subsection on why the epipolar threshold does not transfer across downsample ratios, what each threshold space measures, and a two-line example of the same configuration under both. Section 8 states that a keyword argument outside cfg/default.yaml is ignored and reported, and names configurable_keys(). The default.yaml comment block above the estimation keys explains the coupling at the point of use, and the README feature list gains the new flag.

Compatibility

No behaviour change and no API removal. ransac_threshold_space defaults to 'full', which is what every previous version did, and every existing configuration and call produces the same output.

Verified on the bundled tests/ND_before.jpg / ND_after.jpg pair against v1.4.1: identical homography, inlier count, match count and keypoint counts for the stabilo defaults, for geo-trax's registration kwargs, for SIFT at downsample_ratio: 0.25 (the case the threshold change would move if it reached the default path), and for ORB at full resolution. geo-trax's own estimate_homography was run against this branch, clean and with a deliberately misspelled key forwarded through it: same homography, no exception, and the new warning naming the key it meant.

Same dependency floors and the same Python requirement (>= 3.11) as v1.4.1.

Verification

  • 147 passed, 16 skipped (19 new cases); ruff check and ruff format unchanged from baseline
  • python -m build and twine check pass, and the wheel's metadata keeps the License, Author and Maintainer fields restored in v1.4.1
  • The first commit was checked out on its own and the suite run there, so the branch has no broken intermediate state

Keypoints detected on a downsampled frame are rescaled to full resolution before
the estimator sees them, so ransac_epipolar_threshold is a full-resolution pixel
distance whatever downsample_ratio is, while the keypoint localization error it
has to separate arises in the processed image and is magnified by 1 / ratio. The
same value is therefore a stricter filter at a low ratio than at a high one, and
a threshold tuned at one ratio does not transfer to another.

Add ransac_threshold_space, 'full' (the existing behavior, and the default) or
'processed', which passes ransac_epipolar_threshold / downsample_ratio to the
estimator and so keeps the threshold fixed in the units the noise is generated
in. get_ransac_reproj_threshold() reports the value actually handed to the
estimator. The two settings coincide at downsample_ratio 1.0, and no existing
configuration changes.
Only the keys of cfg/default.yaml are honoured, so a misspelled parameter had no
effect and said nothing: ransac_epipolar_treshold=1.0 ran with the default
threshold. Unknown keys are now collected and logged with the nearest valid name.

They are warned about rather than raising, because callers that forward a user's
configuration block verbatim may carry keys stabilo does not know, and an
exception would turn a typo in a downstream configuration into a crash inside the
constructor. Behavior is otherwise unchanged: the unknown key is ignored exactly
as before and every known key keeps its value.

Stabilizer.configurable_keys() exposes the valid names, so a caller can filter or
check a configuration against stabilo instead of keeping its own list. The CLI
uses it: separate_cli_arguments handed the whole argparse namespace to
Stabilizer(**kwargs), including the subcommand's own options, which would now be
reported on every invocation. A --custom-config file is not filtered, since a
typo there is what the report exists to surface.
…guments

usage.md section 11 gains a subsection on why the epipolar threshold does not
transfer across downsample ratios and what each threshold space measures, with
the two-line example and the parameter table row; section 8 states that a keyword
argument outside cfg/default.yaml is ignored and reported, and names
configurable_keys(). The README feature list gains the new flag.
Version in __init__.py, CITATION.cff, the README citation block and
.zenodo.json; release date 2026-09-17 in CITATION.cff and in the Zenodo
"updated" date; the Zenodo related identifier and the embedded README point at
the v1.4.2 tree.
@rfonod
rfonod merged commit f97dc66 into main Sep 17, 2026
10 checks passed
@rfonod
rfonod deleted the v1.4.2-dev branch September 17, 2026 17:32
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