Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clahe operator #2726

Merged
merged 2 commits into from
May 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions docs/api-operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,30 @@ Alternative spelling of normalise.

Returns **Sharp**

## clahe

Perform contrast limiting adaptive histogram equalization (CLAHE)

This will, in general, enhance the clarity of the image by bringing out
darker details. Please read more about CLAHE here:
[https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE][8]

### Parameters

* `options` **[Object][2]**

* `options.width` **[number][1]** integer width of the region in pixels.
* `options.height` **[number][1]** integer height of the region in pixels.
* `options.maxSlope` **[number][1]** maximum value for the slope of the
cumulative histogram. A value of 0 disables contrast limiting. Valid values
are integers in the range 0-100 (inclusive) (optional, default `3`)

<!---->

* Throws **[Error][5]** Invalid parameters

Returns **Sharp**

## convolve

Convolve the image with the specified kernel.
Expand All @@ -252,7 +276,7 @@ Convolve the image with the specified kernel.
* `kernel` **[Object][2]**

* `kernel.width` **[number][1]** width of the kernel in pixels.
* `kernel.height` **[number][1]** width of the kernel in pixels.
* `kernel.height` **[number][1]** height of the kernel in pixels.
* `kernel.kernel` **[Array][7]<[number][1]>** Array of length `width*height` containing the kernel values.
* `kernel.scale` **[number][1]** the scale of the kernel in pixels. (optional, default `sum`)
* `kernel.offset` **[number][1]** the offset of the kernel in pixels. (optional, default `0`)
Expand Down Expand Up @@ -304,7 +328,7 @@ the selected bitwise boolean `operation` between the corresponding pixels of the

### Parameters

* `operand` **([Buffer][8] | [string][3])** Buffer containing image data or string containing the path to an image file.
* `operand` **([Buffer][9] | [string][3])** Buffer containing image data or string containing the path to an image file.
* `operator` **[string][3]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
* `options` **[Object][2]?**

Expand Down Expand Up @@ -421,4 +445,6 @@ Returns **Sharp**

[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

[8]: https://nodejs.org/api/buffer.html
[8]: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE

[9]: https://nodejs.org/api/buffer.html
2 changes: 1 addition & 1 deletion docs/search-index.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ const Sharp = function (input, options) {
gammaOut: 0,
greyscale: false,
normalise: false,
claheWidth: 0,
claheHeight: 0,
claheMaxSlope: 3,
brightness: 1,
saturation: 1,
hue: 0,
Expand Down
43 changes: 42 additions & 1 deletion lib/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,46 @@ function normalize (normalize) {
return this.normalise(normalize);
}

/**
* Perform contrast limiting adaptive histogram equalization (CLAHE)
*
* This will, in general, enhance the clarity of the image by bringing out
* darker details. Please read more about CLAHE here:
* https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE
*
* @param {Object} options
* @param {number} options.width - integer width of the region in pixels.
* @param {number} options.height - integer height of the region in pixels.
* @param {number} [options.maxSlope=3] - maximum value for the slope of the
* cumulative histogram. A value of 0 disables contrast limiting. Valid values
* are integers in the range 0-100 (inclusive)
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function clahe (options) {
if (!is.plainObject(options)) {
throw is.invalidParameterError('options', 'plain object', options);
}
if (!('width' in options) || !is.integer(options.width) || options.width <= 0) {
throw is.invalidParameterError('width', 'integer above zero', options.width);
} else {
this.options.claheWidth = options.width;
}
if (!('height' in options) || !is.integer(options.height) || options.height <= 0) {
throw is.invalidParameterError('height', 'integer above zero', options.height);
} else {
this.options.claheHeight = options.height;
}
if (!is.defined(options.maxSlope)) {
this.options.claheMaxSlope = 3;
} else if (!is.integer(options.maxSlope) || options.maxSlope < 0 || options.maxSlope > 100) {
throw is.invalidParameterError('maxSlope', 'integer 0-100', options.maxSlope);
} else {
this.options.claheMaxSlope = options.maxSlope;
}
return this;
}

/**
* Convolve the image with the specified kernel.
*
Expand All @@ -368,7 +408,7 @@ function normalize (normalize) {
*
* @param {Object} kernel
* @param {number} kernel.width - width of the kernel in pixels.
* @param {number} kernel.height - width of the kernel in pixels.
* @param {number} kernel.height - height of the kernel in pixels.
baparham marked this conversation as resolved.
Show resolved Hide resolved
* @param {Array<number>} kernel.kernel - Array of length `width*height` containing the kernel values.
* @param {number} [kernel.scale=sum] - the scale of the kernel in pixels.
* @param {number} [kernel.offset=0] - the offset of the kernel in pixels.
Expand Down Expand Up @@ -594,6 +634,7 @@ module.exports = function (Sharp) {
negate,
normalise,
normalize,
clahe,
convolve,
threshold,
boolean,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"Leon Radley <leon@radley.se>",
"alza54 <alza54@thiocod.in>",
"Jacob Smith <jacob@frende.me>",
"Michael Nutt <michael@nutt.im>"
"Michael Nutt <michael@nutt.im>",
"Brad Parham <baparham@gmail.com>"
],
"scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
Expand Down
7 changes: 7 additions & 0 deletions src/operations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ namespace sharp {
return image;
}

/*
* Contrast limiting adapative histogram equalization (CLAHE)
*/
VImage Clahe(VImage image, int const width, int const height, int const maxSlope) {
return image.hist_local(width, height, VImage::option()->set("max_slope", maxSlope));
}

/*
* Gamma encoding/decoding
*/
Expand Down
5 changes: 5 additions & 0 deletions src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ namespace sharp {
*/
VImage Normalise(VImage image);

/*
* Contrast limiting adapative histogram equalization (CLAHE)
*/
VImage Clahe(VImage image, int const width, int const height, int const maxSlope);

/*
* Gamma encoding/decoding
*/
Expand Down
9 changes: 9 additions & 0 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ class PipelineWorker : public Napi::AsyncWorker {
bool const shouldApplyMedian = baton->medianSize > 0;
bool const shouldComposite = !baton->composite.empty();
bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
bool const shouldApplyClahe = baton->claheWidth != 0 && baton->claheHeight != 0;

if (shouldComposite && !sharp::HasAlpha(image)) {
image = sharp::EnsureAlpha(image, 1);
Expand Down Expand Up @@ -650,6 +651,11 @@ class PipelineWorker : public Napi::AsyncWorker {
image = sharp::Normalise(image);
}

// Apply contrast limiting adaptive histogram equalization (CLAHE)
if (shouldApplyClahe) {
image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
}

// Apply bitwise boolean operation between images
if (baton->boolean != nullptr) {
VImage booleanImage;
Expand Down Expand Up @@ -1330,6 +1336,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->linearB = sharp::AttrAsDouble(options, "linearB");
baton->greyscale = sharp::AttrAsBool(options, "greyscale");
baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
baton->angle = sharp::AttrAsInt32(options, "angle");
baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ struct PipelineBaton {
double gammaOut;
bool greyscale;
bool normalise;
int claheWidth;
int claheHeight;
int claheMaxSlope;
bool useExifOrientation;
int angle;
double rotationAngle;
Expand Down Expand Up @@ -234,6 +237,9 @@ struct PipelineBaton {
gamma(0.0),
greyscale(false),
normalise(false),
claheWidth(0),
claheHeight(0),
claheMaxSlope(3),
useExifOrientation(false),
angle(0),
rotationAngle(0.0),
Expand Down
Binary file added test/fixtures/concert.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-100-100-0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-100-50-3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-11-25-14.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-5-5-0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-5-5-5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-50-50-0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/clahe-50-50-14.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ module.exports = {

inputV: getPath('vfile.v'),

inputJpgClahe: getPath('concert.jpg'), // public domain - https://www.flickr.com/photos/mars_/14389236779/

testPattern: getPath('test-pattern.png'),

// Path for tests requiring human inspection
Expand Down
139 changes: 139 additions & 0 deletions test/unit/clahe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict';

const assert = require('assert');

const sharp = require('../../lib');
const fixtures = require('../fixtures');

describe('Clahe', function () {
it('width 5 width 5 maxSlope 0', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 5, height: 5, maxSlope: 0 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-5-5-0.jpg'), data, { threshold: 10 }, done);
});
});

it('width 5 width 5 maxSlope 5', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 5, height: 5, maxSlope: 5 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-5-5-5.jpg'), data, done);
});
});

it('width 11 width 25 maxSlope 14', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 11, height: 25, maxSlope: 14 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-11-25-14.jpg'), data, done);
});
});

it('width 50 width 50 maxSlope 0', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 50, height: 50, maxSlope: 0 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-50-50-0.jpg'), data, done);
});
});

it('width 50 width 50 maxSlope 14', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 50, height: 50, maxSlope: 14 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-50-50-14.jpg'), data, done);
});
});

it('width 100 width 50 maxSlope 3', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 100, height: 50, maxSlope: 3 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-100-50-3.jpg'), data, done);
});
});

it('width 100 width 100 maxSlope 0', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 100, height: 100, maxSlope: 0 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-100-100-0.jpg'), data, done);
});
});

it('invalid maxSlope', function () {
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 100, maxSlope: -5 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 100, maxSlope: 110 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 100, maxSlope: 5.5 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 100, maxSlope: 'a string' });
});
});

it('invalid width', function () {
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100.5, height: 100 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: -5, height: 100 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: true, height: 100 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 'string test', height: 100 });
});
});

it('invalid height', function () {
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 100.5 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: -5 });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: true });
});
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe({ width: 100, height: 'string test' });
});
});

it('invalid options object', function () {
assert.throws(function () {
sharp(fixtures.inputJpgClahe).clahe(100, 100, 5);
});
});

it('uses default maxSlope of 3', function (done) {
sharp(fixtures.inputJpgClahe)
.clahe({ width: 100, height: 50 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
fixtures.assertSimilar(fixtures.expected('clahe-100-50-3.jpg'), data, done);
});
});
});