图像处理技术(一):边缘检测滤波器

Number of views 1

介绍

图像处理技术系列将聚焦于直接对帧缓冲中像素进行操作的技术

这类技术通常需要采用多遍渲染的方式:第一遍渲染生成像素数据,后续的渲染过程则会为这些像素施加特效或进行进一步处理。为了实现这一流程,我们会借助 OpenGL 提供的直接渲染到单个或多个纹理的功能

渲染到纹理的能力,再结合片元着色器的强大功能,能够拓展出极为丰富的应用场景。我们可以在片元着色器的输出阶段前加入额外处理步骤,以此实现亮度调整、对比度调节、饱和度优化和锐化等图像处理技术;也可以应用边缘检测、平滑(模糊)、锐化这类卷积滤波器。关于卷积滤波器的具体内容,我们会在边缘检测的相关实例中展开详细讲解。

还有一类相关技术,会在传统的颜色信息之外,向纹理中渲染更多附加信息,再通过后续的渲染过程对这些信息做进一步处理,最终生成目标图像。这类技术统称为延迟着色

本章中,我们会结合实例分别介绍上述各类技术。首先讲解用于边缘检测、模糊和泛光效果的卷积滤波器实例,随后深入探讨伽马校正多重采样抗锯齿这两个重要主题,最后通过一个完整的实例来详细说明延迟着色技术的实现流程。

本章的大部分实例都涉及多遍渲染。若要对最终渲染图像的像素应用某种滤镜,我们需要先将场景渲染到一个中间缓冲(即纹理)中;之后在最后一遍渲染中,绘制一个全屏四边形,将这个纹理渲染到屏幕上,同时在这一过程中完成滤镜的应用。在接下来的实例中,你会看到该核心流程的多种变体实现。

应用边缘检测滤波器

边缘检测是一种图像处理技术,用于识别图像中亮度发生显著变化的区域。该技术能够检测物体的边界以及表面拓扑结构的变化,广泛应用于计算机视觉、图像处理、图像分析和图像模式识别等领域。如需了解更多相关内容,可参阅 D・齐乌(D. Ziou)与 S・塔博内(S. Tabbone)于 1998 年发表的论文 ——《边缘检测技术综述》,该文刊载于《国际计算机视觉杂志》第 24 卷第 3 期。

边缘检测技术还可用于实现一些视觉效果独特的画面风格。例如,它能让三维场景呈现出类似二维铅笔素描的视觉效果,如下方配图所示。要实现这种效果,我们首先对茶壶与圆环面进行常规渲染,再在第二遍渲染过程中应用边缘检测滤波器。

image1767165849225.png

本章中我们要用到的边缘检测滤波器,其核心原理是借助卷积滤波器(或称卷积核、滤波核)实现。卷积滤波器是一个矩阵,它定义了像素的变换规则:以目标像素周围邻近像素的取值,分别与一组预设权重相乘后求和,最终得到的结果将替代原像素值。举一个简单的例子,请看以下这个卷积滤波器:

image1767165944252.png

左侧展示的是3×3 滤波器,右侧则是一个假设的像素网格

这些像素值既可以代表灰度值,也可以对应 RGB 色彩空间中某一个通道的数值。对右侧的像素集合应用该滤波器时,需要将滤波器与像素网格对应位置的数值相乘,再将所有乘积结果求和,最终得到的数值就是网格中心像素(即数值为 29 的像素)的新值。在这个例子中,计算结果为 (25 + 27 + 2 × 29 + 31 + 33),也就是 174

当然,要应用卷积滤波器,我们首先需要获取原始图像的像素数据,同时还需要一块独立的缓冲区来存储滤波后的结果。在本章的实现方案中,我们会采用双遍渲染算法来达成这一目标:第一遍渲染将图像绘制到一张纹理上;第二遍渲染则从该纹理中读取像素数据,应用滤波器完成处理,并将最终的滤波结果输出到屏幕上。

在基于卷积的边缘检测技术中,索贝尔算子(Sobel operator) 是最简单的方法之一。索贝尔算子旨在近似计算图像中每个像素位置的亮度梯度,其实现方式是对图像应用两个 3×3 滤波核。这两个滤波核分别用于计算梯度的垂直分量水平分量,随后我们可以将梯度的模长作为边缘判断的依据:当梯度模长超过某一特定阈值时,我们就判定该像素处于图像的边缘位置。

索贝尔算子所使用的两个 3×3 滤波核如下式所示:

image1767166950179.png若应用滤波核 Sx 得到的计算结果为 sx,应用滤波核 Sy 得到的计算结果为 sy,那么梯度模长的近似值可由下式给出:

image1767167268495.png

g(梯度模长)的数值超过某一特定阈值,我们便将该像素判定为边缘像素,并在最终生成的图像中对其进行高亮显示。

在本示例中,我们会将该滤波器作为双遍渲染算法的第二遍渲染流程来实现:第一遍渲染时,我们采用合适的光照模型对场景进行完整渲染,但会将渲染结果输出至一张纹理;第二遍渲染时,我们将整张纹理渲染为一个全屏填充四边形,并在这个过程中对该纹理应用上述边缘检测滤波器。

准备工作

创建一个帧缓冲区对象(Framebuffer Object,FBO),其尺寸需与主窗口完全一致。将该 FBO 的第一个颜色附着点关联至纹理单元 0中的一个纹理对象。在第一遍渲染过程中,我们会直接将画面渲染至该纹理。需确保该纹理的放大滤波器(mag filter)缩小滤波器(min filter) 均设置为 GL_NEAREST(最近邻滤波)—— 本算法不需要对像素进行任何插值处理。

将顶点信息传入顶点属性 0,法向量传入顶点属性 1,纹理坐标传入顶点属性 2。

以下统一变量(Uniform 变量) 需要在 OpenGL 应用程序中完成赋值:

  • Width:屏幕窗口的宽度(以像素为单位)
  • Height:屏幕窗口的高度(以像素为单位)
  • EdgeThreshold:判定像素为 “边缘像素” 所需的 g²(梯度模长的平方,在着色器避免开根号)最小值
  • RenderTex:与该 FBO 关联的纹理对象

所有与着色模型相关的其他 Uniform 变量,也需在 OpenGL 应用程序中完成赋值。

怎么做

要创建一个应用索贝尔(Sobel)边缘检测滤波器的着色器程序,要遵循以下步骤:

1. 顶点着色器代码如下:

#version 400
layout (location = 0) in vec3 VertexPosition;  // 顶点位置(绑定至顶点属性0)
layout (location = 1) in vec3 VertexNormal;    // 顶点法向量(绑定至顶点属性1)
layout (location = 2) in vec2 VertexTexCoord;  // 顶点纹理坐标(绑定至顶点属性2)

out vec3 Position;  // 输出:顶点在视图空间的位置
out vec3 Normal;    // 输出:顶点在视图空间的法向量
out vec2 TexCoord;  // 输出:顶点纹理坐标

uniform mat4 ModelViewMatrix;  // 模型视图矩阵
uniform mat3 NormalMatrix;     // 法向量矩阵(法向量的视图空间变换)
uniform mat4 ProjectionMatrix; // 投影矩阵
uniform mat4 MVP;              // 模型-视图-投影复合矩阵

void main()
{
 TexCoord = VertexTexCoord;
 Normal = normalize( NormalMatrix * VertexNormal); // 法向量归一化
 Position = vec3( ModelViewMatrix * vec4(VertexPosition,1.0) ); // 计算视图空间顶点位置
 gl_Position = MVP * vec4(VertexPosition,1.0); // 计算裁剪空间顶点位置
}

2. 片元着色器代码如下:

#version 400
in vec3 Position;   // 输入:顶点在视图空间的位置(来自顶点着色器)
in vec3 Normal;     // 输入:顶点在视图空间的法向量(来自顶点着色器)
in vec2 TexCoord;   // 输入:顶点纹理坐标(来自顶点着色器)

// 存储第一遍渲染结果的纹理
uniform sampler2D RenderTex;
uniform float EdgeThreshold; // 边缘判定阈值(梯度模长的平方值)
uniform int Width;           // 屏幕宽度(像素数)
uniform int Height;          // 屏幕高度(像素数)

// 子例程:用于切换第一遍/第二遍渲染的功能逻辑
subroutine vec4 RenderPassType();
subroutine uniform RenderPassType RenderPass;

// 冯氏(Phong)反射模型所需的其他uniform变量可在此声明……

layout( location = 0 ) out vec4 FragColor; // 输出最终片元颜色

// 冯氏(ADS)基础着色模型实现(ADS:环境光+漫反射+镜面反射)
vec3 phongModel( vec3 pos, vec3 norm )
{
 // 基础ADS着色模型的代码在此实现……
}

// 计算RGB颜色的亮度值(遵循ITU-R BT.709标准,贴合人眼视觉特性)
float luma( vec3 color ) {
 return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
}

// 第一遍渲染:执行常规着色并将结果输出至FBO纹理
subroutine (RenderPassType)
vec4 pass1()
{
 return vec4(phongModel( Position, Normal ),1.0);
}

// 第二遍渲染:应用Sobel边缘检测滤波器
subroutine( RenderPassType )
vec4 pass2()
{
 // 计算单个像素对应的纹理坐标步长(纹理坐标0-1对应屏幕像素宽/高)
 float dx = 1.0 / float(Width);
 float dy = 1.0 / float(Height);
 
 // 采样当前像素周围8个邻域像素的亮度值(对应3×3 Sobel核的采样位置)
 float s00 = luma(texture( RenderTex, TexCoord + vec2(-dx,dy) ).rgb);
 float s10 = luma(texture( RenderTex, TexCoord + vec2(-dx,0.0) ).rgb);
 float s20 = luma(texture( RenderTex, TexCoord + vec2(-dx,-dy) ).rgb);
 float s01 = luma(texture( RenderTex, TexCoord + vec2(0.0,dy) ).rgb);
 float s21 = luma(texture( RenderTex, TexCoord + vec2(0.0,-dy) ).rgb);
 float s02 = luma(texture( RenderTex, TexCoord + vec2(dx, dy) ).rgb);
 float s12 = luma(texture( RenderTex, TexCoord + vec2(dx, 0.0) ).rgb);
 float s22 = luma(texture( RenderTex, TexCoord + vec2(dx, -dy) ).rgb);
 
 // 计算Sobel算子的水平(sx)和垂直(sy)梯度分量
 float sx = s00 + 2 * s10 + s20 - (s02 + 2 * s12 + s22);
 float sy = s00 + 2 * s01 + s02 - (s20 + 2 * s21 + s22);
 
 // 计算梯度模长的平方(避免开平方运算,提升渲染效率)
 float dist = sx * sx + sy * sy;
 
 // 梯度模长平方超过阈值则判定为边缘(白色),否则为非边缘(黑色)
 if( dist>EdgeThreshold )
 return vec4(1.0);
 else
 return vec4(0.0,0.0,0.0,1.0);
}

void main()
{
 // 根据子例程配置,调用pass1()或pass2()
 FragColor = RenderPass();
}
0 Answers