今天在 StackOverflow 上看到这么个问题:想把原图像中的一个点 \textstyle g,通过内参和畸变参数映射到去畸变图像中的点 \textstyle p,并获取这个点的坐标。
你以为我接下来会说,xxx 和我想的一样,但是我想错了,其实并不是这样。
现在,我想说的只有其实并不是这样,因为我连 map1 和 map2 是啥我都不知道,我只会无脑用函数(抱歉)。
remap
像素重映射(Pixel Remapping)是指将图像从一个坐标系映射到另一个坐标系的过程。在 OpenCV 中,cv::remap() 函数可以用来实现像素重映射。
void cv::remap(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
InputArray map1, // 1. (x, y) 映射 2. x 映射
InputArray map2, // 1. 空映射 2. y 映射
int interpolation, // 插值方法
int borderMode = BORDER_CONSTANT, // (可选)边界模式,用于处理超出图像边界的像素
const Scalar& borderValue = Scalar() // (可选)边界值,用于设置超出图像边界的像素值
);
函数通过对 x 和 y 的映射来实现对 src 图像的转换:
其中,dst 表示目标图像,其与 src 有相同的大小和类型,这里的 x 和 y 都是输出图像 dst 中的像素坐标。
在我之前的认知中,我以为 map1 和 map2 中存储的是 src(x,y) 去畸变后在 dst 图像中的 x 和 y 坐标,但实际并不是这样。
根据 OpenCV 官方文档 Geometric Image Transformations 中的说明:
In fact, to avoid sampling artifacts, the mapping is done in the reverse order, from destination to the source.
也就是说,映射是从 dst 到 src 反向进行的。因此,map1 和 map2 的作用是查找 dst 中每个像素应该从 src 中哪个位置采样,即过程是 \textstyle dst \rightarrow src,也被称为反向映射。
寻找 src 在 dst 中的对应点
如果已经得到了 map1 和 map2,想在此基础上找到 src 在 dst 中的对应点,有两种方法。
- 使用 OpenCV 的
undistortPoints()函数
如果问题本身是相机去畸变,例如想把原图中的畸变点转换到去畸变图像坐标系中,推荐优先使用 OpenCV 提供的 undistortPoints()。不过需要注意,这个函数适用于相机畸变校正这类特定几何模型;如果是任意 remap() 变换,这个函数不一定适用。
cv::Mat distortedPoints = (cv::Mat_<cv::Point2f>(4, 1)
<< cv::Point2f(877, 403),
cv::Point2f(877, 403),
cv::Point2f(877, 403),
cv::Point2f(877, 403));
cv::Mat undistortedPoints;
auto start = std::chrono::high_resolution_clock::now();
cv::fisheye::undistortPoints(distortedPoints, undistortedPoints, K, D);
double fx = K.at<double>(0, 0); // 焦距 x
double fy = K.at<double>(1, 1); // 焦距 y
double cx = K.at<double>(0, 2); // 主点 x
double cy = K.at<double>(1, 2); // 主点 y
cv::Mat undistortedPointsPix;
undistortedPoints.convertTo(undistortedPointsPix, CV_32F); // 确保坐标是浮点数
for (int i = 0; i < undistortedPointsPix.rows; ++i) {
undistortedPointsPix.at<cv::Point2f>(i).x =
fx * undistortedPointsPix.at<cv::Point2f>(i).x + cx;
undistortedPointsPix.at<cv::Point2f>(i).y =
fy * undistortedPointsPix.at<cv::Point2f>(i).y + cy;
}
auto end = std::chrono::high_resolution_clock::now();
// 输出校正后的点
std::cout << "Distorted Points:" << std::endl << distortedPoints << std::endl;
std::cout << "Undistorted Points:" << std::endl << undistortedPointsPix << std::endl;
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Execution time: " << duration.count() << " microseconds" << std::endl;
这里使用的是 cv::fisheye::undistortPoints()。默认情况下,它返回的是归一化坐标,所以文中又手动乘以 fx/fy 并加上 cx/cy,将结果转换回像素坐标。
另一种写法是给 undistortPoints() 传入投影矩阵 \text {P = K},让函数直接输出像素坐标。
- 查表
另一种方式是直接在 map1 和 map2 中查找最接近原始点的位置。因为 map1 和 map2 描述的是 dst -> src,所以查表过程本质上是在找哪个 dst 像素采样到了给定的 src 坐标附近。
std::pair<int, int> mapLookUp(
const cv::Mat& map1,
const cv::Mat& map2,
int u,
int v
) {
float min_cost = 1e5;
int min_u = 0;
int min_v = 0;
int height = map1.rows;
int width = map1.cols;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
float u1 = map1.at<float>(i, j);
float v1 = map2.at<float>(i, j);
float dist = std::abs(u1 - static_cast<float>(u)) +
std::abs(v1 - static_cast<float>(v));
if (min_cost > dist) {
min_cost = dist;
min_u = j;
min_v = i;
}
}
}
return std::make_pair(min_u, min_v);
}
这里需要注意,示例中的 map1.at<float> 和 map2.at<float> 只适用于 map1、map2 分别存储 x、y 浮点映射的情况。如果当前 map 是其他格式,需要先转换,或者按照实际的 cv::Mat 类型读取。
另外,查表循环建议按照 map1.rows 和 map1.cols 遍历,因为 map1、map2 的尺寸对应的是 dst 图像,而不是严格由 src 决定。
两种方式能取得非常接近的结果,但是第二种遍历查表的方法效率比较低。在 i7-11700 上测试,运行时间超过 1000 ms,而 OpenCV 的函数就很快,运行时间在几十 ms。
评论