UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

210 lines (183 loc) 7.98 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. * * lsh-knn.js * K approximate nearest neighbors matcher */ import { SpeedyPipelineNode } from '../../../pipeline-node'; import { SpeedyPipelineMessageType, SpeedyPipelineMessageWithKeypoints, SpeedyPipelineMessageWithLSHTables } from '../../../pipeline-message'; import { InputPort, OutputPort } from '../../../pipeline-portbuilder'; import { SpeedyPipelineNodeKeypointDetector } from '../detectors/detector'; import { SpeedyGPU } from '../../../../../gpu/speedy-gpu'; import { SpeedyLSH, LSH_ACCEPTABLE_DESCRIPTOR_SIZES, LSH_ACCEPTABLE_HASH_SIZES } from '../../../../../gpu/speedy-lsh'; import { SpeedyTexture, SpeedyDrawableTexture } from '../../../../../gpu/speedy-texture'; import { Utils } from '../../../../../utils/utils'; import { IllegalArgumentError } from '../../../../../utils/errors'; import { SpeedyPromise } from '../../../../speedy-promise'; /** @typedef {'fastest' | 'default' | 'demanding'} LSHKNNQualityLevel quality of the approximate matching */ /** @type {number} how many neighbors to search for, by default */ const DEFAULT_K = 1; /** @type {LSHKNNQualityLevel} default quality level */ const DEFAULT_QUALITY = 'default'; /** @type {{ [key in LSHKNNQualityLevel]: number }} maps quality level to bit swaps */ const NUMBER_OF_BIT_SWAPS = { 'fastest': 0, 'default': 1, 'demanding': 2, }; /** @type {object} program names indexed as LSH_KNN[descriptorSize][hashSize][level] */ const LSH_KNN = (fd => LSH_ACCEPTABLE_DESCRIPTOR_SIZES.reduce((o,d) => ((o[d] = fd(d)), o), {}))( d => ((fh => LSH_ACCEPTABLE_HASH_SIZES.reduce((o,h) => ((o[h] = fh(h)), o), {}))( h => ((fl => [0,1,2].reduce((o,l) => ((o[l] = fl(l)), o), {}))( l => `lshKnn${d}h${h}lv${l}` )) )) ); /** * K approximate nearest neighbors matcher */ export class SpeedyPipelineNodeLSHKNNMatcher extends SpeedyPipelineNode { /** * Constructor * @param {string} [name] name of the node */ constructor(name = undefined) { super(name, 6, [ InputPort('keypoints').expects(SpeedyPipelineMessageType.Keypoints).satisfying( ( /** @type {SpeedyPipelineMessageWithKeypoints} */ msg ) => msg.descriptorSize > 0 ), InputPort('lsh').expects(SpeedyPipelineMessageType.LSHTables), OutputPort().expects(SpeedyPipelineMessageType.KeypointMatches), ]); /** @type {number} how many neighbors do you want? */ this._k = DEFAULT_K; /** @type {LSHKNNQualityLevel} quality of the matching */ this._quality = DEFAULT_QUALITY; } /** * How many neighbors do you want? * @returns {number} */ get k() { return this._k; } /** * How many neighbors do you want? * @param {number} k number of neighbors */ set k(k) { this._k = Math.max(1, k | 0); } /** * Quality of the matching * @returns {LSHKNNQualityLevel} */ get quality() { return this._quality; } /** * Quality of the matching * @param {LSHKNNQualityLevel} quality */ set quality(quality) { if(!Object.prototype.hasOwnProperty.call(NUMBER_OF_BIT_SWAPS, quality)) throw new IllegalArgumentError(`Invalid quality level: "${quality}"`); this._quality = quality; } /** * Run the specific task of this node * @param {SpeedyGPU} gpu * @returns {void|SpeedyPromise<void>} */ _run(gpu) { const { encodedKeypoints, descriptorSize, extraSize, encoderLength } = /** @type {SpeedyPipelineMessageWithKeypoints} */ ( this.input('keypoints').read() ); /** @type {SpeedyLSH} */ const lsh = this.input('lsh').read().lsh; const keypoints = gpu.programs.keypoints; const tables = lsh.tables; const descriptorDB = lsh.descriptorDB; const tablesStride = tables.width; const descriptorDBStride = descriptorDB.width; const tableCount = lsh.tableCount; const hashSize = lsh.hashSize; const bucketCapacity = lsh.bucketCapacity; const bucketsPerTable = lsh.bucketsPerTable; const sequences = lsh.sequences; const candidatesA = this._tex[0]; const candidatesB = this._tex[1]; const candidatesC = this._tex[2]; const filters = this._tex[3]; const transferA = this._tex[4]; const transferB = this._tex[5]; const level = NUMBER_OF_BIT_SWAPS[this._quality]; const matchesPerKeypoint = this._k; // validate parameters if(descriptorSize !== lsh.descriptorSize) throw new IllegalArgumentError(`Can't match different types of descriptors in ${this.fullName}`); Utils.assert(LSH_KNN[descriptorSize] != undefined); Utils.assert(LSH_KNN[descriptorSize][hashSize] != undefined); Utils.assert(LSH_KNN[descriptorSize][hashSize][level] != undefined); // configure the output texture const capacity = SpeedyPipelineNodeKeypointDetector.encoderCapacity(descriptorSize, extraSize, encoderLength); const matcherLength = Math.max(1, Math.ceil(Math.sqrt(capacity * matchesPerKeypoint))); let encodedMatches = transferB; keypoints.lshKnnTransfer.outputs(matcherLength, matcherLength, transferA, transferB); // prepare the LSH matching const kthMatcherLength = Math.max(1, Math.ceil(Math.sqrt(capacity))); keypoints.lshKnnInitCandidates.outputs(kthMatcherLength, kthMatcherLength, candidatesA); keypoints.lshKnnInitFilters.outputs(kthMatcherLength, kthMatcherLength, filters); const lshKnn = keypoints[LSH_KNN[descriptorSize][hashSize][level]]; lshKnn.outputs(kthMatcherLength, kthMatcherLength, candidatesB, candidatesC); lshKnn.setUBO('LSHSequences', sequences); // match keypoints encodedMatches.clear(); keypoints.lshKnnInitFilters(); for(let i = 0; i < matchesPerKeypoint; i++) { // find the (i+1)-th best match let candidates = keypoints.lshKnnInitCandidates(); for(let tableIndex = 0; tableIndex < tableCount; tableIndex++) { candidates = lshKnn(candidates, filters, kthMatcherLength, tables, descriptorDB, tableIndex, bucketCapacity, bucketsPerTable, tablesStride, descriptorDBStride, encodedKeypoints, descriptorSize, extraSize, encoderLength); gpu.gl.flush(); } candidates.copyTo(filters); // transfer matches to an encoded matches texture encodedMatches = keypoints.lshKnnTransfer(encodedMatches, candidates, matchesPerKeypoint, i); } // done this.output().swrite(encodedMatches, matchesPerKeypoint); /* // debug let data = this._inspect32(filters), debug = []; for(let i = 0; i < data.length; i++) { const bits = MATCH_INDEX_BITS; const mask = (1 << bits) - 1; const u32 = data[i]; const index = u32 & mask, distance = u32 >>> bits; //debug.push('|'+[ u32 ].toString()); debug.push('|'+[ index, distance ].toString()); } console.log(debug.join(',')); */ } }