From 031c808aa5f8763e1d95ea0b7c1e462714501834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Pin=C3=A7on?= Date: Thu, 27 Mar 2025 12:59:02 +0000 Subject: [PATCH] Expose erode and dilate operations #4243 --- docs/public/humans.txt | 3 ++ docs/src/content/docs/api-operation.md | 46 ++++++++++++++++++++++++ docs/src/content/docs/changelog.md | 4 +++ lib/constructor.js | 2 ++ lib/index.d.ts | 16 +++++++++ lib/operation.js | 48 +++++++++++++++++++++++++ src/operations.cc | 22 ++++++++++++ src/operations.h | 9 +++++ src/pipeline.cc | 12 +++++++ src/pipeline.h | 4 +++ test/fixtures/dot-and-lines.png | Bin 0 -> 463 bytes test/fixtures/expected/dilate-1.png | Bin 0 -> 540 bytes test/fixtures/expected/erode-1.png | Bin 0 -> 450 bytes test/fixtures/index.js | 2 ++ test/types/sharp.test-d.ts | 5 +++ test/unit/dilate.js | 38 ++++++++++++++++++++ test/unit/erode.js | 38 ++++++++++++++++++++ 17 files changed, 249 insertions(+) create mode 100644 test/fixtures/dot-and-lines.png create mode 100644 test/fixtures/expected/dilate-1.png create mode 100644 test/fixtures/expected/erode-1.png create mode 100644 test/unit/dilate.js create mode 100644 test/unit/erode.js 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 0000000000000000000000000000000000000000..5c50d2456fbdd9083a058a16392a2213131d91db GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^DImFdh=n2n7~lk=F;=}e%IWQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4NZ$Nk>pEv^p zqo=2fV@SoVx3>)W4jJ&U1|)s`7p^BQ+{AY2R_45Lu|FwmzRlkD{$8G;<9mks5LB>O@NYISf zaBk(=E88oVdYf*Z@TcQh_Mb3zJ>k{bOV>Dv1$)?LvD`6i4G~;F!*x|txR1)zccR6f zPpc|5bURJUWP;jGrvNSDx+bap?AO-RO$olerEX~jwS0-25)MXjWWVY1G4DI-{stQz3S=df3Fq)GS$gF_`NLdVcrWRThVLL7XM2>%whEg ndqgLK3mhN^gEl0@M)+Tvkhy;EgS``gAgTe~DWM4f8lAZa literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/dilate-1.png b/test/fixtures/expected/dilate-1.png new file mode 100644 index 0000000000000000000000000000000000000000..947eb4b0ce1037e4e8f51e7bd5fc052099e95e0a GIT binary patch literal 540 zcmeAS@N?(olHy`uVBq!ia0vp^DImN7?1y#Al(oAg3_cUHS!&PtnQ(|W(kPu<9n%y^8chgE{xKrBJ$KtuyvAUk+XVee0) zYdg-=$YjT4N@dT5ORcuQ*1Go@)BmP;-KXi!$B;BZP5GVF%Y8<7<1y1^8?Q-fOTV5J zS3Gk+FC%uVzRuow?DF~ThVACPcjI}sSZzx#shfUj>(4V+m-nq%dz0O7-(>li)0e-O zapbT2crp36O8WJGcb48wI{lh+r}yro)2<6UrB|gKyRMCu5;C`9{dV0MAB9yPRIq4z zS-0+b^(T67#iHZ=I*yD#{C{8yK?;p00i_>zopr08#ATT>t<8 literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/erode-1.png b/test/fixtures/expected/erode-1.png new file mode 100644 index 0000000000000000000000000000000000000000..54ff8035b8386e04a77ca74c1474855d9d541674 GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0vp^DImFdh=n2n7~+hk46(_cU#$r9Iy66gHf+|;}h2Ir#G#FEq$h4Rdj3F!Z*Mya9ai9AIB@FA|9HRmwp(u9XehLNeuyuLX>ys&apy0t3Jt8>Vmc8U z1P(NT85{`*o3;kn?6pnld*x4=Eo6G?+oCzW+1X9WR+9gEJWSYdkS_UoC z**?c!ty!#;vhhf#c9GfQeIc^zBb-)sgs)KwJ*X`<$?N;4peWJou)q}2>8F}lvz6EE z>{VNu8{2OB{kPuJ?2Cpitl9c&B74=c_blDSl{{C!>&VJXr_{M8&C2)9Ddwp^ZCNWc z|Jr6ZThwS8p^|F)ce|KbYGR*;_=!2yuK3HAxA_Z|kx2?-kl Tzx&Ms1~!AItDnm{r-UW|ab>Y> literal 0 HcmV?d00001 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); + }); + }); +});