一、设计任务
摄像机标定要求自制标定板,使用网络摄像机或手机摄像头进行标定。将标定的摄像机内参和外参进行保存。设计测量方案,使用标定过的摄像机对包含图像能识别的垂直边缘的物品(直尺,画直线的纸张等),进行距离或边长的测量。标定过程和测量过程,均需要保持摄像机与测量平面之间的距离固定,物品高度不能过高,否则影响测量结果。给出设计的中间过程和截图以及最终测量结果,并对测量结果进行误差计算和分析。
二、总体方案
2.1 测量设备设计
根据设计任务,选择如图1所示的USB相机进行相机的标定和实物的测量,相机固定在标定板上方,垂直照射标定板获得视域的图像。所测的物品包括直尺一把和激光加工的物料块一个,由于从上方照射金属物料块会形成反光,造成局部的阈值直方图错误,影响后来dark to light和light to dark的识别错误,故选择底部打光,使用12V的灯条接稳压源从下方打光,透过亚克力板(这里要使用带塑料膜的亚克力板,如果不用膜的话相机入光太多亮度不够,如果使用白纸的话由于白纸密度不均匀,会产生很多的噪声,需要进行平滑处理,但是效果比较差)照射被测物体,可以实现很好的照射效果以及物料和周围的对比,有效地提高测量的精度。
图1 标定和测量设备实物图
本次测量选择30*30mm的标准标定板进行标定,其参数为:黑色原点:7*7;边框长度:30*30mm;内边框长度:28.125mm*28.125mm;黑色原点半径:0.9375mm;原点中心间距:3.75mm;裁剪宽度:30.75*30.75mm。在Halcon中用gen_caltab(::XNum,YNum,MarkDist,DiameterRatio,CalTabDescrFile,CalTabPSFile:)算子来制作标定板,生成的.descr文件为标定板描述文件,.ps文件为标定板图形文件,可以用photoshop打开,要用分辨率*10打开,否则黑色原点是模糊的,Halcon识别不到标定板。
2.2 测量程序设计
在获得USB相机采集后的图像,选取8-10张导入到catlab的文件中,并进行与程序匹配的命名。然后依照图2进行标定。
图2 标定流程图
标定完成后对图像进行测量,导入被测物体采集的图像,具体的采集过程如图3所示。
图3 测量流程图
使用image_points_to_world_plane命令是因为前面有set_origin_pose (Pose, 0, 0,0, Pose)设置了初始姿态,这里要将空间中的点转为平面上的点进行处理。
三、各功能模块设计及结果图
本次设计中共使用了两种被测物体,直尺的测量比较简单,物料块的测量较为复杂。本节中将介绍两个测量方案的实施。本设计在原有的基础上对例程进行了一定的优化。
3.1 直尺的测量
ImgPath := '3d_machine_vision/calib4/'
read_image(Sample,ImgPath + 'calib_' + '01')
get_image_size(Sample,SampleWidth,SampleHeight)
dev_close_window ()
dev_open_window (0, 0, SampleWidth/4, SampleHeight/4, 'black', WindowHandle)
▶这里进行了相机初始值和标定板的设置。
NumImages := 8
for I := 1 to NumImages by 1
read_image (Image, ImgPath + 'calib_' + I$'02d')
dev_display (Image)
find_calib_object (Image, CalibDataID, 0, 0, I, [], [])
get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 0, 0, I)
dev_set_color ('green')
dev_display (Caltab)
Endfor
▶这里进行多张图的标定,如图4所示,目的是修整相机的内参,获得相机的外参,容易发生两个问题,第一个错误代码是can’t find feature找不到标定板,这个有可能是PS没有高分辨率打开,标定板模糊精度低导致的,第二个错误代码是达不到minimum threshold,这个时候要调整拍摄图片的亮度。
图4 标定板标定示意图
calibrate_cameras (CalibDataID, Error)
get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam)
get_calib_data (CalibDataID, 'calib_obj_pose', [0,1], 'pose', Pose)
* Write the internal camera parameters to a file
write_cam_par (CamParam, 'camera_parameters.dat')
write_pose(Pose, 'campose.dat')
Message := 'camera parameters have'
Message[1] := 'been written to file'
disp_message (WindowHandle, Message, 'window', 12, 12, 'red', 'false')
clear_calib_data (CalibDataID)
▶到这里完成标定,获得相机内参和外参,并将内外参写入文件并显示已经完成标定。相机的内参为['area_scan_division', 0.0166427, 50.0049, 7.19915e-006, 7.4e-006, -260.347, 1059.34, 2592, 1944]
该参数确定摄像机从三维空间到二维图像的投影关系。面阵相机模型为8个参数(f,k,Sx,Sy,Cx,Cy,Width, Height);其中f表示焦距,k表示畸变系数,Sx Sy表示一个像素点的实际大小,Cx Cy表示缩放比例因子,Width和Height表示图片的尺寸。可知畸变系数k为正,该相机为枕型畸变。
相机的外参为[0.121423, -0.0152448, 0.18502, 359.918, 357.112, 268.844, 0]。外参决定摄像机坐标与世界坐标系之间相对位置关系。其中Pw为世界坐标,Pc是摄像机坐标,他们之间关系为 Pc = RPw + T,式中,T= (Tx,Ty,Tz),是平移向量,R = R(α,β,γ)是旋转矩阵,分别是绕摄像机坐标系z轴旋转角度为γ,绕y轴旋转角度为β,绕x轴旋转角度为α。6个参数组成(α,β,γ,Tx,Ty,Tz)为摄像机外参。
set_origin_pose (Pose, 0, 0, -0.0115, Pose)
read_image (Image, ImgPath + 'material2')
dev_display (Image)
*get_mbutton (200000, shubiaorow, shubiaocolumn, Button)
▶这里使用算子get_mbutton()获得鼠标所指的位置的坐标。
gen_measure_rectangle2 (1014, 1310, 0, 480, 8, SampleWidth, SampleHeight, 'bilinear', MeasureHandle)
measure_pairs (Image, MeasureHandle, 0.5, 5, 'all', 'all', RowEdgeFirst, ColumnEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)
▶这两行是比较重要的两行,决定着测量的成败,首先得搞懂这两个算子,gen_measure_rectangle2( : : Row, Column, Phi, Length1, Length2, Width, Height, Interpolation : MeasureHandle)算子是要产生一个长方形,为下一步的提取垂直于矩形直边做准备,这里的Row和Column指的是中心的坐标;Phi很重要,一开始提取水平方向的还好,没有出问题,后来提取到第二个图像的时候,涉及到求上下边之间的具体,试了好几次一直Phi设置的0,没有提取到结果,后来读了技术手册发现原来要切的是长边,也就是Width对应的边;这里的Width和Height都指的是半条边的长度,不过实际证明长边(Width)的长度对结果没有什么影响,短边(Height)设置的尽可能小。
下一条算子measure_pairs(Image : : MeasureHandle, Sigma, Threshold, Transition, Select : RowEdgeFirst, ColumnEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)是要提取垂直于矩形或环形弧的直边对,这里的Sigma要设置的尽可能小,高斯平滑处理后会导致边缘区分不明显;Threshold的设置要满足Threshold <= 2*Length2;后面的transition有positive和negative、all,分别对应dark-to-light、light-to-dark和取所有点。如果有多个点,可用_positive和_negative取差值最大的点。
Row := (RowEdgeFirst + RowEdgeSecond) / 2.0
Col := (ColumnEdgeFirst + ColumnEdgeSecond) / 2.0
disp_cross (WindowHandle, Row, Col, 40, rad(45))
▶取正方形的四个顶点求短边的中点并画×表示
image_points_to_world_plane (CamParam, Pose, Row, Col, 'mm', X1, Y1)
distance_pp (X1[0:5], Y1[0:5], X1[1:6], Y1[1:6], Distance)
tuple_mean (Distance, MeanDistance)
tuple_deviation (Distance, DeviationDistance)
disp_message (WindowHandle, 'Mean distance: ' + MeanDistance$'.3f' + 'mm +/- ' + DeviationDistance$'.3f' + 'mm', 'window', 30, 60, 'red', 'false')
▶投影到Z=0的平面上,因为前面set_origin_pose (Pose, 0, 0,0, Pose)设置了初始姿态,这里要将空间中的点转为平面上的点进行处理。然后使用distance_pp计算点与点长度,生成的数值存放在Distance数组里,使用tuple_mean和tuple_deviation分别计算平均长度和误差,使用disp_message给与显示。效果图如图5所示。
图5 测量结果图
比先前做标定测量精度有所提升,先前做的效果图如图6所示。
图6 测量对比图
这次测量结果为10.016mm+/-0.044mm,已经到达了很高的精度,相比于原来9.575mm+/-2.240mm的精度提升了很多,可能有几个原因:
(1)采光条件得到改善,这次采集的尺的图反光较少,阈值区分明显
(2)镜头距离标定板更近,标定板像素点采集的更大,精度得到提高
(3)设备更水平,得到的Pose更接近于Z=0的平面
3.2 物料块的测量
********************
*****求样本参数******
********************
ImgPath := '3d_machine_vision/calib4/'
read_image(Sample,ImgPath + 'calib_' + '01')
get_image_size(Sample,SampleWidth,SampleHeight)
dev_close_window ()
dev_open_window (0, 0, SampleWidth/4, SampleHeight/4, 'black', WindowHandle)
dev_update_off ()
dev_set_draw ('margin')
dev_set_line_width (3)
OpSystem := environment('OS')
set_display_font (WindowHandle, 14, 'mono', 'true', 'false')
▶定义变量,通过程序获取图片信息打开窗口。
********************
******相机标定*******
********************
StartCamPar := [0.016,0,0.0000074,0.0000074,326,247,SampleWidth,SampleHeight]
create_calib_data ('calibration_object', 1, 1, CalibDataID)
set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', StartCamPar)
set_calib_data_calib_object (CalibDataID, 0, 'caltab_30mm.descr')
NumImages := 8
for I := 1 to NumImages by 1
read_image (Image, ImgPath + 'calib_' + I$'02d')
dev_display (Image)
find_calib_object (Image, CalibDataID, 0, 0, I, [], [])
get_calib_data_observ_contours (Caltab, CalibDataID, 'caltab', 0, 0, I)
dev_set_color ('green')
dev_display (Caltab)
endfor
calibrate_cameras (CalibDataID, Error)
get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam)
get_calib_data (CalibDataID, 'calib_obj_pose', [0,1], 'pose', Pose)
write_cam_par (CamParam, 'camera_parameters.dat')
write_pose(Pose, 'campose.dat')
Message := 'camera parameters have'
Message[1] := 'been written to file'
disp_message (WindowHandle, Message, 'window', 12, 12, 'red', 'false')
clear_calib_data (CalibDataID)
set_origin_pose (Pose, 0, 0,0, Pose)
▶进行相机的内参和外参标定
********************
*****物料1D测量*****
********************
read_image (Image, ImgPath + 'material2')
dev_display (Image)
*get_mbutton (200000, shubiaorow, shubiaocolumn, Button)
********************
*******求长边********
********************
for j :=0 to 2 by 1
gen_measure_rectangle2 (1180+10*j,1036, 0, 800,3, SampleWidth, SampleHeight, 'bilinear', MeasureHandle)
measure_pairs (Image, MeasureHandle,0.5,6, 'all', 'all', RowEdgeFirst, ColumnEdgeFirst,\
AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)
Row := (RowEdgeFirst + RowEdgeSecond) / 2.0
Col := (ColumnEdgeFirst + ColumnEdgeSecond) / 2.0
disp_cross (WindowHandle, Row, Col,100, rad(45))
image_points_to_world_plane (CamParam, Pose, Row, Col, 'mm', X1, Y1)
distance_pp (X1[0:0], Y1[0:0], X1[1:1], Y1[1:1], DistanceX)
Distance1[j] := DistanceX
endfor
tuple_mean (Distance1, MeanDistance)
tuple_deviation (Distance1, DeviationDistance)
disp_message (WindowHandle, 'XMean distance: ' + MeanDistance$'.3f' + 'mm +/- ' + DeviationDistance$'.3f' + 'mm', 'window', 30, 60, 'red', 'false')
* close_measure (MeasureHandle)
*get_mbutton (200000, shubiaorow, shubiaocolumn, Button)
▶首先进行长边的求取,这里和上一个测尺不一样了,只能得到两个点了,不能求取像刚才一样求取平均值了,所以这里用了for j :=0 to 2 by 1的循环,定列坐标边横坐标,长方形的中心点为(1180+10*j,1036),进行长边的多组数据求取,然后使用distance_pp (X1[0:0], Y1[0:0], X1[1:1], Y1[1:1], DistanceX)求取长度,并将每次的结果导入到数组Distance1[j]中,Distance1[j] := DistanceX,然后再用Distance1[j]求取平均长度和差值。
********************
*******求短边********
********************
for k := 0 to 3 by 1
gen_measure_rectangle2 (1118,850+50*k,rad(90),400,3, SampleWidth, SampleHeight, 'bilinear', MeasureHandle)
measure_pairs (Image, MeasureHandle,0.5,6, 'all', 'all', RowEdgeFirst, ColumnEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)
Row := (RowEdgeFirst + RowEdgeSecond) / 2.0
Col := (ColumnEdgeFirst + ColumnEdgeSecond) / 2.0
disp_cross (WindowHandle, Row, Col,100, rad(45))
image_points_to_world_plane (CamParam, Pose, Row, Col, 'mm', X1, Y1)
distance_pp (X1[0:0], Y1[0:0], X1[1:1], Y1[1:1], DistanceY)
Distance2[k] := DistanceY
endfor
tuple_mean (Distance2, MeanDistance)
tuple_deviation (Distance2, DeviationDistance)
disp_message (WindowHandle, 'YMean distance: ' + MeanDistance$'.3f' + 'mm +/- ' + DeviationDistance$'.3f' + 'mm', 'window', 60, 60, 'red', 'false')
▶这里和上面使用的思维是一样的,就不赘述了。
******************************
*****求圆的边到长轴的距离******
******************************
gen_measure_rectangle2 (1102,1422,rad(90),400,4, SampleWidth, SampleHeight, 'bilinear', MeasureHandle)
measure_pairs (Image, MeasureHandle,0.5,8, 'all', 'all', RowEdgeFirst, ColumnEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)
Row := (RowEdgeFirst + RowEdgeSecond) / 2.0
Col := (ColumnEdgeFirst + ColumnEdgeSecond) / 2.0
disp_cross (WindowHandle, Row, Col,100, rad(45))
image_points_to_world_plane (CamParam, Pose, Row, Col, 'mm', X1, Y1)
distance_pp (X1[0:0], Y1[0:0], X1[1:1], Y1[1:1], Dot1X)
disp_message (WindowHandle, 'distance between Xside and circle '+ Dot1X+'mm', 'window',90, 60, 'red', 'false')
▶求完边长以后要求四个圆孔到边长的距离,其实这里的程序是在下面求完圆心坐标以后得到的长方形的中心坐标,这里为了程序的逻辑写在了上面,思路是一样的。这里只用了dark-to-light和light-to-dark的采集到的第一个值,也就是正方形经过的第一个孔,没有求第二个,所以只有一次测量的值,没有平均值,没有计算误差。
******************************
*****求圆的边到短轴的距离******
******************************
gen_measure_rectangle2 (936,1064,0,1000,4.5, SampleWidth, SampleHeight, 'bilinear', MeasureHandle)
measure_pairs (Image, MeasureHandle,0.4,6, 'all', 'all', RowEdgeFirst, ColumnEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColumnEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance)
Row := (RowEdgeFirst + RowEdgeSecond) / 2.0
Col := (ColumnEdgeFirst + ColumnEdgeSecond) / 2.0
disp_cross (WindowHandle, Row, Col,100, rad(45))
image_points_to_world_plane (CamParam, Pose, Row, Col, 'mm', X1, Y1)
distance_pp (X1[0:0], Y1[0:0], X1[1:1], Y1[1:1], Dot1Y)
disp_message (WindowHandle, 'distance between Yside and circle '+ Dot1Y+'mm', 'window',120, 60, 'red', 'false')
stop()
▶得到的图像如图6所示。
图6 测量结果1
实际的尺寸如图7所示。
图7 实物测量图
测量结果为长83.429mm+/-0.032mm;宽41.588mm+/-0.034mm;圆的边到长轴的距离为3.59463mm,圆的边到短轴的距离为7.31237mm,长误差3.4mm左右,宽误差1.6mm左右,圆孔距离边的长度误差在1mm以内,测量精度不是很高,可能存在的原因为:
(1)pose修正的问题,在加入铝板厚度后边角有毛刺,造成不是水平平面,但是没有很好地进行修正
(2)亚克力板比较薄,产生了应变,造成了测量误差
(3)打光环境不同,上一个测尺的时候采用的是相机自带的光源+底部打光,这里由于反光,关闭了相机光源,只采用底部打光,光照影响了标定的外参。
*****************************
******求各个点的中心坐标******
*****************************
threshold (Image, Regions, 56, 255)
connection(Regions,ConnectedRegions)
select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 5800, 6200)
dev_clear_window ( )
dev_set_draw ('fill')
dev_display (Image)
dev_display (SelectedRegions)
area_center (SelectedRegions, Area, Rows, Columns)
index := |Rows|
for l := 0 to index-1 by 1
messagex := (l+1)+'Row:'+ Rows[l]
messagey := (l+1)+'Column:'+ Columns[l]
disp_message (WindowHandle, messagex+' '+messagey, 'window',60+30*l, 60, 'red', 'false')
disp_message (WindowHandle, l+1, 'image', Rows[l]-30,Columns[l]-15, 'red', 'false')
Endfor
▶先用threshold对图像进行阈值化处理,然后使用connection进行区域分割,根据area选出面积在5800-6200之间的圆,然后通过area_center进行区域的面积和中心坐标的计算,最后用disp_message进行数据的输出。
得到的图像如图8所示。
图8 测量结果2
如图所示,得到四个点的中心坐标。完成大致的零件的标定,原来准备标定下两个缺口,但是使用正方形去切割的时候找不到点要不就是找到了很多的点,可能是因为边缘比较粗糙,要进行平滑处理。
四、设计总结
(1)设计图1所示的设备在标定的时候还是存在很大的问题,摄像头的外参标定会受光照条件的影响,但是苦于没有市面上那种透明的陶瓷标定板(标定板的光的透射率高),自己打印制成的标定板透光率差,从下方打光的时候会有局部的亮度降低,造成一开始标定的时候报错不够识别的minimum threshold,后来没有办法,只能使用相机自带的LED光源进行补光,才能够完成标定。
(2)定焦摄像头也给精度带来很大的影响。根据资料显示,标定板最好占视域的40%以上才能够达到很高的精度,但是本次摄像头的焦距比较长而且没有合适的镜头,所以只能距离大概8-10cm左右,此时才能够采集到完整而清楚的标定板,所以后来拍摄的物体像素点过少,精度误差较大。
(3)没有进行很好的参数修正,比如正方形与X轴的夹角,pose等,会引起较大的误差。