mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add support to normalise for lower and upper percentiles (#3583)
This commit is contained in:
parent
1eefd4e562
commit
d7776e3b98
@ -235,6 +235,8 @@ const Sharp = function (input, options) {
|
|||||||
gammaOut: 0,
|
gammaOut: 0,
|
||||||
greyscale: false,
|
greyscale: false,
|
||||||
normalise: false,
|
normalise: false,
|
||||||
|
normaliseLower: 1,
|
||||||
|
normaliseUpper: 99,
|
||||||
claheWidth: 0,
|
claheWidth: 0,
|
||||||
claheHeight: 0,
|
claheHeight: 0,
|
||||||
claheMaxSlope: 3,
|
claheMaxSlope: 3,
|
||||||
|
25
lib/index.d.ts
vendored
25
lib/index.d.ts
vendored
@ -447,18 +447,26 @@ declare namespace sharp {
|
|||||||
negate(negate?: boolean | NegateOptions): Sharp;
|
negate(negate?: boolean | NegateOptions): Sharp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhance output image contrast by stretching its luminance to cover the full dynamic range.
|
* Enhance output image contrast by stretching its luminance to cover a full dynamic range.
|
||||||
* @param normalise true to enable and false to disable (defaults to true)
|
*
|
||||||
|
* Uses a histogram-based approach, taking a default range of 1% to 99% to reduce sensitivity to noise at the extremes.
|
||||||
|
*
|
||||||
|
* Luminance values below the `lower` percentile will be underexposed by clipping to zero.
|
||||||
|
* Luminance values above the `upper` percentile will be overexposed by clipping to the max pixel value.
|
||||||
|
*
|
||||||
|
* @param normalise options
|
||||||
|
* @throws {Error} Invalid parameters
|
||||||
* @returns A sharp instance that can be used to chain operations
|
* @returns A sharp instance that can be used to chain operations
|
||||||
*/
|
*/
|
||||||
normalise(normalise?: boolean): Sharp;
|
normalise(normalise?: NormaliseOptions): Sharp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alternative spelling of normalise.
|
* Alternative spelling of normalise.
|
||||||
* @param normalize true to enable and false to disable (defaults to true)
|
* @param normalize options
|
||||||
|
* @throws {Error} Invalid parameters
|
||||||
* @returns A sharp instance that can be used to chain operations
|
* @returns A sharp instance that can be used to chain operations
|
||||||
*/
|
*/
|
||||||
normalize(normalize?: boolean): Sharp;
|
normalize(normalize?: NormaliseOptions): Sharp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform contrast limiting adaptive histogram equalization (CLAHE)
|
* Perform contrast limiting adaptive histogram equalization (CLAHE)
|
||||||
@ -1218,6 +1226,13 @@ declare namespace sharp {
|
|||||||
alpha?: boolean | undefined;
|
alpha?: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NormaliseOptions {
|
||||||
|
/** Percentile below which luminance values will be underexposed. */
|
||||||
|
lower?: number | undefined;
|
||||||
|
/** Percentile above which luminance values will be overexposed. */
|
||||||
|
upper?: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
interface ResizeOptions {
|
interface ResizeOptions {
|
||||||
/** Alternative means of specifying width. If both are present this takes priority. */
|
/** Alternative means of specifying width. If both are present this takes priority. */
|
||||||
width?: number | undefined;
|
width?: number | undefined;
|
||||||
|
@ -469,16 +469,50 @@ function negate (options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhance output image contrast by stretching its luminance to cover the full dynamic range.
|
* Enhance output image contrast by stretching its luminance to cover a full dynamic range.
|
||||||
|
*
|
||||||
|
* Uses a histogram-based approach, taking a default range of 1% to 99% to reduce sensitivity to noise at the extremes.
|
||||||
|
*
|
||||||
|
* Luminance values below the `lower` percentile will be underexposed by clipping to zero.
|
||||||
|
* Luminance values above the `upper` percentile will be overexposed by clipping to the max pixel value.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const output = await sharp(input).normalise().toBuffer();
|
* const output = await sharp(input)
|
||||||
|
* .normalise()
|
||||||
|
* .toBuffer();
|
||||||
*
|
*
|
||||||
* @param {Boolean} [normalise=true]
|
* @example
|
||||||
|
* const output = await sharp(input)
|
||||||
|
* .normalise({ lower: 0, upper: 100 })
|
||||||
|
* .toBuffer();
|
||||||
|
*
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {number} [options.lower=1] - Percentile below which luminance values will be underexposed.
|
||||||
|
* @param {number} [options.upper=99] - Percentile above which luminance values will be overexposed.
|
||||||
* @returns {Sharp}
|
* @returns {Sharp}
|
||||||
*/
|
*/
|
||||||
function normalise (normalise) {
|
function normalise (options) {
|
||||||
this.options.normalise = is.bool(normalise) ? normalise : true;
|
if (is.plainObject(options)) {
|
||||||
|
if (is.defined(options.lower)) {
|
||||||
|
if (is.number(options.lower) && is.inRange(options.lower, 0, 99)) {
|
||||||
|
this.options.normaliseLower = options.lower;
|
||||||
|
} else {
|
||||||
|
throw is.invalidParameterError('lower', 'number between 0 and 99', options.lower);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is.defined(options.upper)) {
|
||||||
|
if (is.number(options.upper) && is.inRange(options.upper, 1, 100)) {
|
||||||
|
this.options.normaliseUpper = options.upper;
|
||||||
|
} else {
|
||||||
|
throw is.invalidParameterError('upper', 'number between 1 and 100', options.upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.options.normaliseLower >= this.options.normaliseUpper) {
|
||||||
|
throw is.invalidParameterError('range', 'lower to be less than upper',
|
||||||
|
`${this.options.normaliseLower} >= ${this.options.normaliseUpper}`);
|
||||||
|
}
|
||||||
|
this.options.normalise = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,13 +520,17 @@ function normalise (normalise) {
|
|||||||
* Alternative spelling of normalise.
|
* Alternative spelling of normalise.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const output = await sharp(input).normalize().toBuffer();
|
* const output = await sharp(input)
|
||||||
|
* .normalize()
|
||||||
|
* .toBuffer();
|
||||||
*
|
*
|
||||||
* @param {Boolean} [normalize=true]
|
* @param {Object} [options]
|
||||||
|
* @param {number} [options.lower=1] - Percentile below which luminance values will be underexposed.
|
||||||
|
* @param {number} [options.upper=99] - Percentile above which luminance values will be overexposed.
|
||||||
* @returns {Sharp}
|
* @returns {Sharp}
|
||||||
*/
|
*/
|
||||||
function normalize (normalize) {
|
function normalize (options) {
|
||||||
return this.normalise(normalize);
|
return this.normalise(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +85,8 @@
|
|||||||
"Brodan <christopher.hranj@gmail.com",
|
"Brodan <christopher.hranj@gmail.com",
|
||||||
"Ankur Parihar <ankur.github@gmail.com>",
|
"Ankur Parihar <ankur.github@gmail.com>",
|
||||||
"Brahim Ait elhaj <brahima@gmail.com>",
|
"Brahim Ait elhaj <brahima@gmail.com>",
|
||||||
"Mart Jansink <m.jansink@gmail.com>"
|
"Mart Jansink <m.jansink@gmail.com>",
|
||||||
|
"Lachlan Newman <lachnewman007@gmail.com>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
|
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <vips/vips8>
|
#include <vips/vips8>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@ -46,7 +45,7 @@ namespace sharp {
|
|||||||
/*
|
/*
|
||||||
* Stretch luminance to cover full dynamic range.
|
* Stretch luminance to cover full dynamic range.
|
||||||
*/
|
*/
|
||||||
VImage Normalise(VImage image) {
|
VImage Normalise(VImage image, int const lower, int const upper) {
|
||||||
// Get original colourspace
|
// Get original colourspace
|
||||||
VipsInterpretation typeBeforeNormalize = image.interpretation();
|
VipsInterpretation typeBeforeNormalize = image.interpretation();
|
||||||
if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
|
if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
|
||||||
@ -56,9 +55,11 @@ namespace sharp {
|
|||||||
VImage lab = image.colourspace(VIPS_INTERPRETATION_LAB);
|
VImage lab = image.colourspace(VIPS_INTERPRETATION_LAB);
|
||||||
// Extract luminance
|
// Extract luminance
|
||||||
VImage luminance = lab[0];
|
VImage luminance = lab[0];
|
||||||
|
|
||||||
// Find luminance range
|
// Find luminance range
|
||||||
int const min = luminance.percent(1);
|
int const min = lower == 0 ? luminance.min() : luminance.percent(lower);
|
||||||
int const max = luminance.percent(99);
|
int const max = upper == 100 ? luminance.max() : luminance.percent(upper);
|
||||||
|
|
||||||
if (std::abs(max - min) > 1) {
|
if (std::abs(max - min) > 1) {
|
||||||
// Extract chroma
|
// Extract chroma
|
||||||
VImage chroma = lab.extract_band(1, VImage::option()->set("n", 2));
|
VImage chroma = lab.extract_band(1, VImage::option()->set("n", 2));
|
||||||
|
@ -22,7 +22,7 @@ namespace sharp {
|
|||||||
/*
|
/*
|
||||||
* Stretch luminance to cover full dynamic range.
|
* Stretch luminance to cover full dynamic range.
|
||||||
*/
|
*/
|
||||||
VImage Normalise(VImage image);
|
VImage Normalise(VImage image, int const lower, int const upper);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Contrast limiting adapative histogram equalization (CLAHE)
|
* Contrast limiting adapative histogram equalization (CLAHE)
|
||||||
|
@ -74,6 +74,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
VipsAngle autoRotation = VIPS_ANGLE_D0;
|
VipsAngle autoRotation = VIPS_ANGLE_D0;
|
||||||
bool autoFlip = FALSE;
|
bool autoFlip = FALSE;
|
||||||
bool autoFlop = FALSE;
|
bool autoFlop = FALSE;
|
||||||
|
|
||||||
if (baton->useExifOrientation) {
|
if (baton->useExifOrientation) {
|
||||||
// Rotate and flip image according to Exif orientation
|
// Rotate and flip image according to Exif orientation
|
||||||
std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
|
std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
|
||||||
@ -682,7 +683,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
|
|
||||||
// Apply normalisation - stretch luminance to cover full dynamic range
|
// Apply normalisation - stretch luminance to cover full dynamic range
|
||||||
if (baton->normalise) {
|
if (baton->normalise) {
|
||||||
image = sharp::Normalise(image);
|
image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply contrast limiting adaptive histogram equalization (CLAHE)
|
// Apply contrast limiting adaptive histogram equalization (CLAHE)
|
||||||
@ -1483,6 +1484,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
|||||||
baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
|
baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
|
||||||
baton->greyscale = sharp::AttrAsBool(options, "greyscale");
|
baton->greyscale = sharp::AttrAsBool(options, "greyscale");
|
||||||
baton->normalise = sharp::AttrAsBool(options, "normalise");
|
baton->normalise = sharp::AttrAsBool(options, "normalise");
|
||||||
|
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
|
||||||
|
baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
|
||||||
baton->tintA = sharp::AttrAsDouble(options, "tintA");
|
baton->tintA = sharp::AttrAsDouble(options, "tintA");
|
||||||
baton->tintB = sharp::AttrAsDouble(options, "tintB");
|
baton->tintB = sharp::AttrAsDouble(options, "tintB");
|
||||||
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
|
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
|
||||||
|
@ -99,6 +99,8 @@ struct PipelineBaton {
|
|||||||
double gammaOut;
|
double gammaOut;
|
||||||
bool greyscale;
|
bool greyscale;
|
||||||
bool normalise;
|
bool normalise;
|
||||||
|
int normaliseLower;
|
||||||
|
int normaliseUpper;
|
||||||
int claheWidth;
|
int claheWidth;
|
||||||
int claheHeight;
|
int claheHeight;
|
||||||
int claheMaxSlope;
|
int claheMaxSlope;
|
||||||
@ -262,6 +264,8 @@ struct PipelineBaton {
|
|||||||
gamma(0.0),
|
gamma(0.0),
|
||||||
greyscale(false),
|
greyscale(false),
|
||||||
normalise(false),
|
normalise(false),
|
||||||
|
normaliseLower(1),
|
||||||
|
normaliseUpper(99),
|
||||||
claheWidth(0),
|
claheWidth(0),
|
||||||
claheHeight(0),
|
claheHeight(0),
|
||||||
claheMaxSlope(3),
|
claheMaxSlope(3),
|
||||||
|
@ -34,7 +34,7 @@ describe('Normalization', function () {
|
|||||||
it('spreads grayscaled image values between 0 and 255', function (done) {
|
it('spreads grayscaled image values between 0 and 255', function (done) {
|
||||||
sharp(fixtures.inputJpgWithLowContrast)
|
sharp(fixtures.inputJpgWithLowContrast)
|
||||||
.greyscale()
|
.greyscale()
|
||||||
.normalize(true)
|
.normalize()
|
||||||
.raw()
|
.raw()
|
||||||
.toBuffer(function (err, data, info) {
|
.toBuffer(function (err, data, info) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
@ -107,4 +107,58 @@ describe('Normalization', function () {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle luminance range', function (done) {
|
||||||
|
sharp(fixtures.inputJpgWithLowContrast)
|
||||||
|
.normalise({ lower: 10, upper: 70 })
|
||||||
|
.raw()
|
||||||
|
.toBuffer(function (err, data, info) {
|
||||||
|
if (err) throw err;
|
||||||
|
assertNormalized(data);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow lower without upper', function () {
|
||||||
|
assert.doesNotThrow(() => sharp().normalize({ lower: 2 }));
|
||||||
|
});
|
||||||
|
it('should allow upper without lower', function () {
|
||||||
|
assert.doesNotThrow(() => sharp().normalize({ upper: 98 }));
|
||||||
|
});
|
||||||
|
it('should throw when lower is out of range', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ lower: -10 }),
|
||||||
|
/Expected number between 0 and 99 for lower but received -10 of type number/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should throw when upper is out of range', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ upper: 110 }),
|
||||||
|
/Expected number between 1 and 100 for upper but received 110 of type number/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should throw when lower is not a number', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ lower: 'fail' }),
|
||||||
|
/Expected number between 0 and 99 for lower but received fail of type string/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should throw when upper is not a number', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ upper: 'fail' }),
|
||||||
|
/Expected number between 1 and 100 for upper but received fail of type string/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should throw when the lower and upper are equal', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ lower: 2, upper: 2 }),
|
||||||
|
/Expected lower to be less than upper for range but received 2 >= 2/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should throw when the lower is greater than upper', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => sharp().normalise({ lower: 3, upper: 2 }),
|
||||||
|
/Expected lower to be less than upper for range but received 3 >= 2/
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user