导语
由来就是最近半年都在追我们公司的一位小姐姐,小姐姐长得眉清目秀,身材也好,关键是性格这块非常让我喜欢,追求了大半年,今天去问小姐姐要喝奶茶的时候,开始是死活不肯点,直到我说了,我发奖金了,组里的都请了,所以点一杯吧!没办法,只能用这个方法,以为我请的是全组的同事,其实我想请的是你呀!希望这篇文章别被她看到哈!
送奶茶的时候,女神叫住了我,我们一起去了茶水间,她问:你为啥会喜欢我,我哪里值得你这么喜欢我嘛。时不时就送早餐送下午茶的!我:其实当你第一天入职,从我身边走过,惊鸿一蹩,我感觉被你迷住了,等我回过神来的时候,我心里不由自主的冒出了个想法,我一定要追你,不管成功与否,然后我还给你画了一幅素描来表达我对你的喜爱之心!女神:那还不给我看一下呀,我满意的话会考虑做不做你女朋友的!
这可让我太开心了哇
讲真,我其实一点都不回画画,那些描边啥的我压根就不会,不过谁让我是个程序员呢,能用代码解决的东西坚决不花钱请人画,而且时间方面也来不及!好了看好下面的操作,多学点,你也能脱单!
今天要用到的编程语言是Python
工具
Python版本:3.6.
相关模块:
opencv-python模块;
numpy模块;
argparse模块;
pillow模块;
scipy模块;
以及一些python自带的模块。
这些模块应该都会装吧!用镜像源贼快!
原理
paper(在相关文件里提供了)是:
Combining Sketch and Tone for Pencil Drawing Production
这里主要介绍一下paper提出的铅笔素描画生成算法步骤,其他诸如相关工作之类的就不介绍了。而且这里我假设想了解该算法原理的小伙伴是掌握图像处理基础知识的。
先放算法框架,然后再慢慢说:
该基于图像的铅笔素描画生成算法主要分为两个步骤:
-
pencil stroke generation;
-
pencil tone drawing.
前者的目标为表示图像场景的一般结构(主要是绘制一些线条),后者则更专注于图像内物体的形状和产生的阴影等内容。下面分别详细地介绍一下这两个步骤。
1.pencil stroke generation
这个步骤的目标主要是通过一些线条来表示图像场景的一般结构(也就是轮廓图)。首先假设我们有输入灰度图I,于是我们可以获得图像的梯度图G:
但显然计算图像梯度是很容易受到噪声干扰的,因此这样生成的图像轮廓图效果肯定不好。为了得到更加稳定的结果,作者首先对G进行8个方向的卷积(例如0,22.5,...,157.5):
每个方向的卷积核为沿着该方向的值为1,其他方向的值为0。当然,实际应用中,考虑抗锯齿问题,卷积核可以通过双线性插值得到(类似于指定角度的运动模糊处理)。同时,在论文中,作者根据经验认为卷积核大小一般取原图大小的1/30。
得到各个方向的卷积结果后,对每个像素点,我们都有8个卷积响应值,每个响应值对应一个方向。由此,我们可以得到每个方向的卷积响应图,其定义为
这里p代表像素点,G为之前获得的图像梯度图。对得到的每个方向的卷积响应图再次进行方向卷积:
然后对得到的S'进行像素值反转和映射到[0, 1]操作就可以得到最终的图像轮廓图S了。作者画了个图更加形象直观地说明了上述处理过程:
以及与直接使用G来生成图像轮廓图的对比:
2.pencil tone drawing
OK,接下来我们来介绍该算法的第二部分,也就是色调渲染,该部分主要包括以下两个步骤:
-
直方图匹配;
-
纹理渲染。
(1)直方图匹配
作者通过对真实的铅笔素描画的观察发现,铅笔画的色调可以由三部分组成,如下图所示:
可以看出,阴影部分很适合用正态分布来拟合,中间调很适合用均匀分布来拟合,而高光部分则很适合用拉普拉斯分布来拟合。具体而言,对于高光部分,使用以下函数来模拟(因为作图用的纸张一般是白色的,所以在接近色阶255时分布曲线很陡峭。):
对于中间调,则使用以下函数来模拟:
对于阴影部分,则使用以下函数来模拟:
由此,我们可以得到最终的铅笔画色调概率分布公式:
即对高光,阴影和中间调进行加权求和(权重系数可以根据不同的需要进行调节)并利用归一化因子Z以保证:
上述公式中,v均代表色调值,未知的参数均为控制参数,它们决定了铅笔画最终的色调直方图。这些未知的参数可以使用最大似然估计来求得。具体而言,这些控制参数的计算公式为:
其中m和s分别代表各层(高光,中间调,阴影)像素值的均值和方差。接下来的工作就是对每层进行直方图匹配(核心思想就是使输出图像具有规定的直方图形状。)并对他们进行重组。
假设该步骤最终输出的图像为J。
(2)纹理渲染
简单而言,就是模拟人反复用铅笔描的过程(在人类绘图时,一般会通过在相同位置重复绘制来实现纹理渲染)。作者使用笔画的乘法来模拟这一过程,表示成公式是这样的:
即使用H进行β次渲染来逼近J。显然,β越大,图像越黑:
同时我们要求β局部平滑,因此问题等价于求解:
作者取λ为0.2。因此上述方程可以转换为标准的线性方程并使用共轭梯度求解。于是我们获得最终的铅笔画纹理图为:
补充一下,H其实就是一张预先选好的真实场景下的铅笔画纹理图,代表绘画模式,网上可以找到很多,例如:
3.Overall Framework
通过前面两个步骤,我们分别获得图像轮廓图S和色调纹理图T。因此,我们可以获得最终的结果图R=S·T。由此,只要我们输入灰度图像I,便可以轻松地获得铅笔素描画R了。
4.Color Pencil Drawing
该算法可以很轻松地扩展到彩色图像上。具体而言,就是把彩色图像从RGB空间转到YUV空间,把Y单独拿出来看作灰度图像进行上述变换后获得新的YUV空间,最后将新的YUV空间重新映射到RGB空间即可。
没有基础的小白,上面这一部分直接掠过!咱们来完整代码和演示效果!
部分代码
import cv2 import math import numpy as np ''' Function: normalize the image value between [0.0, 1.0] Input: --img: np.array Output: --img: np.array ''' def im2double(img): if len(img.shape) == 2: return (img - img.min()) / (img.max() - img.min()) else: return cv2.normalize(img.astype('float'), None, 0.0, 1.0, cv2.NORM_MINMAX) ''' Function: Laplacian distribution with its peak at the brightest value Input: --x: pixel value between [0, 255] --sigma: the scale of the distribution Output: --value: value represent bright layer ''' def Laplace(x, sigma=9): value = (1./sigma) * math.exp(-(256-x)/sigma) * (256 - x) return value ''' Function: use the uniform distribution and encourage full utilization of different gray levels to enrich pencil drawing Input: --x: pixel value between [0, 255] --ua, ub: two controlling parameters defining the range of the distribution Output: --value: value represent mild tone layer ''' def Uniform(x, ua=105, ub=225): value = (1. / (ub - ua)) * (max(x-ua, 0) - max(x-ub, 0)) return value ''' Function: gaussian distribution for dark layer Input: --x: pixel value between [0, 255] --u: the mean value of the dark strokes --sigma: the scale parameter Output: --value: value represent dark layer ''' def Gaussian(x, u=90, sigma=11): value = (1./math.sqrt(2*math.pi*sigma)) * math.exp(-((x-u)**2)/(2*(sigma**2))) return value '''horizontal stitch''' def horizontalStitch(img, width): img_stitch = img.copy() while img_stitch.shape[1] < width: window_size = int(round(img.shape[1] / 4.)) left = img[:, (img.shape[1]-window_size): img.shape[1]] right = img[:, 0:window_size] aleft = np.zeros((left.shape[0], window_size)) aright = np.zeros((left.shape[0], window_size)) for i in range(window_size): aleft[:, i] = left[:, i] * (1 - (i + 1.) / window_size) aright[:, i] = right[:, i] * (i + 1.) / window_size img_stitch = np.column_stack((img_stitch[:, 0: (img_stitch.shape[1]-window_size)], aleft+aright, img_stitch[:, window_size: img_stitch.shape[1]])) img_stitch = img_stitch[:, 0: width] return img_stitch '''vertical stitch''' def verticalStitch(img, height): img_stitch = img.copy() while img_stitch.shape[0] < height: window_size = int(round(img.shape[0] / 4.)) up = img[(img.shape[0]-window_size): img.shape[0], :] down = img[0:window_size, :] aup = np.zeros((window_size, up.shape[1])) adown = np.zeros((window_size, up.shape[1])) for i in range(window_size): aup[i, :] = up[i, :] * (1 - (i + 1.) / window_size) adown[i, :] = down[i, :] * (i + 1.) / window_size img_stitch = np.row_stack((img_stitch[0: img_stitch.shape[0]-window_size, :], aup+adown, img_stitch[window_size: img_stitch.shape[0], :])) img_stitch = img_stitch[0: height, :] return img_stitch
效果
简直完美,你学会了没有!哈哈哈哈
结局
生成出来之后立马打印出来发给女神,她居然有点泪目了!那这男朋友我当定了!不要羡慕哥,哥只是个传说,快去给你们的女神来一副素描吧!需要完整源代码的可私信我