本文借鉴了CSDN博主风吹夏天对此论文算法的理解:风吹夏天的图像铅笔画算法,以及香港中文大学Cewu Lu等人写的该论文的主页。原文作者和博主风吹夏天都给过代码,但是代码不全。我仔细看了原论文和该博主的文章后,基本上,大致算法思想就理通了。本文对于具体算法细节就不细述了,个人建议还是先进行原论文的研读,对该论文所进行的步骤先有个大致的了解。
二,算法思路
1,首先需要产生笔画结构
大致思路:对原图像进行梯度运算,得出大致轮廓 -----> 设计8个方向的卷积核,并依据论文中的公式对像素依据最大值所在子图像进行方向分类 -----> 再依据论文中的公式对每个方向的像素分别和对应的卷积核进行卷积
几个注意的地方:
对于卷积核的大小,论文中写的是原图片高和宽的三十分之一,论文作者设计卷积核的目的是希望产生铅笔画中一笔一笔勾画出的痕迹,但如果太大,会导致轮廓图不清晰,卷积核大小应该设置在3~13间效果好些;
根据输入图片的差异,原图片可能需要去噪,若最后轮廓不清晰,可能需要直方图均衡化使轮廓相对清晰。
2,其次产生色调图
论文作者根据对大量铅笔图片的研究,发现它们的直方图如下,而我们要做的就是将原图片的色阶的直方图转换为以下的直方图,而以下的直方图可以由高斯分布函数,均匀分布函数和拉布拉斯分布函数带权值相加后近似得到(三个权值相加为1),它们的权值比为w1:w2:w3,此后,进行SML映射将原图片的直方图转换为以下的直方图就可以了。
注意的地方:一般采用w1:w2:w3 = 2:22:76,因为铅笔画较谈,w3越大,图片越亮,但对于有些图片,如果太亮,有些较暗的细节就会消失,此时可以选择w1:w2:w3 = 11:37:52,此外也可以根据最后效果调整三个参数大小,一般,w3介于52~76间,w1最好在10以下。
3,由铅笔画背景和色调图生成色调渲染图
我使用的是如下的铅笔背景图:
这一步主要是为了解下面这个方程组中的β矩阵,其中H(x)为上面这张铅笔背景图,J(x)为第二步生成的色调图,现在主要求解β(x),最后色调渲染图T(x) = H(x).^β(x),这样做的目的是使色调图有铅笔反复抹画的痕迹,且保持直方图与铅笔画的直方图一致:
下面是求解公式,风吹夏天博主的文章有详细的求解过程,大致思路是先把图像矩阵变成向量,再进行求解:
注意的地方:根据最后效果的考虑,需要对背景铅笔图片和生成的色调渲染图片进行gamma矫正。
4,最后将第一步生成的结构图片与第二三步生成的色调渲染图片直接点乘就可以了
注意的地方:由于实现过程中,有许多地方用到矩阵点乘和矩阵相乘,因此在计算时应该先用im2double函数将图像矩阵中每个像素映射到0~1间进行计算。
5,输入/得到彩色图像 VS 输入/得到灰度图像
输入灰度图像/彩色图像:输入时进行判断就维数判断就可以了,但在调用函数grayPencilDrawing时,输入的一定要是灰度图像;
输出灰度图像/彩色图像:输出灰度铅笔画图像,调用函数grayPencilDrawing,函数返回时,返回的就是灰度铅笔图像;输出彩色铅笔画图像,这时输入的肯定是彩色图像,需要先将原彩色图像由rgb空间转成yuv空间,对于yuv格式,其中y分量是色阶,uv分量是色调,只需要对y分量调用函数grayPencilDrawing,最后再由yuv空间转成rgb空间。以下是yuv和rgb转换公式:
注意的地方:需要对输入进行iif else条件判断。
三,具体代码
具体需要改的参数在第一个代码段中都有标识,可以先读懂代码,然后修改相关参数,此外,对于每一张图片,所需要的参数有可能不同,主要依据最后效果修改参数:
代码1:输入图片和背景图片:该代码可能作为一种接口代码,将论文中需要修改的参数都提取了出来,只需修改4~17行的相关参数,直接函数调用就可以查看结果了。此外,对最后rgb空间和yuv空间进行了考虑:
clc; clear; %==============================参数修改==================================== Input = imread('input1.jpg');%输入原始图片 dirNum = 8;%表示具有dirNum个方向的卷积核 [m, n, src] = size(Input); ks = floor(min(m/50, n/50) + 0.5);%论文中要求为高或宽的1/30,实际上不能这么大 ks = 10;%卷积核大小,建筑类图片为10~13,风景花卉类图片为3~6 strokeDepth = 2;%对生成的轮廓图多描几遍 w1 = 0.57;%高斯分布权重 w2 = 0.37;%均匀分布权重 w3 = 0.06;%拉布拉斯分布权重 Back = rgb2gray(imread('pencil.jpg'));%输入背景铅笔图 backDepth = 0.7;%对铅笔背景图pencil.jpg多描几遍 renderDepth = 0.7;%对生成的纹理渲染图多描几遍 %如果输入的是灰度图像,对其进行处理 if src == 2 gray_out = rgb2gray(Input); gray_out = grayPencilDrawing(gray_out, dirNum, ks, strokeDepth, w1, w2, w3, Back, backDepth, renderDepth); rgb_out = Input; end %如果输入的是彩色图像,对其进行处理 if src == 3 rgb_out = mat2gray(Input); R = rgb_out(:, :, 1); G = rgb_out(:, :, 2); B = rgb_out(:, :, 3); Y = 0.299*R + 0.587*G + 0.114*B; U = -0.147*R- 0.289*G + 0.436*B; V = 0.615*R - 0.515*G - 0.100*B; Y = uint8(Y .* 255); Y = grayPencilDrawing(Y, dirNum, ks, strokeDepth, w1, w2, w3, Back, backDepth, renderDepth); %yuv_out = cat(3, Y, U, V);%按照第三维组合 rgb_out(:,:,1) = Y + 1.14 * V; rgb_out(:,:,2) = Y - 0.39 * U - 0.58 * V; rgb_out(:,:,3) = Y + 2.03 * U; figure, imshow(rgb_out), title('彩色铅笔图'); imwrite(rgb_out, 'coloredPencil.jpg'); end
代码2:具体调用函数的实现,这一步也是上述算法思路前四个的实现
function Out = grayPencilDrawing(Input, dirNum, ks, strokeDepth, w1, w2, w3, Back, backDepth, renderDepth) %===============================1:获取轮廓图=============================== %读入单通道的图片 [h, w] = size(Input); I_gray = Input; I_gray1 = I_gray; %Input = histeq(Input, 256); %针对有些图片层次不清晰,若最后轮廓图不清晰,加上这两句 %figure, imshow(Input); I_gray = im2double(I_gray); imX = [abs(I_gray(:,1:(end-1)) - I_gray(:,2:end)), zeros(h, 1)];%最右边增加一列0 imY = [abs(I_gray(1:(end-1),:) - I_gray(2:end,:)); zeros(1, w)];%最下边增加一行0 imX = imX .^ 2; imY = imY .^ 2; imEdge = sqrt(imX + imY);%论文中边缘公式(1)实现 imEdge = immultiply(imEdge, 5); %%%%%figure, imshow(imEdge, []), title('边缘提取'); imwrite(imEdge, 'stroke_tmp1.jpg'); %水平方向为1的卷积核 kerRef = zeros(ks*2+1); kerRef(ks+1,:) = 1; %对卷积核依次旋转180/dirNum度并使用论文中的公式先计算8个方向卷积,计算出边缘 response = zeros(h, w, dirNum); for n = 1: dirNum ker = imrotate(kerRef, (n-1)*180/dirNum, 'bilinear', 'crop'); response(:, :, n) = conv2(imEdge, ker, 'same');%对梯度图进行八个方向卷积核,论文中边缘公式(2)实现 end [~ , index] = max(response, [], 3); %index为一个m*n矩阵,每个元素为dirNum个矩形中对应元素的最大纵坐标号(范围为1~dirNum),最大值即为该点方向 C = zeros(h, w, dirNum); for n=1:dirNum C(:,:,n) = imEdge .* (index == n); %index == 2也是一个矩阵,元素值为0或1。之后与imEdge对应元素相乘,论文中边缘公式(3)实现 end Spn = zeros(h, w, dirNum); for n = 1: dirNum ker = imrotate(kerRef, (n-1)*180/dirNum, 'bilinear', 'crop'); Spn(:, :, n) = conv2(C(:,:,n), ker, 'same'); %对得到的各个方面的响应再次进行方向卷积,论文中边缘公式(4)实现 end Sp = sum(Spn, 3); %按照第三维对这dirNum个矩阵的元素各自累加 Sp = (Sp - min(Sp(:))) / (max(Sp(:)) - min(Sp(:)));%min(sp(:))和max(sp(:))分别表示矩阵sp中元素的最小值和最大值,即灰度拉伸 Stroke = 1 - Sp;%由于梯度图背景黑线白,现在使其背景白线黑 Stroke = Stroke .^ strokeDepth;%轮廓图描深 figure, imshow(Stroke), title('Stroke图'); imwrite(Stroke, 'stroke.jpg'); %===============================2:获取色调图=============================== %直方图匹配 p = zeros(1, 256); v = zeros(1, 256); i = 0: 255; p1 = (1/9) * exp(-(255 - i)/9); %axis([0 255 0 1]); p2 = zeros(1, 256); p2(105: 225) = 1/(225-105); p3 = (1/sqrt(2*pi*11)) * exp(-(i-90).*(i-90)/(2.0*11*11)); p = p1*w1 + p2*w2 + p3*w3; t = p; %figure, plot(imhist(I_gray)/(h*w)), title('原始直方图'); for i = 1: h for j = 1: w v(I_gray1(i, j)+1) = v(I_gray1(i, j)+1) + 1; end end v = v / (h*w); for i = 2: 256 p(i) = p(i-1) + p(i); v(i) = v(i-1) + v(i); end p = p /p(256); %计算源图像到目标图像累积直方图中各灰度级的差的绝对值 scrMin = zeros(256, 256); for x = 1: 256 for y = 1: 256 scrMin(x, y) = abs(v(x) - p(y)); end end for x = 1: 256 minX = 1; minValue = scrMin(x, 1); for y = 2: 256 if(minValue > scrMin(x, y)) minValue = scrMin(x, y); minX = y; end end HistogramMapping(x) = minX; end %对原图根据HistogramMapping进行灰度级映射 I_tone = I_gray1; for x = 1: h for y = 1: w I_tone(x, y) = HistogramMapping(I_gray1(x, y) + 1) - 1; end end %%%%%figure, subplot(211), plot(t), title('目标直方图'); %%%%%subplot(212), plot(imhist(I_tone)/(h*w)), title('色调映射图的直方图');%因为进行映射时,目标图像中有些灰度值不存在,因此曲线中接近0的曲折部分表示某些灰度值不存在 figure; subplot(121), imshow(I_gray1), title('原始灰度图'); subplot(122), imshow(I_tone), title('色调映射图'); imwrite(I_tone, 'tone.jpg'); %===============================3:纹理渲染图=============================== %subplot(251), imshow(I);%运行这9行可以查看背景铅笔图效果 a = 1; for i = 0.2: 0.2: 1.8 %a = floor(i / 0.2 + 0.5) + 1; a = a + 1; I0 = ((double(Back)/255).^i); %subplot(2, 5, a), imshow(I0); end I0 = ((double(Back)/255) .^ backDepth); %figure, imshow(I0); I0 = uint8(I0 * 255); imwrite(I0, 'New_texture.jpg'); %开始计算P .^ β = J中的β矩阵 theta = 0.2; P = im2double(imread('New_texture.jpg')); J = im2double(imread('tone.jpg')); %初始化为向量方便计算 P = imresize(P, [h, w]); P = reshape(P, h*w, 1); logP = log(P); logP = spdiags(logP, 0, h*w, h*w); J = imresize(J, [h, w]); J = reshape(J, h*w, 1); logJ = log(J); %两个梯度 e = ones(h*w, 1); Dx = spdiags([-e, e], [0, h], h*w, h*w); Dy = spdiags([-e, e], [0, 1], h*w, h*w); %带入求解公式计算 A = theta * (Dx * Dx' + Dy * Dy') + (logP)' * logP; b = (logP)' * logJ; %计算向量形式的beta beta = pcg(A, b, 1e-6, 60); %转化成矩阵形式并拉伸 beta = reshape(beta, h, w); beta = (beta - min(beta(:))) / (max(beta(:)) - min(beta(:)))*5; P = reshape(P, h, w); T = P .^ beta; A = T .^ renderDepth; %%%%%imshow(A), title('色调渲染图');; imwrite(A, 'new_tone.jpg'); %===============================3:结构图与色调图整合=============================== I = im2double(imread('stroke.jpg')); S = A .* I; figure, imshow(S), title('灰色铅笔画'); imwrite(S, 'grayPencil.jpg'); Out = S; end
四,一些实现效果
原图 灰度铅笔画 彩色铅笔画