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 本章知识体系

为了让大家更直观地理解本章的内容结构,我画了一张流程图:

RGB转Gray算法移植流程 算法原理 加权公式 + 定点化 C/C++代码编写 软件验证 + 定点化 HLS优化 PIPELINE + INTERFACE 本章关键知识点 RGB三通道加权求和公式 浮点转定点:乘2^16再右移 HLS PIPELINE II=1 实现像素级流水 AXI4-Stream接口配置

这张图把本章的三个核心模块串起来了。从算法原理出发,到C代码实现,再到HLS优化,每一步都有明确的输入输出。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 数据类型要小心: 在HLS里,ap_uint<8>和unsigned char虽然都是8位,但综合出来的硬件可能不同。我建议统一用ap_uint系列,可控性更强。
  • 边界处理: 图像边缘的像素,有些算法需要特殊处理。灰度化虽然不需要,但养成习惯总是好的。
  • 仿真验证: 别偷懒。先用C仿真跑一遍,再用RTL仿真跑一遍,对比输出结果。我曾经因为一个移位方向搞反了,浪费了两天时间。

好了,第一章就到这里。灰度化虽然基础,但它是你进入HLS图像处理世界的第一道门。把这一步走扎实了,后面的章节你会轻松很多。

课后练习: 把上面的C++代码改成定点化版本,然后用HLS综合一下,看看资源消耗和II是多少。对比一下浮点版本和定点版本的差别。

可可精品资料库