4、RAW图统计信息:直方图分析、最大最小值统计、均值与标准差、坏点统计方法

各位同学,咱们接着聊RAW图调试。上一章讲了怎么把RAW图抓出来、怎么看格式。但光看图像本身,很多时候是看不出门道的。你得学会看数据,看统计信息。

我刚开始做ISP调试那会儿,就盯着图像看,觉得「嗯,这张图挺亮,那张图有点暗」。结果被老工程师一句话点醒了:「你眼睛会骗你,但直方图不会。」从那以后,我养成了一个习惯——拿到一张RAW图,先跑一遍统计信息。

今天咱们就聊聊,RAW图里那些最基础的统计量,到底怎么看、怎么用。

4.1 直方图分析:图像的「体检报告」

直方图是什么?说白了,就是统计每个灰度级别上有多少个像素。横轴是像素值(0到1023,对于10bit RAW来说),纵轴是像素个数。

我习惯把直方图比作一张图像的「体检报告」。你一看直方图的形状,就能大概知道这张图的曝光情况、对比度、动态范围有没有被充分利用。

直方图的三种典型形态:

  • 左偏峰:像素集中在低值区域,图像偏暗,可能欠曝
  • 右偏峰:像素集中在高值区域,图像偏亮,可能过曝
  • 中间集中:像素集中在中间区域,对比度偏低,图像发灰

你想想看,一张正常的室外场景RAW图,直方图应该是什么样?理想情况下,它应该覆盖整个动态范围,从暗部到亮部都有分布,没有明显的「断崖」或「尖峰」。

我在项目中遇到过一件事:某款传感器在低光照下,直方图总是在低值区域出现一个「小鼓包」。一开始以为是噪声,后来仔细分析才发现是暗电流不均匀导致的固定模式噪声。嗯,这个案例后面讲坏点检测时还会提到。

4.1.1 直方图的计算方法

计算直方图其实很简单。对于一张M×N的RAW图,每个像素值范围是0到2^B-1(B是bit数)。我们创建一个长度为2^B的数组,遍历每个像素,对应位置的计数加1。

// 伪代码:计算10bit RAW图的直方图
uint32_t histogram[1024] = {0};
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        uint16_t pixel = raw_image[y][x];
        histogram[pixel]++;
    }
}

代码很简单,但实际调试时要注意:直方图的bin数量不要太多。10bit数据有1024个bin,看起来精细,但统计意义不大。我一般会合并成64个或128个bin,这样看起来更清晰,也更容易发现趋势。

小技巧:调试时可以把直方图叠加显示在图像旁边。我习惯用Python的matplotlib快速画一下,看看分布是否合理。如果发现直方图在0值或最大值处有「堆积」,那就要小心了——可能是饱和或者裁切了。

4.2 最大最小值统计:快速判断动态范围利用情况

最大最小值,听起来简单吧?就是找图像里最亮的像素和最暗的像素。但这里有个坑——RAW图里的最大值不一定是真实的场景亮度

为什么?因为传感器有饱和电平。当光照超过一定强度,像素的电荷就满了,再亮也是最大值。所以,如果你看到一张图的RAW最大值是1023(10bit),那不一定说明场景里有那么亮的东西,更可能是传感器饱和了。

我个人习惯的做法是:先看最大值是否接近饱和值。如果接近,说明高光部分可能已经裁切了,需要降低增益或曝光时间。再看最小值是否接近0。如果接近0,说明暗部可能被噪声淹没了,或者有黑电平偏移的问题。

注意:不要只看单个像素的最大最小值。传感器本身有噪声,个别像素可能异常高或异常低。我一般会统计前0.1%的最大值和后0.1%的最小值,这样更鲁棒。

4.3 均值与标准差:衡量整体亮度和对比度

均值和标准差,这两个统计量是「黄金搭档」。均值告诉你图像整体有多亮,标准差告诉你图像的对比度有多大。

举个例子:一张灰卡的RAW图,均值应该在中间灰度附近(比如10bit下是512左右),标准差应该很小,因为整张图亮度均匀。而一张风景图,均值可能偏暗或偏亮,标准差会比较大,因为场景里有亮有暗。

我曾经调试过一款安防摄像头,客户反馈夜间图像「一片漆黑」。我一看RAW图的均值,只有30多(10bit),标准差也只有20多。这说明什么?说明图像不仅暗,而且几乎没有对比度,细节全丢了。后来调整了增益策略和黑电平,均值提升到200左右,标准差也上来了,图像才「活」过来。

4.3.1 均值和标准差的计算

// 计算均值和标准差
uint64_t sum = 0;
uint64_t sum_sq = 0;
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        uint16_t pixel = raw_image[y][x];
        sum += pixel;
        sum_sq += pixel * pixel;
    }
}
double mean = (double)sum / (width * height);
double variance = (double)sum_sq / (width * height) - mean * mean;
double stddev = sqrt(variance);

这里要注意:计算方差时,sum_sq可能会溢出。对于一张1200万像素的12bit RAW图,sum_sq的最大值大约是(4095^2)×12M ≈ 2×10^14,已经超过32位整数的范围了。所以一定要用64位整数,或者用浮点数累加。

我的习惯:调试时我会把均值和标准差画成曲线,随着参数调整实时更新。这样能直观地看到参数变化对图像整体亮度的影响。比如调整黑电平,均值会跟着变;调整增益,均值和标准差都会变。

4.4 坏点统计方法:别让一颗老鼠屎坏了一锅粥

坏点,是传感器制造过程中不可避免的缺陷。有的像素永远亮(白点),有的永远暗(黑点),有的响应不正常(闪烁点)。这些坏点如果不处理,会在图像上形成固定的亮点或暗点,非常影响画质。

坏点检测的方法有很多,我挑几个最常用的讲讲。

4.4.1 基于邻域比较的坏点检测

这是最直观的方法。对于每个像素,比较它和周围8个邻域像素的差异。如果差异超过某个阈值,就认为它是坏点。

// 基于邻域比较的坏点检测(简化版)
bool is_bad_pixel(uint16_t pixel, uint16_t neighbors[8], uint16_t threshold) {
    uint16_t min_val = min(neighbors, 8);
    uint16_t max_val = max(neighbors, 8);
    // 如果像素值远大于邻域最大值,可能是白点
    // 如果像素值远小于邻域最小值,可能是黑点
    if (pixel > max_val + threshold || pixel < min_val - threshold) {
        return true;
    }
    return false;
}

这个方法简单,但有个问题:阈值怎么设?设小了,会把正常像素误判为坏点;设大了,又会漏掉真正的坏点。

我一般会先拍一张均匀光照的灰卡图,然后统计每个像素和邻域均值的差异分布。正常情况下,差异应该服从高斯分布。那些偏离3σ以上的像素,基本可以判定为坏点。

4.4.2 基于多帧统计的坏点检测

单帧检测容易受噪声干扰。更可靠的方法是拍多帧图像,对每个像素计算均值和方差。

  • 固定白点:多帧均值明显高于邻域均值,且方差很小
  • 固定黑点:多帧均值明显低于邻域均值,且方差很小
  • 闪烁点:方差明显大于邻域方差

实战经验:我曾经调试一款手机摄像头,发现暗光下总有固定的彩色亮点。用多帧统计一分析,发现这些像素的均值比邻域高了20%以上,但方差正常。确认是固定白点后,在ISP的坏点校正模块里做了替换处理,问题就解决了。

4.4.3 坏点统计的工程实现

在实际项目中,坏点检测通常分两步:

  1. 离线标定:在产线上拍多帧均匀图,生成坏点坐标列表
  2. 在线校正:ISP运行时,根据坏点列表对每个坏点进行插值替换

坏点列表一般保存在传感器的OTP(一次性可编程)存储器里,或者保存在系统的校准文件中。我见过有些方案把坏点列表存成BMP格式的掩膜图,白点标记为255,正常点标记为0,这样加载起来很方便。

注意:坏点检测不是一劳永逸的。传感器在使用过程中,可能会产生新的坏点。所以有些高端方案会做「在线坏点检测」,在ISP运行过程中实时检测并更新坏点列表。不过这会增加计算量,需要权衡。

小结

好了,今天的内容就到这里。咱们讲了直方图、最大最小值、均值标准差、坏点统计,这些都是RAW图调试中最基础的统计信息。你想想看,这些统计量就像医生的听诊器,能帮你快速判断一张RAW图的「健康状况」。

下一章,咱们会深入讲讲黑电平校正和增益控制,这两个是RAW图处理的第一步,也是最重要的一步。到时候我会分享一些我在项目中踩过的坑,保证让你少走弯路。

嗯,今天就先到这儿。有问题随时找我。