本文是由RoboticFan网友Rockets翻译的一篇由国外机器人爱好者撰写的激光测距仪的文章。RobotSky认为这篇文章具有相当的实用型和可操作性,发散一下思维能力,可以发现有很多应用。
有很多现成的测距组件包括超声波、红外线、甚至是激光测距仪。这些设备运行的很好,但是对于飞行机器人来说,重量是一个主要考虑因素。一个可行的办法是增加现有组件的功能,并安装在机身上。例如微型空中机器人的有效载荷是100g。它能利用USB连接的摄像头(或mini无线摄像头)执行视觉任务,例如避障等。更好的是,如采用两个摄像头,能提供立体的机器视觉,这样能增强避障性能,因为双镜头提供了视觉深度。但缺点是需要增加另外一个摄像头的重量。这篇文章就是讨论如何利用一个激光笔和一个摄像头来提供一个单镜头机器视觉和测距的。
下图显示了如何将激光点投射到目标物上,并在摄像头上显示。摄像头和激光点的距离是可以通过计算而得出的。公式很简单,因此这个技术在需要很快运行的机器视觉应用上是适合的。

介绍一下工作原理。一束激光被投射到目标物上,并在摄像头上被显示。激光束被认为是理想的平行于摄像头的中心光轴。激光点由摄像头的其余部分所捕获。一个简单的计算就是寻找最亮点。如果设激光点就是这个场景的最亮点(似乎在室内我的激光发射器确实是最亮的),那么这个点的位置在图帧中的位置是确定的。然后我们只需要计算这个点在沿着y轴的距离,就能计算出目标物离摄像头的距离,激光点距离中心越近,离目标物越远。
如同公式所示,距离D是可以被计算出来的。

为了计算这个等式,我们需要知道激光器和摄像头之间的距离h,这是个常数,还有角度,角度可以计算。

其中:
pfc=从焦平面到中心的像素数量
rpc=单个像素的弧度
ro=弧度补偿(弥补对齐错误)
代入上式,我们得到:

这样,从图像中就能将焦平面到激光点像素数计算出来。那其他的常数怎么办呢?我们需要执行一个校准来得到这些数据。
为了校准这个系统,我们需要收集一系列测量的数据,每次测得的目标物的距离和这个激光点离中心点的像素数。数据如下
校正数据| pixels from center | actual D (cm) |
| 103 | 29 |
| 81 | 45 |
| 65 | 58 |
| 55 | 71 |
| 49 | 90 |
| 45 | 109 |
| 41 | 127 |
| 39 | 159 |
| 37 | 189 |
| 35 | 218 |
使用下面的公式,我们能够利用激光器和摄像头之间的距离h和真实距离计算出真实的角度:

θactual=真实角度
Dactual=真实距离(测量得出)
现在我们有了公式中的每个数值,我们可以利用一个关系式来计算点离中点的像素数。我用了一个线性关系式。这个公式看起来很有用,……
从我的校正数据中,我计算出:
Offset (ro) = -0.056514344 radians
Gain (rpc) = 0.0024259348 radians/pixel
使用:

下表是列举了根据上面ro和rpc值计算出的距离值,实际距离值和误差: RoboticFan
实际和计算的测量数据| pixels from center | calc D (cm) | actual D (cm) | % error |
| 103 | 29.84 | 29 | 2.88 |
| 81 | 41.46 | 45 | -7.87 |
| 65 | 57.55 | 58 | -0.78 |
| 55 | 75.81 | 71 | 6.77 |
| 49 | 93.57 | 90 | 3.96 |
| 45 | 110.85 | 109 | 1.70 |
| 41 | 135.94 | 127 | 7.04 |
| 39 | 153.27 | 159 | -3.60 |
| 37 | 175.66 | 189 | -7.06 |
| 35 | 205.70 | 218 | -5.64 |
软件
我通过两个方式编写了这个软件,一个是vc++,一个是VB。你能发现VB版本的软件会比VC++的软件更容易一些,但是各有取舍。VC++版本能够自由的加入其他软件中?VB版本需要第三方软件支持(在Visual Studio中)
Visual Basic
vb_laser_ranger.zip
这里可以下载到我的VB版本软件。
要使用上面的程序,你必须要安装VideoOCX ActiveX 控件
主程序如下:
Private Sub exit_Click()
' only if running...
If (Timer1.Enabled) Then
Timer1.Enabled = False 'Stop Timer
VideoOCX.Stop
VideoOCX.Close
End If
End
End SubPrivate Sub Start_Click() 'Init VideoOCX Control, allocate memory and start grabbing
If (Not Timer1.Enabled) Then
Start.Caption = "Stop"
' Disable internal error messages in VideoOCX
VideoOCX.SetErrorMessages False
' Init control
If (Not VideoOCX.Init) Then
' Init failed. Display error message and end sub
MsgBox VideoOCX.GetLastErrorString, vbOKOnly, "VideoOCX Error"
End
Else
' Allocate memory for global image handle
capture_image = VideoOCX.GetColorImageHandle
' result_image = VideoOCX_Processed.GetColorImageHandle
Timer1.Enabled = True 'Start capture timer
' Start Capture mode
If (Not VideoOCX.Start) Then
' Start failed. Display error message and end sub
MsgBox VideoOCX.GetLastErrorString, vbOKOnly, "VideoOCX Error"
End
End If
End If
Else
Start.Caption = "Start"
Timer1.Enabled = False 'Stop Timer
VideoOCX.Stop
VideoOCX.Close
End If
End Sub
Private Sub Timer1_Timer()
' Timer for capturing - handles videoOCXTools
Dim matrix As Variant
Dim height, width As Integer
Dim r, c As Integer
Dim max_r, max_c As Integer
Dim max_red As Integer
Dim gain, offset As Variant
Dim h_cm As Variant
Dim range As Integer
Dim pixels_from_center As Integer
' Calibrated parameter for pixel to distance conversion
gain = 0.0024259348
offset = -0.056514344
h_cm = 5.842
max_red = 0
' Capture an image
If (VideoOCX.Capture(capture_image)) Then
' VideoOCX.Show capture_image
' Matrix transformation initialization
matrix = VideoOCX.GetMatrix(capture_image)
height = VideoOCX.GetHeight
width = VideoOCX.GetWidth
' Image processing code
' The laser dot should not be seen above the middle row (with a little pad)
For r = height / 2 - 20 To height - 1
' Our physical setup is roughly calibrated to make the laser
' dot in the middle columns...dont bother lookng too far away
For c = width / 2 - 25 To width / 2 + 24
' Look for the largest red pixel value in the scene (red laser)
If (matrix(c, r, 2) > max_red) Then
max_red = matrix(c, r, 2)
max_r = r
max_c = c
End If
Next c
Next r
' Calculate the distance for the laser dot from middle of frame
pixels_from_center = max_r - 120
' Calculate range in cm based on calibrated parameters
range = h_cm / Tan(pixels_from_center * gain + offset)
' Print laser dot position row and column to screen
row_val.Caption = max_r
col_val.Caption = max_c
' Print range to laser illuminated object to screen
range_val.Caption = range
' Draw a red vertical line to intersect target
For r = 0 To height - 1
matrix(max_c, r, 2) = 255
Next r
' Draw a red horizontal line to intersect target
For c = 0 To width - 1
matrix(c, max_r, 2) = 255
Next c
VideoOCX.ReleaseMatrixToImageHandle (capture_image)RoboticFan
End If
VideoOCX.Show capture_image
End Sub
截屏如下:


Visual C++
我的代码是基于Paul Oh教授的教程。
你需要注意,当跟进这个教程的时候,一些必要的文件也许不再正常连接或丢失,他们可以在下面的位置下载。
qcsdk.exe
qc543enu.exe
根据 TRIPOD 的教程的说明,可以在其源程序中插入一段用户自己的图像处理代码,在这里,我插入了下面的代码:
void CTripodDlg::doMyImageProcessing(LPBITMAPINFOHEADER lpThisBitmapInfoHeader)
{
// doMyImageProcessing: This is where you'd write your own image processing code
// Task: Read a pixel's grayscale value and process accordingly unsigned int W, H; // Width and Height of current frame [pixels]
unsigned int row, col; // Pixel's row and col positions
unsigned long i; // Dummy variable for row-column vector
unsigned int max_row; // Row of the brightest pixel
unsigned int max_col; // Column of the brightest pixel
BYTE max_val = 0; // Value of the brightest pixel
// Values used for calculating range from captured image data
// these values are only for a specific camera and laser setup
const double gain = 0.0024259348; // Gain Constant used for converting
// pixel offset to angle in radians
const double offset = -0.056514344; // Offset Constant
const double h_cm = 5.842; // Distance between center of camera and laser
double range; // Calculated range
unsigned int pixels_from_center; // Brightest pixel location from center
// not bottom of frame
char str[80]; // To print message
CDC *pDC; // Device context need to print message
RoboticFan W = lpThisBitmapInfoHeader->biWidth; // biWidth: number of columns
H = lpThisBitmapInfoHeader->biHeight; // biHeight: number of rows
for (row = 0; row < H; row++) {
for (col = 0; col < W; col++) {
// Recall each pixel is composed of 3 bytes
i = (unsigned long)(row*3*W + 3*col);
// If the current pixel value is greater than any other, it is the new max pixel
if (*(m_destinationBmp + i) >= max_val)
{
max_val = *(m_destinationBmp + i);
max_row = row;
max_col = col;
}
}
}
// After each frame, reset max pixel value to zero
max_val = 0;
for (row = 0; row < H; row++) {
for (col = 0; col < W; col++) {
i = (unsigned long)(row*3*W + 3*col);
// Draw a white cross-hair over brightest pixel in the output display
if ((row == max_row) || (col == max_col))
*(m_destinationBmp + i) =
*(m_destinationBmp + i + 1) =
*(m_destinationBmp + i + 2) = 255;
}
}
// Calculate distance of brightest pixel from center rather than bottom of frame
pixels_from_center = 120 - max_row;
// Calculate range in cm based on bright pixel location, and setup specific constants
range = h_cm / tan(pixels_from_center * gain + offset);
// To print message at (row, column) = (75, 580)
pDC = GetDC();
// Display frame coordinates as well as calculated range
sprintf(str, "Max Value at x= %u, y= %u, range= %f cm ",max_col, max_row, range);
pDC->TextOut(75, 580, str);
ReleaseDC(pDC);
}
完整的代码可以在下面下载到:
LaserRange.zip
可执行文件可在下面下载到:
LaserRange.exe
注意,为了执行这个文件,你可能需要qcsdk和qc543这两个驱动文件。
下面是摄像头激光测距仪的工作截图,注意它是如何工作的。在第二个例子中,有两个激光点,其中的一个是激光点在摄像头里面的反射,这个反射点由于没有那么强烈的,所以不适用于运算法则。

将来的工作
一个重要的改进就是将点改为线,这样可以计算每个光柱的距离而不是单个的光柱。这样的设置可以使车辆能够探测最大的前进距离,同样的,障碍物的最小距离也可以被探测到。