Python图像处理模块PIL

130 阅读10分钟

PIL

Web开发过程中,经常需要保存用户头像等一系列静态图片资源,此时需要我们服务端对前台输入的数据进行严格判断,避免非图片类型文件上传到服务器

图片分类

常见的存储的格式有如下一些

WebP

WebPGoogle推出的影像技术,可以使让网页中的图片有效进行压缩,同时又不影响图片格式兼容与实际清晰度,进而让整体网页下载速度加快

WebP改善了JPEG格式的图片压缩技术

WebP使用了一种基于VP8(能以更少的数据提供更高质量的视频,而且只需较小的处理能力即可播放视频)编码,这种技术已在2010五月开源

利用预测编码技术,同时还采用了一种基于RIFF的非常轻量级的容器。这种容器只会给每张图片增加20字节,但能让图片作者保存他们想要存储的元数据(文件描述信息)

这种格式的主要优势在于高效率,在质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%

微信公众号使用的就是这种类型

# 使用PIL将图片处理为webp格式
def jpeg2webp(filename):
	if filename:
		im = PIL.Image.open(filename)
		im.save(filename.split('.')[0] + '.webp')

JPEG

Joint Photographic Expert Group

头部字段常见为:ffd8 ffe0

JPEG格式文件后辍名为.jpg.jpeg,是最常用的图像文件

JPEG格式是一种有损图片压缩格式,能够将图片压缩在很小的存储空间

但是JPEG格式所使用的是有损压缩格式,虽然图片存储量会减少,但是对应质量也会有丢失

PNG

Graphics Interchange Format

头部字段常见为:8950 4e47

PNG是网上接受的最新图像文件格式。PNG能够提供长度比GIF30%无损压缩图像文件

较旧的浏览器和程序可能不支持PNG文件

PNG只需要下载1/64的图像数据就可以展示低分辨率的预览图像

GIF

GIF1987年开发的图像文件格式

GIF文件的数据,是一种基于LZW算法的连续色调的无损压缩格式

GIF不属于任何应用程序。几乎所有相关软件都支持GIF

GIF格式的另一个特点是其在一个GIF文件中可以存多幅彩色图像

如果把存于一个文件中的多幅图像数据逐幅读出并显示到屏幕上,就可构成一种最简单的动画

PIL模块

是图像处理标准库;PIL功能非常强大,并且API的使用也非常简单

安装同样也非常简单

pip3 install pillow

导入和所安装的包名可能有些不同

import PIL

图片文件识别

文件是否为一张图片识别方式有如下几种

  • 使用标准库imghdr
import imghdr
def is_image(filename):
	if filename:
		type_ = imghdr.what(filename)
		if not type_:
			raise TypeError('Is Not a Image')
       	return type_
  • 使用PIL模块输出文件格式进行判断

文件对象的format属性可以得出当前图片文件格式,当文件无法处理为图片时,PIL打开图片对象时候将会抛出异常OSError

def is_image(filename):
	if filename:
		try:
			im = PIL.Image.open(filename)
		except OSError as e:
			pass
		else:
			return im.format

图片格式转换

PIL模块对于图片格式的转换非常只能,只需要将转换后的文件名后缀确定即可

PIL模块会自动根据后缀对图像文件内容进行处理

def image2webp(filename):
    # 图片转webp格式
	if filename:
		im = PIL.Image.open(filename)
		im.save(filename.split('.')[0] + '.webp')

def image2png(filename):
    # 图片转png格式
	if filename:
		im = PIL.Image.open(filename)
		im.save(filename.split('.')[0] + '.png')

通过打开的文件对象所支持的save函数,传递新文件名作为参数即可

创建缩略图

图片缩略图的创建可以通过PIL模块中的thumbnail方法来进行创建

该方法需要一个元组作为参数,元组代表像素大小

thumbnail方法会通过所传递元组的值,来将图片像素设置为最大不超过元组内数据的图片

def image2thumb(filename):
	im = PIL.Image.open(filename)
	size_ = (150,150)
	im.thumbnail(size_)
	im.save('thumb-' + filename)

注意,这并不会真的创建一个像素为绝对150x150的图片,只是图片的像素不会超过150而已

缩略图的创建也可以使图片文件所占存储空间大大降低

修改图片尺寸

修改图片尺寸使用resize函数,该函数与thumbnail接收类似参数

def image2resize(filename):
	im = PIL.Image.open(filename)
	size_ = (150,150)
	out_ = im.resize(size_)
	out_.save('resize-' + filename)

需要注意的是,直接修改图片尺寸可能会导致图片变成畸形的展示效果

另外这里的resize并不是直接影响打开图片的尺寸,而是返回一个新的修改过后的图片对象

大小的修改也可以使图片文件所占存储空间大大降低

图片压缩

图片压缩也可以时图片所占存储空间大大降低

这里比较科学的办法可以使用cv2模块下的imwrite函数对图片的原始质量进行修改

  • 安装支持模块opencv-python
pip3 install opencv-python  -i https://pypi.tuna.tsinghua.edu.cn/simple

这里对于图片的压缩非常简单,只需要使用cv2模块下的imwrite函数重新保存图片即可

import cv2
def jpeg2cv(filename):
    # 压缩JPEG
    im = cv2.imread(filename)
    cv2.imwrite('cv2-'+filename,im,[cv2.IMWRITE_JPEG_QUALITY,20])

def png2cv2(filename):
    # 压缩PNG
    im = cv2.imread(filename)
    cv2.imwrite('cv2-'+filename,im,[cv2.IMWRITE_PNG_COMPRESSION,9])

imwrite函数的参数部分,最后一个参数有如下意义:

对于JPEG格式的图片,这个参数表示从0-100的图片质量

IMWRITE_JPEG_QUALITY

  • 默认值是95

  • 对于PNG格式的图片,这个参数表示压缩级别

    • IMWRITE_PNG_COMPRESSION
    • 0-9,较高的值意味着更小的尺寸和更长的压缩时间而默认值是3

图像转字符

  1. 构建基本方法,将像素对应灰度值转化为可以代表的字符
  2. 图像内容解析,转化为字符内容
  3. 字符内容转化为像素,写入图像
  4. gif解析为单独的每一帧图片经过以上步骤处理
  5. 将处理完成的单独图像组合成一个gif

必备概念

  • 灰度值:黑白图像中点的颜色深度,范围一般从0~255,白色为255,黑色为0,所以黑白图片也被成为灰度图像
  • alpha通道值:一般用作不透明度参数。如果一个像素的alpha通道数值为0%,那它就是完全透明的(也就是看不见的),而数值为100%则意味着一个完全不透明的像素(传统的数字图像)

灰度值字符映射

每一个灰度值,我们都会有对应的ascii字符与之对应,通过字符所占空间大小,来确定其亮度,越靠前的,那么灰度值越低,也就越暗

灰度值计算公式:0.299 * R + 0.587 * G + 0.144 * B

def gray2char(r, g, b, alpha=256):
    '''
    根据RGBA值进行灰度值计算,并返回对应亮度的字符
    '''
    _ = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
    char_length = len(_)  # 字符序列长度
    proportion = 255 / char_length  # 总灰度值对应列表索引范围
    gray = 0.2126 * r + 0.7152 * g + 0.0722 * b  # 灰度值与rgb的计算公式
    
    return _[int(gray / proportion) - 1]  # 返回当前灰度值所对应字符

图像解析为字符

通过解析图像,我们可以得处图像每处像素点的rgb颜色值,并且通过这色值对应转化出灰度值,利用灰度值字符函数,将这个像素对应的字符拿到

注意由于字符和像素的宽高是有区别的,一个字符要比一个字符大的多,所以一张图像在处理过程中,宽高首先要进行比例划算,一般一个字符的宽高对于一个像素来说是6:1、11:1

def image2text(path):
    '''
    对图像进行解析
    @path: 图像路径
    '''
    img = Image.open(path).convert('RGB')  # 打开图像
    pic_width, pic_height = img.width, img.height  # 图像原始宽高
    width, height = int(pic_width / 6), int(pic_height / 11)  # 字符像素宽高转化
    img = img.resize((width, height), Image.NEAREST)  # 原始图像进行缩略,适合字符处理

    # 像素遍历,进行灰度值字符转化
    content = ''  # 存储转化结果字符
    colors = []  # 对应坐标原始颜色,为了之后给字符上色使用

    for h in range(height):  # 从高开始
        for w in range(width):  # 遍历每一行像素
            px = img.getpixel((w, h))  # 获取某一点的像素值
            char = gray2char(px[0], px[1], px[2], px[3] if len(px) > 3 else 256)
            colors.append((px[0], px[1], px[2]))
            content += char
        content += '\n'  # 每一行像素换行追加\n
        colors.append((255, 255, 255))  # 给换行未来的颜色就是白色
    return content, colors, pic_width, pic_height

灰度字符转图片

通过上一步方法,得到的content返回值,正式接下来需要存储如图片的字符内容,这里需要开启一个新的图像对象

结合对应颜色,将字符写入对应的像素点上,最后存储为图片,格式可以是jpg,这样比较小一些,png质量高,结果会大

def text2image(content, colors, pic_width, pic_height, path):
    '''
    字符存储为图像
    @path: 存储路径
    '''
    image = Image.new("RGB", (pic_width, pic_height), (255, 255, 255))  # 创建存储图像对象
    canvas = ImageDraw.Draw(image)  # 创建一个支持绘制的画布
    font = ImageFont.load_default().font  # 直接使用默认字体对象

    x = 0
    y = 0
    font_w, font_h = font.getsize(content[1])  # 字体的宽高

    for i in range(len(content)):  # 遍历字符内容对象
        if content[i] == '\n':  # 遍历到\n那就是下一行的元素了
            x = -font_w  # 每次初始化横纵坐标
            y += font_h
            continue
        canvas.text((x, y), content[i], colors[i])  # 写入字符,带上颜色
        x += font_w  # 偏移一个字体的像素
    image.save(path)

基本测试

对一张基本图像进行字符转化,并将结果存储为char.jpg

from PIL import Image, ImageFont, ImageDraw
def main():
    content, colors, pic_width, pic_height = image2text(path="test.jpg")
    text2image(content, colors, pic_width, pic_height, path="char.jpg")

GIF迭代

如果需要处理的是一个gif图像对象,那么首先将gif图像对象中的每一帧图片单独保存下来,接着图像解析为字符

接着将灰度字符转图片,最后再将字符图像组合为一个gif即可

def gif2image(path):
    '''
    gif图像拆分,并将拆分结果存储当前工作目录下的temp目录中
    @path: gif图像位置
    '''
    img = Image.open(path)
    work_path = os.getcwd()  # 当前工作路径
    cache_dir = os.path.join(work_path, 'gifTemp')
    if not os.path.exists(cache_dir): # 如果不存在保存单独每一帧图片的目录,则创建该目录
        os.mkdir(cache_dir)
    while True:
        try:
            current = img.tell()  # 获取当前帧位置
            file_name = os.path.join(cache_dir, str(current)+'.png')
            img.save(file_name)
            img.seek(current+1)  # 向下一帧读取
        except EOFError: # GIF读取完毕
            break
    return current

之后即可通过for循环使用上面的图像处理的两个方法对其进行处理,处理完成的图像可以保存至content目录下

通过遍历content目录下的处理好的字符图片,对其进行gif拼接

import imageio
def image2gif(_id, dir_name='content', duration=15 / 130):
    '''
	将之前处理好的字符png图片组合成GIF图像
    通过imageio模块处理合并
    '''
    path = os.path.join(os.getcwd(), dir_name)
    images = []
    for pic_id in range(_id):
        # 遍历取出每一张处理后的字符图片id值
        images.append(imageio.imread(os.path.join(path, '%d.png' % pic_id)))
        # 从文件中读入数据
    imageio.mimsave(os.path.join(os.getcwd(), 'fin.gif'),
                    images, duration=duration)