diff --git a/src/Modules.js b/src/Modules.js index 6dc0bdc528..932b2396aa 100644 --- a/src/Modules.js +++ b/src/Modules.js @@ -36,6 +36,7 @@ module.exports = { // 'invert': require('image-sequencer-invert'), this code imports the invert module from a different repository altogether (using a require statement) // Which is a powerful feature of ImageSequencer, the modules are independent of the rest of the library's source. 'invert': require('./modules/Invert'), + 'motion-blur': require('./modules/MotionBlur'), 'ndvi': require('./modules/Ndvi'), 'ndvi-colormap': require('./modules/NdviColormap'), 'noise-reduction': require('./modules/NoiseReduction'), diff --git a/src/modules/MotionBlur/Module.js b/src/modules/MotionBlur/Module.js new file mode 100644 index 0000000000..788ff40b2e --- /dev/null +++ b/src/modules/MotionBlur/Module.js @@ -0,0 +1,117 @@ +/* + * Rotates image + */ +module.exports = function Rotate(options, UI) { + + let output; + + function draw(input, callback, progressObj) { + + const defaults = require('./../../util/getDefaults.js')(require('./info.json')); + options.rotate = options.rotate || defaults.rotate; + + progressObj.stop(true); + progressObj.overrideFlag = true; + + const step = this; + + function changePixel(r, g, b, a) { + return [r, g, b, a]; + } + + function extraManipulation(pixels) { + const rotate_value = (options.rotate) % 360; + radians = (Math.PI) * rotate_value / 180, + width = pixels.shape[0], + height = pixels.shape[1], + cos = Math.cos(radians), + sin = Math.sin(radians); + // Final dimensions after rotation + + const finalPixels = require('ndarray')( + new Uint8Array( + 4 * + ( + Math.floor( + Math.abs(width * cos) + + Math.abs(height * sin) + + 5 + ) * + ( + Math.floor( + Math.abs(width * sin) + + Math.abs(height * cos) + ) + + 5 + ) + ) + ).fill(255), + [ + Math.floor(Math.abs(width * cos) + Math.abs(height * sin)) + 5, + Math.floor(Math.abs(width * sin) + Math.abs(height * cos)) + 4, + 4 + ] + ); + + pixels = require('./MotionBlur')(pixels, finalPixels, rotate_value, width, height, cos, sin); + + const new_rotate_value = 360 - ((options.rotate) % 360); + new_radians = (Math.PI) * new_rotate_value / 180, + new_width = Math.floor(Math.abs(width * cos) + Math.abs(height * sin) + 5), + new_height = Math.floor(Math.abs(width * sin) + Math.abs(height * cos)) + 5, + new_cos = Math.cos(new_radians), + new_sin = Math.sin(new_radians); + + const new_finalPixels = require('ndarray')( + new Uint8Array( + 4 * + ( + Math.floor( + Math.abs(new_width * new_cos) + + Math.abs(new_height * new_sin) + + 5 + ) * + ( + Math.floor( + Math.abs(new_width * new_sin) + + Math.abs(new_height * new_cos) + ) + + 5 + ) + ) + ).fill(255), + [ + Math.floor(Math.abs(new_width * new_cos) + Math.abs(new_height * new_sin)) + 5, + Math.floor(Math.abs(new_width * new_sin) + Math.abs(new_height * new_cos)) + 4, + 4 + ] + ); + + new_pixels = require('./MotionBlur')(finalPixels, new_finalPixels, new_rotate_value, new_width, new_height, new_cos, new_sin); + return new_pixels; + } + + function output(image, datauri, mimetype, wasmSuccess) { + step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; + } + + return require('../_nomodule/PixelManipulation.js')(input, { + output: output, + ui: options.step.ui, + changePixel: changePixel, + extraManipulation: extraManipulation, + format: input.format, + image: options.image, + inBrowser: options.inBrowser, + callback: callback, + useWasm:options.useWasm + }); + } + + return { + options: options, + draw: draw, + output: output, + UI: UI + }; +}; diff --git a/src/modules/MotionBlur/MotionBlur.js b/src/modules/MotionBlur/MotionBlur.js new file mode 100644 index 0000000000..03d558ba97 --- /dev/null +++ b/src/modules/MotionBlur/MotionBlur.js @@ -0,0 +1,151 @@ +// 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 = -10; y <= 10; y++) { + kernel.push([]); + for (let x = -10; x <= 10; x++) { + let r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + if (y == 0) { + kernel[y + 10].push(Math.exp(-(r / s))); + } + else { + kernel[y + 10].push(0); + } + sum += kernel[y + 10][x + 10]; + } + } + + for (let x = 0; x < 21; x++){ + for (let y = 0; y < 21; y++){ + kernel[y][x] = (kernel[y][x] / sum); + } + } + + return kernel; +} + +const imagejs = require('imagejs'), + ndarray = require('ndarray'); + +module.exports = exports = function(pixels, finalPixels, rotate_value, width, height, cos, sin) { + const pixelSetter = require('../../util/pixelSetter.js'); + var defaults = require('./../../util/getDefaults.js')(require('./info.json')); + options.blur = options.blur || defaults.blur; + + const height_half = Math.floor(height / 2), + width_half = Math.floor(width / 2); + dimension = width + height; + + if (rotate_value % 360 == 0) return pixels; + + function copyPixel(x1, y1, x2, y2, finalPix, initPix) { + finalPix.set(x1, y1, 0, initPix.get(x2, y2, 0)); + finalPix.set(x1, y1, 1, initPix.get(x2, y2, 1)); + finalPix.set(x1, y1, 2, initPix.get(x2, y2, 2)); + finalPix.set(x1, y1, 3, initPix.get(x2, y2, 3)); + } + + const intermediatePixels = new ndarray( + new Uint8Array(4 * dimension * dimension).fill(255), + [dimension, dimension, 4] + ); // Intermediate ndarray of pixels with a greater size to prevent clipping. + + // Copying all the pixels from image to intermediatePixels + for (let x = 0; x < pixels.shape[0]; x++){ + for (let y = 0; y < pixels.shape[1]; y++){ + copyPixel(x + height_half, y + width_half, x, y, intermediatePixels, pixels); + } + } + + // Rotating intermediatePixels + const bitmap = new imagejs.Bitmap({ width: intermediatePixels.shape[0], height: intermediatePixels.shape[1] }); + + for (let x = 0; x < intermediatePixels.shape[0]; x++) { + for (let y = 0; y < intermediatePixels.shape[1]; y++) { + let r = intermediatePixels.get(x, y, 0), + g = intermediatePixels.get(x, y, 1), + b = intermediatePixels.get(x, y, 2), + a = intermediatePixels.get(x, y, 3); + + bitmap.setPixel(x, y, r, g, b, a); + } + } + + const rotated = bitmap.rotate({ + degrees: rotate_value, + }); + + for (let x = 0; x < intermediatePixels.shape[0]; x++) { + for (let y = 0; y < intermediatePixels.shape[1]; y++) { + const {r, g, b, a} = rotated.getPixel(x, y); + pixelSetter(x, y, [r, g, b, a], intermediatePixels); + } + } + + // Cropping extra whitespace + for (let x = 0; x < finalPixels.shape[0]; x++){ + for (let y = 0; y < finalPixels.shape[1]; y++){ + copyPixel( + x, + y, + x + + Math.floor( + dimension / 2 - + Math.abs(width * cos / 2) - + Math.abs(height * sin / 2) + ) - 1, + y + + Math.floor( + dimension / 2 - + Math.abs(height * cos / 2) - + Math.abs(width * sin / 2) + ) - 1, + finalPixels, + intermediatePixels + ); + } + } + + + // blur pixels using GPU + let kernel = kernelGenerator(options.blur), // Generate the Gaussian kernel based on the sigma input. + pixs = { // Separates the rgb channel pixels to convolve on the GPU. + r: [], + g: [], + b: [], + }; + + for (let y = 0; y < finalPixels.shape[1]; y++){ + pixs.r.push([]); + pixs.g.push([]); + pixs.b.push([]); + + for (let x = 0; x < finalPixels.shape[0]; x++){ + pixs.r[y].push(finalPixels.get(x, y, 0)); + pixs.g[y].push(finalPixels.get(x, y, 1)); + pixs.b[y].push(finalPixels.get(x, y, 2)); + } + } + + const convolve = require('../_nomodule/gpuUtils').convolve; // GPU convolution function. + + const conPix = convolve([pixs.r, pixs.g, pixs.b], kernel); // Convolves the pixels (all channels separately) on the GPU. + + for (let y = 0; y < finalPixels.shape[1]; y++){ + for (let x = 0; x < finalPixels.shape[0]; x++){ + var pixelvalue = [Math.max(0, Math.min(conPix[0][y][x], 255)), + Math.max(0, Math.min(conPix[1][y][x], 255)), + Math.max(0, Math.min(conPix[2][y][x], 255))]; + pixelSetter(x, y, pixelvalue, finalPixels); // Sets the image pixels according to the blurred values. + } + } + + return finalPixels; +}; \ No newline at end of file diff --git a/src/modules/MotionBlur/index.js b/src/modules/MotionBlur/index.js new file mode 100644 index 0000000000..71549002ce --- /dev/null +++ b/src/modules/MotionBlur/index.js @@ -0,0 +1,4 @@ +module.exports = [ + require('./Module'), + require('./info.json') +]; \ No newline at end of file diff --git a/src/modules/MotionBlur/info.json b/src/modules/MotionBlur/info.json new file mode 100644 index 0000000000..35d2b76e22 --- /dev/null +++ b/src/modules/MotionBlur/info.json @@ -0,0 +1,23 @@ +{ + "name": "motion-blur", + "description": "Applies a Motion blur given by the intensity and angle value", + "inputs": { + "blur": { + "type": "float", + "desc": "Amount of motion blur", + "default": 3.5, + "min": 0, + "max": 5, + "step": 0.05 + }, + "rotate": { + "type": "integer", + "desc": "Angular value for rotation in degrees", + "default": "90", + "min": "0", + "max": "360", + "step": "1" + } + }, + "docs-link":"https://github.com/publiclab/image-sequencer/blob/main/docs/MODULES.md#blur-module" +}