PIL / Pillow is a compact and fast image library for Python. We have summarized frequently used processes (updated from time to time)
There is basically no reason to use PIL, Pillow has a bugfix for resizing filters and is of higher quality.
Pillow is tuned very fast and always runs faster than its similar library, ImageMagick.
However, getpixel
/ putpixel
is very slow, so don't use it for anything other than image generation.
There is also a faster pillow-simd
. It seems to be about 4 to 5 times faster than the original Pillow.
pillow-simd https://github.com/uploadcare/pillow-simd
https://python-pillow.org/pillow-perf/
mode | Description |
---|---|
1 | Used for 1-bit mask, logical operation is possible |
L | 8bit grayscale |
P | Palette mode |
RGB | 8bit x 3 |
RGBA | 8bit x 4 transparency(alpha)With |
CMYK | 8bit x 4 Commonly used for printing |
YCbCr | Often used for 8bit x 3 video |
HSV | 8bit x 3 pillow only |
RGBa | Multiply RGB value by alpha channel |
LA | Multiply the L value by the alpha channel |
I | 32bit integer |
F | 32bit floating point |
filter | Downscaling quality | Upscaling quality | performance |
---|---|---|---|
Image.NEAREST | ⭐⭐⭐⭐⭐ | ||
Image.BOX | ⭐ | ⭐⭐⭐⭐ | |
Image.BILINEAR | ⭐ | ⭐ | ⭐⭐⭐ |
Image.HAMMING | ⭐⭐ | ⭐⭐⭐ | |
Image.BICUBIC | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
Image.LANCZOS | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
img.convert("L")
alpha.convert("LA")
→
By the way, convert ('L')
does not consider the alpha value.
alpha.convert("L")
→
img.convert("HSV")
Only pillow can be converted to HSV color space. It is composed of three components: Hue, Saturation, and Value. The following is an example of shifting the color wheel.
h, s, v = img.convert("HSV").split()
_h = ImageMath.eval("(h + 128) % 255", h=h).convert("L")
Image.merge("HSV", (_h, s, v)).convert("RGB")
CIE XYZ is a color space adjusted so that the Euclidean distance between colors is the same as the difference perceived by humans.
rgb2xyz = (
0.412453, 0.357580, 0.180423, 0,
0.212671, 0.715160, 0.072169, 0,
0.019334, 0.119193, 0.950227, 0
)
img.convert("RGB", rgb2xyz)
gray = img.convert("L") #Convert to grayscale
gray.point(lambda x: 0 if x < 230 else x) #If the value is 230 or less, it will be 0.
img.point(lambda x: x * 1.5) # 1.Make it 5 times brighter
img.point(lambda x: x * 0.5) # 1 /Darken to 2
Convert the image to grayscale and then sepia it.
gray = img.convert("L")
Image.merge(
"RGB",
(
gray.point(lambda x: x * 240 / 255),
gray.point(lambda x: x * 200 / 255),
gray.point(lambda x: x * 145 / 255)
)
)
Gamma correction can also be converted at a very high speed by using a look-up table. Assuming that src = input color, γ = gamma value, and g = gain value, the gamma correction formula is as follows.
dst = \biggl(\frac{src}{255}\biggr)^{1/γ} \times g \times 255
def gamma_table(gamma_r, gamma_g, gamma_b, gain_r=1.0, gain_g=1.0, gain_b=1.0):
r_tbl = [min(255, int((x / 255.) ** (1. / gamma_r) * gain_r * 255.)) for x in range(256)]
g_tbl = [min(255, int((x / 255.) ** (1. / gamma_g) * gain_g * 255.)) for x in range(256)]
b_tbl = [min(255, int((x / 255.) ** (1. / gamma_b) * gain_b * 255.)) for x in range(256)]
return r_tbl + g_tbl + b_tbl
img.point(gamma_table(1.2, 0.5, 0.5))
point
used in loopsIt is better not to pass lambda
to ʻImage.point in the loop, it is faster to expand it in advance because the argument passed to
point` is just a conversion table.
for ...:
img.point(lambda x: x * 100)
#The lower process is equal to the upper process but faster
table = [x * 100 for x in range(256)] * len(img.getbands())
for ...:
img.point(table)
getbbox
ʻImage.getbboxreturns the smallest non-zero value in the image, an image with all zeros returns
None`.
alpha = Image.open("alpha.png ")
crop = alpha.split()[-1].getbbox()
alpha.crop(crop)
→
If the two images are the same ʻImageChops.difference returns all 0 images, so if
getbboxis
None`, it can be judged to be the same.
ImageChops.difference(img1, img2).getbbox() is None
img.resize((128, 128), Image.LANCZOS)
Thumbnails maintain aspect ratio, unlike resizing.
Note that for some reason thumbnail
is a destructive method, it's ʻImage.copy` so it's a good idea to make a duplicate.
img.thumbnail((128, 128), Image.LANCZOS)
img.size
# (128, 79)
Specifying True
for the argument ʻexpand` expands the image if it grows when rotated.
img.rotate(90, expand=True)
Mosaic processing can be reduced and enlarged with ʻImage.LINEAR`, but if you reduce it after applying Gaussian blur, it will be a soft mosaic.
#Jagged mosaic
img.resize([x // 8 for x in img.size]).resize(img.size)
#Apply Gaussian blur for a soft mosaic
gimg = img.filter(ImageFilter.GaussianBlur(4))
gimg.resize([x // 8 for x in img.size]).resize(img.size)
Image.blend(img,effect_img, 0.5)
img.quantize(4) #Color reduction to 4 colors
To paste an image with alpha, specify the image with alpha in the argument'mask'of ʻImage.paste`.
img.paste(alpha, mask=alpha)
ʻImage.getcolors`, which counts the colors used, cannot count more than 255 colors with no arguments. For images that use more than 255 colors, it is safe to pass the number of pixels as an argument. [^ 1]
[^ 1]: 2017-02-07 Fixed It was'Image.getcount', but it is correctly'Image.getcolors', I will fix it.
img.getcolors(img.size[0] * img.size[1])
Returns a list of image color histograms. Since each band is returned in succession, 256 x 3 = 768 elements are returned in RGB mode.
img.histogram()
There is no method to replace the color, if you want to replace the color please refer to the article below.
Replace image colors fast with PIL / Pillow
ImageOps.invert(img)
ImageOps.mirror(img) #Flip horizontal
ImageOps.flip(img) #flip upside down
Colors a grayscale image with a pixel value of 0 to black
and a pixel value of 255 to white
.
gray = ImageOps.grayscale(img)
ImageOps.colorize(gray, black=(0, 0, 0), white=(255, 255, 0))
→
Reduces the bit depth of the image to the value of the argument to simplify the color.
ImageOps.posterize(img, 2)
Inverts all pixel values above the threshold. I don't know where to use it.
ImageOps.solarize(img, 128)
Equalize the histogram of the image. Apply nonlinear mapping to the input image to create a uniform distribution of grayscale values in the output image.
ImageOps.equalize(img)
The ʻImageChops` module is a module for manipulating channels.
The left is the image to be affected and the right is the image for effects. In this chapter, we will use these two images as samples.
ImageChops.add(img, effect_img) # img + effect_img
ImageChops.subtract(img, effect_img) # img - effect_img
ImageChops.add_modulo(img, effect_img) # img + effect_img % MAX
ImageChops.subtract_modulo(img, effect_img) # img - effect_img % MAX
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)
Performs a convolution (convolution operation). Various image conversions are performed by rearranging the matrix called the kernel.
Parameters | Description |
---|---|
size | Kernel size |
scale | Divide by this value after matrix operation |
offset | Add by this value after matrix operation |
kernel | Convolution matrix |
https://github.com/python-pillow/Pillow/blob/6e7553fb0f12025306b2819b9b842adf6b598b2e/PIL/ImageFilter.py
ImageFilter.BLUR
img.filter(ImageFilter.BLUR)
# size: (5, 5),
# scale: 16,
# offset: 0,
# kernel:(
# 1, 1, 1, 1, 1,
# 1, 0, 0, 0, 1,
# 1, 0, 0, 0, 1,
# 1, 0, 0, 0, 1,
# 1, 1, 1, 1, 1
# )
ImageFilter.DETAIL
img.filter(ImageFilter.DETAIL)
# size: (3, 3),
# scale: 6,
# offset: 0,
# kernel: (
# 0, -1, 0,
# -1, 10, -1,
# 0, -1, 0
# )
ImageFilter.SHAPEN
img.filter(ImageFilter.SHARPEN)
# size: (3, 3),
# scale: 16,
# offset: 0,
# kernel: (
# -2, -2, -2,
# -2, 32, -2,
# -2, -2, -2
# )
ImageFilter.CONTOUR
img.filter(ImageFilter.CONTOUR)
# size: (3, 3),
# scale: 1,
# offset: 255,
# kernel: (
# -1, -1, -1,
# -1, 8, -1,
# -1, -1, -1
# )
ImageFilter.EDGE_ENHANCE / ImageFilter.EDGE_ENHANCE_MORE
img.filter(ImageFilter.EDGE_ENHANCE)
# size: (3, 3),
# scale: 2,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 10, -1,
# -1, -1, -1
# )
img.filter(ImageFilter.EDGE_ENHANCE_MORE)
# size: (3, 3),
# scale: 1,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 9, -1,
# -1, -1, -1
# )
ImageFilter.EMBOSS
img.filter(ImageFilter.EMBOSS)
# size: (3, 3),
# scale: 1,
# offset: 128,
# kernel: (
# -1, 0, 0,
# 0, 1, 0,
# 0, 0, 0
# )
ImageFilter.FIND_EDGES
img.filter(ImageFilter.FIND_EDGES)
# size: (3, 3),
# scale: 1,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 8, -1,
# -1, -1, -1
# )
ImageFilter.SMOOTH / ImageFilter.SMOOTH_MORE
img.filter(ImageFilter.SMOOTH)
# size: (3, 3),
# scale: 13,
# offset: 0,
# kernel: (
# 1, 1, 1,
# 1, 5, 1,
# 1, 1, 1
# )
#
img.filter(ImageFilter.SMOOTH_MORE)
# size: (5, 5),
# scale: 100,
# offset: 0,
# kernel: (
# 1, 1, 1, 1, 1,
# 1, 5, 5, 5, 1,
# 1, 5, 44, 5, 1,
# 1, 5, 5, 5, 1,
# 1, 1, 1, 1, 1
# )
Gaussian Blurにより画面の平滑化します。
img.filter(ImageFilter.GaussianBlur(1.0))
img.filter(ImageFilter.GaussianBlur(1.5))
img.filter(ImageFilter.GaussianBlur(3.0))
MaxFilter
is called Dilation and MinFilter
is called Erosion.
img.filter(ImageFilter.MinFilter())
img.filter(ImageFilter.MaxFilter())
Expansion / contraction / opening / closing
MedianFilter
is often used to remove noise, and its contours are less blurred than Gaussian filters.
img.filter(ImageFilter.MedianFilter())
→
Selects the most frequently used pixel value in a box of the specified size. Pixel values that occur only once or twice are ignored. ~~ I don't know where to use it. See ~~ Make your photos pictorial with Pillow's Mode Filter.
img.filter(ImageFilter.ModeFilter(5))
enhancer = ImageEnhance.Color(img)
enhancer.enhance(0.0) #Black and white
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) #The original image
enhancer = ImageEnhance.Contrast(img)
enhancer.enhance(0.0) #Gray image
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) #The original image
enhancer = ImageEnhance.Brightness(img)
enhancer.enhance(0.0) #Black image
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) #The original image
enhancer = ImageEnhance.Sharpness(img)
enhancer.enhance(0.0) #Blurred image
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) #The original image
enhancer.enhance(1.5) # ↕
enhancer.enhance(2.0) #Sharp image
The ʻImageMath` module is a module that allows you to write operations between pixels as if they were numerical operations. If you master it, you will be able to easily write complicated image processing.
--Only single band can be calculated, multi band is processed after splitting with Image.split
--The range of values when calculating with float 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".
--Various operations (+,-, \ *, /, * \ *,%) are not pixel processing but whole image operation
--The operation is performed for each Image, not for each pixel, and an Image is generated for each operation.
--You can also pass callable objects such as lambda
instead of operations
Since it can only process single bands, it is troublesome when converting images such as RGB, so it is a good idea to prepare the following helper function.
def _blend_f(img1, img2, func):
blend_eval = "convert(func(float(a), float(b)), 'L')"
bands = [
ImageMath.eval(
blend_eval,
a=a,
b=b,
func=func
)
for a, b in zip(img1.split(), img2.split())
]
return Image.merge(img1.mode, bands)
Implement drawing modes such as PhotoShop at high speed with PIL / Pillow
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, effect_img, _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, effect_img, _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, effect_img, _hard_light)
I am creating a module called "Image4Layer" that implements the drawing mode of Photoshop.
https://github.com/pashango2/Image4Layer
Installation is easy with pip, pillow (PIL) must be pre-installed to run.
$pip install image4layer
It's easy to use, it's an example of compositing in color-dodge mode.
from PIL import Image
from image4layer import Image4Layer
source = Image.open("ducky.png ")
backdrop = Image.open("backdrop.png ")
Image4Layer.color_dodge(backdrop, source)
You can write multiple GIFs (GIF animations).
im.save(out, save_all=True, append_images=[im1, im2, ...])
This is an example of creating a simple animated GIF.
imgs = []
for i in range(100):
imgs.append(img.point(lambda x: x * (1.0 - (i/100))))
img.save("anime.gif", save_all=True, append_images=imgs, loop=True)
http://pillow.readthedocs.io/en/4.0.x/handbook/image-file-formats.html?highlight=seek#saving
Converting PyQt to QImage uses the ʻImageQt` module.
ImageQt.ImageQt(img)
If you are using PySide, the following method is recommended.
from PySide.QtGui import *
import io
img_buffer = io.BytesIO()
base.save(img_buffer, "BMP")
qimage = QImage()
qimage.loadFromData(img_buffer.getvalue(), "BMP")
It may seem like a wasteful process, but Pillow / PySide will take care of the troublesome parts such as RGB → BGR conversion and Y-axis inversion problem.
Mutual conversion between PIL.Image and PyQt4.QtGui.QImage
PSNR
An index value that compares PSNR with two images. Currently, SSIM is better than PSNR, but PSNR is also often used. The higher the value, the better the image quality, and the standard quality is PSNR between 30 and 50 when measuring the degree of compression deterioration.
The formula is as follows: MSE is the mean square error and MAX is 255.
PSNR = 10 \times \log 10\frac{MAX^2}{MSE}
The function to find PSNR is as follows. You can find PSNR at high speed by using the ʻImageStat` module.
def psnr(img1, img2):
diff_img = ImageChops.difference(img1, img2)
stat = ImageStat.Stat(diff_img)
mse = sum(stat.sum2) / len(stat.count) / stat.count[0]
return 10 * math.log10(255 ** 2 / mse)
In addition, it is currently difficult to obtain SSIM at high speed with PIL / Pillow, so it is better to use the pyssim module or OpenCV.
[Peak signal-to-noise ratio-Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%94%E3%83%BC%E3%82%AF%E4%BF%A1%E5%8F % B7% E5% AF% BE% E9% 9B% 91% E9% 9F% B3% E6% AF% 94)
gray = img.convert("L")
gray2 = gray.filter(ImageFilter.MaxFilter(5))
senga_inv = ImageChops.difference(gray, gray2)
senga = ImageOps.invert(senga_inv)
I referred to the method of here, which is very wonderful.
Recommended Posts