diff --git a/docs/public/humans.txt b/docs/public/humans.txt index 4c9eb88d..f8bd272f 100644 --- a/docs/public/humans.txt +++ b/docs/public/humans.txt @@ -314,3 +314,6 @@ GitHub: https://github.com/happycollision Name: Florent Zabera GitHub: https://github.com/florentzabera + +Name: Quentin Pinçon +GitHub: https://github.com/qpincon diff --git a/docs/src/content/docs/api-operation.md b/docs/src/content/docs/api-operation.md index 720918c5..dea7c79e 100644 --- a/docs/src/content/docs/api-operation.md +++ b/docs/src/content/docs/api-operation.md @@ -284,6 +284,52 @@ const gaussianBlurred = await sharp(input) ``` +## dilate +> dilate([width]) ⇒ Sharp + +Expand foreground objects using the dilate morphological operator. + + +**Throws**: + +- Error Invalid parameters + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [width] | Number | 1 | dilation width in pixels. | + +**Example** +```js +const output = await sharp(input) + .dilate() + .toBuffer(); +``` + + +## erode +> erode([width]) ⇒ Sharp + +Shrink foreground objects using the erode morphological operator. + + +**Throws**: + +- Error Invalid parameters + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [width] | Number | 1 | erosion width in pixels. | + +**Example** +```js +const output = await sharp(input) + .erode() + .toBuffer(); +``` + + ## flatten > flatten([options]) ⇒ Sharp diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md index 67d222d6..e08b5d8b 100644 --- a/docs/src/content/docs/changelog.md +++ b/docs/src/content/docs/changelog.md @@ -45,6 +45,10 @@ Requires libvips v8.16.1 [#4207](https://github.com/lovell/sharp/pull/4207) [@calebmer](https://github.com/calebmer) +* Expose erode and dilate operations. + [#4243](https://github.com/lovell/sharp/pull/4243) + [@qpincon](https://github.com/qpincon) + * Add support for RGBE images. Requires libvips compiled with radiance support. [#4316](https://github.com/lovell/sharp/pull/4316) [@florentzabera](https://github.com/florentzabera) diff --git a/lib/constructor.js b/lib/constructor.js index 32a80ede..3b88fbd9 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -263,6 +263,8 @@ const Sharp = function (input, options) { trimBackground: [], trimThreshold: -1, trimLineArt: false, + dilateWidth: 0, + erodeWidth: 0, gamma: 0, gammaOut: 0, greyscale: false, diff --git a/lib/index.d.ts b/lib/index.d.ts index 78837a94..a1c5f082 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -504,6 +504,22 @@ declare namespace sharp { */ blur(sigma?: number | boolean | BlurOptions): Sharp; + /** + * Expand foreground objects using the dilate morphological operator. + * @param {Number} [width=1] dilation width in pixels. + * @throws {Error} Invalid parameters + * @returns A sharp instance that can be used to chain operations + */ + dilate(width?: number): Sharp; + + /** + * Shrink foreground objects using the erode morphological operator. + * @param {Number} [width=1] erosion width in pixels. + * @throws {Error} Invalid parameters + * @returns A sharp instance that can be used to chain operations + */ + erode(width?: number): Sharp; + /** * Merge alpha transparency channel, if any, with background. * @param flatten true to enable and false to disable (defaults to true) diff --git a/lib/operation.js b/lib/operation.js index b65cebbf..d6cbbdf8 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -443,6 +443,52 @@ function blur (options) { return this; } +/** + * Expand foreground objects using the dilate morphological operator. + * + * @example + * const output = await sharp(input) + * .dilate() + * .toBuffer(); + * + * @param {Number} [width=1] dilation width in pixels. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function dilate (width) { + if (!is.defined(width)) { + this.options.dilateWidth = 1; + } else if (is.integer(width) && width > 0) { + this.options.dilateWidth = width; + } else { + throw is.invalidParameterError('dilate', 'positive integer', dilate); + } + return this; +} + +/** + * Shrink foreground objects using the erode morphological operator. + * + * @example + * const output = await sharp(input) + * .erode() + * .toBuffer(); + * + * @param {Number} [width=1] erosion width in pixels. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function erode (width) { + if (!is.defined(width)) { + this.options.erodeWidth = 1; + } else if (is.integer(width) && width > 0) { + this.options.erodeWidth = width; + } else { + throw is.invalidParameterError('erode', 'positive integer', erode); + } + return this; +} + /** * Merge alpha transparency channel, if any, with a background, then remove the alpha channel. * @@ -958,6 +1004,8 @@ module.exports = function (Sharp) { flop, affine, sharpen, + erode, + dilate, median, blur, flatten, diff --git a/src/operations.cc b/src/operations.cc index 9856bb2a..ba3a0513 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -472,4 +472,26 @@ namespace sharp { } } + /* + * Dilate an image + */ + VImage Dilate(VImage image, int const width) { + int const maskWidth = 2 * width + 1; + VImage mask = VImage::new_matrix(maskWidth, maskWidth); + return image.morph( + mask, + VIPS_OPERATION_MORPHOLOGY_DILATE).invert(); + } + + /* + * Erode an image + */ + VImage Erode(VImage image, int const width) { + int const maskWidth = 2 * width + 1; + VImage mask = VImage::new_matrix(maskWidth, maskWidth); + return image.morph( + mask, + VIPS_OPERATION_MORPHOLOGY_ERODE).invert(); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index b2881d65..22ff46fc 100644 --- a/src/operations.h +++ b/src/operations.h @@ -120,6 +120,15 @@ namespace sharp { VImage EmbedMultiPage(VImage image, int left, int top, int width, int height, VipsExtend extendWith, std::vector background, int nPages, int *pageHeight); + /* + * Dilate an image + */ + VImage Dilate(VImage image, int const maskWidth); + + /* + * Erode an image + */ + VImage Erode(VImage image, int const maskWidth); } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index 24ae7e02..21529acf 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -609,6 +609,16 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); } + // Dilate - must happen before blurring, due to the utility of dilating after thresholding + if (baton->dilateWidth != 0) { + image = sharp::Dilate(image, baton->dilateWidth); + } + + // Erode - must happen before blurring, due to the utility of eroding after thresholding + if (baton->erodeWidth != 0) { + image = sharp::Erode(image, baton->erodeWidth); + } + // Blur if (shouldBlur) { image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl); @@ -1621,6 +1631,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut"); baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA"); baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB"); + baton->dilateWidth = sharp::AttrAsUint32(options, "dilateWidth"); + baton->erodeWidth = sharp::AttrAsUint32(options, "erodeWidth"); baton->greyscale = sharp::AttrAsBool(options, "greyscale"); baton->normalise = sharp::AttrAsBool(options, "normalise"); baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower"); diff --git a/src/pipeline.h b/src/pipeline.h index 90d6f85c..4bb053fd 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -101,6 +101,8 @@ struct PipelineBaton { int trimOffsetTop; std::vector linearA; std::vector linearB; + int dilateWidth; + int erodeWidth; double gamma; double gammaOut; bool greyscale; @@ -274,6 +276,8 @@ struct PipelineBaton { trimOffsetTop(0), linearA{}, linearB{}, + dilateWidth(0), + erodeWidth(0), gamma(0.0), greyscale(false), normalise(false), diff --git a/test/fixtures/dot-and-lines.png b/test/fixtures/dot-and-lines.png new file mode 100644 index 00000000..5c50d245 Binary files /dev/null and b/test/fixtures/dot-and-lines.png differ diff --git a/test/fixtures/expected/dilate-1.png b/test/fixtures/expected/dilate-1.png new file mode 100644 index 00000000..947eb4b0 Binary files /dev/null and b/test/fixtures/expected/dilate-1.png differ diff --git a/test/fixtures/expected/erode-1.png b/test/fixtures/expected/erode-1.png new file mode 100644 index 00000000..54ff8035 Binary files /dev/null and b/test/fixtures/expected/erode-1.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index ba79504d..f5b686ad 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -128,6 +128,8 @@ module.exports = { inputJPGBig: getPath('flowers.jpeg'), + inputPngDotAndLines: getPath('dot-and-lines.png'), + inputPngStripesV: getPath('stripesV.png'), inputPngStripesH: getPath('stripesH.png'), diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts index 93b421ba..d968e07c 100644 --- a/test/types/sharp.test-d.ts +++ b/test/types/sharp.test-d.ts @@ -740,3 +740,8 @@ sharp([input, input], { valign: 'bottom' } }); + +sharp().erode(); +sharp().erode(1); +sharp().dilate(); +sharp().dilate(1); diff --git a/test/unit/dilate.js b/test/unit/dilate.js new file mode 100644 index 00000000..94588a28 --- /dev/null +++ b/test/unit/dilate.js @@ -0,0 +1,38 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Dilate', function () { + it('dilate 1 png', function (done) { + sharp(fixtures.inputPngDotAndLines) + .dilate(1) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('dilate-1.png'), data, done); + }); + }); + + it('dilate 1 png - default width', function (done) { + sharp(fixtures.inputPngDotAndLines) + .dilate() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('dilate-1.png'), data, done); + }); + }); + + it('invalid dilation width', function () { + assert.throws(function () { + sharp(fixtures.inputJpg).dilate(-1); + }); + }); +}); diff --git a/test/unit/erode.js b/test/unit/erode.js new file mode 100644 index 00000000..4d2da81f --- /dev/null +++ b/test/unit/erode.js @@ -0,0 +1,38 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Erode', function () { + it('erode 1 png', function (done) { + sharp(fixtures.inputPngDotAndLines) + .erode(1) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('erode-1.png'), data, done); + }); + }); + + it('erode 1 png - default width', function (done) { + sharp(fixtures.inputPngDotAndLines) + .erode() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('erode-1.png'), data, done); + }); + }); + + it('invalid erosion width', function () { + assert.throws(function () { + sharp(fixtures.inputJpg).erode(-1); + }); + }); +});