第三章:静态图像检测算法
各位做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 基于帧差法的运动检测
直方图能检测“内容变化”,但有个盲区——如果画面内容在网格内平移,直方图可能变化不大,但像素点其实在动。这时候就需要帧差法来补位。
帧差法的原理太简单了:
- 取当前帧和前一帧的对应像素
- 计算差值(绝对值)
- 差值超过阈值的像素,标记为“运动像素”
- 统计运动像素占比
但直接做像素级帧差,噪声会很大。我在项目中踩过一个坑:屏幕上有轻微的闪烁(比如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},
};
权重评估的流程是这样的:
- 对每个区域分别做直方图检测和帧差法检测
- 乘以该区域的静态权重
- 加权求和得到“整体静态评分”
- 评分超过阈值,触发补偿机制
举个例子:状态栏区域检测到静态评分0.95,乘以权重1.5,得到1.425;中央区域静态评分0.3,乘以权重0.8,得到0.24。整体评分取加权平均,这样状态栏的静态特征会被放大,中央区域的动态特征会被忽略。
核心结论:多区域权重评估的本质,就是把有限的补偿资源,优先分配给烧屏风险最高的区域。说白了,状态栏烧屏的概率是中央区域的10倍以上,那补偿力度也该是10倍。
知识体系总览
下面这张图,是我做这个模块时的设计思路,你可以对照着理解:
从这张图可以看得很清楚:视频帧进来后,兵分两路——直方图检测和帧差法检测并行执行,结果汇聚到阈值判定模块,再经过多区域权重加权,最终决定是否触发补偿。这个流程我在三个量产项目上验证过,效果稳定。