1,目的。
使用多个相机采集图像,经计算处理获得感兴趣区域的映射图,将映射图拼接为一幅图像,以达到扩大视野范围的目的。
示意图
变换后:
2,原理。
计算C1世界坐标系中感兴趣区域的右上角C1A的世界坐标,将此坐标设为创建ImageMap所需的PoseNewOrigin1的原点。
计算出C1世界坐标系中感兴趣区域的右上角C1B基于世界坐标系默认原点C1Origin的偏移矩阵,结合C1世界坐标系的默认原点C1Origin与C2世界坐标系的默认原点C2Origin存在偏移矩阵关系(两个坐标系的原点存在固定的距离),可计算出C1B相对C2世界坐标系默认原点C2Origin的变化矩阵。
因C1B与C2世界坐标系中感兴趣区域的左上角C2A重叠,所以C1B相对C2世界坐标系默认原点C2Origin的偏移旋转矩阵可视为C2A相对C2世界坐标系默认原点C2Origin的偏移旋转矩阵。结合C2相机的位姿,可计算出C2A在C2相机坐标系的位姿PoseNewOrigin2。
通过PoseNewOrigin1,PoseNewOrigin2获得采集图像与感兴趣区域位置的映射关系map,再通过map获得裁剪后的兴趣图像,兴趣图像拼接后即为所需的大视野图像。在像素坐标系中,原点位于图像的左上角。所以这里将原点重置到左上角便于后续生成原点位于左上角的映射图。注意:该案例的标定模型使用的是两个相机,一个标定板,不是两个标定板!不是两个标定板!左相机的标定板水平向右移动
DistancePlates成为右相机的标定板。
2,注意事项。
- 进行变换时尽量使用矩阵而不是位姿,位姿在变换时误差较大,达不到预期效果。
- 标定完成后默认标定板的几何中心或第一个特征点(如棋盘格左上角)为对应世界坐标系的原点。
3,主要的算子。
3.1,elliptic_axis
1. 功能定义
计算输入区域(Region)的等效椭圆参数,包括:
- Ra:等效椭圆长轴半径(主半径,满足
Ra ≥ Rb ≥ 0) - Rb:等效椭圆短轴半径(次半径)
- Phi:长轴与水平轴(X轴)的夹角(弧度制,范围
-π/2 < Phi ≤ π/2)
2. 核心特性
等效性:生成的椭圆与输入区域具有相同的二阶矩(惯性矩)特性,包括方向、长宽比和质心位置。
计算基础:基于区域的几何矩(
moments_region_2nd算子计算的M20、M02、M11)推导出Ra、Rb和Phi特殊输入处理:
- 空区域或单点区域返回
Ra = Rb = 0 - 直线状区域(如线段)返回
Rb = 0,Phi为线段方向角
- 空区域或单点区域返回
3. 应用场景
- 方向检测:通过
Phi判断工业零件(如PCB焊盘、金属条)的旋转角度 - 形状筛选:结合
select_shape算子,通过anisometry(即Ra/Rb)过滤细长或圆形区域 - 椭圆拟合验证:与
gen_ellipse联用生成等效椭圆,可视化验证计算结果
4. 注意事项
- 角度一致性:对于非规则形状(如L型区域),
Phi与smallest_rectangle2算子的角度结果一致,但等效椭圆面积通常大于原区域面积 - 多区域输入:支持区域数组输入,输出参数为对应长度的数组
- XLD扩展:针对轮廓点云(XLD),需使用
elliptic_axis_points_xld算子,其计算逻辑类似但基于点集矩
3.2,gen_image_to_world_plane_map
图像映射算子,用于建立图像平面与世界坐标系中z=0平面之间的投影关系。通过相机内参和世界坐标系的3D位姿(Pose),计算图像像素到世界平面的单应性变换矩阵,消除透视畸变和径向畸变,模拟无畸变相机垂直拍摄的效果。
1. 输入参数
. CameraParam (相机内参矩阵)
- 定义:包含焦距(f)、主点坐标(cx,cy)、畸变系数(k1,k2,k3,p1,p2)等标定信息
- 格式:通常为数组形式,例:焦距,kappa,Sx,Sy,Cx,Cy,焦距,kappa,Sx,Sy,Cx,Cy,图像宽,图像高
- 作用:提供镜头畸变矫正所需的物理参数,直接影响像素坐标系到世界坐标系的转换精度
.WorldPose (世界坐标系位姿) - 定义:描述目标平面(z=0平面)在相机坐标系中的3D位置和方向
- 构成:通常包含旋转分量(Rx,Ry,Rz)和平移分量(Tx,Ty,Tz),使用四元数或旋转向量表示姿态
- 作用:确定世界平面与成像平面的空间几何关系,用于消除透视畸变
. WidthIn/HeightIn (输入图像尺寸) - 单位:像素
- 作用:限定原始图像的物理分辨率范围,确保映射矩阵与输入图像尺寸匹配
2. 输出参数
. Map (映射图像)
数据类型:多通道图像(int4或int8格式)
内容:存储每个输出像素对应的原始图像坐标偏移量,用于后续map_image算子转换
通道数:
- 单通道:直接索引映射(无插值)
- 双通道:支持双线性插值
.WidthMapped/HeightMapped (输出图像尺寸)
- 单位:像素
- 设置依据:根据目标平面实际物理尺寸和Scale参数计算得出
. Scale (像素-世界单位比例)
- 定义:输出图像中单个像素对应的物理尺寸(毫米/英寸等)
- 典型值:当Scale=1时,1像素=1世界单位,常用于等比例映射场景
3. 辅助参数
.MapType (映射类型)
- 可选值:定义插值方式(如’bilinear’或’nearest_neighbor’)
- 影响:决定map_image算子执行时的采样策略和图像质量
.参数关联性
| 参数组 | 关联性说明 |
|---|---|
| CameraParam ↔ Scale | 内参矩阵的焦距参数直接影响Scale的物理意义,需保证单位统一性 |
| WorldPose ↔ Map | 位姿参数决定映射矩阵的几何变换类型(如纯旋转、平移或复合变换) |
| Map ↔ map_image | 映射图像作为中间数据,通过map_image实现实际图像转换 |
4. 注意事项
. CameraParam的完整性
- 需确保包含所有必要畸变参数(如径向畸变k1/k2/k3和切向畸变p1/p2)6
.WorldPose的精度要求
- 姿态误差会导致校正后的图像产生倾斜或比例失真,推荐使用高精度标定方法26
.Scale的合理性
- 过大的Scale值可能导致输出图像分辨率不足,过小则可能超出内存限制
3.3,tile_images
是HALCON中用于平铺多图像的核心算子,其参数体系直接影响图像拼接效果。以下为参数详解及关联分析
输入参数
Images (输入图像组)
- 类型:图像对象数组
- 要求:所有输入图像需具有相同通道数,支持不同尺寸但会统一处理
- 作用:提供待拼接的原始图像数据,图像顺序影响最终排列结构
NumColumns (列数)
- 类型:整数
- 定义:指定输出图像中每行排列的子图列数,控制横向排列密度
- 动态调整:若输入图像总数非列数整数倍,末行可能留空或填充未定义区域
TileOrder (排列顺序)
- 可选值:
'horizontal':从左到右横向优先填充'vertical':从上到下纵向优先填充
- 影响:决定子图在输出画布上的布局方向,适用于不同对比需求
- 可选值:
. 输出参数
TiledImage (拼接图像)
类型:图像对象
尺寸规则:
- 宽度:
NumColumns × 最大子图宽度 - 高度:
ceil(总子图数/NumColumns) × 最大子图高度
- 宽度:
内容特性:
- 小尺寸子图居中显示,边缘填充背景色(默认黑色)
- 通道数与输入一致,支持多通道拼接
.参数耦合关系
| 参数 | 关联影响 |
|---|---|
| NumColumns ↔ 输出尺寸 | 列数直接影响输出图像的宽度,较大的列数会减少行数 |
| TileOrder ↔ 排列逻辑 | 横向优先时按行填充,纵向优先时按列填充,影响视觉对比效果 |
| 输入尺寸 ↔ 输出块尺寸 | 所有子图统一为最大宽高,可能导致小图边缘留空或大图裁剪 |
. 注意事项
- 通道一致性:输入图像通道数必须相同,否则触发运行时错误
- 内存限制:拼接大尺寸图像组时需评估输出画布尺寸,避免内存溢出
- 填充策略:未占满区域显示为黑色,若需自定义背景需结合其他算子预处理
. 与变体算子对比
相较于tile_images_offset的可控布局,tile_images更适用于自动化排列场景,典型差异包括:
tile_images_offset支持自定义子图位置和输出画布尺寸,灵活性更高tile_images通过列数和排列顺序简化参数配置,适合快速拼接对比
4,程序详解。
4.1,相机标定
* 参考案例库:two_camera_calibration.hdev * 基于相机标定的高精度拼接示例程序 dev_update_off () ImgPath := '3d_machine_vision/multiple_cameras/' * * ================================================ * >>> Window of left image and right image <<< * ================================================ dev_close_window () read_image (Image1, ImgPath + 'camera1_ref') get_image_size (Image1, Width, Height) WindowScale := 0.66 dev_open_window (0, 0, Width * WindowScale, Height * WindowScale, 'black', WindowHandle1) dev_set_color ('green') dev_set_draw ('margin') dev_set_line_width (2) dev_open_window (0, Width * WindowScale + 6, Width * WindowScale, Height * WindowScale, 'black', WindowHandle2) dev_set_color ('green') dev_set_draw ('margin') dev_set_line_width (2) dev_set_window (WindowHandle1) dev_set_part (0, 0, Height - 1, Width - 1) set_display_font (WindowHandle1, 16, 'mono', 'true', 'false') dev_set_window (WindowHandle2) dev_set_part (0, 0, Height - 1, Width - 1) set_display_font (WindowHandle2, 16, 'mono', 'true', 'false') * * * * ======================= * >>> Calibration <<< * ======================= * * ----------------------------- * === Two caltab images === * ----------------------------- * Assume that the two cameras are already calibrated (internal camera parameters) * 相机1标定后内参 CamParam1 := [0.01619,-734.789,7.402e-006,7.4e-006,324.911,256.894,640,480] * 相机2标定后的内参 CamParam2 := [0.0162584,-763.35,7.39842e-006,7.4e-006,324.176,245.371,640,480] * read_image (Image1, ImgPath + 'camera1_ref') read_image (Image2, ImgPath + 'camera2_ref') dev_set_window (WindowHandle1) dev_display (Image1) dev_set_window (WindowHandle2) dev_display (Image2) * CaltabName := 'caltab_30mm.descr' create_calib_data ('calibration_object', 2, 1, CalibDataID) set_calib_data_calib_object (CalibDataID, 0, CaltabName) * dev_set_window (WindowHandle1) set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', CamParam1) find_calib_object (Image1, CalibDataID, 0, 0, 0, [], []) get_calib_data_observ_points (CalibDataID, 0, 0, 0, RCoord1, CCoord1, Index1, Pose1) get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 0, 0, 0) dev_set_color ('green') dev_display (Caltab) dev_set_window (WindowHandle2) set_calib_data_cam_param (CalibDataID, 1, 'area_scan_division', CamParam2) find_calib_object (Image2, CalibDataID, 1, 0, 0, [], []) get_calib_data_observ_points (CalibDataID, 1, 0, 0, RCoord2, CCoord2, Index2, Pose2) get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 1, 0, 0) dev_set_color ('green') dev_display (Caltab) * * 显示camera1世界坐标系 dev_set_colored (12) disp_3d_coord_system (WindowHandle1, CamParam1, Pose1, 0.01) * 显示camera2的世界坐标系 disp_3d_coord_system (WindowHandle2, CamParam2, Pose2, 0.01) disp_message (WindowHandle1, 'Calibration successful', 'window', 12, 12, 'black', 'true') disp_continue_message (WindowHandle1, 'black', 'true') stop ()真实物理世界中物体的三维位置,原点通常定义在标定板的几何中心或第一个特征点(如棋盘格左上角),如下所示:
标定板使用过程:
- 首先找到标定板在左相机的姿态。
- 其次向右水平移动
DistancePlates距离至右相机视野。 - 最后查找标定板在右相机坐标系的位姿。
可知:标定板的中心即为世界坐标系的默认原点,camera01的世界坐标系原点水平偏移DistancePlates即可变换为camera02的世界坐标系原点,如下所示:
4.2, 修改poses ,创建 map_image
* ================================================================ * >>> Modify poses such that map_image creates images, <<< * >>> which can be merged in order to form the image mosaic <<< * ================================================================ * * Determine the offset between the calibration plate surface and the object surface * 两个标定板坐标系统原点之间的距离(单位:m),如4.1中的插图所示 DistancePlates := 0.06488 ThicknessCaliper := 2.9 / 1000.0 ThicknessPlate := 5.65 / 1000.0 * 标定板厚度 DiffHeight := ThicknessPlate - ThicknessCaliper * * 分辨率确定映射后图像的大小 * PixelSize := 0.0001 * * Define the upper left corner of the mosaic image and the size of the two mapped images * 视野边框所占比例 BorderInPercent := 7 * 左右相机视野重叠比例 OverlapInPercent := 20 get_image_size (Image1, WidthImage1, HeightImage1) * ROI左上角坐标 ULRow := HeightImage1 * BorderInPercent / 100.0 ULCol := WidthImage1 * BorderInPercent / 100.0 * Camera01视野ROI左上角坐标(世界坐标系) image_points_to_world_plane (CamParam1, Pose1, ULRow, ULCol, 'm', ULX, ULY) LowerRow := HeightImage1 * (100 - BorderInPercent) / 100.0 RightCol := WidthImage1 * (100 - OverlapInPercent / 2.0) / 100.0 * Camera01视野ROI左下角(世界坐标系) image_points_to_world_plane (CamParam1, Pose1, LowerRow, ULCol, 'm', X1, LowerY) * Camera01视野ROI右上角(世界坐标系) image_points_to_world_plane (CamParam1, Pose1, ULRow, RightCol, 'm', RightX, Y1) * Camera01视野ROI有效范围 image_points_to_world_plane (CamParam1, Pose1, LowerRow, RightCol, 'm', X, Y) HeightRect := int((LowerY - ULY) / PixelSize) WidthRect := int((RightX - ULX) / PixelSize) * -------------------------------------------------------------------- * === Translate pose of left camera into plane of measuerement === * -------------------------------------------------------------------- * 设置Camera01世界坐标系的原点 set_origin_pose (Pose1, ULX, ULY, DiffHeight, PoseNewOrigin1) *==========设置 PoseNewOrigin1的第二种方法=================== * create_pose (ULX, ULY, DiffHeight, 0, 0, 0, 'Rp+T', 'gba', 'point', newOriginInWorldPose) * pose_compose (Pose1, newOriginInWorldPose, PoseNewOrigin1) *========================================================== * 图像到世界平面转换 - 生成一个投影图, * 该图描述了图像平面与世界坐标系中 z=0 平面之间的映射关系。 gen_image_to_world_plane_map (MapSingle1, CamParam1, PoseNewOrigin1, Width, Height, WidthRect, HeightRect, PixelSize, 'bilinear') * * ---------------------------------------------------------------- * === Merge right image in coordinate system of left image === * ---------------------------------------------------------------- *第二幅图像必须校正,使其正好适合第一幅校正图像的右侧。 *这意味着第二校正图像的左上角必须与第一校正图像的右上角相同。 *因此,我们需要知道第一个校正图像的右上角在由第二个图像中的校准板定义的坐标系中的坐标。 *首先,我们在世界坐标系中表示第一幅校正图像的右上角,该坐标系由第一幅图像中的校准板定义。 *它可以通过从原点到第一校正图像的左上角的变换(平移)以及沿着第一校正图像的上边界的平移来确定。 *与补偿校准板厚度的位移一起,该变换由齐次变换矩阵表示: hom_mat3d_identity (HomMat3DIdentity) * camera01视野右上角的偏移矩阵 * 注意:与该偏移矩阵配合使用的是原点在中心的pose1而不是原点在视野有效区域左上角的PoseNewOrigin1 hom_mat3d_translate_local (HomMat3DIdentity, ULX + PixelSize * WidthRect, ULY, DiffHeight, cp1Hur1) * camera01世界坐标系与camera02的世界坐标系关系矩阵 * DistancePlates:camera01 世界坐标系原点到 camera02 世界坐标系原点的距离 * 注意这里的原点是基于pose1(原点在中心)而不是视野有效区域左上角的PoseNewOrigin1 hom_mat3d_translate_local (HomMat3DIdentity, DistancePlates, 0, 0, cp1Hcp2) * camera01 世界坐标系在 camera02 世界坐标系的位姿 hom_mat3d_invert (cp1Hcp2, cp2Hcp1) * 因为camera01 视野的有效区域的右上角与camera02视野有效区域的左上角重合 * 所以矩阵合并 可获得camera01 视野的有效区域的右上角在camera02 世界坐标系的矩阵 * 即camera02视野有效区域的左上角在camera02 世界坐标系的矩阵 hom_mat3d_compose (cp2Hcp1, cp1Hur1, cp2Hul2) * camera2世界坐标系在camera2相机坐标系的矩阵 pose_to_hom_mat3d (Pose2, cam2Hcp2) hom_mat3d_compose (cam2Hcp2, cp2Hul2, cam2Hul2) * camera02视野有效区域的左上角(即camer02世界坐标系原点)在相机坐标系的位姿 hom_mat3d_to_pose (cam2Hul2, PoseNewOrigin2) *===========获取PoseNewOrigin2的第二种方法====================== *hom_mat3d_to_pose (cp1Hur1, Hur1Incp1Pose) *create_pose (-DistancePlates,0, 0, 0, 0, 0, 'Rp+T', 'gba', 'point', cp1Incp2Pose) *pose_compose ( cp1Incp2Pose, Hur1Incp1Pose, Hur1InCp2Pose) *pose_compose (Pose2, Hur1InCp2Pose, PoseNewOrigin2) *============================================================ * * 该图描述了图像平面与世界坐标系中 z=0 平面之间的映射关系。 gen_image_to_world_plane_map (MapSingle2, CamParam2, PoseNewOrigin2, Width, Height, WidthRect, HeightRect, PixelSize, 'bilinear') *示意图:
变换后PoseNewOrigin1、PoseNewOrigin2对应的世界坐标系:
4.3,映射、拼接图像并显示。
================================== * >>> Window of merged image <<< * ================================== dev_open_window (Height * WindowScale, 0, Width * 2 * WindowScale, Height * WindowScale, 'black', WindowHandleCombined) set_display_font (WindowHandleCombined, 16, 'mono', 'true', 'false') dev_set_color ('green') dev_set_draw ('margin') ScalePlot := 200 RowPlot := 400 Coord := [0:2000] * * * * ============================= * >>> Start Measurement <<< * ============================= * for I := 1 to 3 by 1 dev_set_window (WindowHandle1) read_image (Image1, ImgPath + 'camera1_' + I$'02d') get_image_size (Image1, WidthImage1, HeightImage1) dev_set_part (0, 0, HeightImage1 - 1, WidthImage1 - 1) dev_display (Image1) dev_set_window (WindowHandle2) read_image (Image2, ImgPath + 'camera2_' + I$'02d') get_image_size (Image2, WidthImage2, HeightImage2) dev_set_part (0, 0, HeightImage2 - 1, WidthImage2 - 1) dev_display (Image2) * === Time measurement === count_seconds (TimeStart1) * 对图像进行变换(截取感兴趣的区域) map_image (Image1, MapSingle1, RectifiedImage1) map_image (Image2, MapSingle2, RectifiedImage2) concat_obj (RectifiedImage1, RectifiedImage2, Concat) count_seconds (TimeEnd1) Time1 := TimeEnd1 - TimeStart1 * dev_set_window (WindowHandleCombined) * === Time measurement === count_seconds (TimeStart2) * 图像拼接 tile_images (Concat, Combined, 2, 'vertical') count_seconds (TimeEnd2) Time2 := TimeEnd2 - TimeStart2 get_image_size (Combined, WidthComb, HeightComb) dev_set_part (0, 0, HeightComb - 1, WidthComb - 1) dev_display (Combined) disp_message (WindowHandle1, 'Merge cameras: ' + (1000 * (Time1 + Time2))$'.3' + ' ms', 'window', 12, 12, 'black', 'true') * plot_mosaicking_accuracy (Combined, WidthRect, HeightRect, WindowHandleCombined, Coord, ScalePlot, RowPlot) if (I < 3) disp_continue_message (WindowHandleCombined, 'black', 'true') stop () endif endfor拼接效果如下:
5,完整程序。
* Example program for high precision mosaicking based on camera calibration dev_update_off () ImgPath := '3d_machine_vision/multiple_cameras/' * * ================================================ * >>> Window of left image and right image <<< * ================================================ dev_close_window () read_image (Image1, ImgPath + 'camera1_ref') get_image_size (Image1, Width, Height) WindowScale := 0.66 dev_open_window (0, 0, Width * WindowScale, Height * WindowScale, 'black', WindowHandle1) dev_set_color ('green') dev_set_draw ('margin') dev_set_line_width (2) dev_open_window (0, Width * WindowScale + 6, Width * WindowScale, Height * WindowScale, 'black', WindowHandle2) dev_set_color ('green') dev_set_draw ('margin') dev_set_line_width (2) dev_set_window (WindowHandle1) dev_set_part (0, 0, Height - 1, Width - 1) set_display_font (WindowHandle1, 16, 'mono', 'true', 'false') dev_set_window (WindowHandle2) dev_set_part (0, 0, Height - 1, Width - 1) set_display_font (WindowHandle2, 16, 'mono', 'true', 'false') * * * * ======================= * >>> Calibration <<< * ======================= * * ----------------------------- * === Two caltab images === * ----------------------------- * Assume that the two cameras are already calibrated (internal camera parameters) CamParam1 := [0.01619,-734.789,7.402e-006,7.4e-006,324.911,256.894,640,480] CamParam2 := [0.0162584,-763.35,7.39842e-006,7.4e-006,324.176,245.371,640,480] * read_image (Image1, ImgPath + 'camera1_ref') read_image (Image2, ImgPath + 'camera2_ref') dev_set_window (WindowHandle1) dev_display (Image1) dev_set_window (WindowHandle2) dev_display (Image2) * CaltabName := 'caltab_30mm.descr' * 两个相机一个标定板 create_calib_data ('calibration_object', 2, 1, CalibDataID) set_calib_data_calib_object (CalibDataID, 0, CaltabName) * dev_set_window (WindowHandle1) set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', CamParam1) find_calib_object (Image1, CalibDataID, 0, 0, 0, [], []) get_calib_data_observ_points (CalibDataID, 0, 0, 0, RCoord1, CCoord1, Index1, Pose1) get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 0, 0, 0) dev_set_color ('green') dev_display (Caltab) dev_set_colored (12) disp_3d_coord_system (WindowHandle1, CamParam1, Pose1, 0.01) * dev_set_window (WindowHandle2) dev_set_color ('green') set_calib_data_cam_param (CalibDataID, 1, 'area_scan_division', CamParam2) find_calib_object (Image2, CalibDataID, 1, 0, 0, [], []) get_calib_data_observ_points (CalibDataID, 1, 0, 0, RCoord2, CCoord2, Index2, Pose2) get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 1, 0, 0) dev_set_color ('green') dev_display (Caltab) * dev_set_colored (12) disp_3d_coord_system (WindowHandle2, CamParam2, Pose2, 0.01) dev_set_color ('green') disp_message (WindowHandle1, 'Calibration successful', 'window', 12, 12, 'black', 'true') disp_continue_message (WindowHandle1, 'black', 'true') stop () * * ================================================================ * >>> Modify poses such that map_image creates images, <<< * >>> which can be merged in order to form the image mosaic <<< * ================================================================ * * Determine the offset between the calibration plate surface and the object surface * 标定板从左相机视野水平移动右相机视野距离 DistancePlates := 0.06488 * 卡尺厚度 ThicknessCaliper := 2.9 / 1000.0 * 标定板厚度 ThicknessPlate := 5.65 / 1000.0 * 标定板与卡尺的厚度差异 DiffHeight := ThicknessPlate - ThicknessCaliper * * Define the pixel size of the mosaic image * 定义映射图的放大比例 PixelSize := 0.0001 * * Define the upper left corner of the mosaic image and the size of the two mapped images * BorderInPercent := 7 OverlapInPercent := 20 get_image_size (Image1, WidthImage1, HeightImage1) * 左图ROI左上角坐标 ULRow := HeightImage1 * BorderInPercent / 100.0 ULCol := WidthImage1 * BorderInPercent / 100.0 image_points_to_world_plane (CamParam1, Pose1, ULRow, ULCol, 'm', ULX, ULY) LowerRow := HeightImage1 * (100 - BorderInPercent) / 100.0 RightCol := WidthImage1 * (100 - OverlapInPercent / 2.0) / 100.0 * 左图ROI右下角映射 image_points_to_world_plane (CamParam1, Pose1, LowerRow, ULCol, 'm', X1, LowerY) * 左图ROI右上角映射 image_points_to_world_plane (CamParam1, Pose1, ULRow, RightCol, 'm', RightX, Y1) * 左图ROI大小,PixelSize:光学放大率决定了下面拼接图的映射分辨率 HeightRect := int((LowerY - ULY) / PixelSize) WidthRect := int((RightX - ULX) / PixelSize) * -------------------------------------------------------------------- * === Translate pose of left camera into plane of measuerement === * -------------------------------------------------------------------- * 设置原点,DiffHeight=标定板厚度-卡尺厚度,意思将坐标系原点移至卡尺上平面 * 由前标定板标定的坐标系可知Z轴正向朝下 * DiffHeight := ThicknessPlate - ThicknessCaliper * ThicknessPlate:将原点沿标定板厚度移至标定板底面(即平面) * - ThicknessCaliper:移到平面后在上升移动到卡尺上面(标定Z轴正向朝下) * 方法1: set_origin_pose (Pose1, ULX, ULY, DiffHeight, PoseNewOrigin1) * 方法2: create_pose (ULX, ULY, DiffHeight, 0, 0, 0, 'Rp+T', 'gba', 'point', OriginInWordPose) pose_compose (Pose1, OriginInWordPose, OriginInCamPose) gen_image_to_world_plane_map (MapSingle1, CamParam1, PoseNewOrigin1, Width, Height, WidthRect, HeightRect, PixelSize, 'bilinear') * * ---------------------------------------------------------------- * === Merge right image in coordinate system of left image === * ---------------------------------------------------------------- * 方法1,计算计算右图ROI左上点在右相机的位姿 hom_mat3d_identity (HomMat3DIdentity) * 变换到左图ROI右上点(世界长度) hom_mat3d_translate_local (HomMat3DIdentity, ULX + PixelSize * WidthRect, ULY, DiffHeight, cp1Hur1) * 左图单位坐标系变换到右图单位坐标系(cp:Calibration Plate) hom_mat3d_translate_local (HomMat3DIdentity, DistancePlates, 0, 0, cp1Hcp2) hom_mat3d_invert (cp1Hcp2, cp2Hcp1) * hom_mat3d_compose (cp2Hcp1, cp1Hur1, cp2Hul2) pose_to_hom_mat3d (Pose2, cam2Hcp2) * 左图右上点(与右图左上点重合)在右相机世界坐标系的位姿 hom_mat3d_compose (cam2Hcp2, cp2Hul2, cam2Hul2) hom_mat3d_to_pose (cam2Hul2, PoseNewOrigin2) * 方法2 计算右图ROI左上点在右相机的位姿 create_pose ( ULX + PixelSize * WidthRect, ULY, DiffHeight, 0, 0, 0, 'Rp+T', 'gba', 'point', URInLeftRefPose) create_pose (-DistancePlates, 0, 0, 0, 0, 0, 'Rp+T', 'gba', 'point', LeftRefInRightRefPose) * 左图ROI的右上点与右图的ROI的左上点重合,所以两者可认为是同一点 pose_compose (LeftRefInRightRefPose, URInLeftRefPose,ULInRightRefPose ) pose_compose (Pose2, ULInRightRefPose, ULInRightCamPose) gen_image_to_world_plane_map (MapSingle2, CamParam2, PoseNewOrigin2, Width, Height, WidthRect, HeightRect, PixelSize, 'bilinear') * * * ================================== * >>> Window of merged image <<< * ================================== dev_open_window (Height * WindowScale, 0, Width * 2 * WindowScale, Height * WindowScale, 'black', WindowHandleCombined) set_display_font (WindowHandleCombined, 16, 'mono', 'true', 'false') dev_set_color ('green') dev_set_draw ('margin') ScalePlot := 200 RowPlot := 400 Coord := [0:2000] * * * * ============================= * >>> Start Measurement <<< * ============================= * for I := 1 to 3 by 1 dev_set_window (WindowHandle1) read_image (Image1, ImgPath + 'camera1_' + I$'02d') get_image_size (Image1, WidthImage1, HeightImage1) dev_set_part (0, 0, HeightImage1 - 1, WidthImage1 - 1) dev_display (Image1) dev_set_window (WindowHandle2) read_image (Image2, ImgPath + 'camera2_' + I$'02d') get_image_size (Image2, WidthImage2, HeightImage2) dev_set_part (0, 0, HeightImage2 - 1, WidthImage2 - 1) dev_display (Image2) * === Time measurement === count_seconds (TimeStart1) map_image (Image1, MapSingle1, RectifiedImage1) map_image (Image2, MapSingle2, RectifiedImage2) concat_obj (RectifiedImage1, RectifiedImage2, Concat) count_seconds (TimeEnd1) Time1 := TimeEnd1 - TimeStart1 * dev_set_window (WindowHandleCombined) * === Time measurement === count_seconds (TimeStart2) tile_images (Concat, Combined, 2, 'vertical') count_seconds (TimeEnd2) Time2 := TimeEnd2 - TimeStart2 get_image_size (Combined, WidthComb, HeightComb) dev_set_part (0, 0, HeightComb - 1, WidthComb - 1) dev_display (Combined) disp_message (WindowHandle1, 'Merge cameras: ' + (1000 * (Time1 + Time2))$'.3' + ' ms', 'window', 12, 12, 'black', 'true') * plot_mosaicking_accuracy (Combined, WidthRect, HeightRect, WindowHandleCombined, Coord, ScalePlot, RowPlot) if (I < 3) disp_continue_message (WindowHandleCombined, 'black', 'true') stop () endif endfor