Expose erode and dilate operations #4243

This commit is contained in:
Quentin Pinçon 2025-03-27 12:59:02 +00:00 committed by Lovell Fuller
parent 03e1b19764
commit 031c808aa5
17 changed files with 249 additions and 0 deletions

View File

@ -314,3 +314,6 @@ GitHub: https://github.com/happycollision
Name: Florent Zabera Name: Florent Zabera
GitHub: https://github.com/florentzabera GitHub: https://github.com/florentzabera
Name: Quentin Pinçon
GitHub: https://github.com/qpincon

View File

@ -284,6 +284,52 @@ const gaussianBlurred = await sharp(input)
``` ```
## dilate
> dilate([width]) ⇒ <code>Sharp</code>
Expand foreground objects using the dilate morphological operator.
**Throws**:
- <code>Error</code> Invalid parameters
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [width] | <code>Number</code> | <code>1</code> | dilation width in pixels. |
**Example**
```js
const output = await sharp(input)
.dilate()
.toBuffer();
```
## erode
> erode([width]) ⇒ <code>Sharp</code>
Shrink foreground objects using the erode morphological operator.
**Throws**:
- <code>Error</code> Invalid parameters
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [width] | <code>Number</code> | <code>1</code> | erosion width in pixels. |
**Example**
```js
const output = await sharp(input)
.erode()
.toBuffer();
```
## flatten ## flatten
> flatten([options]) ⇒ <code>Sharp</code> > flatten([options]) ⇒ <code>Sharp</code>

View File

@ -45,6 +45,10 @@ Requires libvips v8.16.1
[#4207](https://github.com/lovell/sharp/pull/4207) [#4207](https://github.com/lovell/sharp/pull/4207)
[@calebmer](https://github.com/calebmer) [@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. * Add support for RGBE images. Requires libvips compiled with radiance support.
[#4316](https://github.com/lovell/sharp/pull/4316) [#4316](https://github.com/lovell/sharp/pull/4316)
[@florentzabera](https://github.com/florentzabera) [@florentzabera](https://github.com/florentzabera)

View File

@ -263,6 +263,8 @@ const Sharp = function (input, options) {
trimBackground: [], trimBackground: [],
trimThreshold: -1, trimThreshold: -1,
trimLineArt: false, trimLineArt: false,
dilateWidth: 0,
erodeWidth: 0,
gamma: 0, gamma: 0,
gammaOut: 0, gammaOut: 0,
greyscale: false, greyscale: false,

16
lib/index.d.ts vendored
View File

@ -504,6 +504,22 @@ declare namespace sharp {
*/ */
blur(sigma?: number | boolean | BlurOptions): 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. * Merge alpha transparency channel, if any, with background.
* @param flatten true to enable and false to disable (defaults to true) * @param flatten true to enable and false to disable (defaults to true)

View File

@ -443,6 +443,52 @@ function blur (options) {
return this; 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. * Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
* *
@ -958,6 +1004,8 @@ module.exports = function (Sharp) {
flop, flop,
affine, affine,
sharpen, sharpen,
erode,
dilate,
median, median,
blur, blur,
flatten, flatten,

View File

@ -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 } // namespace sharp

View File

@ -120,6 +120,15 @@ namespace sharp {
VImage EmbedMultiPage(VImage image, int left, int top, int width, int height, VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight); VipsExtend extendWith, std::vector<double> 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 } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@ -609,6 +609,16 @@ class PipelineWorker : public Napi::AsyncWorker {
image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); 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 // Blur
if (shouldBlur) { if (shouldBlur) {
image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl); 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->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA"); baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB"); 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->greyscale = sharp::AttrAsBool(options, "greyscale");
baton->normalise = sharp::AttrAsBool(options, "normalise"); baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower"); baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");

View File

@ -101,6 +101,8 @@ struct PipelineBaton {
int trimOffsetTop; int trimOffsetTop;
std::vector<double> linearA; std::vector<double> linearA;
std::vector<double> linearB; std::vector<double> linearB;
int dilateWidth;
int erodeWidth;
double gamma; double gamma;
double gammaOut; double gammaOut;
bool greyscale; bool greyscale;
@ -274,6 +276,8 @@ struct PipelineBaton {
trimOffsetTop(0), trimOffsetTop(0),
linearA{}, linearA{},
linearB{}, linearB{},
dilateWidth(0),
erodeWidth(0),
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
normalise(false), normalise(false),

BIN
test/fixtures/dot-and-lines.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

BIN
test/fixtures/expected/dilate-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

BIN
test/fixtures/expected/erode-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

View File

@ -128,6 +128,8 @@ module.exports = {
inputJPGBig: getPath('flowers.jpeg'), inputJPGBig: getPath('flowers.jpeg'),
inputPngDotAndLines: getPath('dot-and-lines.png'),
inputPngStripesV: getPath('stripesV.png'), inputPngStripesV: getPath('stripesV.png'),
inputPngStripesH: getPath('stripesH.png'), inputPngStripesH: getPath('stripesH.png'),

View File

@ -740,3 +740,8 @@ sharp([input, input], {
valign: 'bottom' valign: 'bottom'
} }
}); });
sharp().erode();
sharp().erode(1);
sharp().dilate();
sharp().dilate(1);

38
test/unit/dilate.js Normal file
View File

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

38
test/unit/erode.js Normal file
View File

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