3. 图像基础知识:像素、分辨率、色彩空间

好,咱们今天聊点实在的。你手里拿个MCU,想让它“看”东西,首先得搞明白——图像在机器眼里到底长什么样?

我刚开始做嵌入式视觉那会儿,也犯过迷糊。总觉得图像就是一张漂亮的照片,但放到MCU里,它其实就是一堆数字。说白了,就是一堆排好队的0和1。

3.1 像素:图像的最小单元

像素是什么?你可以把它想象成一块小瓷砖。很多很多小瓷砖拼在一起,就成了一幅画。

每个像素就是一个点。这个点有自己的颜色和位置。在MCU里,我们用一个或多个字节来存储这个点的信息。

关键理解: 一张图片 = 像素的二维数组。MCU处理图像,本质上就是在处理这个数组。

举个例子。一个320×240的灰度图,就有320×240=76800个像素。每个像素用一个字节(0-255)表示亮度。那这张图在内存里就占76800字节,约75KB。

嗯,这里要注意。MCU的RAM通常只有几十到几百KB。一张图就能吃掉大半。所以选型时,内存是第一道坎。

3.2 分辨率:图像的精细程度

分辨率就是像素的总数。640×480、320×240、160×120,这些数字你肯定不陌生。

分辨率越高,图像越清晰,但数据量也越大。我在项目里遇到过,为了省内存,把分辨率从QVGA(320×240)降到QQVGA(160×120),结果特征点都糊了。后来我学乖了——先算好内存余量,再定分辨率。

分辨率 像素总数 灰度图大小(1字节/像素) 适用场景
QQVGA (160×120) 19,200 18.75 KB 简单物体检测、条形码
QVGA (320×240) 76,800 75 KB 人脸检测、颜色追踪
VGA (640×480) 307,200 300 KB 需要外扩SRAM

我的习惯: 先定算法需要的最小分辨率。比如做色块追踪,160×120就够。做二维码识别,至少320×240。别一上来就上VGA,MCU扛不住。

3.3 色彩空间:RGB、灰度、二值化

色彩空间,说白了就是用什么方式描述颜色。MCU上常用的就三种:RGB、灰度、二值化。

3.3.1 RGB565:MCU上的真彩色

RGB用红绿蓝三个分量表示颜色。每个分量0-255,一个像素就要3个字节。MCU内存那么金贵,谁受得了?

所以有了RGB565。红色5位、绿色6位、蓝色5位,一个像素只占2个字节。颜色损失一点,但内存省了三分之一。

// RGB565 格式:一个像素占2字节
// 高字节:R[4:0] | G[5:3]
// 低字节:G[2:0] | B[4:0]

uint16_t rgb565_pixel = ((red >> 3) << 11) | 
                        ((green >> 2) << 5) | 
                        (blue >> 3);

// 反过来,从RGB565提取分量
uint8_t r = (rgb565_pixel >> 11) << 3;
uint8_t g = ((rgb565_pixel >> 5) & 0x3F) << 2;
uint8_t b = (rgb565_pixel & 0x1F) << 3;

你想想看,一个320×240的RGB565图像,占320×240×2=153600字节,约150KB。很多MCU的RAM也就这么多。所以做彩色图像处理,内存规划要精打细算。

3.3.2 灰度图:丢掉颜色,保留亮度

灰度图每个像素只用一个字节表示亮度。0是纯黑,255是纯白。中间是深浅不一的灰。

为什么用灰度?因为很多算法不需要颜色信息。边缘检测、形状识别、光流追踪,灰度图就够了。而且数据量只有RGB565的一半。

从RGB转灰度,有个经典公式:

// 标准灰度转换公式
// Gray = 0.299*R + 0.587*G + 0.114*B
// MCU上避免浮点运算,用整数近似

uint8_t rgb_to_gray(uint8_t r, uint8_t g, uint8_t b) {
    // 使用移位代替浮点乘法
    return (r*77 + g*150 + b*29) >> 8;
}

我曾经踩过的坑: 直接用 (r+g+b)/3 求平均灰度。结果绿色物体和红色物体在灰度图里几乎一样,算法直接失效。记住,人眼对绿色最敏感,所以绿色权重最高。用标准公式,别偷懒。

3.3.3 二值化:只有黑和白

二值化就是把灰度图变成只有0和255两种值。每个像素只占1位(bit),内存省到极致。

怎么二值化?设定一个阈值。大于阈值的变白,小于阈值的变黑。

// 最简单的二值化
#define THRESHOLD 128

void binarize(uint8_t *gray_img, uint8_t *bin_img, int size) {
    for (int i = 0; i < size; i++) {
        // 每个像素只存1位,8个像素挤在一个字节里
        if (gray_img[i] > THRESHOLD) {
            bin_img[i >> 3] |= (1 << (i & 0x07));
        } else {
            bin_img[i >> 3] &= ~(1 << (i & 0x07));
        }
    }
}

但阈值怎么选?固定阈值遇到光照变化就完蛋。我建议用大津法(OTSU)自动算阈值。虽然计算量大一点,但鲁棒性好很多。

我的经验: 做二值化前,先做高斯模糊去噪。不然噪点会变成一堆小白点,干扰后续处理。我吃过这个亏,后来每次二值化前必先滤波。

3.4 在MCU上如何表示一张图片

好了,现在咱们把理论落地。在MCU里,一张图片到底怎么存?

说白了,就是一块连续的内存区域。加上一个结构体来描述它。

// 图像结构体定义
typedef struct {
    uint16_t width;      // 图像宽度
    uint16_t height;     // 图像高度
    uint8_t  format;     // 格式:0=灰度, 1=RGB565, 2=二值化
    uint8_t  *data;      // 指向像素数据的指针
    uint32_t data_len;   // 数据总字节数
} image_t;

// 创建一张灰度图
image_t img;
img.width = 320;
img.height = 240;
img.format = 0;  // 灰度
img.data_len = 320 * 240;  // 76800字节
img.data = (uint8_t *)malloc(img.data_len);

if (img.data == NULL) {
    // 内存分配失败!这是MCU上最常见的问题
    printf("Error: Not enough memory!\n");
}

// 访问第10行第20列的像素
uint8_t pixel = img.data[10 * img.width + 20];

这里有个关键点:图像数据在内存里是“行优先”存储的。第一行所有像素挨个排,然后第二行,以此类推。你访问像素时,要算好偏移量:y * width + x

总结一下: MCU上的图像 = 一块连续内存 + 一个描述结构体。像素按行排列,每个像素的字节数取决于色彩格式。灰度1字节,RGB565 2字节,二值化1位。

嗯,到这里你应该明白了。图像在MCU里不是什么玄乎的东西,就是一堆数字。你学会了怎么存、怎么取、怎么转格式,后面做算法就顺了。

下一章咱们聊聊摄像头驱动,看看怎么把这堆数字从传感器里读出来。