Merge pull request #194 from bkw/normalize

Add normalize() to use full luminance range.
This commit is contained in:
Lovell Fuller 2015-04-19 16:05:20 +01:00
commit be39297f3b
9 changed files with 240 additions and 3 deletions

View File

@ -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()

View File

@ -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
*/

View File

@ -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": {

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

BIN
test/fixtures/grey-8bit-alpha.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

136
test/unit/normalize.js Executable file
View 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);
});
});