Expose libvips' median filter operation (#1161)

This commit is contained in:
Andrea Bianco 2018-03-17 11:52:44 +01:00 committed by Lovell Fuller
parent f880adbaac
commit 875937e3d8
12 changed files with 105 additions and 1 deletions

View File

@ -153,6 +153,7 @@ const Sharp = function (input, options) {
background: [0, 0, 0, 255],
flatten: false,
negate: false,
medianSize: 0,
blurSigma: 0,
sharpenSigma: 0,
sharpenFlat: 1,

View File

@ -156,6 +156,26 @@ function sharpen (sigma, flat, jagged) {
return this;
}
/**
* Apply median filter using vips_rank( in, out, m, m, m * m / 2 );
* when used witout parameters the defaul window is 3x3
* @param {Number} [size] square mask size: size x size
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function median (size) {
if (!is.defined(size)) {
// No arguments: default to 3x3
this.options.medianSize = 3;
} else if (is.integer(size) && is.inRange(size, 1, 1000)) {
// Numeric argument: specific sigma
this.options.medianSize = size;
} else {
throw new Error('Invalid median size ' + size);
}
return this;
}
/**
* Blur the image.
* When used without parameters, performs a fast, mild blur of the output image.
@ -444,6 +464,7 @@ module.exports = function (Sharp) {
flip,
flop,
sharpen,
median,
blur,
extend,
flatten,

View File

@ -359,6 +359,8 @@ class PipelineWorker : public Nan::AsyncWorker {
bool const shouldBlur = baton->blurSigma != 0.0;
bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
bool const shouldSharpen = baton->sharpenSigma != 0.0;
bool const shouldApplyMedian = baton->medianSize > 0;
bool const shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldResize || shouldBlur || shouldConv || shouldSharpen || shouldOverlayWithAlpha);
@ -544,7 +546,10 @@ class PipelineWorker : public Nan::AsyncWorker {
image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
}
// Median - must happen before blurring, due to the utility of blurring after thresholding
if (shouldApplyMedian) {
image = image.median(baton->medianSize);
}
// Threshold - must happen before blurring, due to the utility of blurring after thresholding
if (baton->threshold != 0) {
image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale);
@ -1194,6 +1199,7 @@ NAN_METHOD(pipeline) {
baton->flatten = AttrTo<bool>(options, "flatten");
baton->negate = AttrTo<bool>(options, "negate");
baton->blurSigma = AttrTo<double>(options, "blurSigma");
baton->medianSize = AttrTo<uint32_t>(options, "medianSize");
baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma");
baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat");
baton->sharpenJagged = AttrTo<double>(options, "sharpenJagged");

View File

@ -73,6 +73,7 @@ struct PipelineBaton {
bool flatten;
bool negate;
double blurSigma;
int medianSize;
double sharpenSigma;
double sharpenFlat;
double sharpenJagged;
@ -157,6 +158,7 @@ struct PipelineBaton {
flatten(false),
negate(false),
blurSigma(0.0),
medianSize(0),
sharpenSigma(0.0),
sharpenFlat(1.0),
sharpenJagged(2.0),

BIN
test/fixtures/expected/median_1.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
test/fixtures/expected/median_3.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

BIN
test/fixtures/expected/median_5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

BIN
test/fixtures/expected/median_color.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -69,6 +69,8 @@ module.exports = {
inputJpgOverlayLayer2: getPath('alpha-layer-2-ink.jpg'),
inputJpgTruncated: getPath('truncated.jpg'), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg
inputJpgCenteredImage: getPath('centered_image.jpeg'),
inputJpgRandom: getPath('random.jpg'), // convert -size 200x200 xc: +noise Random random.jpg
inputJpgThRandom: getPath('thRandom.jpg'), // convert random.jpg -channel G -threshold 5% -separate +channel -negate thRandom.jpg
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngWithTransparency: getPath('blackbug.png'), // public domain

BIN
test/fixtures/random.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
test/fixtures/thRandom.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

72
test/unit/median.js Normal file
View File

@ -0,0 +1,72 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Median filter', function () {
it('1x1 window', function (done) {
sharp(fixtures.inputJpgThRandom)
.median(1)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('median_1.jpg'), data, done);
});
});
it('3x3 window', function (done) {
sharp(fixtures.inputJpgThRandom)
.median(3)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('median_3.jpg'), data, done);
});
});
it('5x5 window', function (done) {
sharp(fixtures.inputJpgThRandom)
.median(5)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('median_5.jpg'), data, done);
});
});
it('color image', function (done) {
sharp(fixtures.inputJpgRandom)
.median(5)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('median_color.jpg'), data, done);
});
});
it('no windows size', function (done) {
sharp(fixtures.inputJpgThRandom)
.median()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(200, info.width);
assert.strictEqual(200, info.height);
fixtures.assertSimilar(fixtures.expected('median_3.jpg'), data, done);
});
});
it('invalid radius', function () {
assert.throws(function () {
sharp(fixtures.inputJpg).median(0.1);
});
});
});