UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

320 lines (264 loc) 9.87 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.js * A pipeline is a network of nodes in which data flows to a sink */ import { Utils } from '../../utils/utils'; import { SpeedyPromise } from '../speedy-promise'; import { IllegalOperationError, IllegalArgumentError, NotSupportedError } from '../../utils/errors'; import { SpeedyPipelineNode, SpeedyPipelineSourceNode, SpeedyPipelineSinkNode } from './pipeline-node'; import { SpeedyPipelinePort, SpeedyPipelineInputPort, SpeedyPipelineOutputPort } from './pipeline-port'; import { SpeedyGPU } from '../../gpu/speedy-gpu'; import { SpeedyMedia } from '../speedy-media'; import { SpeedyKeypoint } from '../speedy-keypoint'; /** * A dictionary indexed by the names of the sink nodes * @typedef {Object<string,any>} SpeedyPipelineOutput */ /** @type {SpeedyGPU} shared GPU programs & textures */ let gpu = null; /** @type {number} gpu reference count */ let referenceCount = 0; /** * A pipeline is a network of nodes in which data flows to a sink */ export class SpeedyPipeline { /** * Constructor */ constructor() { /** @type {SpeedyPipelineNode[]} the collection of all nodes that belong to this pipeline */ this._nodes = []; /** @type {SpeedyPipelineNode[]} a sequence of nodes: from the source(s) to the sink */ this._sequence = []; /** @type {boolean} are we running the pipeline at this moment? */ this._busy = false; } /** * Find a node by its name * @template T extends SpeedyPipelineNode * @param {string} name * @returns {T|null} */ node(name) { for(let i = 0, n = this._nodes.length; i < n; i++) { if(this._nodes[i].name === name) return this._nodes[i]; } return null; } /** * Initialize the pipeline * @param {...SpeedyPipelineNode} nodes * @returns {SpeedyPipeline} this pipeline */ init(...nodes) { // validate if(this._nodes.length > 0) throw new IllegalOperationError(`The pipeline has already been initialized`); else if(nodes.length == 0) throw new IllegalArgumentError(`Can't initialize the pipeline. Please specify its nodes`); // create a GPU instance and increase the reference count if(0 == referenceCount++) { Utils.assert(!gpu, 'Duplicate SpeedyGPU instance'); gpu = new SpeedyGPU(); } // add nodes to the network for(let i = 0; i < nodes.length; i++) { const node = nodes[i]; if(!this._nodes.includes(node)) this._nodes.push(node); } // generate the sequence of nodes this._sequence = SpeedyPipeline._tsort(this._nodes); SpeedyPipeline._validateSequence(this._sequence); // initialize nodes for(let i = 0; i < this._sequence.length; i++) this._sequence[i].init(gpu); // done! return this; } /** * Release the resources associated with this pipeline * @returns {null} */ release() { if(this._nodes.length == 0) throw new IllegalOperationError(`The pipeline has already been released or has never been initialized`); // release nodes for(let i = this._sequence.length - 1; i >= 0; i--) this._sequence[i].release(gpu); this._sequence.length = 0; this._nodes.length = 0; // decrease reference count and release GPU if necessary if(0 == --referenceCount) gpu = gpu.release(); // done! return null; } /** * Run the pipeline * @returns {SpeedyPromise<SpeedyPipelineOutput>} results are indexed by the names of the sink nodes */ run() { Utils.assert(this._sequence.length > 0, `The pipeline has not been initialized or has been released`); // is the pipeline busy? if(this._busy) { // if so, we need to wait 'til it finishes return new SpeedyPromise((resolve, reject) => { setTimeout(() => this.run().then(resolve, reject), 0); }); } else { // the pipeline is now busy and won't accept concurrent tasks // (we allocate textures using a single pool) this._busy = true; } // find the sinks const sinks = /** @type {SpeedyPipelineSinkNode[]} */ ( this._sequence.filter(node => node.isSink()) ); // create output template const template = SpeedyPipeline._createOutputTemplate(sinks); // run the pipeline return SpeedyPipeline._runSequence(this._sequence).then(() => // export results SpeedyPromise.all(sinks.map(sink => sink.export().turbocharge())).then(results => // aggregate results by the names of the sinks results.reduce((obj, val, idx) => ((obj[sinks[idx].name] = val), obj), template) ) ).finally(() => { // clear all ports for(let i = this._sequence.length - 1; i >= 0; i--) this._sequence[i].clearPorts(); // the pipeline is no longer busy this._busy = false; }).turbocharge(); } /** * @internal * * GPU instance * @returns {SpeedyGPU} */ get _gpu() { return gpu; } /** * Execute the tasks of a sequence of nodes * @param {SpeedyPipelineNode[]} sequence sequence of nodes * @param {number} [i] in [0,n) * @param {number} [n] number of nodes * @returns {SpeedyPromise<void>} */ static _runSequence(sequence, i = 0, n = sequence.length) { for(; i < n; i++) { const runTask = sequence[i].execute(gpu); // this call greatly improves performance when downloading pixel data using PBOs gpu.gl.flush(); if(typeof runTask !== 'undefined') return runTask.then(() => SpeedyPipeline._runSequence(sequence, i+1, n)); } return SpeedyPromise.resolve(); } /** * Topological sorting * @param {SpeedyPipelineNode[]} nodes * @returns {SpeedyPipelineNode[]} */ static _tsort(nodes) { /** @typedef {[SpeedyPipelineNode, boolean]} StackNode */ const outlinks = SpeedyPipeline._outlinks(nodes); const stack = nodes.map(node => /** @type {StackNode} */ ([ node, false ]) ); const trash = new Set(); const sorted = new Array(nodes.length); let j = sorted.length; while(stack.length > 0) { const [ node, done ] = stack.pop(); if(!done) { if(!trash.has(node)) { const outnodes = outlinks.get(node); trash.add(node); stack.push([ node, true ]); stack.push(...(outnodes.map(node => /** @type {StackNode} */ ([ node, false ]) ))); if(outnodes.some(node => trash.has(node) && !sorted.includes(node))) throw new IllegalOperationError(`Pipeline networks cannot have cycles!`); } } else sorted[--j] = node; } return sorted; } /** * Figure out the outgoing links of all nodes * @param {SpeedyPipelineNode[]} nodes * @returns {Map<SpeedyPipelineNode,SpeedyPipelineNode[]>} */ static _outlinks(nodes) { const outlinks = new Map(); for(let k = 0; k < nodes.length; k++) outlinks.set(nodes[k], []); for(let i = 0; i < nodes.length; i++) { const to = nodes[i]; const inputs = to.inputNodes(); for(let j = 0; j < inputs.length; j++) { const from = inputs[j]; const links = outlinks.get(from); if(!links) throw new IllegalOperationError(`Can't initialize the pipeline. Missing node: ${from.fullName}. Did you forget to add it to the initialization list?`); if(!links.includes(to)) links.push(to); } } return outlinks; } /** * Generate the output template by aggregating the names of the sinks * @param {SpeedyPipelineNode[]} [sinks] * @returns {SpeedyPipelineOutput} */ static _createOutputTemplate(sinks = []) { const template = Object.create(null); for(let i = sinks.length - 1; i >= 0; i--) template[sinks[i].name] = null; return template; } /** * Validate a sequence of nodes * @param {SpeedyPipelineNode[]} sequence */ static _validateSequence(sequence) { if(sequence.length == 0) throw new IllegalOperationError(`Pipeline doesn't have nodes`); else if(!sequence[0].isSource()) throw new IllegalOperationError(`Pipeline doesn't have a source`); else if(!sequence.find(node => node.isSink())) throw new IllegalOperationError(`Pipeline doesn't have a sink`); } }