import { matrix_t } from "../matrix_t/matrix_t";
import { JSFEAT_CONSTANTS } from "../constants/constants";
export class pose_t {
public R: matrix_t; // 3x3, row-major, OpenCV camera frame
public t: Float64Array; // length 3
public good: boolean; // false if H/K were degenerate
constructor() {
this.R = new matrix_t(3, 3, JSFEAT_CONSTANTS.F64_t | JSFEAT_CONSTANTS.C1_t);
this.t = new Float64Array(3);
this.good = false;
}
}
export class pose_estimator {
private Kinv: Float64Array; // inverse intrinsics, row-major, zero skew
constructor(K: matrix_t) {
this.Kinv = pose_estimator.invertIntrinsics(K);
}
setIntrinsics(K: matrix_t): void {
this.Kinv = pose_estimator.invertIntrinsics(K);
}
private static invertIntrinsics(K: matrix_t): Float64Array {
const k = K.data;
const fx = k[0], cx = k[2], fy = k[4], cy = k[5];
return new Float64Array([
1 / fx, 0, -cx / fx,
0, 1 / fy, -cy / fy,
0, 0, 1,
]);
}
/** Recover pose from a homography mapping model-plane points to pixels. */
estimate(H: matrix_t, out?: pose_t): pose_t {
const pose = out || new pose_t();
const h = H.data;
const ki = this.Kinv;
// B = Kinv * H, row-major
const B = new Float64Array(9);
for (let r = 0; r < 3; ++r) {
for (let c = 0; c < 3; ++c) {
B[r * 3 + c] =
ki[r * 3] * h[c] +
ki[r * 3 + 1] * h[3 + c] +
ki[r * 3 + 2] * h[6 + c];
}
}
const b1 = [B[0], B[3], B[6]];
const b2 = [B[1], B[4], B[7]];
const b3 = [B[2], B[5], B[8]];
const n1 = Math.hypot(b1[0], b1[1], b1[2]);
const n2 = Math.hypot(b2[0], b2[1], b2[2]);
if (n1 < 1e-12 || n2 < 1e-12) { pose.good = false; return pose; }
const lambda = 2.0 / (n1 + n2);
const s = b3[2] >= 0 ? 1 : -1; // target in front of camera (t.z > 0)
const r1 = [s * b1[0] / n1, s * b1[1] / n1, s * b1[2] / n1];
const r2 = [s * b2[0] / n2, s * b2[1] / n2, s * b2[2] / n2];
const t = [s * b3[0] * lambda, s * b3[1] * lambda, s * b3[2] * lambda];
// Closed-form re-orthonormalization of (r1, r2):
const c = pose_estimator.normalize([r1[0] + r2[0], r1[1] + r2[1], r1[2] + r2[2]]);
const p = pose_estimator.cross(r1, r2);
const d = pose_estimator.normalize(pose_estimator.cross(c, p));
const q = Math.SQRT1_2;
const c1 = [(c[0] + d[0]) * q, (c[1] + d[1]) * q, (c[2] + d[2]) * q];
const c2 = [(c[0] - d[0]) * q, (c[1] - d[1]) * q, (c[2] - d[2]) * q];
const c3 = pose_estimator.cross(c1, c2);
const R = pose.R.data; // store columns [c1 c2 c3] into row-major R
R[0] = c1[0]; R[1] = c2[0]; R[2] = c3[0];
R[3] = c1[1]; R[4] = c2[1]; R[5] = c3[1];
R[6] = c1[2]; R[7] = c2[2]; R[8] = c3[2];
pose.t[0] = t[0]; pose.t[1] = t[1]; pose.t[2] = t[2];
pose.good = true;
return pose;
}
/** Column-major 4x4 modelview for three.js / WebGL, CV -> GL via diag(1,-1,-1). */
modelViewGL(pose: pose_t, out?: Float32Array): Float32Array {
const m = out || new Float32Array(16);
const R = pose.R.data;
const t = pose.t;
m[0] = R[0]; m[1] = -R[3]; m[2] = -R[6]; m[3] = 0;
m[4] = R[1]; m[5] = -R[4]; m[6] = -R[7]; m[7] = 0;
m[8] = R[2]; m[9] = -R[5]; m[10] = -R[8]; m[11] = 0;
m[12] = t[0]; m[13] = -t[1]; m[14] = -t[2]; m[15] = 1;
return m;
}
/** Rough pinhole intrinsics from image size + horizontal FOV (uncalibrated bootstrap). */
static intrinsics(width: number, height: number, fovXdeg = 60): matrix_t {
const K = new matrix_t(3, 3, JSFEAT_CONSTANTS.F64_t | JSFEAT_CONSTANTS.C1_t);
const f = 0.5 * width / Math.tan(0.5 * fovXdeg * Math.PI / 180);
const k = K.data;
k[0] = f; k[1] = 0; k[2] = 0.5 * width;
k[3] = 0; k[4] = f; k[5] = 0.5 * height;
k[6] = 0; k[7] = 0; k[8] = 1;
return K;
}
private static cross(a: number[], b: number[]): number[] {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
private static normalize(v: number[]): number[] {
const n = Math.hypot(v[0], v[1], v[2]) || 1;
return [v[0] / n, v[1] / n, v[2] / n];
}
}
Add
bfmatcherandpose_estimatormodules to complete the natural-feature AR pipelineSummary
jsfeatNext already covers most of the natural-feature-tracking pipeline for planar (image) targets: grayscale/pyramid, FAST/YAPE detectors, ORB descriptors, optical flow (
optical_flow_lk), and RANSAC homography (motion_estimator+homography2d).Two pieces are still missing to go from detection to a renderable 3D pose:
match_pattern()insample_orb_pinball.html), not as a reusable module.render_pattern_shape); there is noH → [R | t]decomposition, so no 3D object can be anchored.This issue tracks adding both as first-class modules, with an OpenCV-style API so they cross-validate 1:1 against the Rust
BFMatcherin PureCV and can serve as a numeric reference oracle.Scope
1.
bfmatcher—src/bfmatcher/bfmatcher.tsBrute-force Hamming matcher for binary descriptors (ORB), refactored out of the sample's
match_pattern().match_ttype{ queryIdx, trainIdx, distance }— equivalent tocv::DMatchmatch(query, train, maxDistance)— nearest-neighbour, with optionalcrossCheckknnMatch(query, train, k)— k nearest neighbours per query descriptorratio_test(knn, ratio)— Lowe's ratio test helper (the sample had it commented out)Int32Arrayview — bit-identical to the sample'spopcnt32jsfeatNext.bfmatcher,jsfeatNext.match_t)Prototype implementation —
src/bfmatcher/bfmatcher.ts2.
pose_estimator—src/pose_estimator/pose_estimator.tsClosed-form planar pose from homography + intrinsics (no SVD dependency, to stay
no_std-portable for the Rust side).pose_t{ R: matrix_t(3x3), t: Float64Array(3), good: boolean }estimate(H, out?)—B = K⁻¹H, normalize columns, closed-form re-orthonormalization,r3 = r1 × r2t.z > 0)→ moved to the high-level renderer adapters ([Roadmap] WebAR on jsfeatNext: two-layer architecture & high-level feature set #97); jsfeatNext stays renderer-agnosticmodelViewGL(pose)— column-major 4×4 for three.js / WebGL, CV→GL viadiag(1, -1, -1)intrinsics(width, height, fovX)— rough pinholeKfor uncalibrated bootstrap (camera geometry, not rendering — may stay or move up; decide at implementation)jsfeatNext.pose_estimator,jsfeatNext.pose_t)H = K[r1 r2 t]from a known pose and check round-trip within tolerancePrototype implementation —
src/pose_estimator/pose_estimator.tsProposed API (sketch)
Acceptance criteria
jsfeat.bfmatcher/jsfeat.pose_estimator.match_pattern()in the ORB sample can be replaced by the module as a drop-in.Kand a synthesizedH = K[r1 r2 t], recoveredR,tmatch the ground truth within ε.Out of scope (follow-ups)
optical_flow_lktracking between ORB matches (reduces jitter and recompute cost).intrinsics()helper is a bootstrap only).O(query × train)— fine for image targets (a few hundred descriptors), not general-purpose.Notes
feat(bfmatcher): …andfeat(pose): …(Conventional Commits).sample_orb_pinball.html; this issue promotes that throwaway sample code into maintained, tested modules.