With PIL can I combine two (or more) bands in a customizable way?

Viewed 162

I want to 'decompose' an Image (i.e. split()), modify one of the channels by combining it with another one and re-'compose' (i.e. merge()) them again.

This example should highlight edges by mapping them onto the RED-channel using the feature I wish existed:

from PIL import Image, ImageFilter
base = Image.open("example.jpg")
base.show()

base image

edges = (base
    .convert("L")
    .filter(ImageFilter.Kernel((3, 3), (-1, -1, -1, -1, 8,-1, -1, -1, -1), 1, 0))
    .point(lambda p: 255 if p > 10 else 0))
edges.show()

edges

r, g, b = base.split()

# now I want the blue channel/band for each pixel to be whatever respective
# pixel is brighter on a given set of bands

# this is what I want to do, but `combine()` does not exist
b_with_edges = Image.combine((r, edges), lambda pixels: max(pixels))
# instead - for the example I'm dropping blue and take just the edges
b_with_edges = edges

new_image = Image.merge("RGB", (r, g, b_with_edges))
new_image.show()

combined image with edge highlighting

Of course I can manually iterate through all pixels and combine them manually. From what I've seen from PIL it seems to be a quite well developed and flexible library, so I expect there to be some built-in way..

Update:

Since CrazyChucky's answer works quite well for this use case, I dare to add a distilled version:

from PIL import Image, ImageFilter, ImageChops
base = Image.open("example.jpg").reduce(1)
r, g, b = base.split()
edges = (base
    .convert("L")
    .filter(ImageFilter.Kernel((3, 3), (-1, -1, -1, -1, 8,-1, -1, -1, -1), 1, 0))
    .point(lambda p: 255 if p > 10 else 0))
image_with_edge_highlighting = Image.merge("RGB", (r, g, ImageChops.lighter(b, edges)))
image_with_edge_highlighting.show()
2 Answers

What you describe in this particular case sounds like ImageChops.lighter:

from PIL import ImageChops

b_with_edges = ImageChops.lighter(r, edges)

It produces a new image using whichever pixel is lighter (has a higher value) from the two given images.

After merging as in your code, I get the following result:

Output image

That said... while the ImageChops module has quite a few useful functions, I'm not aware of a more general-purpose, customizable function that accepts a callable as you describe. Such a function, if it existed, could be useful, but would probably not be very performant. It would essentially be a wrapper around a for loop, iterating at the Python level rather than C—much like Pandas' apply method.

(point, which you've already demonstrated in your question, is the closest thing I know of... but of course it operates on only a single image, using a lookup table.)

You could implement it like this, directly accessing and setting pixels through the PixelAccess class returned by each image's load method:

def custom_image_combine(images, fn):
    width, height = images[0].size
    return_image = Image.new(images[0].mode, (width, height))
    return_image_access = return_image.load()
    accessors = [image.load() for image in images]
    for x in range(width):
        for y in range(height):
            return_image_access[x, y] = fn(
                accessor[x, y] for accessor in accessors
            )
    return return_image

b_with_edges = custom_image_combine((r, edges), max)

Note that you can simply provide max, rather than a lambda that uses it.

While CrazyChucky's answer is quite sophisticated and it solves the example use case provided in the question even better than a generic solution (for being more clear and probably a bit faster), you can use ImageMath.eval() as stated by Mark Setchell.

from PIL import Image, ImageFilter, ImageMath

base = Image.open("example.jpg")
edges = (base
    .convert("L")
    .filter(ImageFilter.Kernel((3, 3), (-1, -1, -1, -1, 8,-1, -1, -1, -1), 1, 0))
    .point(lambda p: 255 if p > 10 else 0))

image_with_edge_highlighting = Image.merge(
    "RGB",
    (r, g, ImageMath.eval(
        "convert(max(b, edges), 'L')", b=b, edges=edges)))
image_with_edge_highlighting.show()

recomposed image with combined blue-channel

Related