Joaquin Cuenca Abela
Joaquin Cuenca Abela

Reputation: 2605

How to convert an image to grayscale with Cairo

I want to convert an image, potentially with an alpha channel, to cairo.

The code I wrote converts a fully opaque image to grayscale, but fails when the image contains an alpha channel:

import cairo
CAIRO_OPERATOR_HSL_LUMINOSITY = 28  # my py2cairo seems outdated

def convert_to_grayscale(img_in):
    img_out = img_in.create_similar(
        cairo.CONTENT_COLOR_ALPHA, img_in.get_width(), img_in.get_height())
    cr = cairo.Context(img_out)
    cr.set_source_rgba(1, 1, 1, 1)
    cr.paint()
    cr.set_source_surface(img_in)
    cr.set_operator(CAIRO_OPERATOR_HSL_LUMINOSITY)
    cr.paint()

    return img_out

An image containing the RGBA values (20, 30, 40, 255) will be converted (correctly) to (28, 28, 28, 255). However if the image is not fully opaque the result will be wrong, for example if I convert the image with color (10, 15, 20, 128) I will get back (141, 141, 141, 25), when I'm expecting (14, 14, 14, 128)[*]. How can I get a version of convert_to_grayscale that plays nicely with semitransparent images?

[*] Note that these values have the RGB values premultiplicated by their alpha, as it's usual in cairo.

Upvotes: 2

Views: 2202

Answers (2)

Joaquin Cuenca Abela
Joaquin Cuenca Abela

Reputation: 2605

I finally managed to convert the image respecting the original alpha using NumPy. I asked in the cairo mailing lists, but the only alternative that I got had the same problem that my version had (ie, it didn't respect the original alpha channel).

Here is my solution:

import cairo
import numpy
import sys


def convert_to_grayscale(img_in):
    """Convert an image to grayscale.

    Arguments:
        img_in: (cairo.ImageSurface) input image.

    Return:
        (cairo.ImageSurface) image in grayscale, in ARGB32 mode.

    Timing:
        ~100ms to convert an image of 800x800

    Examples:
        # returns a B&W image
        >>> convert_to_grayscale(cairo.ImageSurface.create_from_png('test.png'))
    """
    a = numpy.frombuffer(img_in.get_data(), numpy.uint8)
    w, h = img_in.get_width(), img_in.get_height()
    a.shape = (w, h, 4)

    assert sys.byteorder == 'little', (
        'The luminosity vector needs to be switched if we\'re in a big endian architecture. '
        'The alpha channel will be at position 0 instead of 3.')
    alpha = a[:, :, 3]
    alpha.shape = (w, h, 1)

    luminosity_float = numpy.sum(a * numpy.array([.114, .587, .299, 0]), axis=2)
    luminosity_int = numpy.array(luminosity_float, dtype=numpy.uint8)
    luminosity_int.shape = (w, h, 1)
    grayscale_gbra = numpy.concatenate((luminosity_int, luminosity_int, luminosity_int, alpha),
                                       axis=2)
    stride = cairo.ImageSurface.format_stride_for_width(cairo.FORMAT_ARGB32, w)
    assert stride == 4 * w, 'We need to modify the numpy code if the stride is different'
    img_out = cairo.ImageSurface.create_for_data(grayscale_gbra, cairo.FORMAT_ARGB32, w, h, stride)

    return img_out

Upvotes: 1

jjacky
jjacky

Reputation: 41

I was trying to do the same today, and figured out a different way to do it. I don't actually do this in Python, in fact I don't know Python at all, so I can't provide any code. But, here's what I did:

  • create a surface from the original image
  • create a pattern for that surface
  • apply an rgb source (black) with CAIRO_OPERATOR_HSL_SATURATION, using the pattern as mask

To maybe makes things clearer, in C it means something like this (where s is the new cairo_surface_t, and cr its associated cairo_t; assuming you've already put the original picture there) :

cairo_pattern_t *pattern = cairo_pattern_create_for_surface (s);
cairo_rectangle (cr, 0, 0, width, height);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_set_operator (cr, CAIRO_OPERATOR_HSL_SATURATION);
cairo_mask (cr, pattern);
cairo_pattern_destroy (pattern);

Adding it here in case/hope it might be helpful to some.

Upvotes: 3

Related Questions