webarkit / webarkit/jsfeatNext
feat(cv_backend): descriptor selection & capability declaration
@kalwalt is already working on this.
Since Aug 16, 2026.
- 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:
- A
DescriptorKindselector ondescribe/detectAndCompute, and akindfield on the returnedDescriptors. - A
capabilitiesdeclaration 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 againstcapabilities, not the backend's. matchrejects mismatched kinds. Ifquery.kind !== train.kind(or the norms differ),matchthrows. This is the cheap guard that turns thepattern_tscenario above from a silent tracking failure into an immediate, obvious error.- Omitting
kindis always valid and yieldsdefaultDescriptor, 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.tsdraft with the types above. - Update the jsfeatNext adapter to declare
capabilities(descriptors: ['orb'],defaultDescriptor: 'orb'at the time of writing) and to populatekind/normon everyDescriptorsit returns. - Add the
kind/normmismatch guard in the adapter'smatch.
Acceptance criteria
-
DescriptorKind,DescriptorNorm,DescribeOptionsandBackendCapabilitiesare added to theCvBackendcontract and documented. -
describe/detectAndComputeaccept the options object; omitting it reproduces current behaviour exactly. -
Descriptorscarrieskindandnorm, populated by the jsfeatNext adapter. - Requesting an unsupported
kindthrows a clear, typed error naming the requested kind and the supported set. -
matchthrows whenquery.kind !== train.kindor the norms differ. - A test demonstrates capability-driven selection: given a backend advertising only
['orb'], a preference list of['teblid', 'orb']deterministically resolves toorbwithout 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(theDetectorKindunion is introduced here; wiring a detector selector intoDetectOptionscan follow if needed). - Float descriptors:
norm: 'l2'is reserved in the type but no float descriptor is contemplated, andDescriptors.datastaysUint8Arrayper the #96 boundary contract. - Match filtering / geometric verification — see the companion issue.
Related
- Contract this amends: #96
- Consumer: #97 (
pattern_tstores 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.