美文网首页
基于DoG的2D卡通化渲染实现流程和原理

基于DoG的2D卡通化渲染实现流程和原理

作者: hjm1fb | 来源:发表于2023-09-01 02:58 被阅读0次

    背景和目标

    卡通渲染是图形学里非真实感渲染的一种。
    卡通化分为很多流派,比如美式动画/日式动画等。我们的目标效果是AE风格化里的卡通化效果。

    效果图:
    只画线稿:


    katy perry instyle (0-00-00-00).png

    画线稿并着色:


    katy perry instyle (0-00-00-00) (1).png

    左图是原图,右图是我们自制AE卡通化插件的效果,和AE原版卡通化效果比较接近。

    渲染流程

    分为三步,本文依次标记为"前菜"(预处理 平滑图片), "主餐"(重头戏 描边) 和 "甜点"(颜色处理 色块化);

    前菜

    双边过滤

    卡通化时,我们既想看到平滑的图像,又需要有清晰的描边,所以会先选择一个合适的滤波器来预处理图像。这个滤波器就是双边过滤。
    AE双边过滤的效果图: 平滑图像,保留边缘信息


    Bilateral-Blur 图片来源: AE官方文档

    双边的意思是,滤波器通过位置权重和像素颜色相似度两个维度来做模糊平滑。因此,双边过滤能在平滑图像的同时,保留剧烈变化的像素区域,也就是边缘细节被保留了。
    核心代码:

    centralColor = SHDR_TEXTURE2D(uTexture, blurCoordinates[4]);
    gaussianWeightTotal = 0.18;
    sum = centralColor * 0.18;
    sampleColor = SHDR_TEXTURE2D(uTexture, blurCoordinates[0]);
    //计算采样点和中心点的像素颜色差距
    distanceFromCentralColor = min(distance(centralColor, sampleColor), uThreshold)/uThreshold;
    // 颜色权重,颜色差别越大,权重越低
    gaussianWeight = 0.05 * (1.0 - distanceFromCentralColor); 
    
    gaussianWeightTotal += gaussianWeight;
    sum += sampleColor * gaussianWeight;
    。。。
    。。。
    sampleColor = SHDR_TEXTURE2D(uTexture, blurCoordinates[7]);
    distanceFromCentralColor = min(distance(centralColor, sampleColor), uThreshold)/uThreshold;
    // 位置权重,这个位置离中心点更近,所以第一个系数比上面的大
    gaussianWeight = 0.09 * (1.0 - distanceFromCentralColor);
    gaussianWeightTotal += gaussianWeight;
    sum += sampleColor * gaussianWeight;
    
    sampleColor = SHDR_TEXTURE2D(uTexture, blurCoordinates[8]);
    distanceFromCentralColor = min(distance(centralColor, sampleColor), uThreshold)/uThreshold;
    gaussianWeight = 0.05 * (1.0 - distanceFromCentralColor);
    gaussianWeightTotal += gaussianWeight;
    sum += sampleColor * gaussianWeight;
    outColor = sum / gaussianWeightTotal;
    

    主餐

    高斯差分(DoG)描边

    几种常见的边缘检测方法


    FindEdge 图片来源:http://holgerweb.net/PhD/Research/papers/DoGToonNPAR11.pdf

    Sobel检测就是用一个小的基于位置加权的一阶卷积核,在x轴和y轴方向分别逐像素计算灰度的梯度值, 满足阈值的像素就标定为边缘。速度比较快。但因为卷积核比较小,所以只能找到局部的边缘,比较粗的边缘可能会漏掉


    sobel2.gif

    Canny检测

    1. 先通过高斯模糊预处理
    2. 用非极大值抑制的方法筛选出像素色值梯度变化较大的点,也就是最有可能为边缘的点。
    3. 通过双阈值检测,保留高置信度的点,舍弃置信度的点。中等置信度的点如果跟高置信度的点挨着,则也当做是边缘点,以防止边缘断断续续

    选择恰当的阈值参数后,会得到比较清晰准确的描边。但由于在梯度方法非极大值抑制,只会取梯度变化最大的像素,所以画出来的线条是单像素宽度。也就是线条的宽度无法反映真实边缘的粗细

    Canny.png

    再次对比Sobel / Canny / DoG的效果差异

    FindEdge 图片来源: http://holgerweb.net/PhD/Research/papers/DoGToonNPAR11.pdf

    高斯差分在计算效率和保留边缘特征上,取得了较好的平衡。
    原理解释:

    先来复习高斯模糊:
    我们知道,高斯模糊能够平滑图像,弱化噪点。边缘,噪点,颜色跳跃等图像变化剧烈的区域,相邻像素点之间颜色差异较大,是图像里的高频信号,在高斯模糊后被弱化。而色块/背景等颜色变化缓慢的区域对应的像素点,是低频信号,在高斯模糊后变化不大。所以高斯模糊是一个低通滤波器,保留低频信号,去除或者衰减高频信号。

    再来理解高斯差分:
    图示


    DoG 图片来源: http://holgerweb.net/PhD/Research/papers/DoGToonNPAR11.pdf

    公式: D0(coord, σ, k, I) = G(coord, σ, I) − tau * G(coord, k · σ, I)
    I表示Image,即输入图
    G表示高斯函数
    coord表示二维坐标
    σ 表示高斯分布标准差, 标准差约大,函数曲线越扁平,一般k大于1,也就是高抖的Center高斯函数减去平坦的Surround高斯函数
    k用来控制两个高斯函数的差距,当tau = 1.6时,DoG的结果和高斯拉普拉斯算子的结果差不多,具有较好的描边效果
    tau 扩展参数,用于调整两个高斯滤波器的权重,为线条增加更多变化空间

    高斯差分的意思,简单概况,就是用一个窄高斯核减去一个宽高斯核,得到的是一个带通滤波器。窄高斯核模糊强度小,去掉了噪点等非常高频的信号,保留了较多的原始图像信息。宽高斯核模糊强度大,去掉了噪点等很高频的信号的同时,也把边缘点等比较高频的信号也去掉了。两者相减,留下的就是边缘了

    DoG核心代码:

    void main(void){
        vec3 destColor = vec3(0.0);
            // 参数初始化
            float tFrag = 1.0 / cvsHeight;
            float sFrag = 1.0 / cvsWidth;
            vec2  Frag = vec2(sFrag,tFrag);
            vec2 uv = vec2(gl_FragCoord.s, cvsHeight - gl_FragCoord.t);
            float twoSigmaESquared = 2.0 * sigma_e  * sigma_e;
            float twoSigmaRSquared = 2.0 * sigma_r  *  sigma_r;
            int halfWidth = int(ceil( 2.0 * sigma_r ));
            const int MAX_NUM_ITERATION = 99999;
            vec2 sum = vec2(0.0);
            vec2 weightSum = vec2(0.0);
            //卷积核
            for(int cnt=0;cnt<MAX_NUM_ITERATION;cnt++){ 
                if(cnt > (2halfWidth+1)(2*halfWidth+1)){break;}
                int i = int(cnt / (2*halfWidth+1)) - halfWidth
                int j = cnt - halfWidth - int(cnt / (2halfWidth+1))  (2*halfWidth+1);// 近似的高斯模糊采样
                float d = length(vec2(i,j));
                // 两个高斯核,位置权重,标准差不同,赋值给kernel的x变量和y变量
                vec2 kernel = vec2( exp( -d * d / twoSigmaESquared ), 
                                    exp( -d * d / twoSigmaRSquared )); 
                vec2 L = texture2D(src, (uv + vec2(i,j)) * Frag).xx;  
                weightSum += 2.0 * kernel;
                // sum保存两个高斯模糊的结果
                sum += kernel * L; 
            }
            sum /= weightSum;
            //差分运算
            float H = 100.0  (sum.x - tau * sum.y); 
            // 检测结果小于0位为黑色边缘。通过映射函数确定灰度程度,比如为过渡浅色边缘
            float edge = ( H > 0.0 )? 1.0 : 2.0 * smoothstep(-2.0, 2.0, phi  * H ); 
            destColor = vec3(edge);
        gl_FragColor = vec4(destColor, 1.0);
    }
    

    甜点

    色彩量化(posterization)

    色差量化是减少图像中颜色数量的过程, 会让图像色块化,呈现出卡通化的风格。


    posterization.png

    我们选择比较简单的,根据灰色值来做色彩量化。
    核心代码:

    // gamma矫正 
    posterizedColor = pow(posterizedColor, vec3(GAMMA_INVERSE));
    //计算像素的灰度值
    float greyscale = 0.299 * posterizedColor.r + 0.587 * posterizedColor.g + 0.114 * posterizedColor.b;
    //uLevel 控制的是灰色值分为几级,级数越小,色块越明显
    //四舍五入归到对应的离散的灰度值
    float level = floor(greyscale * uLevel + 0.5) / uLevel;
    //计算灰度值被调整的幅度
    float adjustment = level / greyscale;
    //像素的RGB值乘以幅度,就是像素被色彩量化后的结果,就会有色块的风格了
    posterizedColor *= adjustment;
    // gamma矫正 是为了让色彩量化的效果更接近AE的效果
    posterizedColor = pow(posterizedColor, vec3(GAMMA));
    
    

    优化

    1. 双边过滤时,因为顶点着色器的执行次数更少,所以在顶点着色器里做采样坐标的计算,作为varying变量。然后在光栅化阶段,硬件会把这些坐标值自动线性插值到片段着色器里

    代码示例:

    void main() {
        gl_Position = aPos;
        vTexCoord = aTexCoord.xy;
    
        int multiplier = 0;
        vec2 blurStep;
    
        for (int i = 0; i < GAUSSIAN_SAMPLES; i++) {
            multiplier = (i - ((GAUSSIAN_SAMPLES - 1) / 2));
            blurStep = float(multiplier) * uStep;
            blurCoordinates[i] = aTexCoord.xy + blurStep;
        }
    }
    
    1. DoG时,将卷积里的指数/开根号等运算在CPU里算好,然后以uniform数组的形式传到片段着色器里,作为卷积时的运算系数,提高了渲染速度

    代码示例:

     //优化前的卷积运算
            for(int cnt=0;cnt<MAX_NUM_ITERATION;cnt++){ 
                if(cnt > (2halfWidth+1)(2*halfWidth+1)){break;}
                int i = int(cnt / (2*halfWidth+1)) - halfWidth
                int j = cnt - halfWidth - int(cnt / (2halfWidth+1))  (2*halfWidth+1);// 近似的高斯模糊采样
                float d = length(vec2(i,j));
                // 两个高斯核,位置权重,标准差不同
                vec2 kernel = vec2( exp( -d * d / twoSigmaESquared ), 
                                    exp( -d * d / twoSigmaRSquared )); 
                vec2 L = texture2D(src, (uv + vec2(i,j)) * Frag).xx;  
                norm += 2.0 * kernel;
                // 两个高斯模糊的结果
                sum += kernel * L; 
            }
      
      //优化后的卷积运算
      for (int cnt = 0; cnt < maxNumberIteration; ++cnt) {
                     int i = int(params[cnt].x);
                    int j = int(params[cnt].y);
                     vec2 kernel = vec2(params[cnt].z, params[cnt].w);
                     vec4 c = texture2D(uTexture, (uvPixel + vec2(i, j)) * Frag);
                     vec2 L = vec2(0.299 * c.r + 0.587*c.g + 0.114*c.b);
                     sum += kernel * L;
    }
    // norm是uniform值,已经在cpu里算好
    sum /= norm;
    
    1. 高斯模糊具有可分性,也就是可以通过分别对 X 轴和 Y 轴进行两次高斯模糊来提高性能。而差分高斯模糊

    D0(coord, σ, k, I) = G(coord, σ, I) − tau * G(coord, k · σ, I)
    是高斯模糊的乘法和减法运算,仍然具有x/y方向可分的性质。这点我在做本文档的时候才发现,以后抽空可以尝试优化,看看效果

    扩展-基于DoG扩展的XDoG

    在DoG的基础上,再添加一些参数,能表现更多的风格化类型。


    可以看到,设置e为较小的值,则更多的像素点会变成边缘。最后一个参数ϕ影响描边深浅的过渡梯度。所以增加的参数和tanh双曲正切函数计算增加了更多的风格化效果类型。变换不同参数,风格化结果示例如下:


    XDoG能实现的效果 图片来源: http://holgerweb.net/PhD/Research/papers/DoGToonNPAR11.pdf

    相关文章

      网友评论

          本文标题:基于DoG的2D卡通化渲染实现流程和原理

          本文链接:https://www.haomeiwen.com/subject/ryqtvdtx.html