OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战
本文基于 OpenCV 官方教程 重映射(Remapping) 展开,系统讲解图像重映射的数学原理、cv::remap 的接口语义,以及随教程提供的 C++ / Python / Java 演示程序如何通过两张映射表实现"缩小居中、上下翻转、左右镜像、双向翻转"四种典型效果。读完本文,你将理解 map_x/map_y 两张映射矩阵的构造规则与坐标系约定,能独立编写出属于自己的任意几何重映射程序,并掌握该函数在 OpenCV 源码中的底层执行路径。

重映射理论示例输入图:教程用图中红色圆形的位置变化直观展示映射前后像素的迁移方向
什么是图像重映射(Remapping)
重映射本质上是"取源图像一个位置上的像素,放到新图像中的另一个位置"这一过程。它并不修改像素自身的值,而是重新安排每个像素的落点,因此常被用来实现图像的翻转、缩放拼接、鱼眼矫正、镜头去畸变等需要"按坐标搬运像素"的操作。
要实现这种搬运,关键问题是目标像素与源像素并不总是一一对应。当映射目标坐标不是整数时,源图像中并不存在"恰好位于该点"的像素,此时就必须借助插值(interpolation)从邻近像素估算出一个颜色值。这正是重映射与简单像素复制(如 copyMakeBorder 裁剪)之间的本质差别。
用数学语言描述:对目标图像中的每个像素位置 ((x,y)),重映射满足如下关系:
[ g(x,y)=f(h(x,y)) ]
其中 (g()) 是重映射后的图像,(f()) 是源图像,而 (h(x,y)) 是作用于坐标 ((x,y)) 的映射函数。
一个直观例子:水平翻转
假设我们有一幅图像 (I),希望对每个坐标 ((x,y)) 施加如下映射:
[ h(x,y)=(I.cols-x,\ y) ]
其效果是:目标图像中位于第 (x) 列的像素,取自源图像第 (I.cols-x) 列、同一行的像素,即整幅图像沿 (x)(水平)方向翻转。教程用红色圆形的位置变化演示了这一过程:源图中位于左侧的红色圆,映射后出现在目标图的右侧,且与水平中轴的距离保持不变。
OpenCV 中的实现入口
在 OpenCV 中,这一重映射能力由 cv::remap 提供。它以两张"坐标查找表"(映射矩阵)为输入,逐像素完成源图到目标图的采样。
cv::remap 函数签名与参数语义
C++ 演示程序调用 cv::remap 的核心语句如下(节选自 Remap_Demo.cpp):
remap( src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0) );
各参数含义如下:
| 参数 | 说明 |
|---|---|
src |
源图像,即被"搬运像素"的输入图。 |
dst |
目标图像,尺寸与 src 相同(实现中 dst 实际按 map 的尺寸创建,见下文源码解读)。 |
map_x |
(x) 方向(列方向)的映射,对应 (h(i,j)) 的第一个分量,即目标像素应读取的源图列坐标。 |
map_y |
(y) 方向(行方向)的映射,对应 (h(i,j)) 的第二个分量,即目标像素应读取的源图行坐标。map_y 与 map_x 必须与 src 同尺寸。 |
INTER_LINEAR |
非整数坐标处的插值方式,教程中使用双线性插值,这也是默认值。 |
BORDER_CONSTANT |
边界填充模式,为默认值;配合 Scalar(0,0,0) 将越界像素填为黑色。 |
坐标约定(务必牢记):在 cv::remap 中,map_x.at<float>(i, j) 存放的是"输出像素 ((i,j)) 应从源图哪一列取样",map_y.at<float>(i, j) 存放的是"应从源图哪一行取样"。也就是说,两张表分别保存的是源图像素位置的 列坐标与行坐标。弄反这一约定(例如把列号写进 map_y),输出图像会被转置或错乱。
演示程序的功能流程
本教程的配套示例程序完成以下任务:
- 加载一幅图像;
- 每隔 1 秒,对图像轮流施加 4 种不同的重映射并实时显示在窗口中;
- 等待用户按键退出(按 ESC 结束)。
四种映射效果会以 ind 计数(0 → 1 → 2 → 3 循环切换)持续演示。源图像默认使用 OpenCV 自带样例图 chicky_512.png(位于 samples/data),也可通过命令行参数传入任意图片路径。
第一步:加载图像
C++ 侧通过 CommandLineParser 读取命令行参数,默认值为 chicky_512.png,再经 samples::findFile 在 OpenCV 数据目录中解析出真实路径(Remap_Demo.cpp):
CommandLineParser parser(argc, argv, "{@image |chicky_512.png|input image name}");
std::string filename = parser.get<std::string>(0);
Mat src = imread( samples::findFile( filename ), IMREAD_COLOR );
if (src.empty())
{
std::cout << "Cannot read image: " << filename << std::endl;
return -1;
}
Python 侧则使用 argparse 接收 --input 参数,同样默认 chicky_512.png(Remap_Demo.py):
parser = argparse.ArgumentParser(description='Code for Remapping tutorial.')
parser.add_argument('--input', help='Path to input image.', default='chicky_512.png')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input), cv.IMREAD_COLOR)
if src is None:
print('Could not open or find the image: ', args.input)
exit(0)
第二步:创建目标图像与两张映射矩阵
目标图像 dst、映射矩阵 map_x 与 map_y 均与 src 同尺寸;两张映射表必须使用单通道 CV_32FC1 类型,因为 cv::remap 需要以浮点精度记录"可能非整数"的源坐标(Remap_Demo.cpp):
Mat dst(src.size(), src.type());
Mat map_x(src.size(), CV_32FC1);
Mat map_y(src.size(), CV_32FC1);
Python 中使用 np.float32 类型的零矩阵即可(Remap_Demo.py):
map_x = np.zeros((src.shape[0], src.shape[1]), dtype=np.float32)
map_y = np.zeros((src.shape[0], src.shape[1]), dtype=np.float32)
第三步:创建显示窗口
const char* remap_window = "Remap demo";
namedWindow( remap_window, WINDOW_AUTOSIZE );
第四步:主循环——更新映射并调用 remap
主循环每 1000 ms 更新一次两张映射矩阵,随即执行 cv::remap 并刷新显示(Remap_Demo.cpp):
int ind = 0;
for(;;)
{
/// Update map_x & map_y. Then apply remap
update_map(ind, map_x, map_y);
remap( src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0) );
/// Display results
imshow( remap_window, dst );
/// Each 1 sec. Press ESC to exit the program
char c = (char)waitKey( 1000 );
if( c == 27 )
{
break;
}
}
Python 对应主循环几乎一一对照(Remap_Demo.py):
ind = 0
while True:
update_map(ind, map_x, map_y)
ind = (ind + 1) % 4
dst = cv.remap(src, map_x, map_y, cv.INTER_LINEAR)
cv.imshow(window_name, dst)
c = cv.waitKey(1000)
if c == 27:
break
四种映射的构造原理与代码实现
教程的核心在于 update_map 函数——它展示了如何把"映射的数学公式"翻译成"两张浮点查找表"。这里 map_x 保存 (h(i,j)) 的第一个坐标(列方向),map_y 保存第二个坐标(行方向)。四种映射逐个解析如下(C++ 实现见 Remap_Demo.cpp)。
模式 0:缩小到一半并居中显示
[ h(i,j)=\left(2 \times j-\frac{src.cols}{2}+0.5,\ 2 \times i-\frac{src.rows}{2}+0.5\right) ]
仅在中央四分之一区域((\frac{src.cols}{4}<j<\frac{3\cdot src.cols}{4}) 且 (\frac{src.rows}{4}<i<\frac{3\cdot src.rows}{4}))内执行映射:目标像素从源图采样时坐标减半(等效于"放大"取样),使源图被压缩到画面中央;区域外则统一填入坐标 (0,0),再配合 BORDER_CONSTANT 与黑色 borderValue,四周自然呈现黑边:
case 0:
if( j > map_x.cols*0.25 && j < map_x.cols*0.75 && i > map_x.rows*0.25 && i < map_x.rows*0.75 )
{
map_x.at<float>(i, j) = 2*( j - map_x.cols*0.25f ) + 0.5f;
map_y.at<float>(i, j) = 2*( i - map_x.rows*0.25f ) + 0.5f;
}
else
{
map_x.at<float>(i, j) = 0;
map_y.at<float>(i, j) = 0;
}
break;
注意源码中 i 为行索引、j 为列索引,因此写入 map_x(列坐标)的表达式基于 j,写入 map_y(行坐标)的表达式基于 i;与公式逐项对应。末尾追加的 0.5f 是一种常见的坐标微调,使采样中心对齐像素中心。
模式 1:上下翻转
[ h(i,j)=(j,\ src.rows-i) ]
列坐标保持不变、行坐标取反,图像以水平中轴为轴上下颠倒:
case 1:
map_x.at<float>(i, j) = (float)j;
map_y.at<float>(i, j) = (float)(map_x.rows - i);
break;
模式 2:左右镜像
[ h(i,j)=(src.cols-j,\ i) ]
行坐标保持不变、列坐标取反,图像沿垂直中轴左右镜像:
case 2:
map_x.at<float>(i, j) = (float)(map_x.cols - j);
map_y.at<float>(i, j) = (float)i;
break;
模式 3:水平 + 垂直双向翻转
[ h(i,j)=(src.cols-j,\ src.rows-i) ]
即模式 1 与模式 2 的组合,等效于把图像旋转 180°:
case 3:
map_x.at<float>(i, j) = (float)(map_x.cols - j);
map_y.at<float>(i, j) = (float)(map_x.rows - i);
break;
ind 在每次更新后自增并对 4 取模,实现四种效果的循环播放:
ind = (ind+1) % 4;
向量化写法(Python)
Python 示例的同一映射用"整行/整列广播赋值"实现,避免了逐像素嵌套循环(Remap_Demo.py)。例如左右镜像写成:
for i in range(map_x.shape[0]):
map_x[i,:] = [map_x.shape[1]-x for x in range(map_x.shape[1])]
for j in range(map_y.shape[1]):
map_y[:,j] = [y for y in range(map_y.shape[0])]
模式 1(上下翻转)则是行方向反向填充:
for i in range(map_x.shape[0]):
map_x[i,:] = [x for x in range(map_x.shape[1])]
for j in range(map_y.shape[1]):
map_y[:,j] = [map_y.shape[0]-y for y in range(map_y.shape[0])]
Java 版本说明
Java 示例的结构与 C++ 完全对应(RemapDemo.java)。差别主要在于:矩阵访问需先经 mapX.get(0, 0, buffX) 将数据拷贝到 float[] 缓冲区,逐像素计算完成后再通过 mapX.put(0, 0, buffX) 写回;调用接口为 Imgproc.remap(src, dst, mapX, mapY, Imgproc.INTER_LINEAR)。主类通过 System.loadLibrary(Core.NATIVE_LIBRARY_NAME) 加载原生 OpenCV 库后运行。
运行演示并观察结果
编译运行 C++ 示例(Linux/macOS 下以 CMake 构建,或直接用示例中的编译配置),传图路径即可启动循环演示:
# 以自带样例图运行(也可换成任意图片路径)
./Remap_Demo chicky_512.png
Python 版等价命令为:
python Remap_Demo.py --input chicky_512.png
原教程使用如下输入图像进行测试:

教程演示输入图像(示例图),将依次施加四种重映射
四种模式的实际输出效果为:
- 模式 0:图像缩小一半并居中显示,四周为黑色——对应 Remap_Tutorial_Result_0.jpg;
- 模式 1:图像上下颠倒——对应 Remap_Tutorial_Result_1.jpg;
- 模式 2:图像左右镜像——对应 Remap_Tutorial_Result_2.jpg;
- 模式 3:双向翻转(等效旋转 180°)——对应下图:

双向翻转效果:模式 1 与模式 2 的组合,等效于将原图旋转 180°
由于主循环每 1 秒切换一次 ind,运行程序后窗口中会依次循环展示上述四种结果,按下 ESC 键退出。
源码级延伸:cv::remap 的实现要点
在 imgwarp.cpp 中,cv::remap 的实现揭示了若干值得注意的底层行为:
- 映射表的数据类型分派:实现针对三种映射表格式走不同路径——两张
CV_32FC1表、单张CV_32FC2表(x/y 打包进两通道),以及CV_16SC2+CV_16UC1的定点整数组合。浮点表精度更高,而定点表常用于对速度敏感的场景。本教程使用的是两张CV_32FC1表。 - 硬件加速入口:
CV_OCL_RUN表明当目标为UMat时可直接走 OpenCL GPU 路径;同时通过CALL_HAL(remap32f, ...)将核心计算下放到 HAL 层,从而能自动利用 IPP、NEON、AVX 等 SIMD 优化实现。 - 目标尺寸取自映射表:
_dst.create(map1.size(), src.type())说明dst的实际尺寸取决于map而非src——这为"输出任意尺寸"(例如仅对图像局部放大输出)留下了灵活性;当尺寸不一致时,OpenCV 会按map的尺寸重建dst。 - 边界与自重叠保护:实现中
CV_Assert要求映射表非空且map2(若提供)与map1尺寸一致,并对行列尺寸做了SHRT_MAX上限校验;若dst与src指向同一内存(原地重映射),函数会先克隆源图以免覆盖正在被采样的数据。
进阶方向与实践建议
掌握了映射表构造方法后,你可以举一反三地实现:
- 自定义翻转/镜像与旋转:只需像本教程一样在
map_x/map_y中做坐标代数运算; - 镜头畸变矫正:
map_x/map_y由畸变模型(如径向 + 切向系数)逐像素计算得到,这也是 OpenCVundistort的内部工作原理之一; - 实时视频流重映射:若映射表在连续帧间保持不变(如固定镜头的校正场景),只需预先构造一次
map_x/map_y,之后每帧直接调用cv::remap即可,开销仅为一次查表采样。
建议你下载 C++、Python 或 Java 示例自行运行,并尝试修改 update_map 中的映射公式(例如改为极坐标映射或自定义鱼眼效果),这是理解重映射最直观的方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00