GPU Acceleration achieved! (#1038)

* add gpu.js 2.0.0-rc.7

* add gpuUtils

* add gpuUtil convolve

* add convolution to edgeDetect

* bench it

* bench change

* newline

* pipeline

* revert edge-detect

* gpu accelerate gaussian blur

* edgeDetect use blur

* tweak values

* remove ndarray-gaussian-filter

* audit

* turn on previews

* remove oldPix

* fix travis

* Travis fix

* Try fixing travis

* Fix

* Retry

* tweaks

* convolution module on GPU

* use babelify

* trial

* remove logs

* Update .travis.yml

* Update .travis.yml

* bump version

* bump to rc.9

* rc.10

* rc.11

* Update package.json

* convolution fix

* unit test gpuUtils

* tests changed, fixed

* new fix

* more obvious parseFloat

* remove old commented code
This commit is contained in:
Harsh Khandeparkar
2019-05-02 22:23:58 +05:30
committed by Jeffrey Warren
parent 43a8d0f2c1
commit b0096a13f4
14 changed files with 1258 additions and 997 deletions

View File

@@ -1,3 +1,4 @@
sudo: required
language: node_js
node_js:
- '6'
@@ -26,6 +27,10 @@ addons:
packages:
- g++-4.8
- xvfb # for tape-run
before_install:
- sudo apt-get update
- sudo apt-get install xserver-xorg-dev libxext-dev libxi-dev
- sudo apt-get install -y build-essential libxi-dev libglu1-mesa-dev libglew-dev pkg-config libglu1-mesa-dev freeglut3-dev mesa-common-dev
install:
- export DISPLAY=':99.0' # for tape-run
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & # for tape-run

1727
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "src/ImageSequencer.js",
"scripts": {
"debug": "TEST=true node ./index.js -i ./examples/images/monarch.png -s invert",
"test": "TEST=true istanbul cover tape test/core/*.js test/core/ui/user-interface.js test/core/modules/canvas-resize.js test/core/modules/QR.js test/core/modules/crop.js | tap-spec; browserify test/core/modules/image-sequencer.js test/core/modules/chain.js test/core/modules/meta-modules.js test/core/modules/replace.js test/core/modules/import-export.js test/core/modules/run.js test/core/modules/dynamic-imports.js test/core/util/parse-input.js test/core/modules/benchmark.js| tape-run --render=\"tap-spec\"",
"test": "TEST=true istanbul cover tape test/core/*.js test/core/ui/user-interface.js test/core/modules/canvas-resize.js test/core/modules/QR.js test/core/modules/crop.js | tap-spec; browserify test/core/modules/image-sequencer.js test/core/modules/chain.js test/core/modules/meta-modules.js test/core/modules/replace.js test/core/modules/import-export.js test/core/modules/run.js test/core/modules/dynamic-imports.js test/core/util/*.js test/core/modules/benchmark.js| tape-run --render=\"tap-spec\"",
"test-ui": "jasmine test/spec/*.js",
"setup": "npm i && npm i -g grunt grunt-cli && grunt build",
"start": "grunt serve"
@@ -24,6 +24,7 @@
"url": "https://github.com/publiclab/image-sequencer/issues"
},
"dependencies": {
"base64-img": "^1.0.4",
"bootstrap": "~3.4.0",
"buffer": "~5.2.1",
"commander": "^2.11.0",
@@ -35,6 +36,7 @@
"get-pixels": "~3.3.0",
"gifshot": "^0.4.5",
"glfx": "0.0.4",
"gpu.js": "^2.0.0-rc.12",
"image-sequencer-invert": "^1.0.0",
"imagejs": "0.0.9",
"imgareaselect": "git://github.com/jywarren/imgareaselect.git#v1.0.0-rc.2",
@@ -44,7 +46,6 @@
"jsqr": "^1.1.1",
"lodash": "^4.17.11",
"ndarray": "^1.0.18",
"ndarray-gaussian-filter": "^1.0.0",
"ora": "^3.0.0",
"pace": "0.0.4",
"puppeteer": "^1.14.0",
@@ -56,7 +57,10 @@
"webgl-distort": "0.0.2"
},
"devDependencies": {
"base64-img": "^1.0.4",
"@babel/core": "^7.4.3",
"@babel/plugin-proposal-object-rest-spread": "^7.4.3",
"@babel/plugin-syntax-object-rest-spread": "^7.2.0",
"babelify": "^10.0.0",
"browserify": "16.2.3",
"grunt": "^1.0.3",
"grunt-browser-sync": "^2.2.0",

View File

@@ -1,84 +1,62 @@
module.exports = exports = function(pixels, blur) {
let kernel = kernelGenerator(blur, 1), oldpix = require('lodash').cloneDeep(pixels);
kernel = flipKernel(kernel);
for (let i = 0; i < pixels.shape[0]; i++) {
for (let j = 0; j < pixels.shape[1]; j++) {
let neighboutPos = getNeighbouringPixelPositions([i, j]);
let acc = [0.0, 0.0, 0.0, 0.0];
for (let a = 0; a < kernel.length; a++) {
for (let b = 0; b < kernel.length; b++) {
acc[0] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 0) * kernel[a][b]);
acc[1] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 1) * kernel[a][b]);
acc[2] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 2) * kernel[a][b]);
acc[3] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 3) * kernel[a][b]);
}
}
pixels.set(i, j, 0, acc[0]);
pixels.set(i, j, 1, acc[1]);
pixels.set(i, j, 2, acc[2]);
}
}
return pixels;
//Generates a 3x3 Gaussian kernel
function kernelGenerator(sigma, size) {
/*
Trying out a variable radius kernel not working as of now
*/
// const coeff = (1.0/(2.0*Math.PI*sigma*sigma))
// const expCoeff = -1 * (1.0/2.0 * sigma * sigma)
// let e = Math.E
// let result = []
// for(let i = -1 * size;i<=size;i++){
// let arr = []
// for(let j= -1 * size;j<=size;j++){
// arr.push(coeff * Math.pow(e,expCoeff * ((i * i) + (j*j))))
// }
// result.push(arr)
// }
// let sum = result.reduce((sum,val)=>{
// return val.reduce((sumInner,valInner)=>{
// return sumInner+valInner
// })
// })
// result = result.map(arr=>arr.map(val=>(val + 0.0)/(sum + 0.0)))
// return result
return [
[2.0 / 159.0, 4.0 / 159.0, 5.0 / 159.0, 4.0 / 159.0, 2.0 / 159.0],
[4.0 / 159.0, 9.0 / 159.0, 12.0 / 159.0, 9.0 / 159.0, 4.0 / 159.0],
[5.0 / 159.0, 12.0 / 159.0, 15.0 / 159.0, 12.0 / 159.0, 5.0 / 159.0],
[4.0 / 159.0, 9.0 / 159.0, 12.0 / 159.0, 9.0 / 159.0, 4.0 / 159.0],
[2.0 / 159.0, 4.0 / 159.0, 5.0 / 159.0, 4.0 / 159.0, 2.0 / 159.0]
];
}
function getNeighbouringPixelPositions(pixelPosition) {
let x = pixelPosition[0], y = pixelPosition[1], result = [];
for (let i = -2; i <= 2; i++) {
let arr = [];
for (let j = -2; j <= 2; j++)
arr.push([x + i, y + j]);
result.push(arr);
}
return result;
let kernel = kernelGenerator(blur),
pixs = {
r: [],
g: [],
b: [],
}
function flipKernel(kernel) {
let result = [];
for (let i = kernel.length - 1; i >= 0; i--) {
let arr = [];
for (let j = kernel[i].length - 1; j >= 0; j--) {
arr.push(kernel[i][j]);
}
result.push(arr);
}
return result;
for (let y = 0; y < pixels.shape[1]; y++){
pixs.r.push([])
pixs.g.push([])
pixs.b.push([])
for (let x = 0; x < pixels.shape[0]; x++){
pixs.r[y].push(pixels.get(x, y, 0))
pixs.g[y].push(pixels.get(x, y, 1))
pixs.b[y].push(pixels.get(x, y, 2))
}
}
}
const convolve = require('../_nomodule/gpuUtils').convolve
const conPix = convolve([pixs.r, pixs.g, pixs.b], kernel)
for (let y = 0; y < pixels.shape[1]; y++){
for (let x = 0; x < pixels.shape[0]; x++){
pixels.set(x, y, 0, Math.max(0, Math.min(conPix[0][y][x], 255)))
pixels.set(x, y, 1, Math.max(0, Math.min(conPix[1][y][x], 255)))
pixels.set(x, y, 2, Math.max(0, Math.min(conPix[2][y][x], 255)))
}
}
return pixels;
//Generates a 5x5 Gaussian kernel
function kernelGenerator(sigma = 1) {
let kernel = [],
sum = 0;
if (sigma == 0) sigma += 0.05
const s = 2 * Math.pow(sigma, 2);
for (let y = -2; y <= 2; y++) {
kernel.push([])
for (let x = -2; x <= 2; x++) {
let r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
kernel[y + 2].push(Math.exp(-(r / s)))
sum += kernel[y + 2][x + 2]
}
}
for (let x = 0; x < 5; x++){
for (let y = 0; y < 5; y++){
kernel[y][x] = (kernel[y][x] / sum)
}
}
return kernel;
}
}

View File

@@ -5,6 +5,7 @@ module.exports = function Blur(options, UI) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
options.blur = options.blur || defaults.blur;
options.blur = parseFloat(options.blur);
var output;
function draw(input, callback, progressObj) {

View File

@@ -8,7 +8,7 @@
"default": 2,
"min": 0,
"max": 5,
"step": 0.25
"step": 0.05
}
},
"docs-link":"https://github.com/publiclab/image-sequencer/blob/main/docs/MODULES.md#blur-module"

View File

@@ -1,71 +1,50 @@
var _ = require('lodash');
module.exports = exports = function(pixels, constantFactor, kernelValues) {
let kernel = kernelGenerator(constantFactor, kernelValues), oldpix = _.cloneDeep(pixels);
kernel = flipKernel(kernel);
for (let i = 0; i < pixels.shape[0]; i++) {
for (let j = 0; j < pixels.shape[1]; j++) {
let neighboutPos = getNeighbouringPixelPositions([i, j]);
let acc = [0.0, 0.0, 0.0, 0.0];
for (let a = 0; a < kernel.length; a++) {
for (let b = 0; b < kernel.length; b++) {
acc[0] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 0) * kernel[a][b]);
acc[1] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 1) * kernel[a][b]);
acc[2] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 2) * kernel[a][b]);
acc[3] += (oldpix.get(neighboutPos[a][b][0], neighboutPos[a][b][1], 3) * kernel[a][b]);
}
}
acc[0] = Math.min(acc[0], 255);
acc[1] = Math.min(acc[1], 255);
acc[2] = Math.min(acc[2], 255);
pixels.set(i, j, 0, acc[0]);
pixels.set(i, j, 1, acc[1]);
pixels.set(i, j, 2, acc[2]);
}
}
return pixels;
function kernelGenerator(constantFactor, kernelValues) {
kernelValues = kernelValues.split(" ");
for (i = 0; i < 9; i++) {
kernelValues[i] = Number(kernelValues[i]) * constantFactor;
}
let k = 0;
let arr = [];
for (i = 0; i < 3; i++) {
let columns = [];
for (j = 0; j < 3; j++) {
columns.push(kernelValues[k]);
k += 1;
}
arr.push(columns);
}
return arr;
module.exports = exports = function(pixels, constantFactor, kernelValues, texMode) {
let kernel = kernelGenerator(constantFactor, kernelValues),
pixs = {
r: [],
g: [],
b: [],
}
function getNeighbouringPixelPositions(pixelPosition) {
let x = pixelPosition[0], y = pixelPosition[1], result = [];
for (let y = 0; y < pixels.shape[1]; y++){
pixs.r.push([])
pixs.g.push([])
pixs.b.push([])
for (let i = -1; i <= 1; i++) {
let arr = [];
for (let j = -1; j <= 1; j++)
arr.push([x + i, y + j]);
result.push(arr);
}
return result;
for (let x = 0; x < pixels.shape[0]; x++){
pixs.r[y].push(pixels.get(x, y, 0))
pixs.g[y].push(pixels.get(x, y, 1))
pixs.b[y].push(pixels.get(x, y, 2))
}
}
function flipKernel(kernel) {
let result = [];
for (let i = kernel.length - 1; i >= 0; i--) {
let arr = [];
for (let j = kernel[i].length - 1; j >= 0; j--) {
arr.push(kernel[i][j]);
}
result.push(arr);
}
return result;
const convolve = require('../_nomodule/gpuUtils').convolve;
const conPix = convolve([pixs.r, pixs.g, pixs.b], kernel, (pixels.shape[0] * pixels.shape[1]) < 400000 ? true : false)
for (let y = 0; y < pixels.shape[1]; y++){
for (let x = 0; x < pixels.shape[0]; x++){
pixels.set(x, y, 0, Math.max(0, Math.min(conPix[0][y][x], 255)))
pixels.set(x, y, 1, Math.max(0, Math.min(conPix[1][y][x], 255)))
pixels.set(x, y, 2, Math.max(0, Math.min(conPix[2][y][x], 255)))
}
}
return pixels;
}
function kernelGenerator(constantFactor, kernelValues) {
kernelValues = kernelValues.split(" ");
for (i = 0; i < 9; i++) {
kernelValues[i] = Number(kernelValues[i]) * constantFactor;
}
let k = 0;
let arr = [];
for (y = 0; y < 3; y++) {
arr.push([])
for (x = 0; x < 3; x++) {
arr[y].push(kernelValues[k]);
k += 1;
}
}
return arr;
}

View File

@@ -4,6 +4,7 @@ module.exports = function Convolution(options, UI) {
options.kernelValues = options.kernelValues || defaults.kernelValues;
options.constantFactor = options.constantFactor || defaults.constantFactor;
options.texMode = options.texMode || defaults.texMode;
var output;
function draw(input, callback, progressObj) {
@@ -14,7 +15,7 @@ module.exports = function Convolution(options, UI) {
var step = this;
function extraManipulation(pixels) {
pixels = require('./Convolution')(pixels, options.constantFactor, options.kernelValues);
pixels = require('./Convolution')(pixels, options.constantFactor, options.kernelValues, options.texMode);
return pixels;
}

View File

@@ -5,10 +5,11 @@
"constantFactor":{
"type": "float",
"desc": "a constant factor, multiplies all the kernel values by that factor",
"default": 0.1111,
"placeholder": 0.1111
"default": 0.111,
"min": 0.001,
"max": 2,
"step": 0.001
},
"kernelValues": {
"type": "string",
"desc": "nine space separated numbers representing the kernel values in left to right and top to bottom format.",

View File

@@ -12,8 +12,9 @@ kernely = [
let pixelsToBeSupressed = [];
module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, hysteresis) {
module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, useHysteresis) {
let angles = [], grads = [], strongEdgePixels = [], weakEdgePixels = [];
for (var x = 0; x < pixels.shape[0]; x++) {
grads.push([]);
angles.push([]);
@@ -31,7 +32,7 @@ module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, hystere
}
nonMaxSupress(pixels, grads, angles);
doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels);
if(hysteresis.toLowerCase() == 'true') hysteresis(strongEdgePixels, weakEdgePixels);
if(useHysteresis.toLowerCase() == 'true') hysteresis(strongEdgePixels, weakEdgePixels);
strongEdgePixels.forEach(pixel => preserve(pixels, pixel));
weakEdgePixels.forEach(pixel => supress(pixels, pixel));
@@ -83,7 +84,7 @@ function sobelFilter(pixels, x, y) {
return {
pixel: [val, val, val, grad],
angle: angle
};
}
}
function categorizeAngle(angle){

View File

@@ -19,30 +19,40 @@ module.exports = function edgeDetect(options, UI) {
var step = this;
// Blur the image
const internalSequencer = ImageSequencer({ inBrowser: false, ui: false });
return internalSequencer.loadImage(input.src, function () {
internalSequencer.importJSON([{ 'name': 'blur', 'options': {blur: options.blur} }]);
return internalSequencer.run(function onCallback(internalOutput) {
require('get-pixels')(internalOutput, function(err, blurPixels){
if (err){
return;
}
// Extra Manipulation function used as an enveloper for applying gaussian blur and Convolution
function changePixel(r, g, b, a) {
return [(r + g + b) / 3, (r + g + b) / 3, (r + g + b) / 3, a];
}
// Extra Manipulation function used as an enveloper for applying gaussian blur and Convolution
function changePixel(r, g, b, a) {
return [(r + g + b) / 3, (r + g + b) / 3, (r + g + b) / 3, a];
}
function extraManipulation(pixels) {
pixels = require('ndarray-gaussian-filter')(pixels, options.blur);
pixels = require('./EdgeUtils')(pixels, options.highThresholdRatio, options.lowThresholdRatio, options.hysteresis);
return pixels;
}
function extraManipulation(){
return require('./EdgeUtils')(blurPixels, options.highThresholdRatio, options.lowThresholdRatio, options.hysteresis);
}
function output(image, datauri, mimetype) {
step.output = { src: datauri, format: mimetype };
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
function output(image, datauri, mimetype) {
step.output = { src: datauri, format: mimetype };
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
})
});
});
}

View File

@@ -6,9 +6,9 @@
"type": "float",
"desc": "Amount of gaussian blur(Less blur gives more detail, typically 0-5)",
"default": 2,
"min": 0,
"min": 0.05,
"max": 5,
"step": 0.25
"step": 0.05
},
"highThresholdRatio":{
"type": "float",
@@ -16,7 +16,7 @@
"default": 0.2,
"min": 0,
"max": 1,
"step": 0.25
"step": 0.01
},
"lowThresholdRatio": {
"type": "float",
@@ -24,7 +24,7 @@
"default": 0.15,
"min": 0,
"max": 1,
"step": 0.05
"step": 0.01
},
"hysteresis": {
"type": "select",

View File

@@ -0,0 +1,94 @@
const GPU = require('gpu.js').GPU
/**
* @method convolve
* @param {Float32Array|Unit8Array|Float64Array} arrays Array of matrices all of the same size.
* @param {Float32Array|Unit8Array|Float64Array} kernel kernelto be convolved on all the matrices.
* @param {Boolean} pipeMode Whether to save the output to a texture.
* @param {Boolean} normalize Whether to normailize the output by dividing it by the total value of the kernel.
* @returns {Float32Array}
*/
const convolve = (arrays, kernel, options = {}) => {
const pipeMode = options.pipeMode || false,
mode = options.mode || 'gpu'
const gpu = new GPU(mode != 'gpu' ? {mode} : {})
const arrayX = arrays[0][0].length,
arrayY = arrays[0].length,
kernelX = kernel[0].length,
kernelY = kernel.length,
paddingX = Math.floor(kernelX / 2),
paddingY = Math.floor(kernelY / 2);
const matConvFunc = `function (array, kernel) {
let sum = 0;
for (let i = 0; i < ${kernelX}; i++){
for (let j = 0; j < ${kernelY}; j++){
sum += kernel[j][i] * array[this.thread.y + j][this.thread.x + i];
}
}
return sum;
}`;
const padIt = (array) => {
let out = []
for (var y = 0; y < array.length + paddingY * 2; y++){
out.push([])
for (var x = 0; x < array[0].length + paddingX * 2; x++){
const positionX = Math.min(Math.max(x - paddingX, 0), array[0].length - 1);
const positionY = Math.min(Math.max(y - paddingY, 0), array.length - 1);
out[y].push(array[positionY][positionX])
}
}
return out
}
const convolveKernel = gpu.createKernel(matConvFunc, {
output: [arrayX, arrayY],
pipeline: pipeMode
})
let outs = [];
for (var i = 0; i < arrays.length; i++){
const paddedArray = padIt(arrays[i])
const outArr = convolveKernel(paddedArray, kernel)
if (pipeMode) outs.push(outArr.toArray())
else outs.push(outArr)
}
return outs
}
/**
*
* @param {Float32Array|'Object'} outputSize Output size of the compute function.
* @param {Function} computeFunc The compute function. Cannot be an arrow function.
* @param {'Object'} constants Constants to be passed to the function. Can be accessed inside the compute function using `this.constants`.
* @param {Boolean} pipeMode Whether to save output array to a texture.
* @returns {Float32Array}
*/
const compute = (outputSize, computeFunc, constants, pipeMode) => {
computeFunc = computeFunc.toString()
const compute = gpu.createKernel(computeFunc, {
output: outputSize,
constants,
pipeline: pipeMode
})
compute.build()
if (pipeMode) return compute().toArray()
else return compute()
}
module.exports = {
convolve,
compute
}

View File

@@ -0,0 +1,96 @@
const test = require('tape');
const { convolve, compute } = require('../../../src/modules/_nomodule/gpuUtils')
test('convolve works with 1x1 array', t => {
const array = [[1]],
kernel = [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
],
expectedOut = [
[9]
]
const out = convolve([array], kernel);
t.equal(out.length, 1, 'convolve returns a single output array')
t.equal(out[0][0].length, 1, 'ouput array width is correct')
t.equal(out[0].length, 1, 'ouput array height is correct')
t.deepEqual(out[0], expectedOut, 'convolve outputs correct array')
t.end()
})
test('convolve works with 3x4 array', t => {
const array = [
[1, 2, 3],
[1, 2, 4],
[1, 3, 3],
[1, 2, 3]
],
kernel = [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
],
expectedOut = [
[12, 19, 26],
[13, 20, 27],
[13, 20, 27],
[13, 19, 25]
]
const out = convolve([array], kernel);
t.equal(out.length, 1, 'convolve returns a single output array')
t.equal(out[0][0].length, 3, 'ouput array width is correct')
t.equal(out[0].length, 4, 'ouput array height is correct')
t.deepEqual(out[0], expectedOut, 'convolve outputs correct array')
t.end()
})
test('convolve works with multiple 3x4 arrays', t => {
const array1 = [
[1, 2, 3],
[1, 2, 4],
[1, 3, 3],
[1, 2, 3]
],
array2 = [
[1, 2, 4],
[2, 2, 1],
[1, 0, 0],
[2, 3, 1]
],
kernel = [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
],
expectedOut1 = [
[12, 19, 26],
[13, 20, 27],
[13, 20, 27],
[13, 19, 25]
],
expectedOut2 = [
[14, 19, 24],
[12, 13, 14],
[15, 12, 9],
[16, 13, 10]
]
const out = convolve([array1, array2], kernel);
t.equal(out.length, 2, 'convolve returns 2 output array')
t.equal(out[0][0].length, 3, 'ouput array1 width is correct')
t.equal(out[0].length, 4, 'ouput array1 height is correct')
t.equal(out[1][0].length, 3, 'ouput array2 width is correct')
t.equal(out[1].length, 4, 'ouput array2 height is correct')
t.deepEqual(out[0], expectedOut1, 'convolve outputs correct array1')
t.deepEqual(out[1], expectedOut2, 'convolve outputs correct array2')
t.end()
})