相关论文的链接: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. 锋利的jQuery--jQuery事件,动画(读书笔记二)

    1.注意$(document).ready()方法和window.onload方法之间的细微区别 $(document).ready()在DOM树构建完成就会执行,而window.onload是在DO ...

  2. Maven中安装本地Jar包到仓库中或将本地jar包上传

    摘要 maven install 本地jar 命令格式 mvn install:install-file -DgroupId=<group_name> -DartifactId=<a ...

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

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

  4. C# 本质论 第三章 操作符和控制流

    操作符通常分为3大类:一元操作符(正.负).二元操作符(加.减.乘.除.取余)和三元操作符( condition?consequence:alternative(consequence和alterna ...

  5. Maven环境配置

    1.下载Maven: 下载地址:http://maven.apache.org/ 2..安装 Maven 如果需要使用到 Maven ,必须首先安装 Maven , Maven 的下载地址在 Apac ...

  6. 安装MySql for Visual Studio的坑

    阅读目录 问题描述 解决过程 解决方案 总结 回到顶部 问题描述 安装MySql for Visual Studio 一般来说是为了能在VS的服务器数据连接的数据源中能选择MySql类型,如下图: 但 ...

  7. GJM : Unity3D HIAR -【 快速入门 】 七、使用本地识别包

    使用本地识别包 本文将向您介绍如何在 Unity 工程中使用本地识别包. Step 1.下载本地识别包 前往 HiAR 管理后台,上传您需要识别的图片并下载识别包,您可以获得一个 unitypacka ...

  8. 修改nginx配置文件解决dx2.5下载附件停止不动的问题

    在下载论坛附件的时候,总是停止在某个字数数不动 如下图 后来查看log发现 如下图 权限拒绝 发现后nginx的配置文件的启动者有关系 改了下 user 为 root 居然好了

  9. ArcGIS Engine开发之空间查询

    空间查询功能是通过用户选择的空间几何体以及该几何体与当前地图中要素之间的几何关系进行空间查找,从而得到查询结果的操作. 相关类与接口 空间查询相关的类主要是SpatialFilter类,其实现的接口主 ...

  10. [转]HttpModule的认识

    HttpModule是向实现类提供模块初始化和处置事件.当一个HTTP请求到达HttpModule时,整个ASP.NET Framework系统还并没有对这个HTTP请求做任何处理,也就是说此时对于H ...