2

I am trying to color the black pixels of a black-and-white image on a canvas.

The naive code I'm using is:

function color_text(canvas, r, g, b, w, h) {
    var ctx = canvas.getContext('2d');
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var pixels = imageData.data;
    for (var x = 0; x < w; x++) {
        for (var y = 0; y < h; y++) {
            var redIndex = ((y - 1) * (canvas.width * 4)) + ((x - 1) * 4);
            var greenIndex = redIndex + 1;
            var blueIndex = redIndex + 2;
            var alphaIndex = redIndex + 3;
            if ((pixels[redIndex] < 240) && (pixels[greenIndex] < 240) && (pixels[blueIndex] < 240)) {
                pixels[redIndex] = r;
                pixels[greenIndex] = g;
                pixels[blueIndex] = b;
            }
        }
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.putImageData(imageData, 0, 0);
}

I use < 240 as a detector for non-white pixels instead of exactly 255 because these are scanned in pieces of hand-drawn calligraphy, so a fudge factor is needed. Applying this algorithm to an image that is scaled down by canvas's native drawImage() produces results that look as good as the black-and-white image.

However, canvas's native drawImage() leaves much to be desired, so instead, I scaled down the image with a slightly-modified version of the code provided in this answer. The black and white image produced by this code is beautiful, much better than canvas's native method. However, when I color the image with the above function, it looks awful again.

A complete jsfiddle is here: http://jsfiddle.net/q9sd9w1k/

Any ideas on how I can color the high-quality version effectively?

Thanks.

Community
  • 1
  • 1
Moustafa Elqabbany
  • 1,110
  • 9
  • 10
  • You can take a look at this topic: [Colorization Using Optimization](http://www.cs.huji.ac.il/~yweiss/Colorization/index.html) – cuixiping Jan 12 '16 at 08:02

2 Answers2

2

You should use HSL color space for coloring images. This will allow you to handle edge cases, literally, such as this where also anti-aliased pixels get colored correctly based on luminance value.

The principle steps needed are:

  • Create a gray-scale version of the image
  • Decide which color you want to use (in HSL this will be a degree [0, 360] - you can convert the color you want to use from RGB to HSL as well).
  • Update a second buffer with the RGB converted from HSL using Hue, same saturation and the gray-scale value from the first buffer as lightness.

Example code with everything you need to do these steps - adopt as needed:

Convert to gray-scale:

var lumas = new Float32Array(width * height),
    idata = ctx.getImageData(0, 0, width, height),
    data = idata.data,
    len = data.length,
    i = 0,
    cnt = 0;

for(; i < len; i += 4)
    lumas[cnt++] = (data[i] * 0.2126 + 
                    data[i+1] * 0.7152 + 
                    data[i+2] * 0.0722) / 255; //normalized value

You will need a hsl2rgb function:

function hsl2rgb(h, s, l) {

    var r, g, b, q, p;

    h /= 360;

    if (s === 0) {
        r = g = b = l;

    }
    else {
        function hue2rgb(p, q, t) {
            t %= 1;
            if (t < 0.1666667) return p + (q - p) * t * 6;
            if (t < 0.5) return q;
            if (t < 0.6666667) return p + (q - p) * (0.6666667 - t) * 6;
            return p;
        }

        q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        p = 2 * l - q;

        r = hue2rgb(p, q, h + 0.3333333);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 0.3333333);
    }

    return {
        r: (r * 255 + 0.5) | 0,
        g: (g * 255 + 0.5) | 0,
        b: (b * 255 + 0.5) | 0
    }
}

Then iterate over the luma buffer, pass in the value as l, put the resulting rgb component with alpha set to 255 into a buffer for the canvas:

var idata = ctx.createImageData(0, 0, width, height),
    buffer = idata.data,
    len = buffer.length,
    hue = 90
    sat = 0.5,
    i = 0,
    cnt = 0;

for(; i < len; i += 4) {

    var color = hsl2rgb(h, s, lumas[cnt++]); // HSL to RGB

    buffer[i  ] = color.r;
    buffer[i+1] = color.g;
    buffer[i+2] = color.b;
    buffer[i+3] = 255;
}

ctx.putImageData(idata, 0, 0);
0

John-Paul Gignac offers the following answer, which works beautifully:

For each pixel, do the following:

c1_r = f1_r + (b1_r-f1_r)*c0_r/255
c1_g = f1_g + (b1_g-f1_g)*c0_g/255
c1_b = f1_b + (b1_b-f1_b)*c0_b/255

Where c1_r is the new red value of the pixel, c0_r is its old value, b1_r is the new background color's red value, and f1_r is the new foreground's red value. The assumption here is that the original background and foreground are white and black, respectively.

For comparison with the original, the new colouring function is:

function color_text_gignac(canvas, r, g, b, w, h) {
    var ctx = canvas.getContext('2d');
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var pixels = imageData.data;
    for (var x = 0; x < w; x++) {
        for (var y = 0; y < h; y++) {
            var redIndex = ((y - 1) * (canvas.width * 4)) + ((x - 1) * 4);
            var greenIndex = redIndex + 1;
            var blueIndex = redIndex + 2;
            var alphaIndex = redIndex + 3;
            pixels[redIndex] = r + (255 - r) * pixels[redIndex] / 255;
            pixels[greenIndex] = g + (255 - g) * pixels[greenIndex] / 255;
            pixels[blueIndex] = b + (255 - b) * pixels[blueIndex] / 255;
        }
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.putImageData(imageData, 0, 0);
}

Here's a complete working example: http://jsfiddle.net/moq9pd7h/2/

Scroll down to see the clear, coloured version.

Community
  • 1
  • 1
Moustafa Elqabbany
  • 1,110
  • 9
  • 10