精华内容
下载资源
问答
  • Tengfei Jianghttps://zhuanlan.zhihu.com/p/81016834本文已由原作者授权,不得擅自二次转载前言三维重建是个跨多学科的应用领域,围绕不同的尺度大小、不同速度要求、不同精度要求、不同硬件成本等要求发展出了各种...

    点击上方“3D视觉工坊”,选择“星标”

    干货第一时间送达

    作者:Tengfei Jiang

    https://zhuanlan.zhihu.com/p/81016834

    本文已由原作者授权,不得擅自二次转载

    前言

    三维重建是个跨多学科的应用领域,围绕不同的尺度大小、不同速度要求、不同精度要求、不同硬件成本等要求发展出了各种各样的技术方案。在这个应用领域,充分体现了,没有最好的设备,只有最合适的方案。在本系列文章中,我尝试解释接触过的不同技术方案,如有错误之处,敬请斧正。

    双目立体视觉原理

    视差 (Disparity) 及 深度计算

    人依靠两只眼睛判断深度(物体离眼睛的距离),具体是如何来判断的呢,我们从小到大似乎并未接受过深度计算的训练。视差(Disparity)是解释原理的基本概念之一。我们可以做个简单的实验,将手指置于双目之间,分别开闭左右眼。怎么样,是不是发现手指不在同一个位置?这就是视差。

    688489045d74e5e008110d683c502a54.png 

    可以参考上图,当左右相机同时观察三维点时,该点分别投影在左右相机的相平面上,这两个投影点之间的差异就是视差:d=Xleft-Xright。 这个公式看起来简单直观,其实有不少未解释清楚的地方,比如这两个 x 是在同一个坐标系内么,这两个像平面一定是平行摆放的吗,为什么可以直接减?等等 。 要解释清楚这些问题,上图还是略简陋,让我们换张图来解释。

    aa599d2bb1d34c135eb196985cc39c7f.png 

    图中,P是三维物体的顶点坐标,其和左右相机光心CL、CR的连线与左右相平面的交点 b7814394170aa953b3d770f677f92162.png8070b2b4dd8afbd48a17b241d2bfc3cd.png 即为投影点。注意现在说的所有坐标都是定义在同一个坐标系内,坐标原点与标架已经在图中左下角标识出来了。 现在问题来了,已知 14a7531fc06173671dd7d241456b2cae.png ,已知d、b、f,求z。这是一个初中几何题,答案很简单:

    a845c89de9f5647380486c57d98b548c.png

    从公式可以看出,视差 d和深度z成反比关系。视差越大,可以探测的深度越小。b是两个相机光心的距离,又叫基线(baseline),f是相机的焦距。f、b与深度均成正比关系。

    立体匹配

    从上一节可以看到,如果要计算深度,我们需要知道视差、基线、焦距。另外注意,上文的推导是基于理想模型,比如不考虑相机的畸变,不考虑双相机光轴不平行的情况。

    在视差计算之前,我们首先给定了两个投影点。但实际应用中,我们并不知道左右相机中哪两个点是对应点。查找对应点是双目立体视觉中非常核心的步骤,可以毫不夸张地说,大部分的结构光重建方案解决的都是如何准确快速地匹配对应点。在介绍具体方案之前,有些通用的背景知识稍微铺垫一下。

    对极几何(Epipolar Geometry)是一个内容非常丰富的范畴(本文不想铺展太多,只是选择几个概念简单描述,详细内容可以参考《计算机视觉中的多视图几何》一书)。对极几何描述的是三维点与两个相机相平面之间的特殊几何关系,我们先看下图的模型。

    2758e42e233e1c8b90dff4784758d9b8.png 

    其中C0、C1为两个相机中心,P为空间中一点,P在C0、C1对应像平面上的投影分别为X0、X1。C0、C1连线与像平面的交点e0、e1称为极点(Epipoles),l0、l1称为极线(Epipolar Lines),C0、C1、P三点组成的平面称为极平面(Epipolar Plane)

    这个模型有个有趣的性质。当三维点P沿着185d6d0a8ba12094db415b5f81f73f94.png方向接近左相机时,我们发现其在左相机上的投影点X0并不会移动,但是其在右相机相平面上的投影点x1发生了变化,其移动轨迹一定是沿着极线l1。反过来,假设我们并不知道P点坐标,只知道X0是其在左相机上的投影,要寻找其在右相机相平面中的投影,则只需要沿着极线l1搜索即可。这个性质使得对应点匹配的搜索空间直接从2维降低到1维。

    聪明的同学看到这肯定会问了,没有P点怎么知道极线在哪,这不是因果不分么?事实上极线的位置仅和X0以及相机的内外参有关,和P点位置无关。这就引出了接下来的约束。在对极几何中有个非常著名的约束---对极约束(Epipolar Constraint)形式化地描述了对应点 X0、X1之间的几何关系:9d2c6428aeada21a001d82b5549507e5.png

    其中F是基础矩阵(Fundamental Matrix)。这个式子是如此简洁,以至于忍不住想要推导一番,推导过程见附录1。

    极线矫正 对极约束描述了对应点匹配可沿极线搜索。在实际应用中,两个相机摆放一定是不平行的,因而相平面中的极线大概率是条斜线,这就给搜索过程带来了不便,为了简化过程,还需要引入额外的极线矫正步骤,使得两相机的极线共线且平行于相平面的X 轴。矫正前后的效果如下面两张图所示,应该比较直观。

     cea625221fa2a33559ddb00f226d4cd3.png

    8250a8643d2aa8818afa5a5e3b3ad450.png 

    对应点查找经过上述处理后,要生成视差图,最核心的步骤就是在相平面的同一行上,查找对应点了。查找的方法有多种,大体上可以分成两类。

    (1)提取图像特征该类方法可以对每张图像单独进行分析,提取“特征”。这里特征可以有不同的表示方法,如边缘、角点等,也可能来自其他主动投射的结构光信息,如正弦条纹相位值、编码值等,通过在双目图像之间查找相同(相似)特征来确定对应点。

    (2)使用相关关系 该类方法假设对应点小领域内有相似的亮度模式,因而可以用两者的相关关系来定位。为了增加额外的亮度变化信息,通常会通过主动光源投射随机散斑这类图案。

    具体的结构光重建原理会在后续文章中展开讨论。

    参考

    7a9397202db1ed570831ed69b74f267d.png

    附录

    对极约束证明:

    be4e998d0bd3f643e64b043adbd993bb.png

    上述内容,如有侵犯版权,请联系作者,会自行删文。

    交流群

    欢迎加入我们公众号读者群一起和同行交流,目前有3D视觉深度学习激光SLAM、VSLAM、三维重建、点云后处理、图像处理、手眼标定、自动驾驶、位姿估计等微信群,请扫描下面微信号加群,备注:”研究方向+学校/公司+昵称“,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进去相关微信群。

    fb7153786173fa22eadc11584714d7bd.png

    知识星球

    学习3D视觉核心技术,扫描查看详情,3天内无条件退款

    ce5420a48cde22838e3286b60cb8839e.png

    圈里有高质量教程资料、可答疑解惑、助你高效解决问题

    展开全文
  • 同时也可申请加入我们的细分方向交流群,目前主要有3D视觉、CV&深度学习、SLAM、三维重建、点云后处理、自动驾驶、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地...

    点击上方“3D视觉工坊”,选择“星标”

    干货第一时间送达

    来源:https://blog.csdn.net/rs_lys/article/details/107102968

    开源代码免费获取,欢迎关注作者的GitHub: https://github.com/ethan-li-coding

    双目立体视觉(Binocular Stereo Vision)是机器视觉的一种重要形式,它是基于视差原理并利用成像设备从不同的位置获取被测物体的两幅图像,通过计算图像对应点间的位置偏差,来获取物体三维几何信息的方法1

    精度,是双目立体视觉至关重要的指标。

    双目立体视觉系统,不谈精度几许,未免显得业余!

    精度不行

    “来,小同志往边上让一让,下一个!”

    精度很高

    “小伙子你们设备多少钱!能打个折不!”

    做过双目研究的伙伴们,肯定是经常和精度打交道的,大部分人也肯定知道,双目立体视觉的精度主要看的是深度方向的精度!深度图是很多双目设备的输出数据,通过深度图以及相机参数,可以算出三维点云的空间坐标,公式如下:

    可知深度DDD的角色感很强,把深度估计准了,那三维点也就准了。(什么,相机标定也不准?当我没说!)

    所以,如何让深度精度更高?

    我们再来看一个经典公式:


    这可再熟悉不过了,DDD是深度,BBB是基线,fff是焦距(像素单位),ddd是视差。咱们一分为二看这个公式,右边分母部分是像素视差值,和算法相关;分子部分是系统硬件参数,和硬件相关。

    可知深度精度是由算法和硬件综合决定。

    研发:“哥们,你们硬件做的不行啊!”
    硬件:“屁,是你们算法不行!”
    老板:“都是废物!”

    对各参数不太清楚的同学可以看下图:

    1 算法因素

    咱们先分析算法对精度的影响。那么硬件参数BBB和fff就假设是已知且恒定的了。

    假设视差偏差为ΔdΔdΔd,则计算视差偏差ΔdΔdΔd下的深度偏差ΔDΔDΔD

    BBB和fff已知且恒定,如果我们把DDD也恒定,也就是在同一个深度距离下评判精度,由公式(1)可知ddd也就恒定。此时容易发现,ΔdΔdΔd越小,ΔDΔDΔD越小。说明:

    (一)视差偏差越小,深度偏差越小;换言之,算法的视差精度越高,深度精度越高,深度精度和视差精度成正比

    所以更高精度的视差估计算法,自然能带来更高的深度精度。

    2 硬件因素

    上面分析算法对精度的影响,所以固定了硬件参数BBB和fff。现在我们来分析视差精度恒定的情况下,硬件参数如何影响精度。还是基于上面三个公式,但是做一些修改,把公式(1)代入到(3)中,如下:

    同样,我们把DDD恒定,也就是在同一个深度距离下评判精度,因为视差精度恒定(也就是视差估计算法定了,精度差不多固定了),因此ΔdΔdΔd也恒定。不难发现,BBB和fff对DDD有相同的影响关系,BBB越大、fff越大,ΔDΔDΔD越小。说明:

    (二)基线越大、焦距(像素单位)越长,深度精度越高。深度精度和基线、焦距成正比。

    从公式中我们看到,影响ΔDΔDΔD的实际是基线和焦距的乘积,所以若两个一起增大,那深度精度必然增大,若一个增大一个减小,那么深度精度可能增大也可能减小。

    另一个需要说明的是,fff是焦距的像素单位,它和焦距的空间尺寸和像素大小有关,设焦距的空间尺寸为f0f_0f0,像素大小为sss,则fff的计算公式为:

    显然,sss越小,fff越大。我们知道sss的大小是相机传感器尺寸决定的,而焦距是镜头决定的。所以选相机的时候要了解第三个知识:

    (三)像素大小越小,同样的物理尺寸焦距有更长的像素尺寸焦距,深度精度就越高。

    3 深度范围

    上面我们分析了算法参数和硬件参数对深度精度的影响,但还有一个重要的因素,它即非硬件相关也非算法相关,它就是深度本身的大小,也即深度范围。简单的说,目标离镜头的距离不同,精度是不一样的。

    对上一节的公式(3)再多做一步推导:

    这里讨论深度范围,所以假设BBB、fff和ΔdΔdΔd都恒定。显然可以发现,DDD越小,ΔDΔDΔD越小。所以我们得出第四个结论:

    (四)测量目标离系统越近,深度精度越高。

    4 总结

    我想大家其实在实际应用中都会有一些直观的理解,比如(1)算法越好,精度越高,明摆着嘛!(2)相机分辨率越高,精度越高,这不废话嘛!(3)基线越长,精度越高,显而易见嘛!

    本文的目的就是通过公式推导来严格证明,以让大家更加清晰的明白各因素影响精度的本质,从而更科学的指导双目系统的设计。再来汇总一下上面的四条结论:

    (一)视差偏差越小,深度偏差越小;换言之,算法的视差精度越高,深度精度越高,深度精度和视差精度成正比。.

    (二)基线越大、焦距(像素单位)越长,深度精度越高。深度精度和基线、焦距成正比。

    (三)像素大小越小,同样的物理尺寸焦距有更长的像素尺寸焦距,深度精度就越高。

    (四)测量目标离系统越近,深度精度越高。

    所以同学们你们现在知道怎么设计双目系统能提高精度了吗?

    1、选高精度视差估计算法

    2、在应用场景允许、结构稳定性满足需求的情况下,尽可能延长基线

    3、在视场范围、景深满足需求的情况下,尽可能选择长焦镜头

    4、在算力允许的情况下,选择高分辨率相机(本质上应该是像素尺寸小的相机,有的相机提高了分辨率,只是提升了视域,但像素尺寸不变,那精度也不变)

    5、测量距离适当拉近(这点往往是场景需求决定的)

    备注:本文作者松博在哔哩哔哩做的立体匹配直播视频:

    本文仅做学术分享,如有侵权,请联系删文。

    下载1

    在「3D视觉工坊」公众号后台回复:3D视觉即可下载 3D视觉相关资料干货,涉及相机标定、三维重建、立体视觉、SLAM、深度学习、点云后处理、多视图几何等方向。

    下载2

    在「3D视觉工坊」公众号后台回复:3D视觉优质源码即可下载包括结构光、标定源码、缺陷检测源码、深度估计与深度补全源码、点云处理相关源码、立体匹配源码、单目、双目3D检测、基于点云的3D检测、6D姿态估计源码汇总等。

    下载3

    在「3D视觉工坊」公众号后台回复:相机标定即可下载独家相机标定学习课件与视频网址;后台回复:立体匹配即可下载独家立体匹配学习课件与视频网址。

    重磅!3DCVer-学术论文写作投稿 交流群已成立

    扫码添加小助手微信,可申请加入3D视觉工坊-学术论文写作与投稿 微信交流群,旨在交流顶会、顶刊、SCI、EI等写作与投稿事宜。

    同时也可申请加入我们的细分方向交流群,目前主要有3D视觉CV&深度学习SLAM三维重建点云后处理自动驾驶、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、学术交流、求职交流等微信群。

    一定要备注:研究方向+学校/公司+昵称,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,可快速被通过且邀请进群。原创投稿也请联系。

    ▲长按加微信群或投稿

    ▲长按关注公众号

    3D视觉从入门到精通知识星球:针对3D视觉领域的知识点汇总、入门进阶学习路线、最新paper分享、疑问解答四个方面进行深耕,更有各类大厂的算法工程人员进行技术指导。与此同时,星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息,打造成集技术与就业为一体的铁杆粉丝聚集区,近2000星球成员为创造更好的AI世界共同进步,知识星球入口:

    学习3D视觉核心技术,扫描查看介绍,3天内无条件退款

     圈里有高质量教程资料、可答疑解惑、助你高效解决问题

    整理不易,请给工坊点赞和在看

    展开全文
  • vs2015+opencv2.4.10实现的双目立体视觉三维重建c++代码。SGBM立体匹配
  • 本博客将实现Python版本的双目三维重建系统,项目代码实现包含:`双目标定`,`立体校正(含消除畸变)`,`立体匹配`,`视差计算`和`深度距离计算/3D坐标计算` 的知识点。限于篇幅,本博客不会过多赘述算法原理,而是...

    双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python


    目录

    双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python

    1.项目结构

    2. Environment

    3.双目相机标定和校准

    (0) 双目摄像头

    (1) 采集标定板的左右视图

    (2) 单目相机标定和校准

    (3) 双目相机标定和校准

    4.视差图和深度图

    (1) 立体校正

    (2) 立体匹配与视差图计算

    (3) Demo

    5.双目测距

    6.3D点云显示

    7.Demo代码

    8.参考资料


    博客将实现Python版本的双目三维重建系统,项目代码实现包含:`双目标定`,`立体校正(含消除畸变)`,`立体匹配`,`视差计算`和`深度距离计算/3D坐标计算` 的知识点。限于篇幅,本博客不会过多赘述算法原理,而是手把手教你,如果搭建一套属于自己的双目三维重建的系统。项目代码包含:

    • 支持双USB连接线的双目摄像头
    • 支持单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示)
    • 支持单目相机标定:mono_camera_calibration.py ,无需Matlab标定
    • 支持双目相机标定:stereo_camera_calibration.py,无需Matlab标定
    • 支持使用WLS滤波器对视差图进行滤波
    • 支持双目测距,误差在1cm内(鼠标点击图像即可获得其深度距离)
    • 支持Open3D和PCL点云显示

    尊重原创,转载请注明出处】:https://panjinquan.blog.csdn.net/article/details/121301896

    先放一张动图,这是最终重建的效果:

    完整的项目代码】:https://download.csdn.net/download/guyuealian/42517006

    三维重建中,除了双目相机,还有TOF和结构光3D 相机

    1. 飞行时间(Time of flight,TOF),代表公司微软Kinect2,PMD,SoftKinect, 联想 Phab,在手机中一般用于3D建模、AR应用,AR测距(华为TOF镜头)

    2. 双目视觉(Stereo Camera),代表公司 Leap Motion, ZED, 大疆;

    3. 结构光(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实现三维重建

    结构光三维重建-3D Scanning Software实现三维重建_pan_jinquan的博客-CSDN博客结构光相机标定-3D Scanning Software使用1. 说明2.Requirements(1)下载相关文件(2)3D Scanning Software源码编译3. Data capture(1)运行程序:scan3d-capture(2)采集校准图片:Capture calibration images(3) 进行校准:Calibration4.扫描模型:Model Scanninhttps://panjinquan.blog.csdn.net/article/details/121113787


    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连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示),基本满足我们测试需求。一般基线越长,可测量的距离越远,网友也可以根据自己需要购买。

    一点注意事项

    1. 双目相机三维重建也可以使用RGB+IR(红外)的摄像头,甚至IR+IR的也是可以,本人亲测,RGB+IR的相机,其效果也是杠杠的。
    2. 基线不太建议太小,作为测试,一般baseline在3~9cm就可以满足需求,有些无人车的双目基线更是恐怖到1~2米长
    3. 从双目三维重建原理中可知,左右摄像头的成像平面尽可能在一个平面内,成像平面不在同一个平面的,尽管可以立体矫正,其效果也差很多。
    4. 一分钱,一分货,相机的质量好坏,直接决定了你的成像效果

    (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 \
    

    参数说明: 

    1. 参数width指的是棋盘格宽方向黑白格子相交点个数
    2. 参数height指的是棋盘格长方向黑白格子相交点个数
    3. 参数left_video是左路相机ID,一般就是相机连接主板的USB接口号
    4. 参数right_video是右路相机ID,一般就是相机连接主板的USB接口号
    5. PS:如果你的双目相机是单USB连接线的双目摄像头(左右摄像头被拼接在同一个视频中显示),则设置left_video=相机ID,而right_video=-1,
    6. 参数detect建议设置True,这样可实时检测棋盘格,方面调整角度
    7. 按键盘s或者c保存左右视图图片
    left_imageright_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.ymlright_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_filestr双目相机的配置文件,如"configs/lenacv-camera/stereo_cam.yml"
    left_videostr左路相机ID或者视频文件
    right_videostr右路相机ID或者视频文件
    left_filestr左路测试图像文件
    right_filestr右路测试图像文件
    filterbool是否对视差图进行滤波
    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:
        """

    运算如下:


     [X,Y,Z,W] ^T=Q*[x,y,disparity(x,y),1]^ T

    _3dImage(x,y)=(X/W,Y/W,Z/W)

    重投影矩阵Q中c_xc_y为左相机主点在图像中的坐标,f为焦距,T_x为两台相机投影中心间的平移(负值),即基线baseline,相当于平移向量T[0], c_{x}^{`}  是右相机主点在图像中的坐标。

    其中Z即是深度距离depth:

    其中 f 为焦距长度(像素焦距),b为基线长度,d为视差,c_{xl}c_{xr}为两个相机主点的列坐标。

    这里有个地方需要注意,如果获得视差图像是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方向的误差与距离成正比;而距离很近时,由于存在死角,会导致难以匹配的问题;想象一下,如果你眼前放置一块物体,那你左眼只能看到物体左侧表面,右眼同理只能看到物体右侧表面,这时由于配准失败,导致视差计算失败;这个问题在基线越长,问题就越严重

     双目立体视觉系统精度分析_3D Vision-CSDN博客_双目视觉定位精度在一个三维测量项目中,如果采用立体视觉方案,首先,要根据测量需求(精度、测量范围、速度等),确定立体视觉的硬件方案。Thomas Luhmann在他的《Close-Range Photogrammetry and 3D Imaging》(2014)中,给出立体视觉系统的简化分析方法。这个方法假设两个相机的光轴平行,基线与光轴垂直,基线长度b和焦距值c不存在误差。分析了图像处理误差对立体定位https://blog.csdn.net/xuyuhua1985/article/details/50151269


    6.3D点云显示

    恢复三维坐标后,就可以使用python-pcl和Open3D库显示点云图

    PCL Python版比较难安装,如果安装不了,那可以采用Open3D勉强凑合使用吧

    如下图所示,你可以用鼠标旋转坐标轴,放大点云

    2D-RGBOpen3D点云显示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.Demo代码

    完整的项目代码】:https://download.csdn.net/download/guyuealian/42517006

    # -*-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.参考资料

    展开全文
  • 今日光电 有人说,20世纪是电的世纪,21世纪是光的世纪;知光解电,再小的个体都可以被...三维重构又被称为三维重建,百度百科中对它的定义为:指对三维物体建立适合计算机表示和处理的数学模型,是在计算机环境下对...

    今日光电

           有人说,20世纪是电的世纪,21世纪是光的世纪;知光解电,再小的个体都可以被赋能。欢迎来到今日光电!


    faa6abe896fcfb00bbde949ec15cd883.png

    ----与智者为伍 为创新赋能----

    1、三维重构

    1.1、三维重构到底是什么?

      首先要了解立体匹配算法,首先要知道立体匹配算法主要具体应用于什么方向。

    三维重构又被称为三维重建,百度百科中对它的定义为:指对三维物体建立适合计算机表示和处理的数学模型,是在计算机环境下对其进行处理、操作和分析其性质的基础,也是在计算机中建立表达客观世界的虚拟现实的关键技术。 

      在计算机视觉中, 三维重建是指根据单视图或者多视图的图像重建三维信息的过程. 由于单视频的信息不完全,因此三维重建需要利用经验知识. 而多视图的三维重建(类似人的双目定位)相对比较容易, 其方法是先对摄像机进行标定, 即计算出摄像机的图象坐标系与世界坐标系的关系.然后利用多个二维图象中的信息重建出三维信息。

      个人认为,三维重构就是通过计算机视觉技术,将现实世界中物体的三维信息获取到计算机中的过程,使物体的三维信息能够通过点云、网格等形式,显示或是储存在计算机中。(如点云的pcd格式文件保存形式、以及利用Gemagic等软件进行点云的可视化处理等。)

    三维数据需要通过三维重建技术获得。广义三维重建是指通过测量工具与解算方法,获取目标局部点、三维坐标、面三维结构乃至整体三维模型;狭义三维重建指通过重建技术,获取包括结构、纹理、尺度等的目标完整三维信息。

    1.2、三维重构具体有什么用?

      在阅读文献的过程中,文献提到了三维重构被广泛应用于无人驾驶、物体导航、无人机避障、医学诊断、逆向工程、文物保护、精密仪器测量等方面。提到的这些方面的应用虽然能够给人一种模糊的印象,但是还是不够具体。因此在此,我想尝试揭开他的神秘面纱。

    1.2.1、无人机避障与无人驾驶

      三维重构在这两种领域的应用主要在于三维地图的构建,具有代表性的是SLAM中的建图、以及视觉测距等。(SLAM,即定位与地图构建。问题可描述为:将一个机器人放入未知环境中的未知位置,是否有办法让机器人一边移动一边逐步绘制出此环境完全的地图,所谓完全的地图(a consistent map)是指不受障碍行进到房间可进入的每个角落。)其中机器人的主体由无人机、无人汽车、移动机器人等替换。

      其中一种解决无人机避障问题的方法是对无人机到障碍物的距离进行测量。 其中可运用视觉避障方法从二维的图像中获取三维信息,获得障碍物的深度图像。在避障的过程中,利用SLAM获得了场景模型,在机载计算机里用算法去搜索优化的避障路径。

    三维重建在SLAM中主要是视觉SLAM应用,主要利用外部传感器获知环境信息。其中相机是常用的传感器之一,相机又分为三种,分别为:单目相机、双目相机、RGB-D相机等。

    单目相机对于三维信息的提取效果较差,因此主要使用双目相机与RGB-D相机。其中双目相机则是通过左右眼图像的差异来判断场景中物体的远近,能直接提取完整的特征数据,但是计算量复杂。而RGB-D相机,可同时获取图像彩色信息和深度信息,是一种实用的获取三维信息的工具。视觉传感器很好地利用了丰富的环境信息,实现了从早期二维地图到三维地图的转化,丰富了地图信息,但是在现实环境下还存在很大的鲁棒性和高适应能力技术挑战。

    1.2.2、医学诊断

    医学图像三维重建技术主要应用于面绘制、体绘制、医学三维可视化系统等方面。主要是计算机断层扫描(CT)、核磁共振(MRI)、超声波等医学成像技术的发展,使得感兴趣区域CT、MRI图像的三维重建成为可能。

    利用重建软件,如中国科学院开发的一款用于医学图像数据的分析和处理的软件3DMed,可以处理各种医学图像,包括计算机断层扫描图像,磁共振成像和原始格式图像。以MRI为例,如可以通过研究对获取的MRI图像进行分割、三维建模和显示,可以辅助医生对疾病的判断和制定治疗的方案。与双目视觉重构相比,获取图像的方式较为不同。

    1.2.3、三维测量

    运用在测量的三维重构方法属于非接触式测量方法,比起三坐标测量机等接触式测量方法更能满足各类不同的测量需求。其中三维重构方法又分为主动式测量与被动式测量方法。主动式测量是利用投射结构光等(如编码结构光、散斑等),运用直接三角法进行重构测量。而被动式测量法可基于不同视角的测量法,通过不同角度获取图像,根据视差恢复待测物体的三维信息,其基本原理是双目交会测量。

    双目交会测量使用两台相机从不同角度对待测目标进行拍摄,在相机内、外参数标定基础上,通过对匹配点对进行空间立体交会,获得目标点云,进而进行相关测量。目前图像匹配方法发展成熟,因此双目交会对材质、颜色等物面性质及背景光等环境因素要求较低,适合对大型三维物体如建筑物等的测量。双目交会测量精度主要受匹配精度、基高比、相机标定精度等因素制约,因此测量精度较低,同时难以实现实时测量,重建与测量范围不能覆盖整个待测物体,目前只有在一些特定场合得到应用。

    1.3、三维重构的步骤

    三维重构的步骤主要分为:图像获取、摄像机标定、特征提取、立体匹配、三维重构等方面。

    (1)图像获取

    在进行图像处理之前,要使用摄像机获取三维物体的二维图形。光照条件、相机的几何特性等对后续的图像处理造成很大的影响。其中,摄像机的传感器又分为CMOS与CCD,CCD传感器的成像质量比CMOS的更好,但是成本更高。

    首先要对相机进行二次开发,而后搭建出双目视觉平台,而双目相机的关系又主要分为平行光轴与相交光轴两种,将在之后对它们进行讨论。

    (2)摄像机标定

    通过摄像机标定来建立有效的成像模型,求解出摄像机的内外参数,这样就可以结合图像的匹配结果得到空间中的三维点坐标,从而达到进行三维重建的目的。目前最常使用棋盘格,利用张氏标定法进行标定。

    (3)特征提取

    特征主要包括特征点、特征线和区域。大多数情况下都是以特征点为匹配基元,特征点以何种形式提取与用何种匹配策略紧密联系。因此在进行特征点的提取时需要先确定用哪种匹配方法。匹配算法又包括全局立体匹配算法与局部立体匹配算法。之后会进行讨论。

    (4)立体匹配

    立体匹配是指根据所提取的特征来建立图像对之间的一种对应关系,也就是将同一物理空间点在两幅不同图像中的成像点进行一一对应起来。在进行匹配时要注意场景中一些因素的干扰,比如光照条件、噪声干扰、景物几何形状畸变、表面物理特性以及摄像机机特性等诸多变化因素。

    (5)三维重建

    有了比较精确的匹配结果,结合摄像机标定的内外参数,就可以恢复出三维场景信息。由于三维重建精度受匹配精度,摄像机的内外参数误差等因素的影响,因此首先需要做好前面几个步骤的工作,使得各个环节的精度高,误差小,这样才能设计出一个比较精确的立体视觉系统。

    之后可以对重构输出的点云数据进行后处理,如去噪处理、表面重建等。

    文章来源:网络收集

    4b743a7c4c859cc4224087cc58bc79e5.png

    申明:感谢原创作者的辛勤付出。本号转载的文章均会在文中注明,若遇到版权问题请联系我们处理。

     faa6abe896fcfb00bbde949ec15cd883.png

    ----与智者为伍 为创新赋能----

    【说明】欢迎企业和个人洽谈合作,投稿发文。欢迎联系我们 诚招运营合伙人 ,对新媒体感兴趣,对光电产业和行业感兴趣。非常有意者通过以下方式联我们!条件待遇面谈 fa9898844f346982606d87cea1e5d6f0.png 806ba21394bd47559af7a784955112aa.png 投稿丨合作丨咨询

    联系邮箱:598204634@qq.com

    QQ:598204634

    微信:13998212910

    51831a5ee030a4448c8e18b032546866.png

    欢迎关注以下公众号了解光电测控相关

    6d9d3deb84f2487a5b8996469c07e7a3.png

    展开全文
  • 0.绪论这篇文章主要为了研究双目立体视觉的最终目标——三维重建,系统的介绍了三维重建的整体步骤。双目立体视觉的整体流程包括:图像获取,摄像机标定,特征提取(稠密匹配中这一步可以省略),立体匹配,三维重建。...
  • 双目三维重建和误差估计

    千次阅读 2018-08-18 10:09:34
    双目测距的精度和基线长度(两台相机之间的距离)有关,两台相机布放的距离越远,测距精度越高。 但问题是:往往在实际应用中,相机的布放空间是有限的,最多也只有几米或几十米的基线长度,这就导致双目测距在远...
  • 记录一下深度学习进行双目三维重建看过的网络 持续更新(时不时更新) 数据集: SceneFlow KITTI 与三维有关的数据集: TanksAndTemples 一大堆Github总结的数据集 Github大佬的笔记 -----------------------------...
  • 双目视觉三维重建框架

    万次阅读 多人点赞 2018-01-23 14:08:52
    玉米竭力用轻松具体的描述来讲述双目三维重建中的一些数学问题。希望这样的方式让大家以一个轻松的心态阅读玉米的《计算机视觉学习笔记》双目视觉数学架构系列博客。这个系列博客旨在捋顺一下已标定的双目视觉中的...
  • 立体视觉、重建
  • 双目相机标定的原理可太多...拍图时要注意:1)双目标定拍照时,要保证标定板精度高,我用的12×9,每个格25mm。 2)标定板图像占界面分之一左右,太小绝对不行,标定板要整个出现在图像里。 3)单目标定的图要...
  • 详细介绍了基于数字散斑相关技术的三维重建方法...通过设计不同尺寸颗粒的散斑图案和采用不同大小的相关计算窗口, 分别对平面陶瓷板和陶瓷标准球进行了三维重建精度分析。实验结果表明, 二值散斑能获得更高的重建精度。
  • 三维重建是指根据基于一个视图或者多个视图所获得的物体或者场景的图像重建三维模型的过程。由于单视图的信息很单一,因此三维重建需要更复杂的算法和过程。相比之下,多视图的三维重建(模仿人类观察世界的方式)就...
  • 双目立体视觉测量系统是工业测量中的重要手段,三维重建双目立体视觉测量体统中非常重要的一环。基于视差原理的传统三维...结果表明该方法可达到传统视觉三维重建方法的测量精度,并可有效判断匹配点是否为误匹配点。
  • 【技术流派】教你提高双目立体视觉系统的精度

    万次阅读 多人点赞 2020-07-04 17:09:03
    双目立体视觉系统,不谈精度几许,未免显得业余!
  • 物体形貌三维重建技术是视觉测量领域研究的热点,当前使用工业相机进行双目视觉测量的方法存在设备费用昂贵、操作过程复杂及需专业软件和相关技术人员进行后期处理等不足。针对这些方面的不足,研究了基于智能手机...
  • 三维重建算法综述|传统+深度学习

    千次阅读 多人点赞 2020-02-22 13:14:08
    来源:基于深度学习的三维重建算法综述 00 前言 01 基于传统多视图几何的三维重建算法 1.1 主动式 (1)结构光 (2)TOF 激光飞行时间法 (3)三角测距法 1.2 被动式 (1)单目视觉 (2)双目/多目视觉 1.3 基于...
  • 三维重建-双目立体视觉原理

    千次阅读 2019-12-11 16:12:13
    三维重建是个跨多学科的应用领域,围绕不同的尺度大小、不同速度要求、不同精度要求、不同硬件成本等要求发展出了各种各样的技术方案。在这个应用领域,充分体现了,没有最好的设备,只有最合适的方案。在本系列文章...
  • 双目测距与三维重建的OpenCV…

    千次阅读 2017-08-05 17:44:40
    原文地址:双目测距与三维重建的OpenCV实现问题集锦(一)图像获取与单目定标【转】作者:乐在平淡中 双目测距与三维重建的OpenCV实现问题集锦(一)图像获取与单目定标 双目测距的基本原理 1.jpg (37.04 KiB) 被...
  • 双目立体视觉系统的精度分析,分析了双目立体视觉系统的误差来源及处理方法
  • 谈到双目相机测距,我们首先要先了解测距的原理:如下图所示,这是双目...在OpenCV中,焦距f的量纲是像素点,基线b的量纲由定标棋盘格的实际尺寸和用户输入值确定,一般总是设成毫米,当然为了精度提高也可以设置为0...
  • 1.修改为 VS2015 Debug win32 版本,支持利用特征点和 OpenCV 立体匹配算法进行进行三维重建及显示,相关代码需要自行修改,代码中添加了修改注释。 2.工程依赖库为 OpenCV2.4.8,内部已完成 OpenCV 相关配置。无论...
  • 测量,三维重建 示例程序 参考文献 关于双目视觉的一些总结 笔者2013年进入吉林大学软件学院,2014年开始写自己的第一个完整的程序,期间受到过无数前辈的帮助,正是这个程序的完成给了我极大的信心,也让我喜欢上...
  • Evision双目视觉关于双目视觉的一些总结相机模型标定视差算法:立体匹配重投影:测量,三维重建,重投影约束三维重建示例程序 关于双目视觉的一些总结 笔者2013年进入吉林大学软件学院,2014年开始写自己的第一个完整的...
  • 双目三维成像

    千次阅读 2020-10-09 17:53:17
    自然界本来就是一个三维空间,自然界所有景物都是立体的。人眼观看自然界景物时,不仅能看到景物的高度与宽度,而且还能分辨出景物的深度,即人眼具有立体视觉。迄今人类使用的电视系统绝大部分仅能显示平面图像,故...
  • 提出一种基于网格点投影灰度相似性的双目立体视觉的三维重建新方法。首先将被测物体所在的世界坐标系划分成间距相等的矩形网格, 将网格节点作为潜在的物点投影到左右图像坐标系上, 然后根据不同深度的空间点在两幅...
  • 三维重建三维重建基础

    千次阅读 2019-04-10 13:55:24
    三维重建技术通过深度数据获取、预处理、点云配准与融合、生成表面等过程,把真实场景刻画成符合计算机逻辑表达的数学模型。这种模型可以对如文物保护、游戏开发、建筑设计、临床医学等研究起到辅助的作用。 1.1 ...
  • 聊聊三维重建-双目立体视觉原理

    千次阅读 2019-12-11 12:00:00
    原文首发于微信公众号「3D视觉工坊」——聊聊三维重建-双目立体视觉原理 作首:Tengfei Jiang https://zhuanlan.zhihu.com/p/81016834 本文已由原作者授权,不得擅自二次转载 前言 三维重建是个跨多学科的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,024
精华内容 809
关键字:

提高双目三维重建精度

友情链接: libdvbpsi3-0.1.4.tar.gz