暑期做的一个项目,开始并不是很熟悉,在网上查找的资料也不是很具体,但是自习学习了理论知识之后还是比较容易的做出来这个项目,现在开源整个项目,由于篇幅有限,本文适合稍微有点点基础的朋友。源码见底部
先显示下最后结果:
主要有步进电机(建议精度高点,这涉及到后面的精确度问题,一般42电机即可)、步进电机驱动、彩色相机、一字型激光头、arduino、各部分的供电单元;基本的硬件框架包括能够固定在步进电机上的旋转台,以及一些整体的固定装置。我的效果如下(不一定和我一样,完成相关功能即可,我也是利用实验室有限资源):
本设计为线结构光三维扫描,是一种基于光学三角法的非接触式物体表面轮廓成像技术,利用线激光投影到被测物体表面,工业相机采集受到物体表面高度调制的形变条纹,经过计算得到表面轮廓三维数据。很多相关设计采用了数学上的几何关系来解算,例如之前参考过一个方法是结合镜头光轴与激光线的距离b以及夹角c0,再用镜头光轴垂直于镜头光心与激光头的连线,结合基本的几何关系以及正弦定理推算出物体距离深度信息,个人认为这种方法确实十分好的利用了激光三角法的基本原理,但是对于装置的硬件要求过高,例如实际上角度和距离并不是很准确的能够控制,加上是光心到激光头中心的这种不易测量的距离。随本设计采用标准的标定的方式,后面能够发现对硬件要求很低。但是由于我也是为了简单的模型设计,项目本身并没有实际参数的要求,故实际测量等对有些变量并没有很好地测量与控制,这并不影响我们对线激光三维成像原理与方法的理解;实际来看实验效果依旧不错,大家可以根据实际情况对硬件加以更严谨的测量和控制。
相机标定: 相机的标定为的是获取相机的内外参数,有一点需要指出,就是标定后相机的内参矩阵只有一个,但是外参矩阵的数目是和标定板是一样的,因为每一个标定板的平面都是被视为一个世界坐标系的XoY面(Z为0),相片摆放不同即表示各个世界坐标系不同,而内参矩阵是代表相机坐标系与世界坐标系的转换关系,相机坐标系是一直变化的,随着世界坐标系的变化外部转移矩阵自然一直变化。为简化标定过程,相机标定直接采用matlab相机标定工具箱,具体教程可参考如下链接 MATLAB--相机标定教程。得到的Intrinsic Matrix为内参矩阵,RotationMatrix 何Translation Vector 即为外部矩阵对应的旋转矩阵和平移向量;注意,我验证过,这里得到的矩阵结果,如代入下面的公式计算需要进行转置,程序上有体现。标定时我默认为倒数第二张标定板图片为唯一的世界坐标系(即后面的计算以此世界坐标系为基础),倒数第一张标定板图片我用来作为后面的激光光平面的标定的临时坐标系;最后两张标定板照片采集后分别保持不动,打开激光,并分别采集激光打在标定平面上的图片,以备后面光平面标定使用。
%% 获取标定结果 MATLAB 获取 经计算 需转置矩阵
load('cameraParams.mat');
intriMatrix = cameraParams.IntrinsicMatrix';
R = cameraParams.RotationMatrices(:, :, 21)';
T = cameraParams.TranslationVectors(21, :)';
% Temporary coordinate system
R_t = cameraParams.RotationMatrices(:, :, 22)';
T_t = cameraParams.TranslationVectors(22, :)';
光平面标定: 本设计目前发现的对硬件结构有一些基本的要求,主要体现在1、要保证旋转台水平;2、尽量保证激光线垂直于旋转台中心(这个我只是目测简单的校准,或许通过算法的改变可以解决这个硬件约束,但是硬件上稍微的一点约束,程序上的数学处理会少很多)。光平面的主要思路是,在唯一世界坐标系上的激光线(前述的激光打在标定板的图片)上找到两个点,再在临时坐标系上找到一点,利用相机坐标系不变的原理,将临时坐标系上的光点通过转换关系(外部矩阵)先转到相机坐标系,再从相机坐标系转到唯一世界坐标;光点的像素坐标获取主要使用steger算法和交比不变性:
%% 计算光平面方程
close all;
% 世界坐标系
chessPoint = cameraParams.ReprojectedPoints(:, :, 21);
second_row = chessPoint(2, 2);
fourth_row = chessPoint(4, 2);
delta_row = sqrt((chessPoint(1, 1) - chessPoint(2, 1))^2 + (chessPoint(1, 2) - chessPoint(2, 2))^2);
frame = imread('F:\旋转式3d激光扫描\Image\Calibration\cali21_laser.jpg');
background = imread('F:\旋转式3d激光扫描\Image\Calibration\cali21.jpg');
laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
[row, column, channel] = size(frame);
image_1=zeros(row,column); % 等大的全黑背景
max = length(laserPixel);
for index = 1 : max
image_1(laserPixel(index, 2), laserPixel(index, 1)) = 255;
end
% find the first point on the light: pixel coordinate ---> (column_index, row_index)
for row_index = round(second_row - delta_row/2) : round(second_row)
column_index = find(image_1(row_index, :) == 255);
if length(column_index) == 1
break;
end
end
x1 = (chessPoint(1, 1) - column_index)/delta_row*15;
y1 = (chessPoint(1, 2) - row_index)/delta_row*15;
z1 = 0;
% find the second point on the light: pixel coordinate ---> (column_index, row_index)
for row_index = round(fourth_row - delta_row/2) : round(fourth_row)
column_index = find(image_1(row_index, :) == 255);
if length(column_index) == 1
break;
end
end
x2 = (chessPoint(1, 1) - column_index)/delta_row*15;
y2 = (chessPoint(1, 2) - row_index)/delta_row*15;
z2 = 0;
%% 临时坐标系
chessPoint = cameraParams.ReprojectedPoints(:, :, 22);
second_row = chessPoint(2, 2);
fourth_row = chessPoint(4, 2);
delta_row = sqrt((chessPoint(1, 1) - chessPoint(2, 1))^2 + (chessPoint(1, 2) - chessPoint(2, 2))^2);
frame = imread('F:\旋转式3d激光扫描\Image\Calibration\cali22_laser.jpg');
background = imread('F:\旋转式3d激光扫描\Image\Calibration\cali22.jpg');
laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
[row, column, channel] = size(frame);
image_1=zeros(row,column); % 等大的全黑背景
max = length(laserPixel);
for index = 1 : max
image_1(laserPixel(index, 2), laserPixel(index, 1)) = 255;
end
% find the first point on the light: pixel coordinate ---> (column_index, row_index)
for row_index = round(second_row - delta_row/2) : round(second_row)
column_index = find(image_1(row_index, :) == 255);
if length(column_index) == 1
break;
end
end
x3_t = (chessPoint(1, 1) - column_index)/delta_row*15;
y3_t = (chessPoint(1, 2) - row_index)/delta_row*15;
z3_t = 0;
CCS3 = R_t * [x3_t; y3_t; z3_t] + T_t;
WCS3 = pinv(R) * (CCS3 - T);
x3 = WCS3(1, 1); y3 = WCS3(2, 1); z3 = WCS3(3, 1);
[a, b, c, d] = createLightPlane(x1, y1, z1, x2, y2, z2, x3, y3, z3, 0); % 后面加上非0参数即可显示
旋转平面的标定:即确定步进电机驱动脉冲与相机的帧的关系;我使用matlab与arduino通信的方式,一定的脉冲之后采集一张图片。关于matlab与arduino通信有很多博客提供了方法 激光扫描三维重建——3.matlab和arduino通信 。此处为了后面的色彩匹配与方便提取激光中线,采用的是第一圈开激光,第二圈关闭激光的方式(我的激光器是直连的,没有激光驱动,故采用分别亮灭的方式,但是这样不一定很好的保证两张图片的对应,因为步进电机的一圈不一定是完整的一圈);最好采用控制激光亮灭的方式拍两张图片,分别作为image和background;
Uno = arduino('COM5');
Uno.pinMode(7, 'output');
video = videoinput('winvideo', 2, 'YUY2_640x480');
triggerconfig(video, 'manual');
start(video);
pause(2);
fprintf(' Catch Frame...... \n');
laser = 0; %是否有激光
for i = 1 : 320
OneStep(Uno, 0.005);
frame = getsnapshot(video);
frame = ycbcr2rgb(frame);
if laser == 1
imwrite(frame,strcat('F:\旋转式3d激光扫描\Image\NongfuSpring\Reconstruct\','image',num2str(i,'%d'),'.jpg'),'jpg');% 保存帧
else
imwrite(frame,strcat('F:\旋转式3d激光扫描\Image\NongfuSpring\Background\','background',num2str(i,'%d'),'.jpg'),'jpg');% 保存帧
end
end
fprintf(' Complete!!! \n');
delete(instrfind({'Port'}, {'COM5'}));
stop(video);
delete(video);
对每张照片处理的时候,采用背景减除 image - background,将直接得到激光区域,减少了环境的干扰;对激光区域使用steger算法求出激光中心;分别对每一帧上的每一个激光中心进行三维坐标的解算;具体如下:
由标定原理:
由上面的公式得到:
由光平面方程:
( 2 )
由(1)和(2)联列,四个方程,四个未知数Xw, Yw, Zw, Zc 可解出我们输入像素点(u,v)的 三维坐标(Xw, Yw, Zw)。
求出单个像素点的空间坐标后,需要结合旋转台的旋转参数来解算出对应的唯一世界坐标系下的坐标;由于前述要求了激光线垂直于圆台中心,对于旋转体,核心是旋转半径 r = Xw^2 + Yw^2以及旋转角(与相片索引和旋转角分辨率相关);
色彩匹配即利用刚刚求出的各激光中心点对应的像素坐标,对应在background相片中提取色彩信息即可!
%% 生成点云
picture_num = 320; %
WCS = []; % 所有点的坐标
color = fopen('apple.txt','w');
for frame_index = 1 : picture_num
frame = imread(['F:\旋转式3d激光扫描\Image\Reconstruct\image',num2str(frame_index),'.jpg']);
background = imread(['F:\旋转式3d激光扫描\Image\Background\background',num2str(frame_index),'.jpg']);
fprintf(' Processing %d th image...\n', frame_index);
laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
for pixel_index = 1 : length(laserPixel)
[Xw, Yw, Zw] = pcs2wcs(laserPixel(pixel_index, 1), laserPixel(pixel_index,2), intriMatrix, R, T, a, b, c, d, 0);
r = sqrt((Xw - 40)^2 + Zw^2);
theta = atan2(-Zw, 40 - Xw);
Xw = r * cos(theta - frame_index/picture_num * 2 * pi);
Zw = r * sin(theta - frame_index/picture_num * 2 * pi);
red = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 1);
green = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 2);
blue = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 3);
WCS = [WCS; Xw Yw Zw];
fprintf(color,'%d %d %d %d %d %d\n',Xw, Yw, Zw, red, green, blue);
end
end
%save('apple.txt', 'WCS', '-ascii');
fprintf(' Processing Complete! Saved as ‘apple.txt‘ \n');
由于篇幅和个人水平有限,博客中代码段均只给出主函数部分,详细全部代码可以在评论处留下邮箱!欢迎各位同仁指正!!!