第三章:静态图像检测算法

各位做OLED驱动开发的同行,今天我们来聊聊烧屏预防中最关键的一环——怎么判断屏幕上哪些区域是“静态”的。

说白了,烧屏的本质就是某些像素点长期显示相同内容,导致发光材料老化不均匀。所以,第一步就是要精准定位这些“静止不动”的区域。我个人习惯把静态检测拆成四个模块来搞:直方图分析、帧差法运动检测、阈值设定、多区域权重评估。

3.1 基于直方图的静态区域检测

先讲直方图。为什么用它?因为简单、高效。

我早期做项目时,试过很多复杂的图像特征提取方法,什么SIFT、HOG都试过。后来发现,在嵌入式平台上跑这些算法,帧率直接掉到个位数。最后回归到直方图,效果反而更好。

核心思路是这样的:

  • 把屏幕划分成N×M个网格(比如8×8或16×16)
  • 对每个网格计算颜色直方图(RGB或YUV均可)
  • 比较相邻帧之间对应网格的直方图相似度
  • 相似度超过阈值,就判定为“疑似静态”

这里有个关键点:直方图比较用什么距离度量?我个人推荐巴氏距离(Bhattacharyya distance)。为什么?

巴氏距离的优势:

  • 对光照变化不敏感——你想想看,用户可能调亮度、开自动背光
  • 计算量适中——比卡方检验快,比相关性度量准
  • 数值范围归一化——0到1之间,阈值好设

代码实现大概长这样:

// 简化版:计算两个直方图的巴氏距离
float bhattacharyya_distance(uint32_t *hist1, uint32_t *hist2, int bins) {
    float sum = 0.0f;
    float sum1 = 0.0f, sum2 = 0.0f;
    
    for (int i = 0; i < bins; i++) {
        sum += sqrt(hist1[i] * hist2[i]);
        sum1 += hist1[i];
        sum2 += hist2[i];
    }
    
    if (sum1 == 0 || sum2 == 0) return 1.0f;
    
    return sqrt(1.0f - sum / sqrt(sum1 * sum2));
}

嗯,这里要注意:直方图的bin数量不能太多。我见过有人用256个bin,结果每个bin里像素数太少,噪声大得离谱。我个人习惯用16~32个bin,够用。

3.2 基于帧差法的运动检测

直方图能检测“内容变化”,但有个盲区——如果画面内容在网格内平移,直方图可能变化不大,但像素点其实在动。这时候就需要帧差法来补位。

帧差法的原理太简单了:

  1. 取当前帧和前一帧的对应像素
  2. 计算差值(绝对值)
  3. 差值超过阈值的像素,标记为“运动像素”
  4. 统计运动像素占比

但直接做像素级帧差,噪声会很大。我在项目中踩过一个坑:屏幕上有轻微的闪烁(比如60Hz的PWM调光),帧差法直接炸了,满屏都是运动像素。

避坑指南:我曾经因为没做预处理,被PWM噪声坑惨了。后来加了两个步骤:

  • 先做高斯模糊(3×3核就够了)
  • 再做形态学开运算(先腐蚀后膨胀)

这样能把孤立噪声点去掉,保留真正的运动区域。

帧差法的改进版本——三帧差分法,效果更好:

// 三帧差分法核心逻辑
void three_frame_diff(uint8_t *frame_t, uint8_t *frame_t1, uint8_t *frame_t2, 
                      uint8_t *motion_mask, int size) {
    for (int i = 0; i < size; i++) {
        int diff1 = abs(frame_t[i] - frame_t1[i]);
        int diff2 = abs(frame_t1[i] - frame_t2[i]);
        // 两个差值都超过阈值,才认为是运动
        motion_mask[i] = (diff1 > THRESHOLD && diff2 > THRESHOLD) ? 255 : 0;
    }
}

为什么用三帧?因为单帧差法遇到物体匀速运动时,前后两帧的差值可能很小,误判为静态。三帧法能有效避免这个问题。

3.3 静态阈值设定策略

阈值怎么设?这是最头疼的部分。设太严,稍微有点变化就判为运动,烧屏预防失效;设太松,明明在动的画面被判为静态,补偿算法乱调。

我总结了一套经验值:

场景 直方图相似度阈值 帧差法运动像素占比阈值 说明
静态桌面 0.95 1% 几乎不动,阈值要严
视频播放 0.70 30% 画面持续变化,阈值要松
网页浏览 0.85 10% 部分区域静止(如导航栏)
游戏画面 0.60 50% 快速变化,但可能有HUD

但实际项目中,你不能让用户手动选场景。我建议用自适应阈值

  • 统计过去N帧的运动像素占比,取移动平均
  • 根据平均值动态调整阈值
  • 比如:平均运动占比高 → 阈值放松;平均运动占比低 → 阈值收紧

小技巧:我习惯加一个“滞回区间”。比如判定为静态的阈值是0.9,判定为动态的阈值是0.85。这样画面在临界状态时不会频繁切换,避免补偿算法抖动。

3.4 多区域权重评估

屏幕不同区域,烧屏风险是不一样的。你想想看:

  • 状态栏、导航栏——几乎永远显示相同内容,风险极高
  • 屏幕中央——用户看视频、玩游戏,内容变化快,风险低
  • 边缘区域——可能显示时间、信号图标,风险中等

所以,不能对所有区域一视同仁。我设计了一套权重体系:

// 多区域权重配置示例
typedef struct {
    int row_start, row_end;   // 行范围
    int col_start, col_end;   // 列范围
    float static_weight;      // 静态检测权重(越高越敏感)
    float compensation_gain;  // 补偿增益(越高补偿越强)
} RegionConfig;

RegionConfig g_regions[] = {
    // 顶部状态栏区域(高权重)
    {0, 60, 0, 1080, 1.5f, 1.2f},
    // 底部导航栏区域(高权重)
    {2220, 2400, 0, 1080, 1.5f, 1.2f},
    // 中央内容区域(低权重)
    {60, 2220, 0, 1080, 0.8f, 0.6f},
};

权重评估的流程是这样的:

  1. 对每个区域分别做直方图检测和帧差法检测
  2. 乘以该区域的静态权重
  3. 加权求和得到“整体静态评分”
  4. 评分超过阈值,触发补偿机制

举个例子:状态栏区域检测到静态评分0.95,乘以权重1.5,得到1.425;中央区域静态评分0.3,乘以权重0.8,得到0.24。整体评分取加权平均,这样状态栏的静态特征会被放大,中央区域的动态特征会被忽略。

核心结论:多区域权重评估的本质,就是把有限的补偿资源,优先分配给烧屏风险最高的区域。说白了,状态栏烧屏的概率是中央区域的10倍以上,那补偿力度也该是10倍。

知识体系总览

下面这张图,是我做这个模块时的设计思路,你可以对照着理解:

静态图像检测算法体系 视频帧输入 直方图静态检测 巴氏距离 + 网格划分 帧差法运动检测 三帧差分 + 形态学处理 静态阈值设定 自适应阈值 + 滞回区间 多区域权重评估 状态栏高权重 · 中央区域低权重 触发补偿机制

从这张图可以看得很清楚:视频帧进来后,兵分两路——直方图检测和帧差法检测并行执行,结果汇聚到阈值判定模块,再经过多区域权重加权,最终决定是否触发补偿。这个流程我在三个量产项目上验证过,效果稳定。


专注资料整理