UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

421 lines (365 loc) 14.1 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.js * Matrix class */ import { SpeedyMatrixExpr } from './speedy-matrix-expr'; import { SpeedyMatrixWASM } from './speedy-matrix-wasm'; import { SpeedyPromise } from './speedy-promise'; import { Utils } from '../utils/utils'; /** @typedef {"float32"} SpeedyMatrixDtype Matrix data type */ /** @typedef {Float32Array} SpeedyMatrixBufferType Buffer type */ /** @typedef {Float32ArrayConstructor} SpeedyMatrixBufferTypeConstructor Buffer class */ /** @typedef {import('./speedy-matrix-wasm').SpeedyMatrixWASMMemory} SpeedyMatrixWASMMemory */ /** @typedef {import('./speedy-matrix-wasm').SpeedyMatrixWASMHandle} SpeedyMatrixWASMHandle */ /** * Matrix class */ export class SpeedyMatrix extends SpeedyMatrixExpr { /** * @private * * Low-level constructor * @param {number} rows number of rows * @param {number} columns number of columns * @param {number} step0 step size between two consecutive elements (e.g., 1) * @param {number} step1 step size between two consecutive columns (e.g., rows) * @param {SpeedyMatrixBufferType} data entries in column-major format */ constructor(rows, columns, step0, step1, data) { super(rows, columns, SpeedyMatrixExpr.DEFAULT_DTYPE); Utils.assert(data.constructor === SpeedyMatrixExpr.BUFFER_TYPE[this.dtype]); Utils.assert(step0 > 0 && step1 >= step0); Utils.assert( data.length + rows * columns === 0 || // empty matrix and empty buffer, or data.length === 1 + step0 * (rows - 1) + step1 * (columns - 1) // correctly sized buffer ); /** @type {number} step size between two consecutive elements */ this._step0 = step0 | 0; /** @type {number} step size between two consecutive columns */ this._step1 = step1 | 0; /** @type {SpeedyMatrixBufferType} buffer containing the entries of the matrix in column-major order */ this._data = data; } /** * Create a new matrix with the specified size and entries * @param {number} rows number of rows * @param {number} columns number of columns * @param {number[]} entries in column-major format * @param {SpeedyMatrixDtype} [dtype] data type * @returns {SpeedyMatrix} */ static Create(rows, columns, entries, dtype = SpeedyMatrixExpr.DEFAULT_DTYPE) { Utils.assert(rows * columns > 0, `Can't create a matrix without a shape`); Utils.assert(rows * columns === entries.length, `Can't create matrix: expected ${rows * columns} entries, but found ${entries.length}`); Utils.assert(Object.prototype.hasOwnProperty.call(SpeedyMatrixExpr.BUFFER_TYPE, dtype), `Invalid dtype: "${dtype}"`); return new SpeedyMatrix(rows, columns, 1, rows, Reflect.construct(SpeedyMatrixExpr.BUFFER_TYPE[dtype], [entries])); } /** * Create a new matrix filled with zeros with the specified size * @param {number} rows number of rows * @param {number} [columns] number of columns * @param {SpeedyMatrixDtype} [dtype] data type * @returns {SpeedyMatrix} */ static Zeros(rows, columns = rows, dtype = SpeedyMatrixExpr.DEFAULT_DTYPE) { Utils.assert(rows * columns > 0, `Can't create a matrix without a shape`); Utils.assert(Object.prototype.hasOwnProperty.call(SpeedyMatrixExpr.BUFFER_TYPE, dtype), `Invalid dtype: "${dtype}"`); return new SpeedyMatrix(rows, columns, 1, rows, Reflect.construct(SpeedyMatrixExpr.BUFFER_TYPE[dtype], [rows * columns])); } /** * Create a new matrix filled with ones with the specified size * @param {number} rows number of rows * @param {number} [columns] number of columns * @param {SpeedyMatrixDtype} [dtype] data type * @returns {SpeedyMatrix} */ static Ones(rows, columns = rows, dtype = SpeedyMatrixExpr.DEFAULT_DTYPE) { Utils.assert(rows * columns > 0, `Can't create a matrix without a shape`); Utils.assert(Object.prototype.hasOwnProperty.call(SpeedyMatrixExpr.BUFFER_TYPE, dtype), `Invalid dtype: "${dtype}"`); return new SpeedyMatrix(rows, columns, 1, rows, Reflect.construct(SpeedyMatrixExpr.BUFFER_TYPE[dtype], [rows * columns]).fill(1)); } /** * Create a new identity matrix with the specified size * @param {number} rows number of rows * @param {number} [columns] number of columns * @param {SpeedyMatrixDtype} [dtype] data type * @returns {SpeedyMatrix} */ static Eye(rows, columns = rows, dtype = SpeedyMatrixExpr.DEFAULT_DTYPE) { Utils.assert(rows * columns > 0, `Can't create a matrix without a shape`); Utils.assert(Object.prototype.hasOwnProperty.call(SpeedyMatrixExpr.BUFFER_TYPE, dtype), `Invalid dtype: "${dtype}"`); const data = Reflect.construct(SpeedyMatrixExpr.BUFFER_TYPE[dtype], [rows * columns]); for(let j = Math.min(rows, columns) - 1; j >= 0; j--) data[j * rows + j] = 1; return new SpeedyMatrix(rows, columns, 1, rows, data); } /** * Evaluate an expression synchronously and store the result in a new matrix * @param {SpeedyMatrixExpr} expr matrix expression * @returns {SpeedyMatrix} */ static From(expr) { return SpeedyMatrix.Zeros(expr.rows, expr.columns, expr.dtype).setToSync(expr); } /** * 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>} */ static ready() { return SpeedyMatrixWASM.ready().then(_ => void(0)); } /** * Get the underlying buffer * @returns {SpeedyMatrixBufferType} */ get data() { return this._data; } /** * Row-step * @returns {number} defaults to 1 */ get step0() { return this._step0; } /** * Column-step * @returns {number} defaults to this.rows */ get step1() { return this._step1; } /** * Extract a block from this matrix. Use a shared underlying buffer * @param {number} firstRow * @param {number} lastRow * @param {number} firstColumn * @param {number} lastColumn * @returns {SpeedyMatrix} */ block(firstRow, lastRow, firstColumn, lastColumn) { Utils.assert( firstRow <= lastRow && firstColumn <= lastColumn, `Invalid indices: [${firstRow}:${lastRow},${firstColumn}:${lastColumn}]` ); // ensure that the indices are within bounds firstRow = Math.max(firstRow, 0); lastRow = Math.min(lastRow, this._rows - 1); firstColumn = Math.max(firstColumn, 0); lastColumn = Math.min(lastColumn, this._columns - 1); // compute the dimensions of the new submatrix const rows = lastRow - firstRow + 1; const columns = lastColumn - firstColumn + 1; // obtain the relevant portion of the data const step0 = this._step0, step1 = this._step1; const begin = firstRow * step0 + firstColumn * step1; // inclusive const end = 1 + lastRow * step0 + lastColumn * step1; // exclusive // create new matrix return new SpeedyMatrix(rows, columns, step0, step1, this._data.subarray(begin, end)); } /** * Extract a row from this matrix * @param {number} index 0-based * @returns {SpeedyMatrix} */ row(index) { return this.block(index, index, 0, this._columns - 1); } /** * Extract a column from this matrix * @param {number} index 0-based * @returns {SpeedyMatrix} */ column(index) { return this.block(0, this._rows - 1, index, index); } /** * Extract the main diagonal from this matrix * @returns {SpeedyMatrix} as a column-vector */ diagonal() { const diagsize = Math.min(this._rows, this._columns); // compute the dimensions of the new submatrix const rows = diagsize; // make it a column vector const columns = 1; // obtain the relevant portion of the data const diagstep = this._step0 + this._step1; // jump a row and a column const begin = 0; // inclusive const end = 1 + (diagsize - 1) * diagstep; // exclusive // create new matrix return new SpeedyMatrix(rows, columns, diagstep, diagstep, this._data.subarray(begin, end)); } /** * Read a single entry of this matrix * @param {number} row 0-based index * @param {number} column 0-based index * @returns {number} */ at(row, column) { if(row >= 0 && row < this._rows && column >= 0 && column < this._columns) return this._data[this._step0 * row + this._step1 * column]; else return Number.NaN; } /** * Read the entries of the matrix in column-major format * @returns {number[]} */ read() { const entries = new Array(this._rows * this._columns); const step0 = this._step0, step1 = this._step1; let i = 0; for(let column = 0; column < this._columns; column++) { for(let row = 0; row < this._rows; row++) entries[i++] = this._data[row * step0 + column * step1]; } return entries; } /** * Returns a human-readable string representation of the matrix * @returns {string} */ toString() { const DECIMALS = 5; const rows = this.rows, columns = this.columns; const entries = this.read(); const mat = /** @type {number[][]} */ ( new Array(rows) ); for(let i = 0; i < rows; i++) { mat[i] = new Array(columns); for(let j = 0; j < columns; j++) mat[i][j] = entries[j * rows + i]; } const fix = x => x.toFixed(DECIMALS); const fmt = mat.map(row => ' ' + row.map(fix).join(', ')).join(',\n'); const str = `SpeedyMatrix(rows=${rows}, columns=${columns}, data=[\n${fmt}\n])`; return str; } /** * Set the contents of this matrix to the result of an expression * @param {SpeedyMatrixExpr} expr matrix expression * @returns {SpeedyPromise<SpeedyMatrix>} resolves to this */ setTo(expr) { return SpeedyMatrixWASM.ready().then(_ => { // TODO: add support for WebWorkers return this.setToSync(expr); }); } /** * Synchronously set the contents of this matrix to the result of an expression * @param {SpeedyMatrixExpr} expr matrix expression * @returns {SpeedyMatrix} this */ setToSync(expr) { const { wasm, memory } = SpeedyMatrixWASM.handle; // evaluate the expression const result = expr._evaluate(wasm, memory); /* // shallow copy the results to this matrix // limitation: can't handle blocks properly // (a tree-like structure could be useful) this._rows = result.rows; this._columns = result.columns; //this._dtype = result.dtype; this._data = result.data; this._step0 = result.step0; this._step1 = result.step1; */ // validate shape Utils.assert( this._rows === result._rows && this._columns === result._columns && this.dtype === result.dtype, `Can't set the values of a ${this.rows} x ${this.columns} ${this.dtype} matrix to those of a ${result.rows} x ${result.columns} ${result.dtype} matrix` ); // deep copy const step0 = this._step0, step1 = this._step1, rstep0 = result._step0, rstep1 = result._step1; if(step0 === rstep0 && step1 === rstep1 && this._data.length === result._data.length) { // fast copy this._data.set(result._data); } else { // copy each element for(let column = this._columns - 1; column >= 0; column--) { for(let row = this._rows - 1; row >= 0; row--) this._data[row * step0 + column * step1] = result._data[row * rstep0 + column * rstep1]; } } // done! return this; } /** * Fill this matrix with a scalar value * @param {number} value * @returns {SpeedyPromise<SpeedyMatrix>} resolves to this */ fill(value) { this.fillSync(value); return SpeedyPromise.resolve(this); } /** * Synchronously fill this matrix with a scalar value * @param {number} value * @returns {SpeedyMatrix} this */ fillSync(value) { value = +value; if(this._rows * this._columns === this._data.length) { this._data.fill(value); return this; } for(let column = 0; column < this._columns; column++) { for(let row = 0; row < this._rows; row++) { this._data[row * this._step0 + column * this._step1] = value; } } return this; } /** * Evaluate this expression * @param {WebAssembly.Instance} wasm * @param {SpeedyMatrixWASMMemory} memory * @returns {SpeedyMatrix} */ _evaluate(wasm, memory) { return this; } }