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

相关论文的链接: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

                              原图

                   边缘检测图

                         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 = 64, 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 == 1)
    {
        int Amount = 2 * LineLength + 1;
        int Width = Src->Width, Height = Src->Height;
        int X, Y, Z, Index, MaxValue, Sum;
        float Value;
        unsigned char *LinePS, *LinePD;

        TMatrix **Response = (TMatrix **)malloc(8 * sizeof(TMatrix));
        TMatrix *ImageEdge = NULL;
        Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &ImageEdge);
        TMatrix *LineShape = NULL;
        Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &LineShape);
        if (Ret != IS_RET_OK) goto Done8;
        Ret = GuassBlur(Src, ImageEdge, 1);            //    去除点噪音
        if (Ret != IS_RET_OK) goto Done8;
        Ret = EdgeCoarse(ImageEdge, ImageEdge);        //    两个参数相同对速度无影响,对应论文公式1
        if (Ret != IS_RET_OK) goto Done8;

        for (Z = 0; Z < 8; Z++)
        {
            Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &Response[Z]);
            if (Ret != IS_RET_OK) goto Done8;
            Ret = MotionBlur(ImageEdge, Response[Z], Amount, Z * 180.0 / 8, EdgeMode::Smear);        //    对应论文公式2    
            if (Ret != IS_RET_OK) goto Done8;            //    各个方向卷积
        }

        for (Y = 0; Y < Height; Y++)
        {
            LinePS = ImageEdge->Data + Y * ImageEdge->WidthStep;
            for (X = 0; X < Width; X++)
            {
                MaxValue = 0;
                for (Z = 0; Z < 8; Z++)
                {
                    LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep + X);            
                    if (MaxValue < LinePD[0]) 
                    {
                        Index = Z;
                        MaxValue = LinePD[0];
                    }
                }
                for (Z = 0; Z < 8; Z++)
                {
                    LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep);            //    对应公式3        
                    if (Z == Index) 
                        LinePD[X] = LinePS[X];
                    else                 
                        LinePD[X] = 0;
                }
            }   
        }
        for (Z = 0; Z < 8; Z++)
        {
            MotionBlur(Response[Z], Response[Z], Amount, Z * 180.0 / 8, EdgeMode::Smear);        //    对应公式S'    
        }
        
        for (Y = 0; Y < Height; Y++)
        {
            LinePD = LineShape->Data + Y * LineShape->WidthStep;
            for (X = 0; X < Width; X++)
            {
                Sum = 0;
                for (Z = 0; Z < 8; Z++)
                {
                    LinePS = (Response[Z]->Data + Y * Response[Z]->WidthStep);
                    Sum += LinePS[X];
                }
                LinePD[X] = (255 - 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(256 * sizeof(float));
        float *HistgramFC = (float *)IS_AllocMemory(256 * sizeof(float));
        int *Histgram = (int *)IS_AllocMemory(256 * sizeof(int));                 
        int Ua = 105, Ub = 225, Mud = 90, DeltaB = 9, DeltaD = 11, Omega1 = 76, Omega2 = 22, Omega3 = 2, Iter = 5;
        for (Y = 0; Y < 256; Y++)
        {
            if (Y < Ua || Y > Ub)                //    表1中的参数 
                Value = 0;
            else
                Value = 1.0 / (Ub - Ua);
            HistgramF[Y] = (Omega2 * Value +  1.0 / DeltaB * exp(-(255.0 - Y) / DeltaB) * Omega1 + 1.0 /sqrt(2 * PI * 11) * exp(-(Y - Mud) * (Y - Mud) / (2.0 * DeltaD * DeltaD)) * Omega3) * 0.01;
            HistgramFC[Y] = HistgramF[Y];        //    拷贝一个备份
        }
        for (Z = 0; Z < Iter; Z++)                        //    这样的直方图并不平滑,做一点平滑处理
        {
            HistgramFC[0] = (HistgramF[0] + HistgramF[1]) / 2;                                    // 第一点
            for (Y = 1; Y < 255; Y++)
                HistgramFC[Y] = (HistgramF[Y - 1] + HistgramF[Y] + HistgramF[Y + 1]) / 3;        // 中间的点
            HistgramFC[255] = (HistgramF[254] + HistgramF[255]) / 2;                            // 最后一点
            memcpy(HistgramF, HistgramFC, 256 * sizeof(float));
        }
        for (Y = 0; Y < 256; Y++)    Histgram[Y] = HistgramF[Y] * Width * Height;

        TMatrix *ToneMap = NULL;
        Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &ToneMap);
        if (Ret != IS_RET_OK) goto Done8;
        Ret = GuassBlur(Src, ToneMap, 1);                                                        //    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, 255);                            //    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 = 0; Z < 8; 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, 1, &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 = 0; Y < Height; Y++)
        {
            LinePS = Gray->Data + Y * Gray->WidthStep;
            LinePD = GrayC->Data + Y * GrayC->WidthStep;
            for (X = 0; X < Width; X++)                //    恢复V分量
            {
                LinePD[0] = LinePS[X];
                LinePD[1] = LinePS[X];
                LinePD[2] = LinePS[X];
                LinePD += 3;
            }
        }
        BlendImage(Dest, GrayC,   Dest, BlendMode::Luminosity, 255);
    Done:
        IS_FreeMatrix(&Gray);
        IS_FreeMatrix(&GrayC);
    }
}

  贴一些处理的效果:

               原图

                        处理结果图

               原图

                        处理结果图

               原图

                        处理结果图

               原图

                        处理结果图

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

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT派

六行代码!完成你的第一个机器学习算法

Google出了一个面向新手的机器学习教程,每集六七分钟,言简意赅,只掌握最基础的Python语法知识,便可以实现一些基本的机器学习算法。接下来我准备分几次...

3416
来自专栏企鹅号快讯

外国网友如何使用机器学习将邮件分类?其实很简单

AiTechYun 编辑:Yining 背景:一名叫做Anthony Dm.的外国网友试图利用机器学习将一堆未标记的电子邮件进行分类,以下是他对这次操作发表的文...

1938
来自专栏生信小驿站

差异分析③

一些研究需要不止一个调整后的p值cutoff值。 为了对重要性进行更严格的定义,可能需要log-fold-change(log-FC)超过最小值。 一般用来计算...

823
来自专栏大数据风控

评分卡模型开发-基于逻辑回归的标准评分卡实现

由逻辑回归的基本原理,我们将客户违约的概率表示为p,则正常的概率为1-p。因此,可以得到: ? 此时,客户违约的概率p可表示为: ? 评...

1.1K6
来自专栏QQ会员技术团队的专栏

基于YOLO的王者荣耀精彩时刻自动剪辑

为了丰富游戏短视频内容,针对王者荣耀,需要一套自动化剪辑精彩时刻的系统,以能够快速根据主播直播内容生成精彩时刻反馈到游戏短视频社区。

68011
来自专栏深度学习与数据挖掘实战

【今日热门】优秀资源

592
来自专栏计算机视觉战队

从Bounding Boxes中能够学习什么?

Where does it come fromThe Third Research Institute of the Ministry of Public Se...

2825
来自专栏新智元

【谷歌新项目公开】无需学编程,用手机摄像头和浏览器即可机器学习

【新智元导读】谷歌最新的 Teachable Machine 项目,可以让用户无需编程就能利用摄像头采集数据、设计机器学习。作为 AI Experiment 的...

2575
来自专栏顶级程序员

阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么?

? 在月饼事件中的新闻中提到。阿里对员工访问的界面做了一定的处理。貌似这不是简单的水印。这种处理是什么,是怎么做到的呢? =====第三次更新===== 评论...

4159
来自专栏机器学习人工学weekly

机器学习人工学weekly-2018/8/26

Safety-first AI for autonomous data centre cooling and industrial control

603

扫码关注云+社区