[PYTHON] Implement drawing modes such as PhotoShop at high speed with PIL / Pillow

Implement drawing modes such as PhotoShop using only PIL / Pillow.

An easy way to come up with is to use Image.getpixel / putpixel, but it's very slow. There is also a way to use numpy, but considering that it will be provided as a tool, I want to reduce the number of dependent modules as much as possible.

Since the number of drawing modes is very large, not all are listed. However, reading this article will make it easier to implement other drawing modes.

I made an Image4Layer module

We have packaged a pillow implementation of blend mode in PhotoShop.

https://github.com/pashango2/Image4Layer

Installation can be done with pip.

pip install image4layer

This is a fast blend mode implementation using the method in this article.

Use the ImageChops module

The ImageChops module allows you to easily combine images.

from PIL import Image, ImageChops

img = Image.open("sample.png ")
effect_img = Image.open("effect.png ")

B9BSxGZmEQpmAAAAAElFTkSuQmCC.pngkL+ySM465ToAlAAAAAElFTkSuQmCC.png

The left is the original image and the right is the effect image. These two images will be used as samples.

Dodge (linear)

ImageChops.add(img, effect_img)

wE7Grw7M8iQMgAAAABJRU5ErkJggg==.png

Subtraction

ImageChops.subtract(img, effect_img)

X+81Pf98mz2EwAAAABJRU5ErkJggg==.png

Multiply

ImageChops.multiply(img, effect_img)

8yo3kAAAAASUVORK5CYII=.png

screen

ImageChops.screen(img, effect_img)

P8BtLqhlqZTLrgAAAAASUVORK5CYII=.png

Comparison (bright) / comparison (dark)

ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)

X+81Pf98mz2EwAAAABJRU5ErkJggg==.pngwDMatqtY+79MgAAAABJRU5ErkJggg==.png

Absolute value of difference

ImageChops.difference(img, effect_img)

diff.png

offset

ImageChops.offset(img, 100, 100)

offset.png

Use the ImageMath module

Drawing modes not found in ImageChops are implemented in the ImageMath module. The ImageMath module is very quirky, but once you get used to it, you will be able to perform free image conversion at high speed. The points to note are as follows.

--Only single band image can be calculated, multi band image is split by Image.split -Although it is possible to calculate with float, the range of values is 0.0 to 255.0 instead of 0.0 to 1.0. --The mode of the image being calculated will be "I" (int) or "F" (float), and finally the mode will be converted to "L". --A new Image is created every time you perform various operations (+,-, *, /, **,%). --The operation is performed on an Image-by-Image basis, not on a pixel-by-pixel basis.

In the following example, the R band is compared (bright).

from PIL import ImageMath

img_r = img.split()[0]
eff_r = effect_img.split()[0]

ImageMath.eval("convert(max(a, b), 'L')", a=img_r, b=eff_r)

AX6XflESotN9AAAAAElFTkSuQmCC.png

It is also possible to specify an equal sign, in which case the value will be 0 or 1. In the example below, a value of 100 is set for pixels with a value of 128 or less.

ImageMath.eval("(a < 128) * 100", a=img_r).convert("L")

fill.png

You can call Python functions, but keep in mind that the Image passed as an argument is an operand (ImageMath._Operand), not a pixel number.

Since it is not a numerical value, it is impossible to divide the processing by the if statement according to the pixel value. If you want to divide the processing according to the pixel value, you need to combine masks. The following is an example of dividing the process according to the value of 128 or more and the value of 128 or less.

def _threshold(a):
    #255 if the value is 128 or less, 1 if 128 or more/Set to 2
    div2 = a / 2 * (a >= 128)
    white = (a < 128) * 255
    return div2 + white

ImageMath.eval("func(a)", a=img_r, func=_threshold).convert("L")

white.png

Since it can only handle single bands, create a series of functions that decompose, process, and integrate multiband. Float operation has a higher degree of freedom, so I will convert it to float.

def _blend_f(bands1, bands2, func):
    blend = "convert(func(float(a), float(b)), 'L')"
    bands = [
        ImageMath.eval(
            blend,
            a=a,
            b=b,
            func=func
        )
        for a, b in zip(bands1, bands2)
    ]
    return Image.merge("RGB", bands)

Based on the above, we will implement a complicated drawing mode.

overlay

def _over_lay(a, b):
    _cl = 2 * a * b / 255
    _ch = 2 * (a + b - a * b / 255) - 255
    return _cl * (a < 128) + _ch * (a >= 128)

_blend_f(img.split(), effect_img.split(), _over_lay)

overlay.png

Soft light

def _soft_light(a, b):
    _cl = (a / 255) ** ((255 - b) / 128) * 255
    _ch = (a / 255) ** (128 / b) * 255
    return _cl * (b < 128) + _ch * (b >= 128)

_blend_f(img.split(), effect_img.split(), _soft_light)

softlight.png

Hard light

def _hard_light(a, b):
    _cl = 2 * a * b / 255
    _ch = 2.0 * (a + b - a * b / 255.0) - 255.0
    return _cl * (b < 128) + _ch * (b >= 128)

_blend_f(img.split(), effect_img.split(), _hard_light)

hardlight.png

About processing speed

I implemented an overlay with Image.putpixel to compare the processing speed.

def _put_pixel_overlay(a, b):
    c = Image.new(a.mode, a.size)
    for x in range(a.size[0]):
        for y in range(b.size[1]):
            cola = a.getpixel((x, y))
            colb = b.getpixel((x, y))
            
            colc = [
                _a * _b * 2 / 255 if _a < 128 else (2 *(_a + _b - _a * _b / 255) - 255)
                for _a, _b in zip(cola, colb)
            ]
            c.putpixel((x, y), tuple(int(_) for _ in colc))
    return c

The execution speed is as follows.

%timeit _put_pixel_overlay(img, effect_img)
1 loop, best of 3: 663 ms per loop

%timeit _blend_f(img.split(), effect_img.split(), _over_lay)
100 loops, best of 3: 5.63 ms per loop

The ImageMath version is 100 times faster.

Packaged

We have uploaded a package that implements the drawing mode of Photoshop.

https://github.com/pashango2/Image4Layer

Installation is easy with pip.

pip install image4layer

Finally

Looking at the PIL code on the street, there are some scenes where Image.getpixel / putpixel is used.

Image.getpixel / putpixel is used for image creation, and it is the last resort to use for image conversion. There are examples of image conversion using numpy, but the compact implementation of PIL only is still attractive. PIL is a compact yet extremely powerful and fast library. Have a good PIL life.

Recommended Posts

Implement drawing modes such as PhotoShop at high speed with PIL / Pillow
Perform half-width / full-width conversion at high speed with Python
Image processing with PIL (Pillow)