UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

1,542 lines (1,206 loc) 57.3 kB
/* * speedy-vision.js * GPU-accelerated Computer Vision for JavaScript * Copyright 2020-2022 Alexandre Martins <alemartf(at)gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * transform.js * Unit testing */ describe('Geometric transformations', function() { beforeEach(function() { jasmine.addMatchers(speedyMatchers); }); function printp(...points) { if(points.length > 0) { const p = points.shift(); if(typeof p === 'object') { if(Array.isArray(p)) print(p.map(p => p.toString()).join(', ')); else print(p.toString()); } else print(p); return printp(...points); } } describe('Perspective transform', function() { it('computes a perspective transform from four correspondences of points', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const tstQuad = Speedy.Matrix.Zeros(2, 4); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography, srcQuad, dstQuad); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('computes another perspective transform from four correspondences of points', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const tstQuad = Speedy.Matrix.Zeros(2, 4); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography, srcQuad, dstQuad); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('computes yet another perspective transform from four correspondences of points', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix(2, 4, [ 1, 0, 4, 0, 4, 2, 1, 2, ]); const tstQuad = Speedy.Matrix.Zeros(2, 4); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography, srcQuad, dstQuad); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('computes an identity transform from four non-distinct correspondences of points', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography, srcQuad, dstQuad); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); const eye = Speedy.Matrix.Eye(3, 3); expect(homography.read()).toBeElementwiseNearlyEqual(eye.read()); }); it('fails to compute a homography if 3 or more points are collinear', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 2, 0, 1, 1, 0, 2, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography, srcQuad, dstQuad); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); const homdata = homography.read(); expect(homdata).toBeElementwiseNaN(); print('----------'); const homography2 = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.perspective(homography2, dstQuad, srcQuad); printm('From:', dstQuad); printm('To:', srcQuad); printm('Homography:', homography2); const homdata2 = homography2.read(); expect(homdata2).toBeElementwiseNaN(); }); it('fails to compute a perspective transform using matrices of incorrect shape', async function() { const srcQuad = Speedy.Matrix(2, 5, [ 0, 0, 1, 0, 1, 1, 0, 1, 51, 42, ]); const dstQuad = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const homography = Speedy.Matrix.Zeros(3, 3); expect(() => Speedy.Matrix.perspective(homography, srcQuad, dstQuad)).toThrow(); expect(() => Speedy.Matrix.perspective(homography, dstQuad, srcQuad)).toThrow(); }); it('fails to compute a perspective transform using incorrect input', async function() { const srcQuad = Speedy.Matrix(2, 3, [ 0, 0, 2, 0, 1, 1, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const fooQuad = Speedy.Matrix(2, 4, [ 0, 0, 2, 0, 1, 1, 0, 2, ]); const homography = Speedy.Matrix.Zeros(3, 3); expect(() => Speedy.Matrix.perspective(homography, srcQuad, dstQuad)).toThrow(); expect(() => Speedy.Matrix.perspective(homography, dstQuad, srcQuad)).toThrow(); expect(() => Speedy.Matrix.perspective(homography, srcQuad, srcQuad)).toThrow(); expect(() => Speedy.Matrix.perspective(homography, srcQuad, fooQuad)).toThrow(); expect(() => Speedy.Matrix.perspective(homography, fooQuad, srcQuad)).toThrow(); }); it('applies a perspective transform to a set of points', async function() { const homography = Speedy.Matrix(3, 3, [ 3, 0, 0, 0, 2, 0, 2, 1, 1, ]); const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix.Zeros(2, 4); await Speedy.Matrix.applyPerspectiveTransform(dstQuad, srcQuad, homography); printm('homography:', homography, 'srcQuad:', srcQuad, 'dstQuad:', dstQuad); const actual = dstQuad.read(); const expected = [2, 1, 5, 1, 5, 3, 2, 3]; expect(actual).toBeElementwiseEqual(expected); }); it('applies a linear transform to a set of points', async function() { const mat = Speedy.Matrix(2, 2, [ 3, 0, 0, 2, ]); const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix.Zeros(2, 4); await dstQuad.setTo(mat.times(srcQuad)); printm('linear transform:', mat, 'srcQuad:', srcQuad, 'dstQuad:', dstQuad); const actual = dstQuad.read(); const expected = [0, 0, 3, 0, 3, 2, 0, 2]; expect(actual).toBeElementwiseEqual(expected); }); }); describe('Planar homography with PRANSAC', function() { const countInliers = maskdata => maskdata.reduce((sum, val) => sum + (val | 0), 0); const countOutliers = maskdata => maskdata.length - countInliers(maskdata); const noise = (w = 1.0) => (Math.random() - 0.5) * w; it('computes a planar homography using only 4 inliers without noise', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); expect(countInliers(mask.read())).toEqual(srcQuad.columns); }); it('computes a planar homography using only 8 inliers without noise', async function() { const srcQuad = Speedy.Matrix(2, 8, [ 0, 0, 1, 0, 1, 1, 0, 1, 2, 2, 3, 2, 3, 3, 2, 3, ]); const dstQuad = Speedy.Matrix(2, 8, [ 0, 0, 3, 0, 3, 2, 0, 2, 6, 4, 9, 4, 9, 6, 6, 6, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); expect(countInliers(mask.read())).toEqual(srcQuad.columns); }); it('computes a planar homography using 80% of inliers', async function() { const numInliers = 8; // 8/10 const srcQuad = Speedy.Matrix(2, 10, [ // ---- inliers: ---- 0, 0, 100, 0, 100, 100, 0, 100, 0, 0, 100, 0, 100, 100, 0, 100, // ---- outliers: ---- 9999, 9999, 9999, 9999, ]); const dstQuad = Speedy.Matrix(2, 10, [ // ---- inliers: ---- 0, 0, 300, 0, 300, 200, 0, 200, 0, 0, 300, 0, 300, 200, 0, 200, // ---- outliers: ---- 19999, 9999, 9999, 9999, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const srcQuadInliers = srcQuad.block(0, 1, 0, numInliers - 1); const dstQuadInliers = dstQuad.block(0, 1, 0, numInliers - 1); const maskOutliers = mask.block(0, 0, numInliers, mask.columns - 1); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); const tstQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); const difQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuadInliers, srcQuadInliers, homography); await difQuadInliers.setTo(tstQuadInliers.minus(dstQuadInliers)); const err2 = difQuadInliers.read().reduce((err, x) => err + x*x, 0); printm('Reprojection:', tstQuadInliers, 'vs', dstQuadInliers); printm('Reprojection error: ' + Math.sqrt(err2)); expect(maskOutliers.read()).toBeElementwiseZero(); expect(countInliers(mask.read())).toEqual(numInliers); expect(err2).toBeNearlyZero(); }); it('computes a planar homography using 75% of inliers', async function() { const numInliers = 6; // 6/8 const srcQuad = Speedy.Matrix(2, 8, [ // ---- inliers: ---- 0, 0, 100, 0, 100, 100, 0, 100, 50, 50, 0, 50, // ---- outliers: ---- 9999, 9999, -9999, -9999, ]); const dstQuad = Speedy.Matrix(2, 8, [ // ---- inliers: ---- 0, 0, 300, 0, 300, 200, 0, 200, 150, 100, 0, 100, // ---- outliers: ---- 19999, 9999, 999, 9999, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const srcQuadInliers = srcQuad.block(0, 1, 0, numInliers - 1); const dstQuadInliers = dstQuad.block(0, 1, 0, numInliers - 1); const maskOutliers = mask.block(0, 0, numInliers, mask.columns - 1); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); const tstQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); const difQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuadInliers, srcQuadInliers, homography); await difQuadInliers.setTo(tstQuadInliers.minus(dstQuadInliers)); const err2 = difQuadInliers.read().reduce((err, x) => err + x*x, 0); printm('Reprojection:', tstQuadInliers, 'vs', dstQuadInliers); printm('Reprojection error: ' + Math.sqrt(err2)); expect(maskOutliers.read()).toBeElementwiseZero(); expect(countInliers(mask.read())).toEqual(numInliers); expect(err2).toBeNearlyZero(); }); it('computes a planar homography using 50% of inliers', async function() { const numInliers = 8; // 8/16 const srcQuad = Speedy.Matrix(2, 16, [ // ---- inliers: ---- 100, 0, 100, 100, 0, 100, -50, -50, 100, 0, 100, 100, 0, 100, -50, -50, // ---- outliers: ---- 999, 999, -999, -999, -999, 999, 999, -999, 7999, 0, -1, -99999, -0, 7999, 7999, -0, ]); const dstQuad = Speedy.Matrix(2, 16, [ // ---- inliers: ---- 100, 0, 100, 100, 0, 100, -50, -50, 100, 0, 100, 100, 0, 100, -50, -50, // ---- outliers: ---- -9, -9, -221999, -999, 0, 0, -221999, -999, -2, -9, -1, -2, 912717, 0, 33, -2, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const srcQuadInliers = srcQuad.block(0, 1, 0, numInliers - 1); const dstQuadInliers = dstQuad.block(0, 1, 0, numInliers - 1); const maskOutliers = mask.block(0, 0, numInliers, mask.columns - 1); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask, numberOfHypotheses: 2000, // increase the number of hypotheses for low inlier ratios bundleSize: 2000 / 5, reprojectionError: 0.5, }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); const tstQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); const difQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuadInliers, srcQuadInliers, homography); await difQuadInliers.setTo(tstQuadInliers.minus(dstQuadInliers)); const err2 = difQuadInliers.read().reduce((err, x) => err + x*x, 0); printm('Reprojection:', tstQuadInliers, 'vs', dstQuadInliers); printm('Reprojection error: ' + Math.sqrt(err2)); expect(maskOutliers.read()).toBeElementwiseZero(); expect(countInliers(mask.read())).toEqual(numInliers); expect(err2).toBeNearlyZero(); }); it('fails to compute a planar homography using too few points', async function() { const srcQuad = Speedy.Matrix(2, 3, [ 0, 0, 100, 0, 100, 100, ]); const dstQuad = Speedy.Matrix(2, 3, [ 0, 0, 300, 0, 300, 200, ]); const homography = Speedy.Matrix.Zeros(3, 3); printm('From:', srcQuad); printm('To:', dstQuad); expect(() => Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', })).toThrow(); }); it('fails to compute a planar homography using a degenerate configuration', async function() { const srcQuad = Speedy.Matrix(2, 7, [ 0, 0, 100, 0, 100, 100, 0, 0, 100, 0, 100, 100, 50, 50, ]); const dstQuad = Speedy.Matrix(2, 7, [ 0, 0, 300, 0, 300, 200, 0, 0, 300, 0, 300, 200, 150, 100, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask, }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); expect(homography.read()).toBeElementwiseNaN(); expect(countInliers(mask.read())).toEqual(0); }); it('fails to compute a planar homography using 4 copies of a single point', async function() { const srcQuad = Speedy.Matrix.Zeros(2, 4); const dstQuad = Speedy.Matrix.Zeros(2, 4); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask, }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); printm('Inliers mask:', mask); expect(homography.read()).toBeElementwiseNaN(); expect(countInliers(mask.read())).toEqual(0); }); describe('computes a correct homography despite random noise', function() { const noiseTable = { 'easy for rookies': 1.5, 'medium': 2, 'bad': 3, 'really bad!': 4, 'outrageous!!!!!': 5 }; for(const difficulty in noiseTable) { it(`computes a correct homography with noise level: ${difficulty}`, async function() { const numPoints = 50; const reprojErrTolerance = 1; const noiseLevel = noiseTable[difficulty]; // map [0,100]x[0,100] to [200,400]x[200,400] const entries = Array.from({ length: numPoints * 2 }, () => 100 * Math.random()); const srcQuad = Speedy.Matrix(2, numPoints, entries); const dstQuad = Speedy.Matrix(2, numPoints, entries.map(x => 200 + 2 * x + noise(noiseLevel))); const mask = Speedy.Matrix.Zeros(1, numPoints); // compute homography const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'pransac', mask: mask, reprojectionError: reprojErrTolerance, }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Inliers mask:', mask); printm('Homography:', homography); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); const difQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); await difQuad.setTo(tstQuad.minus(dstQuad)); const reprojectionError2 = difQuad.read().reduce((err, x) => err + x*x, 0); const reprojectionError = Math.sqrt(reprojectionError2); const numberOfInliers = mask.read().reduce((cnt, x) => cnt + x, 0); const percentageOfInliers = 100 * numberOfInliers / numPoints; printm('Percentage of inliers:', percentageOfInliers + '%'); printm('Average reprojection error:', reprojectionError / numPoints); expect(reprojectionError).toBeLessThan(numPoints * reprojErrTolerance); }); } }); }); describe('Planar homography with the default method (normalized DLT)', function() { it('computes a planar homography using 4 correspondences', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('computes a planar homography using 5 correspondences', async function() { const srcQuad = Speedy.Matrix(2, 5, [ 0, 0, 1, 0, 1, 1, 0, 1, 0.5, 0.5, ]); const dstQuad = Speedy.Matrix(2, 5, [ 0, 0, 3, 0, 3, 2, 0, 2, 1.5, 1.0, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('computes a planar homography using 8 correspondences', async function() { const srcQuad = Speedy.Matrix(2, 8, [ 0, 0, 1, 0, 1, 1, 0, 1, 0.5, 0.5, 2, 2, -1, 0, -1, -1, ]); const dstQuad = Speedy.Matrix(2, 8, [ 0, 0, 3, 0, 3, 2, 0, 2, 1.5, 1.0, 6, 4, -3, 0, -3, -2, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); const tstQuad = Speedy.Matrix.Zeros(srcQuad.rows, srcQuad.columns); await Speedy.Matrix.applyPerspectiveTransform(tstQuad, srcQuad, homography); expect(tstQuad.read()).toBeElementwiseNearlyEqual(dstQuad.read()); }); it('fails to compute a planar homography using too few points', async function() { const srcQuad = Speedy.Matrix(2, 3, [ 0, 0, 100, 0, 100, 100, ]); const dstQuad = Speedy.Matrix(2, 3, [ 0, 0, 300, 0, 300, 200, ]); printm('From:', srcQuad); printm('To:', dstQuad); const homography = Speedy.Matrix.Zeros(3, 3); expect(() => Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', })).toThrow(); }); it('fails to compute a planar homography using a degenerate configuration', async function() { const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 100, 0, 100, 100, 0, 100, ]); const dstQuad = Speedy.Matrix(2, 4, [ 0, 0, 300, 0, 300, 200, 150, 0, ]); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); expect(homography.read()).toBeElementwiseNaN(); }); it('fails to compute a planar homography using 4 copies of a single point', async function() { const srcQuad = Speedy.Matrix.Zeros(2, 4); const dstQuad = Speedy.Matrix.Zeros(2, 4); const homography = Speedy.Matrix.Zeros(3, 3); await Speedy.Matrix.findHomography(homography, srcQuad, dstQuad, { method: 'default', }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Homography:', homography); expect(homography.read()).toBeElementwiseNaN(); }); }); describe('Affine transform', function() { it('computes an affine transform from three correspondences of points', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 0, 0, 1, 0, 1, 1, ]); const dstTrig = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const tstTrig = Speedy.Matrix.Zeros(2, 3); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform, srcTrig, dstTrig); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); await Speedy.Matrix.applyAffineTransform(tstTrig, srcTrig, transform); expect(tstTrig.read()).toBeElementwiseNearlyEqual(dstTrig.read()); }); it('computes another affine transform from three correspondences of points', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const dstTrig = Speedy.Matrix(2, 3, [ 0, 0, 1, 0, 1, 1, ]); const tstTrig = Speedy.Matrix.Zeros(2, 3); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform, srcTrig, dstTrig); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); await Speedy.Matrix.applyAffineTransform(tstTrig, srcTrig, transform); expect(tstTrig.read()).toBeElementwiseNearlyEqual(dstTrig.read()); }); it('computes yet another affine transform from three correspondences of points', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 0, 0, 1, 0, 1, 1, ]); const dstTrig = Speedy.Matrix(2, 3, [ 1, 0, 4, 0, 4, 2, ]); const tstTrig = Speedy.Matrix.Zeros(2, 3); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform, srcTrig, dstTrig); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); await Speedy.Matrix.applyAffineTransform(tstTrig, srcTrig, transform); expect(tstTrig.read()).toBeElementwiseNearlyEqual(dstTrig.read()); }); it('computes an identity transform from three non-distinct correspondences of points', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const dstTrig = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform, srcTrig, dstTrig); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); const eye = Speedy.Matrix.Eye(2, 3); expect(transform.read()).toBeElementwiseNearlyEqual(eye.read()); }); it('fails to compute an affine transform if 3 points are collinear', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 2, 0, 1, 1, 0, 2, ]); const dstTrig = Speedy.Matrix(2, 3, [ 3, 0, 3, 2, 0, 2, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform, srcTrig, dstTrig); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); const matdata = transform.read(); expect(matdata).toBeElementwiseNaN(); print('----------'); const transform2 = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.affine(transform2, dstTrig, srcTrig); printm('From:', dstTrig); printm('To:', srcTrig); printm('Transform:', transform2); const matdata2 = transform2.read(); expect(matdata2).toBeElementwiseNaN(); }); it('fails to compute an affine transform using matrices of incorrect shape', async function() { const src = Speedy.Matrix(2, 5, [ 0, 0, 1, 0, 1, 1, 0, 1, 51, 42, ]); const dst = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const transform = Speedy.Matrix.Zeros(3, 3); expect(() => Speedy.Matrix.affine(transform, src, dst)).toThrow(); expect(() => Speedy.Matrix.affine(transform, dst, src)).toThrow(); }); it('fails to compute an affine transform using incorrect input', async function() { const src = Speedy.Matrix(2, 3, [ 0, 0, 2, 0, 1, 1, ]); const dst = Speedy.Matrix(2, 4, [ 0, 0, 3, 0, 3, 2, 0, 2, ]); const foo = Speedy.Matrix(2, 4, [ 0, 0, 2, 0, 1, 1, 0, 2, ]); const transform = Speedy.Matrix.Zeros(3, 3); expect(() => Speedy.Matrix.affine(transform, src, dst)).toThrow(); expect(() => Speedy.Matrix.affine(transform, dst, src)).toThrow(); expect(() => Speedy.Matrix.affine(transform, src, src)).toThrow(); expect(() => Speedy.Matrix.affine(transform, src, foo)).toThrow(); expect(() => Speedy.Matrix.affine(transform, foo, src)).toThrow(); }); it('applies an affine transform to a set of points', async function() { const transform = Speedy.Matrix(2, 3, [ 3, 0, 0, 2, 2, 1, ]); const srcQuad = Speedy.Matrix(2, 4, [ 0, 0, 1, 0, 1, 1, 0, 1, ]); const dstQuad = Speedy.Matrix.Zeros(2, 4); await Speedy.Matrix.applyAffineTransform(dstQuad, srcQuad, transform); printm('transform:', transform, 'srcQuad:', srcQuad, 'dstQuad:', dstQuad); const actual = dstQuad.read(); const expected = [2, 1, 5, 1, 5, 3, 2, 3]; expect(actual).toBeElementwiseEqual(expected); }); }); describe('Affine transform with the default method', function() { it('computes an affine transform using 3 correspondences', async function() { const srcTrig = Speedy.Matrix(2, 3, [ 0, 0, 1, 0, 1, 1, ]); const dstTrig = Speedy.Matrix(2, 3, [ 0, 0, 3, 0, 3, 2, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, srcTrig, dstTrig, { method: 'default', }); printm('From:', srcTrig); printm('To:', dstTrig); printm('Transform:', transform); const tstTrig = Speedy.Matrix.Zeros(srcTrig.rows, srcTrig.columns); await Speedy.Matrix.applyAffineTransform(tstTrig, srcTrig, transform); expect(tstTrig.read()).toBeElementwiseNearlyEqual(dstTrig.read()); }); it('computes an affine transform using 5 correspondences', async function() { const src = Speedy.Matrix(2, 5, [ 0, 0, 1, 0, 1, 1, 0, 1, 0.5, 0.5, ]); const dst = Speedy.Matrix(2, 5, [ 0, 0, 3, 0, 3, 2, 0, 2, 1.5, 1.0, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'default', }); printm('From:', src); printm('To:', dst); printm('Homography:', transform); const tst = Speedy.Matrix.Zeros(src.rows, src.columns); await Speedy.Matrix.applyAffineTransform(tst, src, transform); expect(tst.read()).toBeElementwiseNearlyEqual(dst.read()); }); it('computes an affine transform using 8 correspondences', async function() { const src = Speedy.Matrix(2, 8, [ 0, 0, 1, 0, 1, 1, 0, 1, 0.5, 0.5, 2, 2, -1, 0, -1, -1, ]); const dst = Speedy.Matrix(2, 8, [ 0, 0, 3, 0, 3, 2, 0, 2, 1.5, 1.0, 6, 4, -3, 0, -3, -2, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'default', }); printm('From:', src); printm('To:', dst); printm('Transform:', transform); const tst = Speedy.Matrix.Zeros(src.rows, src.columns); await Speedy.Matrix.applyAffineTransform(tst, src, transform); expect(tst.read()).toBeElementwiseNearlyEqual(dst.read()); }); it('fails to compute an affine transform using too few points', async function() { const src = Speedy.Matrix(2, 2, [ 0, 0, 100, 0, ]); const dst = Speedy.Matrix(2, 2, [ 0, 0, 300, 0, ]); printm('From:', src); printm('To:', dst); const transform = Speedy.Matrix.Zeros(2, 3); expect(() => Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'default', })).toThrow(); }); it('fails to compute an affine transform using a degenerate configuration', async function() { const src = Speedy.Matrix(2, 3, [ 0, 0, 50, 50, 100, 100, ]); const dst = Speedy.Matrix(2, 3, [ 0, 0, 100, 100, 200, 200, ]); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'default', }); printm('From:', src); printm('To:', dst); printm('Transform:', transform); expect(transform.read()).toBeElementwiseNaN(); }); it('fails to compute an affine transform using 3 copies of a single point', async function() { const src = Speedy.Matrix.Zeros(2, 3); const dst = Speedy.Matrix.Zeros(2, 3); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'default', }); printm('From:', src); printm('To:', dst); printm('Transform:', transform); expect(transform.read()).toBeElementwiseNaN(); }); }); describe('Affine transform with PRANSAC', function() { const countInliers = maskdata => maskdata.reduce((sum, val) => sum + (val | 0), 0); const countOutliers = maskdata => maskdata.length - countInliers(maskdata); const noise = (w = 1.0) => (Math.random() - 0.5) * w; it('computes an affine transform using only 3 inliers without noise', async function() { const src = Speedy.Matrix(2, 3, [ 1, 0, 1, 1, 0, 1, ]); const dst = Speedy.Matrix(2, 3, [ 3, 0, 3, 2, 0, 2, ]); const mask = Speedy.Matrix.Zeros(1, src.columns); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'pransac', mask: mask }); printm('From:', src); printm('To:', dst); printm('Transform:', transform); printm('Inliers mask:', mask); const tst = Speedy.Matrix.Zeros(src.rows, src.columns); await Speedy.Matrix.applyAffineTransform(tst, src, transform); expect(tst.read()).toBeElementwiseNearlyEqual(dst.read()); expect(countInliers(mask.read())).toEqual(src.columns); }); it('computes an affine transform using only 6 inliers without noise', async function() { const src = Speedy.Matrix(2, 6, [ 1, 0, 1, 1, 0, 1, 3, 2, 3, 3, 2, 3, ]); const dst = Speedy.Matrix(2, 6, [ 3, 0, 3, 2, 0, 2, 9, 4, 9, 6, 6, 6, ]); const mask = Speedy.Matrix.Zeros(1, src.columns); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, src, dst, { method: 'pransac', mask: mask }); printm('From:', src); printm('To:', dst); printm('Transform:', transform); printm('Inliers mask:', mask); const tst = Speedy.Matrix.Zeros(src.rows, src.columns); await Speedy.Matrix.applyAffineTransform(tst, src, transform); expect(tst.read()).toBeElementwiseNearlyEqual(dst.read()); expect(countInliers(mask.read())).toEqual(src.columns); }); it('computes an affine transform using 80% of inliers', async function() { const numInliers = 8; // 8/10 const srcQuad = Speedy.Matrix(2, 10, [ // ---- inliers: ---- 0, 0, 100, 0, 100, 100, 0, 100, 0, 0, 100, 0, 100, 100, 0, 100, // ---- outliers: ---- 9999, 9999, 9999, 9999, ]); const dstQuad = Speedy.Matrix(2, 10, [ // ---- inliers: ---- 0, 0, 300, 0, 300, 200, 0, 200, 0, 0, 300, 0, 300, 200, 0, 200, // ---- outliers: ---- 19999, 9999, 9999, 9999, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const srcQuadInliers = srcQuad.block(0, 1, 0, numInliers - 1); const dstQuadInliers = dstQuad.block(0, 1, 0, numInliers - 1); const maskOutliers = mask.block(0, 0, numInliers, mask.columns - 1); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Transform:', transform); printm('Inliers mask:', mask); const tstQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); const difQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); await Speedy.Matrix.applyAffineTransform(tstQuadInliers, srcQuadInliers, transform); await difQuadInliers.setTo(tstQuadInliers.minus(dstQuadInliers)); const err2 = difQuadInliers.read().reduce((err, x) => err + x*x, 0); printm('Reprojection:', tstQuadInliers, 'vs', dstQuadInliers); printm('Reprojection error: ' + Math.sqrt(err2)); expect(maskOutliers.read()).toBeElementwiseZero(); expect(countInliers(mask.read())).toEqual(numInliers); expect(err2).toBeNearlyZero(); }); it('computes a planar homography using 75% of inliers', async function() { const numInliers = 6; // 6/8 const srcQuad = Speedy.Matrix(2, 8, [ // ---- inliers: ---- 0, 0, 100, 0, 100, 100, 0, 100, 50, 50, 0, 50, // ---- outliers: ---- 9999, 9999, -9999, -9999, ]); const dstQuad = Speedy.Matrix(2, 8, [ // ---- inliers: ---- 0, 0, 300, 0, 300, 200, 0, 200, 150, 100, 0, 100, // ---- outliers: ---- 19999, 9999, 999, 9999, ]); const mask = Speedy.Matrix.Zeros(1, srcQuad.columns); const srcQuadInliers = srcQuad.block(0, 1, 0, numInliers - 1); const dstQuadInliers = dstQuad.block(0, 1, 0, numInliers - 1); const maskOutliers = mask.block(0, 0, numInliers, mask.columns - 1); const transform = Speedy.Matrix.Zeros(2, 3); await Speedy.Matrix.findAffineTransform(transform, srcQuad, dstQuad, { method: 'pransac', mask: mask }); printm('From:', srcQuad); printm('To:', dstQuad); printm('Transform:', transform); printm('Inliers mask:', mask); const tstQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); const difQuadInliers = Speedy.Matrix.Zeros(srcQuadInliers.rows, srcQuadInliers.columns); await Speedy.Matrix.applyAffineTransform(tstQuadInliers, srcQuadInliers, transform); await difQuadInliers.setTo(tstQuadInliers.minus(dstQuadInliers)); const err2 = difQuadInliers.read().reduce((err, x) => err + x*x, 0); printm('Reprojection:', tstQuadInliers, 'vs', dstQuadInliers); printm('Reprojection error: ' + Math.sqrt(err2)); expect(maskOutliers.read()).toBeElementwiseZero(); expect(countInliers(mask.read())).toEqual(numInliers); expect(err2).toBeNearlyZero(); }); it('computes a planar homography using 50% of inliers', async function() { const numInliers = 8; // 8/16 const srcQuad = Speedy.Matrix(2, 16, [ // ---- inliers: ---- 100, 0, 100, 100, 0, 100, -50, -50, 100, 0, 100, 100, 0, 100, -50, -50, // ---- outliers: ---- 999, 999, -999, -999, -999, 999, 999, -999, 7999, 0, -1, -9