diff --git a/docs/api-constructor.md b/docs/api-constructor.md index c8bf3d43..9535c218 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -38,6 +38,10 @@ Implements the [stream.Duplex][1] class. - `options.create.width` **[number][8]?** integral number of pixels wide. - `options.create.height` **[number][8]?** integral number of pixels high. - `options.create.channels` **[number][8]?** integral number of channels, either 3 (RGB) or 4 (RGBA). + - `options.create.noise` **[Object][6]?** describes a noise to be created. + - `options.create.noise.type` **[string][5]?** type of generated noise. (supported: `gaussian`) + - `options.create.noise.mean` **[number][8]?** mean of pixels in generated image. + - `options.create.noise.sigma` **[number][8]?** standard deviation of pixels in generated image. - `options.create.background` **([string][5] \| [Object][6])?** parsed by the [color][9] module to extract values for red, green, blue and alpha. ### Examples diff --git a/lib/input.js b/lib/input.js index c9c284e8..a81036d6 100644 --- a/lib/input.js +++ b/lib/input.js @@ -137,22 +137,50 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { is.object(inputOptions.create) && is.integer(inputOptions.create.width) && inputOptions.create.width > 0 && is.integer(inputOptions.create.height) && inputOptions.create.height > 0 && - is.integer(inputOptions.create.channels) && is.inRange(inputOptions.create.channels, 3, 4) && - is.defined(inputOptions.create.background) + is.integer(inputOptions.create.channels) ) { inputDescriptor.createWidth = inputOptions.create.width; inputDescriptor.createHeight = inputOptions.create.height; inputDescriptor.createChannels = inputOptions.create.channels; - const background = color(inputOptions.create.background); - inputDescriptor.createBackground = [ - background.red(), - background.green(), - background.blue(), - Math.round(background.alpha() * 255) - ]; + // Noise + if (is.defined(inputOptions.create.noise)) { + if (!is.object(inputOptions.create.noise)) { + throw new Error('Expected noise to be an object'); + } + if (!is.inArray(inputOptions.create.noise.type, ['gaussian'])) { + throw new Error('Only gaussian noise is supported at the moment'); + } + if (!is.inRange(inputOptions.create.channels, 1, 4)) { + throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels); + } + inputDescriptor.createNoiseType = inputOptions.create.noise.type; + if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) { + inputDescriptor.createNoiseMean = inputOptions.create.noise.mean; + } else { + throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean); + } + if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) { + inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma; + } else { + throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma); + } + } else if (is.defined(inputOptions.create.background)) { + if (!is.inRange(inputOptions.create.channels, 3, 4)) { + throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels); + } + const background = color(inputOptions.create.background); + inputDescriptor.createBackground = [ + background.red(), + background.green(), + background.blue(), + Math.round(background.alpha() * 255) + ]; + } else { + throw new Error('Expected valid noise or background to create a new input image'); + } delete inputDescriptor.buffer; } else { - throw new Error('Expected valid width, height, channels and background to create a new input image'); + throw new Error('Expected valid width, height and channels to create a new input image'); } } } else if (is.defined(inputOptions)) { diff --git a/package.json b/package.json index adae7176..05987556 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "Guillermo Alfonso Varela ChouciƱo ", "Christian Flintrup ", "Manan Jadhav ", - "Leon Radley " + "Leon Radley ", + "alza54 " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/common.cc b/src/common.cc index 9091a257..0e879745 100644 --- a/src/common.cc +++ b/src/common.cc @@ -109,7 +109,13 @@ namespace sharp { descriptor->createChannels = AttrAsUint32(input, "createChannels"); descriptor->createWidth = AttrAsUint32(input, "createWidth"); descriptor->createHeight = AttrAsUint32(input, "createHeight"); - descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground"); + if (HasAttr(input, "createNoiseType")) { + descriptor->createNoiseType = AttrAsStr(input, "createNoiseType"); + descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean"); + descriptor->createNoiseSigma = AttrAsDouble(input, "createNoiseSigma"); + } else { + descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground"); + } } // Limit input images to a given number of pixels, where pixels = width * height descriptor->limitInputPixels = AttrAsUint32(input, "limitInputPixels"); @@ -318,15 +324,35 @@ namespace sharp { } else { if (descriptor->createChannels > 0) { // Create new image - std::vector background = { - descriptor->createBackground[0], - descriptor->createBackground[1], - descriptor->createBackground[2] - }; - if (descriptor->createChannels == 4) { - background.push_back(descriptor->createBackground[3]); + if (descriptor->createNoiseType == "gaussian") { + int const channels = descriptor->createChannels; + image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight); + std::vector bands = {}; + bands.reserve(channels); + for (int _band = 0; _band < channels; _band++) { + bands.push_back(image.gaussnoise( + descriptor->createWidth, + descriptor->createHeight, + VImage::option()->set("mean", descriptor->createNoiseMean)->set("sigma", descriptor->createNoiseSigma))); + } + image = image.bandjoin(bands); + image = image.cast(VipsBandFormat::VIPS_FORMAT_UCHAR); + if (channels < 3) { + image = image.colourspace(VIPS_INTERPRETATION_B_W); + } else { + image = image.colourspace(VIPS_INTERPRETATION_sRGB); + } + } else { + std::vector background = { + descriptor->createBackground[0], + descriptor->createBackground[1], + descriptor->createBackground[2] + }; + if (descriptor->createChannels == 4) { + background.push_back(descriptor->createBackground[3]); + } + image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight).new_from_image(background); } - image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight).new_from_image(background); image.get_image()->Type = VIPS_INTERPRETATION_sRGB; imageType = ImageType::RAW; } else { diff --git a/src/common.h b/src/common.h index c0dbca71..b8a0d929 100644 --- a/src/common.h +++ b/src/common.h @@ -64,6 +64,9 @@ namespace sharp { int createWidth; int createHeight; std::vector createBackground; + std::string createNoiseType; + double createNoiseMean; + double createNoiseSigma; InputDescriptor(): buffer(nullptr), @@ -82,7 +85,9 @@ namespace sharp { createChannels(0), createWidth(0), createHeight(0), - createBackground{ 0.0, 0.0, 0.0, 255.0 } {} + createBackground{ 0.0, 0.0, 0.0, 255.0 }, + createNoiseMean(0.0), + createNoiseSigma(0.0) {} }; // Convenience methods to access the attributes of a Napi::Object diff --git a/test/unit/noise.js b/test/unit/noise.js new file mode 100644 index 00000000..555a39eb --- /dev/null +++ b/test/unit/noise.js @@ -0,0 +1,258 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Gaussian noise', function () { + it('generate single-channel gaussian noise', function (done) { + const output = fixtures.path('output.noise-1-channel.png'); + const noise = sharp({ + create: { + width: 1024, + height: 768, + channels: 1, // b-w + noise: { + type: 'gaussian', + mean: 128, + sigma: 30 + } + } + }); + noise.toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(1024, info.width); + assert.strictEqual(768, info.height); + assert.strictEqual(1, info.channels); + sharp(output).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual('b-w', metadata.space); + assert.strictEqual('uchar', metadata.depth); + done(); + }); + }); + }); + + it('generate 3-channels gaussian noise', function (done) { + const output = fixtures.path('output.noise-3-channels.png'); + const noise = sharp({ + create: { + width: 1024, + height: 768, + channels: 3, // sRGB + noise: { + type: 'gaussian', + mean: 128, + sigma: 30 + } + } + }); + noise.toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(1024, info.width); + assert.strictEqual(768, info.height); + assert.strictEqual(3, info.channels); + sharp(output).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual('srgb', metadata.space); + assert.strictEqual('uchar', metadata.depth); + done(); + }); + }); + }); + + it('overlay 3-channels gaussian noise over image', function (done) { + const output = fixtures.path('output.noise-image.jpg'); + const noise = sharp({ + create: { + width: 320, + height: 240, + channels: 3, + noise: { + type: 'gaussian', + mean: 0, + sigma: 5 + } + } + }); + noise.toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(3, info.channels); + sharp(fixtures.inputJpg) + .resize(320, 240) + .composite([ + { + input: data, + blend: 'exclusion', + raw: { + width: info.width, + height: info.height, + channels: info.channels + } + } + ]) + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + assert.strictEqual(3, info.channels); + // perceptual hashing detects that images are the same (difference is <=1%) + fixtures.assertSimilar(output, fixtures.inputJpg, function (err) { + if (err) throw err; + done(); + }); + }); + }); + }); + + it('overlay strong single-channel (sRGB) gaussian noise with 25% transparency over transparent png image', function (done) { + const output = fixtures.path('output.noise-image-transparent.png'); + const width = 320; + const height = 240; + const rawData = { + width, + height, + channels: 1 + }; + const noise = sharp({ + create: { + width, + height, + channels: 1, + noise: { + type: 'gaussian', + mean: 200, + sigma: 30 + } + } + }); + noise + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(1, info.channels); + sharp(data, { raw: rawData }) + .joinChannel(data, { raw: rawData }) // r channel + .joinChannel(data, { raw: rawData }) // b channel + .joinChannel(Buffer.alloc(width * height, 64), { raw: rawData }) // alpha channel + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(4, info.channels); + sharp(fixtures.inputPngRGBWithAlpha) + .resize(width, height) + .composite([ + { + input: data, + blend: 'exclusion', + raw: { + width: info.width, + height: info.height, + channels: info.channels + } + } + ]) + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(width, info.width); + assert.strictEqual(height, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(output, fixtures.inputPngRGBWithAlpha, { threshold: 10 }, function (err) { + if (err) throw err; + done(); + }); + }); + }); + }); + }); + + it('no create object properties specified', function () { + assert.throws(function () { + sharp({ + create: {} + }); + }); + }); + + it('invalid noise object', function () { + assert.throws(function () { + sharp({ + create: { + width: 100, + height: 100, + channels: 3, + noise: 'gaussian' + } + }); + }); + }); + + it('unknown type of noise', function () { + assert.throws(function () { + sharp({ + create: { + width: 100, + height: 100, + channels: 3, + noise: { + type: 'unknown' + } + } + }); + }); + }); + + it('gaussian noise, invalid amount of channels', function () { + assert.throws(function () { + sharp({ + create: { + width: 100, + height: 100, + channels: 5, + noise: { + type: 'gaussian', + mean: 5, + sigma: 10 + } + } + }); + }); + }); + + it('gaussian noise, invalid mean', function () { + assert.throws(function () { + sharp({ + create: { + width: 100, + height: 100, + channels: 1, + noise: { + type: 'gaussian', + mean: -1.5, + sigma: 10 + } + } + }); + }); + }); + + it('gaussian noise, invalid sigma', function () { + assert.throws(function () { + sharp({ + create: { + width: 100, + height: 100, + channels: 1, + noise: { + type: 'gaussian', + mean: 0, + sigma: -1.5 + } + } + }); + }); + }); +});