UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

397 lines (343 loc) 11.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. * * pipeline-node.js * Node of a pipeline */ import { Utils } from '../../utils/utils'; import { LITTLE_ENDIAN } from '../../utils/globals'; import { SpeedyPromise } from '../speedy-promise'; import { AbstractMethodError, IllegalArgumentError } from '../../utils/errors'; import { SpeedyPipelinePort, SpeedyPipelineInputPort, SpeedyPipelineOutputPort } from './pipeline-port'; import { SpeedyPipelinePortBuilder } from './pipeline-portbuilder'; import { SpeedyGPU } from '../../gpu/speedy-gpu'; import { SpeedyTexture, SpeedyDrawableTexture } from '../../gpu/speedy-texture'; import { SpeedyTextureReader } from '../../gpu/speedy-texture-reader'; /** @typedef {Object<string,SpeedyPipelineInputPort>} InputPortDictionary */ /** @typedef {Object<string,SpeedyPipelineOutputPort>} OutputPortDictionary */ /** Generate a random name for a node */ const generateRandomName = () => Math.random().toString(16).substr(2); /** Create an empty input port dictionary */ const createInputPortDictionary = () => /** @type {InputPortDictionary} */ ( Object.create(null) ); /** Create an empty output port dictionary */ const createOutputPortDictionary = () => /** @type {OutputPortDictionary} */ ( Object.create(null) ); /** * Map an array of input ports to an InputPortDictionary whose keys are their names * @param {SpeedyPipelineInputPort[]} ports * @returns {InputPortDictionary} */ function InputPortDictionary(ports) { return ports.reduce((dict, port) => ((dict[port.name] = port), dict), createInputPortDictionary()); } /** * Map an array of output ports to an OutputPortDictionary whose keys are their names * @param {SpeedyPipelineOutputPort[]} ports * @returns {OutputPortDictionary} */ function OutputPortDictionary(ports) { return ports.reduce((dict, port) => ((dict[port.name] = port), dict), createOutputPortDictionary()); } /** A flag used for debugging purposes */ let _texView = false; /** * Node of a pipeline * @abstract */ export class SpeedyPipelineNode { /** * Constructor * @param {string} [name] the name of this node * @param {number} [texCount] number of work textures * @param {SpeedyPipelinePortBuilder[]} [portBuilders] port builders */ constructor(name = generateRandomName(), texCount = 0, portBuilders = []) { /** @type {string} the name of this node */ this._name = String(name); /** @type {SpeedyDrawableTexture[]} work texture(s) */ this._tex = (new Array(texCount)).fill(null); // build the ports const ports = portBuilders.map(builder => builder.build(this)); const inputPorts = /** @type {SpeedyPipelineInputPort[]} */ ( ports.filter(port => port.isInputPort()) ); const outputPorts = /** @type {SpeedyPipelineOutputPort[]} */ ( ports.filter(port => port.isOutputPort()) ); /** @type {InputPortDictionary} input ports */ this._inputPorts = InputPortDictionary(inputPorts); /** @type {OutputPortDictionary} output ports */ this._outputPorts = OutputPortDictionary(outputPorts); // validate if(this._name.length == 0) throw new IllegalArgumentError(`Invalid name "${this._name}" for node ${this.fullName}`); else if(portBuilders.length == 0) throw new IllegalArgumentError(`No ports have been found in node ${this.fullName}`); } /** * The name of this node * @returns {string} */ get name() { return this._name; } /** * Name and type of this node * @returns {string} */ get fullName() { return `${this.constructor.name}[${this.name}]`; } /** * Find input port by name * @param {string} [portName] * @returns {SpeedyPipelineInputPort} */ input(portName = SpeedyPipelineInputPort.DEFAULT_NAME) { if(portName in this._inputPorts) return this._inputPorts[portName]; throw new IllegalArgumentError(`Can't find input port ${portName} in node ${this.fullName}`); } /** * Find output port by name * @param {string} [portName] * @returns {SpeedyPipelineOutputPort} */ output(portName = SpeedyPipelineOutputPort.DEFAULT_NAME) { if(portName in this._outputPorts) return this._outputPorts[portName]; throw new IllegalArgumentError(`Can't find output port ${portName} in node ${this.fullName}`); } /** * Get data from the input ports and execute * the task that this node is supposed to! * @param {SpeedyGPU} gpu * @returns {void|SpeedyPromise<void>} */ execute(gpu) { let portName; // clear output ports for(portName in this._outputPorts) this._outputPorts[portName].clearMessage(); // let the input ports receive what is due for(portName in this._inputPorts) this._inputPorts[portName].pullMessage(this.fullName); // run the task const runTask = this._run(gpu); if(typeof runTask === 'undefined') { for(portName in this._outputPorts) // ensure that no output ports are empty Utils.assert(this._outputPorts[portName].hasMessage(), `Did you forget to write data to the output port ${portName} of ${this.fullName}?`); return undefined; } else return runTask.then(() => { for(portName in this._outputPorts) // ensure that no output ports are empty Utils.assert(this._outputPorts[portName].hasMessage(), `Did you forget to write data to the output port ${portName} of ${this.fullName}?`); }); } /** * Run the specific task of this node * @abstract * @param {SpeedyGPU} gpu * @returns {void|SpeedyPromise<void>} */ _run(gpu) { throw new AbstractMethodError(); } /** * Initializes this node * @param {SpeedyGPU} gpu */ init(gpu) { gpu.subscribe(this._allocateWorkTextures, this, gpu); this._allocateWorkTextures(gpu); } /** * Releases this node * @param {SpeedyGPU} gpu */ release(gpu) { this._deallocateWorkTextures(gpu); gpu.unsubscribe(this._allocateWorkTextures, this); } /** * Clear all ports */ clearPorts() { let portName; for(portName in this._inputPorts) this._inputPorts[portName].clearMessage(); for(portName in this._outputPorts) this._outputPorts[portName].clearMessage(); } /** * Find all nodes that feed input to this node * @returns {SpeedyPipelineNode[]} */ inputNodes() { const nodes = []; for(const portName in this._inputPorts) { const port = this._inputPorts[portName]; if(port.incomingLink != null) nodes.push(port.incomingLink.node); } return nodes; } /** * Is this a source of the pipeline? * @returns {boolean} */ isSource() { return false; } /** * Is this a sink of the pipeline? * @returns {boolean} */ isSink() { return false; // note: a portal sink has no output ports, but it isn't a sink of the pipeline! //return Object.keys(this._outputPorts).length == 0; } /** * Allocate work texture(s) * @param {SpeedyGPU} gpu */ _allocateWorkTextures(gpu) { for(let j = 0; j < this._tex.length; j++) this._tex[j] = gpu.texturePool.allocate(); } /** * Deallocate work texture(s) * @param {SpeedyGPU} gpu */ _deallocateWorkTextures(gpu) { for(let j = this._tex.length - 1; j >= 0; j--) this._tex[j] = gpu.texturePool.free(this._tex[j]); } /** * Inspect the pixels of a texture for debugging purposes * @param {SpeedyGPU} gpu * @param {SpeedyDrawableTexture} texture * @returns {Uint8Array} */ _inspect(gpu, texture) { const textureReader = new SpeedyTextureReader(); textureReader.init(gpu); const pixels = textureReader.readPixelsSync(texture); textureReader.release(gpu); return new Uint8Array(pixels); // copy the array } /** * Inspect the pixels of a texture as unsigned 32-bit integers * @param {SpeedyGPU} gpu * @param {SpeedyDrawableTexture} texture * @returns {Uint32Array} */ _inspect32(gpu, texture) { Utils.assert(LITTLE_ENDIAN); // make sure we use little-endian return new Uint32Array(this._inspect(gpu, texture).buffer); } /** * Visually inspect a texture for debugging purposes * @param {SpeedyGPU} gpu * @param {SpeedyDrawableTexture} texture */ _visualize(gpu, texture) { const canvas = gpu.renderToCanvas(texture); if(!_texView) { document.body.appendChild(canvas); _texView = true; } } } /** * Source node (a node with no input ports) * @abstract */ export class SpeedyPipelineSourceNode extends SpeedyPipelineNode { /** * Constructor * @param {string} [name] the name of this node * @param {number} [texCount] number of work textures * @param {SpeedyPipelinePortBuilder[]} [portBuilders] port builders */ constructor(name = undefined, texCount = undefined, portBuilders = undefined) { super(name, texCount, portBuilders); Utils.assert(Object.keys(this._inputPorts).length == 0); } /** * Is this a source of the pipeline? * @returns {boolean} */ isSource() { return true; } } /** * Sink node (a node with no output ports) * @abstract */ export class SpeedyPipelineSinkNode extends SpeedyPipelineNode { /** * Constructor * @param {string} [name] the name of this node * @param {number} [texCount] number of work textures * @param {SpeedyPipelinePortBuilder[]} [portBuilders] port builders */ constructor(name = undefined, texCount = undefined, portBuilders = undefined) { super(name, texCount, portBuilders); Utils.assert(Object.keys(this._outputPorts).length == 0); } /** * Export data from this node to the user * @abstract * @returns {SpeedyPromise<any>} */ export() { throw new AbstractMethodError(); } /** * Is this a sink of the pipeline? * @returns {boolean} */ isSink() { return true; } }