116 lines
4 KiB
JavaScript
116 lines
4 KiB
JavaScript
// Unsharp mask filter
|
|
//
|
|
// http://stackoverflow.com/a/23322820/1031804
|
|
// USM(O) = O + (2 * (Amount / 100) * (O - GB))
|
|
// GB - gaussian blur.
|
|
//
|
|
// Image is converted from RGB to HSL, unsharp mask is applied to the
|
|
// lightness channel and then image is converted back to RGB.
|
|
//
|
|
'use strict';
|
|
|
|
|
|
var glur_mono16 = require('glur/mono16');
|
|
var hsl_l16 = require('./hsl_l16');
|
|
|
|
|
|
module.exports = function unsharp(img, width, height, amount, radius, threshold) {
|
|
var r, g, b;
|
|
var h, s, l;
|
|
var min, max;
|
|
var m1, m2, hShifted;
|
|
var diff, iTimes4;
|
|
|
|
if (amount === 0 || radius < 0.5) {
|
|
return;
|
|
}
|
|
if (radius > 2.0) {
|
|
radius = 2.0;
|
|
}
|
|
|
|
var lightness = hsl_l16(img, width, height);
|
|
|
|
var blured = new Uint16Array(lightness); // copy, because blur modify src
|
|
|
|
glur_mono16(blured, width, height, radius);
|
|
|
|
var amountFp = (amount / 100 * 0x1000 + 0.5)|0;
|
|
var thresholdFp = (threshold * 257)|0;
|
|
|
|
var size = width * height;
|
|
|
|
/* eslint-disable indent */
|
|
for (var i = 0; i < size; i++) {
|
|
diff = 2 * (lightness[i] - blured[i]);
|
|
|
|
if (Math.abs(diff) >= thresholdFp) {
|
|
iTimes4 = i * 4;
|
|
r = img[iTimes4];
|
|
g = img[iTimes4 + 1];
|
|
b = img[iTimes4 + 2];
|
|
|
|
// convert RGB to HSL
|
|
// take RGB, 8-bit unsigned integer per each channel
|
|
// save HSL, H and L are 16-bit unsigned integers, S is 12-bit unsigned integer
|
|
// math is taken from here: http://www.easyrgb.com/index.php?X=MATH&H=18
|
|
// and adopted to be integer (fixed point in fact) for sake of performance
|
|
max = (r >= g && r >= b) ? r : (g >= r && g >= b) ? g : b; // min and max are in [0..0xff]
|
|
min = (r <= g && r <= b) ? r : (g <= r && g <= b) ? g : b;
|
|
l = (max + min) * 257 >> 1; // l is in [0..0xffff] that is caused by multiplication by 257
|
|
|
|
if (min === max) {
|
|
h = s = 0;
|
|
} else {
|
|
s = (l <= 0x7fff) ?
|
|
(((max - min) * 0xfff) / (max + min))|0 :
|
|
(((max - min) * 0xfff) / (2 * 0xff - max - min))|0; // s is in [0..0xfff]
|
|
// h could be less 0, it will be fixed in backward conversion to RGB, |h| <= 0xffff / 6
|
|
h = (r === max) ? (((g - b) * 0xffff) / (6 * (max - min)))|0
|
|
: (g === max) ? 0x5555 + ((((b - r) * 0xffff) / (6 * (max - min)))|0) // 0x5555 == 0xffff / 3
|
|
: 0xaaaa + ((((r - g) * 0xffff) / (6 * (max - min)))|0); // 0xaaaa == 0xffff * 2 / 3
|
|
}
|
|
|
|
// add unsharp mask mask to the lightness channel
|
|
l += (amountFp * diff + 0x800) >> 12;
|
|
if (l > 0xffff) {
|
|
l = 0xffff;
|
|
} else if (l < 0) {
|
|
l = 0;
|
|
}
|
|
|
|
// convert HSL back to RGB
|
|
// for information about math look above
|
|
if (s === 0) {
|
|
r = g = b = l >> 8;
|
|
} else {
|
|
m2 = (l <= 0x7fff) ? (l * (0x1000 + s) + 0x800) >> 12 :
|
|
l + (((0xffff - l) * s + 0x800) >> 12);
|
|
m1 = 2 * l - m2 >> 8;
|
|
m2 >>= 8;
|
|
// save result to RGB channels
|
|
// R channel
|
|
hShifted = (h + 0x5555) & 0xffff; // 0x5555 == 0xffff / 3
|
|
r = (hShifted >= 0xaaaa) ? m1 // 0xaaaa == 0xffff * 2 / 3
|
|
: (hShifted >= 0x7fff) ? m1 + ((m2 - m1) * 6 * (0xaaaa - hShifted) + 0x8000 >> 16)
|
|
: (hShifted >= 0x2aaa) ? m2 // 0x2aaa == 0xffff / 6
|
|
: m1 + ((m2 - m1) * 6 * hShifted + 0x8000 >> 16);
|
|
// G channel
|
|
hShifted = h & 0xffff;
|
|
g = (hShifted >= 0xaaaa) ? m1 // 0xaaaa == 0xffff * 2 / 3
|
|
: (hShifted >= 0x7fff) ? m1 + ((m2 - m1) * 6 * (0xaaaa - hShifted) + 0x8000 >> 16)
|
|
: (hShifted >= 0x2aaa) ? m2 // 0x2aaa == 0xffff / 6
|
|
: m1 + ((m2 - m1) * 6 * hShifted + 0x8000 >> 16);
|
|
// B channel
|
|
hShifted = (h - 0x5555) & 0xffff;
|
|
b = (hShifted >= 0xaaaa) ? m1 // 0xaaaa == 0xffff * 2 / 3
|
|
: (hShifted >= 0x7fff) ? m1 + ((m2 - m1) * 6 * (0xaaaa - hShifted) + 0x8000 >> 16)
|
|
: (hShifted >= 0x2aaa) ? m2 // 0x2aaa == 0xffff / 6
|
|
: m1 + ((m2 - m1) * 6 * hShifted + 0x8000 >> 16);
|
|
}
|
|
|
|
img[iTimes4] = r;
|
|
img[iTimes4 + 1] = g;
|
|
img[iTimes4 + 2] = b;
|
|
}
|
|
}
|
|
};
|