美文网首页
基于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

相关文章

  • SpringSecurity认证原理

    认证流程原理 认证流程 SpringSecurity是基于Filter实现认证和授权,底层通过FilterChai...

  • 推荐系统实战之——基于内容CB推荐

    目录 1、基于内容推荐原理 2、核心推荐流程 3、python实现 4、总结 基于内容推荐原理 : 基于内容的推荐...

  • TCP/IP网络应用Socket编程实验报告

    摘要 基于Socket编程的基本原理和开发流程,本文设计并实现了基于单播和组播的多人聊天工具,以及基于组播的...

  • 复杂业务如何保证Flutter的高性能高流畅度

    Flutter渲染原理简介 优化之前我们先来介绍下Flutter的渲染原理,通过这部分基础了解渲染流程以及主要耗时...

  • Ⅸ非真实感渲染

    非真实感渲染 卡通风格的渲染 原理 要实现卡通渲染有很多方法,其中之一就是使用基于色调的着色技术(tone-bas...

  • 商卡排毒

    排毒瑜伽 商卡排毒的原理流程益处 商卡排毒的原理流程益处 1、商卡排毒的原理 消化道和呼吸道是人体与外界接触并从外...

  • iOS开发进阶:性能优化与稳定性优化实践

    页面卡顿原理与优化 离屏渲染原理与优化 复杂视图的渲染优化 崩溃监控方案

  • HashMap

    HashMap 的实现原理 HashMap 是基于 hashing 原理,我们通过 put() 和 get() ...

  • weex

    weex 原理weexBoxweex 全部weex实战分享weex渲染机制渲染流程weex 解读 weexUI 详...

  • Android 7.1切数据卡

    版本说明 本文基于安卓7.1的高通版本,可能和原生的实现有些出入。仅是记录高通版本的大概流程。 数据卡 对于双卡手...

网友评论

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

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