摘要: 本文介绍如何复用 HALCON 标定得到的相机内外参与位姿,直接处理左右物体图像并输出拼接结果。核心流程包括:读取固化的 division 相机参数与两台相机位姿,为每个物体场景施加局部平移,通过 buildDivisionWorldPlaneMap 生成世界平面反向映射表,再依次调用 remapWithMask 完成带有效域掩膜的重映射、stitchLikeHalcon 做带掩膜约束的拼接,最后经 rotateCropMirror 旋转、裁剪与镜像,输出 result_01.png 到 result_04.png。文章还详细解析了位姿与投影几何、映射生成、拼接后处理、对象处理主流程以及标定参数与关键常量,帮助读者理解如何用 C++ 与 OpenCV 近似还原 HALCON 的 region/domain 语义。
摘要:使用 HALCON 标定好的内外参和相机位姿。
只做一件事:复用 HALCON 标定得到的相机参数与位姿,直接处理物体图像,输出拼接结果。
实际的执行链路:
- 读取写死的相机 division 参数。
- 读取写死的两台相机 pose。
- 针对每个物体场景,为左右相机施加一个局部平移,定义物体在世界平面中的放置位置。
- 调用
buildDivisionWorldPlaneMap为左右相机生成世界平面的反向映射表。 - 读取左右物体图像。
- 调用 `remapWithMask` 完成重映射并生成有效域掩膜。
- 调用 `stitchLikeHalcon` 做带掩膜约束的拼接。
- 调用 `rotateCropMirror` 做旋转、裁剪和镜像。
- 输出 `result_01.png` 到 `result_04.png`。
整体数据流:
目标图像像素 (col,row)
│ × scale, Z=0
▼
世界平面点 (X, Y, 0)
│ projector:R,t + 针孔 + division 畸变 + 像素换算
▼
源图像像素 (u, v)
│ 写入 mapX, mapY
▼
cv::remap(src, dst, mapX, mapY, INTER_LINEAR)
`remapWithMask` 先做 `cv::remap`,再根据 `mapX/mapY` 生成有效域掩膜。
这样做的原因是:
-
OpenCV 双线性插值在边界外半个像素附近,可能仍混入一些非零灰度。
-
HALCON 的 `map_image` 会结合 domain 排除这些域外像素。
-
这里显式把域外区域清零,可以减少拼接时的黑线和脏边。
`stitchLikeHalcon` 的处理逻辑是:
-
先求两张图有效域的交集。
-
对交叠区域做一个小尺度闭运算,修补针孔。
-
在交叠区域内优先去掉 `world1` 的内容。
-
再把 `world2` 叠加上去。
这是一种近似 HALCON region/domain 语义的实现,目的不是做复杂融合,而是让覆盖关系和边界效果更接近原示例。
最后的后处理:
-
将拼接后的世界平面图旋转 12 度。
-
裁剪固定矩形区域。
-
先做上下翻转,再做左右翻转。
内部函数解析:
基础工具
-
`readImageOrThrow`:按彩色方式读取图像,读不到直接抛异常。
-
`twoDigit`:把索引格式化成两位字符串(`1` -> `"01"`),用于拼出文件名。
位姿与投影几何
// 位姿与投影几何:完成旋转平移组合,以及 HALCON division 相机投影。
// 将局部平移旋转到世界坐标系,并叠加到位姿平移量。
Pose translateLocal(const Pose& pose, const cv::Vec3d& local) {
return {pose.R, pose.R * local + pose.t};
}
-
`translateLocal`:在相机自身坐标系下做局部平移,得到新的世界位姿,满足 `t' = R * local + t`。
// 将传感器物理坐标换算为图像像素坐标。
cv::Point2d pixelFromSensor(const DivisionCamera& camera, const cv::Point2d& sensor) {
// 传感器上相邻两个像素中心的物理间距是 sx(水平)、sy(垂直) 单位换算:把「米」换算成「像素个数」
// 水平像素偏移 = sensor.x / sx
// 垂直像素偏移 = sensor.y / sy/// 加上主点偏移 :sensor.x = 0 的位置是光轴与传感器平面的交点(主点),
// 但它在图像里并不一定对应像素 (0, 0),而是对应像素 (cx, cy)
// cx, cy 就是主点在图像坐标系中的落点,理想情况下接近图像中心 (width/2, height/2),但实际会有偏移,标定时会估计出来。
return {
sensor.x / camera.sx + camera.cx,
sensor.y / camera.sy + camera.cy,
};
} -
`pixelFromSensor`:把传感器物理坐标按 `sx/sy` 缩放并加上主点 `cx/cy`,得到像素坐标。
// 数据模型:保存复用 HALCON 标定结果所需的相机模型、位姿和映射表。
struct DivisionCamera {
double focus;
double kappa;
double sx;
double sy;
double cx;
double cy;
int width;
int height;
};
// 按 HALCON division 模型将理想传感器坐标转换为畸变后的坐标。
//u = 2U / (1 + sqrt(1 - 4·Kappa·(U² + V²)))
//v = 2V / (1 + sqrt(1 - 4·Kappa·(U² + V²)))
cv::Point2d distortDivisionSensor(const DivisionCamera& camera, const cv::Point2d& ideal) {
const double radiusSquared = ideal.x * ideal.x + ideal.y * ideal.y;
const double discriminant = 1.0 - 4.0 * camera.kappa * radiusSquared;
if (discriminant <= 0.0) {
return {-1.0, -1.0};
}
const double scale = 2.0 / (1.0 + std::sqrt(discriminant));
return {ideal.x * scale, ideal.y * scale};
} -
`distortDivisionSensor`:按 HALCON division 模型给理想像点加入径向畸变,判别式小于等于 0 时返回无效点 `(-1, -1)`。
// 将世界坐标点变换到相机坐标系,执行透视投影和畸变校正。
cv::Point2f projectDivisionPoint(const cv::Vec3d& world, const Pose& pose, const DivisionCamera& camera) {
const cv::Vec3d cameraPoint = pose.R * world + pose.t;
if (cameraPoint[2] <= 1e-12) {
return {-1.0f, -1.0f};
}
const cv::Point2d idealSensor(
camera.focus * cameraPoint[0] / cameraPoint[2],
camera.focus * cameraPoint[1] / cameraPoint[2]);
const cv::Point2d distortedSensor = distortDivisionSensor(camera, idealSensor);
const cv::Point2d pixel = pixelFromSensor(camera, distortedSensor);
return cv::Point2f(static_cast<float>(pixel.x), static_cast<float>(pixel.y));
} -
`projectDivisionPoint`:把世界点依次做位姿变换、透视除法、畸变和像素换算,得到投影像素;相机后方点返回无效值。
映射生成
// 世界平面映射:把目标平面上的每个像素反投影回相机原图。
template <typename Projector>
void buildPlaneMap(
int targetWidth,
int targetHeight,
double scale,
Projector projector,
cv::Mat& mapX,
cv::Mat& mapY) {
mapX.create(targetHeight, targetWidth, CV_32FC1);
mapY.create(targetHeight, targetWidth, CV_32FC1);
for (int row = 0; row < targetHeight; ++row) {
float* mx = mapX.ptr<float>(row);
float* my = mapY.ptr<float>(row);
for (int col = 0; col < targetWidth; ++col) {
const cv::Point2f point = projector(cv::Vec3d(col * scale, row * scale, 0.0));
mx[col] = point.x;
my[col] = point.y;
}
}
}
-
`buildPlaneMap`:模板函数,遍历输出平面上的每个像素,调用投影器填入 `mapX/mapY`。
// 使用 division 相机模型将目标世界平面投影到相机原图,生成 remap 所需的 x/y 坐标表。
void buildDivisionWorldPlaneMap(
const Pose& pose,
const DivisionCamera& camera,
int targetWidth,
int targetHeight,
double scale,
cv::Mat& mapX,
cv::Mat& mapY) {
buildPlaneMap(
targetWidth,
targetHeight,
scale,
[&](const cv::Vec3d& world) { return projectDivisionPoint(world, pose, camera); },
mapX,
mapY);
} -
`buildDivisionWorldPlaneMap`:用 division 相机模型把目标世界平面投影到相机原图,生成 `cv::remap` 所需的 x/y 坐标表,是反向映射的核心。
拼接后处理
// 图像拼接后处理:生成有效域、重映射图像,并模拟 HALCON 的拼接结果。
// 根据映射坐标是否落在源图像范围内生成有效像素掩码。
cv::Mat buildFloatMapDomain(const cv::Mat& mapX, const cv::Mat& mapY, const cv::Size& sourceSize) {
cv::Mat domain(mapX.size(), CV_8UC1, cv::Scalar(0));
const float maxX = static_cast<float>(sourceSize.width - 1);
const float maxY = static_cast<float>(sourceSize.height - 1);
for (int row = 0; row < mapX.rows; ++row) {
const float* mx = mapX.ptr<float>(row);
const float* my = mapY.ptr<float>(row);
uchar* dst = domain.ptr<uchar>(row);
for (int col = 0; col < mapX.cols; ++col) {
const float x = mx[col];
const float y = my[col];
dst[col] = (x >= 0.0f && x < maxX && y >= 0.0f && y < maxY) ? 255 : 0;
}
}
return domain;
}
-
`buildFloatMapDomain`:根据 `mapX/mapY` 判断采样点是否落在原图范围内,生成有效域掩膜。
// 对图像执行双线性重映射,并用有效域掩码清除无效区域。
cv::Mat remapWithMask(const cv::Mat& image, const cv::Mat& mapX, const cv::Mat& mapY, cv::Mat& mask) {
cv::Mat mapped;
//remap 要求 map 给出的是 目标像素 → 源像素 的对应,也就是逆向重采样
cv::remap(image, mapped, mapX, mapY, cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar::all(0));mask = buildFloatMapDomain(mapX, mapY, image.size());
mapped.setTo(cv::Scalar::all(0), ~mask);
return mapped;
} -
`remapWithMask`:先 `cv::remap`,再用有效域掩膜把域外像素清零,避免边界黑线。
-
`stitchLikeHalcon`:对两张图的有效域求交集并做闭运算,在交集内清空 `world1`,再叠加 `world2`。
-
`rotateCropMirror`:旋转 12 度、裁剪固定矩形,再分别做上下翻转和左右翻转。
对象处理主流程
-
`buildResultImage`:读取一对左右物体图像,完成重映射、拼接和后处理。
-
`writeResultImage`:把结果写为 `result_XX.png` 并打印尺寸。
-
`processObjectImages`:遍历高度修正值,对每个场景处理两张图像并写出结果。
标定参数与入口
-
`calibratedCamera1` / `calibratedCamera2`:返回固化的 division 内参。
-
`calibratedPose1` / `calibratedPose2`:返回相机相对标定板世界坐标系的位姿。
-
`processObjectsWithReusedCalibration`:对外入口,组装上述参数并驱动整套流程。
关键常量
`processObjectsWithReusedCalibration` 中固化的几个关键参数:
| 常量 | 值 | 含义 |
| --- | --- | --- |
| `targetWidth` | `1340` | 输出世界平面宽度(像素) |
| `targetHeight` | `800` | 输出世界平面高度(像素) |
| `scale` | `0.0002` | 每个输出像素对应的物理尺寸(米/像素) |
| `heightCorrections` | `{-0.0025, 0.0}` | 两个物体场景的高度修正量(米) |
| 局部平移 `local` | `(-0.14, -0.07, heightCorrection)` | 物体在相机坐标系下的摆放位置 |
因此每个场景会按 `2 * objectIndex + imageIndex` 生成索引,最终输出 `result_01.png` 到 `result_04.png`。
结果图:

拼接细节和halcon 完全一致。