GPU Gems 2 · Chapter 40. Computer Vision on the GPU

把计算机视觉反向映射到纹理与片元流水线,用过滤、归约、特征布局和多 GPU 取舍搭出实时路径。

学习目标

  • 能解释图像如何经过纹理、片元程序和结果纹理组成的 GPU 视觉流水线
  • 能修改 Computer Vision Lab 的阈值、归约遍数和错误模式,预测边缘、质心与读回量的变化
  • 能回答:为什么把密集计算留在 GPU、只在阶段边界读回少量统计量,通常比每遍读回整张图更合适?

先把相机画面送进一条流水线

把相机想成一台不断吐出方格纸的机器。每个小格都可以同时做同一种检查,检查完的方格纸再交给下一台机器;如果每一步都把整张纸搬回桌面,机器就会频繁停下来等搬运。

本章解决的问题是:怎样把“从画面找出边缘、区域和特征”拆成一串规则的小步骤,并让每一步的结果直接成为下一步的输入?没有这条路径,中央处理器会被重复的逐像素计算和总线往返占满,留给交互逻辑的时间就会变少。

把“看图”反转成一条可重复的图形流水线输入纹理camera image片元程序one fragment / pixel结果纹理filtered texture下一阶段next pass input每个输出位置只写回自己的位置:规则输出,适合 SIMD 并行render-to-texture / frame buffer copy → 下一次过滤的输入

1. 视觉任务的 GPU 形状:一个位置写回一个位置

图形硬件原本要把数值变成画面;视觉任务则把画面变成数值,正好把方向反过来。先把相机帧初始化为纹理,再把纹理映射到与图像等大的矩形上。矩形的一个片元对应一个输出位置,fragment program 就像普通 for 循环的循环体,只是所有位置可以并行执行。

一遍处理结束后,把结果放入新的纹理;下一遍把它当作输入。这个边界很重要:当前片元只写自己的结果,不需要把一份结果任意地撒到许多位置。32 位浮点纹理还能保留足够的中间精度,适合连续的视觉计算。

每一遍只做一种规则的过滤RGB原始相机帧pass 1HSV颜色分割pass 2blur x/y可分离滤波pass 3mask候选区域pass 415 × 15 的二维核 → 先横向、再纵向多一遍 pass,少很多 texture lookup

如果一个算法只是逐点变换,流水线很直接;真正需要设计的是“全图都做什么、最后怎样合成一个答案”。边缘检测是前者与后者的交界:每个位置先产生梯度,再用邻居比较把线变细。

2. Canny:把边缘拆成可并行的过滤接力

原书把 Canny 拆成三类片元程序。第一类用可分离的 Gaussian filter 平滑并求导;第二类在每个位置计算梯度强度和方向;第三类沿梯度的前后方向读取邻居,决定当前点是不是局部峰值。把 15 × 15 的二维核拆成 1 × 15 与 15 × 1,牺牲一次 pass,换来更少的纹理读取。

非极大值抑制不是简单的亮度阈值:它先看梯度指向哪里,再在那条方向上找峰值。因此梯度方向量化为 3 × 3 邻域可走的八个方向;当前强度只有大于前后两个样本才留下。最后再用阈值调节环境噪声下的灵敏度。

Canny:从模糊的变化到一条细边缘1filterGaussian x / y2gradientmagnitude + direction3suppressforward / backward4edgethin line非极大值抑制只留下沿梯度方向比邻居更强的点点击播放、暂停、单步或拖动进度,观察每个 pass 如何接力

第 1 / 4 步 · 先平滑并求导

把 Canny 拆成四个片元 pass:每一步都产生下一步所需的纹理。

// WebGL2 片元阶段:当前像素沿梯度方向比较前后样本
vec4 magDir = texture(gradientTexture, uv);
vec2 direction = normalize(magDir.xy);
vec2 forwardUv = uv + direction * texelSize;
vec2 backwardUv = uv - direction * texelSize;
float forwardMag = texture(gradientTexture, forwardUv).z;
float backwardMag = texture(gradientTexture, backwardUv).z;
float isPeak = step(forwardMag, magDir.z) * step(backwardMag, magDir.z);
float isEdge = isPeak * step(edgeThreshold, magDir.z);
outColor = vec4(vec3(isEdge), 1.0);

这段代码的关键不是把所有邻域一次性展开,而是先拿到已经编码好的方向与强度,再沿方向取两个样本。对应原书的 fragment program 时,WebGL2 用 texture 和显式的 sampler2D;数据流仍然是“当前片元读取、当前片元写回”。

3. 颜色分割与矩:从整张 mask 压到一个质心

跟踪手部可以先把 RGB 转成 HSV,再按与目标颜色的距离阈值产生 mask。mask 中的像素输出 1,其他位置输出 0;同时输出 xy 和颜色分量,就能在后续归约中得到 M00M10M01。质心是 (M10 / M00, M01 / M00),所以它不需要 CPU 扫完整张图。

GPU 归约可以先把每一行压成一个值,再把这一列压成一个值。最后只读回一个像素,CPU 负责除法和跟踪状态更新;大部分乘加仍留在 GPU 上。这个“先密集算、后小结果读回”的边界同样适用于图像配准中 A x = b 的矩阵初始化。

从每个像素的统计量归约到一个质心1/x/y0/0/01/x/y0/0/0mask → (E, xE, yE)第一遍row sums第二遍column sumcentroid = (M10 / M00, M01 / M00)只在最后读取一个像素,减少 CPU↔GPU 往返
// mask 片元阶段:为后续两遍归约准备 M00/M10/M01
vec4 hsv = rgbToHsv(texture(cameraTexture, uv));
float inside = step(distance(hsv, meanHsv), skinThreshold);
vec2 pixel = uv * imageSize;
outStats = vec4(inside, pixel.x * inside, pixel.y * inside, hsv.z * inside);

这里的 outStats 不是最终画面,而是一张给归约 pass 使用的统计纹理。常见错误是把统计纹理当成普通颜色贴图去显示;它的通道各自有含义,读回时要按相同的分量协议解释。

4. 图像配准与特征:让少数点得到固定长度的描述

VideoOrbits 处理相机平移、倾斜、旋转或缩放后两帧的配准。每个矩阵元素都是全图许多乘法和加法的和,因此可以像矩统计一样:片元先写出每个位置对某个元素的贡献,再用归约得到矩阵项;等 8 × 8 的 A 和向量 b 准备好后才读回,CPU 再解线性系统。

特征向量是另一种输出形状:不是每个像素一份结果,而是少数特征点各有一长行。一个 16 × 16 邻域被分成 16 个 4 × 4 区域,每区把梯度方向量化到 8 个直方图 bin,连接后得到 16 × 8 = 128 个元素。先由 CPU 快速找到少量点,再让 GPU 为每个点画一行顶点并填充向量,可以同时保持灵活的纹理坐标和规则的输出布局。

从“全图求和”切换到“稀疏点的向量布局”VideoOrbitsper-pixel multiply/add↓ GPU reductionA x = bfeature point16 × 8 = 128histogram bins每个特征点占一行,每个点写出一条固定长度的向量先由 CPU 找到少数坐标,再让 GPU 并行填充这些行
// 特征点的一行输出:每个 fragment 对应一个 histogram bin
vec4 gradient = texture(gradientTexture, localUv);
float bin = quantizeDirection(gradient.y);
float weight = gradient.z * localGaussian(localUv);
vec4 histogram = vec4(0.0);
histogram = addToBin(histogram, bin, weight);
featureRow[featureIndex] = packHalf(histogram);

这类工作与全图过滤不同:工作单元是“一个特征点的一行”,而不是“一个图像位置”。因此顶点布局和纹理坐标绑定本身就是算法的一部分;只要输出行长度固定,CPU 就能在帧与帧之间用欧氏距离寻找最佳匹配。

5. 并行部署:把搬运预算留给真正的状态

如果应用同时需要 Canny、手部跟踪和特征向量,可以把互不依赖的阶段分到不同 GPU。GPU 0 处理过滤与边缘,GPU 1 处理颜色分割和矩,GPU 2 处理特征行;中央处理器只编排阶段边界。原书讨论了从 PCI 到 PCI Express/SLI 的演进,但不变的判断是:只有当各任务有足够的独立工作,多个 GPU 才能抵消同步与共享状态的成本。

把互不等待的视觉算法分到不同 GPUCPU orchestrator只在阶段边界读回GPU 0filter + CannyGPU 1HSV + momentsGPU 2feature vectorsshared memory 让结果互相交换;独立 pass 才真正获得并行收益

先猜一猜:在下面的实验中,把 edge threshold 调高会让候选边缘变多还是变少?把 reduction passes 增加,为什么质心稳定性会提高但不代表每个场景都值得增加 pass?打开 scatter 错误模式,观察规则写回为何消失。

当前路径:Canny
edge candidates63%
hand mask92%
centroid stability100%
feature confidence80%
readback1 mask texture
片元吞吐占主导;把 2D 核拆成 x/y 两遍。

三步验收:从片元 pass 到可部署的视觉系统

分步1 / 3

第一步:把图像拆成连续的过滤 pass

先将相机帧放入纹理,用等尺寸矩形保证一个位置对应一个片元;每一遍只做一种过滤,输出纹理直接成为下一遍输入。Canny 的高斯、梯度、抑制和阈值因此可以分别检查。

把“看图”反转成一条可重复的图形流水线输入纹理camera image片元程序one fragment / pixel结果纹理filtered texture下一阶段next pass input每个输出位置只写回自己的位置:规则输出,适合 SIMD 并行render-to-texture / frame buffer copy → 下一次过滤的输入
每一遍只做一种规则的过滤RGB原始相机帧pass 1HSV颜色分割pass 2blur x/y可分离滤波pass 3mask候选区域pass 415 × 15 的二维核 → 先横向、再纵向多一遍 pass,少很多 texture lookup

本章小结

  • 视觉任务可反向映射到纹理→片元程序→结果纹理的流水线。
  • 可分离滤波用多一遍 pass 换更少的二维邻域读取。
  • 矩归约把整张 mask 压成少量状态,再交给 CPU 更新。
  • 特征向量用“每点一行”的布局处理稀疏、长输出。
  • 多 GPU 的收益取决于独立工作是否足以覆盖同步成本。

练习

问题 1|画出数据流。 请写出 Canny 的四个阶段,并说明为什么“结果纹理成为下一阶段输入”比每阶段读回 CPU 更适合实时处理。

问题 2|修改 Demo 代码。 为 Computer Vision Lab 增加一个 readbackMode 开关,比较“每遍读回整张 mask”和“行列归约后只读一个像素”两种模式,并在界面显示读回字节数。你要在哪里插入同步点?

问题 3|场景选型。 现在有一个全图 Canny、一个手部质心和几十个角点的 128 维描述。哪些工作适合逐像素过滤,哪些适合归约或按点输出?为什么不能用同一种输出布局?

名词解释

本章出现的专业名词,用大白话再讲一遍。

fragment program
separable filter
nonmaximal suppression
moment
VideoOrbits
feature vector
multi-GPU

资料与写作方式声明

本章以GPU Gems 2 · Chapter 40. Computer Vision on the GPU权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…