54

三行代码生成一张素描图片

 5 years ago
source link: http://ccc013.github.io/2018/09/09/实战-三行代码生成一张素描图片/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文大约 2000 字,阅读大约需要 6 分钟

我们知道图片除了最普通的彩色图,还有很多类型,比如素描、卡通、黑白等等,今天就介绍如何使用 Python 和 Opencv 来实现图片变素描图。

主要参考这篇文章来实现— How to create a beautiful pencil sketch effect with OpenCV and Python

简介

如何将图片转换成素描图呢,只需要下面四个步骤即可:

  1. 首先将彩色图转换成灰度图;
  2. 对灰度图进行求其反色的操作;
  3. 对第2步得到的结果采用一个高斯模糊的操作;
  4. 采用颜色亮化(color dodge)的技术将第一步的灰度图和第三步操作后的图片进行混合。

事先准备,首先是安装好 opencv,可以直接通过 pip 进行安装:

pip install opencv-python

接着准备一张图片,最好是颜色鲜明一点的图片,方便对比转换的效果。

VBNzuaz.jpg!web

第一步:彩色图变灰度图

第一步变成灰度图,其实非常简单,直接调用 opencv 的函数即可,如下面代码所示:

import cv2

img_rgb = cv2.imread('example.jpg')
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

图片转换效果如下所示:

MVZZb2q.png!web

上面的代码是读取图片后,再通过调用 cv2.cvtColor 函数将图片转换成灰度图,实际上我们可以直接在读取图片时候就直接转换图片,即:

img_gray = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

这里调用 cv2.imread 函数时,设置了 cv2.IMREAD_GRAYSCALE 的标志,表示加载灰度图。在 imread 函数中是设置了三种标志,分别是

  • cv2.IMREAD_COLOR : 默认使用该种标识。加载一张彩色图片,忽视它的透明度。
  • cv2.IMREAD_GRAYSCALE : 加载一张灰度图。
  • cv2.IMREAD_UNCHANGED : 加载图像,包括它的Alpha 通道(Alpha 表示图片的透明度)。

另外,如果觉得以上标志太长,可以简单使用 1,0,-1 代替,效果是相同的。

第二步:灰度图进行反色操作

第二步就是对灰度图进行反色操作,其实就是非常简单的采用灰度图的最大像素值 255 减去当前像素值即可(因为灰度图的范围是[0, 255]),代码如下所示:

img_gray_inv = 255 - img_gray

结果如下所示:

aMVnYvu.png!web

其实就是原本比较暗的地方变光亮了,而比较亮的地方变暗了。

第三步:高斯模糊

高斯模糊操作是一个有效减少图片噪音以及对图片进行平滑操作的方法,在数学上等价于对图像采用高斯核进行卷积的操作。我们可以直接调用 cv2.GaussianBlur 来实现高斯模糊操作,这里需要设置参数 ksize ,表示高斯核的大小, sigmaXsigmaY 分别表示高斯核在 X 和 Y 方向上的标准差。

img_blur = cv2.GaussianBlur(img_gray_inv, ksize=(21, 21),
                            sigmaX=0, sigmaY=0)

效果如下所示,右边图是进行高斯模糊后的结果,是有了一定的模糊效果。

Nn2eyiA.png!web

第四步:混合操作

第四步,就是见证奇迹的时刻!这一步骤自然就是需要得到最终的素描图结果了。在传统照相技术中,当需要对图片某个区域变得更亮或者变暗,可以通过控制它的曝光时间,这里就用到亮化(Dodging)和暗化(burning)的技术。

在现代图像编辑工具,比如 PS 可以实现上述说的两种技术。比如对于颜色亮化技术,给定一张图片 A 和 蒙版 B,那么实现做法如下所示:

(B[idx] == 255)?B[idx]:min(255, ((A[idx] << 8) / (255-B[idx])))

通过 python 代码实现上述公式,那么原始代码如下所示:

import cv2
import numpy as np

def dodgeNaive(image, mask):
    # determine the shape of the input image
    width, height = image.shape[:2]

    # prepare output argument with same size as image
    blend = np.zeros((width, height), np.uint8)

    for col in range(width):
        for row in range(height):
            # do for every pixel
            if mask[col, row] == 255:
                # avoid division by zero
                blend[col, row] = 255
            else:
                # shift image pixel value by 8 bits
                # divide by the inverse of the mask
                tmp = (image[col, row] << 8) / (255 - mask)

                # make sure resulting value stays within bounds
                if tmp > 255:
                    tmp = 255
                    blend[col, row] = tmp

    return blend

上述代码虽然实现了这个功能,但是很明显会非常耗时,中间采用了一个两层循环,计算复杂度是 O(w*h) ,也就是如果图片的宽和高的乘积越大,耗时就越长,所以就有了升级版的代码版本:

def dodgeV2(image, mask):
    return cv2.divide(image, 255 - mask, scale=256)

运行上述代码,得到的最终结果如下所示:

uEvmuqN.png!web

效果看起来还可以,除了右下角部分对于原图中黑色区域处理得不是很好。

而另一种技术—-暗化操作的代码如下所示:

def burnV2(image, mask):
    return 255 - cv2.divide(255 - image, 255 - mask, scale=256)

效果如下图所示:

aInmqiR.png!web

完整版代码如下所示:

import cv2
import numpy as np


def dodgeNaive(image, mask):
    # determine the shape of the input image
    width, height = image.shape[:2]

    # prepare output argument with same size as image
    blend = np.zeros((width, height), np.uint8)

    for col in range(width):
        for row in range(height):
            # do for every pixel
            if mask[col, row] == 255:
                # avoid division by zero
                blend[col, row] = 255
            else:
                # shift image pixel value by 8 bits
                # divide by the inverse of the mask
                tmp = (image[col, row] << 8) / (255 - mask)
                # print('tmp={}'.format(tmp.shape))
                # make sure resulting value stays within bounds
                if tmp.any() > 255:
                    tmp = 255
                    blend[col, row] = tmp

    return blend


def dodgeV2(image, mask):
    return cv2.divide(image, 255 - mask, scale=256)


def burnV2(image, mask):
    return 255 - cv2.divide(255 - image, 255 - mask, scale=256)


def rgb_to_sketch(src_image_name, dst_image_name):
    img_rgb = cv2.imread(src_image_name)
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
    # 读取图片时直接转换操作
    # img_gray = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

    img_gray_inv = 255 - img_gray
    img_blur = cv2.GaussianBlur(img_gray_inv, ksize=(21, 21),
                                sigmaX=0, sigmaY=0)
    img_blend = dodgeV2(img_gray, img_blur)

    cv2.imshow('original', img_rgb)
    cv2.imshow('gray', img_gray)
    cv2.imshow('gray_inv', img_gray_inv)
    cv2.imshow('gray_blur', img_blur)
    cv2.imshow("pencil sketch", img_blend)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    cv2.imwrite(dst_image_name, img_blend)


if __name__ == '__main__':
    src_image_name = 'example.jpg'
    dst_image_name = 'sketch_example.jpg'
    rgb_to_sketch(src_image_name, dst_image_name)

最后,还有一种更加快速的实现,代码如下所示,仅需四行代码即可实现转换成素描图的效果。

def rgb_to_sketch_v2(src_image_name):
    img_gray = cv2.imread(src_image_name, 0)
    img_blur = cv2.GaussianBlur(img_gray, (21, 21), 0, 0)
    img_blend = cv2.divide(img_gray, img_blur, scale=256)
    img_result = cv2.cvtColor(img_blend, cv2.COLOR_GRAY2BGR)

最后用本人比较喜欢的一个女演员的照片来看看这个转换的效果:

QJvquea.png!web

效果还是挺不错的!

以上就是本文的主要内容和总结,欢迎关注我的微信公众号—一个算法汪的技术成长之路或者扫描下方的二维码,和我分享你的建议和看法,指正文章中可能存在的错误,大家一起交流,学习和进步!

rmQruyy.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK