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.
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.
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 ")
The left is the original image and the right is the effect image. These two images will be used as samples.
ImageChops.add(img, effect_img)
ImageChops.subtract(img, effect_img)
ImageChops.multiply(img, effect_img)
ImageChops.screen(img, effect_img)
ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)
ImageChops.difference(img, effect_img)
ImageChops.offset(img, 100, 100)
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)
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")
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")
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.
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)
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)
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)
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.
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
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.