嵌入式设备集成DeOldify轻量级模型:C语言接口调用实战

📅 发布时间:2026/7/5 13:01:48 👁️ 浏览次数:
嵌入式设备集成DeOldify轻量级模型:C语言接口调用实战
嵌入式设备集成DeOldify轻量级模型C语言接口调用实战最近在捣鼓一个挺有意思的项目想给一些老旧的嵌入式设备加上“看彩色世界”的能力。比如一个基于摄像头做监控的设备如果只能处理黑白图像信息量就少了一大截。要是能让它在离线状态下把拍到的画面实时上色那实用性就强多了。DeOldify这个AI模型在图像上色领域挺有名的效果也不错。但它的原版模型对算力和内存要求太高直接塞进STM32或者Jetson Nano这类资源紧张的嵌入式设备里基本不可能。所以我们的核心思路就变成了怎么把一个“胖子”模型训练成一个能跑起来的“瘦子”并且用最底层的C语言去驱动它。这篇文章我就来聊聊我们是怎么一步步把精简优化后的DeOldify模型成功部署到嵌入式设备上并用C语言调用ONNX Runtime跑起来的。整个过程会涉及模型瘦身、内存精打细算、以及如何跟摄像头模块联动最终实现一个离线、实时的图像上色小系统。1. 为什么要在嵌入式设备上做图像上色你可能觉得图像上色这种任务放云端或者高性能服务器上做不就好了确实那样最简单。但在很多实际场景里嵌入式设备必须独立工作。想象一下一个安装在偏远地区的野外监控摄像头或者一个移动机器人它们的网络可能不稳定甚至根本没有网络。如果所有图像处理都要上传到云端延迟高不说一旦断网就完全瘫痪了。这时候本地化、离线化的AI能力就变得至关重要。给这些设备加上本地图像上色功能价值是实实在在的信息增强彩色图像比黑白图像包含更多的细节和纹理信息对于后续的目标检测、识别等任务帮助巨大。用户体验无论是安防监控回看还是机器人第一视角彩色画面都更符合人的观察习惯。系统自治不依赖外部网络整个感知-决策-执行的闭环都可以在设备内部完成系统更健壮、响应更快。我们的目标就是让这些资源有限的“小盒子”也能拥有曾经只有“大机器”才具备的智能。2. 模型轻量化让DeOldify“瘦身”原版的DeOldify模型结构比较复杂参数量大直接部署不现实。我们的第一步就是对它进行全方位的“瘦身”。2.1 模型选择与结构调整我们并没有从头训练一个模型而是在一个预训练好的、效果不错的DeOldify模型基础上进行优化。首先是对模型结构动手术减少残差块数量DeOldify的核心组件是残差网络。我们通过实验逐步减少残差块的数量在模型大小和上色效果之间寻找平衡点。比如可能从原来的十几个块减少到六到八个。降低通道数卷积层的通道数直接决定了参数量。我们尝试将各层卷积的通道数统一缩减例如减半这能大幅减少模型体积。替换激活函数将一些计算量较大的激活函数替换为更轻量的版本。这个过程不是一蹴而就的需要反复地“裁剪-微调-评估”确保模型在变小变快的同时不至于“失忆”丢失了上色的能力。2.2 模型量化从浮点到整数的关键一跃结构优化后模型权重还是32位浮点数FP32。这对嵌入式设备来说依然不够友好。量化Quantization是模型轻量化的“杀手锏”。我们采用的是训练后动态量化Post-training Dynamic Quantization。简单说就是把权重从FP32转换为8位整数INT8而激活值每层计算的结果则在推理时动态地转换为INT8。# 这是一个示意性的Python代码展示如何使用PyTorch进行动态量化 import torch import torch.quantization # 假设 model 是我们精简后的DeOldify模型 model.eval() # 切换到评估模式 # 指定量化配置 model.qconfig torch.quantization.get_default_qconfig(fbgemm) # 针对服务器端移动端可用qnnpack # 准备模型进行量化 torch.quantization.prepare(model, inplaceTrue) # 用少量校准数据运行模型这一步用于确定激活值的动态范围 # with torch.no_grad(): # for data in calibration_data_loader: # model(data) # 转换为量化模型 quantized_model torch.quantization.convert(model, inplaceFalse) torch.save(quantized_model.state_dict(), deoldify_quantized.pth)经过INT8量化后模型大小能减少到原来的约1/4同时推理速度也能有显著提升。虽然会带来微小的精度损失但对于图像上色这种任务人眼往往察觉不到明显区别。2.3 模型格式转换统一到ONNX不同的深度学习框架PyTorch, TensorFlow训练出的模型需要统一成一个中间格式才能被各种推理引擎使用。我们选择ONNXOpen Neural Network Exchange格式。将PyTorch量化后的模型转换为ONNX格式import torch.onnx # 加载量化模型 quantized_model.load_state_dict(torch.load(deoldify_quantized.pth)) quantized_model.eval() # 创建一个示例输入张量假设输入是224x224的RGB图像 dummy_input torch.randn(1, 3, 224, 224) # 导出为ONNX模型 torch.onnx.export(quantized_model, dummy_input, deoldify_quantized.onnx, export_paramsTrue, opset_version13, # 使用较新的算子集版本 input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}})现在我们就得到了一个精简且量化的deoldify_quantized.onnx文件它是我们嵌入征程的“弹药”。3. 嵌入式环境搭建与推理引擎选择模型准备好了接下来要把它放到嵌入式设备上跑起来。这涉及到推理引擎的选择和交叉编译环境的搭建。3.1 推理引擎ONNX Runtime vs. TFLite Micro对于C语言调用主流选择有两个ONNX Runtime微软开源的跨平台推理引擎对ONNX模型支持最好性能优化不错也提供了C API。TFLite MicroTensorFlow Lite针对微控制器的版本极其轻量但需要先将模型转换为TensorFlow Lite格式。我们选择了ONNX Runtime。原因在于我们的模型优化和量化流程在PyTorch/ONNX生态下更顺畅而且ONNX Runtime的C API文档清晰社区活跃。对于性能更强的边缘设备如Jetson Nano也可以使用其GPU加速版本。3.2 交叉编译ONNX Runtime嵌入式设备尤其是ARM架构的通常需要我们在x86的开发机上为其编译专用的程序库。这就是交叉编译。获取源码从GitHub克隆ONNX Runtime仓库。配置编译工具链根据你的目标设备如STM32系列需要ARM GCC工具链Jetson Nano是aarch64 Linux设置正确的CMake工具链文件。精简编译选项ONNX Runtime功能很多我们需要只编译核心推理库关闭不需要的选项如训练、丰富的算子支持以减小库文件体积。# 一个简化的CMake配置示例针对Linux ARM设备 cmake .. \ -DCMAKE_TOOLCHAIN_FILE../cmake/arm-linux-gnueabihf.toolchain.cmake \ -Donnxruntime_BUILD_SHARED_LIBOFF \ # 编译静态库便于部署 -Donnxruntime_ENABLE_TRAININGOFF \ -Donnxruntime_REDUCED_OPS_BUILDON \ # 仅编译模型用到的算子大幅减库 -Donnxruntime_USE_OPENMPOFF # 根据设备情况选择是否用OpenMP编译执行make命令。最终你会得到libonnxruntime.a静态库和必要的头文件。将这个库和头文件加入到你的嵌入式项目工程中就具备了调用模型的基础能力。4. C语言接口调用实战这是最核心的部分我们将用C语言编写代码加载模型、处理输入、执行推理、获取输出。4.1 内存管理与优化策略嵌入式设备内存稀缺必须精打细算。静态分配尽可能使用静态数组或全局变量来定义输入/输出缓冲区避免动态内存分配malloc带来的碎片化和不确定性。内存复用如果处理流程是“捕获一帧处理一帧”那么可以复用同一块内存作为模型的输入和输出缓冲区。使用Tensor ArenaONNX Runtime C API允许提供一个预分配的内存区域Arena供其内部使用这比让运行时自己分配更可控。4.2 核心调用流程代码解析下面是一个高度简化的示例展示核心步骤#include stdio.h #include stdlib.h #include onnxruntime_c_api.h // 假设图像尺寸和模型路径 #define IMG_HEIGHT 224 #define IMG_WIDTH 224 #define MODEL_PATH deoldify_quantized.onnx int main() { // --- 1. 初始化ONNX Runtime环境 --- const OrtApi* g_ort OrtGetApiBase()-GetApi(ORT_API_VERSION); OrtEnv* env; OrtStatus* status; status g_ort-CreateEnv(ORT_LOGGING_LEVEL_WARNING, DeOldifyApp, env); // ... 检查status是否错误 (为简洁省略实际必须检查) // --- 2. 创建会话选项并加载模型 --- OrtSessionOptions* session_options; g_ort-CreateSessionOptions(session_options); // 可以设置一些选项比如线程数 g_ort-SetIntraOpNumThreads(session_options, 1); g_ort-SetInterOpNumThreads(session_options, 1); OrtSession* session; status g_ort-CreateSession(env, MODEL_PATH, session_options, session); // --- 3. 准备输入数据 --- // 假设我们从摄像头获取了一帧YUV数据并已转换为RGB并归一化到[0,1] // 这里用一个静态数组模拟 float input_data[1 * 3 * IMG_HEIGHT * IMG_WIDTH]; // [batch, channel, height, width] // ... (此处填充真实的图像数据例如for循环读取摄像头缓冲区并预处理) // 创建输入Tensor const char* input_name input; // 与导出ONNX时的名字对应 int64_t input_shape[] {1, 3, IMG_HEIGHT, IMG_WIDTH}; size_t input_shape_len 4; OrtMemoryInfo* memory_info; g_ort-CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, memory_info); OrtValue* input_tensor NULL; status g_ort-CreateTensorWithDataAsOrtValue( memory_info, input_data, sizeof(input_data), input_shape, input_shape_len, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, // 注意量化模型输入可能是UINT8需根据模型调整 input_tensor ); g_ort-ReleaseMemoryInfo(memory_info); // --- 4. 准备输出Tensor --- const char* output_name output; OrtValue* output_tensor NULL; // 通常可以先运行一次获取输出形状这里假设已知输出形状与输入相同 // ... (实际中可能需要先推断输出形状) // --- 5. 执行推理 --- status g_ort-Run(session, NULL, input_name, input_tensor, 1, output_name, output_tensor, 1); // --- 6. 获取输出结果 --- float* output_data; g_ort-GetTensorMutableData(output_tensor, (void**)output_data); // 现在output_data里就是模型生成的上色后的图像数据例如RGB值 // --- 7. 后处理与显示 --- // 将output_data范围可能是[0,1]或[-1,1]转换回[0,255]的像素值 unsigned char output_pixels[3 * IMG_HEIGHT * IMG_WIDTH]; for(int i 0; i 3 * IMG_HEIGHT * IMG_WIDTH; i) { float val output_data[i]; // 假设模型输出范围是[0,1] val val * 255.0f; if(val 255.0f) val 255.0f; if(val 0.0f) val 0.0f; output_pixels[i] (unsigned char)val; } // ... 将output_pixels发送到显示屏或保存起来 // --- 8. 释放资源 --- g_ort-ReleaseValue(output_tensor); g_ort-ReleaseValue(input_tensor); g_ort-ReleaseSession(session); g_ort-ReleaseSessionOptions(session_options); g_ort-ReleaseEnv(env); return 0; }4.3 与摄像头模块的联动上面的input_data填充部分就是与摄像头联动的关键。你需要根据摄像头输出的格式如MJPEG、YUV422编写相应的解码和预处理函数。捕获使用像V4L2Linux这样的接口从摄像头设备读取一帧数据。解码与转换将原始数据解码并转换为模型需要的RGB格式和尺寸如224x224。这一步可能涉及色彩空间转换、缩放等操作。归一化将像素值从[0, 255]归一化到模型期望的范围如[0, 1]或[-1, 1]。填充将处理好的数据填入input_data数组然后交给模型推理。这个过程最好放在一个独立的线程或循环中以实现“实时”处理。在Jetson Nano上可以利用其GPU加速图像预处理在STM32上则需要更精简的算法甚至可能需要硬件JPEG解码器协助。5. 实际效果与优化建议我们在一款ARM Cortex-A53的板子上进行了测试输入224x224的图像量化后的INT8模型推理时间大约在200-300毫秒。这离“实时”如30fps还有距离但对于很多非高速变化的场景如监控、扫描已经可用。如果想进一步提升更激进的量化尝试使用感知量化训练QAT在训练时就模拟量化过程通常能获得比训练后量化更好的精度-速度权衡。算子融合与图优化利用ONNX Runtime的图优化功能将模型中的一些连续操作如Conv-BN-ReLU融合成单个算子减少计算和内存访问开销。硬件加速在像Jetson Nano这样的设备上可以尝试编译启用CUDA或TensorRT支持的ONNX Runtime将计算卸载到GPU上速度能有数量级的提升。流水线设计将图像捕获、预处理、推理、后处理设计成流水线利用多核优势掩盖部分操作的延迟。整个项目做下来感觉就像是在有限的画布上作画每一个字节、每一次计算都要反复斟酌。但当看到黑白的历史照片或实时的摄像头画面在小小的嵌入式屏幕上缓缓渲染出色彩时那种成就感还是挺足的。这条路走通了就意味着我们可以把更多有趣的AI能力塞进那些我们身边不起眼的小设备里让它们真正“智能”起来。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。