Python3 NumPy裁剪图片可视区域

0x01 解决思路

如何裁剪一张图片的可视区域?解决这个问题的方法有很多种,本文采用的方法很简单:想象有四条线从图片四周向内推进,如果碰到不是白色的内容,那么就停止推进。最终这四条线内部的区域就是该图片的可视区域。
Python3 NumPy裁剪图片可视区域

测试环境:Ubuntu 18.04 + Python3.6 + Pillow-5.3.0 + NumPy-1.15.2

0x02 实现(Pillow)

def visible_box_pil(img, white=240, ratio=0.99):
    """
    获取图像的可视区域
    :param img: PIL.Image.Image,目标图像
    :param white: 白色像素灰度阀值
    :param ratio: 白色像素比例阀值
    :return: 可视区域,4元素元组
    """
    gray = img.convert('L')
    first_col, first_row = 0, 0
    last_col, last_row = width, height = img.size

    # 从上往下,按行推进
    for y in range(height):
        pixel_count = 0
        for x in range(width):
            if gray.getpixel((x, y)) > white:
                pixel_count += 1
        if pixel_count / width < ratio:
            first_row = y
            break
    # 从下往上,按行推进
    for y in reversed(range(height)):
        pixel_count = 0
        for x in range(width):
            if gray.getpixel((x, y)) > white:
                pixel_count += 1
        if pixel_count / width < ratio or y + 1 <= first_row:
            last_row = y + 1
            break
    # 从左往右,按列推进
    for x in range(width):
        pixel_count = 0
        for y in range(height):
            if gray.getpixel((x, y)) > white:
                pixel_count += 1
        if pixel_count / height < ratio:
            first_col = x
            break
    # 从右往左,按列推进
    for x in reversed(range(width)):
        pixel_count = 0
        for y in range(height):
            if gray.getpixel((x, y)) > white:
                pixel_count += 1
        if pixel_count / height < ratio or x + 1 <= first_col:
            last_col = x + 1
            break

    return first_col, first_row, last_col, last_row

from PIL import Image
img = open('no_watermmark.jpg')
print(visible_box_pil(img))
img.close()

visible_box_pil函数共接收3个参数:imgwhiteratioimg不用多说,是待处理的Image对象。那么whiteratio参数是什么呢?
在函数中为了方便处理,我们将图像转成了灰度图,而在灰度图里,像素的灰度值为0代表像素为纯黑色,255代表像素为纯白色,所以我们可以设置一个接近255的阀值:如果一个像素的灰度值高于这个阀值,我们就可以判定这个像素为白色,这个阀值就是white参数。
对于一行或一列上来说,除了要判断单个像素是否为白色,还要判断这一行是不是空白内容。ratio参数也代表一个阀值:如果某一行上白色的像素占总像素数的比例超过了这个阀值,就判定这一行为空白内容。其默认值为0.99,即99%

这两个参数存在的意义,是为了避免JPEG图片压缩特性的干扰,以及像素级别的图片污损。

相信任何人都不会喜欢上面这种裹脚布式的代码。造成代码臃肿最主要的原因,就是应为Pillow对行、列操作的支持相当有限,只能使用getpixel方法进行单个像素的操作,而重复的getpixel操作只会带来更多的重复代码以及更低的运行效率。幸运的是我们可以使用NumPy模块来大幅简化这个函数。

0x03 实现(NumPy)

import numpy as np

def visible_box(img, white=240, ratio=0.99):
    """
    获取图像的可视区域
    :param img: PIL.Image.Image,目标图像
    :param white: 白色像素灰度阀值
    :param ratio: 白色像素比例阀值
    :return: 可视区域,4元素元组
    """
    first_col, first_row = 0, 0
    last_col, last_row = width, height = img.size

    white_arr = np.where(np.asarray(img.convert('L')) > white, 1, 0)
    # 从上往下,按行推进
    for i, row in enumerate(white_arr):
        if sum(row) / width < ratio:
            first_row = i
            break
    # 从下往上,按行推进
    for i, row in enumerate(reversed(white_arr)):
        if sum(row) / width < ratio or height - i <= first_row:
            last_row = height - i
            break
    # 倒置数组
    white_arr = np.transpose(white_arr)
    # 从左往右,按列推进
    for i, col in enumerate(white_arr):
        if sum(col) / height < ratio:
            first_col = i
            break
    # 从右往左,按列推进
    for i, col in enumerate(reversed(white_arr)):
        if sum(col) / height < ratio or width - i <= first_col:
            last_col = width - i
            break

    return first_col, first_row, last_col, last_row

from PIL import Image
img = Image.open('no_watermark.jpg')
print(visible_box(img))
img.close()

在一开始,函数就用不到60个字符完成了下列操作:将图像转换为灰度图、将灰度图转换为NumPy的二维数组、对这个二维数组的每一个像素使用white阀值判断其是否是白色像素。完成这一切后,我们就可以用sum函数对行、列上白色像素的数量进行统计了。相比于上一种实现方式来说,减少了相当多的重复代码,同时也大幅提升了运行效率。

0x04 运行效率

测试方法:使用timeit计时库重复运行100次目标函数,取平均值

使用Pillowvisible_box_pil函数处理一次目标图片的耗时大约为0.150秒,而使用NumPyvisible_box函数处理一次目标图片的耗时仅需约0.226秒,速度相差近7倍,而且速度慢的一方还有更为臃肿的代码结构,足以体现NumPy在数据处理上的巨大优势。

0x05 总结

其实Image对象有一个内置函数用于裁剪可视区域,而且效率非常高,只不过裁剪的是黑色背景:

from PIL import ImageOps

def visible_box_invert(img):
    inverted = ImageOps.invert(img.convert('L'))  # 转换成灰度图后反色
    return inverted.getbbox()

实际上visible_box_invert(img)的效果与visible_box(img, 254, 1)的结果完全一致,也就是说那四根“线”碰到任何一个不是纯白色的像素都会停下来。但问题是很多应用场景所要处理的图片都达不到这么严格的要求,这个时候就可以通过调整whiteratio两个阀值来获得更合乎要求的结果。