Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/jsfeatNext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,28 @@ class affine2d extends motion_model {

return 1;
}

// Per-point reprojection error for the affine model. Ported from original
// jsfeat's affine2d; jsfeatNext was missing it, which made RANSAC/LMEDS
// with an affine2d kernel throw. See issue #51.
error(from: point_t[], to: point_t[], model: matrix_t, err: Int32Array | Float32Array, count: number): void {
let i = 0;
let pt0, pt1;
const m = model.data;

for (; i < count; ++i) {
pt0 = from[i];
pt1 = to[i];

err[i] =
this.sqr(pt1.x - m[0] * pt0.x - m[1] * pt0.y - m[2]) +
this.sqr(pt1.y - m[3] * pt0.x - m[4] * pt0.y - m[5]);
}
}

check_subset(from: point_t[], to: point_t[], count: number): boolean {
return true; // all good
}
}

class homography2d extends motion_model {
Expand Down
27 changes: 16 additions & 11 deletions tests/parity/motion_estimator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,35 +149,40 @@ describe("parity: motion_estimator vs original jsfeat.motion_estimator", () => {
to.push({ x: X, y: Y });
}

// BUG FOUND BY THIS SUITE (documented divergence, see the tracking
// issue): jsfeatNext's motion_model base class lost the default
// check_subset() (original jsfeat returns true there; only
// homography2d overrides it), so RANSAC with an affine2d kernel
// throws TypeError in jsfeatNext while it works in original jsfeat.
// Regression test for #51: jsfeatNext's motion_model base class was
// missing the default check_subset() (original jsfeat returns true
// there; only homography2d overrides it), which made RANSAC with an
// affine2d kernel throw TypeError. Now fixed — full parity check.
const params = new jsfeatNext.ransac_params_t(3, 3.0, 0.5, 0.99);
const me = new jsfeatNext.motion_estimator();
const kernel = new jsfeatNext.affine2d();
const model = new jsfeatNext.matrix_t(3, 3, F32C1);
const mask = new jsfeatNext.matrix_t(N, 1, U8C1);

seededRandom(31337);
expect(() => me.ransac(params, kernel, from, to, N, model, mask, 1000)).toThrow(TypeError);
const okN = me.ransac(params, kernel, from, to, N, model, mask, 1000);
vi.restoreAllMocks();

// ...whereas the original jsfeat succeeds on the same data:
const paramsO = new jsfeat.ransac_params_t(3, 3.0, 0.5, 0.99);
const kernelO = new jsfeat.motion_model.affine2d();
const modelO = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t);
const maskO = new jsfeat.matrix_t(N, 1, jsfeat.U8_t | jsfeat.C1_t);

seededRandom(31337);
seededRandom(31337); // identical random sequence for the oracle
const okO = jsfeat.motion_estimator.ransac(paramsO, kernelO, from, to, N, modelO, maskO, 1000);
vi.restoreAllMocks();

expect(okO).toBe(true);
// the oracle also rejects the synthetic outliers
expect(okN).toBe(okO);
expect(okN).toBe(true);
for (let i = 0; i < N; i++) {
expect(mask.data[i]).toBe(maskO.data[i]);
}
for (let i = 0; i < 9; i++) {
expect(model.data[i]).toBeCloseTo(modelO.data[i], 4);
}
// the outliers must be rejected on both sides
for (let i = 0; i < OUT; i++) {
expect(maskO.data[i]).toBe(0);
expect(mask.data[i]).toBe(0);
}
});
});
Loading