Add trim operation to remove "boring" edges (#491 #492)

This commit is contained in:
Kleis Auke Wolthuizen 2016-07-08 23:19:10 +02:00 committed by Lovell Fuller
parent 673d8278b5
commit b69627891d
9 changed files with 136 additions and 6 deletions

View File

@ -86,6 +86,7 @@ var Sharp = function(input, options) {
sharpenJagged: 2, sharpenJagged: 2,
threshold: 0, threshold: 0,
thresholdGrayscale: true, thresholdGrayscale: true,
trimTolerance: 0,
gamma: 0, gamma: 0,
greyscale: false, greyscale: false,
normalize: 0, normalize: 0,
@ -364,13 +365,13 @@ Sharp.prototype.overlayWith = function(overlay, options) {
setTileOption(options.tile, this.options); setTileOption(options.tile, this.options);
} }
if(isDefined(options.cutout)) { if(isDefined(options.cutout)) {
setCutoutOption(options.cutout, this.options); setCutoutOption(options.cutout, this.options);
} }
if(isDefined(options.left) || isDefined(options.top)) { if(isDefined(options.left) || isDefined(options.top)) {
setOffsetOption(options.top, options.left, this.options); setOffsetOption(options.top, options.left, this.options);
} }
if (isDefined(options.gravity)) { if (isDefined(options.gravity)) {
setGravityOption(options.gravity, this.options); setGravityOption(options.gravity, this.options);
} }
} }
return this; return this;
@ -384,7 +385,7 @@ function setTileOption(tile, options) {
options.overlayTile = tile; options.overlayTile = tile;
} else { } else {
throw new Error('Invalid Value for tile ' + tile + ' Only Boolean Values allowed for overlay.tile.'); throw new Error('Invalid Value for tile ' + tile + ' Only Boolean Values allowed for overlay.tile.');
} }
} }
function setCutoutOption(cutout, options) { function setCutoutOption(cutout, options) {
@ -553,14 +554,31 @@ Sharp.prototype.threshold = function(threshold, options) {
} else { } else {
throw new Error('Invalid threshold (0 to 255) ' + threshold); throw new Error('Invalid threshold (0 to 255) ' + threshold);
} }
if(typeof options === 'undefined' || if(typeof options === 'undefined' ||
options.greyscale === true || options.grayscale === true) { options.greyscale === true || options.grayscale === true) {
this.options.thresholdGrayscale = true; this.options.thresholdGrayscale = true;
} else { } else {
this.options.thresholdGrayscale = false; this.options.thresholdGrayscale = false;
} }
return this;
};
/*
Automatically remove "boring" image edges.
tolerance - if present, is a percentaged tolerance level between 0 and 100 to trim away similar color values
Defaulting to 10 when no tolerance is given.
*/
Sharp.prototype.trim = function(tolerance) {
if (typeof tolerance === 'undefined') {
this.options.trimTolerance = 10;
} else if (isInteger(tolerance) && inRange(tolerance, 1, 99)) {
this.options.trimTolerance = tolerance;
} else {
throw new Error('Invalid trim tolerance (1 to 99) ' + tolerance);
}
return this; return this;
}; };

View File

@ -115,6 +115,10 @@ namespace sharp {
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight, std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const x, int const y); int const outWidth, int const outHeight, int const x, int const y);
/*
Return the image alpha maximum. Useful for combining alpha bands. scRGB
images are 0 - 1 for image data, but the alpha is 0 - 255.
*/
int MaximumImageAlpha(VipsInterpretation interpretation); int MaximumImageAlpha(VipsInterpretation interpretation);

View File

@ -399,4 +399,56 @@ namespace sharp {
return image.bandbool(boolean); return image.bandbool(boolean);
} }
VImage Trim(VImage image, int const tolerance) {
using sharp::MaximumImageAlpha;
// An equivalent of ImageMagick's -trim in C++ ... automatically remove
// "boring" image edges.
// We use .project to sum the rows and columns of a 0/255 mask image, the first
// non-zero row or column is the object edge. We make the mask image with an
// amount-different-from-background image plus a threshold.
// find the value of the pixel at (0, 0) ... we will search for all pixels
// significantly different from this
std::vector<double> background = image(0, 0);
int max = MaximumImageAlpha(image.interpretation());
// we need to smooth the image, subtract the background from every pixel, take
// the absolute value of the difference, then threshold
VImage mask = (image.median(3) - background).abs() > (max * tolerance / 100);
// sum mask rows and columns, then search for the first non-zero sum in each
// direction
VImage rows;
VImage columns = mask.project(&rows);
VImage profileLeftV;
VImage profileLeftH = columns.profile(&profileLeftV);
VImage profileRightV;
VImage profileRightH = columns.fliphor().profile(&profileRightV);
VImage profileTopV;
VImage profileTopH = rows.profile(&profileTopV);
VImage profileBottomV;
VImage profileBottomH = rows.flipver().profile(&profileBottomV);
int left = static_cast<int>(floor(profileLeftV.min()));
int right = columns.width() - static_cast<int>(floor(profileRightV.min()));
int top = static_cast<int>(floor(profileTopH.min()));
int bottom = rows.height() - static_cast<int>(floor(profileBottomH.min()));
int width = right - left;
int height = bottom - top;
if(width <= 0 || height <= 0) {
throw VError("Unexpected error while trimming. Try to lower the tolerance");
}
// and now crop the original image
return image.extract_area(left, top, width, height);
}
} // namespace sharp } // namespace sharp

View File

@ -87,6 +87,11 @@ namespace sharp {
*/ */
VImage Bandbool(VImage image, VipsOperationBoolean const boolean); VImage Bandbool(VImage image, VipsOperationBoolean const boolean);
/*
Trim an image
*/
VImage Trim(VImage image, int const tolerance);
} // namespace sharp } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@ -56,6 +56,7 @@ using sharp::EntropyCrop;
using sharp::TileCache; using sharp::TileCache;
using sharp::Threshold; using sharp::Threshold;
using sharp::Bandbool; using sharp::Bandbool;
using sharp::Trim;
using sharp::ImageType; using sharp::ImageType;
using sharp::ImageTypeId; using sharp::ImageTypeId;
@ -207,6 +208,11 @@ class PipelineWorker : public AsyncWorker {
RemoveExifOrientation(image); RemoveExifOrientation(image);
} }
// Trim
if(baton->trimTolerance != 0) {
image = Trim(image, baton->trimTolerance);
}
// Pre extraction // Pre extraction
if (baton->topOffsetPre != -1) { if (baton->topOffsetPre != -1) {
image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre); image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
@ -1145,6 +1151,10 @@ NAN_METHOD(pipeline) {
baton->sharpenJagged = attrAs<double>(options, "sharpenJagged"); baton->sharpenJagged = attrAs<double>(options, "sharpenJagged");
baton->threshold = attrAs<int32_t>(options, "threshold"); baton->threshold = attrAs<int32_t>(options, "threshold");
baton->thresholdGrayscale = attrAs<bool>(options, "thresholdGrayscale"); baton->thresholdGrayscale = attrAs<bool>(options, "thresholdGrayscale");
baton->trimTolerance = attrAs<int32_t>(options, "trimTolerance");
if(baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && baton->trimTolerance != 0) {
baton->accessMethod = VIPS_ACCESS_RANDOM;
}
baton->gamma = attrAs<double>(options, "gamma"); baton->gamma = attrAs<double>(options, "gamma");
baton->greyscale = attrAs<bool>(options, "greyscale"); baton->greyscale = attrAs<bool>(options, "greyscale");
baton->normalize = attrAs<bool>(options, "normalize"); baton->normalize = attrAs<bool>(options, "normalize");

View File

@ -63,6 +63,7 @@ struct PipelineBaton {
double sharpenJagged; double sharpenJagged;
int threshold; int threshold;
bool thresholdGrayscale; bool thresholdGrayscale;
int trimTolerance;
double gamma; double gamma;
bool greyscale; bool greyscale;
bool normalize; bool normalize;
@ -127,6 +128,7 @@ struct PipelineBaton {
sharpenJagged(2.0), sharpenJagged(2.0),
threshold(0), threshold(0),
thresholdGrayscale(true), thresholdGrayscale(true),
trimTolerance(0),
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
normalize(false), normalize(false),

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

39
test/unit/trim.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Trim borders', function() {
it('Threshold default', function(done) {
var expected = fixtures.expected('alpha-layer-1-fill-trim-resize.png');
sharp(fixtures.inputPngOverlayLayer1)
.resize(450, 322)
.trim()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(450, info.width);
assert.strictEqual(322, info.height);
fixtures.assertSimilar(expected, data, done);
});
});
it('16-bit PNG with alpha channel', function(done) {
sharp(fixtures.inputPngWithTransparency16bit)
.resize(32, 32)
.trim()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(32, info.width);
assert.strictEqual(32, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('trim-16bit-rgba.png'), data, done);
});
});
});