4. 图像灰度化算法移植:RGB转Gray算法原理、C/C++代码编写、HLS优化指令初探
各位同学,欢迎来到《HLS图像加速算法移植实战》的第一章。
说实话,每次带新人做FPGA图像处理项目,我都是从“灰度化”这个点切入的。为什么?因为它足够简单,但又包含了算法移植的完整流程——从原理到C代码,再到HLS优化,一条线全串起来了。
今天我们就来啃下这第一块骨头。
4.1 RGB转Gray算法原理
先问大家一个问题:一张彩色图像,在计算机里是怎么存的?
嗯,最常见的格式就是RGB——每个像素用R(红)、G(绿)、B(蓝)三个分量表示。而灰度图呢,每个像素只有一个亮度值。
那怎么把三个数变成一个数?说白了,就是加权求和。
核心公式:
Gray = 0.299 × R + 0.587 × G + 0.114 × B
这三个系数怎么来的?我简单解释一下:人眼对绿色最敏感,对蓝色最不敏感。所以绿色权重最高,蓝色最低。这是ITU-R BT.601标准里定义的,也是工业界最常用的方案。
当然,也有简化版本:
- 平均值法: Gray = (R + G + B) / 3 —— 简单,但效果偏暗
- 仅取绿色法: Gray = G —— 最省资源,但丢失了红蓝信息
我个人习惯用标准加权法。之前在某个工业检测项目里,客户要求灰度图必须保留原始图像的对比度特征,用平均值法出来的图,边缘检测效果差了一大截。后来换成加权法,问题就解决了。
4.2 C/C++代码编写
算法原理搞清楚了,接下来就是写代码。我们先在PC上验证功能,再移植到HLS。
4.2.1 纯软件实现(C++)
#include <iostream>
#include <vector>
#include <cmath>
// RGB转Gray函数
void rgb2gray(const unsigned char* rgb_in,
unsigned char* gray_out,
int width, int height) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
unsigned char r = rgb_in[idx + 0];
unsigned char g = rgb_in[idx + 1];
unsigned char b = rgb_in[idx + 2];
// 标准加权法
int gray = (int)(0.299f * r + 0.587f * g + 0.114f * b);
if (gray > 255) gray = 255;
if (gray < 0) gray = 0;
gray_out[y * width + x] = (unsigned char)gray;
}
}
}
int main() {
// 假设有一张640x480的RGB图像
int width = 640, height = 480;
std::vector<unsigned char> rgb(width * height * 3, 128);
std::vector<unsigned char> gray(width * height, 0);
rgb2gray(rgb.data(), gray.data(), width, height);
std::cout << "灰度化完成!" << std::endl;
return 0;
}
这段代码很简单,但有个坑——浮点运算。在FPGA里,浮点乘加是很贵的。所以我们需要做定点化。
4.2.2 定点化优化
把浮点系数放大成整数,再移位回来:
// 定点化版本:系数放大2^16倍
#define R_WEIGHT 19595 // 0.299 * 65536
#define G_WEIGHT 38469 // 0.587 * 65536
#define B_WEIGHT 7472 // 0.114 * 65536
int gray = (R_WEIGHT * r + G_WEIGHT * g + B_WEIGHT * b) >> 16;
你看,这样就把浮点运算变成了整数乘加和移位。在HLS里,这种写法综合出来的硬件资源会少很多。
小技巧: 我一般用2^16作为放大倍数,因为右移16位在硬件里就是截掉低16位,不消耗任何逻辑资源。
4.3 HLS优化指令初探
代码写好了,怎么让它跑得快?这就轮到HLS优化指令上场了。
我们先看一个最基础的HLS版本:
#include "hls_stream.h"
#include "ap_int.h"
typedef ap_uint<8> pixel_t;
typedef ap_uint<24> rgb_pixel_t;
void rgb2gray_hls(hls::stream<rgb_pixel_t>& rgb_in,
hls::stream<pixel_t>& gray_out,
int rows, int cols) {
#pragma HLS INTERFACE axis port=rgb_in
#pragma HLS INTERFACE axis port=gray_out
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
#pragma HLS PIPELINE II=1
rgb_pixel_t rgb_val = rgb_in.read();
pixel_t r = rgb_val.range(23, 16);
pixel_t g = rgb_val.range(15, 8);
pixel_t b = rgb_val.range(7, 0);
ap_uint<32> gray = R_WEIGHT * r + G_WEIGHT * g + B_WEIGHT * b;
gray = gray >> 16;
gray_out.write((pixel_t)gray);
}
}
}
这里我用了两个关键指令:
- #pragma HLS INTERFACE axis —— 把接口配置成AXI4-Stream,这是视频处理最常用的接口
- #pragma HLS PIPELINE II=1 —— 让循环流水线化,目标是一个时钟周期处理一个像素
为什么II=1这么重要?
你想想看,如果II=2,处理一张1080p的图像就要多花一倍的时间。在视频领域,帧率是硬指标。我曾经有个项目,就因为II没压到1,导致帧率只有30fps,客户直接拒收。后来优化了数据通路,II=1,帧率跑到60fps,一次过。
注意: 不是所有循环都能实现II=1。如果循环体里有数据依赖,或者资源冲突,综合器会自动增加II。这时候就需要进一步优化,比如数组分割、数据缓存等。我们后面章节会详细讲。
4.4 本章知识体系
为了让大家更直观地理解本章的内容结构,我画了一张流程图:
这张图把本章的三个核心模块串起来了。从算法原理出发,到C代码实现,再到HLS优化,每一步都有明确的输入输出。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 数据类型要小心: 在HLS里,ap_uint<8>和unsigned char虽然都是8位,但综合出来的硬件可能不同。我建议统一用ap_uint系列,可控性更强。
- 边界处理: 图像边缘的像素,有些算法需要特殊处理。灰度化虽然不需要,但养成习惯总是好的。
- 仿真验证: 别偷懒。先用C仿真跑一遍,再用RTL仿真跑一遍,对比输出结果。我曾经因为一个移位方向搞反了,浪费了两天时间。
好了,第一章就到这里。灰度化虽然基础,但它是你进入HLS图像处理世界的第一道门。把这一步走扎实了,后面的章节你会轻松很多。
课后练习: 把上面的C++代码改成定点化版本,然后用HLS综合一下,看看资源消耗和II是多少。对比一下浮点版本和定点版本的差别。