Premultiply alpha channel to avoid dark artifacts during tranformation

Add `Sharp.compare(file1, file2, callback)` function for comparing images
using mean squared error (MSE). This is useful for unit tests.

See:
- https://github.com/jcupitt/libvips/issues/291
- http://entropymine.com/imageworsener/resizealpha/
This commit is contained in:
Daniel Gasienica
2015-05-06 20:58:22 -07:00
committed by Lovell Fuller
parent c792a047b1
commit ef8db1eebf
33 changed files with 755 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -2,9 +2,13 @@
var path = require('path');
var assert = require('assert');
var sharp = require('../../index');
// Constants
var MAX_ALLOWED_MEAN_SQUARED_ERROR = 0.0005;
// Helpers
var getPath = function(filename) {
return path.join(__dirname, filename);
};
@@ -57,6 +61,8 @@ module.exports = {
inputPngOverlayLayer2: getPath('alpha-layer-2-ink.png'),
inputPngOverlayLayer1LowAlpha: getPath('alpha-layer-1-fill-low-alpha.png'),
inputPngOverlayLayer2LowAlpha: getPath('alpha-layer-2-ink-low-alpha.png'),
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.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
@@ -116,12 +122,37 @@ module.exports = {
}
if (distance > options.threshold) {
return callback(new Error('Maximum similarity distance: ' + options.threshold + '. Actual: ' + distance));
return callback(new Error('Expected maximum similarity distance: ' + options.threshold + '. Actual: ' + distance + '.'));
}
callback();
});
});
},
assertEqual: function(actualImagePath, expectedImagePath, callback) {
if (typeof actualImagePath !== 'string') {
throw new TypeError('`actualImagePath` must be a string; got ' + actualImagePath);
}
if (typeof expectedImagePath !== 'string') {
throw new TypeError('`expectedImagePath` must be a string; got ' + expectedImagePath);
}
if (typeof callback !== 'function') {
throw new TypeError('`callback` must be a function');
}
sharp.compare(actualImagePath, expectedImagePath, function (error, info) {
if (error) return callback(error);
var meanSquaredError = info.meanSquaredError;
if (typeof meanSquaredError !== 'undefined' && meanSquaredError > MAX_ALLOWED_MEAN_SQUARED_ERROR) {
return callback(new Error('Expected images be equal. Mean squared error: ' + meanSquaredError + '.'));
}
return callback(null, info);
});
}
};

View File

@@ -1,9 +1,8 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
var sharp = require('../../index');
sharp.cache(0);
@@ -76,4 +75,30 @@ describe('Alpha transparency', function() {
});
});
it('Enlargement with non-nearest neighbor interpolation shouldnt cause dark edges', function(done) {
var BASE_NAME = 'alpha-premultiply-enlargement-2048x1536-paper.png';
var actual = fixtures.path('output.' + BASE_NAME);
var expected = fixtures.expected(BASE_NAME);
sharp(fixtures.inputPngAlphaPremultiplicationSmall)
.resize(2048, 1536)
.interpolateWith('bicubic')
.toFile(actual, function(err) {
if (err) throw err;
fixtures.assertEqual(actual, expected, done);
});
});
it('Reduction with non-nearest neighbor interpolation shouldnt cause dark edges', function(done) {
var BASE_NAME = 'alpha-premultiply-reduction-1024x768-paper.png';
var actual = fixtures.path('output.' + BASE_NAME);
var expected = fixtures.expected(BASE_NAME);
sharp(fixtures.inputPngAlphaPremultiplicationLarge)
.resize(1024, 768)
.interpolateWith('bicubic')
.toFile(actual, function(err) {
if (err) throw err;
fixtures.assertEqual(actual, expected, done);
});
});
});

77
test/unit/compare.js Normal file
View File

@@ -0,0 +1,77 @@
'use strict';
var assert = require('assert');
var fixtures = require('../fixtures');
var fs = require('fs');
var sharp = require('../../index');
sharp.cache(0);
// Constants
var MAX_ALLOWED_MEAN_SQUARED_ERROR = 0.0005;
// Tests
describe('sharp.compare', function() {
it('should report equality when comparing an image to itself', function(done) {
var image = fixtures.inputPngOverlayLayer0;
sharp.compare(image, image, function (error, info) {
if (error) return done(error);
assert.strictEqual(info.isEqual, true, 'image is equal to itself');
assert.strictEqual(info.status, 'success', 'status is correct');
assert(0 <= info.meanSquaredError &&
info.meanSquaredError <= MAX_ALLOWED_MEAN_SQUARED_ERROR,
'MSE is within tolerance');
done();
});
});
it('should report that two images have a mismatched number of bands (channels)', function(done) {
var actual = fixtures.inputPngOverlayLayer1;
var expected = fixtures.inputJpg;
sharp.compare(actual, expected, function (error, info) {
if (error) return done(error);
assert.strictEqual(info.isEqual, false);
assert.strictEqual(info.status, 'mismatchedBands');
assert(typeof info.meanSquaredError === 'undefined', 'MSE is undefined');
done();
});
});
it('should report that two images have a mismatched dimensions', function(done) {
var actual = fixtures.inputJpg;
var expected = fixtures.inputJpgWithExif;
sharp.compare(actual, expected, function (error, info) {
if (error) return done(error);
assert.strictEqual(info.isEqual, false);
assert.strictEqual(info.status, 'mismatchedDimensions');
assert(typeof info.meanSquaredError === 'undefined', 'MSE is undefined');
done();
});
});
it('should report the correct mean squared error for two different images', function(done) {
var actual = fixtures.inputPngOverlayLayer0;
var expected = fixtures.inputPngOverlayLayer1;
sharp.compare(actual, expected, function (error, info) {
if (error) return done(error);
var meanSquaredError = info.meanSquaredError;
assert.strictEqual(info.isEqual, false);
assert.strictEqual(info.status, 'success');
// ImageMagick reports: 42242.5
// `compare -metric mse 'actual' 'expected' comparison.png`
assert(41900 <= meanSquaredError && meanSquaredError <= 41950,
'Expected: 41900 <= meanSquaredError <= 41950. Actual: ' + meanSquaredError);
done();
});
});
});

View File

@@ -6,19 +6,78 @@ var sharp = require('../../index');
sharp.cache(0);
// Main
// Constants
var MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR = 0.3;
// Helpers
var getPaths = function(baseName, extension) {
if (typeof extension === 'undefined') {
extension = 'png';
}
var actual = fixtures.path('output.' + baseName + '.' + extension);
var expected = fixtures.expected(baseName + '.' + extension);
var expectedMagick = fixtures.expected(baseName + '-imagemagick.' + extension);
return {
actual: actual,
expected: expected,
expectedMagick: expectedMagick
};
};
var assertEqual = function (paths, callback) {
if (typeof callback !== 'function') {
throw new TypeError('`callback` must be a function');
}
fixtures.assertEqual(paths.actual, paths.expected, function (error) {
if (error) return callback(error);
sharp.compare(paths.actual, paths.expectedMagick, function (error, info) {
if (error) return callback(error);
if (info.meanSquaredError > MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR) {
return callback(new Error('Expected MSE against ImageMagick to be <= ' +
MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR + '. Actual: ' +
info.meanSquaredError));
}
callback();
});
});
};
// Test
describe('Overlays', function() {
it('Overlay transparent PNG on solid background', function(done) {
var paths = getPaths('alpha-layer-01');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data, info) {
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-01.png'), data, {threshold: 0}, done);
assertEqual(paths, done);
});
});
it('Overlay low-alpha transparent PNG on solid background', function(done) {
var paths = getPaths('alpha-layer-01-low-alpha');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
.toFile(paths.actual, function (error) {
if (error) return done(error);
assertEqual(paths, done);
});
});
it('Composite three transparent PNGs into one', function(done) {
var paths = getPaths('alpha-layer-012');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data, info) {
@@ -26,26 +85,56 @@ describe('Overlays', function() {
sharp(data)
.overlayWith(fixtures.inputPngOverlayLayer2)
.toBuffer(function (error, data, info) {
.toFile(paths.actual, function (error) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-012.png'), data, {threshold: 0}, done);
assertEqual(paths, done);
});
});
});
// This tests that alpha channel unpremultiplication is correct:
it('Composite three low-alpha transparent PNGs into one', function(done) {
sharp(fixtures.inputPngOverlayLayer1LowAlpha)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toBuffer(function (error, data, info) {
it('Composite two transparent PNGs into one', function(done) {
var paths = getPaths('alpha-layer-12');
sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputPngOverlayLayer2)
.toFile(paths.actual, function (error, data, info) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-012-low-alpha.png'), data, {threshold: 0}, done);
assertEqual(paths, done);
});
});
it('Composite two low-alpha transparent PNGs into one', function(done) {
var paths = getPaths('alpha-layer-12-low-alpha');
sharp(fixtures.inputPngOverlayLayer1LowAlpha)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toFile(paths.actual, function (error, data, info) {
if (error) return done(error);
assertEqual(paths, done);
});
});
it('Composite three low-alpha transparent PNGs into one', function(done) {
var paths = getPaths('alpha-layer-012-low-alpha');
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
.toBuffer(function (error, data, info) {
if (error) return done(error);
sharp(data)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toFile(paths.actual, function (error, data, info) {
if (error) return done(error);
assertEqual(paths, done);
});
});
});
// This tests that alpha channel unpremultiplication is correct:
it('Composite transparent PNG onto JPEG', function(done) {
sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputPngOverlayLayer1)