mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 02:30:12 +02:00
Add normalize() for simple histogram stretching
Available as normalize() or normalise(). Normalization takes place in LAB place and thus should not change any colors. Existing alpha channels are preserved untouched by normalization.
This commit is contained in:
parent
ba034a8164
commit
dce36e0074
@ -486,6 +486,10 @@ This is a linear operation. If the input image is in a non-linear colour space s
|
||||
|
||||
The output image will still be web-friendly sRGB and contain three (identical) channels.
|
||||
|
||||
#### normalize() / normalise()
|
||||
|
||||
Stretch histogram to cover full dynamic range before output to enhance contrast.
|
||||
|
||||
### Output options
|
||||
|
||||
#### jpeg()
|
||||
|
10
index.js
10
index.js
@ -59,6 +59,7 @@ var Sharp = function(input) {
|
||||
sharpenJagged: 2,
|
||||
gamma: 0,
|
||||
greyscale: false,
|
||||
normalize: 0,
|
||||
// output options
|
||||
output: '__input',
|
||||
progressive: false,
|
||||
@ -333,6 +334,15 @@ Sharp.prototype.gamma = function(gamma) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Normalize histogram
|
||||
*/
|
||||
Sharp.prototype.normalize = function(normalize) {
|
||||
this.options.normalize = (typeof normalize === 'boolean') ? normalize : true;
|
||||
return this;
|
||||
};
|
||||
Sharp.prototype.normalise = Sharp.prototype.normalize;
|
||||
|
||||
/*
|
||||
Convert to greyscale
|
||||
*/
|
||||
|
@ -15,7 +15,8 @@
|
||||
"Maurus Cuelenaere <mcuelenaere@gmail.com>",
|
||||
"Linus Unnebäck <linus@folkdatorn.se>",
|
||||
"Victor Mateevitsi <mvictoras@gmail.com>",
|
||||
"Alaric Holloway <alaric.holloway@gmail.com>"
|
||||
"Alaric Holloway <alaric.holloway@gmail.com>",
|
||||
"Bernhard K. Weisshuhn <bkw@codingforce.com>"
|
||||
],
|
||||
"description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library",
|
||||
"scripts": {
|
||||
|
@ -83,6 +83,7 @@ struct ResizeBaton {
|
||||
double sharpenJagged;
|
||||
double gamma;
|
||||
bool greyscale;
|
||||
bool normalize;
|
||||
int angle;
|
||||
bool rotateBeforePreExtract;
|
||||
bool flip;
|
||||
@ -115,6 +116,7 @@ struct ResizeBaton {
|
||||
sharpenJagged(2.0),
|
||||
gamma(0.0),
|
||||
greyscale(false),
|
||||
normalize(false),
|
||||
angle(0),
|
||||
flip(false),
|
||||
flop(false),
|
||||
@ -694,6 +696,86 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
image = gammaDecoded;
|
||||
}
|
||||
|
||||
// Apply normalization
|
||||
if (baton->normalize) {
|
||||
VipsInterpretation typeBeforeNormalize = image->Type;
|
||||
if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
|
||||
typeBeforeNormalize = VIPS_INTERPRETATION_sRGB;
|
||||
}
|
||||
|
||||
// normalize the luminance band in LAB space:
|
||||
VipsImage *lab;
|
||||
if (vips_colourspace(image, &lab, VIPS_INTERPRETATION_LAB, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, lab);
|
||||
|
||||
VipsImage *luminance;
|
||||
if (vips_extract_band(lab, &luminance, 0, "n", 1, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, luminance);
|
||||
|
||||
VipsImage *chroma;
|
||||
if (vips_extract_band(lab, &chroma, 1, "n", 2, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, chroma);
|
||||
|
||||
VipsImage *stats;
|
||||
if (vips_stats(luminance, &stats, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, stats);
|
||||
double min = *VIPS_MATRIX(stats, 0, 0);
|
||||
double max = *VIPS_MATRIX(stats, 1, 0);
|
||||
|
||||
VipsImage *normalized;
|
||||
if (min == max) {
|
||||
// Range of zero: create black image
|
||||
if (vips_black(&normalized, image->Xsize, image->Ysize, "bands", 1, NULL )) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, normalized);
|
||||
} else {
|
||||
double f = 100.0 / (max - min);
|
||||
double a = -(min * f);
|
||||
|
||||
VipsImage *luminance100;
|
||||
if (vips_linear1(luminance, &luminance100, f, a, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, luminance100);
|
||||
|
||||
VipsImage *normalizedLab;
|
||||
if (vips_bandjoin2(luminance100, chroma, &normalizedLab, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, normalizedLab);
|
||||
if (vips_colourspace(normalizedLab, &normalized, typeBeforeNormalize, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, normalized);
|
||||
}
|
||||
|
||||
if (HasAlpha(image)) {
|
||||
VipsImage *alpha;
|
||||
if (vips_extract_band(image, &alpha, image->Bands - 1, "n", 1, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, alpha);
|
||||
|
||||
VipsImage *normalizedAlpha;
|
||||
if (vips_bandjoin2(normalized, alpha, &normalizedAlpha, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, normalizedAlpha);
|
||||
image = normalizedAlpha;
|
||||
} else {
|
||||
image = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert image to sRGB, if not already
|
||||
if (image->Type != VIPS_INTERPRETATION_sRGB) {
|
||||
// Switch intrepretation to sRGB
|
||||
@ -762,10 +844,10 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
|
||||
} else if (baton->output == "__raw") {
|
||||
// Write raw, uncompressed image data to buffer
|
||||
if (baton->greyscale) {
|
||||
if (baton->greyscale || image->Type == VIPS_INTERPRETATION_B_W) {
|
||||
// Extract first band for greyscale image
|
||||
VipsImage *grey;
|
||||
if (vips_extract_band(image, &grey, 1, NULL)) {
|
||||
if (vips_extract_band(image, &grey, 0, NULL)) {
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, grey);
|
||||
@ -1082,6 +1164,7 @@ NAN_METHOD(resize) {
|
||||
baton->sharpenJagged = options->Get(NanNew<String>("sharpenJagged"))->NumberValue();
|
||||
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
|
||||
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
|
||||
baton->normalize = options->Get(NanNew<String>("normalize"))->BooleanValue();
|
||||
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
|
||||
baton->rotateBeforePreExtract = options->Get(NanNew<String>("rotateBeforePreExtract"))->BooleanValue();
|
||||
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
|
||||
|
BIN
test/fixtures/2x2_fdcce6.png
vendored
Normal file
BIN
test/fixtures/2x2_fdcce6.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 B |
BIN
test/fixtures/grey-8bit-alpha.png
vendored
Normal file
BIN
test/fixtures/grey-8bit-alpha.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
3
test/fixtures/index.js
vendored
3
test/fixtures/index.js
vendored
@ -15,9 +15,12 @@ module.exports = {
|
||||
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
|
||||
inputJpgWithCmykNoProfile: getPath('Channel_digital_image_CMYK_color_no_profile.jpg'),
|
||||
inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'),
|
||||
inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
|
||||
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
||||
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
|
||||
inputPngWithOneColor: getPath('2x2_fdcce6.png'),
|
||||
|
||||
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
||||
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||
|
BIN
test/fixtures/low-contrast.jpg
vendored
Normal file
BIN
test/fixtures/low-contrast.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
136
test/unit/normalize.js
Executable file
136
test/unit/normalize.js
Executable file
@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
|
||||
sharp.cache(0);
|
||||
|
||||
describe('Normalization', function () {
|
||||
|
||||
it('uses the same prototype for both spellings', function () {
|
||||
assert.strictEqual(sharp.prototype.normalize, sharp.prototype.normalise);
|
||||
});
|
||||
|
||||
it('spreads rgb image values between 0 and 255', function(done) {
|
||||
sharp(fixtures.inputJpgWithLowContrast)
|
||||
.normalize()
|
||||
.raw()
|
||||
.toBuffer(function (err, data, info) {
|
||||
if (err) throw err;
|
||||
var min = 255, max = 0, i;
|
||||
for (i = 0; i < data.length; i += 3) {
|
||||
min = Math.min(min, data[i], data[i + 1], data[i + 2]);
|
||||
max = Math.max(max, data[i], data[i + 1], data[i + 2]);
|
||||
}
|
||||
assert.strictEqual(0, min);
|
||||
assert.strictEqual(255, max);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('spreads grayscaled image values between 0 and 255', function(done) {
|
||||
sharp(fixtures.inputJpgWithLowContrast)
|
||||
.gamma()
|
||||
.greyscale()
|
||||
.normalize()
|
||||
.raw()
|
||||
.toBuffer(function (err, data, info) {
|
||||
if (err) throw err;
|
||||
var min = 255, max = 0, i;
|
||||
for (i = 0; i < data.length; i++) {
|
||||
min = Math.min(min, data[i]);
|
||||
max = Math.max(max, data[i]);
|
||||
}
|
||||
assert.strictEqual(0, min);
|
||||
assert.strictEqual(255, max);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('stretches greyscale images with alpha channel', function (done) {
|
||||
sharp(fixtures.inputPngWithGreyAlpha)
|
||||
.normalize()
|
||||
.raw()
|
||||
.toBuffer(function (err, data, info) {
|
||||
// raw toBuffer does not return the alpha channel (yet?)
|
||||
var min = 255, max = 0, i;
|
||||
for (i = 0; i < data.length; i++) {
|
||||
min = Math.min(min, data[i]);
|
||||
max = Math.max(max, data[i]);
|
||||
}
|
||||
assert.strictEqual(0, min);
|
||||
assert.strictEqual(255, max);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps an existing alpha channel', function (done) {
|
||||
sharp(fixtures.inputPngWithTransparency)
|
||||
.normalize()
|
||||
.toBuffer(function (err, data, info) {
|
||||
sharp(data)
|
||||
.metadata()
|
||||
.then(function (metadata) {
|
||||
assert.strictEqual(4, metadata.channels);
|
||||
assert.strictEqual(true, metadata.hasAlpha);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the alpha channel of greyscale images intact', function (done) {
|
||||
sharp(fixtures.inputPngWithGreyAlpha)
|
||||
.normalize()
|
||||
.toBuffer(function (err, data, info) {
|
||||
sharp(data)
|
||||
.metadata()
|
||||
.then(function (metadata) {
|
||||
assert.strictEqual(true, metadata.hasAlpha);
|
||||
// because of complications with greyscale
|
||||
// we return everything in srgb for now.
|
||||
//
|
||||
// assert.strictEqual(2, metadata.channels);
|
||||
// assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(4, metadata.channels);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a black image for images with only one color', function (done) {
|
||||
sharp(fixtures.inputPngWithOneColor)
|
||||
.normalize()
|
||||
.toBuffer()
|
||||
.bind({})
|
||||
.then(function (imageData) {
|
||||
this.imageData = imageData;
|
||||
return sharp(imageData)
|
||||
.metadata();
|
||||
})
|
||||
.then(function (metadata) {
|
||||
assert.strictEqual(false, metadata.hasAlpha);
|
||||
// because of complications with greyscale
|
||||
// we return everything in srgb for now.
|
||||
//
|
||||
// assert.strictEqual(1, metadata.channels);
|
||||
// assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
})
|
||||
.then(function () {
|
||||
return sharp(this.imageData)
|
||||
.raw()
|
||||
.toBuffer();
|
||||
})
|
||||
.then(function (rawData) {
|
||||
// var blackBuffer = new Buffer([0,0,0,0]);
|
||||
var blackBuffer = new Buffer([0,0,0, 0,0,0, 0,0,0, 0,0,0]);
|
||||
assert.strictEqual(blackBuffer.toString(), rawData.toString());
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user