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,
threshold: 0,
thresholdGrayscale: true,
trimTolerance: 0,
gamma: 0,
greyscale: false,
normalize: 0,
@ -564,6 +565,23 @@ Sharp.prototype.threshold = function(threshold, options) {
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;
};
/*
Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
Improves brightness of resized image in non-linear colour spaces.

View File

@ -115,6 +115,10 @@ namespace sharp {
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
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);

View File

@ -399,4 +399,56 @@ namespace sharp {
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

View File

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

View File

@ -56,6 +56,7 @@ using sharp::EntropyCrop;
using sharp::TileCache;
using sharp::Threshold;
using sharp::Bandbool;
using sharp::Trim;
using sharp::ImageType;
using sharp::ImageTypeId;
@ -207,6 +208,11 @@ class PipelineWorker : public AsyncWorker {
RemoveExifOrientation(image);
}
// Trim
if(baton->trimTolerance != 0) {
image = Trim(image, baton->trimTolerance);
}
// Pre extraction
if (baton->topOffsetPre != -1) {
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->threshold = attrAs<int32_t>(options, "threshold");
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->greyscale = attrAs<bool>(options, "greyscale");
baton->normalize = attrAs<bool>(options, "normalize");

View File

@ -63,6 +63,7 @@ struct PipelineBaton {
double sharpenJagged;
int threshold;
bool thresholdGrayscale;
int trimTolerance;
double gamma;
bool greyscale;
bool normalize;
@ -127,6 +128,7 @@ struct PipelineBaton {
sharpenJagged(2.0),
threshold(0),
thresholdGrayscale(true),
trimTolerance(0),
gamma(0.0),
greyscale(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);
});
});
});