目录
- 摄像头硬件配置
- 车载安装示意图
- 图像数据获取(接口)
- 相机成像关键参数解析
- 相机内参和畸变校正
- camera to camera 外参标定
- 目标检测追踪
- 目标级融合
- 单目测距
- 输出障碍物位置、大小、方向、类型信息
- 参考链接
1、apollo采用的摄像头配置为:
3个摄像头,两个带有 6mm镜头和一个带有 25mm镜头,以达到交通灯检测应用所需的性能。具体型号采用如下三种中的一种
-
Leopard Imaging Inc's Camera - LI-USB30-AZ023WDRB
-
Truly Argus Camera
-
Wissen Camera
#todo 为什么采用这几种摄像头,选型技术细节方案等相关研究
Argus摄像头参数如下:
2、车载安装示意图
#todo 摄像头具体安装位置的要求 代码中在做camera to camera映射的时候有一条假设是长焦相机和短焦相机光学中心相同
3、图像数据获取(接口)
数字图像成像是把光信号转变为电信号。摄像头图像采集设备是由透镜、传感器、ISP等硬件组成。并且通过线性纠正、噪点去除、坏点修补、颜色差值、白平衡校正、曝光校正等处理后形成的图像。3A技术指的是自动对焦(AF)、自动曝光(AE)及自动白平衡(AWB)。自动对焦算法通过既得图像对比度移动镜头使图像对比度达到最大,自动曝光算法将根据可用的光源条件自动设置曝光值,自动白平衡算法根据光源条件调整图片颜色的保真程度。
摄像头成像采用的物理原理是小孔成像和透镜成像原理(聚光增加清晰度、但是会带来镜像畸变,畸变校正后基本不会影响单目测距的精度)。
一般我们从摄像头读取图像数据,不考虑性能的话采用opencv即可。获取代码:
#include "opencv2/opencv.hpp"
cv::Mat cv_img = cv::imread("demo.png");
apollo获取数据的方式:
#todo 实车运行时数据流形态
多种传感器的数据组织成.record包(3之前的版本似乎是 ros bag包)
感知模块中摄像头感知部分的数据流动形态(参照了caffe的数据流形态):
cv::Mat ——》image8u(自定义的图像表示结构 借用caffe数据流形态 syncedmem ——》blob——》image8u——》distortion_model (获取相机内参、畸变系数、图像大小等参数)——》undistortion_handler(畸变校正))
io部分:
io_util sensor_manager
涉及的知识点:内存管理、cuda显存的调用、gpu/cpu之前数据的调度。
4、相机成像关键参数解析
想要知道相机的工作原理要从小孔成像开始:
根据上述示意图,重点解析关键参数焦距、FOV、帧率:
焦距是镜片中心到成像传感器的距离,一般以mm毫米为单位,2.8mm、4mm、6mm、8mm、12mm、16mm甚至还有25mm等可以选。
帧率一般为30fps,高速摄像机可以达到上千帧每秒。
FOV(视场角)可分为对角线视场角(FOV-D)、水平视场角(FOV-H)、以及垂直视场角(FOV-V)
1)根据内参和sensor尺寸计算FOV
水平FOV = 2 atan(0.5 width(sensor width) / focal(mm))
垂直FOV = 2 atan(0.5 height(sensor heght) / focal(mm))
(2)根据内参和图像宽度计算FOV
水平FOV = 2 atan(0.5 * image_width / focal(pixel))
垂直FOV = 2 atan(0.5 * image_heght) / focal(pixel))
5、相机内参和畸变校正
相机的内参是指像素坐标系和相机坐标系之间的映射关系矩阵。
图像的畸变是由相机模组硬件组成部分的透镜和安装误差导致的。分为径向畸变和切向畸变。
内参核畸变校准的泰山之作是张氏标定法。(Flexible Camera Calibration By Viewing a Plane From Unknown Orientations )值得提出的是 张氏标定法只考虑的径向畸变 没有考虑切向畸变 后续开发者把切向畸变一起考虑在内了。
如上述公式:
内参矩阵为:
畸变系数为 :
k1 k2 k3 p1 p2(五元素)
代码实现(opencv 和apollo方式做了对比,效果和时间在本机测试结果和耗时基本一致,两者的区别数图像像素数据流形态不同):
OpenCV 不同畸变校正函数的使用说明
OpenCV 针对不同的使用场景提供了几个不同用法的畸变校正函数。
主要有以下几种:
- initUndistortRectifyMap() remap()组合,initUndistortRectifyMap()用于产生映射表,remap()用于执行映射。
- undistort()(initUndistortRectifyMap() remap()组合,写在了一个函数里。方便只校正一次,默认参数采用的是cv::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Mat(), cameraMatrix, cv::Size(width, height), CV_32FC1, map1, map2);)
- undistortPoints()
参数解析(下图展示了不同参数得到的校准图片的差异,roi获取的范围不同)
cv::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Mat(), cameraMatrix, cv::Size(width, height), CV_32FC1, map1, map2);
cv::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Mat(),cv::Mat(), cv::Size(width, height), CV_32FC1, map1, map2);
时间测试:
Opencv remap 3ms 到4ms
apollo Gpu cuda方式 3ms到4ms
适用场景:
当要进行多次畸变校正时,使用initUndistortRectifyMap() remap()组合比较有效率,只需要执行一次initUndistortRectifyMap(),后面畸变校正只需要执行remap()即可
6、camera to camera 外参标定
使用相机进行二维图像上的object detection,激光雷达可以辅助测距,从而帮助我们判断物体相对于相机坐标系的位置。实现联合标定的框架主要有Autoware、Apollo、lidar_camera_calibration、but_velodyne,具体请参考:激光雷达和相机的联合标定。虽然这些框架已经帮我们实现了,但是框架对使用的雷达和相机的品牌有要求。
像素坐标和相机坐标之间的转换关系(关于什么是像素坐标系 图像坐标系 相机坐标系 师姐坐标系网上有很多很全的相关资料 这里不做过多介绍):
内参为:相机坐标系(小孔成像原理)转换到图像坐标系(比例尺和原点变换)转换到像素坐标系
外参为:世界坐标系到相机坐标系的转关(旋转加平移)
值得提出的是相机内参(3*3) 外参(4*4)是可逆矩阵。
外参矩阵:
其中R是激光相对于相机0的旋转矩阵,t是偏移。
R 可以分为 x 方向、y 方向和 z 方向的旋转矩阵的乘积:
激光坐标系到相机坐标系可先沿x轴旋转-90度,然后沿 y 轴旋转 90 度。
设激光到相机距离为 z0,则偏移t=[0,0,-z0]
一个三维中的坐标点,的确可以在图像中找到一个对应的像素点,但是反过来,通过图像中的一个点找到它在三维中对应的点就很成,一个问题,因为我们并不知道等式左边的Zc值。
代码实现:
apollo的实现方式是:
像素到像素的映射矩阵:
*project_matrix =intrinsic_map.at(base_camera_name).cast<double>() *extrinsic_map.at(base_camera_name).block<3, 3>(0, 0).inverse() *extrinsic_map.at(camera_name).block<3, 3>(0, 0) *intrinsic_map.at(camera_name).cast<double>().inverse();
像素点到限速点的映射函数:
void ProjectBox(const base::BBox2DF &box_origin,
const Eigen::Matrix3d &transform,
base::BBox2DF *box_projected) {
Eigen::Vector3d point;
// top left
point << box_origin.xmin, box_origin.ymin, 1;
point = transform * point;
box_projected->xmin = static_cast<float>(point[0] / point[2]);
box_projected->ymin = static_cast<float>(point[1] / point[2]);
// bottom right
point << box_origin.xmax, box_origin.ymax, 1;
point = transform * point;
box_projected->xmax = static_cast<float>(point[0] / point[2]);
box_projected->ymax = static_cast<float>(point[1] / point[2]);
}
apollo效果:
我尝试后apollo的外参获得方式在自己的摄像头数据上试验不成功,猜测原因是apollo假设两个相机的光学中心是一个,两个相机挨得很近,而我试验的数据相机移动了很大的距离以及没有百度标定间那种获得准备的棋盘格世界坐标 模拟的单元坐标。
我使用的方式是利用opencv的findHomography求单应矩阵即图像到图像的映射 :
# camera2camera_extrinsic.py
import numpy as np
np.set_printoptions(suppress=True)
import cv2 as cv
MIN_MATCH_COUNT = 10
#基于棋盘格的校准方法
def _get_Homography_matrix(img_base, img_curr):
gray_base = cv.cvtColor(img_base, cv.COLOR_BGR2GRAY)
gray_curr = cv.cvtColor(img_curr, cv.COLOR_BGR2GRAY)
ret, corners1 = cv.findChessboardCorners(gray_base, (8, 6), None)
ret, corners2 = cv.findChessboardCorners(gray_curr, (8, 6), None)
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners11 = cv.cornerSubPix(gray_base, corners1, (11, 11), (-1, -1), criteria)
corners22 = cv.cornerSubPix(gray_curr, corners2, (11, 11), (-1, -1), criteria)
src_pts = np.float32(corners11).reshape(-1, 1, 2)
dst_pts = np.float32(corners22).reshape(-1, 1, 2)
H2, mask = cv.findHomography(dst_pts, src_pts, cv.RANSAC, 5.0)
return H2
#基于sift匹配的方法
def SIFT(img1, img2):
# Initiate SIFT detector
sift = cv.xfeatures2d.SIFT_create()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img2,None)
kp2, des2 = sift.detectAndCompute(img1,None)
# BFMatcher with default params
bf = cv.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Apply ratio test
good = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good.append(m)
# cv2.drawMatchesKnn expects list of lists as matches.
# good_2 = np.expand_dims(good, 1)
# matching = cv.drawMatchesKnn(img1,kp1,img2,kp2,good_2[:20],None, flags=2)
if len(good)>MIN_MATCH_COUNT:
# 获取关键点的坐标
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1,1,2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1,1,2)
H2, mask = cv.findHomography(dst_pts, src_pts, cv.RANSAC, 5.0)
return H2
else:
return []
def _match(img1, img2):
wrap = cv.warpPerspective(img2, H, (img1.shape[1] + img2.shape[1], img1.shape[0] + img2.shape[0]))
zeros = np.zeros((wrap.shape), dtype=np.uint8)
zeros[0:img1.shape[0], 0:img1.shape[1]] = img1
# alpha 为第一张图片的透明度
alpha = 1
# beta 为第二张图片的透明度
beta = 0.5
gamma = 0
# cv2.addWeighted 将原始图片与 mask 融合
mask_img = cv.addWeighted(zeros, alpha, wrap, beta, gamma)
rows, cols = np.where(mask_img[:, :, 0] != 0)
min_row, max_row = min(rows), max(rows) + 1
min_col, max_col = min(cols), max(cols) + 1
result = mask_img[min_row:max_row, min_col:max_col, :] # 去除黑色无用部分
return result
def add_mask(img):
zeros = np.zeros((img.shape), dtype=np.uint8)
zeros_mask = cv.rectangle(zeros, (0, 0), (img.shape[1], img.shape[0]),
color=(255, 0, 0), thickness=-1)
# alpha 为第一张图片的透明度
alpha = 1
# beta 为第二张图片的透明度
beta = 0.5
gamma = 0
# cv2.addWeighted 将原始图片与 mask 融合
mask_img = cv.addWeighted(img, alpha, zeros_mask, beta, gamma)
# cv.imshow('result1.jpg', mask_img)
# cv.waitKey(0)
return mask_img
if __name__ == "__main__":
img_base = cv.imread("17.jpg")
img_curr = cv.imread("24.jpg")
H = _get_Homography_matrix(img_base, img_curr)
mask_img_curr = add_mask(img_curr)
match_pic = _match(img_base, mask_img_curr)
cv.namedWindow('result.jpg', 0)
cv.resizeWindow('result.jpg', 960, 540)
cv.imshow('result.jpg', match_pic)
cv.waitKey(0)
cv.destroyAllWindows()
原图:
结果:
单应矩阵
[[ 0.84263856 -0.50900755 -302.96243102]
[ 0.55155739 0.92349768 -573.22506249]
[ -0.00006829 0.00004366 1. ]]
7、目标检测追踪(待续)
目标检测目前的深度学习方案相对成熟。目标追踪主要分为KCF等传统追踪方法,和deep sort等基于检测的匈牙利算法和卡尔曼滤波的关系滤波匹配方法。
apollo障碍物检测的类别:
if (type_name == "car") { return base::ObjectSubType::CAR; } else if (type_name == "van") { return base::ObjectSubType::VAN; } else if (type_name == "bus") { return base::ObjectSubType::BUS; } else if (type_name == "truck") { return base::ObjectSubType::TRUCK; } else if (type_name == "cyclist") { return base::ObjectSubType::CYCLIST; } else if (type_name == "motorcyclist") { return base::ObjectSubType::MOTORCYCLIST; } else if (type_name == "tricyclelist") { return base::ObjectSubType::TRICYCLIST; } else if (type_name == "pedestrian") { return base::ObjectSubType::PEDESTRIAN; } else if (type_name == "trafficcone") { return base::ObjectSubType::TRAFFICCONE; } else { // type_name is "" or unknown return base::ObjectSubType::UNKNOWN;
8、目标级融合 (待续)
融合方法分为多传感器的前融合(内参外参标定映射)和目标级的后融合方法(匈牙利算法和卡尔曼滤波)。
9、单目测距
单目测距方法:
1)基于目标物大小(需要精确确定目标物的长度或者宽度,一般受角度方位 检测精度的影响);基于FOV角度等小孔成像原理 (birdview鸟瞰图原理 要求被测地面水平)。
2)深度学习单目估计方法(基于单幅图像的3D目标检测方法)。
测距一般误差在10%左右,效果好的综合解决方案一般在6%左右。
10、参考链接
- https://blog.csdn.net/qq_34009929/article/details/109360655?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-7.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-7.control
- https://docs.opencv.org/3.0-beta/modules/imgproc/doc/geometric_transformations.html?highlight=undistort#cv2.undistort
- apllo联合标定 - 程序员宅基地 (cxyzjd.com)
- 激光雷达和相机的联合标定的实现(源代码,干货满满)_Tom Hardy的博客-程序员宅基地_激光雷达和相机联合标定 - 程序员宅基地 (cxyzjd.com)
- https://blog.csdn.net/qq_43552842/article/details/83795457
- https://docs.nvidia.com/cuda/npp/group__image__remap.html#details
- https://docs.huihoo.com/nvidia/cuda/7.5/pdf/NPP_Library.pdf
- https://docs.opencv.org/3.0-beta/modules/imgproc/doc/geometric_transformations.html?highlight=undistort#cv2.undistort