首页
/ OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战

OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战

2026-09-06 19:12:08作者:温玫谨Lighthearted

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

OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战

重映射理论示例输入图:教程用图中红色圆形的位置变化直观展示映射前后像素的迁移方向

什么是图像重映射(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_ymap_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. 加载一幅图像;
  2. 每隔 1 秒,对图像轮流施加 4 种不同的重映射并实时显示在窗口中;
  3. 等待用户按键退出(按 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.pngRemap_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_xmap_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

原教程使用如下输入图像进行测试:

OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战

教程演示输入图像(示例图),将依次施加四种重映射

四种模式的实际输出效果为:

OpenCV 图像重映射(Remap)深入解析:从 cv::remap 原理到 C++/Python/Java 实战

双向翻转效果:模式 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 上限校验;若 dstsrc 指向同一内存(原地重映射),函数会先克隆源图以免覆盖正在被采样的数据。

进阶方向与实践建议

掌握了映射表构造方法后,你可以举一反三地实现:

  • 自定义翻转/镜像与旋转:只需像本教程一样在 map_x/map_y 中做坐标代数运算;
  • 镜头畸变矫正map_x/map_y 由畸变模型(如径向 + 切向系数)逐像素计算得到,这也是 OpenCV undistort 的内部工作原理之一;
  • 实时视频流重映射:若映射表在连续帧间保持不变(如固定镜头的校正场景),只需预先构造一次 map_x/map_y,之后每帧直接调用 cv::remap 即可,开销仅为一次查表采样。

建议你下载 C++PythonJava 示例自行运行,并尝试修改 update_map 中的映射公式(例如改为极坐标映射或自定义鱼眼效果),这是理解重映射最直观的方式。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388