光学处理 这里我用一段视觉引导着陆的算法来举例,在嵌入式平台或者机载计算机平台上,我们的算力并不是非常的强大,所以我们需要使用一些简单的占用率比较小的算法来完成,这就不得不使用opencv进行图像处理了。
这个课题的目的是视觉引导无人机进行着陆,无人机的机臂下会捆绑四个红色灯点,视觉部分在地面上扫描无人机所在位置,然后获取坐标并引导移动到中心部位着陆,同时还需要计算机体的旋转角度。
这个题目的难度主要集中在如何搜索无人机的四个红色灯点,我们大可以把环境假设的极端一些,也就是在阳光下的红色灯点。
1、色彩增强 为了确保在一些强光环境下导致相机过曝,我们必须把相机的曝光值调节的较低,这样可以过滤掉白天极亮的环境,也可以保留四个高亮的红色灯点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Camera (int id) : cap (id) { if (!cap.isOpened ()) { cerr << "Error: Camera not opened" << endl; exit (1 ); } cap.set (CAP_PROP_FRAME_WIDTH, 1920 ); cap.set (CAP_PROP_FRAME_HEIGHT, 1080 ); cap.set (CAP_PROP_AUTO_EXPOSURE, 0.25 ); cap.set (CAP_PROP_FPS, 30 ); camera_center_x = cap.get (CAP_PROP_FRAME_WIDTH) / 2 ; camera_center_y = cap.get (CAP_PROP_FRAME_HEIGHT) / 2 ; }
这里为了方便,我把整个图像处理环节写作了Camera类,同时也可以设置相机的内参用于之后的姿态恢复。
一些私有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private : double x{}, y{}; double angle{}; double distance{}; double f{}; double width{}, height{}; double camera_center_x{}, camera_center_y{}; Mat params; double k1, k2, p1, p2, k3; VideoCapture cap; Mat frame;
接下来需要给低曝光图像的色彩进行增强,也就是对RGB进行放大。这里可以直接使用opencv的方法:
1 2 Mat superframe; frame.convertTo (superframe, -1 , 0.4 , 0.6 );
其中的frame是private属性,是不断对相机捕获的照片帧。这个方法的原型为:
1 void convertTo( OutputArray m , int rtype , double alpha =1, double beta =0 ) const;
其中rtype为输出矩阵的位深度(即最大值比如8位深度就是255,即R最大取255,如果小于0则和原来一样,位深度决定了色彩的丰富度和精细度)。alpha为尺度变化,也就是把图像的RGB值都乘以alpha。beta为偏移量,即在最终结果加beta。也可以写作:
2、色彩提取 这里我需要把除了红色以外的所有颜色给降低影响,我们可以通过差值滤波,这里需要知道:
对于相机而言,CMOS上的一个像素需要同时保存RGB三种颜色信息,我们单纯提取单一色通道可能会夹杂着其他通道带来的影响。所以我们需要把红色通道依次减去蓝色绿色,将两个差加到一起就可以去除掉除了红色以外所有颜色的影响了
代码可以通过absdiff进行运算即:
1 2 3 4 5 6 7 Mat blue = channels[0 ]; Mat green = channels[1 ]; Mat diff1, diff2;absdiff (red, blue, diff1);absdiff (red, green, diff2); red = diff1 + diff2;
这样输出的图像就会变成:
可以看到白色直接被完全的削弱了。一旦我点个红灯就会发现:
这个清晰度已经达到了可以准确识别的效果,但是因为外面还存在一些色散,我们需要去除色散并二值化,这里需要用到高斯模糊和闭运算。
1 2 3 4 5 6 7 8 9 GaussianBlur(red , red , Size(5, 5) , 0 ); threshold(red, red, 100 , 125 , THRESH_BINARY); Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5) ); morphologyEx(red , red , MORPH_CLOSE, kernel ) ;
所谓的闭运算就是腐蚀后膨胀,开运算就是先膨胀后腐蚀。闭运算可以把色散去除掉,开运算可以保留更多光学信息。对于我们的要求,我们希望把无关的色彩信息与色散去除掉,我们必须使用闭运算去除掉散射的光线。通过此法就可以得到一个相对规矩的图像:
3、优化劣化 我们发现红色区域就是圆形,只不过边边角角太丑了,我们可以简单的膨胀一下,但是这里有更好的办法即:图形增益算法。假设我们需要增益的是圆形我们就需要进行边缘检测,我们就需要给定一个拟合度,当拟合度过低是把当前的边缘给掩盖住。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void enhanceCircles (cv::Mat& input, cv::Mat& UPend, double threshold) { Mat binaryImage = input.clone (); cv::Mat output = cv::Mat::zeros (binaryImage.size (), CV_8UC1); std::vector<std::vector<cv::Point>> contours; cv::findContours (binaryImage, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); for (const auto & contour : contours) { cv::Point2f center; float radius; cv::minEnclosingCircle (contour, center, radius); double areaContour = cv::contourArea (contour); double areaCircle = CV_PI * radius * radius; if (std::abs (areaContour - areaCircle) < areaCircle * threshold) { cv::circle (output, center, static_cast <int >(radius), cv::Scalar (255 ), -1 ); } } cv::GaussianBlur (output, output, cv::Size (0 , 0 ), 5 ); UPend = output.clone (); cv::bitwise_or (output, binaryImage, binaryImage); }
这个方法通过模糊劣化非圆形区域得到较为真实的圆形区域,判断轮廓仅需判断边界与半径的距离是否超过了阈值比例即可。看看此法的输出效果:
1 2 3 4 Mat UPend;enhanceCircles (red,UPend, 0.7 ); _imgshow("UPend" , UPend);
4、边缘识别
此法把原本图形的杂七杂八部分全部去除掉,保留了人工绘制的区域。这样我们再去寻找圆形区域就变得即为简单了。
1 2 3 4 5 6 vector<vector<Point>> filter_contours; vector<Vec4i> filter_hierarchy;findContours (UPend, filter_contours, filter_hierarchy, RETR_TREE, CHAIN_APPROX_TC89_KCOS);drawContours (frame, filter_contours, -1 , Scalar (0 , 255 , 0 ), 2 );
这样绘制出几个圆形就可以显示一下了。
因为我们需要通过四个灯得到飞机的位置和姿态,所以我们把四个圆形的圆心当作四个顶点计算几何中心,并以四个点为参考系计算相对姿态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 vector<Point> centers;for (const auto & contour : filter_contours) { Moments m = moments (contour); Point center (m.m10 / m.m00, m.m01 / m.m00) ; centers.push_back (center); }if (centers.size () > 4 ) { NoOpetion; }else if (centers.size () < 4 && !centers.empty ()){ }else { }
我们可以看一下经过上述算法处理后的效果:
我们可以看到绘制的都非常精确,中心点找的也非常好。假设我们正好找到四个点的话,我们就需要进行中心的绘制(也就是中心点紫色的位置,因为代码已经写好了,所以我就没有取消中心点的绘制)
5、坐标恢复 这里只需要简单的计算就好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 poseRecovery (centers); vector<Point> square_centers;for (const auto& center1 : centers) { for (const auto& center2 : centers) { for (const auto& center3 : centers) { for (const auto& center4 : centers) { double d1 = norm (center1 - center2); double d2 = norm (center2 - center3); double d3 = norm (center3 - center4); double d4 = norm (center4 - center1); if (d1 > 100 || d2 > 100 || d3 > 100 || d4 > 100 ) continue ; double d5 = norm (center1 - center3); double d6 = norm (center2 - center4); if (std::abs (d1 - d2) < 10 && std::abs (d2 - d3) < 10 && std::abs (d3 - d4) < 10 && std::abs (d4 - d1) < 10 && std::abs (d5 - d6) < 10 ) { square_centers.push_back (center1); square_centers.push_back (center2); square_centers.push_back (center3); square_centers.push_back (center4); } } } } }if (square_centers.size () < 4 ) NoOpetion;this ->width = norm (square_centers[0 ] - square_centers[1 ]);this ->height = norm (square_centers[1 ] - square_centers[2 ]); Point square_center;for (const auto& center : square_centers) square_center += center;if (square_centers.empty ()) NoOpetion; square_center.x /= square_centers.size (); square_center.y /= square_centers.size ();this ->x = square_center.x - this ->camera_center_x;this ->y = square_center.y - this ->camera_center_y;this ->angle = atan2 (this ->y, this ->x) * 180 / CV_PI;circle (frame, square_center, 15 , Scalar (255 , 0 , 255 ), -1 );
一旦正好检测到四个点,并且对于这个焦段在5m的相机来讲,飞机肯定在很近很近的位置了,所以我们必须要确保飞机的姿态正常,我们需要计算一下四个点组成的形状是否合规,是否存在异常情况。
这里用的姿态恢复是PnP约束,也就是简单的三角约束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void poseRecovery (std::vector<Point>& specialPoint) { if (this ->params.empty ()) { cerr << "Error: Camera parameters not set" << endl; return ; } if (this ->k1 == 0 && this ->k2 == 0 && this ->p1 == 0 && this ->p2 == 0 && this ->k3 == 0 ) { cerr << "Error: Distortion coefficients not set" << endl; return ; } std::vector<Point2f> specialPointCamera; cv::undistortPoints (specialPoint, specialPointCamera, params, Mat::eye (3 , 3 , CV_64F), Mat::zeros (5 , 1 , CV_64F)); std::vector<Point3f> specialPointWorld; for (const auto & point : specialPointCamera) { double x = point.x; double y = point.y; double z = f; specialPointWorld.emplace_back (x, y, z); } Mat rvec, tvec; cv::solvePnP (specialPointWorld, specialPointCamera, params, Mat::zeros (5 , 1 , CV_64F), rvec, tvec); Mat R; cv::Rodrigues (rvec, R); Mat euler; cv::Rodrigues (R, euler); Mat relativePose = Mat::eye (4 , 4 , CV_64F); R.copyTo (relativePose (Rect (0 , 0 , 3 , 3 ))); tvec.copyTo (relativePose (Rect (3 , 0 , 1 , 3 ))); }
6、数码变焦 通过计算投影光线与成像平面组成的三角形,利用相机内参求解三角形边长就可以恢复相机的旋转矩阵。将平移矩阵当作计算出的边长就可以了。此法运行后就得到了上面的图片。对于超过4个点的情况我们直接不做处理就好。对于0到4个点的情况,我们只需要进行权重数码缩放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Mat scaleImageAtPosition (Mat treatIMG, int x, int y, double f) { Mat inputImage = treatIMG.clone (); Mat outputImage; int center_width = (int )(inputImage.cols / f); int center_height = (int )(inputImage.rows / f); int roi_x = std::max (0 , x - center_width / 2 ); int roi_y = std::max (0 , y - center_height / 2 ); if (roi_x + center_width > inputImage.cols) roi_x = inputImage.cols - center_width; if (roi_y + center_height > inputImage.rows) roi_y = inputImage.rows - center_height; cv::Rect roi (roi_x, roi_y, center_width, center_height) ; cv::Mat roiImage = inputImage (roi); cv::resize (roiImage, outputImage, cv::Size (), f, f); return outputImage; }
数码变焦就是给定缩放中心点,给定缩放倍数,框选一个ROI区域进行方法即可。这样可以保证图像尺寸不变,此法劣于光学变焦的点在于会损失像素信息,优点就是总体的场视角不会改变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Point center;for (const auto & c : centers) center += c; center.x /= centers.size (); center.y /= centers.size (); Mat zoomed = scaleImageAtPosition (red, center.x, center.y, ZOOM); Mat scaleFrame = scaleImageAtPosition (frame, center.x, center.y, ZOOM); Mat NPend;enhanceCircles (zoomed,NPend, 0.5 ); _imgshow("NPend" , NPend); vector<vector<Point>> scale_contours; vector<Vec4i> scale_hierarchy;findContours (NPend, scale_contours, scale_hierarchy, RETR_TREE, CHAIN_APPROX_TC89_KCOS); vector<Point> new_centers;for (const auto & contour : scale_contours) { Moments m2 = moments (contour); Point new_center (m2.m10 / m2.m00, m2.m01 / m2.m00) ; new_centers.push_back (new_center); }if (new_centers.size () != 4 ) NoOpetion; Point square_center;for (const auto & center : new_centers) square_center += center; square_center.x /= new_centers.size (); square_center.y /= new_centers.size ();circle (scaleFrame, square_center, 15 , Scalar (255 , 0 , 255 ), -1 ); _imgshow("scaleFrame" , scaleFrame); Point relatice_center = relativePosition (square_center, Point (zoomed.cols / 2 , zoomed.rows / 2 ), center, ZOOM);this ->x = relatice_center.x - this ->camera_center_x;this ->y = relatice_center.y - this ->camera_center_y;this ->angle = atan2 (this ->y, this ->x) * 180 / CV_PI;circle (frame, relatice_center, 15 , Scalar (255 , 0 , 255 ), -1 );return ;
这里存在了一个缩放后目标恢复到原坐标系的过程,这个很简单,高中生都可以推出:
1 2 3 4 5 6 7 8 9 10 11 Point relativePosition (Point aim, Point change_center, Point change_normal_center, double f) { double dx = (change_center.x - aim.x) * (1 - 1 / f); double dy = (change_center.y - aim.y) * (1 - 1 / f); Point relative; relative.x = change_normal_center.x + dx; relative.y = change_normal_center.y + dy; return relative; }
也就是把缩放后的偏差缩放回去,毕竟虽然坐标系变了,但是相对关系没变。这样我们就得到了一个权重数码变焦算法。
7、结束 通过这个办法就可以实现一个10m内的10寸无人机引导降落。如果在外太空还可以进行相对姿态解算进行空间站对接。