上文我们自己用C++手写代码,真实加载一张图片,运行了Yolo模型,一个图片的预处理流程:加载图片、等比例缩放、格式转换。我们其中用到了最近邻插值来获取缩放后的RGB数据。
但实际使用中,一般使用双线性插值。本文我们来实现双线性插值。
文章目录
0. 系列文章
1. 理论
为什么最近邻不够好?
-
最近邻 (Nearest Neighbor):
-
逻辑:坐标算出是 (10.2, 5.7),四舍五入变成 (10, 6),直接取像素值。
-
问题:丢失了小数部分的精度,线条会断裂或呈锯齿状。
-
-
双线性 (Bilinear):
-
逻辑:坐标算出是 (10.2, 5.7)。它落在 (10, 5), (11, 5), (10, 6), (11, 6) 这四个点围成的矩形里。
-
计算:根据距离这四个点的远近,进行加权平均。距离越近,权重越大。
-
2. 实践:手写双线性插值 C++ 代码
回到 src/YoloDetector.cpp,找到 preprocess 函数。
我们需要修改那个双重 for 循环的内容。
2.1 辅助函数 (放在 preprocess 之前)
为了代码整洁,我们先写一个 helper,用于获取像素值并防止越界。
// 辅助:安全获取像素值 (HWC 格式)
// data: 原始图片数据
// w, h, c: 原图宽、高、通道
// x, y: 坐标
// channel: 当前要取的通道 (0=R, 1=G, 2=B)
inline float get_pixel(unsigned char* data, int w, int h, int c, int x, int y, int channel) {
// 边界钳制 (Clamp)
if (x < 0) x = 0;
if (x >= w) x = w-1;
if (y < 0) y = 0;
if (y >= h) y = h-1;
// 计算内存偏移量
return data[(y * w + x) * c + channel];
}
2.2 修改 preprocess 循环逻辑
(1)映射回原图坐标 (浮点数)
// 1. 映射回原图坐标 (浮点数)
// (j - dx) 是相对于填充区域起点的坐标
// + 0.5f 是为了几何中心对齐 (Center Aligned),这是 CV 里的标准做法
float src_x = (j - dx + 0.5f) / scale - 0.5f;
float src_y = (i - dy + 0.5f) / scale - 0.5f;
- 为什么要加 0.5f?
如果直接用 (j - dx) / scale 行不行?
行,但是会有 Pixel Shift (像素偏移)。
原因:
像素不是一个点,而是一个小方块。像素的中心应该在 0.5 的位置。
如果不加: 缩放后的图像中心会往左上角偏一点点。
后果就是在目标检测中,如果你把图缩放了,框的位置也会偏。虽然人眼看不出来,但计算 IoU 时会掉点。
这是 OpenCV 和 TensorFlow 等框架的标准对齐方式。
更具体一点解释:
第 0 个像素的范围是 [0.0, 1.0]。
它的几何中心在哪里?在 0.5。
第 1 个像素的中心在 1.5,第 2 个在 2.5…
假设我们要把一个 2x2 的图片放大成 4x4 (Scale = 2.0)。
我们希望这两个网格的中心点重合,而不是左上角重合。
如果不加 0.5 (左上角对齐):
目标图第 0 个像素 (index 0) 对应原图 0 / 2.0 = 0。
目标图第 3 个像素 (index 3) 对应原图 3 / 2.0 = 1.5。
结果:图像会整体向左上角发生微小的偏移(Pixel Shift)。在多次卷积或深层网络中,这个误差会累积,导致检测框歪掉。
如果加了 0.5 (中心对齐):
(dst_x + 0.5): 先找到目标像素的中心点坐标。
/ scale: 把这个中心点坐标映射回原图尺度。
- 0.5: 映射回原图后,我们拿到的是原图的中心点坐标,但我们需要的是数组索引 (左上角坐标),所以要把中心点那个 0.5 减掉。
直观总结:
+0.5 是为了从“格子左上角”走到“格子中心”。
-0.5 是为了从“原图格子中心”走回“原图格子左上角”以便计算索引。
(2)找到左上角的整数坐标 (Floor)
// 2. 找到左上角的整数坐标 (Floor)
int x0 = (int)std::floor(src_x);
int y0 = (int)std::floor(src_y);
int x1 = x0 + 1;
int y1 = y0 + 1;
(3)计算权重 (即小数部分)
// 3. 计算权重 (即小数部分)
// u, v 是距离左上角的距离 (0.0 ~ 1.0)
float u = src_x - x0;
float v = src_y - y0;
// 权重系数
float w00 = (1 - u) * (1 - v); // 左上
float w01 = (1 - u) * v; // 左下
float w10 = u * (1 - v); // 右上
float w11 = u * v; // 右下
权重计算公式是怎么出来的?
这是双线性插值的核心。
我们把 src_x = 3.7 拆解一下:
整数部分 x0 = 3
小数部分 u = 0.7 (这就是距离左边的距离)
同理 src_y = 5.2:
整数部分 y0 = 5
小数部分 v = 0.2 (这就是距离上边的距离)
面积法(最直观的理解)
想象一个正方形,边长是 1。点 P 把它切成了 4 个小矩形。
双线性插值的规则是:点 P 离哪个角越近,那个角的权重就越大。
神奇的数学规律是:某个角的权重 = 该角【对角】那个小矩形的面积。
P 点坐标 (u, v) = (0.7, 0.2)。
左上角 (Top-Left) 的权重 w00:
P 离左上角比较远 (水平远,垂直近)。
它对应的面积是右下角的那块空白。
右下角的宽是 1 - u (即 0.3),高是 1 - v (即 0.8)。
w00 = (1 - u) * (1 - v) = 0.3 * 0.8 = 0.24
右上角 (Top-Right) 的权重 w10:
P 离右上角很近 (u=0.7 大,v=0.2 小)。
它对应的面积是左下角的那块。
左下角的宽是 u (0.7),高是 1 - v (0.8)。
w10 = u * (1 - v) = 0.7 * 0.8 = 0.56 (权重最大,符合直觉!)
左下角 (Bottom-Left) 的权重 w01:
对应的面积是右上角那块。
宽 1 - u (0.3),高 v (0.2)。
w01 = (1 - u) * v = 0.3 * 0.2 = 0.06
右下角 (Bottom-Right) 的权重 w11:
对应的面积是左上角那块。
宽 u (0.7),高 v (0.2)。
w11 = u * v = 0.7 * 0.2 = 0.14

(4)对三个通道分别进行插值
// 4. 对三个通道分别进行插值
// 归一化 (/255.0f) 也可以放在最后做,这里为了清晰先做
// --- R Channel ---
float r00 = get_pixel(img_data, w, h, channels, x0, y0, 0);
float r01 = get_pixel(img_data, w, h, channels, x0, y1, 0);
float r10 = get_pixel(img_data, w, h, channels, x1, y0, 0);
float r11 = get_pixel(img_data, w, h, channels, x1, y1, 0);
float val_r = w00*r00 + w01*r01 + w10*r10 + w11*r11;
// --- G Channel ---
float g00 = get_pixel(img_data, w, h, channels, x0, y0, 1);
float g01 = get_pixel(img_data, w, h, channels, x0, y1, 1);
float g10 = get_pixel(img_data, w, h, channels, x1, y0, 1);
float g11 = get_pixel(img_data, w, h, channels, x1, y1, 1);
float val_g = w00*g00 + w01*g01 + w10*g10 + w11*g11;
// --- B Channel ---
float b00 = get_pixel(img_data, w, h, channels, x0, y0, 2);
float b01 = get_pixel(img_data, w, h, channels, x0, y1, 2);
float b10 = get_pixel(img_data, w, h, channels, x1, y0, 2);
float b11 = get_pixel(img_data, w, h, channels, x1, y1, 2);
float val_b = w00*b00 + w01*b01 + w10*b10 + w11*b11;
(5)填入 Tensor 并归一化
// 5. 填入 Tensor 并归一化
input_tensor[idx_r] = val_r / 255.0f;
input_tensor[idx_g] = val_g / 255.0f;
input_tensor[idx_b] = val_b / 255.0f;
3. 总结
双线性插值的详细公式可以参考:https://blog.csdn.net/xbinworld/article/details/65660665
你现在可能会想:“双线性插值要算 4 次取值、4 次乘法、3 次加法,而且还要做 3 个通道。这比最近邻慢多了!”
没错,这是要优化的,后面我们一点一点来。

1342

被折叠的 条评论
为什么被折叠?



