Skip to content

Commit

Permalink
Add normalize() for simple histogram stretching
Browse files Browse the repository at this point in the history
Available as normalize() or normalise().
Normalization takes place in LAB place, to the resulting sRGB image is
not guaranteed to cover exactly 0-255, but it should be close enough.

Existing alpha channels are preserved untouched by normalization.

A possible side effect of this commit is that input grayscale images
are no longer forced to sRGB on output.
  • Loading branch information
bkw committed Apr 16, 2015
1 parent ba034a8 commit 3e52a84
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var Sharp = function(input) {
sharpenJagged: 2,
gamma: 0,
greyscale: false,
normalize: 0,
// output options
output: '__input',
progressive: false,
Expand Down Expand Up @@ -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
*/
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
81 changes: 78 additions & 3 deletions src/resize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct ResizeBaton {
double sharpenJagged;
double gamma;
bool greyscale;
bool normalize;
int angle;
bool rotateBeforePreExtract;
bool flip;
Expand Down Expand Up @@ -115,6 +116,7 @@ struct ResizeBaton {
sharpenJagged(2.0),
gamma(0.0),
greyscale(false),
normalize(false),
angle(0),
flip(false),
flop(false),
Expand Down Expand Up @@ -694,8 +696,80 @@ 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);
double f = 100.0 / (max - min);
double a = -(min * f) + 0.5;

VipsImage *luminance100;
if (vips_linear1(luminance, &luminance100, f, a, NULL)) {
return Error();
}
vips_object_local(hook, luminance100);

VipsImage *normalized;
if (vips_bandjoin2(luminance100, chroma, &normalized, NULL)) {
return Error();
}
vips_object_local(hook, normalized);

VipsImage *origFormat;
if (vips_colourspace(normalized, &origFormat, typeBeforeNormalize, NULL)) {
return Error();
}
vips_object_local(hook, origFormat);

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(origFormat, alpha, &normalizedAlpha, NULL)) {
return Error();
}
vips_object_local(hook, normalizedAlpha);
image = normalizedAlpha;
} else {
image = origFormat;
}
}

// Convert image to sRGB, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) {
if (image->Type != VIPS_INTERPRETATION_sRGB && image->Type != VIPS_INTERPRETATION_B_W) {
// Switch intrepretation to sRGB
VipsImage *rgb;
if (vips_colourspace(image, &rgb, VIPS_INTERPRETATION_sRGB, NULL)) {
Expand Down Expand Up @@ -762,10 +836,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);
Expand Down Expand Up @@ -1082,6 +1156,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();
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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
Expand Down
Binary file added test/fixtures/low-contrast.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions test/unit/normalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

var assert = require('assert');

var sharp = require('../../index');
var fixtures = require('../fixtures');

sharp.cache(0);

describe('Normalization', function () {
it('spreads grayscale 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(2, min);
assert.strictEqual(255, max);
return done();
});
});

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('uses the same prototype for both spellings', function () {
assert.strictEqual(sharp.prototype.normalize, sharp.prototype.normalise);
});
});

0 comments on commit 3e52a84

Please sign in to comment.