抠图是基本需求,最常见的应用场景就是证件照,每次去拍照,都要用个纯色的幕布,而且要求衣服不能太浅。其实背后是有原因的:为了管理部门更准确识别出人像。许多科幻电影也是要求演员在绿幕前表演,后期抠图合成逼真的电影。抠图工具非常多,例如PhotoShop就是抠图利器,可以很神奇地把图片里的元素单独提取出来,和其他的背景合并。事实上,ps的这些能力背后也是基于图像处理的算法。抠图的基本原理,就是找出目标物的边界范围,然后把其他地方变透明。常见做法有3种:

  • 根据像素色彩范围,识别出背景。适合纯背景色场景,如证件照。
  • 如果背景色彩复杂,就分块识别,在某小块中色彩相对集中。但需要单独设计切分算法。
  • 机器学习算法,训练出识别物的特征模型。训练过程非常耗资源,但有了模型就可以快速应用,目前不少大平台也开放出了自己的训练模型,如百度的paddlehub项目。

以下是背景杂乱的原图。

Python工具箱系列(五十六)_右键

下述代码使用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")


下图是输出图片,可以看出效果还不错。


Python工具箱系列(五十六)_bc_02


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的交互能力,人工简单的勾勒要抠图的主要部分,随后就会生成当前的抠图效果。勾勒的效果如下图所示。


Python工具箱系列(五十六)_抠图_03



勾勒时不需要太精确。算法就会自动生成当前的抠图如下所示。


Python工具箱系列(五十六)_右键_04


从上图可以看出,交互式抠图的效果不错,不过还是直接使用paddlehub来的方便。


Python工具箱系列(五十六)_bc_05