webarkit / webarkit/jsfeatNext

feat(cv_backend): descriptor selection & capability declaration

Open
#128 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

Extend CvBackend with descriptor selection and capability declaration

Summary

Amend the CvBackend contract defined in #96 so that the descriptor type is an explicit, negotiated choice rather than an implicit property of whichever backend happens to be plugged in.

Two additions:

  1. A DescriptorKind selector on describe / detectAndCompute, and a kind field on the returned Descriptors.
  2. A capabilities declaration so the high-level layer can query what a backend actually implements before asking for it.

This is a prerequisite for adding any second descriptor to either backend (TEBLID/BEBLID, FREAK — see the related issues), and it is deliberately scoped to the contract only: no descriptor is implemented here.

Why

The interface as drafted in #96 exposes:

describe(image: GrayImage, keypoints: Keypoint[]): Descriptors;

The descriptor type is implicit. Descriptors.bytesPerDescriptor is documented as 32 for ORB — it is a return value describing what the backend chose, not a parameter expressing what the caller wanted. There is no way to ask for a specific descriptor, and no way to discover which ones a backend supports.

This is invisible while ORB is the only option. It breaks as soon as the backends diverge, which is exactly what is about to happen: PureCV and jsfeatNext will not gain TEBLID and FREAK on the same day, and WebARKitLib (which already ships cv::ORB and cv::AKAZE) exposes a different set again.

The failure mode is silent and severe. pattern_t (#97) stores a trained descriptor set for a target. If a target is trained with TEBLID on one backend and matched against ORB descriptors at runtime — same Uint8Array, same bytesPerDescriptor when both are 32 bytes, same Hamming metric — nothing throws. The matcher returns plausible-looking matches with meaningless distances, RANSAC finds no consensus, and the tracker simply never locks on. Debugging that from the symptom is expensive.

The contract in #96 states that backends are interchangeable because they share the same signatures. Once they no longer share the same capabilities, the signatures alone are not enough: the capability set has to become part of the contract.

Proposed changes

1. Descriptor kind
/**
 * Binary descriptor families a backend may implement.
 * Extend as new descriptors land; backends declare support via `capabilities`.
 */
export type DescriptorKind = 'orb' | 'freak' | 'beblid' | 'teblid' | 'akaze';

/**
 * Distance metric the descriptors must be compared with.
 * All current kinds are binary/Hamming; the field exists so a future float
 * descriptor does not silently get matched with the wrong metric.
 */
export type DescriptorNorm = 'hamming' | 'l2';

export interface DescribeOptions {
    /** Descriptor family to compute. Defaults to the backend's `defaultDescriptor`. */
    kind?: DescriptorKind;
    /** Descriptor size in bits, where the family supports more than one
     *  (e.g. BEBLID/TEBLID: 256 or 512). Ignored by fixed-size families. */
    bits?: number;
}

Descriptors gains two self-describing fields:

export interface Descriptors {
    data: Uint8Array;
    count: number;
    bytesPerDescriptor: number;
    kind: DescriptorKind;      // NEW — what was actually produced
    norm: DescriptorNorm;      // NEW — how it must be compared
}

Method signatures (third parameter is optional, so this is source-compatible with the #96 draft):

describe(image: GrayImage, keypoints: Keypoint[], options?: DescribeOptions): Descriptors;

detectAndCompute?(
    image: GrayImage,
    options?: DetectOptions & DescribeOptions
): { keypoints: Keypoint[]; descriptors: Descriptors };
2. Capability declaration
export interface BackendCapabilities {
    /** Human-readable backend id, e.g. 'jsfeatnext', 'purecv-wasm'. */
    readonly name: string;
    /** Detectors this backend can run. */
    readonly detectors: readonly DetectorKind[];
    /** Descriptor families this backend can compute. */
    readonly descriptors: readonly DescriptorKind[];
    /** Descriptor used when `DescribeOptions.kind` is omitted. */
    readonly defaultDescriptor: DescriptorKind;
}

export interface CvBackend {
    /** Static description of what this backend implements. Cheap, no side effects. */
    readonly capabilities: BackendCapabilities;
    // ... existing members
}

capabilities is a property rather than a method: it is static per backend instance, and the contract's stateless/synchronous rules make a call unnecessary.

3. Negotiation semantics (the part that matters)
  • An unsupported explicit request throws. describe(img, kps, { kind: 'teblid' }) on a backend that does not implement TEBLID must throw, not fall back to ORB. Silent degradation is precisely the failure this issue exists to prevent; choosing a fallback is the high-level layer's decision, made against capabilities, not the backend's.
  • match rejects mismatched kinds. If query.kind !== train.kind (or the norms differ), match throws. This is the cheap guard that turns the pattern_t scenario above from a silent tracking failure into an immediate, obvious error.
  • Omitting kind is always valid and yields defaultDescriptor, so existing call sites keep working unchanged.
4. Suggested high-level usage (illustrative, not part of this issue)
const preferred: DescriptorKind[] = ['teblid', 'freak', 'orb'];
const kind = preferred.find(k => cv.capabilities.descriptors.includes(k))
          ?? cv.capabilities.defaultDescriptor;

The AR layer picks deterministically from its own preference order; the backend never guesses.

jsfeatNext deliverable

  • Update the cv_backend.ts draft with the types above.
  • Update the jsfeatNext adapter to declare capabilities (descriptors: ['orb'], defaultDescriptor: 'orb' at the time of writing) and to populate kind / norm on every Descriptors it returns.
  • Add the kind/norm mismatch guard in the adapter's match.

Acceptance criteria

  • DescriptorKind, DescriptorNorm, DescribeOptions and BackendCapabilities are added to the CvBackend contract and documented.
  • describe / detectAndCompute accept the options object; omitting it reproduces current behaviour exactly.
  • Descriptors carries kind and norm, populated by the jsfeatNext adapter.
  • Requesting an unsupported kind throws a clear, typed error naming the requested kind and the supported set.
  • match throws when query.kind !== train.kind or the norms differ.
  • A test demonstrates capability-driven selection: given a backend advertising only ['orb'], a preference list of ['teblid', 'orb'] deterministically resolves to orb without the backend making the decision.

Out of scope

  • Implementing any new descriptor (TEBLID/BEBLID, FREAK) — separate issues per repository.
  • The PureCV, WebARKitLib and jsartoolkitNFT adapters.
  • Detector-kind selection beyond declaring it in capabilities (the DetectorKind union is introduced here; wiring a detector selector into DetectOptions can follow if needed).
  • Float descriptors: norm: 'l2' is reserved in the type but no float descriptor is contemplated, and Descriptors.data stays Uint8Array per the #96 boundary contract.
  • Match filtering / geometric verification — see the companion issue.

Related

  • Contract this amends: #96
  • Consumer: #97 (pattern_t stores per-target descriptors and is the component most exposed to a silent kind mismatch)
  • Descriptor work this unblocks: #80 (FREAK), plus the TEBLID/BEBLID issues in jsfeatNext and PureCV
  • Companion contract amendment: the filterMatches / GMS seam issue #129

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.