
在嵌入式设备上跑通图像分割模型很多人以为最难的是模型推理本身。等模型真正部署到 STM32H747 上拿到推理结果之后新的问题很快就出现了模型输出的只是一堆概率和类别索引人眼根本看不出效果。如果只是想验证“分割准不准”一帧一帧截图传回电脑不现实直接在屏幕上叠加半透明彩色掩码又发现 CPU 被大量像素操作拖住画面刷新跟不上。这个阶段最容易被低估却是决定一个视觉 Demo 能不能真正“跑起来给人看”的关键。本集要解决的就是这个问题如何基于 STM32H747 的 DMA2D高效完成图像分割模型结果的实时可视化。不是用 CPU 逐像素画掩码再混合而是把“颜色填充、图像拷贝、两层合成”这些事情交给硬件去并行处理让 CPU 继续专注在推理和控制逻辑上。读完这篇文章你应该能理解 DMA2D 在视觉显示链路里的定位知道自己手上的 H747 工程应该在哪一步调用 DMA2D也能掌握一套可移植的分割结果叠加显示方案。这篇文章适合正在做嵌入式视觉、MCU 端 AI 推理、STM32 图形显示相关项目的开发者。如果你之前只做过“推理 串口打印”的验证方式那么本集的内容会帮你把整套流程从“能算”推进到“能看”。1. 嵌入式分割落地时显示这块才是真瓶颈先看一个常见场景。一个基于 UNet 或类似结构的语义分割模型输入是 320×240 的 RGB 图像输出的是 320×240 个像素的分类结果。若按类别通道做 argmax最终会得到一张 8bit 的标签图每个像素的值代表这个像素属于哪个类别比如 0 表示背景1 表示道路2 表示行人。到了这一步真正的问题才浮出水面。标签图是一块很“抽象”的数据直接显示出来的图像几乎全黑因为像素值从 0 到类别总数之间跨度很小。它的实际意义需要经过一个映射把类别 ID 转成肉眼可辨的颜色才能形成效果图。若要把分割区域的轮廓看得清楚还需要把这层彩色结果以半透明方式叠加到原图上像 PC 端 OpenCV 的addWeighted接口那样做一次 alpha 混合。在 PC 上Python 里调一次cv2.addWeighted非常轻松几万像素的运算对 CPU 来说不值一提。但在 MCU 上问题的性质完全不同。以 640×480 的 VGA 分辨率为例一帧就有 30 万个像素。标签图转彩色掩码需要访问 30 万个类别 ID写入 30 万个 ARGB 像素再做原图和掩码的 alpha 混合又要读取 60 万个像素做 30 万次加权运算最终写回显存。这些操作如果全部由 Cortex-M7 主核执行每帧可能只增加十几毫秒甚至几十毫秒的负担。单看一次还好如果推理本身已经占用了 50ms显示再占用 20ms帧率就会明显下降。更重要的是嵌入式视觉系统往往还有摄像头采集、显示刷新、外设交互等多条数据流在并行工作。CPU 一旦陷入大量像素搬运和计算其他任务就容易出现抖动。真正做过产品化项目的人都知道视觉 Demo 在实验室能跑通很容易能长时间在线预览、交互不卡顿、推理和显示互不干扰才是难点。于是我们需要的是一个不依赖 CPU 逐像素操作的硬件路径。STM32H747 内部集成的 DMA2D正好就是为这种场景准备的专用图形加速外设。它虽然不像 GPU 那样能跑任意着色器但针对颜色填充、块拷贝、格式转换和 alpha 混合这些嵌入式可视化最常见的基础操作可以做到非常高效。2. DMA2D 能做什么从简单搬运到二维混合DMA2D 的完整名称是 2D DMA 控制器它不是普通 DMA 那种“从 A 地址搬到 B 地址”的线性搬运器。它运行在独立于 CPU 的状态机里专门处理矩形像素块。当你告诉它源地址、目标地址、图像宽度和高度之后它会按行扫描完成传输期间还可以对像素格式做转换或者执行前景层与背景层的混合运算。对嵌入式显示开发者来说最直观的说法是你可以像操作一个微型图形引擎一样使用 DMA2D。它通常有四种经典模式工作模式功能描述典型应用寄存器到存储器用固定颜色填充一块矩形区域清屏、画纯色背景存储器到存储器将一块图像区域拷贝到另一块区域图像搬移、图层切换、帧缓冲刷新存储器到存储器并格式转换在拷贝过程中完成 RGB888、RGB565、ARGB8888 等格式互转摄像头帧格式转换、显示格式适配存储器到存储器并混合将前景层与背景层按 alpha 值混合后输出半透明菜单、分割掩码叠加、图标浮层这里最容易混淆的是 DMA2D 和普通 DMA。普通 DMA 在嵌入式系统里负责搬运连续数据它并不知道数据是一张图片还是串口数据也不会对像素做任何解释。DMA2D 则始终以“图像”为操作对象能够识别像素格式理解行宽和行距还可以自动跳过行尾的空白区域。这个特性在图像处理里非常关键因为很多显存布局并不是一行紧接一行的紧凑排列而是每一行之间存在对齐填充或保留区域。另一个容易让人忽略的优点是 DMA2D 对 CPU 完全异步。启动一次混合传输之后CPU 可以立刻返回去做推理的下一个阶段或者去处理网络协议、传感器事件。当 DMA2D 传输完成后可以通过中断通知 CPU。这样显示刷新和模型推理可以形成一条流水线避免“先算完再做可视化”这种串行等待的出现。如果之前用过 TouchGFX、LVGL 这类图形库你会发现它们的底层刷新机制也在默默使用 DMA2D。当一个界面需要把多张带透明度的图片合成到背景上时图形库最终会把这些层级关系映射到 DMA2D 的混合模式。现在我们把同样的能力用在图像分割结果可视化上本质上做的也是一回事只是“前景图”变成了模型输出的分割掩码。3. 图像分割结果可视化到底要处理几个环节在写代码之前先把图像分割结果可视化这件事拆解清楚。它不是一个单一函数而是一条完整的数据处理链。第一个环节是“从概率图到标签图”。语义分割网络的一个关键设计是输出的特征图通常有 C 个通道C 等于类别数量。每个像素在每个类别上都有一个概率值表示这个像素属于该类别的置信度。要想得到直观的分割图最常见的做法是对这个概率张量做 argmax也就是取概率最大的那个类别作为该像素的最终标签。在 MCU 端模型经过量化后这个操作通常可以直接在推理输出缓冲上完成结果写入一块uint8_t数组每个元素的值就是类别 ID。第二个环节是“从标签图到颜色图”。有了类别 ID 之后需要建立一套类别到颜色的映射规则也就是调色板。比如背景用黑色透明、树干用棕色、草地用绿色、天空用蓝色。在图像分割中这种配色表通常由应用场景决定实际工程里甚至需要根据不同项目调整。医学图像分割场景中不同器官或病变区域颜色差异要大广告牌分割或街景分割场景中前景物体要醒目同时不能完全盖住原图。第三个环节是“从颜色图到叠加图”。把彩色的分割结果直接显示在一张纯黑背景上可以算作一种可视化风格但视觉效果不如把分割掩码半透明叠加到原图上那么直观。最终工程中更常用的效果是原图仍然清晰可见分割区域以带透明度的一层颜色涂在原图之上形成类似“蒙了一层半透明彩色玻璃纸”的感觉。第四个环节是“从叠加图到显示缓冲”。STMCU 通过 LTDC 读取帧缓冲扫描到屏幕所以要显示什么最终都要写入到一块连续的帧缓冲内存中。如果是非直接内存访问的屏幕上位机或串口可视化则需要通过接口把最终图像转发出去。拆开以后容易发现第四步本身就是 DMA2D 的典型应用领域而第三步被很多人误以为只能靠 CPU 逐像素完成。实际上STM32H747 的 DMA2D 完全可以在数据搬运和格式转换的同时完成混合运算。在理想情况下标签图甚至不需要先经 CPU 扩展成 ARGB 像素可以直接作为一组索引数据交给 DMA2D 的前景层配合调色板完成颜色映射。这一块的具体实现空间很大下文会展开对比。4. 环境准备先把“摄像头 推理 显示”链路理清本集讨论的方案需要一块带显示控制器的 STM32H747 开发板或者一块已经把 LTDC、DMA2D 外设引出来的自制板。STM32H747 是一款双核 MCU内部通常包含 Cortex-M7 主核和 Cortex-M4 从核M7 主频相对更高适合运行神经网络推理或图像预处理M4 可以负责传感器采集、通信、控制逻辑等任务。开发环境方面主要使用的是 STM32CubeMX 生成基础工程配合 STM32CubeH7 HAL 库进行外设初始化。DMA2D 相关的底层驱动可以由 CubeMX 生成的工程直接提供外设句柄也可以自己根据参考手册写寄存器级驱动。对于刚入门的读者建议先用 CubeMX 配好 LTDC 和 DMA2D把“显示一张静态图”跑通再回来对接分割结果这样能将问题定位范围缩小很多。硬件链路的基本结构如下在板级系统里摄像头采集的数据或者预先存放在存储介质中的测试图片会先放到一块帧缓冲里。图像分割模型推理时从这块缓冲读取输入经过前处理、模型推理、后处理最终输出类别 ID 图。与此同时原来的彩色原图需要保留下来作为背景层。DMA2D 把前景层和背景层读入混合后写入显示帧缓冲LTDC 外设再周期性地从帧缓冲读取像素并发送到屏幕。设计链路时需要认真考虑内存布局。H747 内部 SRAM 虽然容量不小但如果分辨率较高存放原图、推理输出、彩色掩码层和显示帧缓冲的空间需求会迅速增长。工程上通常会把大块图像缓冲放在外部 SDRAM 中。本文示例以 320×240 分辨率为基准这个尺寸既贴近常用嵌入式视觉模型输入也让缓冲区计算变得直观。如果实际项目是 640×480 或更高分辨率内存占用和带宽需求按面积同比例放大即可。有一类问题在 H7 平台上尤其常见就是 Cache 一致性问题。Cortex-M7 带有 D-Cache当 CPU 向缓冲区写入推理结果后如果 DMA2D 紧接着读取这块内存DMA2D 不一定能立刻看到 CPU 写入的最新数据因为一部分数据可能还停留在 CPU 的 Cache 里。反过来当 DMA2D 写完了输出缓冲CPU 去读取或者 LTDC 去扫描显示时也可能读到旧的缓存内容。所以在 DMA2D 操作前后必须根据实际工程选择 clean 或 invalidate 对应的 D-Cache 区域这是 STM32H7 上图像搬运类工程绕不开的细节。5. 掩码渲染方案选型不是所有像素都要让 CPU 画把标签图变成半透明掩码再叠加到原图上实际操作至少有三条技术路径。第一条路径是纯 CPU 操作。CPU 遍历整张标签图根据类别 ID 查调色板将每一个像素的 RGBA 颜色写入前景层缓冲区再把前景层和背景层逐像素做加法运算输出到显示缓冲。它的优点是逻辑清晰不依赖太多外设细节。缺点是每一帧都会占用大量 CPU 周期而且中间会额外产生一块庞大的 ARGB 前景缓存搬运成本很高。这种方案适合在系统刚开发、暂时不想引入 DMA2D 复杂配置时快速验证算法效果但不适合做产品级实时显示。第二条路径是“CPU 上色 DMA2D 混合”。CPU 仍然负责把标签图扩展成带上 alpha 通道的彩色掩码但这一步也是开销所在。在 320×240 分辨率下写入 76800 个 32bit 像素大概只需几毫秒。而 alpha 混合那一半工作交给 DMA2D让 CPU 从两张大图的读改写运算中抽身。这个方案的实现难度较低也是本集示例代码采用的主方案适合绝大多数现有工程快速改造。第三条路径是“DMA2D 直接读取标签图用 CLUT 完成颜色映射”。DMA2D 的前景层支持 L8 等调色板格式也就是说设备端可以准备一块颜色查找表DMA2D 每读到一个标签图中的 8bit 值并不是直接把它当成灰度颜色而是用这个值作为索引去颜色表中查出真正的 ARGB 颜色。由此标签图不需要经过 CPU 逐步扩展成彩色掩码而是可以直接以原始类别 ID 图的形式交给 DMA2D由硬件边读边映射再和背景层混合。三种方案的核心区别可以概括为方案CPU 负担内存占用实现难度实时性纯 CPU 上色 混合高高低差CPU 上色 DMA2D 混合中中中中DMA2D CLUT 一次映射混合低低较高好从工程演进角度看比较合理的策略是先用第二条路径跑通整条链路验证颜色映射逻辑和显示效果等到系统帧率确实吃紧再去阅读参考手册的 CLUT 寄存器部分把标签图从 CPU 上色中解放出来。事实上很多产品在 320×240 分辨率下即使只用 CPU 做标签图上色M7 也能承受真正拖累系统的是“逐像素读改写再回写”那段混合循环这也正是 DMA2D 最能替代的部分。6. 完整示例用 DMA2D 输出半透明分割叠加结果下面给出一个最小可运行的分割结果可视化流程。示例把流程拆成三层标签图生成、颜色层填充、DMA2D 混合显示。代码以 320×240 分辨率为例类别数设定为 8读者移植到自己的项目时可以直接替换分辨率和调色板。6.1 调色板定义与基础缓冲区调色板是可视化效果的核心。这里给每个类别定义一个 RGBA 颜色其中 A 是 alpha代表透明度。alpha 值越高越不透明分割色块对原图的遮挡越强。在嵌入式可视化中分割层通常希望既明显又不过度遮挡背景建议 alpha 值取 80 到 160 之间。// 文件vis_config.h #ifndef VIS_CONFIG_H #define VIS_CONFIG_H #define VIS_IMG_W 320 #define VIS_IMG_H 240 #define VIS_CLASS_NUM 8 typedef struct { uint8_t b; uint8_t g; uint8_t r; uint8_t a; } VisColor; /* 每个类别的显示颜色按 class id 索引 */ static const VisColor s_class_color[VIS_CLASS_NUM] { { 0, 0, 0, 0 }, /* 背景透明 */ { 0, 0, 255, 110 }, /* 类别1红色半透明 */ { 0, 255, 0, 110 }, /* 类别2绿色半透明 */ {255, 0, 0, 110 }, /* 类别3蓝色半透明 */ { 0, 255, 255, 110 }, /* 类别4黄色半透明 */ {255, 0, 255, 110 }, /* 类别5品红半透明 */ {255, 255, 0, 110 }, /* 类别6青色半透明 */ {255, 255, 255, 110 }, /* 类别7白色半透明 */ }; /* 标签图由模型后处理 argmax 得到 */ extern uint8_t s_label_buf[VIS_IMG_W * VIS_IMG_H]; /* 前景层DMA2D 混合时作为前景存放带 alpha 的彩色掩码 */ extern uint32_t s_fg_buf[VIS_IMG_W * VIS_IMG_H]; /* 原图背景层摄像头采集的 RGB 数据先转换成 ARGB8888 */ extern uint32_t s_bg_buf[VIS_IMG_W * VIS_IMG_H]; /* 输出缓冲DMA2D 混合后的最终帧 */ extern uint32_t s_out_buf[VIS_IMG_W * VIS_IMG_H]; #endifARGB8888 每个像素占 4 字节在内存中的常见字节顺序是蓝、绿、红、透明在构造 32bit 像素时需要注意位排列。上面的VisColor结构体按 BGR 顺序排列能够在常见小端平台上通过结构体赋值实现像素写入。实际工程中如果使用平台相关宏或编译器特性也可以改用移位拼接的方式两种写法效果相同。6.2 标签图转半透明颜色层填充函数的核心逻辑并不复杂遍历标签图中的每个像素取出类别 ID再从调色板中复制对应的颜色。这段代码在一个 320×240 的图上通常只需要几毫秒。如果后续使用带 CLUT 的 DMA2D 方案这一步可以被省略但在本示例中它是理解整个流程的起点。// 文件vis_mask_render.c #include vis_config.h uint8_t s_label_buf[VIS_IMG_W * VIS_IMG_H]; uint32_t s_fg_buf[VIS_IMG_W * VIS_IMG_H]; uint32_t s_bg_buf[VIS_IMG_W * VIS_IMG_H]; uint32_t s_out_buf[VIS_IMG_W * VIS_IMG_H]; void vis_render_mask_from_label(const uint8_t *label_buf, const VisColor *color_table, uint32_t *fg_buf, uint32_t pixel_count) { for (uint32_t i 0; i pixel_count; i) { uint8_t class_id label_buf[i]; if (class_id VIS_CLASS_NUM) { class_id 0; } VisColor color color_table[class_id]; /* 注意大小端下的内存顺序蓝 / 绿 / 红 / alpha */ ((uint8_t *)fg_buf[i])[0] color.b; ((uint8_t *)fg_buf[i])[1] color.g; ((uint8_t *)fg_buf[i])[2] color.r; ((uint8_t *)fg_buf[i])[3] color.a; } }这段代码用数组下标逐像素操作编译器开优化后效率不错。如果希望进一步缩短循环耗时可以对label_buf和fg_buf使用双字访问或者对调色板做查表展开但作为通用实现已经足够。