SLAM-同时定位与地图构建

同时定位与地图构建(英语:Simultaneous localization and mapping,一般直接称SLAM)是一种概念:希望机器人从未知环境的未知地点出发,在运动过程中通过重复观测到的地图特征(比如,墙角,柱子等)定位自身位置和姿态,再根据自身位置增量式的构建地图,从而达到同时定位和地图构建的目的。

一、SLAM的典型应用领域

机器人定位导航领域:地图建模。SLAM可以辅助机器人执行路径规划、自主探索、导航等任务。国内的科沃斯、塔米以及最新面世的岚豹扫地机器人都可以通过用SLAM算法结合激光雷达或者摄像头的方法,让扫地机高效绘制室内地图,智能分析和规划扫地环境,从而成功让自己步入了智能导航的阵列。国内思岚科技(SLAMTEC)为这方面技术的主要提供商,SLAMTEC的命名就是取自SLAM的谐音,其主要业务就是研究服务机器人自主定位导航的解决方案。目前思岚科技已经让关键的二维激光雷达部件售价降至百元,这在一定程度上无疑进一步拓展了SLAM技术的应用前景。

VR/AR方面:辅助增强视觉效果。SLAM技术能够构建视觉效果更为真实的地图,从而针对当前视角渲染虚拟物体的叠加效果,使之更真实没有违和感。VR/AR代表性产品中微软Hololens、谷歌ProjectTango以及MagicLeap都应用了SLAM作为视觉增强手段。

无人机领域:地图建模。SLAM可以快速构建局部3D地图,并与地理信息系统(GIS)、视觉对象识别技术相结合,可以辅助无人机识别路障并自动避障规划路径,曾经刷爆美国朋友圈的Hovercamera无人机,就应用到了SLAM技术。

无人驾驶领域:视觉里程计。SLAM技术可以提供视觉里程计功能,并与GPS等其他定位方式相融合,从而满足无人驾驶精准定位的需求。例如,应用了基于激光雷达技术Google无人驾驶车以及牛津大学MobileRoboticsGroup11年改装的无人驾驶汽车野猫(Wildcat)均已成功路测。

二、SLAM框架

SLAM系统框架如图所示,一般分为五个模块,包括传感器数据、视觉里程计、后端、建图及回环检测。

传感器数据:主要用于采集实际环境中的各类型原始数据。包括激光扫描数据、视频图像数据、点云数据等。

视觉里程计:主要用于不同时刻间移动目标相对位置的估算。包括特征匹配、直接配准等算法的应用。

后端:主要用于优化视觉里程计带来的累计误差。包括滤波器、图优化等算法应用。

建图:用于三维地图构建。

回环检测:主要用于空间累积误差消除

其工作流程大致为:

传感器读取数据后,视觉里程计估计两个时刻的相对运动(Ego-motion),后端处理视觉里程计估计结果的累积误差,建图则根据前端与后端得到的运动轨迹来建立地图,回环检测考虑了同一场景不同时刻的图像,提供了空间上约束来消除累积误差。

三、SLAM分类(基于传感器的SLAM分类)

目前用在SLAM上的传感器主要分为这两类,一种是基于激光雷达的激光SLAM(Lidar SLAM)和基于视觉的VSLAM(Visual SLAM)。

1.激光SLAM

激光SLAM采用2D或3D激光雷达(也叫单线或多线激光雷达),2D激光雷达一般用于室内机器人上(如扫地机器人),而3D激光雷达一般使用于无人驾驶领域。激光雷达的出现和普及使得测量更快更准,信息更丰富。激光雷达采集到的物体信息呈现出一系列分散的、具有准确角度和距离信息的点,被称为点云。通常,激光SLAM系统通过对不同时刻两片点云的匹配与比对,计算激光雷达相对运动的距离和姿态的改变,也就完成了对机器人自身的定位。

激光雷达测距比较准确,误差模型简单,在强光直射以外的环境中运行稳定,点云的处理也比较容易。同时,点云信息本身包含直接的几何关系,使得机器人的路径规划和导航变得直观。激光SLAM理论研究也相对成熟,落地产品更丰富。

对比相机、ToF 和其他传感器,激光可以使精确度大大提高,常用于自动驾驶汽车和无人机等高速移动运载设备的相关应用。激光传感器的输出值一般是二维 (x, y) 或三维 (x, y, z) 点云数据。激光传感器点云提供了高精确度距离测度数据,特别适用于 SLAM 建图。一般来说,首先通过点云匹配来连续估计移动。然后,使用计算得出的移动数据(移动距离)进行车辆定位。对于激光点云匹配,会使用迭代最近点 (ICP) 和正态分布变换 (NDT) 等配准算法。二维或三维点云地图可以用栅格地图或体素地图表示。

但就密度而言,点云不及图像精细,因此并不总能提供充足的特征来进行匹配。例如,在障碍物较少的地方,将难以进行点云匹配,因此可能导致跟丢车辆。此外,点云匹配通常需要高处理能力,因此必须优化流程来提高速度。鉴于存在这些挑战,自动驾驶汽车定位可能需要融合轮式测距、全球导航卫星系统 (GNSS) 和 IMU 数据等其他测量结果。仓储机器人等应用场景通常采用二维激光雷达 SLAM,而三维激光雷达点云 SLAM 则可用于无人机和自动驾驶。

2.视觉SLAM

眼睛是人类获取外界信息的主要来源。视觉SLAM也具有类似特点,它可以从环境中获取海量的、富于冗余的纹理信息,拥有超强的场景辨识能力。早期的视觉SLAM基于滤波理论,其非线性的误差模型和巨大的计算量成为了它实用落地的障碍。近年来,随着具有稀疏性的非线性优化理论(Bundle Adjustment)以及相机技术、计算性能的进步,实时运行的视觉SLAM已经不再是梦想。

视觉SLAM的优点是它所利用的丰富纹理信息。例如两块尺寸相同内容却不同的广告牌,基于点云的激光SLAM算法无法区别他们,而视觉则可以轻易分辨。这带来了重定位、场景分类上无可比拟的巨大优势。同时,视觉信息可以较为容易的被用来跟踪和预测场景中的动态目标,如行人、车辆等,对于在复杂动态场景中的应用这是至关重要的。

通过对比我们发现,激光SLAM和视觉SLAM各擅胜场,单独使用都有其局限性,而融合使用则可能具有巨大的取长补短的潜力。例如,视觉在纹理丰富的动态环境中稳定工作,并能为激光SLAM提供非常准确的点云匹配,而激光雷达提供的精确方向和距离信息在正确匹配的点云上会发挥更大的威力。而在光照严重不足或纹理缺失的环境中,激光SLAM的定位工作使得视觉可以借助不多的信息进行场景记录。

近年来,SLAM导航技术已取得了很大的发展,它将赋予机器人和其他智能体前所未有的行动能力,而激光SLAM与视觉SLAM必将在相互竞争和融合中发展,使机器人从实验室和展厅中走出来,做到真正的服务于人类。

顾名思义,视觉 SLAM(又称 vSLAM)使用从相机和其他图像传感器采集的图像。视觉 SLAM 可以使用普通相机(广角、鱼眼和球形相机)、复眼相机(立体相机和多相机)和 RGB-D 相机(深度相机和 ToF 相机)。

视觉 SLAM 所需的相机价格相对低廉,因此实现成本较低。此外,相机可以提供大量信息,因此还可以用来检测路标(即之前测量过的位置)。路标检测还可以与基于图的优化结合使用,这有助于灵活实现 SLAM。

使用单个相机作为唯一传感器的 vSLAM 称为单目 SLAM,此时难以定义深度。这个问题可以通过以下方式解决:检测待定位图像中的 AR 标记、棋盘格或其他已知目标,或者将相机信息与其他传感器信息融合,例如测量速度和方向等物理量的惯性测量单元 (IMU) 信息。vSLAM 相关的技术包括运动重建 (SfM)、视觉测距和捆绑调整。

视觉 SLAM 算法可以大致分为两类。稀疏方法:匹配图像的特征点并使用 PTAM 和 ORB-SLAM 等算法。稠密方法:使用图像的总体亮度以及 DTAM、LSD-SLAM、DSO 和 SVO 等算法。

SIFT–尺度不变特征变换

https://docs.opencv.org/4.1.2/da/df5/tutorial_py_sift_intro.html

SIFT,即尺度不变特征变换(Scale-invariant feature transform,SIFT),是用于图像处理领域的一种描述。这种描述具有尺度不变性,可在图像中检测出关键点,是一种局部特征描述子。

尺度不变特征变换 (Scale-invariant feature transform, SIFT) 是计算机视觉中一种检测、描述和匹配图像局部特征点的方法,通过在不同的尺度空间中检测极值点或特征点 (Conrner Point, Interest Point) ,提取出其位置、尺度和旋转不变量,并生成特征描述子,最后用于图像的特征点匹配。SIFT 特征凭借其良好的性能广泛应用于运动跟踪 (Motion tracking) 、图像拼接 (Automatic mosaicing) 、3D 重建 (3D reconstruction) 、移动机器人导航 (Mobile robot navigation) 以及目标识别 (Object Recognition) 等领域。

SIFT特征的特点

SIFT是一种检测、描述、匹配图像局部特征点的算法,通过在尺度空间中检测极值点,提取位置、尺度、旋转不变量,并抽象成特征向量加以描述,最后用于图像特征点的匹配。SIFT特征对灰度、对比度变换、旋转、尺度缩放等保持不变性,对视角变化、仿射变化、噪声也具有一定的鲁棒性。但其实时性不高,对边缘光滑的目标无法准确提取特征点。

SIFT算法主要包括四个步骤。

1. 尺度空间极值检测

从上图可以明显看出,我们不能使用相同的窗口来检测具有不同比例的关键点。即便小拐角可以。但是要检测更大的拐角,我们将需要更大的窗口。为此,使用了比例空间滤波。在其中,找到具有各种σ值的图像的高斯拉普拉斯算子。LoG用作斑点检测器,可检测由于σ的变化而导致的各种大小的斑点。简而言之,σ用作缩放参数。例如,在上图中,低σ的高斯核对于较小的拐角给出较高的值,而高σ的高斯核对于较大的拐角而言非常合适。因此,我们可以找到整个尺度和空间上的局部最大值,这给了我们(x,y,σ)值的列表,这意味着在(x,y在σ尺度上有一个潜在的关键点。

但是这种LoG代价昂贵,因此SIFT算法使用的是高斯差值,它是LoG的近似值。高斯差是作为具有两个不同σ的图像的高斯模糊差而获得的,设为σ和kσ。此过程是针对高斯金字塔中图像的不同八度完成的。如下图所示:

一旦找到该DoG,便会在图像上搜索比例和空间上的局部极值。例如,将图像中的一个像素与其8个相邻像素以及下一个比例的9个像素和前一个比例的9个像素进行比较。如果是局部极值,则可能是关键点。从根本上说,关键点是最好的代表。如下图所示:

对于不同的参数,本文给出了一些经验数据,可以概括为:octaves=4,缩放尺度=5,初始σ=1.6,k=√2等作为最佳值。

2. 关键点定位

一旦找到潜在的关键点位置,就必须对其进行优化以获取更准确的结果。他们使用了标度空间的泰勒级数展开来获得更精确的极值位置,如果该极值处的强度小于阈值(根据论文为0.03),则将其拒绝。在OpenCV DoG中,此阈值称为**ContrastThreshold**,它对边缘的响应较高,因此也需要删除边缘。

为此,使用类似于哈里斯拐角检测器的概念。他们使用2×2的Hessian矩阵(H)计算主曲率。从哈里斯拐角检测器我们知道,对于边缘,一个特征值大于另一个特征值。因此,这里他们使用了一个简单的函数。

如果该比率大于一个阈值(在OpenCV中称为**edgeThreshold**),则该关键点将被丢弃。论文上写的值为10。

因此,它消除了任何低对比度的关键点和边缘关键点,剩下的就是很可能的目标点。

3. 方向分配

现在,将方向分配给每个关键点,以实现图像旋转的不变性。根据比例在关键点位置附近采取邻域,并在该区域中计算梯度大小和方向。创建了一个具有36个覆盖360度的bin的方向直方图(通过梯度幅度和σ等于关键点比例的1.5的高斯加权圆窗加权)。提取直方图中的最高峰,并且将其超过80%的任何峰也视为计算方向。它创建的位置和比例相同但方向不同的关键点。它有助于匹配的稳定性。

4. 关键点描述

现在创建了关键点描述符。在关键点周围采用了16×16的邻域。它分为16个4×4大小的子块。对于每个子块,创建8 bin方向直方图。因此共有128个bin值可用。它被表示为形成关键点描述符的向量。除此之外,还采取了几种措施来实现针对照明变化,旋转等的鲁棒性。

5. 关键点匹配

通过识别两个图像的最近邻,可以匹配两个图像之间的关键点。但是在某些情况下,第二个最接近的匹配可能非常接近第一个。它可能是由于噪音或其他原因而发生的。在那种情况下,采用最接近距离与第二最接近距离之比。如果大于0.8,将被拒绝。根据论文,它可以消除大约90%的错误匹配,而仅丢弃5%的正确匹配。 因此,这是SIFT算法的总结。有关更多详细信息和理解,强烈建议阅读原始论文。记住一件事,该算法已申请专利。所以这个算法包含在opencv contrib repo中.

OpenCV中的SIFT

现在,让我们来看一下OpenCV中可用的SIFT功能。让我们从关键点检测开始并进行绘制。首先,我们必须构造一个SIFT对象。我们可以将不同的参数传递给它,这些参数是可选的,它们在docs中已得到很好的解释。

import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY)
sift = cv.xfeatures2d.SIFT_create()
kp = sift.detect(gray,None)
img=cv.drawKeypoints(gray,kp,img)
cv.imwrite('sift_keypoints.jpg',img)

sift.detect()函数在图像中找到关键点。如果只想搜索图像的一部分,则可以通过掩码。每个关键点是一个特殊的结构,具有许多属性,例如其(x,y)坐标,有意义的邻域的大小,指定其方向的角度,指定关键点强度的响应等。

OpenCV还提供**cv.drawKeyPoints**()函数,该函数在关键点的位置绘制小圆圈。 如果将标志**cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS**传递给它,它将绘制一个具有关键点大小的圆,甚至会显示其方向。 请参见以下示例。

img=cv.drawKeypoints(gray,kp,img,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) cv.imwrite('sift_keypoints.jpg',img)

查看下面的结果: 

现在要计算描述符,OpenCV提供了两种方法。 1. 由于已经找到关键点,因此可以调用**sift.compute**(),该函数根据我们找到的关键点来计算描述符。例如:kp,des = sift.compute(gray,kp) 2. 如果找不到关键点,则可以使用**sift.detectAndCompute**()函数在单步骤中直接找到关键点和描述符。

我们将看到第二种方法:

sift = cv.xfeatures2d.SIFT_create() 
kp, des = sift.detectAndCompute(gray,None)

这里的kp将是一个关键点列表,而des是一个形状为NumberofKeypoints×128的数字数组。

这样我们得到了关键点,描述符等。

MVS学习–《A Comparison and Evaluation of Multi-View Stereo Reconstruction Algorithms》

1. 前言

Middlebury是计算机视觉和三维中间领域著名的高校,特别是提供了著名的立体匹配benchmark数据库,并不断提供新数据的更新。在MVS领域,也同样提供了经典的benchmark数据库,包含两个物体-Temple和Dino,其中Temple有312张相片,Dino有363张相片,如下图所示。并且每个物体还提供了由激光Lidar测量得到的地面真值(Groud Truth)数据,因此可以用来准确的衡量不同MVS算法的准确性(重建的三维模型与真值的差异)和完整性(有多少真值包含在重建的三维模型中)。

在建立该数据库的过程中,Middlebury的研究团队分类总结了当时(2006年)的state-of-art的算法,提出了算法有效性评价标准。基于该标准,并使用该数据库验证这些算法的有效性,最终形成该文章1。这篇文章是后来几乎每一篇研究MVS算法的文章的必引参考文献,其中对于算法的分类介绍和有效性验证规则十分经典,下面分别进行总结。

2. MVS算法分类

MVS是指Multiview Stereo,具体来说是通过多幅已知拍摄方位信息(外方位元素)的图像来估计目标三维信息的算法,数据基于图像的三维重建中一大类非常重要和实用的算法。文章中提到类似的方法还有双目或者三目立体匹配方法,这一类方法能够获得单一的视差图,但是受限于照片数量和拍摄角度,无法覆盖物体的全部表面。另一类方法是多基线立体重建方法,可以构建稀疏特征点集。

一般来说,MVS可以按照如下6个方面的标准进行分类:

  1. 场景表达方式(scene representation);
  2. 图像一致性计算方法(photo consistency measure);
  3. 可见性模型(visible model);
  4. 在重建时优先考虑的形状约束(shape prior);
  5. 重建算法(reconstruction algorithm);
  6. 初始化条件(initialization requirements)。

下面分别对每个方面进行简单的描述。

2.1 场景表达(Scene Representation)

场景表达是指重建得到的三维场景使用什么样的数学模型进行表达,一般来说有如下4种方式:

  1. 体素(Voxel)
  2. 层次级(level set):记录每个点到某个最近平面的距离
  3. 多边形实体(polygon mesh):这是应该是我们最熟悉的表达方式,也是人工三维建模最常见的数据表达方式
  4. 深度图(depth map):一般基于像方立体匹配算法算法生成的结果就是深度图,每个像素的灰度值代表该像素距离当前图像平面的距离。

2.2 图像一致性计算方法(Photo Consistency Measure)

这部分和双目立体匹配中用到的图像一致性计算方法类似,但是考虑到MVS本身的特殊性,一般来说,MVS中图像一致性计算根据搜索内容的不同分为以下两种方法

1. 基于物方的图像一致性计算方法

通常使用体素表达方法,搜索空间中的每个体素在对应两幅图像中的投影位置的图像一致性,如果该一致性计算值小于某个阈值,则该体素可以认为是代表了真实物体。

2. 基于像方的图像一致性计算方法

根据极线约束,对于一幅图像的某个点,搜索其对应极线上最相似的匹配点(一致性最高),这种方法通常在双目立体视觉中使用。

需要注意的是这两种方法都是基于物体表面为Lambartian的假设,但是也有进一步的研究利用BRDF进行计算,或者考虑物体的阴影,消除物体阴影对于一致性计算的影响。

2.3 可见性模型(Visible Model)

可见性模型是在计算图像一致性时,决定究竟哪些图像和参考图像有共视区域,可以进行图像一致性计算的方法。一般来说,有如下三种模型

  • 几何模型。
  • 准几何模型。
  • 基于粗差(outlier)的模型,通常是将遮挡视为粗差,因为对于一个点来说,在两视中被看到的可能性大于被遮挡的可能性。

2.4 在重建时优先考虑的形状约束(Shape Prior)

由于常见的弱纹理(大范围区域颜色相同或者相近)或者无纹理等原因,导致在匹配是在这些区域无法得到良好的匹配结果,因此需要引入形状约束来近似约束这些区域的可能形状,可以使得最终得到的场景具有某种特殊的性质。这种方法在双目立体匹配的研究中是极为常见的方法,但是在MVS中,由于多幅图像提供更强的约束,较少使用这种方法。常见的形状约束方法如下:

  1. 基于场景重建的技术通常采用“最少平面数”约束,因为过多的多边形面片会使得场景过于破碎。
  2. 基于体素和Space carve的重建方法通常增加“最多平面数”约束,使得表面具有更加丰富的细节。
  3. 在基于像方的匹配方法中,通常添加局部平滑约束:例如双目立体匹配中常见的piece-wise smothness,假设场景中的弱纹理区域是与摄影平面平行的小平面。

2.5 重建算法(Reconstruction Algorithm)

  1. 体素着色算法:从一个volumn中提取一个平面出来
  2. 通过递推的方法展开一个平面:在过程中最小化代价函数(based on voxels, level-set, mesh)
  3. 基于像方的匹配,生成深度图,并对不同图像间的深度图进行融合
  4. 提取特征点,拟合一个面来重建特征

2.6 初始化条件(Initialization Requirements)

  1. 需要图像集(毕竟是基于图像的三维重建,需要尽可能多的多角度拍摄的同一场景的图像)
  2. 几乎所有的算法都要求或者假设待重建三维目标的空间范围或者scene geometry
  3. 基于像方的方法要求最大/最小视差(这一点要求和2类似)

3. MVS算法的评价

文章中提出,对于MVS算法应该从一下两个方面进行评价

1. 准确性

准确性是指重建结果与真值间的差距,一般方法是,对于重建结果中的一个三维空间点,寻找其对应真值中的点,计算其距离,最后统计所有点距离真值的距离。 根据统计结果来评价重建结果的准确性。

2. 完整性

完整性是指有多少真值被包含在重建结果中。一般方法与准确性计算类似,但是是计算真值中的点到重建结果中最近点的距离,统计所有真值点的计算结果来评价重建的完整性。需要注意的是,如果真值中的点距离重建结果中最近点的距离大于某个阈值,则认为是没有找打匹配点,也就是该真值点没有被覆盖。

SFM的重建成果是稀疏三维点云,而MVS可以获得更好的结果

(1)如何理解密集点云的生成原理

  MVS是生成密集点云的方法,事实上,为什么我们在SFM中不能得到密集点云?因为,SFM中我们用来做重建的点是由特征匹配提供的!这些匹配点天生不密集!而使用计算机来进行三维点云重建,我们必须认识到,点云的密集程度是由人为进行编程进行获取的。SFM获得点的方式决定了它不可能直接生成密集点云。

  而MVS则几乎对照片中的每个像素点都进行匹配,几乎重建每一个像素点的三维坐标,这样得到的点的密集程度可以较接近图像为我们展示出的清晰度。

  其实现的理论依据在于,多视图照片间,对于拍摄到的相同的三维几何结构部分,存在极线几何约束。

描述这种几何约束:

  想象,对于在两张图片中的同一个点。现在回到拍摄照片的那一刻,在三维世界中,存在一条光线从照片上这一点,同时穿过拍摄这张照片的相机的成像中心点,最后会到达空间中一个三维点,这个三维点同时也会在另一张照片中以同样的方式投影。

  这个过程这样看来,很普通,就如同普通的相机投影而已。但是因为两张图片的原因,他们之间存在联系,这种联系的证明超过了能力范围,但是我们只需要知道,此种情况下,两张照片天然存在了一种约束。

  X表示空间中的一点,x1、x2为X在两张图片中的同一点。由于天然的约束,已知x1,想要在另一张图片中找到x2,可以在直线L2上进行一维寻找。  MVS主要做的就是如何最佳搜索匹配不同相片的同一个点。

2)初步探究MVS中的点匹配方法

  在有了约束的基础上,接下来就是在图片上的一条线上进行探测,寻找两张图片上的同一点。主要方法为逐像素判断,两个照片上的点是否是同一点——为此提出图像点间的“一致性判定函数”

   π (p)是使得点p投影到照片上一点的函数, Ω(x) 函数定义了一个点x周围的区域,I(x) 函数代表了照片区域的强度特征,ρ(f, g) 是用来比较两个向量之间的相似程度的

  ρ函数和Ω函数的具体选择决定这个”一致性判别“的准确度。这个函数的具体实现,由编程实现。

  

参考文献


  1. Seitz, S. M., Curless, B., Diebel, J., Scharstein, D., & Szeliski, R. (n.d.). A Comparison and Evaluation of Multi-View Stereo Reconstruction Algorithms. In 2006 IEEE Computer Society Conference on Computer Vision and Pattern Recognition – Volume 1 (CVPR’06) (Vol. 1, pp. 519–528). IEEE. https://doi.org/10.1109/CVPR.2006.19

基于SfM(Structure from motion)的三维重建

SfM(Structure from motion) 是一种三维重建的方法,用于从motion中实现3D重建。也就是从时间系列的2D图像中推算3D信息。

人的大脑可以从动的物体中取得其三维的信息,是因为大脑在动的2D图像中找到了匹配的地方,即Corresponding area (points)。然后通过匹配点之间的视差得到相对的深度信息,在这一点上,原理和基于Stereo的三维重建相同。

SfM的输入是一段motion或者一时间系列的2D图群,如下图所示 [1],这里不需要任何相机的信息。然后通过2D图之间的匹配可以推断出相机的各项参数Corresponding points可以用SIFT,SURF来匹配,也可以用最新的AKAZE(SIFT的改进版,2010)来匹配。而Corresponding points的跟踪则可以用Lucas-Kanede的Optical Flow来完成。

在SfM中,误匹配会造成较大的Error,所以要对匹配进行筛选,目前流行的方法是RANSAC(Random Sample Consensus)。2D的误匹配点可以应用3D的Geometric特征来进行排除。

Bundler [2] 就是一种SfM的方法,Bundler使用了基于SIFT的匹配算法,并且对匹配进行了过滤去噪处理。下图显示了一组测试数据(一时间系列的2D图群):

将这些图片保存到同一个文件夹,然后将文件夹的目录输入,Bundler会自行处理,之后会得到一群Corresponding points。比如其中的一组Corresponding points (A1,A2,A3,…Am),其实他们来自同一个三维点A的Projection。所以通过这些点可以重建三维点A。然后将很多组Corresponding points 进行重建,则得到了一群三维的点,这里称为3D点阵。

然后3D点阵可以通过MeshLab(开源Source,支持Windows/Linux/Mac)来重建稀疏的Mesh。也可以通过PMVS(Patch-based Multi-view Stereo)来重建Dense的Mesh[3]。

[1] 満上育久 ”Structure from Motion – Osaka University“ 映像情报メディア学会志 Vol.65, No.4, pp.479-482, 2011.

[2] N.Snavely, S.M. Seitz, R.Szeliski, “Modeling the World from Internet Photo Collections”, International Journal of Computer Vision, vol.80, no.2, 2008.

[3] Y. Furukawa, J.Ponce, “Accurate, Dense and Robust Multi-view Stereopsis” IEEE Transactions on Pattern Analysis and Machine Intelligence, 2009.

补充:通过视差d求解深度图:

同一水平线上的两个照相机拍摄到的照片是服从以下物理规律的:

这种思路最先应用于使用单张图片生成新视角问题:DeepStereo 和 Deep3d之中, 在传统的视角生成问题之中,首先会利用两张图(或多张)求取图片之间的视差d,其次通过得到的视差(相当于三维场景)来生成新视角

通过同一水平线的两个视图获得深度图

神奇的达尔文进化定律告诉我们,单个眼睛的自然界生物大都灭绝了。自然界的大多物种都是和人一样,需要两只眼睛来做三维空间定位。那为什么需要两只眼睛呢?

因为一只眼睛看到的图像是二维的,二维的信息是无法用来表示三维的空间的,如上图所示,虽然处于同一水平面上的照相机L,R拍摄了同一个物体,两者之间产生的图片是不同的。并且这种不同是不能通过平移生成的图片所消除的。离照相机近的物体偏离的位置比较大,离照相机远的物体偏离的比较少。这种差异性的存在就是三维空间带来的。(这部分请参考双眼可以测距和建立立体环境,双摄像头可以吗?)。同时同一水平线上的两个照相机拍摄到的照片是服从以下物理规律的:

这种思路最先应用于使用单张图片生成新视角问题:DeepStereo 和 Deep3d之中, 在传统的视角生成问题之中,首先会利用两张图(或多张)求取图片之间的视差d,其次通过得到的视差(相当于三维场景)来生成新视角

MVSNeRF:多视角立体图像的快速广义辐射场重建

MVSNeRF: Fast Generalizable Radiance Field Reconstruction
from Multi-View Stereo

https://github.com/apchenstu/mvsnerf

提出了一种新的神经渲染方法neural rendering approach  MVSNeRF,它可以有效地重建用于视图合成的神经辐射场。与先前关于神经辐射场的工作不同,这些工作考虑对密集捕获的图像进行逐场景优化,我们提出了一种通用的深度神经网络,该网络可以通过快速网络推理仅从三个附近的输入视图重建辐射场。我们的方法利用平面扫描成本体plane-swept cost volumes(广泛用于多视图立体multi-view stereo)进行几何感知场景推理,并将其与基于物理的体渲染相结合进行神经辐射场重建。我们在DTU数据集中的真实对象上训练我们的网络,并在三个不同的数据集上测试它以评估它的有效性和可推广性generalizability我们的方法可以跨场景(甚至室内场景,完全不同于我们的对象训练场景)进行推广generalize across scenes,并仅使用三幅输入图像生成逼真的视图合成结果,明显优于目前的广义辐射场重建generalizable radiance field reconstruction工作。此外,如果捕捉到密集图像dense images are captured,我们估计的辐射场表示可以容易地微调easily fine-tuned;这导致快速的逐场景重建fast per-scene reconstruction,比NeRF具有更高的渲染质量和更少的优化时间。

我们利用最近在深度多视图立体(MVS)deep multi- view stereo (MVS)上的成功[50,18,10]。这一系列工作可以通过对成本体积应用3D卷积applying 3D convolutions on cost volumes来训练用于3D重建任务的可概括的神经网络。与[50]类似,我们通过将来自附近输入视图的2D图像特征(由2D CNN推断)扭曲warping到参考视图的平截头体中的扫描平面上sweeping planes in the reference view’s frustrum,在输入参考视图处构建成本体。不像MVS方法[50,10]仅在这样的成本体积上进行深度推断depth inference,我们的网络推理关于场景几何形状和外观reasons about both scene geometry and appearance,并输出神经辐射场(见图2),实现视图合成。具体来说,利用3D CNN,我们重建(从成本体)神经场景编码体neural scene encoding volume,其由 编码关于局部场景几何形状和外观的信息的每个体素神经特征per-voxel neural features 组成。然后,我们利用多层感知器(MLP)在编码体积encoding volume内使用三线性插值神经特征tri-inearly interpolated neural features来解码任意连续位置处的体积密度volume density和辐射度radiance。本质上,编码体是辐射场的局部神经表示;一旦估计,该体积可以直接用于(丢弃3D CNN)通过可微分射线行进differentiable ray marching(如在[34]中)的最终渲染。

我们的方法结合了两个世界的优点,基于学习的MVS和神经渲染。与现有的MVS方法相比,我们实现了可微分神经渲染differentiable neural rendering,允许在没有3D监督和推理时间优化的情况下进行训练,以进一步提高质量。与现有的神经渲染作品相比,我们的类MVS架构可以自然地进行跨视图对应推理cross-view correspondence reasoning,有利于推广到未知的测试场景,也导致更好的神经场景重建和渲染。因此,我们的方法可以明显优于最近的并行可概括NeRF工作concurrent generalizable NeRF work[54,46],该工作主要考虑2D图像特征,而没有明确的几何感知3D结构(参见表。1和图4)。 我们证明,仅使用三个输入图像,我们从DTU数据集训练的网络可以在测试DTU场景上合成照片级逼真的图像,甚至可以在具有非常不同的场景分布的其他数据集上生成合理的结果。此外,我们估计的三个图像辐射场(神经编码体积)可以在新的测试场景上进一步轻松优化,以在捕获更多图像的情况下改善神经重建,从而产生与每个场景过拟合NeRF相当甚至更好的照片级逼真结果,尽管我们的优化时间比NeRF少得多。

该方法利用深度MVS的成功,在成本体上应用3D卷积来训练用于3D重建任务的可泛化神经网络。与MVS方法不同的是,MVS方法仅对这样的成本体进行深度推断,而该网络对场景几何和外观进行推理,并输出神经辐射场,从而实现视图合成。具体而言,利用3D CNN,重建(从成本体)神经场景编码体,由编码局部场景几何和外观信息的体素神经特征组成。然后,多层感知器(MLP)在编码体内用三线性插值的神经特征对任意连续位置处的体密度和辐射度进行解码。本质上,编码体是辐射场的局部神经表征;其一旦估计,可直接用于(丢弃3D CNN)可微分光线行进(ray-marching)进行最终渲染。

与现有的MVS方法相比,MVSNeRF启用可微分神经渲染,在无3D监督的情况下进行训练,并优化推断时间,以进一步提高质量。与现有的神经渲染方法相比,类似MVS的体系结构自然能够进行跨视图的对应推理,有助于对未见测试场景进行泛化,引向更好的神经场景重建和渲染。

如图1是MVSNeRF的概览:(a)基于摄像头参数,首先将2D图像特征warp(单应变换)到一个平面扫描(plane sweep)上,构建成本体;这种基于方差的成本体编码了不同输入视图之间的图像外观变化,解释了由场景几何和视图相关明暗效果引起的外观变化;(b)然后,用3D CNN重建逐体素神经特征的一个神经编码体;3D CNN 是一个3D UNet,可以有效地推断和传播场景外观信息,从而产生有意义的场景编码体;注:该编码体是无监督预测的,并在端到端训练中用体渲染进行推断;另外,还将原图像像素合并到下一个体回归阶段,这样可恢复下采样丢失的高频;(c)用MLP,通过编码体插值的特征,在任意位置回归体密度和RGB辐射度,这些体属性由可微分光线行进做最终的渲染。

SLAM、实时三维重建、SfM、多视角立体视觉MVS

SLAM(Simultaneous Localization And Mapping) 同时定位与地图构建

SLAM是Simultaneous Location and Mapping,同时定位与地图构建。是指搭载特定传感器的主体,在没有环境先验信息的情况下,于运动过程中建立环境的模型,同时估计自己的运动。
目的是解决自主机器人“定位”和“建图”两个问题。同时要求能够实时地,没有先验知识地进行。

  • 一般假设相机的内参已知;
  • 实时处理;
  • 以定位为主要目标
  • 输入数据包括RGB、RGB-D、激光、IMU等
  • 输出稀疏或半稠密地图
  • 机器人和计算机视觉研究领域
  • 典型软件:x-SLAM

实时三维重建

  • 一般假设相机的内参已知;
  • 实时处理;
  • 以建图为主体
  • 输入数据以RGB-D图像为主
  • 输出稠密地图
  • 计算机视觉和计算机图形研究领域
  • 典型软件:xFusion

SfM(Structure from Motion) 运动恢复结构

  • 估计相机内参;
  • 不实时处理;
  • 输入数据以RGB图像为主
  • 输出稠密地图
  • 计算机视觉和计算机图形研究领域
  • 典型软件:Agisoft PhotoScan、Agisoft Metashape、COLMAP

多视角立体视觉MVS

多视角立体视觉(Multiple View Stereo,MVS)是对立体视觉的推广,能够在多个视角(从外向里)观察和获取景物的图像,并以此完成匹配和深度估计。某种意义上讲,SLAM/SFM其实和MVS是类似的,只是前者是摄像头运动,后者是多个摄像头视角。也可以说,前者可以在环境里面“穿行”,而后者更像在环境外“旁观”。

  • 收集图像;
  • 针对每个图像计算相机参数;
  • 从图像集和相应的摄像机参数重建场景的3D几何图形;
  • 可选择地重建场景的形状和纹理颜色。

共同点

都需要估计和优化相机的位姿

基础:三维运动、相机模型、非线性优化

python 和 c++ 联合编译—ctypes库

Python 使用 ctypes 调用 C/C++ DLL 动态链接库

Python ctypes模块优点与适用场景

ctypes 有以下优点:

  • Python内建,不需要单独安装
  • 可以直接调用二进制的动态链接库,在Python一侧,不需要了解 c/c++ dll 内部的工作方式
  • 对C/C++与Python基本类型的相互映射有良好的支持。

ctypes 在下列场景可以发挥较大作用

  • 运算量大的操作可以写成 C/C++ dll, python 通过 ctypes 来调用, 大幅提升Python代码性能。
  • python可以直接使用 C/C++各类资源,如boost库等。
  • 第3方软件或硬件提供的SDK库,通过ctypes来实现调用对接。 理论上C/C++的库都可以对接,这比其它语言方便得多。

Python本身开发效率高,还具备丰富的生态资源,有了 ctypes 加持,还可以使用C/C++的优秀资源,因此,掌握了这个工具,相信我,在大部分项目上将给你带来惊喜。

ctypes 官方文档,更着重是一份产品说明书,而不是教程,阅读官方文档来学习比较耗精力。 本人旨在给提供一份优秀教程,尽可能有条理地讲清楚原理、通过实例代码演示如何使用,同时也涉及一些更复杂的使用场景,如回调函数等。

除了ctypes 外,还可以通过 python C API、Cython来编写C/C++代码模块,或者利用第3方工具包来完成融合,如
Swig
pybind11

1、Visual Studio 2022 把项目打包成DLL

在示例讲解之前,我们先用Visual Studio2022创建并编译1个DLL项目

1) 创建新项目, 选择dll项目

至此创建完成,包含源文件dllmain.cpp、pch.cpp,头文件framework.h、pch.h。 ![在这里插入图片描述]

2) 定义头文件 boFirst.h

#define IMPORT_DLL __declspec(dllimport) 定义了IMPORT_DLL 宏, __declspec(dllimport)表示函数将被编译为dll
extern "C" IMPORT_DLL  ==》 指定该函数可被外部调用。 
#pragma once

#ifdef DLL_EXPORT
#define IMPORT_DLL __declspec(dllexport)
#else
#define IMPORT_DLL __declspec(dllimport)
#endif

//自定义结构体
struct boShape
{
    char  shape[20];
    float width; 
    float height;
    float depth; 
};

// 自定义函数
extern "C" IMPORT_DLL int bo_add(int a, int b); 
//自定义函数,参数为结构体 boStruct 
extern "C" IMPORT_DLL float bo_shape_vol(boShape bs);

这里分别添加bo_add, bo_shape_vol 两个可供外部调用函数声明, 也可以更简化,只要函数前面有extern “C” __declspec(dllexport) 即可。

#ifndef _PY_LIST_2_C_ARRAY_H_
#define _PY_LIST_2_C_ARRAY_H_

#include <stdio.h>

typedef struct {
    int value;
    wchar_t* name;
} Item;

extern "C" __declspec(dllexport)
int sum_diagonal(Item** field, size_t size);

#endif

3)实现函数代码

// boFirst.cpp : Defines the exported functions for the DLL.
#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier
#include <utility>
#include <limits.h>
#include "boFirst.h"
# include <iostream>

int bo_add(int a, int b)
{
    int c;
    c = a + b; 
    return c; 
}

float bo_shape_vol(boShape bs)
{
    float volume = bs.width * bs.height * bs.depth;  
    std::cout << bs.shape << "volume is " << volume << std::endl; 
    return volume; 
}

4) 编译生成dll

选择build solution , 输出窗口可以看到,在x64\Debug\ 目录下生成了DLL_Test.dll 文件

Rebuild started...
1>------ Rebuild All started: Project: DLL_Test, Configuration: Debug x64 ------
1>pch.cpp
1>dllmain.cpp
1>boFirst.cpp
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(8,5): warning C4273: 'bo_add': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(20,27): message : see previous definition of 'bo_add'
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(15,7): warning C4273: 'bo_shape_vol': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(22,29): message : see previous definition of 'bo_shape_vol'
1>Generating Code...
1>   Creating library D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.lib and object D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.exp
1>DLL_Test.vcxproj -> D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.dll
1>Done building project "DLL_Test.vcxproj".
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

2、通过 ctypes 调用DLL的简单示例

在上一节中的dll有两个方法,其中1个输入参数使用了C++ Struct 结构体, python没有对应的数据结构。

第1种调用方式 (标准方式):

Step-1:

将DLL_Test.dll 文件拷贝至python文件所在目录, 该dll中包含如下可供调用的函数

int bo_add(int a, int b)
float bo_shape_vol(boShape bs); 其中boShape为自定义struct

Step-2:

编写代码,调用 bo_add()方法

from ctypes import *
dll = cdll.LoadLibrary("./DLL_Test.dll")
print("bo_add ", dll.bo_add(20, 80))

运行,即可以得到输出 100

cdll.LoadLibrary() 方法返回 cdll 对象,cdll 调用C/C++函数的方法遵从 cdecl 方式(C/C++函数调用的标准方式)。

上例是直接调用 dll中的函数.

第2种调用方式( 函数签名方式)

函数签名方式,相当于在python内重新申明1个C++函数的别名,申明内容包括用ctype指定函数形参与返回值的数据类型。

import ctypes
clibrary = ctypes.CDLL('clibrary.so')

# 定义1个python函数名,指向 C++ 函数
addTwoNumbers = clibrary.add

# 用ctypes 数据类型,定义函数的参数类与返回值类型, 
addTwoNumbers.argtypes = [ctypes.c_int, ctypes.c_int]
addTwoNumbers.restype = ctypes.c_int

# 使用签名函数
print("Sum of two numbers is :", addTwoNumbers(20, 10))

Output: Sum of two numbers is : 30

ctypes 数据类型 –> Python 数据类型的转换是程序在后台自动完成的。

3、ctypes 数据类型

C/C++的数据类型与 Python数据类型及使用方式都有较多差异,因此python调用C++函数,必须需要对DLL函数形参以及返回值类型进行转换,才能把数据传入DLL,以及解析DLL的返回值。ctypes 模块提供了中间数据类型,来帮助Python完成数据转换,这些类型在python中都是以对象方式出现。

Python 调用DLL函数分3步:

1) 在Python中重新申明 DLL 函数: 除了函数名之外,还要用ctypes的数据类型将 DLL 函数的形参与返回值重新申明一遍。

2) 实参准备:当实际调用该函数时,将数据赋值给ctypes参数后,调用该函数。

3) 解析返回值:如果有返回值,从 ctypes 类型中解析出数据。

其中,第1步,用ctypes 类型申明 DLL函数形参最为关键,可以认为这一步目的是将 C++的数据类型转换为Python数据类型,ctypes 起到桥梁作用。

下面介绍一下 ctypes 提供了哪些数据类型,以及如何使用。

1) ctypes 与c、python数据类型的对应关系

2) 整数型参数变量定义

i = c_int()

赋值 :

i = c_int(99) 
i.value=99
print(i) 
c_long(99)

3) 浮点数值参数

d1 = c_float()
d1.value=10.3
d1
c_float(10.300000190734863)
>>> d1.value
10.300000190734863

4) 字符串参数

用 char * 32 定义1个32个字符的字符串,赋值用 value属性

>>> str1 = c_char * 32
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value = b"hello"
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value
b'hello'

C/C++中字符串是用char* 指针或数组来代替,ctypes 提供对应的字符串指针类型 c_char_p,给c_char_p赋值通常有两种方式: – 把 python 字符串转为 bytes 类型, 使用endcode()方法。 – 直接使用bytes 类型字节串。

将字符串转换为 bytes 对象后,用c_char_p的value属性赋值, 用法示例 str2.value = x 赋值。

>>> x = b"abcdef"
>>> str2 = c_char_p()
>>> str2.value = x
>>> str2
c_char_p(140250436808304)
>>> str2.value
b'abcdef'
>>> str2 = str1
>>> str2
<class '__main__.c_char_Array_32'>
>>> str2.value
b'hello'

c_char_p 指向的数据类型必须是二进制编码,即Bytes类型,如果是中文,可以用utf-8编码,显示时再解码.

>>> y=bytes("hello, 小王","utf-8")
>>> str2.value=y
>>> str2.value
b'hello, \xe5\xb0\x8f\xe7\x8e\x8b'
>>> str2.value.decode(encoding="utf-8")
'hello, 小王'

ctypes 还提供了 create_string_buffer() 方法用于生成字符串。 格式: ctypes.create_string_buffer(init_or_size, size=None)

str3 = create_string_buffer(b’world’,32)
print(str3)
<main.c_char_Array_32 object at 0x7f8e9826eac0>
print(str3.value)
b’world’
print(str3.raw)
b’\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00′ repr(str3.raw)

create a 3 byte buffer, initialized to NUL bytes

p = create_string_buffer(3)

create a buffer containing a NUL terminated string

p = create_string_buffer(b'Hello') 
print(sizeof(p)
repr(p.raw))

create a 10 byte buffer

p = create_string_buffer(b'Hello', 10) print(sizeof(p), repr(p.raw))

5) 数组类型变量


数组的创建和C/C++语言的类似,给定数据类型和长度即可
如定义 c_int 类型数组, 长度为10. 先定义1个 INT_ARRAY 类型,再创建1个数组变量,其类型为 c_long_Array_10, 示例代码:

>>> INT_ARRAY = c_int * 10
>>> array1 = INT_ARRAY(10,20,30,40,50,60,70,80,90,100)
>>> array1
<__main__.c_long_Array_10 object at 0x00000283665D7040>
>>> array1[5]
60
>>> array1[7:]
[80, 90, 100]

创建数据变量过程可以将两步并为1步,

>>> array2 = (c_int * 10)(10,20,30,40,50,60,70,80,90,100)
>>> array2
<__main__.c_long_Array_10 object at 0x0000028366558B40>
>>> array2[7:]
[80, 90, 100]

6) Structure 变量

Python的dict类型虽然与C/C++ struct 结构体类型很相似,但不能直接拿来用。ctypes模块提供了 Structure 类来进行 C/C++ struct结构体类型转换。 示例,定义1个POINT 像素点结构体

class POINT(Structure):
...     _fields_ = [("x", c_int),
...                 ("y", c_int)]
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20

定义1个RECT 结构体,其字段为POINT结构体

>>> class RECT(Structure):
...     _fields_ = [("upperleft", POINT),
...                 ("lowerright", POINT)]
...
>>> rc = RECT(point)
>>> print(rc.upperleft.x, rc.upperleft.y)
0 5
>>> print(rc.lowerright.x, rc.lowerright.y)
0 0
>>>r = RECT(POINT(1, 2), POINT(3, 4))

1个更复杂的结构体, 字段 b , 为浮点数, “point_arrary” 为1个包含POINT结构体类型的Array

>>> class MyStruct(Structure):
...     _fields_ = [("a", c_int),
...                 ("b", c_float),
...                 ("point_array", POINT * 4)]

说明: python3 新增了struct 模块,基于序列化的原理,可将不同类型的数据打包进 ctypes bytes buffer变量,用于向c/c++ struct 变量传值, 但直观度不如 Structure变量,有兴趣者可以深入了解一下 python struct 模块的使用

7) 指针变量

ctypes提供了pointer()和POINTER()两种方法创建指针

pointer( object )用于将某个类型的对象转化为指针

其参数必须是ctypes的变量对象,不能是类型. 如下例,int_p 指向1个c_int 对象的指针,当前值为99

int_obj = c_int(99)
int_p = pointer(int_obj)
print(int_p)
使用contents方法访问指针
print(int_p.contents)
# 获取指针指向的值
print(int_p[0])

output 如下

<ctypes.wintypes.LP_c_long object at 0x00000217897102C0>
c_long(99)
99

POINTER()用于定义指针变量类型

POINTER() 的参数必须是类型名称,相当于定义了新的1个指针变量类型,但指向值的类型必须是指定的类型名称。然后再用这个新类型实例化1个指针变量。 而 pointer() 方法隐式地完成了POINTER()的工作。

# 指针类型
INT_P  = POINTER(c_int)  # 定义了1个新的指针变量类型
# 实例化
int_obj = c_int(4)
int_p_obj = INT_P(int_obj)   # 实例化1个指针变量
print(int_p_obj)
print(int_p_obj.contents)
print(int_p_obj[0])

output :

<__main__.LP_c_int object at 0x7f47df7f79e0>
c_int(4)
4

指针类型的转换

ctypes提供 cast() 方法将一个ctypes实例转换为指向另一个ctypes数据类型的指针,cast()接受两个参数,一个是ctypes对象,它是或可以转换成某种类型的指针,另一个是ctypes指针类型。它返回第二个参数的一个实例,该实例引用与第一个参数相同的内存块。

# 初始化 1个 c_int的指针,当前值为4
int_p = pointer(c_int(99))
print(int_p)
# 定义1个 c_char类型的指针类型
char_p_type = POINTER(c_char)
print(char_p_type)
# 转 c_int变量转换为c_char类型指针
cast_type = cast(int_p, char_p_type)
print(cast_type)

output:

<ctypes.wintypes.LP_c_long object at 0x0000021789710140>
<class 'ctypes.LP_c_char'>
<ctypes.LP_c_char object at 0x0000021789710E40>

8) enum 枚举类型

在 C/C++中还有1个经常用到的类型是 enum,ctypes 没有相关类型,但python有1个enum类. 下面介绍如何 将 python enum 类用于 dll 函数参数。 如 C++程序中定义了1个枚举类型

enum Priority {
        CRITICAL = 0x1,     
        IMPORTANT= 0x2,     
        NORMAL = 0x3,       
        MINOR = 0x4,    
        INFO = 0x5  
    };

有1个函数: long set_priority(Priority n) 在python中,也定义1个enum类

import enum 
class EnumPriority(enum.IntEnum):
    CRITICAL = 0x1
    IMPORTANT= 0x2
    NORMAL = 0x3
    MINOR = 0x4
    INFO = 0x5

    #注意必须将添加这个方法,ctyps 要求,将obj转为 init型
    @classmethod
    def from_param(cls, obj):
        return int(obj)

注意,必须要添加1个 from_param()方法., 将上面的枚举元素转为int型 下面将 python enum 做为参数传入dll

dll =  cdll.LoadLibrary(dll_name)
set_priority=dll.set_priority
set_priority.restype = c_init
set_priority.argtypes = [EnumPriority]
# 调用该函数
print("call back result is ",set_priority(EnumPriority.INFO))

4. 加载DLL库

Ctypes 提供有2种动态链接库的调用方式

ctypes.cdll. LoadLibrary(‘xxx.dll’) 加载 cdel调用方式的dll, 默认c++, vc++生成的dll库都是cdel方式。

ctypes.windll.LoadLibrary(‘xxx.dll’) 加载 win32调用方式(stdcall)的dll,所以很少使用此种调用方式

示例

def find_example_ctypes(required):
    '''
    Finds and loads example shared object of the required major
    '''
    # Importing ``ctypes`` should be in scope of this function to prevent failure
    from ctypes import util, cdll

    so_name = util.find_library('example.dll')
    if so_name is None:
        raise ExampleImportError('EXAMPLE shared object not found.')
    example = cdll.LoadLibrary(so_name)
    require_version(example.example_version(), required)
    return example

5. ctypes 向DLL函数传入参数

指定dll函数参数类型 通过argtypes属性来设置参数类型,

>>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]

例2:

myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("call bo_add() with function signature approach \n", myadd(20, 80))

传入指针参数 c++ 函数addx()的形参为两个指针, int addx( int p1, int p2), python 实例两个c_int型的指针传入

int_obj1 = c_int(20)
ptr1 = pointer(int_obj1)
int_obj2 = c_int(80)
ptr2 = pointer(int_obj2)
print("pass pointer to function addx(), ", dll.addx(ptr1, ptr2))

传入结构体参数

步骤: – 在python定义1个Ctypes.Structure 类, 字段也C++ Struct类型保持一致。 – 用此类构建参数值 – 传入dll函数,前面要加byref 示例 :

from ctypes import *

class MyStruct(Structure):
    _fields_ = [('shape', c_char*20),
                ('w', c_double),
                ('h', c_double),
                ('d', c_double),
                ]

第2种调用方式

myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("another call method to bo_add() : ", myadd(20, 80))

传入结构体参数

dw, dh, dd = 4.0, 7.0, 3.0
dstr = b"cuboid "
s1 = MyStruct(dstr, dw, dh, dd)
print(s1.w, s1.h, s1.d)
print(s1.shape)
print(dll.bo_shape_vol(byref(s1)))

Output 4.0 7.0 3.0 b’cuboid ‘ cuboid volume is 84 233390192

问题: print(dll.bo_shape_vol(byref(s1))) 打印出的是地址,不是结果 84 解决办法:用类的调用方式,定义dll函数返回值类型

print("第2种调用方法") 
myVol = dll.bo_shape_vol
myVol.restype = c_double  #定义dll函数返回值类型
myVol.argtypes = [POINTER(MyStruct)]  #结构体参数是以地址方式传入,因此要转为指针
dx = myVol(byref(s1))
print(dx)

Output

第2种调用方法
cuboid volume is 84
84.0

下面是另1个例 子

from ctypes import *

class Passport(Structure):              
    _fields_ = [("name", c_char_p),    
                ("surname", c_char_p),
                ("var", c_int)]

lib_dll = cdll.LoadLibrary("DLL_example.dll")   
lib_dll.SetPassport.argtypes  = [POINTER(Passport)] 

lib_dll.GetPassport()          

lib_dll.SetName(c_char_p(b"Yury"))
lib_dll.SetSurname(c_char_p(b"Wang"))

lib_dll.GetPassport()         

name = str.encode(("Feng"))  
surname = c_char_p((b'Li')) 

passport = Passport(name, surname, 34) 

lib_dll.SetPassport(pointer(passport)) 

lib_dll.GetPassport()

Output:

Load DLL in Python
SetName
SetSurname
GetPassport: Default | Passport | 17
SetName
SetSurname
GetPassport: Yury | Orlov | 17
SetPassport
GetPassport: Vasiliy | Pupkin | 34
DETACH DLL

6. 对回调函数的支持

C++中,用函数指针非常容易地实现回调函数,python也可以实现。 第1步: 用 CFUNCTYPE() 定义1个函数指针,windows 使用 WINFUNCTYPE() 。 CFUNCTYPE()第1个参数是回调函数返回值类型,后面是函数形参。 第2步:将回调函数名赋给CFUNCTYPE变量,相当于将函数地址赋给函数指针。 第3步:将该回调函数指针做为另1个函数的形参使用。

示例如下:

from ctypes import *
import sys

# 回调函数类型定义
if 'linux' in sys.platform:
    fun_ctype = CFUNCTYPE
else:
    fun_ctype = WINFUNCTYPE

def add(int a,int b):
    return a+b

ADDFUNC = fun_ctype(c_int, c_int, c_int)
add_callback = ADDFUNC(add)
#将CFUNCTYPE变量做为函数形参
def test(x,y,ADDFUNC):
    print(f"data is {x} and {y} 
    return add_callback(x,y)

#实际测试时,把回调函数做为参数传入
test(10,20,add_callback)

总结:

1) ctypes 对c/c++ dll/so中的函数与形参,需要先申明才能使用

2)ctypes 提供了一套数据类型,用于C 数据类型与 python 数据类型的转换, 注意数组,指针类型的转换。

3) ctypes 调用 c/c++ dll/so 动态链接库函数的方法有两种

  • 函数名直接调用
  • 签名函数的方式调用

裸眼3D–原理介绍

裸眼3D基本上都是针对双目视差来说的。

什么是双目视差:人有两只眼睛,它们之间大约相隔65mm。当我们观看一个物体,两眼视轴辐合在这个物体上时,物体的映像将落在两眼网膜的对应点上。这时如果将两眼网膜重叠起来,它们的视像应该重合在一起,即看到单一、清晰的物体。根据这一事实,当两眼辐合到空间中的一点时,我们可以确定一个假想的平面,这个平面上的所有各点都将刺激两眼网膜的对应区域。这个表面就叫做视觉单像区(horopter)。它可以定义为在一定的辐合条件下,在视网膜对应区域的成像空间中所有各点的轨迹。位于视觉单像区的物体,都将落在视网膜对应点而形成单个的映像。

如果两眼成像的网膜部位相差太大,那么人们看到的将是双像,即把同一个物体看成两个。例如,我们用右手举起一支铅笔,让它和远处墙角的直线平行。这时如果我们注视远处墙角的直线,那么近处的铅笔就将出现双像;如果我们注视近处的铅笔,远处的墙角直线就将出现双像。

正因为双目视差,才会让我们看到的物体有纵深感和空间感。

裸眼3D是怎么做到蒙骗双眼来营造空间和纵深感呢,现在的3D视频或者图像都是通过区分左右眼来拍摄的两幅图,视差距约为65mm,通过让你左眼看到左眼的图像,右眼看到右眼的图像就可以让你的大脑合成一副有纵深感的立体画面。

人的两只眼睛相距约6cm,就像两部相距6cm放置的照相机,拍出的照片角度会有一点点不同(侈开)。

这种侈开在大脑里就可以融合成立体的感觉。

我们再抽丝剥茧制作一个最简单的侈开立体图:

越简单的图越容易说明原理,但观看起来越消耗眼睛“内功”。请您用原理一的透视方法,让左眼看左图,右眼看右图,当您能看到三个双圈的时候,中间那个小圆就会凸出纸面呈现立体感

静态库(静态链接库lib/a)和动态库(动态链接库dll/so)

方法库大体上可以分为两类:静态库和动态库(共享库)。

1. windows中静态库是以 .lib 为后缀的文件,动态库是以 .dll 为后缀的文件。

2. linux中静态库是以 .a 为后缀的文件,动态库是以 .so为后缀的文件。

静态链接:

  • 静态库 在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
  • 静态库可以简单看成是一组目标文件(.o .obj文件)的集合, 将若干个.o文件转换为静态库的过程,称之为打包. Linux下是使用ar工具, Windows下是使用lib.exe。
  • Linux下静态链接库的后缀是.libWindows下静态链接库的后缀是.a

动态链接:

  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
  • 在Windows系统下的执行文件格式是PE(Portable Executable)格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。 跟exe有个main或者WinMain入口函数一样,DLL也有一个入口函数,就是DllMain。根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
    对于动态链接库,DllMain是一个可选的入口函数。一个动态链接库不一定要有DllMain函数,比如仅仅包含资源信息的DLL是没有DllMain函数的。
  • Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
    无需打包工具,直接使用编译器即可创建动态库。
  • Linux下动态链接库的后缀是.so;Windows下动态链接库的后缀是.dll

一、动态链接库创建和使用

1.创建hello.so动态库

#include <stdio.h>void hello(){	printf("hello world\n");}编译:gcc -fPIC -shared hello.c -o libhello.so

2.hello.h头文件

void hello();

3.链接动态库

#include <stdio.h>#include "hello.h" int main(){	printf("call hello()");	hello();}编译:gcc main.c -L. -lhello -o main

这里-L的选项是指定编译器在搜索动态库时搜索的路径,告诉编译器hello库的位置。”.”意思是当前路径.

3.编译成够后执行./main,会提示:

In function `main':
 
main.c:(.text+0x1d): undefined reference to `hello'
collect2: ld returned 1 exit status

这是因为在链接hello动态库时,编译器没有找到。
解决方法:

sudo cp libhello.so /usr/lib/

这样,再次执行就成功输入:
call hello()

二、静态库 创建和使用

文件有:main.c、hello.c、hello.h
1.编译静态库hello.o: 

gcc hello.c -o hello.o  #这里没有使用-shared

2.把目标文档归档

ar -r libhello.a hello.o  #这里的ar相当于tar的作用,将多个目标打包。

程序ar配合参数-r创建一个新库libhello.a,并将命令行中列出的文件打包入其中。这种方法,如果libhello.a已经存在,将会覆盖现在文件,否则将新创建。

3.链接静态库

gcc main.c -lhello -L. -static -o main

这里的-static选项是告诉编译器,hello是静态库。
或者:

gcc main.c libhello.a -L. -o main

这样就可以不用加-static

4.执行./main

输出:call hello()

区别:

可执行文件大小不一样

从前面也可以观察到,静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。

占用磁盘大小不一样

如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。

扩展性与兼容性不一样

如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。

依赖不一样

静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。

即便如此,系统中一般存在一些大量公用的库,所以使用动态库并不会有什么问题。

复杂性不一样

相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。这也不在本文的讨论范围。

加载速度不一样

由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。