Python3 NumPy裁剪图片可视区域
0x01 解决思路
如何裁剪一张图片的可视区域?解决这个问题的方法有很多种,本文采用的方法很简单:想象有四条线从图片四周向内推进,如果碰到不是白色的内容,那么就停止推进。最终这四条线内部的区域就是该图片的可视区域。
测试环境: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个参数:img
、white
、ratio
。img
不用多说,是待处理的Image
对象。那么white
和ratio
参数是什么呢?
在函数中为了方便处理,我们将图像转成了灰度图,而在灰度图里,像素的灰度值为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次目标函数,取平均值
使用Pillow
的visible_box_pil
函数处理一次目标图片的耗时大约为0.150秒,而使用NumPy
的visible_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)
的结果完全一致,也就是说那四根“线”碰到任何一个不是纯白色的像素都会停下来。但问题是很多应用场景所要处理的图片都达不到这么严格的要求,这个时候就可以通过调整white
和ratio
两个阀值来获得更合乎要求的结果。