diff --git a/lib/constructor.js b/lib/constructor.js index 1f1d39ec..d83b685f 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -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, diff --git a/lib/operation.js b/lib/operation.js index f08a2f3b..13e7a99d 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -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, diff --git a/src/pipeline.cc b/src/pipeline.cc index 3a631836..a920c08f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -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(options, "flatten"); baton->negate = AttrTo(options, "negate"); baton->blurSigma = AttrTo(options, "blurSigma"); + baton->medianSize = AttrTo(options, "medianSize"); baton->sharpenSigma = AttrTo(options, "sharpenSigma"); baton->sharpenFlat = AttrTo(options, "sharpenFlat"); baton->sharpenJagged = AttrTo(options, "sharpenJagged"); diff --git a/src/pipeline.h b/src/pipeline.h index 386155e8..f8367270 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -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), diff --git a/test/fixtures/expected/median_1.jpg b/test/fixtures/expected/median_1.jpg new file mode 100644 index 00000000..df153592 Binary files /dev/null and b/test/fixtures/expected/median_1.jpg differ diff --git a/test/fixtures/expected/median_3.jpg b/test/fixtures/expected/median_3.jpg new file mode 100644 index 00000000..ccb091ab Binary files /dev/null and b/test/fixtures/expected/median_3.jpg differ diff --git a/test/fixtures/expected/median_5.jpg b/test/fixtures/expected/median_5.jpg new file mode 100644 index 00000000..ba536e98 Binary files /dev/null and b/test/fixtures/expected/median_5.jpg differ diff --git a/test/fixtures/expected/median_color.jpg b/test/fixtures/expected/median_color.jpg new file mode 100644 index 00000000..f521f0af Binary files /dev/null and b/test/fixtures/expected/median_color.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index bd9aaf4c..3ccc7167 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -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 diff --git a/test/fixtures/random.jpg b/test/fixtures/random.jpg new file mode 100644 index 00000000..723c342e Binary files /dev/null and b/test/fixtures/random.jpg differ diff --git a/test/fixtures/thRandom.jpg b/test/fixtures/thRandom.jpg new file mode 100644 index 00000000..aa01d199 Binary files /dev/null and b/test/fixtures/thRandom.jpg differ diff --git a/test/unit/median.js b/test/unit/median.js new file mode 100644 index 00000000..a25ef1a1 --- /dev/null +++ b/test/unit/median.js @@ -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); + }); + }); +});