Add boolean feature for bitwise image operations (#501)

This commit is contained in:
Matt Hirsch 2016-07-11 04:51:43 -04:00 committed by Lovell Fuller
parent 99f960bf56
commit d17e8d3450
14 changed files with 256 additions and 22 deletions

View File

@ -520,6 +520,21 @@ sharp('input.png')
In the above example if `input.png` is a 3 channel RGB image, `output.png` will be a 1 channel grayscale image where each pixel `P = R & G & B`. For example, if `I(1,1) = [247, 170, 14] = [0b11110111, 0b10101010, 0b00001111]` then `O(1,1) = 0b11110111 & 0b10101010 & 0b00001111 = 0b00000010 = 2`.
#### boolean(image, operation)
Perform a bitwise boolean operation with `image`.
`image` is one of the following.
* Buffer contianing PNG, WebP, GIF or SVG image data, or
* String containing the path to an image file
This operation creates an output image where each pixel is the result of the selected bitwise boolean `operation`, between the corresponding pixels of the input images. The boolean operation can be one of the following:
* `and` performs a bitwise and operation, like the c-operator `&`
* `or` performs a bitwise or operation, like the c-operator `|`
* `eor` performs a bitwise exclusive or operation, like the c-operator `^`
### Output
#### toFile(path, [callback])

View File

@ -91,6 +91,9 @@ var Sharp = function(input, options) {
greyscale: false,
normalize: 0,
bandBoolOp: null,
booleanOp: null,
booleanBufferIn: null,
booleanFileIn: '',
// overlay
overlayFileIn: '',
overlayBufferIn: null,
@ -365,6 +368,30 @@ Sharp.prototype.negate = function(negate) {
return this;
};
/*
Bitwise boolean operations between images
*/
Sharp.prototype.boolean = function(operand, operator) {
if (isString(operand)) {
this.options.booleanFileIn = operand;
} else if (isBuffer(operand)) {
this.options.booleanBufferIn = operand;
} else {
throw new Error('Unsupported boolean operand ' + typeof operand);
}
if (!isString(operator)) {
throw new Error('Invalid boolean operation ' + operator);
}
operator = operator.toLowerCase();
var ops = ['and', 'or', 'eor'];
if(ops.indexOf(operator) == -1) {
throw new Error('Invalid boolean operation ' + operator);
}
this.options.booleanOp = operator;
return this;
};
/*
Overlay with another image, using an optional gravity
*/

View File

@ -330,4 +330,13 @@ namespace sharp {
}
}
/*
Get VIPS Boolean operatoin type from string
*/
VipsOperationBoolean GetBooleanOperation(std::string opStr) {
return static_cast<VipsOperationBoolean>(
vips_enum_from_nick(nullptr, VIPS_TYPE_OPERATION_BOOLEAN, opStr.data())
);
}
} // namespace sharp

View File

@ -123,6 +123,11 @@ namespace sharp {
*/
int MaximumImageAlpha(VipsInterpretation interpretation);
/*
Get VIPS Boolean operatoin type from string
*/
VipsOperationBoolean GetBooleanOperation(std::string opStr);
} // namespace sharp

View File

@ -399,6 +399,13 @@ namespace sharp {
return image.bandbool(boolean);
}
/*
Perform bitwise boolean operation between images
*/
VImage Boolean(VImage image, VImage imageR, VipsOperationBoolean const boolean) {
return image.boolean(imageR, boolean);
}
VImage Trim(VImage image, int const tolerance) {
using sharp::MaximumImageAlpha;
// An equivalent of ImageMagick's -trim in C++ ... automatically remove

View File

@ -87,6 +87,11 @@ namespace sharp {
*/
VImage Bandbool(VImage image, VipsOperationBoolean const boolean);
/*
Perform bitwise boolean operation between images
*/
VImage Boolean(VImage image, VImage imageR, VipsOperationBoolean const boolean);
/*
Trim an image
*/

View File

@ -9,6 +9,9 @@
#include <node.h>
#include <node_buffer.h>
#include <sstream>
#include <iomanip>
#include "nan.h"
#include "common.h"
@ -56,6 +59,7 @@ using sharp::EntropyCrop;
using sharp::TileCache;
using sharp::Threshold;
using sharp::Bandbool;
using sharp::Boolean;
using sharp::Trim;
using sharp::ImageType;
@ -78,19 +82,22 @@ using sharp::FreeCallback;
using sharp::CalculateCrop;
using sharp::counterProcess;
using sharp::counterQueue;
using sharp::GetBooleanOperation;
typedef struct BufferContainer_t {
std::string name;
Local<Object> nodeBuf;
} BufferContainer;
class PipelineWorker : public AsyncWorker {
public:
PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener,
const Local<Object> &bufferIn, const Local<Object> &overlayBufferIn) :
AsyncWorker(callback), baton(baton), queueListener(queueListener) {
if (baton->bufferInLength > 0) {
SaveToPersistent("bufferIn", bufferIn);
}
if (baton->overlayBufferInLength > 0) {
SaveToPersistent("overlayBufferIn", overlayBufferIn);
}
const std::vector<BufferContainer> saveBuffers) :
AsyncWorker(callback), baton(baton), queueListener(queueListener), saveBuffers(saveBuffers) {
for (const BufferContainer buf : saveBuffers) {
SaveToPersistent(buf.name.c_str(), buf.nodeBuf);
}
}
~PipelineWorker() {}
/*
@ -784,6 +791,44 @@ class PipelineWorker : public AsyncWorker {
}
}
// Apply bitwise boolean operation between images
if (baton->booleanOp != VIPS_OPERATION_BOOLEAN_LAST &&
(baton->booleanBufferInLength > 0 || !baton->booleanFileIn.empty())) {
VImage booleanImage;
ImageType booleanImageType = ImageType::UNKNOWN;
if (baton->booleanBufferInLength > 0) {
// Buffer input for boolean operation
booleanImageType = DetermineImageType(baton->booleanBufferIn, baton->booleanBufferInLength);
if (booleanImageType != ImageType::UNKNOWN) {
try {
booleanImage = VImage::new_from_buffer(baton->booleanBufferIn, baton->booleanBufferInLength,
nullptr, VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Boolean operation buffer has corrupt header");
booleanImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Boolean operation buffer contains unsupported image format");
}
} else if (!baton->booleanFileIn.empty()) {
// File input for boolean operation
booleanImageType = DetermineImageType(baton->booleanFileIn.data());
if (booleanImageType != ImageType::UNKNOWN) {
try {
booleanImage = VImage::new_from_file(baton->booleanFileIn.data(),
VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Boolean operation file has corrupt header");
}
}
}
if (booleanImageType == ImageType::UNKNOWN) {
return Error();
}
// Apply the boolean operation
image = Boolean(image, booleanImage, baton->booleanOp);
}
// Apply per-channel Bandbool bitwise operations after all other operations
if (shouldBandbool) {
image = Bandbool(image, baton->bandBoolOp);
@ -1007,11 +1052,8 @@ class PipelineWorker : public AsyncWorker {
}
// Dispose of Persistent wrapper around input Buffers so they can be garbage collected
if (baton->bufferInLength > 0) {
GetFromPersistent("bufferIn");
}
if (baton->overlayBufferInLength > 0) {
GetFromPersistent("overlayBufferIn");
for (const BufferContainer buf : saveBuffers) {
GetFromPersistent(buf.name.c_str());
}
delete baton;
@ -1028,6 +1070,7 @@ class PipelineWorker : public AsyncWorker {
private:
PipelineBaton *baton;
Callback *queueListener;
std::vector<BufferContainer> saveBuffers;
/*
Calculate the angle of rotation and need-to-flip for the output image.
@ -1155,6 +1198,14 @@ NAN_METHOD(pipeline) {
baton->overlayYOffset = attrAs<int32_t>(options, "overlayYOffset");
baton->overlayTile = attrAs<bool>(options, "overlayTile");
baton->overlayCutout = attrAs<bool>(options, "overlayCutout");
// Boolean options
baton->booleanFileIn = attrAsStr(options, "booleanFileIn");
Local<Object> booleanBufferIn;
if (node::Buffer::HasInstance(Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked())) {
booleanBufferIn = Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked().As<Object>();
baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn);
baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn);
}
// Resize options
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
baton->crop = attrAs<int32_t>(options, "crop");
@ -1232,14 +1283,10 @@ NAN_METHOD(pipeline) {
}
}
// Bandbool operation
std::string opStr = attrAsStr(options, "bandBoolOp");
if(opStr == "and" ) {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_AND;
} else if(opStr == "or") {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_OR;
} else if(opStr == "eor") {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_EOR;
}
baton->bandBoolOp = GetBooleanOperation(attrAsStr(options, "bandBoolOp"));
// Boolean operation
baton->booleanOp = GetBooleanOperation(attrAsStr(options, "booleanOp"));
// Function to notify of queue length changes
Callback *queueListener = new Callback(
@ -1248,7 +1295,15 @@ NAN_METHOD(pipeline) {
// Join queue for worker thread
Callback *callback = new Callback(info[1].As<Function>());
AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn, overlayBufferIn));
std::vector<BufferContainer> saveBuffers;
if (baton->bufferInLength)
saveBuffers.push_back({"bufferIn", bufferIn});
if (baton->overlayBufferInLength)
saveBuffers.push_back({"overlayBufferIn", overlayBufferIn});
if (baton->booleanBufferInLength)
saveBuffers.push_back({"booleanBufferIn", booleanBufferIn});
AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, saveBuffers));
// Increment queued task counter
g_atomic_int_inc(&counterQueue);

View File

@ -39,6 +39,9 @@ struct PipelineBaton {
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
std::string booleanFileIn;
char *booleanBufferIn;
size_t booleanBufferInLength;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
@ -94,6 +97,7 @@ struct PipelineBaton {
double convKernelScale;
double convKernelOffset;
VipsOperationBoolean bandBoolOp;
VipsOperationBoolean booleanOp;
int extractChannel;
int tileSize;
int tileOverlap;
@ -116,6 +120,7 @@ struct PipelineBaton {
overlayYOffset(-1),
overlayTile(false),
overlayCutout(false),
booleanBufferInLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
@ -156,6 +161,7 @@ struct PipelineBaton {
convKernelScale(0.0),
convKernelOffset(0.0),
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
extractChannel(-1),
tileSize(256),
tileOverlap(0),

BIN
test/fixtures/booleanTest.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -95,6 +95,8 @@ module.exports = {
inputPngStripesV: getPath('stripesV.png'),
inputPngStripesH: getPath('stripesH.png'),
inputJpgBooleanTest: getPath('booleanTest.jpg'),
inputV: getPath('vfile.v'),
outputJpg: getPath('output.jpg'),

103
test/unit/boolean.js Normal file
View File

@ -0,0 +1,103 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var fixtures = require('../fixtures');
var sharp = require('../../index');
describe('Boolean operation between two images', function() {
it('\'and\' Operation, file', function(done) {
sharp(fixtures.inputJpg) //fixtures.inputJpg
.resize(320,240)
.boolean(fixtures.inputJpgBooleanTest, 'and')
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_and_result.jpg'), data, done);
});
});
it('\'and\' Operation, buffer', function(done) {
sharp(fixtures.inputJpg) //fixtures.inputJpg
.resize(320,240)
.boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), sharp.bool.and)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_and_result.jpg'), data, done);
});
});
it('\'or\' Operation, file', function(done) {
sharp(fixtures.inputJpg)
.resize(320,240)
.boolean(fixtures.inputJpgBooleanTest, sharp.bool.or)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_or_result.jpg'), data, done);
});
});
it('\'or\' Operation, buffer', function(done) {
sharp(fixtures.inputJpg)
.resize(320,240)
.boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), 'or')
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_or_result.jpg'), data, done);
});
});
it('\'eor\' Operation, file', function(done) {
sharp(fixtures.inputJpg)
.resize(320,240)
.boolean(fixtures.inputJpgBooleanTest, 'eor')
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_eor_result.jpg'), data, done);
});
});
it('\'eor\' Operation, buffer', function(done) {
sharp(fixtures.inputJpg)
.resize(320,240)
.boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), sharp.bool.eor)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fixtures.assertSimilar(fixtures.expected('boolean_eor_result.jpg'), data, done);
});
});
it('Invalid operation', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), 'fail');
});
});
it('Invalid operation, non-string', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), null);
});
});
if('Invalid buffer input', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.resize(320,240)
.boolean([],'eor');
});
});
});