三色墨水屏怎样用两行误差保住渐变细节

2026-10-164
AI 快速预览详细 收起
本文介绍了如何在三色墨水屏上保留渐变细节的方法。通过C语言实现的误差扩散算法,确保灰阶不混入红点,并用两行误差缓冲完成转换。适用于152像素宽的屏幕,解决了图像细节丢失的问题。文章详细说明了选色、误差扩散和位图打包的过程,并提供了完整的源码和验证记录。

一张带红色提示条的图片,放到黑白红墨水屏上,最容易出现两种尴尬:灰色渐变突然断成黑白两块,浅红背景也变成一条生硬的分界线。文字还在,层次却没了。

这和像素不够多是两回事。一个像素只能选黑、白、红,中间色需要靠周围像素的比例去表达。逐点选最接近的颜色,当然省事,却把每个像素丢掉的误差都扔了。

下面用一份可运行的 C 实现,把选色、误差扩散、位图打包放在同一条数据通路里。目标很具体:三色输出互斥,灰阶不混入红点,152 像素宽时用两行误差缓冲完成转换。

图1 合成输入与两个算法输出

图1全部由程序生成。中间是“逐点最近色”的对照,右边是本文实现的输出,均为数字图像,放大的小方点就是像素。它能验证算法如何分配颜色,不能替代墨水屏实物的色偏、反射率或残影测试。

先把量化器放回真实的数据通路

这块卡片的 MCU 是 STM32F401CCU6,外部存储是 W25Q32JVSSIQ。读取同版 EDA 图可以核对到,Flash 使用 CS_1、SCK_1、MISO_1、MOSI_1;屏幕侧另有 SCK_2、CS_2、DC、RST、MOSI_2 和 BUSY。

其中 Flash 的四条信号接 PA4、PA5、PA6、PA7;屏幕侧分别接 PB10、PB12、PB13、PB14、PB15、PA8。这里说的是图上的网络连接,具体 GPIO 模式和外设复用仍由显示驱动配置。

图2 同版EDA原理图的MCU与Flash局部

图2来自实际 EDA 导出,保留原始连线与标号。本文的修改位于“已经解码的 RGB 行”与“显示驱动消费的逻辑位平面”之间,Flash、USB、充电电路都不参与量化计算。

图3 同版PCB顶底层

图3是同一工程重新导出的 PCB 顶层与底层。底层没有镜像,是从顶向下看底层铜箔的透视方向。本文不改板,也不据此给出整板量产或电源可靠性的结论。

这个位置很适合做成独立模块。上游可以是 BMP 解码器,也可以是串口收到的 RGB 数据;下游可以是不同控制器的三色屏驱动。把它们直接写进同一个函数,才会让调图变成调一整套固件。

一个像素先选颜色,再拆成两个平面

先约定三个数字颜色:黑是 (0,0,0),白是 (255,255,255),红是 (255,0,0)。这是编码 RGB 空间中的简化调色板,并非屏幕实测的色度值,也不是完整的 CMYK 模型。

对经过误差修正的像素,分别计算它到三种颜色的三通道平方距离,选距离最小的一种。不做开方,大小关系一样。距离相等时固定按黑、白、红的顺序决定,避免不同平台跑出不同结果。

图4 联合选色与位平面生成流程

关键在于只有一个标签。不能先独立做一张黑色抖动图,再独立做一张红色抖动图,最后才考虑同一位置该听谁的。两次独立决策需要额外的冲突规则;一个标签则从生成方式上就排除了双重有效。

核心选色代码如下,完整文件中的调色板使用放大 16 倍的数值。

int32_t best_distance = INT32_MAX;
unsigned chosen = 0;
for (unsigned k = 0; k < 3; ++k) {
    int32_t distance = 0;
    for (unsigned c = 0; c < 3; ++c) {
        const int32_t d = v[c] - palette[k][c];
        distance += d * d;
    }
    if (distance < best_distance) {
        best_distance = distance;
        chosen = k;
    }
}

中性灰的三个分量相等。选黑或选白后,三个通道留下的误差仍相等,后续像素也保持这个性质。这个调色板下,红色不会成为中性灰的最近色,因此灰色输入不会凭空长出红点。这个结论针对本文的数值模型,验证中还覆盖了全部 256 个灰度等级。

红色提示条却有一个取舍。纯红可以原样保留,浅红会由红白点阵表达,深红则会出现红黑点阵。若业务要求红色区域必须连成整片,应在量化前明确分区策略,不能一边要求连续色块,一边要求保留同一色块里的所有渐变。

两行误差够用,边缘判断不能省

选完颜色,输入与输出之间的差就是量化残差。本文使用从左到右的 Floyd–Steinberg 扩散:右侧得到 7/16,左下得到 3/16,正下得到 5/16,右下得到 1/16。RGB 三个分量分别传播,选色则始终联合进行。

图5 扩散权重和边界规则

当前行只会影响尚未处理的右邻点和下一行,所以不必保存整幅误差图。两行轮换即可;新的一轮先清空下一行,再把当前行累积的误差接着用。

边缘要逐个邻点判断。最左一列没有左下邻点,但右侧、正下、右下依然存在。最后一行没有下一行,向右的传播仍然有效。把所有传播都塞进一个“不是边缘”的条件,会让整圈边界都失去扩散。

const int32_t e = v[c] - palette[chosen][c];
if (x + 1u < w) {
    cur[x + 2u][c] += (e * 7) / 16;
    next[x + 2u][c] += e / 16;
}
next[x + 1u][c] += (e * 5) / 16;
if (x > 0) next[x][c] += (e * 3) / 16;

这里的 x+1 是带哨兵列的内部下标;有效像素仍从 x=0 开始。越界权重直接丢弃,不把其他权重重新归一化。末行算出的下一行误差随图像结束丢弃,当前行向右的误差照常使用。

实现把一个颜色单位细分为 16 份,RGB 输入先乘 16,允许误差保留小数部分。修正后的分量限幅到 0…4080,再计算距离和残差;负数除以 16 使用 C11 的向零截断规则,避免用有符号右移冒充相同计算。

最大距离不超过 3×4080²=49,939,200,能放进 int32_t。残差乘 7 也不会越界。限幅和整数截断会丢掉部分误差,因此这里不承诺严格守恒或最优视觉质量;得到的是一个确定、可复现、便于移植的基线。

从两个输出字节检查颜色契约

本文规定逻辑位 1 表示该颜色有效,最高位对应最左像素。黑白红标签分别变成 (1,0)、(0,0)、(0,1),其中前一位属于黑平面,后一位属于红平面。

每行输出先清零,再按唯一标签置位。

const uint8_t mask = (uint8_t)(0x80u >> (x & 7u));
if (chosen == 0) black[x >> 3u] |= mask;
else if (chosen == 2) red[x >> 3u] |= mask;
图6 位平面实际输出与已知字节例子

例如连续八个像素是“黑、白、红、黑、红、白、黑、红”,对应的黑字节必须是 0x92,红字节必须是 0x29。两个字节按位与为零。这个例子既检查颜色,也检查字节内的方向。

宽度不被 8 整除时,行大小按 (width+7)/8 向上取整,最后一个字节的无效尾位保持零。本实现支持宽度 1…152;每次新图都要调用 epd_begin(),不能把上一张图的残差带过来。

屏幕控制器可能规定 0 有效,也可能有不同的行方向、发送顺序和 RAM 指令。适配应放在显示驱动边界。本文没有把未知的控制器协议混进量化器,附件也不包含可直接烧录点屏的整机固件。

把内存数字和实际验证对上

152×152 的 RGB888 整帧占 69,312 B,双灰度帧占 46,208 B。STM32F401CC 的 SRAM 为 64 KiB,前者连单帧都装不下,后者还没有算文件系统、USB、显示驱动和栈。

本实现的两行三通道误差数组占 3,696 B,加上宽度、轮换标志与结构体对齐后,状态结构为 3,700 B。再加一行 RGB 的 456 B 和两行打包输出的 38 B,显式缓冲合计 4,194 B。

图7 缓冲预算和验证记录

这是这些缓冲本身的预算,不是整机总 RAM,也没有计函数调用栈。若显示驱动需要先保存整幅黑、红位图,还需另外准备 5,776 B。如果驱动允许按平面分次写入,可重读同一 RGB 图、从清零状态重跑两次量化,分别发送黑与红;输入必须完全一致,代价是多读一次、再算一次。

验证采用 Zig 0.13.0 的 C 编译器。C11 主机库与命令行程序实际运行通过,另生成了 Cortex-M4 的独立目标文件;目标编译不代表已经完成固件链接、烧录或屏幕实测。

主机检查共 442 组图像、169,419 个像素、16,287 项断言。包括每一种支持宽度、随机 RGB、4,096 色网格、256 级中性灰、纯色保持、已知位图字节、输出尾位、缓冲区哨兵、短缓冲拒绝、状态重置,以及短文件和多余输入。C 的两行实现逐像素对照 Python 的整帧误差模型,结果一致。

32×32 的合成中性灰块也能直观看出面积比例。输入灰度 128,输出恰好是 512 个黑点、512 个白点,平均数字 RGB 为 127.5;输入 192 时为 245 黑、779 白,平均约 193.99。边缘丢弃、限幅和取整都会造成有限图像的偏差。它们是数字统计,不是亮度测量。

想复跑图1,附件中的 verify.py 会生成同一张输入,构建程序并检查输出。换成自己的图时,先用可信的图片解码工具输出 RGB888 原始字节,再交给量化器。文件解码、缩放和透明背景合成需要在这个边界之前完成;文字和二维码通常还应避免过度抖动,单独保留硬边会更合适。

完整源码、运行说明和验证记录见附件 epaper-tricolor-v1.0.0.zip。包内 README.md 给出主机构建命令与 MCU 接入约定,results/verification.json 是本次运行结果。最小核心只有 epd_quant.c 和 epd_quant.h,不依赖 OpenCV、动态内存或浮点库。

资料与范围:硬件图取自 ZERO--0 的 ePaper_card 工程,其项目及所标注的驱动板参考工程均标识 Public Domain;许可含义按平台项目授权许可协议核对。器件容量参考 ST 官方 STM32F401CC 资料。图2、图3来自同版实际 EDA 的高清导出与裁剪,未改连线;其余为本文代码或说明图。本文独立实现的量化库采用 MIT 许可,验证范围为数字模型、主机运行及目标对象编译,未做实体屏幕测试。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -