Skip to content

Commit 18b9991

Browse files
committed
Add experimental 'attention' crop strategy
1 parent 739178d commit 18b9991

21 files changed

Lines changed: 438 additions & 51 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ coverage
44
test/bench/node_modules
55
test/fixtures/output*
66
test/leak/libvips.supp
7+
test/saliency/report.json
8+
test/saliency/Image*
9+
test/saliency/[Uu]serData*
10+
!test/saliency/userData.js
711
lib
812
include
913
packaging/libvips*

.jshintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
test/bench/node_modules
3+
test/saliency/humanae/node_modules
34
coverage

docs/api.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,11 @@ Possible attributes of `sharp.gravity` are
175175
`north`, `northeast`, `east`, `southeast`, `south`,
176176
`southwest`, `west`, `northwest`, `center` and `centre`.
177177

178-
Possible attributes of the experimental `sharp.strategy` are:
178+
The experimental strategy-based approach resizes so one dimension is at its target length
179+
then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy.
179180

180-
* `entropy`: resize so one dimension is at its target size
181-
then repeatedly remove pixels from the edge with the lowest
182-
[Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29)
183-
until it too reaches the target size.
181+
* `entropy`: focus on the region with the highest [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29).
182+
* `attention`: focus on the region with the highest luminance frequency, colour saturation and presence of skin tones.
184183

185184
The default crop option is a `center`/`centre` gravity.
186185

docs/changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Requires libvips v8.3.3
88

99
* C++11 ABI version is now auto-detected, remove sharp-cxx11 installation flag.
1010

11+
* Add experimental 'attention' crop strategy.
12+
[#295](https://github.com/lovell/sharp/issues/295)
13+
1114
* Include .node extension for Meteor's require() implementation.
1215
[#537](https://github.com/lovell/sharp/issues/537)
1316
[@isjackwild](https://github.com/isjackwild)

index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ module.exports.gravity = {
260260

261261
// Strategies for automagic behaviour
262262
module.exports.strategy = {
263-
entropy: 16
263+
entropy: 16,
264+
attention: 17
264265
};
265266

266267
/*
@@ -277,7 +278,7 @@ Sharp.prototype.crop = function(crop) {
277278
} else if (isString(crop) && isInteger(module.exports.gravity[crop])) {
278279
// Gravity (string)
279280
this.options.crop = module.exports.gravity[crop];
280-
} else if (isInteger(crop) && crop === module.exports.strategy.entropy) {
281+
} else if (isInteger(crop) && crop >= module.exports.strategy.entropy) {
281282
// Strategy
282283
this.options.crop = crop;
283284
} else {

src/operations.cc

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <algorithm>
2-
#include <tuple>
2+
#include <functional>
33
#include <memory>
4+
#include <tuple>
45
#include <vips/vips8>
56

67
#include "common.h"
@@ -289,69 +290,104 @@ namespace sharp {
289290
}
290291
}
291292

293+
/*
294+
Calculate the Shannon entropy
295+
*/
296+
double EntropyStrategy::operator()(VImage image) {
297+
return image.hist_find().hist_entropy();
298+
}
299+
300+
/*
301+
Calculate the intensity of edges, skin tone and saturation
302+
*/
303+
double AttentionStrategy::operator()(VImage image) {
304+
// Convert to LAB colourspace
305+
VImage lab = image.colourspace(VIPS_INTERPRETATION_LAB);
306+
VImage l = lab[0];
307+
VImage a = lab[1];
308+
VImage b = lab[2];
309+
// Edge detect luminosity with the Sobel operator
310+
VImage sobel = vips::VImage::new_matrixv(3, 3,
311+
-1.0, 0.0, 1.0,
312+
-2.0, 0.0, 2.0,
313+
-1.0, 0.0, 1.0);
314+
VImage edges = l.conv(sobel).abs() + l.conv(sobel.rot90()).abs();
315+
// Skin tone chroma thresholds trained with http://humanae.tumblr.com/
316+
VImage skin = (a >= 3) & (a <= 22) & (b >= 4) & (b <= 31);
317+
// Chroma >~50% saturation
318+
VImage lch = lab.colourspace(VIPS_INTERPRETATION_LCH);
319+
VImage c = lch[1];
320+
VImage saturation = c > 60;
321+
// Find maximum in combined saliency mask
322+
VImage mask = edges + skin + saturation;
323+
return mask.max();
324+
}
325+
292326
/*
293327
Calculate crop area based on image entropy
294328
*/
295-
std::tuple<int, int> EntropyCrop(VImage image, int const outWidth, int const outHeight) {
329+
std::tuple<int, int> Crop(
330+
VImage image, int const outWidth, int const outHeight, std::function<double(VImage)> strategy
331+
) {
296332
int left = 0;
297333
int top = 0;
298334
int const inWidth = image.width();
299335
int const inHeight = image.height();
300336
if (inWidth > outWidth) {
301-
// Reduce width by repeated removing slices from edge with lowest entropy
337+
// Reduce width by repeated removing slices from edge with lowest score
302338
int width = inWidth;
303-
double leftEntropy = 0.0;
304-
double rightEntropy = 0.0;
339+
double leftScore = 0.0;
340+
double rightScore = 0.0;
305341
// Max width of each slice
306342
int const maxSliceWidth = static_cast<int>(ceil((inWidth - outWidth) / 8.0));
307343
while (width > outWidth) {
308344
// Width of current slice
309345
int const slice = std::min(width - outWidth, maxSliceWidth);
310-
if (leftEntropy == 0.0) {
311-
// Update entropy of left slice
312-
leftEntropy = Entropy(image.extract_area(left, 0, slice, inHeight));
346+
if (leftScore == 0.0) {
347+
// Update score of left slice
348+
leftScore = strategy(image.extract_area(left, 0, slice, inHeight));
313349
}
314-
if (rightEntropy == 0.0) {
315-
// Update entropy of right slice
316-
rightEntropy = Entropy(image.extract_area(width - slice - 1, 0, slice, inHeight));
350+
if (rightScore == 0.0) {
351+
// Update score of right slice
352+
rightScore = strategy(image.extract_area(width - slice - 1, 0, slice, inHeight));
317353
}
318-
// Keep slice with highest entropy
319-
if (leftEntropy >= rightEntropy) {
354+
// Keep slice with highest score
355+
if (leftScore >= rightScore) {
320356
// Discard right slice
321-
rightEntropy = 0.0;
357+
rightScore = 0.0;
322358
} else {
323359
// Discard left slice
324-
leftEntropy = 0.0;
360+
leftScore = 0.0;
325361
left = left + slice;
326362
}
327363
width = width - slice;
328364
}
329365
}
330366
if (inHeight > outHeight) {
331-
// Reduce height by repeated removing slices from edge with lowest entropy
367+
// Reduce height by repeated removing slices from edge with lowest score
332368
int height = inHeight;
333-
double topEntropy = 0.0;
334-
double bottomEntropy = 0.0;
369+
double topScore = 0.0;
370+
double bottomScore = 0.0;
335371
// Max height of each slice
336372
int const maxSliceHeight = static_cast<int>(ceil((inHeight - outHeight) / 8.0));
337373
while (height > outHeight) {
338374
// Height of current slice
339375
int const slice = std::min(height - outHeight, maxSliceHeight);
340-
if (topEntropy == 0.0) {
341-
// Update entropy of top slice
342-
topEntropy = Entropy(image.extract_area(0, top, inWidth, slice));
376+
if (topScore == 0.0) {
377+
// Update score of top slice
378+
topScore = strategy(image.extract_area(0, top, inWidth, slice));
343379
}
344-
if (bottomEntropy == 0.0) {
345-
// Update entropy of bottom slice
346-
bottomEntropy = Entropy(image.extract_area(0, height - slice - 1, inWidth, slice));
380+
if (bottomScore == 0.0) {
381+
// Update score of bottom slice
382+
bottomScore = strategy(image.extract_area(0, height - slice - 1, inWidth, slice));
347383
}
348-
// Keep slice with highest entropy
349-
if (topEntropy >= bottomEntropy) {
384+
// Keep slice with highest score
385+
if (topScore >= bottomScore) {
350386
// Discard bottom slice
351-
bottomEntropy = 0.0;
387+
bottomScore = 0.0;
352388
} else {
353389
// Discard top slice
354-
topEntropy = 0.0;
390+
topScore = 0.0;
355391
top = top + slice;
356392
}
357393
height = height - slice;
@@ -360,13 +396,6 @@ namespace sharp {
360396
return std::make_tuple(left, top);
361397
}
362398

363-
/*
364-
Calculate the Shannon entropy for an image
365-
*/
366-
double Entropy(VImage image) {
367-
return image.hist_find().hist_entropy();
368-
}
369-
370399
/*
371400
Insert a tile cache to prevent over-computation of any previous operations in the pipeline
372401
*/

src/operations.h

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#ifndef SRC_OPERATIONS_H_
22
#define SRC_OPERATIONS_H_
33

4-
#include <tuple>
4+
#include <algorithm>
5+
#include <functional>
56
#include <memory>
7+
#include <tuple>
68
#include <vips/vips8>
79

810
using vips::VImage;
@@ -63,14 +65,21 @@ namespace sharp {
6365
VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged);
6466

6567
/*
66-
Calculate crop area based on image entropy
68+
Crop strategy functors
6769
*/
68-
std::tuple<int, int> EntropyCrop(VImage image, int const outWidth, int const outHeight);
70+
struct EntropyStrategy {
71+
double operator()(VImage image);
72+
};
73+
struct AttentionStrategy {
74+
double operator()(VImage image);
75+
};
6976

7077
/*
71-
Calculate the Shannon entropy for an image
78+
Calculate crop area based on given strategy (Entropy, Attention)
7279
*/
73-
double Entropy(VImage image);
80+
std::tuple<int, int> Crop(
81+
VImage image, int const outWidth, int const outHeight, std::function<double(VImage)> strategy
82+
);
7483

7584
/*
7685
Insert a tile cache to prevent over-computation of any previous operations in the pipeline

src/pipeline.cc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,13 +488,18 @@ class PipelineWorker : public Nan::AsyncWorker {
488488
std::tie(left, top) = sharp::CalculateCrop(
489489
image.width(), image.height(), baton->width, baton->height, baton->crop
490490
);
491-
} else {
491+
} else if (baton->crop == 16) {
492492
// Entropy-based crop
493-
std::tie(left, top) = sharp::EntropyCrop(image, baton->width, baton->height);
493+
std::tie(left, top) = sharp::Crop(image, baton->width, baton->height, sharp::EntropyStrategy());
494+
} else {
495+
// Attention-based crop
496+
std::tie(left, top) = sharp::Crop(image, baton->width, baton->height, sharp::AttentionStrategy());
494497
}
495498
int width = std::min(image.width(), baton->width);
496499
int height = std::min(image.height(), baton->height);
497500
image = image.extract_area(left, top, width, height);
501+
baton->cropCalcLeft = left;
502+
baton->cropCalcTop = top;
498503
}
499504
}
500505

@@ -890,6 +895,10 @@ class PipelineWorker : public Nan::AsyncWorker {
890895
Set(info, New("width").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(width)));
891896
Set(info, New("height").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(height)));
892897
Set(info, New("channels").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->channels)));
898+
if (baton->cropCalcLeft != -1 && baton->cropCalcLeft != -1) {
899+
Set(info, New("cropCalcLeft").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcLeft)));
900+
Set(info, New("cropCalcTop").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcTop)));
901+
}
893902

894903
if (baton->bufferOutLength > 0) {
895904
// Pass ownership of output data to Buffer instance

src/pipeline.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ struct PipelineBaton {
4646
int channels;
4747
Canvas canvas;
4848
int crop;
49+
int cropCalcLeft;
50+
int cropCalcTop;
4951
std::string kernel;
5052
std::string interpolator;
5153
double background[4];
@@ -112,6 +114,8 @@ struct PipelineBaton {
112114
channels(0),
113115
canvas(Canvas::CROP),
114116
crop(0),
117+
cropCalcLeft(-1),
118+
cropCalcTop(-1),
115119
flatten(false),
116120
negate(false),
117121
blurSigma(0.0),

0 commit comments

Comments
 (0)