Expose libvips gaussnoise operation (#2527)

This commit is contained in:
alza54 2021-01-16 15:03:25 +01:00 committed by GitHub
parent 419cbe50f6
commit c9f85fe27f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 343 additions and 21 deletions

View File

@ -38,6 +38,10 @@ Implements the [stream.Duplex][1] class.
- `options.create.width` **[number][8]?** integral number of pixels wide. - `options.create.width` **[number][8]?** integral number of pixels wide.
- `options.create.height` **[number][8]?** integral number of pixels high. - `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.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. - `options.create.background` **([string][5] \| [Object][6])?** parsed by the [color][9] module to extract values for red, green, blue and alpha.
### Examples ### Examples

View File

@ -137,12 +137,37 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
is.object(inputOptions.create) && is.object(inputOptions.create) &&
is.integer(inputOptions.create.width) && inputOptions.create.width > 0 && is.integer(inputOptions.create.width) && inputOptions.create.width > 0 &&
is.integer(inputOptions.create.height) && inputOptions.create.height > 0 && is.integer(inputOptions.create.height) && inputOptions.create.height > 0 &&
is.integer(inputOptions.create.channels) && is.inRange(inputOptions.create.channels, 3, 4) && is.integer(inputOptions.create.channels)
is.defined(inputOptions.create.background)
) { ) {
inputDescriptor.createWidth = inputOptions.create.width; inputDescriptor.createWidth = inputOptions.create.width;
inputDescriptor.createHeight = inputOptions.create.height; inputDescriptor.createHeight = inputOptions.create.height;
inputDescriptor.createChannels = inputOptions.create.channels; inputDescriptor.createChannels = inputOptions.create.channels;
// 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); const background = color(inputOptions.create.background);
inputDescriptor.createBackground = [ inputDescriptor.createBackground = [
background.red(), background.red(),
@ -150,9 +175,12 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
background.blue(), background.blue(),
Math.round(background.alpha() * 255) Math.round(background.alpha() * 255)
]; ];
} else {
throw new Error('Expected valid noise or background to create a new input image');
}
delete inputDescriptor.buffer; delete inputDescriptor.buffer;
} else { } 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)) { } else if (is.defined(inputOptions)) {

View File

@ -73,7 +73,8 @@
"Guillermo Alfonso Varela Chouciño <guillevch@gmail.com>", "Guillermo Alfonso Varela Chouciño <guillevch@gmail.com>",
"Christian Flintrup <chr@gigahost.dk>", "Christian Flintrup <chr@gigahost.dk>",
"Manan Jadhav <manan@motionden.com>", "Manan Jadhav <manan@motionden.com>",
"Leon Radley <leon@radley.se>" "Leon Radley <leon@radley.se>",
"alza54 <alza54@thiocod.in>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",

View File

@ -109,8 +109,14 @@ namespace sharp {
descriptor->createChannels = AttrAsUint32(input, "createChannels"); descriptor->createChannels = AttrAsUint32(input, "createChannels");
descriptor->createWidth = AttrAsUint32(input, "createWidth"); descriptor->createWidth = AttrAsUint32(input, "createWidth");
descriptor->createHeight = AttrAsUint32(input, "createHeight"); descriptor->createHeight = AttrAsUint32(input, "createHeight");
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"); descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground");
} }
}
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
descriptor->limitInputPixels = AttrAsUint32(input, "limitInputPixels"); descriptor->limitInputPixels = AttrAsUint32(input, "limitInputPixels");
// Allow switch from random to sequential access // Allow switch from random to sequential access
@ -318,6 +324,25 @@ namespace sharp {
} else { } else {
if (descriptor->createChannels > 0) { if (descriptor->createChannels > 0) {
// Create new image // Create new image
if (descriptor->createNoiseType == "gaussian") {
int const channels = descriptor->createChannels;
image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight);
std::vector<VImage> 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<double> background = { std::vector<double> background = {
descriptor->createBackground[0], descriptor->createBackground[0],
descriptor->createBackground[1], descriptor->createBackground[1],
@ -327,6 +352,7 @@ namespace sharp {
background.push_back(descriptor->createBackground[3]); 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; image.get_image()->Type = VIPS_INTERPRETATION_sRGB;
imageType = ImageType::RAW; imageType = ImageType::RAW;
} else { } else {

View File

@ -64,6 +64,9 @@ namespace sharp {
int createWidth; int createWidth;
int createHeight; int createHeight;
std::vector<double> createBackground; std::vector<double> createBackground;
std::string createNoiseType;
double createNoiseMean;
double createNoiseSigma;
InputDescriptor(): InputDescriptor():
buffer(nullptr), buffer(nullptr),
@ -82,7 +85,9 @@ namespace sharp {
createChannels(0), createChannels(0),
createWidth(0), createWidth(0),
createHeight(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 // Convenience methods to access the attributes of a Napi::Object

258
test/unit/noise.js Normal file
View File

@ -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
}
}
});
});
});
});