UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

565 lines (488 loc) 23.5 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. * * speedy-matrix-factory.js * A factory of matrices */ import { SpeedyMatrixExpr } from './speedy-matrix-expr'; import { SpeedyMatrixWASM } from './speedy-matrix-wasm'; import { SpeedyMatrix } from './speedy-matrix'; import { SpeedyPromise } from './speedy-promise'; import { Utils } from '../utils/utils'; import { IllegalArgumentError } from '../utils/errors'; /** * Matrix routines */ export class SpeedyMatrixFactory extends Function { /** * Constructor */ constructor() { // This factory can be invoked as a function super('...args', 'return args.length > 1 ? this._create(...args) : this._from(args[0])'); return this.bind(this); } /** * @private * * Create a new matrix filled with the specified size and entries * @param {number} rows * @param {number} [columns] * @param {number[]} [entries] in column-major format * @returns {SpeedyMatrix} */ _create(rows, columns = rows, entries = []) { return SpeedyMatrix.Create(rows, columns, entries); } /** * @private * * Evaluate an expression synchronously and store the result in a new matrix * @param {SpeedyMatrixExpr} expr matrix expression * @returns {SpeedyMatrix} */ _from(expr) { return SpeedyMatrix.From(expr); } /** * Create a new matrix filled with zeros with the specified size * @param {number} rows * @param {number} [columns] * @returns {SpeedyMatrix} */ Zeros(rows, columns = rows) { return SpeedyMatrix.Zeros(rows, columns); } /** * Create a new matrix filled with ones with the specified size * @param {number} rows * @param {number} [columns] * @returns {SpeedyMatrix} */ Ones(rows, columns = rows) { return SpeedyMatrix.Ones(rows, columns); } /** * Create an identity matrix with the specified size * @param {number} rows * @param {number} [columns] * @returns {SpeedyMatrix} */ Eye(rows, columns = rows) { return SpeedyMatrix.Eye(rows, columns); } /** * Returns a promise that resolves immediately if the WebAssembly routines * are ready to be used, or as soon as they do become ready * @returns {SpeedyPromise<void>} */ ready() { return SpeedyMatrix.ready(); } /** * QR decomposition * @param {SpeedyMatrix} Q is m x n (reduced) or m x m (full), output * @param {SpeedyMatrix} R is n x n (reduced) or m x n (full), output * @param {SpeedyMatrix} mat is m x n, input * @param {object} [options] * @param {'reduced'|'full'} [options.mode] * @returns {SpeedyPromise<[SpeedyMatrix,SpeedyMatrix]>} resolves to [Q,R] */ qr(Q, R, mat, { mode = 'reduced' } = {}) { const A = mat, m = mat.rows, n = mat.columns; // validate shapes & mode if(mode == 'reduced') { if(Q.rows != m || Q.columns != n || R.rows != n || R.columns != n) throw new IllegalArgumentError(`Invalid shape for reduced QR`); } else if(mode == 'full') { if(Q.rows != m || Q.columns != m || R.rows != m || R.columns != n) throw new IllegalArgumentError(`Invalid shape for full QR`); } else throw new IllegalArgumentError(`Invalid mode for QR: "${mode}"`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const Qptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, Q); const Rptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, R); const Aptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, A); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, Aptr, A); // run the WASM routine if(mode == 'reduced') wasm.exports.Mat32_qr_reduced(Qptr, Rptr, Aptr); else wasm.exports.Mat32_qr_full(Qptr, Rptr, Aptr); // copy output matrices from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, Qptr, Q); SpeedyMatrixWASM.copyFromMat32(wasm, memory, Rptr, R); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, Aptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, Rptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, Qptr); // done! return [Q, R]; }); } /** * Solve a possibly overdetermined system of linear * equations Ax = b for x using ordinary least squares * @param {SpeedyMatrix} solution n x 1, output * @param {SpeedyMatrix} A m x n, m >= n, input * @param {SpeedyMatrix} b m x 1, output * @param {object} [options] * @param {'qr'} [options.method] method of resolution * @returns {SpeedyPromise<SpeedyMatrix>} resolves to solution */ ols(solution, A, b, { method = 'qr' } = {}) { const m = A.rows, n = A.columns; const x = solution; // validate shapes if(m < n || n == 0) throw new IllegalArgumentError(`Can't solve an underdetermined system of equations`); else if(b.rows != m || b.columns != 1 || x.rows != n || x.columns != 1) throw new IllegalArgumentError(`Invalid shapes`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const Aptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, A); const bptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, b); const xptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, x); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, Aptr, A); SpeedyMatrixWASM.copyToMat32(wasm, memory, bptr, b); // run the WASM routine switch(method) { case 'qr': wasm.exports.Mat32_qr_ols(xptr, Aptr, bptr, 2); break; default: throw new IllegalArgumentError(`Invalid method: "${method}"`); } // copy output matrix from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, xptr, x); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, xptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, bptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, Aptr); // done! return solution; }); } /** * Solve a system of linear equations Ax = b for x * @param {SpeedyMatrix} solution m x 1, output * @param {SpeedyMatrix} A m x m, input * @param {SpeedyMatrix} b m x 1, output * @param {object} [options] * @param {'qr'} [options.method] method of resolution * @returns {SpeedyPromise<SpeedyMatrix>} resolves to solution */ solve(solution, A, b, { method = 'qr' } = {}) { const m = A.rows, n = A.columns; const x = solution; // validate shapes if(m != n) throw new IllegalArgumentError(`Can't solve an over or underdetermined system of equations`); else if(b.rows != m || b.columns != 1 || x.rows != m || x.columns != 1) throw new IllegalArgumentError(`Invalid shapes`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // select method switch(method) { case 'qr': return this.ols(x, A, b, { method }); /*case 'lu': break;*/ default: throw new IllegalArgumentError(`Invalid method: "${method}"`); } }); } /** * Compute a perspective transformation using 4 correspondences of points * @param {SpeedyMatrix} homography 3x3 output - homography matrix * @param {SpeedyMatrix} src 2x4 input points - source coordinates * @param {SpeedyMatrix} dest 2x4 input points - destination coordinates * @returns {SpeedyPromise<SpeedyMatrix>} resolves to homography */ perspective(homography, src, dest) { // validate shapes if(src.rows != 2 || src.columns != 4 || dest.rows != 2 || dest.columns != 4) throw new IllegalArgumentError(`You need two 2x4 input matrices to compute a perspective transformation`); else if(homography.rows != 3 || homography.columns != 3) throw new IllegalArgumentError(`The output of perspective() is a 3x3 homography`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const homptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, homography); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, destptr, dest); // run the WASM routine wasm.exports.Mat32_homography_ndlt4(homptr, srcptr, destptr); // copy output matrix from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, homptr, homography); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, homptr); // done! return homography; }); } /** * Compute a perspective transformation using n >= 4 correspondences of points * @param {SpeedyMatrix} homography 3x3 output - homography matrix * @param {SpeedyMatrix} src 2 x n input points - source coordinates * @param {SpeedyMatrix} dest 2 x n input points - destination coordinates * @param {object} [options] * @param {'default'|'pransac'} [options.method] method of computation * @param {SpeedyMatrix|null} [options.mask] (pransac) 1 x n output: i-th entry will be 1 if the i-th input point is an inlier, or 0 otherwise * @param {number} [options.reprojectionError] (pransac) given in pixels, used to separate inliers from outliers of a particular model (e.g., 1 pixel) * @param {number} [options.numberOfHypotheses] (pransac) number of hypotheses to be generated up-front (e.g., 512) * @param {number} [options.bundleSize] (pransac) how many points should we check before reducing the number of viable hypotheses (e.g., 128) * @returns {SpeedyPromise<SpeedyMatrix>} resolves to homography */ findHomography(homography, src, dest, { method = 'default', mask = null, reprojectionError = 3, numberOfHypotheses = 512, bundleSize = 128, } = {}) { // validate shapes if(src.rows != 2 || src.columns < 4 || dest.rows != 2 || dest.columns != src.columns) throw new IllegalArgumentError(`You need two 2 x n (n >= 4) input matrices to compute a homography`); else if(homography.rows != 3 || homography.columns != 3) throw new IllegalArgumentError(`The output of findHomography() is a 3x3 homography`); else if(mask != null && (mask.rows != 1 || mask.columns != src.columns)) throw new IllegalArgumentError(`Invalid shape of the inliers mask`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const homptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, homography); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); const maskptr = mask != null ? SpeedyMatrixWASM.allocateMat32(wasm, memory, mask) : 0; // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, destptr, dest); // run the WASM routine switch(method) { case 'pransac': Utils.assert(reprojectionError >= 0 && numberOfHypotheses > 0 && bundleSize > 0); wasm.exports.Mat32_pransac_homography(homptr, maskptr, srcptr, destptr, numberOfHypotheses, bundleSize, reprojectionError); break; case 'default': case 'dlt': // obsolete wasm.exports.Mat32_homography_ndlt(homptr, srcptr, destptr); break; default: throw new IllegalArgumentError(`Illegal method for findHomography(): "${method}"`); } // copy output matrices from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, homptr, homography); if(mask != null) SpeedyMatrixWASM.copyFromMat32(wasm, memory, maskptr, mask); // deallocate matrices if(mask != null) SpeedyMatrixWASM.deallocateMat32(wasm, memory, maskptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, homptr); // done! return homography; }); } /** * Apply a perspective transformation to a set of 2D points * @param {SpeedyMatrix} dest 2 x n output matrix * @param {SpeedyMatrix} src 2 x n input matrix (a set of points) * @param {SpeedyMatrix} transform 3x3 homography matrix * @returns {SpeedyPromise<SpeedyMatrix>} resolves to dest */ applyPerspectiveTransform(dest, src, transform) { // validate shapes if(src.rows != 2 || dest.rows != 2 || src.columns != dest.columns) throw new IllegalArgumentError(`Invalid shapes`); else if(transform.rows != 3 || transform.columns != 3) throw new IllegalArgumentError(`The perspective transformation must be a 3x3 matrix`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const matptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, transform); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, matptr, transform); // run the WASM routine wasm.exports.Mat32_transform_perspective(destptr, srcptr, matptr); // copy output matrix from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, destptr, dest); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, matptr); // done! return dest; }); } /** * Compute an affine transform using 3 correspondences of points * @param {SpeedyMatrix} transform 2x3 output - affine transform * @param {SpeedyMatrix} src 2x3 input points - source coordinates * @param {SpeedyMatrix} dest 2x3 input points - destination coordinates * @returns {SpeedyPromise<SpeedyMatrix>} resolves to homography */ affine(transform, src, dest) { // validate shapes if(src.rows != 2 || src.columns != 3 || dest.rows != 2 || dest.columns != 3) throw new IllegalArgumentError(`You need two 2x3 input matrices to compute an affine transform`); else if(transform.rows != 2 || transform.columns != 3) throw new IllegalArgumentError(`The output of affine() is a 2x3 matrix`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const matptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, transform); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, destptr, dest); // run the WASM routine wasm.exports.Mat32_affine_direct3(matptr, srcptr, destptr); // copy output matrix from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, matptr, transform); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, matptr); // done! return transform; }); } /** * Compute an affine transformation using n >= 3 correspondences of points * @param {SpeedyMatrix} transform 2x3 output - affine transform * @param {SpeedyMatrix} src 2 x n input points - source coordinates * @param {SpeedyMatrix} dest 2 x n input points - destination coordinates * @param {object} [options] * @param {'default'|'pransac'} [options.method] method of computation * @param {SpeedyMatrix|null} [options.mask] (pransac) 1 x n output: i-th entry will be 1 if the i-th input point is an inlier, or 0 otherwise * @param {number} [options.reprojectionError] (pransac) given in pixels, used to separate inliers from outliers of a particular model (e.g., 1 pixel) * @param {number} [options.numberOfHypotheses] (pransac) number of hypotheses to be generated up-front (e.g., 512) * @param {number} [options.bundleSize] (pransac) how many points should we check before reducing the number of viable hypotheses (e.g., 128) * @returns {SpeedyPromise<SpeedyMatrix>} resolves to an affine transform */ findAffineTransform(transform, src, dest, { method = 'default', mask = null, reprojectionError = 3, numberOfHypotheses = 512, bundleSize = 128, } = {}) { // validate shapes if(src.rows != 2 || src.columns < 3 || dest.rows != 2 || dest.columns != src.columns) throw new IllegalArgumentError(`You need two 2 x n (n >= 3) input matrices to compute an affine transform`); else if(transform.rows != 2 || transform.columns != 3) throw new IllegalArgumentError(`The output of findAffineTransform() is a 2x3 matrix`); else if(mask != null && (mask.rows != 1 || mask.columns != src.columns)) throw new IllegalArgumentError(`Invalid shape of the inliers mask`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const matptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, transform); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); const maskptr = mask != null ? SpeedyMatrixWASM.allocateMat32(wasm, memory, mask) : 0; // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, destptr, dest); // run the WASM routine switch(method) { case 'pransac': Utils.assert(reprojectionError >= 0 && numberOfHypotheses > 0 && bundleSize > 0); wasm.exports.Mat32_pransac_affine(matptr, maskptr, srcptr, destptr, numberOfHypotheses, bundleSize, reprojectionError); break; case 'default': wasm.exports.Mat32_affine_direct(matptr, srcptr, destptr); break; default: throw new IllegalArgumentError(`Illegal method for findAffineTransform(): "${method}"`); } // copy output matrices from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, matptr, transform); if(mask != null) SpeedyMatrixWASM.copyFromMat32(wasm, memory, maskptr, mask); // deallocate matrices if(mask != null) SpeedyMatrixWASM.deallocateMat32(wasm, memory, maskptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, matptr); // done! return transform; }); } /** * Apply an affine transformation to a set of 2D points * @param {SpeedyMatrix} dest 2 x n output matrix * @param {SpeedyMatrix} src 2 x n input matrix (a set of points) * @param {SpeedyMatrix} transform 2x3 affine transform * @returns {SpeedyPromise<SpeedyMatrix>} resolves to dest */ applyAffineTransform(dest, src, transform) { // validate shapes if(src.rows != 2 || dest.rows != 2 || src.columns != dest.columns) throw new IllegalArgumentError(`Invalid shapes`); else if(transform.rows != 2 || transform.columns != 3) throw new IllegalArgumentError(`The affine transformation must be a 2x3 matrix`); return SpeedyMatrixWASM.ready().then(({wasm, memory}) => { // allocate matrices const matptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, transform); const srcptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, src); const destptr = SpeedyMatrixWASM.allocateMat32(wasm, memory, dest); // copy input matrices to WASM memory SpeedyMatrixWASM.copyToMat32(wasm, memory, srcptr, src); SpeedyMatrixWASM.copyToMat32(wasm, memory, matptr, transform); // run the WASM routine wasm.exports.Mat32_transform_affine(destptr, srcptr, matptr); // copy output matrix from WASM memory SpeedyMatrixWASM.copyFromMat32(wasm, memory, destptr, dest); // deallocate matrices SpeedyMatrixWASM.deallocateMat32(wasm, memory, destptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, srcptr); SpeedyMatrixWASM.deallocateMat32(wasm, memory, matptr); // done! return dest; }); } }