相关论文的链接:Combining Sketch and Tone for Pencil Drawing Production

第一次看《Combining Sketch and Tone for Pencil Drawing Production》一文是在两年前,随意看了一下,觉得论文里的公式比较多,以为实现有一定的难度,没有去细究,最近在作者主页上看到有 [code of direction classification] 部分代码,下载后觉得还是有自己实现的可能,下面记录下自己实现过程中的一些体会和心得。

铅笔画其实一直是一个比较难以获得较为理想效果的算法,我看到的论文里这篇文章应该是说相当优秀的。总的来说,其算法分为两个步骤:

1、Line Drawing with Strokes  得到一幅图 S。

2、Tone Mapping 得到另外一副图T。

3、得到最终结果 R = S * T;

  应该说第一步决定了最终的效果,作者通过以下四个步骤得到S图。

(1)、对原图进行边缘检测,作者论文给出的公式是:

按照这个公式实现的效果实际上检测的效果很弱,我认为作者真正意义上可能不是使用的改公式,因为这一步对最终效果的影响很大, 我采用了一些其他能够更好的检测出效果的边缘检测算法,如果Sobel或者PS里FindEges之类的算法。

作者认为这个公式得到的结果含有太多的噪音并且边缘部分的线条很多不连续,因此提出继续一下几个步骤得到更稳定的效果。

(2) 对得到的G进行8个方向的卷积,卷积核为沿指定的方向为1,其他的值均为0(实际上考虑抗锯齿问题,用了双线性插值得到卷积核的),卷积核的大小论文提出为图像宽度或高度的1/30(这个我觉得有点不行,当太大时,会有明显的不合理线条出现),具体公式如下:

在论文给出的相关代码中,有如下部分:

%% convolution kernel with horizontal direction
kerRef = zeros(ks*2+1);
kerRef(ks+1,:) = 1; %% classification
response = zeros(H,W,dirNum);
for ii = 0 : (dirNum-1)
ker = imrotate(kerRef, ii*180/dirNum, 'bilinear', 'crop');
response(:,:,ii+1) = conv2(imEdge, ker, 'same');
end

其实这里的卷积就是按照指定的角度的运动模糊,我们可以用matlab的代码来进行验证:

比如当角度为22.5(ii = 1),核的大小为5(ks = 5)时,按照上面的代码得到的ker变量为:

归一化后的结果为:

  而对应的运动模糊的卷积矩阵为(对应的matlab代码为: H = fspecial('motion', 2*5+1, 22.5):

可见只有很小的差别。

(3) 得到各个方向的卷积结果后,对每一个像素点,具有最大的卷积值的那一个方向的响应设置为G,而其他方向的响应设置为0,原文的话语是:

The classification is performed by selecting the maximum value among the responses in all directions and is written as

                 

我觉得有点不可思议的是上面语句说的是最大值,下面给出的公式确是最小值,这难道是大家的笔误。(实际上应该是最大值的)。

    还有注意的是这里的G(p)是指公式(1)中的G。

(4)对得到的各个方面的响应再次进行方向卷积,即:

论文中提出要对这个结果进行卷积并进行反相处理得到结果S,这个其实就看你自己的编码方式了。

      这里给出以上4个步骤一些中间结果:

      

         

原图                  边缘检测图                         22.5度的卷积图                                 22.5度的响应图                         中间结果图S

   第二部的Tone Mapping 实际上也是由两个步骤来实现的。

  (1)直方图匹配。

论文中并没有这样起小标题,而是用了较大的篇幅说了一堆事情,我总结一句话就是,根据对大量的手工绘制的铅笔画图像数据的观察和分析,其直方图的分布和我们拍摄的图像有很大的不同,但是都成一定的规律,这个规律可以用一定的经验公式来表达。因此,我们可以设定一个固定的直方图,然后将图像自身的直方图映射到这个直方图,作为结果。

简单的阐述下过程吧。借作者论文中的一副图像来做说明。在下图中,(a)是一副手工绘制的铅笔画,(b)图是硬性划分的阴影、中间调及高光图像,(c)图分别对应阴影、中间调及高光的直方图分布。可以看出,阴影部分基本成正态分布,中间调大致为均匀分布,而高光部分成拉普拉斯分布。因此,作者对三个部分分别构建了三个函数来模拟曲线。

a、高光部分。

在人手工绘制的铅笔画中,由于纸张一般都是白色,因此,高光占有的比例实际上肯定是非常大的,这在直方图中反应就是在接近色阶255时分布曲线越陡峭。作者提出以下函数作为这部分的分布曲线。

而关于中间调,则用一截水平分布的线条来模拟:

                 

而暗调部分则表明了图像深度的变化,用一个高斯曲线来模拟:

以上公式对于不同的铅笔画来说,各部分的权重是不一样,因此作者提出了一个综合的公式来获取最终的铅笔画对应的直方图:

  根据不同的需要,可以调节不同的权重系数来达到不同的混合效果。

作者根据经验,提出了一组数据:

如果权重都相同,三部分的曲线如下图所示:

可见暗调部分的比例和人工绘制的不协调,如果按照上表中论文给出的数据,得到的最终混合直方图效果如下图:

似乎也和人工的结果不一致,因此我认为此处也是论文的一个笔误或者错误,w1和W3的值很明显弄反了,即W1应该为52,W3为11,修改后的直方图为:

  基本和人工绘制的一致了,同时注意到上述曲线有两个巨变之处,实际处理时需要对曲线进行一定程度的平滑最好。

  附绘制曲线的matlab代码:

a=1:255;
p1 = 1/9*exp(-(255-a)/9);
p3=1/sqrt(2*pi*11) * exp(-(a-80).*(a-80) / (2.0*11*11));
p2= 1:255;
p2(:)=1/(225-105);
p2(1:105)=0;
p2(225:255)=0;
p = 0.52 * p1 + 0.37 * p2 + 0.11 * p3;
plot(a,p)

接下来的工作就是进行直方图匹配,这方面的资料可以参考何斌那本VC++的数字图像处理数,里面有SML和GML两种匹配方式。

(2)纹理渲染

这一部分论文说的也很简单,就是求解一个方程:

这不是我擅长的东西,有兴趣的朋友可能要自己研究下,我也没有实现它,由这一步得到中间结果T。

那最后一步就是将S 和 T相乘,就类似于PS中的正片叠底 混合算法。

以上的一些操作都是针对灰度图像,对于彩色图像,如果直接将三通道分开,然后分别调用灰度算法,再合成这样处理, 是有问题的,出来的结果很不理想,这主要是由于各通道在进行Line Drawing with Strokes时所获得的线条方向不太可能完全一致,导致合成后偏色,解决的方式有多种,比如将RGB转换到HSL空间,然后对L分量进行处理,然后在转换到RGB空间,或者借用LAB空间作为中转平台也是可以的。

下面贴出我的C++部分的核心代码(并不能直接运行,大概体现了算法的思路):

extern IS_RET EdgeCoarse(TMatrix *Src, TMatrix *Dest);
extern IS_RET MotionBlur(TMatrix *Src, TMatrix *Dest, int Length, float Angle, EdgeMode Edge);
extern IS_RET SMLHistgramMaping(TMatrix *Src, TMatrix *Dest, int* HistgramB, int *HistgramG, int *HistgramR);
extern IS_RET BlendImage(TMatrix *Base, TMatrix *Mixed, TMatrix *Result, BlendMode BlendOp, int Opacity);
extern IS_RET GuassBlur(TMatrix *Src, TMatrix *Dest, float Radius);
extern IS_RET DecolorizationWithContrastPreserved(TMatrix *Src, TMatrix *Dest, int Level = , float Sigma = 0.05); IS_RET __stdcall PencilDrawing(TMatrix *Src, TMatrix *Dest, int LineLength)
{
if (Src == NULL || Dest == NULL) return IS_RET_ERR_NULLREFERENCE;
if (Src->Data == NULL || Dest->Data == NULL) return IS_RET_ERR_NULLREFERENCE;
if (Src->Width != Dest->Width || Src->Height != Dest->Height || Src->Channel != Dest->Channel || Src->Depth != Dest->Depth || Src->WidthStep != Dest->WidthStep) return IS_RET_ERR_PARAMISMATCH;
if (Src->Depth != IS_DEPTH_8U || Dest->Depth != IS_DEPTH_8U) return IS_RET_ERR_NOTSUPPORTED;
IS_RET Ret = IS_RET_OK; if (Src->Data == Dest->Data)
{
TMatrix *Clone = NULL;
Ret = IS_CloneMatrix(Src, &Clone);
if (Ret != IS_RET_OK) return Ret;
Ret = PencilDrawing(Clone, Dest, LineLength);
IS_FreeMatrix(&Clone);
return Ret;
} if (Src->Channel == )
{
int Amount = * LineLength + ;
int Width = Src->Width, Height = Src->Height;
int X, Y, Z, Index, MaxValue, Sum;
float Value;
unsigned char *LinePS, *LinePD; TMatrix **Response = (TMatrix **)malloc( * sizeof(TMatrix));
TMatrix *ImageEdge = NULL;
Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, , &ImageEdge);
TMatrix *LineShape = NULL;
Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, , &LineShape);
if (Ret != IS_RET_OK) goto Done8;
Ret = GuassBlur(Src, ImageEdge, ); // 去除点噪音
if (Ret != IS_RET_OK) goto Done8;
Ret = EdgeCoarse(ImageEdge, ImageEdge); // 两个参数相同对速度无影响,对应论文公式1
if (Ret != IS_RET_OK) goto Done8; for (Z = ; Z < ; Z++)
{
Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, , &Response[Z]);
if (Ret != IS_RET_OK) goto Done8;
Ret = MotionBlur(ImageEdge, Response[Z], Amount, Z * 180.0 / , EdgeMode::Smear); // 对应论文公式2
if (Ret != IS_RET_OK) goto Done8; // 各个方向卷积
} for (Y = ; Y < Height; Y++)
{
LinePS = ImageEdge->Data + Y * ImageEdge->WidthStep;
for (X = ; X < Width; X++)
{
MaxValue = ;
for (Z = ; Z < ; Z++)
{
LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep + X);
if (MaxValue < LinePD[])
{
Index = Z;
MaxValue = LinePD[];
}
}
for (Z = ; Z < ; Z++)
{
LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep); // 对应公式3
if (Z == Index)
LinePD[X] = LinePS[X];
else
LinePD[X] = ;
}
}
}
for (Z = ; Z < ; Z++)
{
MotionBlur(Response[Z], Response[Z], Amount, Z * 180.0 / , EdgeMode::Smear); // 对应公式S'
} for (Y = ; Y < Height; Y++)
{
LinePD = LineShape->Data + Y * LineShape->WidthStep;
for (X = ; X < Width; X++)
{
Sum = ;
for (Z = ; Z < ; Z++)
{
LinePS = (Response[Z]->Data + Y * Response[Z]->WidthStep);
Sum += LinePS[X];
}
LinePD[X] = ( - ClampToByte(Sum) * 0.5); // The final pencil stroke map S is obtained by inverting pixel values and mapping them to [0,1].
}
} float *HistgramF = (float *)IS_AllocMemory( * sizeof(float));
float *HistgramFC = (float *)IS_AllocMemory( * sizeof(float));
int *Histgram = (int *)IS_AllocMemory( * sizeof(int));
int Ua = , Ub = , Mud = , DeltaB = , DeltaD = , Omega1 = , Omega2 = , Omega3 = , Iter = ;
for (Y = ; Y < ; Y++)
{
if (Y < Ua || Y > Ub) // 表1中的参数
Value = ;
else
Value = 1.0 / (Ub - Ua);
HistgramF[Y] = (Omega2 * Value + 1.0 / DeltaB * exp(-(255.0 - Y) / DeltaB) * Omega1 + 1.0 /sqrt( * PI * ) * exp(-(Y - Mud) * (Y - Mud) / (2.0 * DeltaD * DeltaD)) * Omega3) * 0.01;
HistgramFC[Y] = HistgramF[Y]; // 拷贝一个备份
}
for (Z = ; Z < Iter; Z++) // 这样的直方图并不平滑,做一点平滑处理
{
HistgramFC[] = (HistgramF[] + HistgramF[]) / ; // 第一点
for (Y = ; Y < ; Y++)
HistgramFC[Y] = (HistgramF[Y - ] + HistgramF[Y] + HistgramF[Y + ]) / ; // 中间的点
HistgramFC[] = (HistgramF[] + HistgramF[]) / ; // 最后一点
memcpy(HistgramF, HistgramFC, * sizeof(float));
}
for (Y = ; Y < ; Y++) Histgram[Y] = HistgramF[Y] * Width * Height; TMatrix *ToneMap = NULL;
Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, , &ToneMap);
if (Ret != IS_RET_OK) goto Done8;
Ret = GuassBlur(Src, ToneMap, ); // Initially, the grayscale input I is slightly Gaussian smoothed.
if (Ret != IS_RET_OK) goto Done8;
SMLHistgramMaping(ToneMap, ToneMap, Histgram, Histgram, Histgram); // we adjust the tone maps using simple histogram matching in all the three layers and superpose them again.
BlendImage(ToneMap, LineShape, Dest, BlendMode::Multiply, ); // We combine the pencil stroke S and tonal texture T by multiplying the stroke and texture values for each pixel to accentuate important contours
Done8:
IS_FreeMatrix(&ImageEdge);
IS_FreeMatrix(&ToneMap);
IS_FreeMatrix(&LineShape);
IS_FreeMemory(HistgramF);
IS_FreeMemory(HistgramFC);
IS_FreeMemory(Histgram);
for (Z = ; Z < ; Z++)IS_FreeMatrix(&Response[Z]);
IS_FreeMemory(*Response);
}
else
{
unsigned char *LinePS, *LinePD;
int X, Y, Z, Width = Src->Width, Height = Src->Height;
TMatrix *Gray = NULL, *GrayC = NULL;
IS_RET Ret = IS_CreateMatrix(Src->Width, Src->Height, IS_DEPTH_8U, , &Gray);
if (Ret != IS_RET_OK) goto Done;
Ret = IS_CreateMatrix(Src->Width, Src->Height, IS_DEPTH_8U, Src->Channel, &GrayC);
Ret = DecolorizationWithContrastPreserved(Src, Gray);
if (Ret != IS_RET_OK) goto Done;
PencilDrawing(Gray, Gray, LineLength);
for (Y = ; Y < Height; Y++)
{
LinePS = Gray->Data + Y * Gray->WidthStep;
LinePD = GrayC->Data + Y * GrayC->WidthStep;
for (X = ; X < Width; X++) // 恢复V分量
{
LinePD[] = LinePS[X];
LinePD[] = LinePS[X];
LinePD[] = LinePS[X];
LinePD += ;
}
}
BlendImage(Dest, GrayC, Dest, BlendMode::Luminosity, );
Done:
IS_FreeMatrix(&Gray);
IS_FreeMatrix(&GrayC);
}
}

  贴一些处理的效果:

   

   

   

   

              原图                                            处理结果图

  和论文里处理的效果还有不小的差距,这主要是由于最后一步纹理没有做,然后就是还有细节有问题,不过也算有点收获。

提供一个测试小工具: http://files.cnblogs.com/files/Imageshop/PencilDrawing.rar

****************************作者: laviewpbt   时间: 2015.2.21    联系QQ:  33184777 转载请保留本行信息**********************

关于Cewu Lu等的《Combining Sketch and Tone for Pencil Drawing Production》一文铅笔画算法的理解和笔录。的更多相关文章

  1. 【NPR】铅笔画

    写在前面 今天打算写一篇跟Unity基本无关的文章.起因是我上个星期不知怎么的搜到了一个网站 ,里面实现的效果感觉挺好的,后来发现是2012年的NPAR会议的最佳论文.看了下文章,觉得不是很难,就想着 ...

  2. CVPR 2015 papers

    CVPR2015 Papers震撼来袭! CVPR 2015的文章可以下载了,如果链接无法下载,可以在Google上通过搜索paper名字下载(友情提示:可以使用filetype:pdf命令). Go ...

  3. Fine-Grained(细粒度) Image – Papers, Codes and Datasets

    Table of contents Introduction Survey papers Benchmark datasets Fine-grained image recognition Fine- ...

  4. Official Program for CVPR 2015

    From:  http://www.pamitc.org/cvpr15/program.php Official Program for CVPR 2015 Monday, June 8 8:30am ...

  5. 三维网格去噪算法(L0 Minimization)

    [He et al. 2013]文章提出了一种基于L0范数最小化的三角网格去噪算法.该思想最初是由[Xu et al. 2011]提出并应用于图像平滑,假设c为图像像素的颜色向量,▽c为颜色向量的梯度 ...

  6. cg tut

    Gesture Drawing with Alex Woo Gesture Drawing with Alex Woo and Louis Gonzales http://eisneim.com/?p ...

  7. (转)Multi-Object-Tracking-Paper-List

    Multi-Object-Tracking-Paper-List 2018-08-07 22:18:05 This blog is copied from: https://github.com/Sp ...

  8. (zhuan) awesome-object-proposals

      awesome-object-proposals  A curated list of object proposals resources for object detection. This ...

  9. KDD2016,Accepted Papers

    RESEARCH TRACK PAPERS - ORAL Title & Authors NetCycle: Collective Evolution Inference in Heterog ...

随机推荐

  1. 【转】利用反射快速给Model实体赋值

    原文地址:http://blog.csdn.net/gxiangzi/article/details/8629064 试想这样一个业务需求:有一张合同表,由于合同涉及内容比较多所以此表比较庞大,大概有 ...

  2. leetcode--5. Longest Palindromic Substring

    题目来自 https://leetcode.com/problems/longest-palindromic-substring/ 题目:Given a string S, find the long ...

  3. 《PDF.NE数据框架常见问题及解决方案-初》

    <PDF.NE数据框架常见问题及解决方案-初> 1.新增数据库后,获取标识列的值: 解决方案:    PDF.NET数据框架,已经为我们考略了很多,因为用PDF.NET进行数据的添加操作时 ...

  4. Oracle冷备迁移脚本(文件系统)

    Oracle冷备迁移脚本(文件系统) 两个脚本: 配置文件生成脚本dbinfo.sh 网络拷贝到目标服务器的脚本cpdb16.sh 1. 配置文件生成脚本 #!/bin/bash #Usage: cr ...

  5. 【分布式】Chubby与Paxos

    一.前言 在上一篇理解了Paxos算法的理论基础后,接下来看看Paxos算法在工程中的应用. 二.Chubby Chubby是一个面向松耦合分布式系统的锁服务,GFS(Google File Syst ...

  6. Python(六)面向对象、异常处理、反射、单例模式

    本章内容: 创建类和对象 面向对象三大特性(封装.继承.多态) 类的成员(字段.方法.属性) 类成员的修饰符(公有.私有) 类的特殊成员 isinstance(obj, cls) & issu ...

  7. C# 文件下载之断点续传

    注意,本文所说的断点续传特指 HTTP 协议中的断点续传.本文主要聊聊思路和关键代码,更多细节请参考本文附带的 demo. 工作原理 HTTP 协议中定义了一些请求/响应头,通过组合使用这些头信息.我 ...

  8. 后缀数组(suffix array)详解

    写在前面 在字符串处理当中,后缀树和后缀数组都是非常有力的工具. 其中后缀树大家了解得比较多,关于后缀数组则很少见于国内的资料. 其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现, ...

  9. .Net语言 APP开发平台——Smobiler学习日志:如何实现快速跳转网页

    Smobiler是一个在VS环境中使用.Net语言来开发APP的开发平台,也许比Xamarin更方便 样式一 一.跳转网页代码(Button的Click事件) Private Sub Button1_ ...

  10. C/C++内存泄漏及检测

    参考 http://www.cnblogs.com/skynet/archive/2011/02/20/1959162.html