读 ncnn 源码(Ⅻ):图像预处理流水线——从像素到张量的“最后一公里”
读 ncnn 源码(Ⅻ):图像预处理流水线——从像素到张量的“最后一公里”
在深度学习推理场景中,将原始图像数据适配到神经网络输入格式,是“最后一公里”的关键挑战。这一过程,即图像预处理(Image Pre-processing),涵盖尺寸调整、格式转换、数值归一化等多个环节,其效率和精度直接影响模型推理的整体性能。
本篇,我们将深入剖析 ncnn 中从外部像素数据到内部
Mat张量输入的完整预处理流水线,重点关注Mat::from_pixels_resize、resize_bilinear_cX、Mat::from_pixels以及Mat::substract_mean_normalize几个核心函数的协同工作,并通过源码揭示其背后的工程考量与优化策略。
TL;DR
- 流水线: ncnn 的图像预处理分为尺寸适配 → 格式转换与封装 → 数值归一化三阶段。
- 尺寸适配 (
from_pixels_resize,resize_bilinear_cX):from_pixels_resize提供便捷接口,自动计算stride并分发。核心缩放由resize_bilinear_cX完成。resize_bilinear_cX(以 c3 为例) 采用双线性插值,通过预计算插值系数 (xofs,yofs,ialpha,ibeta) 并使用定点数 (short,INTER_RESIZE_COEF_BITS) 优化,结合行缓存复用 (rowsbuf0,rowsbuf1) 和 SIMD (NEON) 加速水平/垂直插值,实现高效重采样。
- 格式转换与封装 (
from_pixels):- 将
unsigned char像素转换为float,并处理RGB2BGR,RGBA2GRAY等多种格式转换(通过委托from_rgb等函数)。 - 核心是调用
Mat::create生成最终Mat,确保内存对齐和 ncnn 内部数据布局要求。
- 将
- 数值归一化 (
substract_mean_normalize):- 执行
output = (input - mean) * norm。 - 关键优化: 不手动实现,而是动态创建并复用 ncnn 内部已优化的
Bias或Scale层,通过构造权重实现减均值/归一化(或融合操作),并通过forward_inplace高效执行。
- 执行

1. 图像预处理流水线概览
神经网络通常对输入数据的尺寸、通道顺序和数值范围有严格要求。ncnn 的预处理流水线正是为了桥接外部图像的灵活性与内部网络的严谨性。其典型调用链为:
Mat::from_pixels_resize(pixels, type, w, h, target_w, target_h, ...)(用户调用)-> Mat::from_pixels_resize(pixels, type, w, h, stride, target_w, target_h, ...)(内部分发)-> resize_bilinear_cX(...)(核心缩放)-> Mat::from_pixels(dst_pixels, type, target_w, target_h, target_stride, ...)(封装结果)-> Mat::substract_mean_normalize(mean_vals, norm_vals)(数值归一化,用户后续调用)
这条流水线确保了送入 Net::input() 的 Mat 对象具备网络所需的精确属性。
2. 阶段一:尺寸适配——接口与核心
2.1 接口分发 (Mat::from_pixels_resize 无 stride 版本)
用户通常调用此便捷接口。它首先解析像素格式,计算出正确的行步长 stride,然后调用功能更全的重载版本。
1 | Mat Mat::from_pixels_resize(const unsigned char* pixels, int type, int w, int h, int target_width, int target_height, Allocator* allocator) |
2.2 核心缩放调用 (Mat::from_pixels_resize 带 stride 版本)
此函数负责调度核心缩放逻辑。
1 | Mat Mat::from_pixels_resize(const unsigned char* pixels, int type, int w, int h, int stride, int target_width, int target_height, Allocator* allocator) |
关键点:先缩放到一个临时的 unsigned char 类型的 Mat dst,再调用 Mat::from_pixels 进行最终的类型转换和内存封装。
2.3 双线性插值实现 (resize_bilinear_c3)
这是图像缩放的核心算法所在地,充满了优化技巧。
a) 预计算插值系数:
1 | const int INTER_RESIZE_COEF_BITS = 11; |
核心思想: 将浮点运算尽可能提前到循环外,并将插值系数转换为定点数 (short),便于后续使用 SIMD 或整数运算加速。
b) 行缓存与水平插值:
1 | Mat rowsbuf0(w * 3 + 1, (size_t)2u); // short 类型行缓存 |
核心思想: 利用 rowsbuf0, rowsbuf1 缓存已完成水平插值的相邻两行数据,并通过 prev_sy1 判断避免重复计算。水平插值本身利用 NEON 进行向量化。
c) 垂直插值:
1 | if (dy + 1 < h && yofs[dy + 1] == sy) // 检查是否可一次处理两行 |
核心思想: vresize_one/vresize_two (未展示代码) 利用预计算的 ibeta 系数,对 rows0 和 rows1 中的 short 类型数据进行垂直方向的加权平均,并将最终的 unsigned char 结果写入目标 dst 内存。同样会进行 SIMD 优化。
3. 阶段二:格式转换与内存封装 (Mat::from_pixels)
此函数接收 unsigned char* 数据(可能是原始像素,也可能是 resize 后的 dst),将其转换为 float 类型并封装到最终的 Mat 对象中。
1 | Mat Mat::from_pixels(const unsigned char* pixels, int type, int w, int h, int stride, Allocator* allocator) |
核心逻辑:
- 格式转换分发: 通过
switch语句将复杂的颜色空间转换(如 RGB<->BGR, RGB->Gray, RGBA->RGB)委托给专门的、同样经过 SIMD 优化的底层函数。 - 基础类型转换与封装: 对于无需格式转换的情况,调用
from_rgb,from_gray,from_rgba等函数。这些函数内部会:- 调用
m.create(w, h, c, elemsize, ...)来申请一块新的、内存对齐的Mat内存。 - 遍历
pixels数据,将unsigned char值转换为float(通常伴随/ 255.f操作)。 - 将
float数据写入新申请的Mat内存中。
- 调用
4. 阶段三:数值归一化 (Mat::substract_mean_normalize)
这是预处理的最后一步,确保数值范围符合模型要求。
1 | void Mat::substract_mean_normalize(const float* mean_vals, const float* norm_vals) |
核心思想: 通过动态创建 Bias 或 Scale 层,并巧妙构造其权重,将归一化操作委托给 ncnn 内部优化过的层实现。forward_inplace 避免了额外的内存分配和拷贝,实现了高效的原地数值归一化。
5. 结语
ncnn 的图像预处理流水线,从 from_pixels_resize 到 substract_mean_normalize,展现了兼顾易用性、功能完备性和极致性能的设计哲学。通过接口封装、算法特化、定点数优化、SIMD 加速、行缓存复用以及巧妙的层复用,ncnn 确保了从原始像素到标准化网络输入的每一步都尽可能高效,为端侧设备上的实时推理奠定了坚实的基础。





