-
2020-12-17 19:52:58
点击上方“3D视觉工坊”,选择“星标”
干货第一时间送达
结构光三维重建系统是由一个相机和一个投影仪组成,关于结构光三维重建系统的理论有很多,其中有一个简单的模型是把投影仪看做相机来使用,从而得到物体的三维信息。接下来我将详细介绍这个模型的原理。
在把投影仪当相机使用之前,我们得知道如何通过两个相机的信息得到物体的三维信息。
如图所示是一个双目相机系统,如果只有一个相机(以左相机为例),相机上的一个像素点可以对应三维空间中光心与相机成像点连线OLP上的无穷个点,所以仅凭一个相机的信息是无法得到空间中物体的三维信息。但是如果能有另一个相机(右相机),则物体的三维信息可以唯一确定。因为离左相机不同距离的物体,在右相机上的成像位置是不同的,例如,图中线OLP上的点,在右相机的成像位置为图中红线,如果能知道物体在左相机成像位置PL以及物体在右相机的成像位置PR的话,就可以唯一确定物体三维位置P。
熟悉双目成像系统的小伙伴应该知道,要真正求解出物体的三维坐标值,我们需要知道左右相机的内参矩阵KL和KR,以及左右相机之间的外参R,T。其中R是右相机坐标系和左相机坐标系之间的旋转矩阵,T是右相机坐标系和左相机坐标系之间的平移矩阵。
下面我将推导已知三维空间中的物体在左右相机上的成像位置分别为PL(uL,vL)和PR(uR,vR)的情况下,如何解出物体的三维信息P(X,Y,Z)。
由于世界坐标系的原点可以是空间中的任意一点,为了使计算简单,不妨让其与左相机坐标系的原点重合。则左相机的外参矩阵(左相机和世界坐标系的转换矩阵)
,
右相机的外参矩阵(右相机和世界坐标系的转换矩阵),
根据相机模型我们可以得到如下方程等式:
联立上述方程组,方程组中有5个未知数,sL,sR,X,Y,Z,和六个方程。完全足够解出5个未知数。对于双目系统来说,最大的问题是怎么找到左相机成像点(uL,vL)对应的右相机成像点(uR,vR),这个方法有很多,首先通常需要依据极线约束对左右相机图像做立体矫正,把对应点在图像中二维的搜索放到一维(同一行)上,然后在根据各种代价计算找到准确匹配点,具体方法这里就不详述了。
知道如何通过左右相机得到物体的三维信息后,我们来看怎么把投影仪看做相机,或者说看做相机的逆。相机捕捉的是物体反射的光信息,而投影仪是把光信息投射到物体上。如果我们对投影仪投射的每个像素点的光进行编码,把编码后的光投射到物体表面上,并通过对相机捕捉到的物体表面光信息进行解码,就可以知道打在物体表面的光是由投影仪那个像素发出的,也就可以知道物体表面会在虚拟的投影仪图像上的成像位置。
投影光编码的方式很多,通常的编码是分别对投影图片的行和列编码,以格雷码为例,常见的格雷码编码如下图所示。
列格雷码示例
通过对相机图片上拍摄的物体表面投射的列格雷码光编码及行格雷码光编码进行解码,就可以知道物体在相机图片上成像位置(uc,vc)及其对应的投影仪虚拟的成像位置(up,vp),则上述双目方程可以改为:
其中Kc和Kp分别代表的是相机和投影的内参,和双目系统一样,我们不妨假设世界坐标系的原点与相机坐标系重合,则
,Rp=R,Tp=T,其中R,T分别为投影仪坐标系对相机坐标系的旋转和平移矩阵。
在上述方程中,一共有五个未知数sc,sp,X,Y,Z,和六个方程。实际上,六个方程可以解六个未知数,即使我们只需要知道知道up和vp中的一个,这也是为什么在结构光三维重建时我们通常只需要投一个方向的条纹(横条纹或者竖条纹)的原因。
通过上述方程,我们可以解出物体的三维信息,以投影列格雷码(获得up)为例,物体的三维信息的解法推导如下:
则结构光三维成像系统物体的三维解为:
自此,我们就把将投影仪看成的模型推导完毕,想要真正的完成物体的三维重建,我们还需要知道如何获取相机和投影仪的标定参数,以及如何对投影光进行编码和解码,实际上除了格雷码编码外,还有很多常见光编码方式,如相移法,格雷码+相移法,多频外差法等,每个编码方法都有他们的优劣。今后我将对一一介绍以上内容。
本文的参考文献是High-accuracy, high-speed 3D structured light imaging techniques and potential applications to intelligent robotics,这篇文章很详细的介绍了如何通过结构光系统获得物体的三维信息。Ps:这篇文献中给出的三维解有印刷错误,笔者在这篇文章中给出了正确解,感兴趣的小伙伴也可以自己推导。
备注:本文来自我们技术星球里的一位小伙伴的知识分享,也欢迎更多小伙伴们的来稿,一起学习,相互成就~
上 述内容,如有侵犯版权,请联系作者,会自行删文。重磅!3DCVer-学术交流群已成立
欢迎加入我们公众号读者群一起和同行交流,目前有3D视觉、CV&深度学习、SLAM、三维重建、点云后处理、自动驾驶、CV入门、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别等微信群,请扫描下面微信号加群,备注:”研究方向+学校/公司+昵称“,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进去相关微信群。原创投稿也请联系。
▲长按加群或投稿
学习3D视觉核心技术,扫描查看介绍,3天内无条件退款圈里有高质量教程资料、可答疑解惑、助你高效解决问题
更多相关内容 -
双目三维重建系统(双目标定 立体校正 双目测距 三维重建)Python.zip
2021-11-15 19:56:58双目三维重建系统(双目标定+立体校正+双目测距+三维重建) 博客地址:https://panjinquan.blog.csdn.net/article/details/121301896 -
OpenCV实现SfM:双目三维重建
2015-09-02 14:29:19使用OpenCV3.0进行双目三维重建。 代码是用VS2013写的,OpenCV版本为3.0且包含扩展部分,如果不使用SIFT特征,可以修改源代码,然后使用官方未包含扩展部分的库。软件运行后会将三维重建的结果写入Viewer目录下的... -
基于双目视觉的三维重建(matlab 语言实现)(推荐)_基于双目的三维模型重建_三维重建matlab_双目重建_
2021-10-02 11:28:37基于双目的三维模型重建,模型比较简单,适合刚入门需要的 -
双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
2021-11-13 11:23:19本博客将实现Python版本的双目三维重建系统,项目代码实现包含:`双目标定`,`立体校正(含消除畸变)`,`立体匹配`,`视差计算`和`深度距离计算/3D坐标计算` 的知识点。限于篇幅,本博客不会过多赘述算法原理,而是...双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
目录
双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
本篇博客将实现Python版本的双目三维重建系统,项目代码实现包含:`双目标定`,`立体校正(含消除畸变)`,`立体匹配`,`视差计算`和`深度距离计算/3D坐标计算` 的知识点。限于篇幅,本博客不会过多赘述算法原理,而是手把手教你,如何搭建一套属于自己的双目三维重建的系统。项目代码包含:
- 支持双USB连接线的双目摄像头
- 支持单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示)
- 支持单目相机标定:mono_camera_calibration.py ,无需Matlab标定
- 支持双目相机标定:stereo_camera_calibration.py,无需Matlab标定
- 支持使用WLS滤波器对视差图进行滤波
- 支持双目测距,误差在1cm内(鼠标点击图像即可获得其深度距离)
- 支持Open3D和PCL点云显示
诚然,网上有很多双测距的代码,但项目都不是十分完整,而且恢复视差图效果也一般,难以达到商业实际应用,究其原因,主要有下面2个:
- 双目摄像头质量问题,
- 双目标定存在问题,导致校准误差较大
- 没有使用WLS滤波器对视差图进行滤波,该方法可以极大提高视差图的效果
【尊重原创,转载请注明出处】:https://panjinquan.blog.csdn.net/article/details/121301896
【完整的项目代码】双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
先放一张动图,这是最终重建的效果:
三维重建中,除了双目相机,还有TOF和结构光3D 相机
-
飞行时间(Time of flight,TOF),代表公司微软Kinect2,PMD,SoftKinect, 联想 Phab,在手机中一般用于3D建模、AR应用,AR测距(华为TOF镜头)
-
双目视觉(Stereo Camera),代表公司 Leap Motion, ZED, 大疆;
-
结构光(Structured-light),代表公司有奥比中光,苹果iPhone X(Prime Sense),微软 Kinect1,英特尔RealSense, Mantis Vision 等,在手机(iPhone,华为)中3D结构光主要用于人脸解锁、支付、美颜等场景。
关于3D相机技术(飞行时间+双目+结构光)的区别,可以参考我的一篇博客《3D相机技术调研(飞行时间TOF+双目+结构光)》:
3D相机技术调研(飞行时间TOF+双目+结构光)_pan_jinquan的博客-CSDN博客1.深度估计3D相机方案目前市面上常有的 3D 相机方案就就是这3种:飞行时间(Time of flight,TOF),代表公司微软Kinect2,PMD,SoftKinect, 联想 Phab,在手机中一般用于3D建模、AR应用,AR测距(华为TOF镜头)双目视觉(Stereo Camera),代表公司 Leap Motion, ZED, 大疆;结构光(Structured-light),代表公司有奥比中光,苹果iPhone X(Prime Sense),微软Kin..
https://panjinquan.blog.csdn.net/article/details/119649838如果你对结构光三维重建感兴趣,可参考鄙人的博客《结构光三维重建-3D Scanning Software实现三维重建》
1.项目结构
. ├── config # 相机参数文件 ├── core # 相机核心算法包 ├── data # 相机采集的数据 ├── demo # demo文件 ├── libs # 第三方依赖包 ├── scripts # 脚本 │ ├── mono_camera_calibration.sh # 单目相机校准脚本 │ └── stereo_camera_calibration.sh # 双目相机校准脚本 ├── get_stereo_images.py # 采集标定文件 ├── mono_camera_calibration.py # 单目相机标定 ├── stereo_camera_calibration.py # 双目相机标定 └── README.md
2. Environment
- 依赖包,可参考requirements.txt
- python-pcl (安装python-pcl需要一丢丢耐心,实在不行,就用open3d吧)
- open3d-python=0.7.0.0
- opencv-python
- opencv-contrib-python
3.双目相机标定和校准
(0) 双目摄像头
下面这款双目摄像头(RGB+RGB)是在某宝购买的(几百元,链接就不发了,免得说我打广告),作为本项目的双目相机,其基线是固定的6cm,是单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示),基本满足我们测试需求。一般基线越长,可测量的距离越远,网友也可以根据自己需要购买。
一点注意事项:
- 双目相机三维重建也可以使用RGB+IR(红外)的摄像头,甚至IR+IR的也是可以,本人亲测,RGB+IR的相机,其效果也是杠杠的。
- 基线不太建议太小,作为测试,一般baseline在3~9cm就可以满足需求,有些无人车的双目基线更是恐怖到1~2米长
- 从双目三维重建原理中可知,左右摄像头的成像平面尽可能在一个平面内,成像平面不在同一个平面的,尽管可以立体矫正,其效果也差很多。
- 一分钱,一分货,相机的质量好坏,直接决定了你的成像效果
(1) 采集标定板的左右视图
- 采集数据前,请调节相机焦距,尽可能保证视图中标定板清洗可见
- 采集棋盘格图像时,标定板一般占视图1/2到1/3左右
- 一般采集15~30张左右
width=8 height=11 left_video=0 right_video=-1 save_dir="data/camera" detect=True python get_stereo_images.py \ --left_video $left_video \ --right_video $right_video \ --width $width \ --height $height \ --save_dir $save_dir \ --detect $detect \
参数说明:
- 参数width指的是棋盘格宽方向黑白格子相交点个数
- 参数height指的是棋盘格长方向黑白格子相交点个数
- 参数left_video是左路相机ID,一般就是相机连接主板的USB接口号
- 参数right_video是右路相机ID,一般就是相机连接主板的USB接口号
- PS:如果你的双目相机是单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示),则设置left_video=相机ID,而right_video=-1,
- 参数
detect
建议设置True
,这样可实时检测棋盘格,方面调整角度 - 按键盘
s
或者c
保存左右视图图片
left_image right_image 下面是采集双目摄像头标定板左右视图的Python代码:get_stereo_images.py,除了OpenCV,没啥依赖,直接干就完事。
import os import argparse import cv2 class StereoCamera(object): """采集双目标定图片,按键盘【c】或【s】保存图片""" def __init__(self, chess_width, chess_height, detect=False): """ :param chess_width: chessboard width size,即棋盘格宽方向黑白格子相交点个数, :param chess_height: chessboard height size,即棋盘格长方向黑白格子相交点个数 :param detect: 是否实时检测棋盘格,方便采集数据 """ self.chess_width = chess_width self.chess_height = chess_height self.detect = detect self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) def detect_chessboard(self, image): """检测棋盘格""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (self.chess_width, self.chess_height), None) if ret: # 角点精检测 corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), self.criteria) # Draw and display the corners image = cv2.drawChessboardCorners(image, (self.chess_width, self.chess_height), corners2, ret) return image def capture2(self, left_video, right_video, save_dir): """ 用于采集双USB连接线的双目摄像头 :param left_video:int or str,左路视频路径或者摄像头ID :param right_video:int or str,右视频路径或者摄像头ID :param save_dir: str,保存左右图片的路径 :return: """ self.create_file(save_dir) capL = cv2.VideoCapture(left_video) capR = cv2.VideoCapture(right_video) widthL, heightL, numFramesL, fpsL = self.get_video_info(capL) widthR, heightR, numFramesR, fpsR = self.get_video_info(capR) print("capL:\n", widthL, heightL, numFramesL, fpsL) print("capR:\n", widthR, heightR, numFramesR, fpsR) save_videoL = self.create_file(save_dir, "video", "left_video.avi") save_videoR = self.create_file(save_dir, "video", "right_video.avi") writerL = self.get_video_writer(save_videoL, widthL, heightL, fpsL) writerR = self.get_video_writer(save_videoR, widthR, heightR, fpsR) i = 0 while True: isuccessL, frameL = capL.read() isuccessR, frameR = capR.read() if not (isuccessL and isuccessR): print("No more frames") break if self.detect: l = self.detect_chessboard(frameL.copy()) r = self.detect_chessboard(frameR.copy()) else: l = frameL.copy() r = frameR.copy() cv2.imshow('left', l) cv2.imshow('right', r) key = cv2.waitKey(10) if key == ord('q'): break elif key == ord('c') or key == ord('s'): print("save image:{:0=3d}".format(i)) cv2.imwrite(os.path.join(save_dir, "left_{:0=3d}.png".format(i)), frameL) cv2.imwrite(os.path.join(save_dir, "right_{:0=3d}.png".format(i)), frameR) i += 1 writerL.write(frameL) writerR.write(frameR) capL.release() capR.release() cv2.destroyAllWindows() def capture1(self, video, save_dir): """ 用于采集单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) :param video:int or str,视频路径或者摄像头ID :param save_dir: str,保存左右图片的路径 """ self.create_file(save_dir) cap = cv2.VideoCapture(video) width, height, numFrames, fps = self.get_video_info(cap) print("capL:\n", width, height, numFrames, fps) save_videoL = self.create_file(save_dir, "video", "left_video.avi") save_videoR = self.create_file(save_dir, "video", "right_video.avi") writerL = self.get_video_writer(save_videoL, int(width / 2), height, fps) writerR = self.get_video_writer(save_videoR, int(width / 2), height, fps) i = 0 while True: isuccess, frame = cap.read() if not isuccess: print("No more frames") break # 分离左右摄像头 frameL = frame[:, :int(width / 2), :] frameR = frame[:, int(width / 2):, :] if self.detect: l = self.detect_chessboard(frameL.copy()) r = self.detect_chessboard(frameR.copy()) else: l = frameL.copy() r = frameR.copy() cv2.imshow('left', l) cv2.imshow('right', r) key = cv2.waitKey(10) if key == ord('q'): break elif key == ord('c') or key == ord('s'): print("save image:{:0=3d}".format(i)) cv2.imwrite(os.path.join(save_dir, "left_{:0=3d}.png".format(i)), frameL) cv2.imwrite(os.path.join(save_dir, "right_{:0=3d}.png".format(i)), frameR) i += 1 writerL.write(frameL) writerR.write(frameR) cap.release() cv2.destroyAllWindows() @staticmethod def get_video_info(video_cap): width = int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) numFrames = int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = int(video_cap.get(cv2.CAP_PROP_FPS)) return width, height, numFrames, fps @staticmethod def get_video_writer(save_path, width, height, fps): if not os.path.exists(os.path.dirname(save_path)): os.makedirs(os.path.dirname(save_path)) fourcc = cv2.VideoWriter_fourcc(*'XVID') frameSize = (int(width), int(height)) video_writer = cv2.VideoWriter(save_path, fourcc, fps, frameSize) print("video:width:{},height:{},fps:{}".format(width, height, fps)) return video_writer @staticmethod def create_file(parent_dir, dir1=None, filename=None): out_path = parent_dir if dir1: out_path = os.path.join(parent_dir, dir1) if not os.path.exists(out_path): os.makedirs(out_path) if filename: out_path = os.path.join(out_path, filename) return out_path def str2bool(v): return v.lower() in ('yes', 'true', 't', 'y', '1') def get_parser(): width = 8 height = 11 left_video = -1 right_video = 0 save_dir = "data/camera" parser = argparse.ArgumentParser(description='Camera calibration') parser.add_argument('--width', type=int, default=width, help='chessboard width size') parser.add_argument('--height', type=int, default=height, help='chessboard height size') parser.add_argument('--left_video', type=int, default=left_video, help='left video file or camera ID') parser.add_argument('--right_video', type=int, default=right_video, help='right video file or camera ID') parser.add_argument('--detect', type=str2bool, nargs='?', const=True, help='detect chessboard ') parser.add_argument('--save_dir', type=str, default=save_dir, help='YML file to save calibrate matrices') return parser if __name__ == '__main__': args = get_parser().parse_args() stereo = StereoCamera(args.width, args.height, detect=args.detect) if args.left_video > -1 and args.right_video > -1: # 双USB连接线的双目摄像头 stereo.capture2(left_video=args.left_video, right_video=args.right_video, save_dir=args.save_dir) elif args.left_video > -1: # 单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) stereo.capture1(video=args.left_video, save_dir=args.save_dir) elif args.right_video > -1: # 单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) stereo.capture1(video=args.right_video, save_dir=args.save_dir) else: raise Exception("Error: Check your camera{}".format(args.left_video, args.right_video))
双目标定的目标是获得左右两个相机的内参、外参和畸变系数,其中内参包括左右相机的fx,fy,cx,cy,外参包括左相机相对于右相机的旋转矩阵和平移向量,畸变系数包括径向畸变系数(k1, k2,k3)和切向畸变系数(p1,p2)。
双目标定工具最常用的莫过于是MATLAB的工具箱: Stereo Camera Calibrator App,网上有太多教程,我就不赘述了。
我采用的是Deepin系统,懒得去安装Matlab了,所以就参考各路神仙,使用OpenCV实现了单目和双目的标定程序。
(2) 单目相机标定和校准
bash scripts/mono_camera_calibration.sh
#!/usr/bin/env bash image_dir=data/lenacv-camera # 棋盘格图片 save_dir=configs/lenacv-camera # 保存标定结果 width=8 height=11 square_size=20 #mm image_format=png # 图片格式,如png,jpg show=True # 是否显示检测结果 # left camera calibration python mono_camera_calibration.py \ --image_dir $image_dir \ --image_format $image_format \ --square_size $square_size \ --width $width \ --height $height \ --prefix left \ --save_dir $save_dir \ --show $show # right camera calibration python mono_camera_calibration.py \ --image_dir $image_dir \ --image_format $image_format \ --square_size $square_size \ --width $width \ --height $height \ --prefix right \ --save_dir $save_dir \ --show $show
一点注意事项:
- 标定代码会显示每一张图像的棋盘格的角点检测效果,如果发现有检测不到,或者角点检测出错,则需要自己手动删除这些图像,避免引入太大的误差
- 若误差超过0.1,建议重新调整摄像头并标定,不然效果会差很多
执行后,在
$save_dir
目录下会生成left_cam.yml
和right_cam.yml
左右相机参数文件%YAML:1.0 --- K: !!opencv-matrix rows: 3 cols: 3 dt: d data: [ 7.6327773983725410e+02, 0., 2.8768149780495781e+02, 0., 7.6350419482076416e+02, 2.1897333674659842e+02, 0., 0., 1. ] D: !!opencv-matrix rows: 1 cols: 5 dt: d data: [ 3.5020967324140520e-02, -4.0770563420764315e-02, -4.4231047037511739e-04, -1.0552565305999332e-03, -9.7750323762439514e-02 ]
其中K是相机内参矩阵,D是畸变系数矩阵
(3) 双目相机标定和校准
bash scripts/stereo_camera_calibration.sh
image_dir=data/lenacv-camera # 棋盘格图片 save_dir=configs/lenacv-camera # 保存标定结果 width=8 height=11 square_size=20 #mm image_format=png # 图片格式,如png,jpg #show=True # 是否显示检测结果 show=False # 是否显示检测结果 # stereo camera calibration python stereo_camera_calibration.py \ --left_file $save_dir/left_cam.yml \ --right_file $save_dir/right_cam.yml \ --left_prefix left \ --right_prefix right \ --width $width \ --height $height \ --left_dir $image_dir \ --right_dir $image_dir \ --image_format $image_format \ --square_size $square_size \ --save_dir $save_dir \
一点注意事项:
- 若误差超过0.1,建议重新调整摄像头并标定
执行后,在
$save_dir
目录下会生成stereo_cam.yml
相机参数文件,这个文件,包含了左右相机的相机内参矩阵(K1,K2),畸变系数矩阵(D1,D2),左右摄像机之间的旋转矩阵R和平移矩阵T,以及本质矩阵E和基本矩阵F等.有了双目相机内外参数信息(
stereo_cam.yml
),下面就可以进行立体矫正,计算视差了4.视差图和深度图
(1) 立体校正
这部分基础知识来源于:https://blog.csdn.net/dulingwen/article/details/100115157
立体校正的目的是将拍摄于同一场景的左右两个视图进行数学上的投影变换,使得两个成像平面平行于基线,且同一个点在左右两幅图中位于同一行,简称共面行对准。只有达到共面行对准以后才可以应用三角原理计算距离。
def get_rectify_image(self, imgL, imgR): """ 畸变校正和立体校正 根据更正map对图片进行重构 获取用于畸变校正和立体校正的映射矩阵以及用于计算像素空间坐标的重投影矩阵 :param imgL: :param imgR: :return: """ # camera_params.get_rectify_transform(K1, D1, K2, D2, R, T, image_size) left_map_x, left_map_y = self.camera_config["left_map_x"], self.camera_config["left_map_y"] right_map_x, right_map_y = self.camera_config["right_map_x"], self.camera_config["right_map_y"] rectifiedL = cv2.remap(imgL, left_map_x, left_map_y, cv2.INTER_LINEAR, borderValue=cv2.BORDER_CONSTANT) rectifiedR = cv2.remap(imgR, right_map_x, right_map_y, cv2.INTER_LINEAR, borderValue=cv2.BORDER_CONSTANT) return rectifiedL, rectifiedR
(2) 立体匹配与视差图计算
立体匹配的目的是为左图中的每一个像素点在右图中找到其对应点(世界中相同的物理点),这样就可以计算出视差:
(xi和xj分别表示两个对应点在图像中的列坐标)。
大部分立体匹配算法的计算过程可以分成以下几个阶段:匹配代价计算、代价聚合、视差优化、视差细化。立体匹配是立体视觉中一个很难的部分,主要困难在于:
1.图像中可能存在重复纹理和弱纹理,这些区域很难匹配正确;
2.由于左右相机的拍摄位置不同,图像中几乎必然存在遮挡区域,在遮挡区域,左图中有一些像素点在右图中并没有对应的点,反之亦然;
3.左右相机所接收的光照情况不同;
4.过度曝光区域难以匹配;
5.倾斜表面、弯曲表面、非朗伯体表面;
6.较高的图像噪声等。
常用的立体匹配方法基本上可以分为两类:局部方法,例如BM、SGM、ELAS、Patch Match等,非局部的,即全局方法,例如Dynamic Programming、Graph Cut、Belief Propagation等,局部方法计算量小,匹配质量相对较低,全局方法省略了代价聚合而采用了优化能量函数的方法,匹配质量较高,但是计算量也比较大。
目前OpenCV中已经实现的方法有BM、binaryBM、SGBM、binarySGBM、BM(cuda)、Bellief Propogation(cuda)、Constant Space Bellief Propogation(cuda)这几种方法。比较好用的是SGBM算法,它的核心是基于SGM算法,但和SGM算法又有一些不同,比如匹配代价部分用的是BT代价(原图+梯度图)而不是HMI代价等等。有关SGM算法的原理解释,可以参考另一篇博客 : 《双目立体匹配算法:SGM 》https://blog.csdn.net/dulingwen/article/details/104142149
def get_disparity(self, imgL, imgR, use_wls=True): """ :param imgL: 畸变校正和立体校正后的左视图 :param imgR:畸变校正和立体校正后的右视图 :param use_wls:是否使用WLS滤波器对视差图进行滤波 :return dispL:ndarray(np.float32),返回视差图 """ dispL = disparity.get_disparity_filter(imgL, imgR, use_wls=use_wls) # dispL = disparity.get_disparity_simple(imgL, imgR) return dispL
(3) Demo
- 运行Demo进行立体矫正,计算视差图并恢复深度图,
- Demo的参数说明如下
参数 类型 说明 calibration_file str 双目相机的配置文件,如"configs/lenacv-camera/stereo_cam.yml" left_video str 左路相机ID或者视频文件 right_video str 右路相机ID或者视频文件 left_file str 左路测试图像文件 right_file str 右路测试图像文件 filter bool 是否对视差图进行滤波 python demo.py \ --stereo_file "configs/lenacv-camera/stereo_cam.yml" \ --left_video "data/lenacv-video/left_video.avi" \ --right_video "data/lenacv-video/right_video.avi" \ --filter True
- 视差图滤波:
在立体匹配生成视差图之后,还可以对视差图进行滤波后处理,例如Guided Filter、Fast Global Smooth Filter(一种快速WLS滤波方法)、Bilatera Filter、TDSR、RBS等。 视差图滤波能够将稀疏视差转变为稠密视差,并在一定程度上降低视差图噪声,改善视差图的视觉效果,但是比较依赖初始视差图的质量。
左视图 右视图 视差图(未滤波) 深度图(未滤波) 视差图(滤波后) 深度图(滤波后) 可以看到,使用WLS滤波后,视差图的整体效果都有明显改善
- 最终效果图
5.双目测距
得到了视差图之后,就可以计算像素深度了,在opencv中使用StereoRectify()函数可以得到一个重投影矩阵Q,它是一个4*4的视差图到深度图的映射矩阵(disparity-to-depth mapping matrix ),使用Q矩阵和cv2.reprojectImageTo3D即可实现将像素坐标转换为三维坐标,该函数会返回一个3通道的矩阵,分别存储X、Y、Z坐标(左摄像机坐标系下)。
def reprojectImageTo3D(disparity, Q, _3dImage=None, handleMissingValues=None, ddepth=None): """ :param disparity: 输入视差图 :param Q: 输入4*4的视差图到深度图的映射矩阵,即重投影矩阵 通过stereoRectify得到 (disparity-to-depth mapping matrix) :param _3dImage: 映射后存储三维坐标的图像 contains 3D coordinates of the point (x,y) computed from the disparity map :param handleMissingValues: 计算得到的非正常值是否给值,如果为true则给值10000 :param ddepth: 输出类型 -1 即默认为CV_32FC3 还可以是 CV_16S, CV_32S, CV_32F :return: """
运算如下:
重投影矩阵Q中
和
为左相机主点在图像中的坐标,f为焦距,
为两台相机投影中心间的平移(负值),即基线baseline,相当于平移向量T[0],
是右相机主点在图像中的坐标。
其中Z即是深度距离depth:
其中 f 为焦距长度(像素焦距),b为基线长度,d为视差,
与
为两个相机主点的列坐标。
这里有个地方需要注意,如果获得视差图像是CV_16S类型的,这样的视差图的每个像素值由一个16bit表示,其中低位的4位存储的是视差值得小数部分,所以真实视差值应该是该值除以16。在进行映射后应该乘以16,以获得毫米级真实位置。
def get_depth(self, disparity, Q, scale=1.0, method=False): """ reprojectImageTo3D(disparity, Q),输入的Q,单位必须是毫米(mm) :param disparity: 视差图 :param Q: 重投影矩阵Q=[[1, 0, 0, -cx] [0, 1, 0, -cy] [0, 0, 0, f] [1, 0, -1/Tx, (cx-cx`)/Tx]] 其中f为焦距,Tx相当于平移向量T的第一个参数 :param scale: 单位变换尺度,默认scale=1.0,单位为毫米 :return depth:ndarray(np.uint16),depth返回深度图, 即距离 """ # 将图片扩展至3d空间中,其z方向的值则为当前的距离 if method: points_3d = cv2.reprojectImageTo3D(disparity, Q) # 单位是毫米(mm) x, y, depth = cv2.split(points_3d) else: # baseline = abs(camera_config["T"][0]) baseline = 1 / Q[3, 2] # 基线也可以由T[0]计算 fx = abs(Q[2, 3]) depth = (fx * baseline) / disparity depth = depth * scale # depth = np.asarray(depth, dtype=np.uint16) depth = np.asarray(depth, dtype=np.float32) return depth
- 运行
demo.py
后,鼠标点击图像任意区域,终端会打印对应距离 - 鼠标点击手部区域会打印距离摄像头的距离约633mm,即0.63米,还是比较准的
(x,y)=(203,273),depth=633.881653mm (x,y)=(197,329),depth=640.386047mm (x,y)=(222,292),depth=631.549072mm (x,y)=(237,270),depth=630.389221mm (x,y)=(208,246),depth=652.560669mm
双目测距的精度 说明:
根据上式可以看出,某点像素的深度精度取决于该点处估计的视差d的精度。假设视差d的误差恒定,当测量距离越远,得到的深度精度则越差,因此使用双目相机不适宜测量太远的目标。
如果想要对与较远的目标能够得到较为可靠的深度,一方面需要提高相机的基线距离,但是基线距离越大,左右视图的重叠区域就会变小,内容差异变大,从而提高立体匹配的难度,另一方面可以选择更大焦距的相机,然而焦距越大,相机的视域则越小,导致离相机较近的物体的距离难以估计。
理论上,深度方向的测量误差与测量距离的平方成正比,而X/Y方向的误差与距离成正比;而距离很近时,由于存在死角,会导致难以匹配的问题;想象一下,如果你眼前放置一块物体,那你左眼只能看到物体左侧表面,右眼同理只能看到物体右侧表面,这时由于配准失败,导致视差计算失败;这个问题在基线越长,问题就越严重
6.3D点云显示
恢复三维坐标后,就可以使用python-pcl和Open3D库显示点云图
PCL Python版比较难安装,如果安装不了,那可以采用Open3D勉强凑合使用吧
如下图所示,你可以用鼠标旋转坐标轴,放大点云
2D-RGB Open3D点云显示 PCL点云显示 def show_3dcloud_for_open3d(self, frameL, frameR, points_3d): """ 使用open3d显示点云 :param frameL: :param frameR: :param points_3d: :return: """ if self.use_open3d: x, y, depth = cv2.split(points_3d) # depth = points_3d[:, :, 2] self.open3d_viewer.show(color_image=frameL, depth_image=depth) def show_3dcloud_for_pcl(self, frameL, frameR, points_3d): """ 使用PCL显示点云 :param frameL: :param frameR: :param points_3d: :return: """ if self.use_pcl: self.pcl_viewer.add_3dpoints(points_3d, frameL) self.pcl_viewer.show()
7.项目代码
【完整的项目代码】双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
python demo.py
示例代码如下, 项目配套了双目摄像头的校准参数文件,以及左右摄像的视频文件,可以作为Demo直接测试和使用
# -*-coding: utf-8 -*- """ @Author : panjq @E-mail : pan_jinquan@163.com @Date : 2020-04-10 18:24:06 """ import os import cv2 import argparse import numpy as np from core.utils import image_utils, file_utils from core import camera_params, stereo_matcher class StereoDepth(object): """双目测距""" def __init__(self, stereo_file, width=640, height=480, filter=True, use_open3d=True, use_pcl=True): """ :param stereo_file: 双目相机内外参数配置文件 :param width: 相机分辨率width :param height:相机分辨率height :param filter: 是否使用WLS滤波器对视差图进行滤波 :param use_open3d: 是否使用open3d显示点云 :param use_pcl: 是否使用PCL显示点云 """ self.count = 0 self.filter = filter self.camera_config = camera_params.get_stereo_coefficients(stereo_file) self.use_pcl = use_pcl self.use_open3d = use_open3d # 初始化3D点云 if self.use_pcl: # 使用open3d显示点云 from core.utils_pcl import pcl_tools self.pcl_viewer = pcl_tools.PCLCloudViewer() if self.use_open3d: # 使用PCL显示点云 from core.utils_3d import open3d_visual self.open3d_viewer = open3d_visual.Open3DVisual(camera_intrinsic=self.camera_config["K1"], depth_width=width, depth_height=height) self.open3d_viewer.show_image_pcd(True) self.open3d_viewer.show_origin_pcd(True) self.open3d_viewer.show_image_pcd(True) assert (width, height) == self.camera_config["size"], Exception("Error:{}".format(self.camera_config["size"])) def test_pair_image_file(self, left_file, right_file): """ 测试一对左右图像 :param left_file: 左路图像文件 :param right_file: 右路图像文件 :return: """ frameR = cv2.imread(left_file) frameL = cv2.imread(right_file) self.task(frameR, frameL, waitKey=0) def capture1(self, video): """ 用于采集单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) :param video:int or str,视频路径或者摄像头ID :param save_dir: str,保存左右图片的路径 """ cap = image_utils.get_video_capture(video) width, height, numFrames, fps = image_utils.get_video_info(cap) self.count = 0 while True: success, frame = cap.read() if not success: print("No more frames") break frameL = frame[:, :int(width / 2), :] frameR = frame[:, int(width / 2):, :] self.count += 1 self.task(frameL, frameR, waitKey=5) if cv2.waitKey(1) & 0xFF == ord('q'): # Get key to stop stream. Press q for exit break cap.release() cv2.destroyAllWindows() def capture2(self, left_video, right_video): """ 用于采集双USB连接线的双目摄像头 :param left_video:int or str,左路视频路径或者摄像头ID :param right_video:int or str,右视频路径或者摄像头ID :return: """ capL = image_utils.get_video_capture(left_video) capR = image_utils.get_video_capture(right_video) width, height, numFrames, fps = image_utils.get_video_info(capL) width, height, numFrames, fps = image_utils.get_video_info(capR) self.count = 0 while True: successL, frameL = capL.read() successR, frameR = capR.read() if not (successL and successR): print("No more frames") break self.count += 1 self.task(frameL, frameR, waitKey=50) if cv2.waitKey(1) & 0xFF == ord('q'): # Get key to stop stream. Press q for exit break capL.release() capR.release() cv2.destroyAllWindows() def get_3dpoints(self, disparity, Q, scale=1.0): """ 计算像素点的3D坐标(左相机坐标系下) reprojectImageTo3D(disparity, Q),输入的Q,单位必须是毫米(mm) :param disparity: 视差图 :param Q: 重投影矩阵Q=[[1, 0, 0, -cx] [0, 1, 0, -cy] [0, 0, 0, f] [1, 0, -1/Tx, (cx-cx`)/Tx]] 其中f为焦距,Tx相当于平移向量T的第一个参数 :param scale: 单位变换尺度,默认scale=1.0,单位为毫米 :return points_3d:ndarray(np.float32),返回三维坐标points_3d,三个通道分布表示(X,Y,Z) 其中Z是深度图depth, 即距离,单位是毫米(mm) """ # 返回三维坐标points_3d,三个通道分布表示(X,Y,Z) # depth = stereo_matcher.get_depth(disparity, Q, scale=1.0) points_3d = cv2.reprojectImageTo3D(disparity, Q) # x, y, depth = cv2.split(points_3d) # baseline = abs(camera_config["T"][0]) # baseline = 1 / Q[3, 2] # 基线也可以由T[0]计算 # fx = abs(Q[2, 3]) # depth = (fx * baseline) / disparity points_3d = points_3d * scale points_3d = np.asarray(points_3d, dtype=np.float32) return points_3d def get_disparity(self, imgL, imgR, use_wls=True): """ :param imgL: 畸变校正和立体校正后的左视图 :param imgR:畸变校正和立体校正后的右视图 :param use_wls:是否使用WLS滤波器对视差图进行滤波 :return dispL:ndarray(np.float32),返回视差图 """ dispL = stereo_matcher.get_filter_disparity(imgL, imgR, use_wls=use_wls) # dispL = disparity.get_simple_disparity(imgL, imgR) return dispL def get_rectify_image(self, imgL, imgR): """ 畸变校正和立体校正 根据更正map对图片进行重构 获取用于畸变校正和立体校正的映射矩阵以及用于计算像素空间坐标的重投影矩阵 :param imgL: :param imgR: :return: """ # camera_params.get_rectify_transform(K1, D1, K2, D2, R, T, image_size) left_map_x, left_map_y = self.camera_config["left_map_x"], self.camera_config["left_map_y"] right_map_x, right_map_y = self.camera_config["right_map_x"], self.camera_config["right_map_y"] rectifiedL = cv2.remap(imgL, left_map_x, left_map_y, cv2.INTER_LINEAR, borderValue=cv2.BORDER_CONSTANT) rectifiedR = cv2.remap(imgR, right_map_x, right_map_y, cv2.INTER_LINEAR, borderValue=cv2.BORDER_CONSTANT) return rectifiedL, rectifiedR def task(self, frameL, frameR, waitKey=5): """ :param frameL: 左路视频帧图像(BGR) :param frameR: 右路视频帧图像(BGR) """ # 畸变校正和立体校正 rectifiedL, rectifiedR = self.get_rectify_image(imgL=frameL, imgR=frameR) # 绘制等间距平行线,检查立体校正的效果 # calibrate_tools.draw_line_rectify_image(rectifiedL, rectifiedR) # We need grayscale for disparity map. grayL = cv2.cvtColor(rectifiedL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(rectifiedR, cv2.COLOR_BGR2GRAY) # Get the disparity map dispL = self.get_disparity(grayL, grayR, self.filter) points_3d = self.get_3dpoints(disparity=dispL, Q=self.camera_config["Q"]) self.show_3dcloud_for_open3d(frameL, frameR, points_3d) self.show_3dcloud_for_pcl(frameL, frameR, points_3d) self.show_2dimage(frameL, frameR, points_3d, dispL, waitKey=waitKey) def show_3dcloud_for_open3d(self, frameL, frameR, points_3d): """ 使用open3d显示点云 :param frameL: :param frameR: :param points_3d: :return: """ if self.use_open3d: x, y, depth = cv2.split(points_3d) # depth = points_3d[:, :, 2] self.open3d_viewer.show(color_image=frameL, depth_image=depth) def show_3dcloud_for_pcl(self, frameL, frameR, points_3d): """ 使用PCL显示点云 :param frameL: :param frameR: :param points_3d: :return: """ if self.use_pcl: self.pcl_viewer.add_3dpoints(points_3d, frameL) self.pcl_viewer.show() def show_2dimage(self, frameL, frameR, points_3d, dispL, waitKey=0): """ :param frameL: :param frameR: :param dispL: :param points_3d: :return: """ x, y, depth = cv2.split(points_3d) # depth = points_3d[:, :, 2] depth_colormap = stereo_matcher.get_visual_depth(depth) dispL_colormap = stereo_matcher.get_visual_disparity(dispL) image_utils.addMouseCallback("left", depth, info="depth=%fmm") image_utils.addMouseCallback("right", depth, info="depth=%fmm") image_utils.addMouseCallback("disparity-color", depth, info="depth=%fmm") image_utils.addMouseCallback("depth-color", depth, info="depth=%fmm") result = {"frameL": frameL, "frameR": frameR, "disparity": dispL_colormap, "depth": depth_colormap} cv2.imshow('left', frameL) cv2.imshow('right', frameR) cv2.imshow('disparity-color', dispL_colormap) cv2.imshow('depth-color', depth_colormap) key = cv2.waitKey(waitKey) self.save_images(result, self.count, key) if self.count <= 2: cv2.moveWindow("left", 700, 0) cv2.moveWindow("right", 1400, 0) cv2.moveWindow("disparity-color", 700, 700) cv2.moveWindow("depth-color", 1400, 700) def save_images(self, result, count, key, save_dir="./data/temp"): """ :param result: :param count: :param key: :param save_dir: :return: """ if key == ord('q'): exit(0) elif key == ord('c') or key == ord('s'): file_utils.create_dir(save_dir) print("save image:{:0=4d}".format(count)) cv2.imwrite(os.path.join(save_dir, "left_{:0=4d}.png".format(count)), result["frameL"]) cv2.imwrite(os.path.join(save_dir, "right_{:0=4d}.png".format(count)), result["frameR"]) cv2.imwrite(os.path.join(save_dir, "disparity_{:0=4d}.png".format(count)), result["disparity"]) cv2.imwrite(os.path.join(save_dir, "depth_{:0=4d}.png".format(count)), result["depth"]) def str2bool(v): return v.lower() in ('yes', 'true', 't', 'y', '1') def get_parser(): stereo_file = "configs/lenacv-camera/stereo_cam.yml" # left_video = None # right_video = None left_video = "data/lenacv-video/left_video.avi" right_video = "data/lenacv-video/right_video.avi" left_file = "docs/left.png" right_file = "docs/right.png" parser = argparse.ArgumentParser(description='Camera calibration') parser.add_argument('--stereo_file', type=str, default=stereo_file, help='stereo calibration file') parser.add_argument('--left_video', default=left_video, help='left video file or camera ID') parser.add_argument('--right_video', default=right_video, help='right video file or camera ID') parser.add_argument('--left_file', type=str, default=left_file, help='left image file') parser.add_argument('--right_file', type=str, default=right_file, help='right image file') parser.add_argument('--filter', type=str2bool, nargs='?', default=True, help='use disparity filter') return parser if __name__ == '__main__': args = get_parser().parse_args() stereo = StereoDepth(args.stereo_file, filter=args.filter) if args.left_video is not None and args.right_video is not None: # 双USB连接线的双目摄像头 stereo.capture2(left_video=args.left_video, right_video=args.right_video) elif args.left_video is not None: # 单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) stereo.capture1(video=args.left_video) elif args.right_video is not None: # 单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示) stereo.capture1(video=args.right_video) if args.left_file and args.right_file: # 测试一对左右图像 stereo.test_pair_image_file(args.left_file, args.right_file)
8.参考资料
- <真实场景的双目立体匹配(Stereo Matching)获取深度图详解> : 真实场景的双目立体匹配(Stereo Matching)获取深度图详解 - 一度逍遥 - 博客园
- <双目测距理论及其python实现>
-
【完整的项目代码】双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python
-
利用matlab实现三维重建,双目视觉的三维重构
2020-10-21 20:20:16很好的源码,思路清晰,利用双目实现的,大家有兴趣使用的可以直接下载,可以应用到比赛和论文内的,工具是matlab,代码也很好理解 -
双目视觉三维重建
2018-08-14 12:45:37一些双目三维重建的代码,有matlab和c++的,效果不错。 -
目前有没有双目三维重建开源项目?
2020-12-05 17:00:20双目三维对好像就听过:Bundler和CMVS-PMVS但是三维重建开源项目就很多了1、Meshroom ⭐4,474Meshroom是一款基于AliceVision摄影测量计算机视觉框架的免费开源三维重建软件。...Ope...双目三维对好像就听过:
Bundler和CMVS-PMVS
但是三维重建开源项目就很多了
1、Meshroom ⭐4,474
Meshroom是一款基于AliceVision摄影测量计算机视觉框架的免费开源三维重建软件。https://github.com/alicevision/meshroomgithub.com
2、 Openmvg ⭐2,829
Openmvg库根据三维计算机视觉和结构的运动。OpenMVG提供了一个端到端的3D重建,它由图像框架组成,包含库、二进制文件和管道。这些库提供了简单的功能,如:图像处理,功能描述和匹配,功能跟踪,相机模型,多视图几何,旋转估计…
该二进制文件解决了管道可能需要的单元任务:场景初始化、特征检测与匹配和运动重建的结构,并将重建的场景导出到其他多视点立体视觉框架中,以计算密集的点云或纹理网格。
这些管道通过链接各种二进制文件来计算图像匹配关系
OpenMVG是用c++开发的,可以在Android、iOS、Linux、macOS和Windows上运行。https://github.com/openMVG/openMVGgithub.com
3、 Awesome_3dreconstruction_list ⭐2,261
与图像3D重建相关的论文和资源精选清单https://github.com/openMVG/awesome_3DReconstruction_listgithub.com
4、Awesome Point Cloud Analysis ⭐1,801
关于点云分析(处理)的论文和数据集列表https://github.com/Yochengliu/awesome-point-cloud-analysisgithub.com
5、Opensfm ⭐1,635
OpenSfM是一个用Python编写的运动库的结构。该库作为一个处理管道,用于从多个图像重建相机姿态和3D场景。它由运动结构的基本模块(特征检测/匹配,最小解算)组成,重点是构建一个健壮的、可伸缩的重建管道。它还集成了外部传感器(如GPS、加速计)测量,以实现地理定位和鲁棒性。提供了一个JavaScript查看器来预览模型和调试管道。
mapillary/OpenSfMgithub.com
6、Alicevision ⭐1,318
AliceVision是摄影测量计算机视觉框架,可提供3D重建和相机跟踪算法。AliceVision旨在通过可测试,分析和重用的最新计算机视觉算法提供强大的软件基础。该项目是学术界和工业界合作的结果,旨在为尖端算法提供鲁棒性和生产使用所需的质量。https://github.com/alicevision/AliceVisiongithub.com
7、Openmvs ⭐1,193
OpenMVS是面向计算机视觉的库,尤其是针对多视图立体重建社区的。尽管有针对运动结构管道(例如OpenMVG)的成熟而完整的开源项目,这些管道可以从输入的图像集中恢复相机的姿势和稀疏的3D点云,但没有一个解决摄影测量链的最后一部分-流。OpenMVS旨在通过提供一套完整的算法来恢复要重建场景的整个表面来填补这一空白。输入是一组摄影机姿势加上稀疏的点云,输出是带纹理的网格。该项目涉及的主要主题是:密集的点云重构,以获得尽可能完整,准确的点云
网格重建,用于估计最能解释输入点云的网格表面
网格细化可恢复所有精细细节
网格纹理,用于计算清晰准确的纹理以对网格着色https://github.com/cdcseacave/openMVSgithub.com
8、Bundler_sfm ⭐1,158snavely/bundler_sfmgithub.com
9、Bundlefusion ⭐752
使用在线表面重新整合进行实时全局一致的三维重建https://github.com/niessner/BundleFusiongithub.com
10、Face_swap ⭐636
面部交换:https://github.com/YuvalNirkin/face_swapgithub.com
11、Scannet ⭐678
ScanNet是一个RGB-D视频数据集,包含超过1500次扫描中的250万次视图,使用3D摄像机姿态、表面重建和实例级语义分段进行注释。ScanNet/ScanNetgithub.com
12、Softras⭐540
SoftRas是一个真正的可微分渲染框架,把渲染作为一个可微分的聚合过程,融合所有网格三角形的概率贡献相对于渲染像素。ShichenLiu/SoftRasgithub.com
13、Pifu ⭐474
https://github.com/shunsukesaito/PIFugithub.com
14、Matterport ⭐460
用于RGB-D机器学习任务的非常棒的数据集。niessner/Matterportgithub.com
15、Kimera⭐456
Kimera是一个用于实时度量-语义同步定位和映射的c++库,它使用摄像机图像和惯性数据来构建环境的语义注释3D网格。Kimera是模块化的,支持ros,在CPU上运行。https://github.com/MIT-SPARK/Kimeragithub.com
16、 Mvs Texturing ⭐421
项目可以根据图像对3D重建进行纹理处理。该项目专注于使用运动和多视图立体技术的结构生成的3D重建。nmoehrle/mvs-texturinggithub.com
17、Livescan3d ⭐402
LiveScan3D是一个实时三维重建系统,使用多个Kinect v2深度传感器同时进行三维重建。产生的3D重建形式是有色点云的形式,所有Kinect的点都放置在同一坐标系中。该系统的可能使用场景包括:同时从多个视点捕获对象的3D结构,
捕获场景的“全景” 3D结构(通过使用多个传感器来扩展一个传感器的视场),
将重建的点云流式传输到远程位置,
通过让多个传感器捕获同一场景来提高单个传感器捕获的点云的密度。MarekKowalski/LiveScan3Dgithub.com
18、Voxelhashing ⭐364
大规模、实时三维重建:niessner/VoxelHashinggithub.com
19、Layoutnet ⭐298
从单个RGB图像重建三维房间布局
https://github.com/zouchuhang/LayoutNetgithub.com
20、 Tsdf Fusion Python ⭐295
这是一个轻量级的python脚本,可将多个颜色和深度图像融合到TSDF体积中,然后可以将其用于创建高质量的3D表面网格和点云。在Ubuntu 16.04上测试效果如下图:
andyzeng/tsdf-fusion-pythongithub.com
21、Intrinsic3d ⭐231
通过外观和几何优化以及空间变化的照明实现高质量3D重构
NVlabs/intrinsic3dgithub.com
22、Kimera Semantics ⭐228
从2D数据进行实时3D语义重构
https://github.com/MIT-SPARK/Kimera-Semanticsgithub.com
23、Awesome Holistic 3d ⭐209
3D重建的论文和资源清单:https://github.com/holistic-3d/awesome-holistic-3dgithub.com
24、3dreconstruction ⭐151
使用Python3进行SFM的3D重建alyssaq/3Dreconstructiongithub.com
25、Structured3d ⭐121
用于结构化3D建模的大型照片级数据集
https://github.com/bertjiazheng/Structured3Dgithub.com
26、Synthesize3dviadepthorsil ⭐117
通过对多视图深度图或轮廓建模来生成和重建3D形状
Amir-Arsalan/Synthesize3DviaDepthOrSilgithub.com
27、Msn Point Cloud Completion ⭐111
https://github.com/Colin97/MSN-Point-Cloud-Completiongithub.com
28、Cnncomplete ⭐107
用于训练体积深层神经网络以完成部分扫描的3D形状的代码
angeladai/cnncompletegithub.com
29、Reconstructiondataset ⭐95
用于进行三维重建的一组图像https://github.com/rperrot/ReconstructionDataSetgithub.com
30、3d Recgan Extended ⭐81
从单个深度视图进行密集的3D对象重建Yang7879/3D-RecGAN-extendedgithub.com
-
OpenCV3.0实现SfM双目三维重建.zip
2022-05-03 16:59:56OpenCV3.0实现SfM双目三维重建,代码是用VS2013写的,OpenCV版本为3.0且包含扩展部分,如果不使用SIFT特征,可以修改源代码,然后使用官方未包含扩展部分的库。软件运行后会将三维重建的结果写入Viewer目录下的... -
双目三维重建_聊聊三维重建双目立体视觉原理
2020-12-17 19:53:12点击上方“3D视觉工坊”,选择“星标”干货第一时间送达作者:Tengfei Jianghttps://zhuanlan.zhihu.com/p/81016834本文已由原作者授权,不得擅自二次转载前言三维重建是个跨多学科的应用领域,围绕不同的尺度大小、...点击上方“3D视觉工坊”,选择“星标”
干货第一时间送达
作者:Tengfei Jiang
https://zhuanlan.zhihu.com/p/81016834
本文已由原作者授权,不得擅自二次转载
前言
三维重建是个跨多学科的应用领域,围绕不同的尺度大小、不同速度要求、不同精度要求、不同硬件成本等要求发展出了各种各样的技术方案。在这个应用领域,充分体现了,没有最好的设备,只有最合适的方案。在本系列文章中,我尝试解释接触过的不同技术方案,如有错误之处,敬请斧正。双目立体视觉原理
视差 (Disparity) 及 深度计算
人依靠两只眼睛判断深度(物体离眼睛的距离),具体是如何来判断的呢,我们从小到大似乎并未接受过深度计算的训练。视差(Disparity)是解释原理的基本概念之一。我们可以做个简单的实验,将手指置于双目之间,分别开闭左右眼。怎么样,是不是发现手指不在同一个位置?这就是视差。
可以参考上图,当左右相机同时观察三维点时,该点分别投影在左右相机的相平面上,这两个投影点之间的差异就是视差:d=Xleft-Xright。 这个公式看起来简单直观,其实有不少未解释清楚的地方,比如这两个 x 是在同一个坐标系内么,这两个像平面一定是平行摆放的吗,为什么可以直接减?等等 。 要解释清楚这些问题,上图还是略简陋,让我们换张图来解释。
图中,P是三维物体的顶点坐标,其和左右相机光心CL、CR的连线与左右相平面的交点、
即为投影点。注意现在说的所有坐标都是定义在同一个坐标系内,坐标原点与标架已经在图中左下角标识出来了。 现在问题来了,已知
,已知d、b、f,求z。这是一个初中几何题,答案很简单:
从公式可以看出,视差 d和深度z成反比关系。视差越大,可以探测的深度越小。b是两个相机光心的距离,又叫基线(baseline),f是相机的焦距。f、b与深度均成正比关系。
立体匹配
从上一节可以看到,如果要计算深度,我们需要知道视差、基线、焦距。另外注意,上文的推导是基于理想模型,比如不考虑相机的畸变,不考虑双相机光轴不平行的情况。
在视差计算之前,我们首先给定了两个投影点。但实际应用中,我们并不知道左右相机中哪两个点是对应点。查找对应点是双目立体视觉中非常核心的步骤,可以毫不夸张地说,大部分的结构光重建方案解决的都是如何准确快速地匹配对应点。在介绍具体方案之前,有些通用的背景知识稍微铺垫一下。
对极几何(Epipolar Geometry)是一个内容非常丰富的范畴(本文不想铺展太多,只是选择几个概念简单描述,详细内容可以参考《计算机视觉中的多视图几何》一书)。对极几何描述的是三维点与两个相机相平面之间的特殊几何关系,我们先看下图的模型。
其中C0、C1为两个相机中心,P为空间中一点,P在C0、C1对应像平面上的投影分别为X0、X1。C0、C1连线与像平面的交点e0、e1称为极点(Epipoles),l0、l1称为极线(Epipolar Lines),C0、C1、P三点组成的平面称为极平面(Epipolar Plane)。
这个模型有个有趣的性质。当三维点P沿着
方向接近左相机时,我们发现其在左相机上的投影点X0并不会移动,但是其在右相机相平面上的投影点x1发生了变化,其移动轨迹一定是沿着极线l1。反过来,假设我们并不知道P点坐标,只知道X0是其在左相机上的投影,要寻找其在右相机相平面中的投影,则只需要沿着极线l1搜索即可。这个性质使得对应点匹配的搜索空间直接从2维降低到1维。
聪明的同学看到这肯定会问了,没有P点怎么知道极线在哪,这不是因果不分么?事实上极线的位置仅和X0以及相机的内外参有关,和P点位置无关。这就引出了接下来的约束。在对极几何中有个非常著名的约束---对极约束(Epipolar Constraint)形式化地描述了对应点 X0、X1之间的几何关系:
其中F是基础矩阵(Fundamental Matrix)。这个式子是如此简洁,以至于忍不住想要推导一番,推导过程见附录1。
极线矫正 对极约束描述了对应点匹配可沿极线搜索。在实际应用中,两个相机摆放一定是不平行的,因而相平面中的极线大概率是条斜线,这就给搜索过程带来了不便,为了简化过程,还需要引入额外的极线矫正步骤,使得两相机的极线共线且平行于相平面的X 轴。矫正前后的效果如下面两张图所示,应该比较直观。
对应点查找经过上述处理后,要生成视差图,最核心的步骤就是在相平面的同一行上,查找对应点了。查找的方法有多种,大体上可以分成两类。
(1)提取图像特征该类方法可以对每张图像单独进行分析,提取“特征”。这里特征可以有不同的表示方法,如边缘、角点等,也可能来自其他主动投射的结构光信息,如正弦条纹相位值、编码值等,通过在双目图像之间查找相同(相似)特征来确定对应点。
(2)使用相关关系 该类方法假设对应点小领域内有相似的亮度模式,因而可以用两者的相关关系来定位。为了增加额外的亮度变化信息,通常会通过主动光源投射随机散斑这类图案。
具体的结构光重建原理会在后续文章中展开讨论。
参考
附录
对极约束证明:
上述内容,如有侵犯版权,请联系作者,会自行删文。
交流群
欢迎加入我们公众号读者群一起和同行交流,目前有3D视觉、深度学习、激光SLAM、VSLAM、三维重建、点云后处理、图像处理、手眼标定、自动驾驶、位姿估计等微信群,请扫描下面微信号加群,备注:”研究方向+学校/公司+昵称“,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进去相关微信群。
知识星球
学习3D视觉核心技术,扫描查看详情,3天内无条件退款
圈里有高质量教程资料、可答疑解惑、助你高效解决问题
-
双目三维重建_【光电视界】简单介绍双目视觉三维重构
2020-12-17 19:53:01今日光电 有人说,20世纪是电的世纪,21世纪是光的世纪;知光解电,再小的个体都可以被...三维重构又被称为三维重建,百度百科中对它的定义为:指对三维物体建立适合计算机表示和处理的数学模型,是在计算机环境下对... -
双目标定后的三维重建
2019-04-09 10:34:23从大佬那里保存的,这个需要输入立体校正后的图片和相机焦距。如何输入及数据来源在我上传的另一份文件里。 -
HALCON双目三维重建实验
2021-09-10 19:07:47下面是标定双目并进行双目标定的halcon代码,我使用的图片分辨率太高了,所以笔记本无法运行,使用台式机测试成功,但是图片需要拍得比较好才行。 *set_calib_data_calib_object (CalibDataID, 0, CalTabFile) ... -
双目三维重建.zip 通过标定 匹配进行重建的
2021-01-04 18:03:50双目三维重建程序 -
双目三维重建_传统双目三维重建IEEE Trans 论文生产流程
2020-12-17 19:52:50这一方面是由于这个主题论文写作经验的缺乏,没有很好地把我们的观点凸显出来,另一方面就是看的论文不够多,不知道别人做到了什么程度,因此在表述自己的观点的时候,对现有文献的深度没有挖掘出来,三是看的业界... -
基于Python的三维重建开源代码
2020-03-02 18:32:12基于Python的三维重建开源代码,包括特征提取,SFM,PMVS以及CMVS等相关功能! -
vs2015+opencv2.4.10实现的双目立体视觉三维重建c++代码
2018-06-11 18:55:14vs2015+opencv2.4.10实现的双目立体视觉三维重建c++代码。SGBM立体匹配 -
opencv写的双目视觉摄像机标定和三维重建代码
2018-10-20 21:17:53opencv写的双目视觉摄像机标定和三维重建代码opencv写的双目视觉摄像机标定和三维重建代码opencv写的双目视觉摄像机标定和三维重建代码 -
双目视觉opencv opengl三维重建
2018-03-24 12:40:16双目视觉opencv opengl三维重建双目视觉opencv opengl三维重建双目视觉opencv opengl三维重建,请修改代码中opencv对应版本号 -
双目视觉的三维重建
2015-04-08 16:44:42这是双目视觉的三维重建代码,希望对大家有所帮助 -
双目三维重建—基于特征点匹配的被动式三维重建(OpenCV+OpenGL)
2018-11-14 16:24:08这里特别感谢博主shiter的原创文章:OpenCV+OpenGL 双目立体视觉三维重建 本博文参考了该博主的的核心代码,并针对该博主博文中声明的一些BUG进行了修正: 本文代码下载地址(已修正相关问题问题):... -
基于Kinect深度图像的三维重建
2020-10-16 21:40:14随着机器视觉理论的发展和硬件技术的进步,三维重建在生产、生活中的应用越来越广泛,基于Kinect传感器的三维重建得到广泛的应用。针对于现有的Kinect传感器获得的深度图像深度信息丢失的问题,提出了一种新的基于... -
双目相机三维重建
2019-04-09 10:29:56matlab实现双目标定,畸变矫正和立体校正,再用vs实现三维重建,亲测可用。 -
双目视觉+三维重建.rar
2020-07-08 17:02:10基于双目视觉的深度计算和三维重建,双目视觉opencv opengl三维重建,简单的三维重建系统,代码可正常运行 -
OpenCV+OpenGL 双目立体视觉三维重建
2016-08-08 00:15:25OpenCV+OpenGL 双目立体视觉三维重建代码以及文档 使用opencv进行立体匹配获取视差图,三角剖分 使用opengl进行纹理贴图 需要配置opengl,opencv,vs2015工程 博客地址: ... -
python图像处理三维重建所有代码
2018-05-27 00:01:52计算三维重建的方法称为SfM(Structure from Motion).\ 假设计算机已经标定,计算重建的部分可以分为下面四个步骤:\ (1)、检测特征点,然后在两幅图间进行特征点匹配。\ (2)、有匹配算出基础矩阵。\ (3)、由... -
基于双目视觉的三维重建
2014-03-19 21:18:08基于双目视觉的三维重建 基于双目视觉的三维重建 -
OpenCV实现SfM(二):双目三维重建
2018-01-16 15:16:40在三维重建前,我们先研究一下同一点在两个相机中的像的关系。假设在世界坐标系中有一点 p ,坐标为 X ,它在1相机中的像为 x 1 ,在2相机中的像为 x 2 (注意 x 1 和 x 2 为齐次坐标,最后一个元素是1),如下图。...