抠图是基本需求,最常见的应用场景就是证件照,每次去拍照,都要用个纯色的幕布,而且要求衣服不能太浅。其实背后是有原因的:为了管理部门更准确识别出人像。许多科幻电影也是要求演员在绿幕前表演,后期抠图合成逼真的电影。抠图工具非常多,例如PhotoShop就是抠图利器,可以很神奇地把图片里的元素单独提取出来,和其他的背景合并。事实上,ps的这些能力背后也是基于图像处理的算法。抠图的基本原理,就是找出目标物的边界范围,然后把其他地方变透明。常见做法有3种:
- 根据像素色彩范围,识别出背景。适合纯背景色场景,如证件照。
- 如果背景色彩复杂,就分块识别,在某小块中色彩相对集中。但需要单独设计切分算法。
- 机器学习算法,训练出识别物的特征模型。训练过程非常耗资源,但有了模型就可以快速应用,目前不少大平台也开放出了自己的训练模型,如百度的paddlehub项目。
以下是背景杂乱的原图。
下述代码使用paddlehub进行抠图,这是基于训练模型的AI抠图。
import paddlehub as hub
import cv2 as cv
def cutperson(inputfile):
"""
AI抠图
Args:
inputfile (string): 要处理的图片文件名
"""
image = cv.imread(inputfile)
humanseg = hub.Module(name='deeplabv3p_xception65_humanseg')
filelist = [inputfile]
res = humanseg.segmentation(data = {'image':filelist},visualizatinotallow=True,output_dir='.output')
cutperson("/mnt/d/test/cut2.bmp")
下图是输出图片,可以看出效果还不错。
OpenCV也提供了交互式抠图能力,也就是著名的grubcut算法。
import cv2
import numpy as np
#绘制前景/背景标识线标志
drawing = False
# 定义GrabCut类,作用是设置一些参数
class GrabCut:
def __init__(self, t_img):
self.img = t_img
self.img_raw = img.copy()
self.img_width = img.shape[0]
self.img_height = img.shape[1]
self.img_show = self.img.copy()
self.img_gc = self.img.copy()
self.img_gc = cv2.GaussianBlur(self.img_gc, (3, 3), 0)
self.lb_up = False
self.rb_up = False
self.lb_down = False
self.rb_down = False
self.mask = np.full(self.img.shape[:2], 2, dtype=np.uint8)
self.firt_choose = True
# 鼠标操作的的回调函数
def mouse_event(event, x, y, flags, param):
global drawing, last_point, start_point
# 左键按下,开始标识前景
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
# 设置鼠标按下的起始点
last_point = (x, y)
start_point = last_point
param.lb_down = True
# 右键按下,开始标识背景
elif event == cv2.EVENT_RBUTTONDOWN:
# 读者请先标识前景,否则无法分割
if param.firt_choose:
print("Please select foreground first!")
return
drawing = True
last_point = (x, y)
start_point = last_point
param.rb_down = True
# 鼠标移动,绘制标识前景和背景的线
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
# 鼠标左键按下的绘制
if param.lb_down:
cv2.line(param.img_show, last_point, (x,y), (0, 0, 255), 2, -1)
cv2.rectangle(param.mask, last_point, (x, y), 1, -1, 4)
# 鼠标右键按下的绘制
if param.rb_down:
cv2.line(param.img_show, last_point, (x, y), (255, 0, 0), 2, -1)
cv2.rectangle(param.mask, last_point, (x, y), 0, -1, 4)
last_point = (x, y)
# 左键释放,结束标识前景
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
param.lb_up = True
param.lb_down = False
cv2.line(param.img_show, last_point, (x,y), (0, 0, 255), 2, -1)
# 如果第一次标识,切换状态
if param.firt_choose:
param.firt_choose = False
cv2.rectangle(param.mask, last_point, (x,y), 1, -1, 4)
# 右键释放,结束标识背景
elif event == cv2.EVENT_RBUTTONUP:
# 如果首先标识背景则不做处理
if param.firt_choose:
return
drawing = False
param.rb_up = True
param.rb_down = False
cv2.line(param.img_show, last_point, (x,y), (255, 0, 0), 2, -1)
cv2.rectangle(param.mask, last_point, (x,y), 0, -1, 4)
#执行操作
def process(img):
if img is None:
print('Can not read image correct!')
return
g_img = GrabCut(img)
cv2.namedWindow('image')
# 定义鼠标的回调函数
cv2.setMouseCallback('image', mouse_event, g_img)
while (True):
cv2.imshow('image', g_img.img_show)
# 鼠标左键或者右键抬起时,按照标识执行Grabcut算法
if g_img.lb_up or g_img.rb_up:
g_img.lb_up = False
g_img.rb_up = False
# 背景model
bgdModel = np.zeros((1, 65), np.float64)
# 前景model
fgdModel = np.zeros((1, 65), np.float64)
rect = (1, 1, g_img.img.shape[1], g_img.img.shape[0])
mask = g_img.mask
g_img.img_gc = g_img.img.copy()
#执行Grabcut算法
cv2.grabCut(g_img.img_gc, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_MASK)
# 0和2做背景
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
# 使用蒙板来获取前景区域
g_img.img_gc = g_img.img_gc * mask2[:, :, np.newaxis]
cv2.imshow('Grabcut_result', g_img.img_gc)
# 按下ESC键退出
if cv2.waitKey(20) == 27:
break
if __name__ == '__main__':
img = cv2.imread("/mnt/d/test/cut2.bmp")
process(img)
利用opencv的交互能力,人工简单的勾勒要抠图的主要部分,随后就会生成当前的抠图效果。勾勒的效果如下图所示。
勾勒时不需要太精确。算法就会自动生成当前的抠图如下所示。
从上图可以看出,交互式抠图的效果不错,不过还是直接使用paddlehub来的方便。