UNPKG

speedy-vision

Version:

GPU-accelerated Computer Vision for JavaScript

639 lines (513 loc) 22.6 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. * * image-processing.js * Unit testing */ describe('Image processing', function() { let pipeline; let media, square; beforeEach(function() { jasmine.addMatchers(speedyMatchers); }); beforeEach(async function() { const image = await loadImage('speedy-wall.jpg'); media = await Speedy.load(image); const sqr = await loadImage('square.png'); square = await Speedy.load(sqr); pipeline = Speedy.Pipeline(); }); afterEach(async function() { square.release(); media.release(); }); it('is a SpeedyPipeline object', async function() { expect(typeof pipeline).toBe('object'); expect(pipeline.constructor.name).toBe('SpeedyPipeline'); }); it('does nothing if the pipeline is empty', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); source.media = media; source.output().connectTo(sink.input()); pipeline.init(source, sink); const { image } = await pipeline.run(); const error = imerr(media, image); display(media, 'Original image'); display(image, 'After going through the GPU'); display(imdiff(media, image), `Error: ${error}`); expect(media.width).toBe(image.width); expect(media.height).toBe(image.height); expect(error).toBeAnAcceptableImageError(); pipeline.release(); }); it('converts to greyscale', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const greyscale = Speedy.Filter.Greyscale(); source.media = media; source.output().connectTo(greyscale.input()); greyscale.output().connectTo(sink.input()); pipeline.init(source, greyscale, sink); const { image } = await pipeline.run(); display(media); display(image); // RGB channels are the same const rgb = pixels(image).filter((p, i) => i % 4 < 3); const rrr = Array(rgb.length).fill(0).map((_, i) => rgb[3 * ((i/3)|0)]); expect(rgb).toBeElementwiseEqual(rrr); // Not equal to the original media const pix = pixels(media).filter((p, i) => i % 4 < 3); expect(pix).not.toBeElementwiseNearlyTheSamePixels(rrr); // done pipeline.release(); }); it('blurs an image', async function() { const filters = [ Speedy.Filter.GaussianBlur(), Speedy.Filter.SimpleBlur() ]; const ksizes = [3, 5, 7]; display(media, 'Original image'); for(const blur of filters) { let lastError = 1e-5; print(); for(const ksize of ksizes) { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink('blurred'); source.media = media; blur.kernelSize = Speedy.Size(ksize, ksize); source.output().connectTo(blur.input()); blur.output().connectTo(sink.input()); pipeline.init(source, blur, sink); const { blurred } = await pipeline.run(); const error = imerr(blurred, media); display(blurred, `Used ${blur.constructor.name} with kernel size = ${blur.kernelSize}. Error: ${error}`); // no FFT... expect(error).toBeGreaterThan(lastError); expect(error).toBeLessThan(0.2); lastError = error; pipeline.release(); } } }); describe('Convolution', function() { it('convolves with identity kernels', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const convolution = Speedy.Filter.Convolution(); const I3 = Speedy.Matrix(3, 3, [ 0, 0, 0, 0, 1, 0, 0, 0, 0, ]); const I5 = Speedy.Matrix(5, 5, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); const I7 = Speedy.Matrix(7, 7, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); source.media = square; source.output().connectTo(convolution.input()); convolution.output().connectTo(sink.input()); pipeline.init(source, sink, convolution); // 3x3 convolution.kernel = I3; const convolved3x3 = (await pipeline.run()).image; display(square, 'Original image'); display(convolved3x3, 'Convolution 3x3'); display(imdiff(square, convolved3x3), 'Difference'); expect(pixels(convolved3x3)).toBeElementwiseNearlyTheSamePixels(pixels(square)); print(); // 5x5 convolution.kernel = I5; const convolved5x5 = (await pipeline.run()).image; display(square, 'Original image'); display(convolved5x5, 'Convolution 5x5'); display(imdiff(square, convolved5x5), 'Difference'); expect(pixels(convolved5x5)).toBeElementwiseNearlyTheSamePixels(pixels(square)); print(); // 7x7 convolution.kernel = I7; const convolved7x7 = (await pipeline.run()).image; display(square, 'Original image'); display(convolved7x7, 'Convolution 7x7'); display(imdiff(square, convolved7x7), 'Difference'); expect(pixels(convolved7x7)).toBeElementwiseNearlyTheSamePixels(pixels(square)); print(); // done! pipeline.release(); }); it('doesn\'t accept kernels with invalid sizes', function() { [0, 2, 4, 6, 8, 10, 12, 14, 16].forEach(ksize => { expect(() => { const convolution = Speedy.Filter.Convolution(); convolution.kernel = Speedy.Matrix.Zeros(ksize, ksize); }).toThrow(); }); }); it('brightens an image', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const convolution = Speedy.Filter.Convolution(); source.media = media; convolution.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0,1.5,0, 0, 0, 0, ]); source.output().connectTo(convolution.input()); convolution.output().connectTo(sink.input()); pipeline.init(source, sink, convolution); const brightened = (await pipeline.run()).image; const groundTruth = await Speedy.load(createCanvasFromPixels( media.width, media.height, pixels(media).map(p => p * 1.5) )); const error = imerr(groundTruth, brightened); display(groundTruth, 'Ground truth'); display(brightened, 'Image brightened by Speedy'); display(imdiff(brightened, groundTruth), `Error: ${error}`); expect(error).toBeAnAcceptableImageError(); pipeline.release(); groundTruth.release(); }); it('darkens an image', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const convolution = Speedy.Filter.Convolution(); source.media = media; convolution.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0,.5, 0, 0, 0, 0, ]); source.output().connectTo(convolution.input()); convolution.output().connectTo(sink.input()); pipeline.init(source, sink, convolution); const brightened = (await pipeline.run()).image; const groundTruth = await Speedy.load(createCanvasFromPixels( media.width, media.height, pixels(media).map(p => p * 0.5) )); const error = imerr(groundTruth, brightened); display(groundTruth, 'Ground truth'); display(brightened, 'Image darkened by Speedy'); display(imdiff(brightened, groundTruth), `Error: ${error}`); expect(error).toBeAnAcceptableImageError(); pipeline.release(); groundTruth.release(); }); it('accepts chains of convolutions', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const conv1 = Speedy.Filter.Convolution(); const conv2 = Speedy.Filter.Convolution(); const conv3 = Speedy.Filter.Convolution(); source.media = square; conv1.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0, 1, 0, 0, 0, 0, ]); conv2.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0, 1, 0, 0, 0, 0, ]); conv3.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0, 1, 0, 0, 0, 0, ]); source.output().connectTo(conv1.input()); conv1.output().connectTo(conv2.input()); conv2.output().connectTo(conv3.input()); conv3.output().connectTo(sink.input()); pipeline.init(source, sink, conv1, conv2, conv3); const convolved = (await pipeline.run()).image; const error = imerr(square, convolved); display(square, 'Original'); display(convolved, 'Convolved'); display(imdiff(convolved, square), `Error: ${error}`); expect(pixels(square)) .toBeElementwiseNearlyTheSamePixels(pixels(convolved)); pipeline.release(); }); it('accepts chains of convolutions of different sizes', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const conv1 = Speedy.Filter.Convolution(); const conv2 = Speedy.Filter.Convolution(); const conv3 = Speedy.Filter.Convolution(); source.media = square; conv1.kernel = Speedy.Matrix(3, 3, [ 0, 0, 0, 0, 1, 0, 0, 0, 0, ]); conv2.kernel = Speedy.Matrix(5, 5, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); conv3.kernel = Speedy.Matrix(7, 7, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); source.output().connectTo(conv1.input()); conv1.output().connectTo(conv2.input()); conv2.output().connectTo(conv3.input()); conv3.output().connectTo(sink.input()); pipeline.init(source, sink, conv1, conv2, conv3); const convolved = (await pipeline.run()).image; const error = imerr(square, convolved); display(square, 'Original'); display(convolved, 'Convolved'); display(imdiff(convolved, square), `Error: ${error}`); expect(pixels(square)) .toBeElementwiseNearlyTheSamePixels(pixels(convolved)); pipeline.release(); }); it('convolves with a Sobel filter', async function() { const sobelX = await Speedy.load(await loadImage('square-sobel-x.png')); const sobelY = await Speedy.load(await loadImage('square-sobel-y.png')); const source = Speedy.Image.Source(); const sink1 = Speedy.Image.Sink('mySobelX'); const sink2 = Speedy.Image.Sink('mySobelY'); const conv1 = Speedy.Filter.Convolution(); const conv2 = Speedy.Filter.Convolution(); source.media = square; conv1.kernel = await Speedy.Matrix.Zeros(3).setTo(Speedy.Matrix(3, 3, [ // Sobel X 1, 0,-1, 2, 0,-2, 1, 0,-1, ]).transpose()); // column-major format conv2.kernel = await Speedy.Matrix.Zeros(3).setTo(Speedy.Matrix(3, 3, [ // Sobel Y 1, 2, 1, 0, 0, 0, -1,-2,-1, ]).transpose()); // column-major format source.output().connectTo(conv1.input()); source.output().connectTo(conv2.input()); conv1.output().connectTo(sink1.input()); conv2.output().connectTo(sink2.input()); pipeline.init(source, conv1, conv2, sink1, sink2); const { mySobelX, mySobelY } = await pipeline.run(); const errorX = imerr(sobelX, mySobelX); const errorY = imerr(sobelY, mySobelY); display(sobelX, 'Ground truth'); display(mySobelX, 'Sobel filter computed by Speedy'); display(imdiff(sobelX, mySobelX), `Error: ${errorX}`); print(); display(sobelY, 'Ground truth'); display(mySobelY, 'Sobel filter computed by Speedy'); display(imdiff(sobelY, mySobelY), `Error: ${errorY}`); print(); display(square, 'Original image'); expect(errorX).toBeAnAcceptableImageError(2); expect(errorY).toBeAnAcceptableImageError(2); pipeline.release(); sobelY.release(); sobelX.release(); }); it('captures outlines', async function() { const outline = await Speedy.load(await loadImage('square-outline.png')); const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const conv = Speedy.Filter.Convolution(); source.media = square; conv.kernel = Speedy.Matrix(3, 3, [ -1,-1,-1, -1, 8,-1, -1,-1,-1, ]); source.output().connectTo(conv.input()); conv.output().connectTo(sink.input()); pipeline.init(source, sink, conv); const myOutline = (await pipeline.run()).image; const error = imerr(outline, myOutline); display(square, 'Original image'); display(outline, 'Ground truth'); display(myOutline, 'Outline computed by Speedy'); display(imdiff(outline, myOutline), `Error: ${error}`); expect(error).toBeAnAcceptableImageError(); outline.release(); pipeline.release(); }); }); it('bufferizes an image', async function() { const source = Speedy.Image.Source(); const buffer = Speedy.Image.Buffer(); const sink = Speedy.Image.Sink(); source.output().connectTo(buffer.input()); buffer.output().connectTo(sink.input()); pipeline.init(source, buffer, sink); const src = [ media, square, media, square, square ]; const n = src.length; const input = new Array(n); const output = new Array(n); let t; for(t = 0; t < n; t++) { input[t] = source.media = src[t]; output[t] = (await pipeline.run()).image; print(`Input / Output at time ${t}`); display(input[t], `Input at time ${t}`); display(output[t], `Output at time ${t}`); } expect(imerr(input[0], output[0])).toBeAnAcceptableImageError(); for(t = 1; t < n; t++) expect(imerr(input[t-1], output[t])).toBeAnAcceptableImageError(); }); it('recovers from WebGL context loss', async function() { const source = Speedy.Image.Source(); const sink = Speedy.Image.Sink(); const conv = Speedy.Filter.Convolution(); const blur = Speedy.Filter.SimpleBlur(); source.media = media; conv.kernel = Speedy.Matrix(3, 3, [ -1,-1,-1, -1, 3, 0, -1, 0, 2, ]); source.output().connectTo(blur.input()); blur.output().connectTo(conv.input()); conv.output().connectTo(sink.input()); print('Lose WebGL context, repeat the algorithm'); // step 1 pipeline.init(source, blur, conv, sink); const img1 = (await pipeline.run()).image; const pix1 = pixels(img1); display(img1, 'Before losing context'); // lose and restore context await pipeline._gpu.loseAndRestoreWebGLContext(); pipeline.release(); // step 2 pipeline.init(source, blur, conv, sink); const img2 = (await pipeline.run()).image; const pix2 = pixels(img2); display(img2, 'After losing context'); pipeline.release(); // verify expect(pix1).toBeElementwiseNearlyTheSamePixels(pix2); }); it('mixes two images', async function() { const pipeline = Speedy.Pipeline(); const source = Speedy.Image.Source(); const convolution = Speedy.Filter.Convolution(); const white = Speedy.Image.Mixer(); const black = Speedy.Image.Mixer(); const blend = Speedy.Image.Mixer(); const sink1 = Speedy.Image.Sink('image0'); const sink2 = Speedy.Image.Sink('image1'); const sink3 = Speedy.Image.Sink('whiteImage'); const sink4 = Speedy.Image.Sink('blackImage'); const sink5 = Speedy.Image.Sink('blendedImage'); source.media = media; convolution.kernel = Speedy.Matrix(3, 3, [ 0, 1, 0, 1,-5, 1, 0, 1, 0, ]); white.alpha = white.beta = 0; white.gamma = 1; black.alpha = black.beta = black.gamma = 0; blend.alpha = blend.beta = 0.5; blend.gamma = 0; source.output().connectTo(white.input('in0')); source.output().connectTo(black.input('in0')); source.output().connectTo(blend.input('in0')); source.output().connectTo(convolution.input()); convolution.output().connectTo(white.input('in1')); convolution.output().connectTo(black.input('in1')); convolution.output().connectTo(blend.input('in1')); source.output().connectTo(sink1.input()); convolution.output().connectTo(sink2.input()); white.output().connectTo(sink3.input()); black.output().connectTo(sink4.input()); blend.output().connectTo(sink5.input()); pipeline.init(source, convolution, white, black, blend, sink1, sink2, sink3, sink4, sink5); const { image0, image1, whiteImage, blackImage, blendedImage } = await pipeline.run(); print(`We'll blend two images:`); display(image0); display(image1); print(`Result:`); display(blendedImage, 'Blend'); display(blackImage, 'Black'); display(whiteImage, 'White'); const pix0 = pixels(image0); const pix1 = pixels(image1); expect(pix0.length).toEqual(pix1.length); const pixwhite = Array.from({ length: pix0.length }, () => 255); // rgba(255,255,255,255) const pixblack = Array.from({ length: pix0.length }, (_, i) => (i % 4 == 3) ? 255 : 0); // rgba(0,0,0,255) expect(pixels(whiteImage)).toBeElementwiseNearlyTheSamePixels(pixwhite); expect(pixels(blackImage)).toBeElementwiseNearlyTheSamePixels(pixblack); const pixblend = Array.from({ length: pix0.length }, (_, i) => Math.floor((pix0[i] + pix1[i]) / 2)); expect(pixels(blendedImage)).toBeElementwiseNearlyTheSamePixels(pixblend); }); it('travels through portals', async function() { const createPipeline1 = (media) => { const pipeline = Speedy.Pipeline(); const source = Speedy.Image.Source(); const convolution = Speedy.Filter.Convolution(); const sink = Speedy.Image.Sink(); const portal = Speedy.Image.Portal.Sink('portal'); source.media = media; convolution.kernel = Speedy.Matrix(3, 3, [ 0, 1, 1, 1,-5, 1, 0, 1, 0, ]); source.output().connectTo(convolution.input()); convolution.output().connectTo(sink.input()); convolution.output().connectTo(portal.input()); pipeline.init(source, convolution, sink, portal); return pipeline; }; const createPipeline2 = (source) => { const pipeline = Speedy.Pipeline(); const sink = Speedy.Image.Sink(); const portal = Speedy.Image.Portal.Source(); portal.source = source; portal.output().connectTo(sink.input()); pipeline.init(sink, portal); return pipeline; }; const pipeline1 = createPipeline1(media); const pipeline2 = createPipeline2(pipeline1.node('portal')); const image1 = (await pipeline1.run()).image; const image2 = (await pipeline2.run()).image; print(`This image will travel through a portal:`); display(image1); print(`This image have traveled through a portal:`); display(image2); expect(imerr(image1, image2)).toBeAnAcceptableImageError(); }); });