OpenCV 单目测距实现

       最近要做一个小项目,要完成相机的测距实现,最先考虑的是三角激光测距,但是实现起来太麻烦了,基本要搭一个简易的激光测距雷达...然后是双目测距,然而一个便宜的双目工业相机也要四五百,而且以前也没接触过双目测距...

       于是就想试试单目测距的效果怎么样,通过参考网络上的各种资料,加上以前玩过三角激光测距,所以也算比较顺利的写出来个简易实现单目测距的代码,精度还算差强人意。

原理大概是这样的 :

这是三维情况下的小孔成像模型 :

                                  

我们把它简化成更为直观的二维平面模型(渣画勿喷...):

                                                

       这里要说明下,这是一个典型的小孔成像模型,与单目相机的成像原理类似。

       中间通过红蓝的垂线是相机的主光轴,d是被测物体至镜头的距离,f为相机镜头的焦距,w为被测物体的实际宽度(高度),w'为物体在成像平面(感光元件)上的宽度(高度)。 

       根据相似三角形公式可得:f / d = w' / w

       由于f(相机镜头焦距)一般都是已知参数(买相机的时候总要选镜头焦距吧...),即使不清楚也可以通过计算得出,这里就不具体说了,w(实际物体宽或高)则是可以实际测得的常量。           

       重点说下怎么通过计算得出w'(物像宽或高)。举个栗子,假设你手上的相机是200w像素捕获画面尺寸为1920*1080,感光元件尺寸为1/2.7'',宽高分别为5.3mm和4mm(估值),被测物体在画面上的像素宽高都为600 pixel,那么w'(x) = (5.3 / 1920 * 600)(mm),同样的,w'(y) = (4 / 1080 * 600)(mm)。关于感光元件尺寸和画面尺寸可以通过查看相机参数手册获得,而感光元件的宽和高可以通过其尺寸计算出来,这里直接扔张感光元件靶面尺寸表作为参考,图来源自网络:

                                           

       这样,得知了 f,w,w' ,就可以根据上文提到的相似三角形公式计算得出d啦。

       不过以上考虑的的是理想情况,前提是保证物像光线通过主光轴,相机是完全无畸变的,且被测物体平面要与相机成像平面保持平行状态。在这里我使用的相机是无畸变工业相机(低程度畸变,完全没畸变的相机是不存在的...),如果是较为廉价的相机或者是广角镜头相机都会存在较大程度的画面畸变,可以通过软件进行校正。准确来说这些情况都属于无法完全消除的偏差,只能通过后期软件来尽量减少这种偏差。

       以上也就是为什么单目测距的精度要比双目测距差的多的原因,但是在所需测量精度不需要很精确的情况(偏差 < 10% ~ 15%),以及成本受限的情况下,单目测距依然是首选方案,何况其实现起来也十分简单。

如果想了解更加复杂的情况和更详细的原理可以参考这篇文章:【机器视觉】 单目视觉测距_机器视觉测距-CSDN博客

以下是代码实现部分,使用 OpenCV 3.4.0

// 单目测距.cpp: 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "opencv2/opencv.hpp"
#include <iostream>
#include <stdio.h>
#include <vector>

// 单位像素宽/高(cm/pixel)
#define UNIT_PIXEL_W 0.0008234375
#define UNIT_PIXEL_H 0.000825

using namespace std;

int main(void)
{
	cv::Mat frame;
	cv::VideoCapture capture(0);

	const double f = 0.35;  // 焦距
	const double w = 8.5;   // 被测物体宽度
	const double h = 5.6;   // 被测物体高度

	if (!capture.isOpened()) {
		printf("The camera is not opened.\n");
		return EXIT_FAILURE;
	}

	for (;;) {
		capture >> frame;
		if (frame.empty()) {
			printf("The frame is empty.\n");
			break;
		}
		cv::medianBlur(frame, frame, 3);
		
		cv::Mat grayImage;
		cv::cvtColor(frame, grayImage, cv::COLOR_BGR2GRAY);
		// otsu 可以换用动态阈值
		cv::threshold(grayImage, grayImage, NULL, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
		
		vector<vector<cv::Point>> contours;
		vector<cv::Point> maxAreaContour;
		
		cv::findContours(grayImage, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
		//cv::drawContours(frame, contours, -1, cv::Scalar(0, 0, 255), 2, 8);
		
		// 提取面积最大轮廓
		double maxArea = 0;
		for (size_t i = 0; i < contours.size(); i++) {
			double area = fabs(cv::contourArea(contours[i]));			
			if (area > maxArea) {
				maxArea = area;
				maxAreaContour = contours[i];
			}
		}
		// 轮廓外包正矩形
		cv::Rect rect = cv::boundingRect(maxAreaContour);
		cv::rectangle(frame, cv::Point(rect.x, rect.y), cv::Point(rect.x + rect.width, rect.y + rect.height), cv::Scalar(255, 0, 0), 2, 8);

		// 计算成像宽/高
		double width = rect.width * UNIT_PIXEL_W;
		double height = rect.height * UNIT_PIXEL_H;
		// 分别以宽/高为标准计算距离
		double distanceW = w * f / width;
		double distanceH = h * f / height;

		char disW[50], disH[50];
		sprintf_s(disW, "DistanceW : %.2fcm", distanceW);
		sprintf_s(disH, "DistanceH : %.2fcm", distanceH);
		cv::putText(frame, disW, cv::Point(5, 20), cv::FONT_HERSHEY_COMPLEX_SMALL, 1, cv::Scalar(0, 255, 0), 1, 8);
		cv::putText(frame, disH, cv::Point(5, 40), cv::FONT_HERSHEY_COMPLEX_SMALL, 1, cv::Scalar(0, 255, 0), 1, 8);

		cv::imshow("Frame", frame);
		cv::imshow("Gray", grayImage);
		if ((cv::waitKey(10) & 0XFF) == 27) break;
	}
	cv::destroyAllWindows();
	capture.release();
	
	return EXIT_SUCCESS;
}

这里使用的相机感光元件尺寸 CMOS 1/2.7'',捕获画面尺寸设置为640 * 480。

代码分别使用了w'(x)和w'(y)作为参数进行测距。

实际测试结果:

                                 

实际距离15cm,计算距离14.40 ~ 14.57cm,偏差量0.43 ~ 0.60cm

                               

实际距离7cm,计算距离6.83 ~ 6.85cm,偏差量0.15 ~ -0.17cm

                             

实际距离20cm,计算距离18.86 ~ 19.12cm,偏差量0.88 ~ 1.14cm

        可以看出,测距精度和实际距离呈反比,这里的测试结果跟其他博主写两篇文章测出的结果情况一样,都是偏小。另外,测量精度与画面尺寸(相机像素值)呈正比,如果用更高像素的相机来测量会跟精确。