webarkit / webarkit/jsfeatNext

feat(cv_backend): optional filterMatches step (GMS seam)

Open
#129 0 comments 0 reactions 1 assignee View on GitHub

@kalwalt is already working on this.

Since Aug 16, 2026.

code design enhancement Typescript
Dominant language
TypeScript
Stars
12
Forks
4
Avg merge
16h 24m
Merged PRs (30d)
34

Description

Add an optional filterMatches step to CvBackend (seam for GMS and other geometric filters)

Summary

Add one optional method to the CvBackend contract (#96), sitting between match and estimateHomography:

filterMatches?(
    matches: Match[],
    query: FilterView,
    train: FilterView,
    options?: FilterOptions
): Match[];

This is the seam for geometric consistency filtering — concretely, GMS (Grid-based Motion Statistics, Bian et al., CVPR 2017), with room for LOGOS or others later. No filter is implemented in this issue; this defines where one plugs in.

Why

The step is missing from the pipeline, and it is not optional in practice

The contract currently goes match → estimateHomography. Raw descriptor matches — even after a Lowe ratio test — typically carry a large fraction of outliers, and RANSAC absorbs the cost. That cost is not linear: the iterations needed for a given confidence go as

N = log(1 - p) / log(1 - r^4)

where r is the inlier ratio and the exponent is the 4-point minimal sample for a homography. Dropping from r = 0.6 to r = 0.25 raises N by roughly an order of magnitude. In a 30–60 fps tracking loop that is the difference between a stable lock and a homography that intermittently collapses onto a degenerate solution — the jitter and flipping that #97 lists under "geometric validation".

A cheap pre-RANSAC filter attacks this at the source, by raising r before the estimator ever runs.

It belongs below the contract line, not above it

#97 draws a clear boundary: jsfeatNext stays generic and stateless; everything stateful and AR-specific lives in the high-level layer. A geometric match filter is unambiguously on the stateless side — it is a pure function of two keypoint sets and their matches, holds no per-frame state, and has nothing AR-specific about it. Putting it in the high-level layer would mean reimplementing it once per orchestration, and would deny it to the WASM backends where it is cheapest.

Without an explicit seam, though, there is nowhere for it to go: match is the wrong place (a filter needs keypoint coordinates and image dimensions, which match does not take), and estimateHomography is the wrong place (it takes point arrays, not matches, and the filtering must happen before sampling).

GMS specifically is close to free

Partition both images into a grid (20×20 in the OpenCV reference), accumulate how many matches connect each cell pair, then accept or reject each cell's matches wholesale by comparing the support summed over the 9-cell neighbourhood against α·sqrt(mean points per cell). It is integer counting over a small array — no geometry, no SVD, no iteration — linear in the number of matches, and the reference implementation is ~12 KB of source with no trained data of any kind. On the WASM backends the cost is negligible next to detection and description.

Its assumption (locally uniform motion) is weakest under strong parallax and non-rigid scenes, and strongest on planar rigid targets — which is exactly the WebAR target class in #97.

Proposed changes

Types
/** A keypoint set together with the dimensions of the image it came from.
 *  Filters need both: grid-based methods normalise coordinates by image size. */
export interface FilterView {
    keypoints: Keypoint[];
    width: number;
    height: number;
}

/** Geometric match-filtering families a backend may implement. */
export type MatchFilterKind = 'gms';

export interface FilterOptions {
    /** Filter family. Defaults to the backend's only/preferred implementation. */
    kind?: MatchFilterKind;
    /** GMS: score threshold factor (alpha). Reference default: 6. */
    thresholdFactor?: number;
    /** GMS: evaluate the 8 neighbourhood rotation patterns. Costs ~8x, buys
     *  rotation invariance. Reference default: false. */
    withRotation?: boolean;
    /** GMS: evaluate the 5 grid scale ratios. Costs ~5x, buys scale
     *  invariance. Reference default: false. */
    withScale?: boolean;
    /** GMS: grid subdivision per axis. Reference default: 20. */
    gridSize?: number;
}
Method
export interface CvBackend {
    /**
     * Optional geometric consistency filter, applied between `match` and
     * `estimateHomography`. Returns a subset of the input matches.
     *
     * Backends that do not implement it omit the method; the caller then
     * proceeds with the unfiltered matches.
     */
    filterMatches?(
        matches: Match[],
        query: FilterView,
        train: FilterView,
        options?: FilterOptions
    ): Match[];

    // ... existing members
}
Semantics
  • Returns a subset, indices preserved. Every returned Match keeps its original queryIdx / trainIdx / distance. The method must not renumber, reorder-with-meaning, or synthesise matches — callers hold the keypoint arrays those indices point into.
  • Optional, and skipping is always valid. if (cv.filterMatches) matches = cv.filterMatches(...). Absence degrades to today's behaviour, so this cannot break an existing backend.
  • Stateless and synchronous, per the #96 boundary contract: pure function over buffers, no per-frame state, caller owns the result.
  • An unsupported explicit kind throws, consistent with the descriptor-selection companion issue — no silent substitution of one filter for another.
  • Declared in capabilities as matchFilters: readonly MatchFilterKind[] (empty array when unimplemented), so the high-level layer can discover it without feature-sniffing the method.

jsfeatNext deliverable

  • Add FilterView, MatchFilterKind, FilterOptions and the optional filterMatches member to cv_backend.ts.
  • Extend BackendCapabilities with matchFilters.
  • The jsfeatNext adapter declares matchFilters: [] for now and omits the method; wiring it to a concrete GMS module is a separate issue.

Acceptance criteria

  • The types and the optional filterMatches member are added to the contract and documented.
  • BackendCapabilities carries matchFilters; the jsfeatNext adapter declares it accurately.
  • The documented call pattern (skip when absent) is shown in the round-trip demo from #96, so the composed pipeline reads detect → describe → match → [filterMatches] → estimateHomography → poseFromHomography.
  • A no-op reference filter (identity) is used in a test to prove the seam composes and that indices survive the round trip into estimateHomography.
  • Requesting an unsupported kind throws a typed error naming the supported set.

Out of scope

  • Implementing GMS — one issue per repository (jsfeatNext and PureCV), both blocked on this one.
  • LOGOS or any other filter family; MatchFilterKind is a union precisely so they can be added without another contract change.
  • Homography plausibility checks (projected-corner convexity, determinant sign, aspect-ratio sanity). Those run after estimation and are stateful/AR-specific — they stay in the high-level layer per #97.
  • Temporal filtering across frames — stateful, high-level, out of the contract by construction.

Related

  • Contract this amends: #96
  • Consumer: #97 — tracker_t is where the improved inlier ratio pays off, and this is the pre-RANSAC counterpart to the post-RANSAC "geometric validation" item listed there
  • Companion contract amendment: the descriptor selection / capability declaration issue #128
  • Reference: Bian et al., GMS: Grid-based Motion Statistics for Fast, Ultra-robust Feature Correspondence, CVPR 2017; OpenCV implementation in xfeatures2d (gms.cpp, matchGMS)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.