SegmentFault 知识梳理最新的文章
2022-04-02T16:39:29+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
网格拟合
https://segmentfault.com/a/1190000041648684
2022-04-02T16:39:29+08:00
2022-04-02T16:39:29+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<p>这里的网格拟合,当然是三维网格拟合。</p><p>三维网格拟合有什么用?呃.....以后再说吧......</p><h2>什么是网格拟合</h2><p>嗯,经常经常听到<strong><a href="https://link.segmentfault.com/?enc=sVyqeWrutg21VNkKh3z%2FSA%3D%3D.LHypAvOoq5v8jm7c2VNDAEQZISRWne%2B1zvVKIz5rdYDGLneHCj7BVh9gT%2F5iiB%2BLbPUtxVJrwdFw0QzZNw1Cvw%3D%3D" rel="nofollow">[拟合</a>]</strong>这个名词,它表示一个动作。最常听到的是<strong>最小二乘拟合</strong>---把一系列点<em>拟合</em>出一条线---把三维空间散乱点<em>拟合</em>出一个平面、球面、柱面...</p><p><strong>拟合</strong>,总需要一个<strong>拟合源</strong>,总要有一个<strong>目标</strong>。上面场景中,拟合源一般是二维或者三维的点数据,目标一般是线、平面、球面或者柱面等等的数学参数表示。</p><p><img src="/img/bVcYURu" alt="" title=""></p><p>OK,大概已经知道拟合是做什么的了。那我们的三维网格拟合,又是怎么回事呢?</p><p>其实,<strong>三维网格的拟合</strong>和上述所谓拟合并没有本质区别,只是数据源和拟合目标都是可任意的三维网格数据而已,将两个三维网格拟合,其结果仍然是一个三维网格,只不过拟合后的结果网格可能带有源数据和目标数据的部分显著特征。用公式表示就是:</p><pre><code class="c++">Mesh A + Mesh B = Mesh C,其中 “+”表示拟合计算</code></pre><p><strong>注意:上述所谓三维网格的拟合,并不包含网格带纹理的情况。</strong>即纹理不参与拟合运算。</p><p>说白了,所谓三维网格的拟合,其实就是多个三维网格经过一定的 ‘’ 修修剪剪 ‘’ 产生一个新网格的过程,新网格就是拟合的结果。</p><h2>怎样进行网格拟合</h2><p>哦,我已经知道了什么是网格拟合,但怎么进行网格拟合计算呢。</p><p>假如,我有A和B两个三维网格数据,网格A有10w点,30w三角面片,网格B有7w点,20w三角面片,额(⊙﹏⊙)....它俩应该怎么进行拟合计算呢?---我应分别取A或B的哪一部分参加计算呢?A和B不在同一个坐标系下怎么办呢?我的拟合运算会不会改变A或者B的三角网格顶点之间的连接关系呢?A和B体积相差太大(比如一只狗和一只猫)怎么办呢?需要缩放吗?缩放系数怎么处理呢?拟合质量如何评价呢?......呃呃...头好大....</p><p>既然头好大,那就规定特殊场景吧。</p><h3>场景</h3><p>还是来拿手头的人头网格说事。首先,人头点云,隐含了点云的体积头差不多(小孩子除外);其次,同一个扫描设备获取的三维人头网格数据量不会差别太大(其实这个条件没啥影响);最后,就是...我手头只有三维人头数据可以测试(么有去找更多的)。</p><p>有下面两组数据,其中上边的图像记为Mesh A,下边的记为Mesh B,</p><p><img src="/img/bVcYURM" alt="" title=""></p><p><img src="/img/bVcYURW" alt="" title=""></p><p>Mesh A:只有真实人脸部分,网格和顶点数量较多,三角网格之间并不是很“顺滑”;Mesh B:虚拟全人头网格,网格数目和顶点数相对A来说较少,且mesh表面光顺性较好。</p><p>目标,将A和B拟合,构造带有真实人脸网格模型的三维人头(实际应用中是有重要意义的)。</p><h3>简单拟合</h3><p>至此,可能很多很多朋友,已经想到了一种非常简单的网格拟合方法。怎么做呢?</p><p>首先,将Mesh A和Mesh B<strong>初步对齐</strong>,即将两个网格人脸部分对齐,放在同一个坐标系下。如何对齐呢?很简单,因为Mesh A和Mesh B都表示人脸网格,则其面部必有共同特征---眼睛、鼻子、嘴巴、鼻梁、眉毛.....通过选两组Mesh中对应的特征点,基于指定的精确的对应点对,通过ICP计算,即可获得某Mesh的变换矩阵,从而使得两个Mesh统一到统一坐标系下。示意图如下:</p><p><img src="/img/bVcYUSn" alt="" title=""></p><p>其次,在完成上述初步对齐--即两个模型的眼睛对眼睛、鼻尖对鼻尖...后,我们只要改变Mesh B的面部网格结构形状即可。通过设定脸部距离阈值,在保证Mesh B各个顶点链接关系不变的条件下(即不改变拓扑关系),改变Mesh B的脸部特征点的网格顶点坐标,使得Mesh B脸部呈现Mesh A的外观特征,即完成了两个网格的拟合。</p><p>如何实现上述计算呢?最简单的---置换或者说替换。通过设定的距离阈值,找到Mesh B和Mesh A的脸部对应点对,将两个Mesh的对应点对的坐标直接进行替换,同时保证Mesh B中点的链接关系不变(若该边,可能 “ 脸就不是脸了 ”)。如下图:</p><p><img src="/img/bVcYUSz" alt="" title=""></p><p>可以看到,基于<strong>【简单拟合】</strong>理论,基本可以两个网格的拟合,并能基本得到自己想要的结果---含有真实人脸形状的全人头网格模型。</p><p>这就够了吗?</p><h3>风险</h3><p>显然不够!其实,上边的<strong>【简单拟合】</strong>存在较大的风险,其结果基本无法应用在实际生产项目中。</p><p>风险点1:质量较差。以上述方式拟合网格模型,如上图边界部分所示,拟合结果存在较大的突起,有非常突兀的形状变化。</p><p>风险点2:精度无法保证。其实上述方式中,只是简单的对应点的顶点替换,这就导致了在替换时,各个参与计算的点没有考虑其邻域点的分布情况。计算过程中,极大概率导致某个特征点的坐标多次变换,而且该特征点的坐标只与最后一次变化(最后一个对应点)有关---因为,Mesh B和 Mesh A的点数、网格数目及点密度都不一样,当将两个网格拟合计算时,必然存在一对多或者多对一的情况。</p><p>风险点3:尺寸差别较大。这个问题应该怎么对待呢?!实际中,绝对没有两个一摸一样的人,那么尺寸必然存在一定误差。那我们只能尽可能的减少这种网格尺寸上的误差,可见,在上面计算中,没有考虑这一点。</p><p>......</p><p>当然,还有其他很多的风险点,自己也无法一一穷举。总之,上述描述方案也许会是一种网格拟合的解决方案,但是对网格的后处理工作增加了许多的难度,需要更进一步的研究与改进。</p><p>......</p><p>有没有其他办法呢?</p><h2>实现</h2><p>有!先上一篇论文 《Review of Statistical Shape Spaces for 3D Data with Comparative Analysis for Human Faces》.</p><p>其实,其他方案的网格拟合(如论文)和上述<em><怎样进行网格拟合></em>大方向上基本一致:初步对齐-->更新顶点-->改变网格形状,只不过需要在拟合过程中将上述几个风险点考虑进去。</p><p>基于上述基本理解以及对所提到论文的浅见,设计并实现了如下网格拟合方案:</p><p>首先,初步对齐同<怎样进行网格拟合>所述一致;</p><p>其次,分别统计两个Mesh中特征点之间的欧式距离之和,定义:若两个距离之比接近1.0,则认为两个Mesh尺寸接近,若大于1.2或者小于0.8,怎认为尺寸差异较大,则对较小距离和的Mesh进行适当缩放,直至重新计算的比值接近1.0 (针对风险3);</p><p>第三,分别计算两个Mesh网格各个顶点法向量,设置对应点对角度阈值和距离阈值,对应点对采用如下方式计算:(1)从Mesh A中取特征点f;(2)在Mesh B中寻找距离f最近的三角面片T;(3)计算 f 到T的垂足,则该垂足即为与f对应的特征点;若该对应点对满足设置的角度阈值和距离阈值,怎认为该点对满足计算条件;</p><p>第四,基于特征点邻域信息,计算Mesh A中每个特征点的3*4变换矩阵,该变换矩阵使得特征点基于局部形状向Mesh B的对应点逐步变换,其中,变换矩阵采用<a href="https://link.segmentfault.com/?enc=RrEf0RDAqAHo9Gp%2FBR1bTQ%3D%3D.np%2BJITst%2FWN8wtQ44T2wqDDLeal%2FSSO7yXu1TG%2ByjHZ%2BssX7PCiq2f%2FIOjgFoBpSKfyt%2BAxUX9u3Mx8391EHlQ%3D%3D" rel="nofollow">L--BFGS 算法</a>(关于如何构造LBFGS求解函数,可能还需要另外大幅篇幅介绍,且自己也并不是很精通,故此舍去相关介绍,待后期补充吧)。并设置Lbfgs算法求解参数:迭代次数、梯度容差、代价函数、前后两次误差变换阈值、近邻点计算权重等。</p><p>第五、设置总体迭代次数。</p><p>基于上述大体思路,获得到结果如下所示:</p><p><img src="/img/bVcYUSB" alt="" title=""></p><p>可见,基本不存在上图中的问题。</p><h2>小结</h2><p>三维网格拟合是三维运算的一种,可以产生新的自己所需要的网格。</p><p>在我的场景中,有一个人脸模型和一个全人头模型,我想获得一个带有真实(扫描仪获取)人脸的全人头三维网格,通过网格拟合的形式,获得了我想要的结果。</p><p>其实现在回头来看,这不仅仅是网格的拟合问题,<strong>【实现】</strong>中步骤算法还能更进一步的扩展到用于解决非刚性配准问题。因为在上述求解方案中,基于特征点及其邻域点集,为每一个特征点都计算了一个3*4的仿射变换矩阵,该矩阵在局部上能够使得点以线性的方式逐步接近目标点,从全局来看更加平稳和光滑。本质上点云或网格的非刚性配准也是如此,同时非刚性配准又有很多很多实际应用...</p><p>那三角网格拟合到底有啥用呢?比如:可以用来解决网格大面积缺失修复问题;可以用来产生新的有意思的网格,如蛇头换狗脸....;可以用来解决人头补全问题;更进一步的可是用来实现虚拟整容与预览等医美方面,能够为不同人脸虚拟定制、预览不同发型;可以用来处理牙齿建模问题....</p><p>在这里,<strong>【实现】</strong>步骤中,并没有具体介绍如何进行拟合计算的细节,同时也并没有贴相关代码,因为这需要更大的篇幅,也需要更多更深入的理解和公式推理。</p><p>在本片文章介绍中,其实还剩去了非常重要或着说非常有必要的一个步骤:纹理贴图。因为在很多很多场景中,网格和纹理是密不可分的--如医美。在进行三角网格拟合变换后,再使用原来的纹理图其实已经没有太大意义,其一:网格点坐标已经变换,基本不适用于原来的纹理坐标,会产生严重的纹理变形,视觉效果非常差;其二:网格拟合的目的就在于获取新的模型,那么再贴原来的纹理图又有什么意义呢。</p><p>关于网格拟合后纹理重映射,涉及到纹理替换、网格(纹理)展开、二三维仿射矩阵计算、投影矩阵......,需要大量篇幅介绍,这里暂且留坑吧。</p><p>注意:关于本篇文章的细节(包含纹理映射部分)已于去年(2021年)申请发明专利,目前状态还在实申中,专利中主要包含了拟合的数学公式推导、纹理映射计算等相关内容。</p>
多视角三维模型纹理映射 03
https://segmentfault.com/a/1190000041638091
2022-03-31T16:11:25+08:00
2022-03-31T16:11:25+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<p>首先,严格意义来说,本文题目并非符合有关多视角纹理映射的相关内容,而是基于上一篇文章<a href="https://segmentfault.com/a/1190000041341663">《多视角三维模型纹理映射 02》</a>的留坑继续...</p><p>其实,应该叫做 -- <strong>3D扫描仪转台标定 </strong>吧。</p><p>在开启这部分的工作前,查阅了很多论文、资料等,其中比较有借鉴意义的是《旋转平台点云数据的配准方法》,【作者周朗明,郑顺义,黄荣永,武汉大学】,这篇文章给了自己极大的启发。</p><h2>设备与准备</h2><p>先来看一些自己所用到设备及其大体组合结构示意图:</p><p><img src="/img/bVcYR5e" alt="" title=""></p><p>自己所使用的基本设备如上图所示:相机支架,深度相机(直接产生点云,内参已知)、旋转平台,待测物体(标定块)。嗯嗯,只有也仅有上述几个设备而已。其中旋转平台和相机的距离应在合理范围内。</p><p>在固定好上述设备的位置关系后,接下来就是设计标定算法并验证了。</p><h2>3D扫描仪转台标定算法设计</h2><p>首先要明白,标定的目的是什么,预想的标定结果是什么。</p><h3>标定目的</h3><p>第一个问题:标定的目的??目的难道不是确定转台和相机的相对位置关系嘛?!是,当然是。更进一步的,因为相机无法直接拍摄转台表面获取相应数据(此处转台为纯黑色,且转台表面没有标记点),因此需要借助<strong>标定块</strong>来实现相应目的。此时,上图中 [<em>实物</em> ]可以暂时理解为 [<em>标定块</em> ]。</p><p>此时,又引来另一个问题,选择什么样的标定块。注意,这里的标定块,是【<strong>块</strong>】,不是标定板,是一个三维的实体标准块。在选择标定块时候,最好该标定块有严格且已知的标准尺寸,形状最好比较简单,且<strong>数学意义上比较好描述</strong>,如球、圆柱等等。</p><p>算了,直接上我选的标定块吧,如图:</p><p><img src="/img/bVcYR5s" alt="" title=""></p><p>对,没有错。我选择了台球,台球几乎符合上述所有要求(标准直径,标准数学描述公式、易拟合),而且还比较便宜。此时<strong>标定块</strong>或许称为<strong>标定球</strong>更为合适。在实际实施中,我选择了那个纯白色的球球...eee...因为其表面色彩比较单一....</p><p>注意:因为相机内参已经知道了,不需要对深度相机进行重复标定,所以也不需要标定板。</p><p>至此,基本解决了所有外部依赖条件。</p><h3>标定的结果</h3><p>在准备妥当所有的外部硬件条件后,进一步便需要寻求标定结果。</p><p>想象一下扫描过程:转台在位置1,相机拍摄该位置下的标定块点云1-1;转台在位置2,相机拍摄该位置下的标定块点云2-2;...如此循环直到完成标定块的360度扫描。</p><p>转台旋转一周,拍摄的台球点云如下图:<br><img src="/img/bVcYR5w" alt="" title=""></p><p>显而易见,“<strong>貌似</strong>”标准球在围绕<strong>一个东西</strong>旋转了一周。想想也是如此,不就是转台带着标准球转了一圈嘛。</p><p>确实,这里的<strong>一个东西</strong>就是我们3D转台标定要求解的东西。从论文和上图数据,不难猜测:扫描得到的点云,是在绕某个<strong>固定轴</strong>--这个东西旋转。至此,基本知道,我要求解的结果应该是个<strong>固定轴</strong>。</p><p>是什么样的固定轴呢?--旋转平台的中心轴---旋转平台的中心轴在相机坐标系下的表示---<strong>旋转平台的中心点</strong>和<strong>旋转平台的平面法向量</strong> 在相机坐标系下的表示==>标定结果。</p><p>至此,彻底明白了,所谓的标定结果,应该包含两个数据:点坐标和向量。</p><p>在上述场景下,如何求解这两个东西?!基本求解过程如下:</p><ol><li>将台球固定在转台表明,控制旋转平台绕固定角度旋转;</li><li>相机拍摄每个固定角度下的台球点云数据;</li><li>为减少误差,步骤1和步骤2数据量个数越多越好;</li><li>拟合每个角度下的台球点云---球数据,并提取球心(提取球半径,与标准台球半径比较,侧面检验标准球拟合算法精度);</li><li>重复步骤4,直至计算出所有拍摄角度下的球心坐标;</li><li>基于步骤5,在三维空间中拟合圆;</li><li>提取三维圆形的圆心和法向量==>标定结果。</li></ol><p>为了进一步提高精度,可以重复步骤1--7,取多次测量结果的均值作为最终的标定结果。</p><h2>测试</h2><p>将标准球的标定结果应用于标准球,实验结果如下:</p><p><img src="/img/bVcYR50" alt="" title=""><br>由上图可见,标定过程基本正确,结果基本可接受。</p><p>测试人头数据:</p><p><img src="/img/bVcYR51" alt="" title=""></p><p>本篇文章的记录基本都能够基于PCL实现,所以不再贴源码。</p><h2>小节</h2><p>本篇文章结合前面几篇,理论上基本可以搭建一个转台式三维扫描仪,当然,还有很多很多地方需要改进。</p><p>最近一系列文章涵盖了3D标定(本文)、<a href="https://segmentfault.com/a/1190000041046057">点云非线性全局优化</a>、<a href="https://segmentfault.com/a/1190000041341515">全局点云融合</a>、<a href="https://segmentfault.com/a/1190000041341515">三维网格模型纹理映射</a>等几个方面的基本要点及基础理论。虽然目前都已实现,但还有待更进一步的深入挖掘。</p>
多视角三维模型纹理映射 02
https://segmentfault.com/a/1190000041341663
2022-01-26T09:56:45+08:00
2022-01-26T09:56:45+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
3
<h2>前言</h2><p>通过前篇文章<a href="https://segmentfault.com/a/1190000041341515">《多视角三维模型纹理映射 01》</a>,基本对OpenMVS框架和使用方法有了一个简单的理解,这里继续基于上一篇文章进行自己的探究,也对上篇文章中留下的问题进行解释。</p><p>已知:</p><p>1、手头有从8个角度拍摄的点云数据,且点云已经经过配准、融合、网格化形成了Mesh;</p><p>2、8个<strong>点云的外参</strong>(确认正确无误);</p><p>3、8个角度的图片;</p><p>原始点云是这样的:</p><table><img src="/img/bVcXCYY" alt="" title=""></table><p>点云配准后如上篇文章<a href="https://segmentfault.com/a/1190000041046057#item-4-7">《G2O与多视角点云全局配准优化》</a>图片所示。</p><h2>实现</h2><p>基于OpenMVS,主要实现代码片段如下:</p><pre><code class="c++">void texture() {
int number_of_thread = 1;
Scene scene(number_of_thread);
Eigen::Matrix4d temp;
std::string t_inPath = "./test/scene/1_in.txt";
loadMat4(t_inPath, temp);
Eigen::Matrix3d _k = temp.block<3, 3>(0, 0); //相机内参
//归一化
_k(0, 0) = _k(0, 0) / 1600;
_k(1, 1) = _k(1, 1) / 1600;
_k(0, 2) = _k(0, 2) / 1600;
_k(1, 2) = _k(1, 2) / 1600;
{ //填充platform
Platform &plat = scene.platforms.AddEmpty();
//1、name
plat.name = "platform";
CameraIntern &cam = plat.cameras.AddEmpty();
cam.R = RMatrix::IDENTITY;
cam.C = Point3(0, 0, 0);
cam.K = _k;
//已知 有8个相机位姿
std::string matrix_path = "./test/scene/";
for (int i = 1; i <= vieNum; ++i)
{
std::string _path = matrix_path + std::to_string(i) + "_ex.txt";
Eigen::Matrix4d temp;
loadMat4(_path, temp);
Platform::Pose &pose = plat.poses.AddEmpty();
pose.C = temp.block<3, 1>(0, 3);
pose.R = temp.block<3, 3>(0, 0);
}
}
{//填充images
std::string imag_path = "test/image/";
std::string matrix_path = "test/scene/";
//ImageArr imgarr = scene.images;
for (int i = 1; i <= vieNum; ++i) {
std::string t_img = imag_path + std::to_string(i) + ".jpg";
String _imgP(t_img);
Image &_img = scene.images.AddEmpty();
_img.ID = i-1;
_img.platformID = 0;
_img.cameraID = 0;
_img.poseID = i-1;
_img.LoadImage(_imgP);
scene.images.push_back(_img);
}
}
scene.mesh.Load("test/sm_mesh.ply");
unsigned nResolutionLevel = 0;
unsigned nMinResolution = 1280;
float fOutlierThreshold = 0.f;
float fRatioDataSmoothness = 0.0f;
bool bGlobalSeamLeveling = true;
bool bLocalSeamLeveling = true;
unsigned nTextureSizeMultiple = 0;
unsigned nRectPackingHeuristic = 0;
bool res = scene.TextureMesh(nResolutionLevel, nMinResolution, fOutlierThreshold,
fRatioDataSmoothness, bGlobalSeamLeveling, bLocalSeamLeveling,
nTextureSizeMultiple, nRectPackingHeuristic);
std::cout << "texture res:" << res << std::endl;
//scene.Save("./test/res_tex.mvs",ARCHIVE_TEXT);
scene.mesh.Save("test/res_tex.ply");
}</code></pre><p>按照上篇文章<a href="https://segmentfault.com/a/1190000041341515">《多视角三维模型纹理映射 01》</a>中所述,需要填充<code>Scene</code>中的相关数据成员。在这里我添加了一个<code>Platform</code>以及一个<code>camera</code>,然后添加了8个相机的<code>Pose</code>,这是符合自己的实际使用情况的,我有一个相机,拍摄了目标的8个角度的图像,所以我只有一个平台,一个相机,但是<strong>恢复</strong>了8个相机的位姿。</p><p>然后就是填充了8个<code>Image</code>,每一个<code>Image</code>的<code>poseID</code>需要和<code>Platform</code>中的<code>Pose</code>严格对应。</p><p>最后填充<code>Mesh</code>,直接使用了<code>Load()</code>函数。</p><h2>释疑</h2><p>针对上篇文章中的两个问题,以及自己为什么在这里以上述方式填充<code>Platform</code>,主要原因在于:</p><p>进入<code>scene.TextureMesh()</code>代码实现部分,在其视图选择模块中有</p><pre><code class="c++"> imageData.UpdateCamera(scene.platforms);</code></pre><p>一块代码,显然这是更新相机参数,更确切的,这是更新<code>Image</code>类中成员<code>camera</code>的;进一步的进入该代码:</p><pre><code class="c++">// compute the camera extrinsics from the platform pose and the relative camera pose to the platform
//从platform计算相机外参
Camera Image::GetCamera(const PlatformArr& platforms, const Image8U::Size& resolution) const
{
ASSERT(platformID != NO_ID);
ASSERT(cameraID != NO_ID);
ASSERT(poseID != NO_ID);
// compute the normalized absolute camera pose
//根据platformid提取该image对应的platform信息
const Platform& platform = platforms[platformID];
Camera camera(platform.GetCamera(cameraID, poseID));
// compute the unnormalized camera
//计算原始相机内参(归一化前的,真实相机内参)
camera.K = camera.GetK<REAL>(resolution.width, resolution.height);
//将相机内外惨整合为3*4的仿射矩阵(P=KR[I|-C])
camera.ComposeP();
return camera;
} // GetCamera
void Image::UpdateCamera(const PlatformArr& platforms)
{
camera = GetCamera(platforms, Image8U::Size(width, height));
} // UpdateCamera</code></pre><p>从上述代码块可见,<code>Image</code>类中成员<code>camera</code>本质是从<code>Platform</code>中根据<strong>对应的</strong><code>ID</code>提取计算的,也就是说<code>Image::camera</code>是依赖<code>Platform</code>的!最后进入<code>platform.GetCamera(cameraID, poseID)</code>代码实现部分:</p><pre><code class="c++">// return the normalized absolute camera pose
Platform::Camera Platform::GetCamera(uint32_t cameraID, uint32_t poseID) const
{
const Camera& camera = cameras[cameraID];
const Pose& pose = poses[poseID];
// add the relative camera pose to the platform
Camera cam;
cam.K = camera.K;
cam.R = camera.R*pose.R;
cam.C = pose.R.t()*camera.C+pose.C;
return cam;
} // GetCamera</code></pre><p>从上述代码可见,真实参与纹理映射的是<code>Image</code>中<code>camera</code>,该<code>camera</code>的外参由<code>Platform</code>的<code>camera</code>和对应<code>pose</code>共同决定。</p><p>至此上述代码已经解释了:</p><ol><li>上篇【问题1】:每张纹理图片对应的相机位姿其实是由<code>Platform</code>中的相机和<code>Platform</code>中的位姿决定的</li><li>上篇【问题2】:没有必要在 ”外” 代码中填充实现<code>Image</code>中的<code>camera</code>,无论怎么样填充该数据,它都会被Platform中的属性所覆盖。当然,前提是你已经正确填充了<code>Paltform</code>.</li><li>上述源代码也解释了为什么在自己代码中,我只是创造了一个<code>Platform</code>的<code>Camera</code>,并且所创造的<code>Camera</code>旋转矩阵为单位矩阵,平移矩阵为0的原因---<code>Platform</code>的<code>Camera</code>和<code>Pose</code>会同时参与<code>Image</code>中<code>camera</code>的计算,此时自己所填充的<code>Platform</code>的<code>pose</code>已经是正确且真实对应<code>Image</code>的位姿矩阵,所以没必要也无法再去填充<code>Platfrom</code>的<code>camera</code>。</li></ol><h2>实验</h2><p>恢复的相机位姿与全局配准点云的关系如下:</p><table><tr><img src="/img/bVcXC0J" alt="" title=""></tr></table><p>Mesh结果如图:</p><table><tr><img src="/img/bVcXC0U" alt="" title=""></tr></table><p>Mesh对应的合成纹理如下:</p><table><img src="/img/bVcXC0V" alt="" title=""></table><h2>反思、总结</h2><p>上述自己关于OpenMVS的理解,也不尽然完全正确,还有待进一步的提升,只是目前暂时达到了自己初步预想结果。</p><p>再来说一说<strong>外参</strong>。点云的外参表示点云的运动,相对的,点云外参的逆则表示所对应相机的运动,将所有点云做全局配准统一到世界坐标系下之后,每块点云外参的逆则代表了其所对应的相机在世界坐标系下的位姿矩阵。</p><p>另外,自己所使用的相机为RGB--D相机,即红外相机负责生成点云,RGB相机负责为点云提供纹理图,也就是说初始点云的坐标系是在红外相机下的,而纹理图则属于RGB相机,所以该RGB—D相机模组之间还有红外相机和RGB彩色相机的之间的标定,这里的标定矩阵,本质是点云的外参---将点云变换到RGB相机下才能贴图嘛!也正是因为使用的是RGB-D类型的相机,所以OpenMVS所需的矩阵才需要自己手动去填充(个人理解:OpenMVS是直接从RGB图像中恢复三维模型,然后贴图,不存在额外的点云到RGB的外参)。</p><p>再扯远一点,<a href="https://link.segmentfault.com/?enc=z%2F6yCr4p5gD%2BsEQ9MO0xKg%3D%3D.VlZ85pjGwdNhPQOx9UgmCdTlda7%2BB%2FDP9FyEvRL9g4vlfRzj2Ybm1jHRy9O7Ug6CGnPDBMM8U78%2BCXwjjdc5Tg%3D%3D" rel="nofollow">TexRecon</a>也是专门用来解决网格纹理映射,它的输入也是mesh +camera+纹理图,与OpenMVS中不同,TexRecon中的<code>camera</code>其实是用点云的外参来填充!!但是TexRecon在进行纹理映射时候会对原始网格进行删减,可能会导致原本光滑的网格产生额外的孔洞,另外TexRecon依赖第三方(MVE等)比较多,这也是自己没有更加深入源码探讨的原因。</p><p>(TexRecon效果不是很好,不贴图了,或是自己使用并不是完全正确)</p><p>《Let There Be Color! Large-Scale Texturing of 3D Reconstructions》</p><p>留坑:</p><p>后续若有时间、精力,则进一步记录自己之前对转台的标定实现过程,也算是这几篇文章的前传吧。</p>
多视角三维模型纹理映射 01
https://segmentfault.com/a/1190000041341515
2022-01-26T09:30:52+08:00
2022-01-26T09:30:52+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<p>本片文章算是作为上一篇文章<a href="https://segmentfault.com/a/1190000041046057">【G2O与多视角点云全局配准优化】</a> 的延续。<br>从上篇文章杠已知,现在目前手头已经有了配准好的全局点云、每块点云的变换矩阵以及对应每块点云的RGB图像。接下来,自然而然的便是:对全局点云重建三维网格,并对重建后的三维网格进行纹理贴图。<br>已知:三维模型(三角面片,格式任意),多视角RGB图片(纹理)、变换矩阵;求:为三维模型贴上纹理图。<br>针对上述待求解纹理,熟悉的小伙伴肯定直到,其实在求解多视角立体匹配的最后一步--表面重建与纹理生成。<br>接上篇,为完成自己的目的,首先需要重构三角网格,从点云重建三角网格,与相机、纹理图片等没有任何关系,可直接使用pcl中的重建接口或者cgal甚至meshlab等软件,直接由三维点云重建三角网格。<br>在完成重建三角网格之前,还有很重要--但是可以酌情省略的一个关键步骤:多视角三维点云融合!下面继续使用自己之前所拍摄的8个点云片段为例进行示例记录。(注意,此处强调8个点云片段,并不表示8个视角的点云,后续说明)。</p><h2>多视角点云融合</h2><p>将多个点云片段拼接之后,无法避免的存在多个视角下点云相互重叠的情况。如下图所示,在重叠区域的点云疏密程度一般都大于非重叠区域点云。进行点云融合的目的一是为了点云去重,降低点云数据量;二是为了进一步平滑点云,提高精度,为后续的其他计算(如测量等)提供高质量的数据。</p><table><tr><img src="/img/bVcXCYo" alt="重叠" title="重叠"></tr></table><p>恰巧,自己对移动最小二乘算法有一定了解,在pcl中,N年之前【<a href="https://segmentfault.com/a/1190000007938371">pcl之MLS平滑</a>】和【<a href="https://segmentfault.com/a/1190000007941880">pcl之MLS算法计算并统一法向量</a>】也做过一些小测试。在我的印象中,移动最小二乘是这样工作的:针对深入数据,计算目标点及其邻域,这些待计算的局部数据便构成了目标点的<strong>紧支域</strong>,在<strong>紧支域</strong>内有某个函数对目标点进行运算,运算的基本规则是依据<strong>紧支域</strong>内其他点到目标点的<strong>权重</strong>不同,这里的<strong>某个函数</strong>即所谓的<strong>紧支函数</strong>。<strong>紧支函数 + 紧支域</strong> <strong>+权重函数</strong>构成了移动最小二乘算法的基本数学概念,所谓<strong>移动性</strong>则体现在<strong>紧支域</strong>在允许的空间中“滑动”计算,直至覆盖所有数据。最小二乘一般针对全局求最优,而移动最小二乘由于其 “移动性”(紧支)不仅能针对全局优化求解,而且也具有局部优化性,更进一步的,针对三维点云,其能够提取等值面,结合MC(移动立方体)算法,实现表面三角网格的重建。</p><p>pcl中,关于MLS算法的例子很多很多,自己之前的两篇水文也算也有个基本的应用介绍,此处不在过多记录。在这里,直接使用如下代码:</p><pre><code class="c++">pcl::PointCloud<pcl::PointXYZRGB> mls_points; //存结果
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZRGB>);
pcl::MovingLeastSquares<pcl::PointXYZRGB, pcl::PointXYZRGB> mls;
mls.setComputeNormals(false);
mls.setInputCloud(res);
mls.setPolynomialOrder(2); //MLS拟合的阶数,
mls.setSearchMethod(tree);
mls.setSearchRadius(3);
mls.setUpsamplingMethod(pcl::MovingLeastSquares<pcl::PointXYZRGB, pcl::PointXYZRGB>::UpsamplingMethod::VOXEL_GRID_DILATION);
mls.setDilationIterations(0); //设置VOXEL_GRID_DILATION方法的迭代次数
mls.process(mls_points);</code></pre><p>注意,上述计算过程中,使用了<code>pcl::MovingLeastSquares::VOXEL_GRID_DILATION</code>方法,按照pcl官方解释,该方法不仅能够修复少量点云空洞,而且能够对点云坐标进行局部优化,输出为全局疏密程度一样的点云,通过设置不同的迭代次数,该方法不仅能够<code>降采样</code>,还能<code>上采样</code>(主要通过迭代次数控制).</p><p>将自己的数据放入上述计算过程,点云融合局部效果如下:</p><table><tr><td> <img src="/img/bVcXCYy" alt="" title=""></td>
</tr></table><p>肉眼可见点云更加均匀、平滑,同同时数据量从100w+ 降到 10w+.</p><p>上述操作,基本满足了自己下一步的要求。</p><p>进一步的,鉴于表面重建并非重点,针对上述结果,这里直接使用MeshLab软件中的泊松重建,结果记录如下:</p><table><img src="/img/bVcXCYK" alt="" title=""></table><h2>多视角纹理映射</h2><p>在这里,正式引入自己对所谓“多视角”的理解。</p><p>首先,<strong>多视角</strong>与<strong>多角度</strong>,通常情况下,这两个概念基本都是在说同一件事情,即从不同方位角拍摄三维模型。但是这其中又隐含两种不同的 [操作方式],其一如手持式三维扫描仪、SLAM中的状态估计、甚至搭载摄像机/雷达的自动驾驶汽车等,这些场景下基本都是<strong>相机在动,目标不动</strong>,即相机绕目标运动;其二 如转台式三维扫描仪等,这些场景基本都是<strong>相机不动,目标动</strong>,即目标本身具有运动属性。所以这里,有必要对<strong>多视角</strong>与<strong>多角度</strong>做进一步的区分,<strong>多视角</strong>指的是相机的多视角(对呀,相机才具有真实的物理视角、位姿、拍摄角度 巴拉巴拉...),<strong>多角度</strong>指的是目标物体的不同角度(对呀,一个物体可以从不同的角度被观察)。</p><p>其次,<strong>外参</strong>,外参是很重要的一个概念(废话,还用你说!),可真的引起其他使用者足够的重视吗?未必! 我们一般常说的外参,其实是带有主语的,只是我们太习以为常从而把主语省略了。外参---一般指<strong>相机的外参</strong>,即描述相机的变化。在多视角三维点云配准中,每个点云都有一个变化矩阵,该变化矩阵可以称其为<strong>点云的外参</strong>,即描述点云的变化。至此,至少出现了两个外参,且这两个外参所代表的物理意义完全不一样,但是又有千丝万缕的联系。我们都知道,宏观下运动是相互的,则必然点云的外参和<strong>对应</strong>相机的外参互逆。</p><p>注: 之所以这里对上述概念做严格区分,主要还是因为自己之前对上述所提到的概念没有真正深入理解,尤其是对外参的理解,导致后期算法计算出现错误;其二还是后续库、框架等对上述有严格区分。</p><h2>OpenMVS与纹理映射</h2><p>终于到了OpenMVS。。。。</p><p>已知OpenMVS可以用来稠密重建(点云)、Mesh重建(点云-->网格)、Mesh优化(非流行处理、补洞等)、网格纹理映射,正好对应源码自带的几个APP。</p><p>此处,目的只是单纯的需要OpenMVS的网格纹理映射功能!浏览国内外有关OpenMVS的使用方法,尤其国内,基本都是”一条龙“服务,总结使用流程就是 colmap/OpenMVG/visualSFM...+ OpenMVS,基本都是使用可执行文件的傻瓜式方式(网上一查一大堆),而且基本都是翻来覆去的相互”引用“(抄袭),显然不符合自己要求与目的。吐个槽。。。</p><p>为了使用Openmvs的纹理映射模块,输入数据必须是 <code>.mvs</code>格式文件,额。。。<code>.mvs</code>是个什么鬼,我没有啊,怎么办?!</p><p>好吧,进入OpenMVS源码吧,用自己的数据去填充OpenMVS所需的数据接口。(Window10 + VS2017+OpenMVS编译省略,主要是自己当时编译的时候没做记录,不过CMake工程一般不太复杂)。</p><h3>场景Scene</h3><p>查看OpenMVS自带的几个例子,发现其必须要填充<code>Scene</code>类,针对自己所面对的问题,<code>Scene</code>类的主要结构如下:</p><pre><code class="c++">class MVS_API Scene
{
public:
PlatformArr platforms; //相机参数和位姿 // camera platforms, each containing the mounted cameras and all known poses
ImageArr images; //纹理图,和相机对应 // images, each referencing a platform's camera pose
PointCloud pointcloud; //点云 // point-cloud (sparse or dense), each containing the point position and the views seeing it
Mesh mesh; //网格 // mesh, represented as vertices and triangles, constructed from the input point-cloud
unsigned nCalibratedImages; // number of valid images
unsigned nMaxThreads; // maximum number of threads used to distribute the work load
... //省略代码
bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f, bool bGlobalSeamLeveling=true, bool bLocalSeamLeveling=true, unsigned nTextureSizeMultiple=0, unsigned nRectPackingHeuristic=3, Pixel8U colEmpty=Pixel8U(255,127,39));
... //省略代码
}</code></pre><p>自己所需要的主要函数为<code>bool TextureMesh()</code>,可见其自带了很多参数,参数意义后期使用时再讨论。</p><h4>Platforms</h4><p>首先来看<code>platforms</code>,这个东西是<code>Platform</code>的数组。在OpenMVS中,<code>Platform</code>定义如下:</p><pre><code class="c++">class MVS_API Platform
{
...
public:
String name; // platform's name
CameraArr cameras; // cameras mounted on the platform
PoseArr poses;
...
}</code></pre><p>对我们而言,必须需要填充<code>CameraArr</code>和<code>PoseArr</code>两个数组。</p><p><code>CameraArr</code>是<code>CameraIntern</code>类型的数组。<code>CameraIntern</code>是最基本的相机父类,一提到相机,则必须包含两个矩阵:内参和外参,<code>CameraIntern</code>也不例外,它需要使用如下三个参数填充,其中<code>K</code>为归一化的3X3相机内参,可用<code>Eigen</code>中的矩阵填充,所谓归一化,其实就是该内参矩阵的每个元素除以纹理图片的最大宽度或高度;<code>R</code>顾名思义,表示<strong>相机的旋转</strong>,<code>C</code>表示<strong>相机的平移</strong>,R和C共同构成了<strong>相机的外参</strong>。</p><pre><code class="c++"> KMatrix K; //相机内参(归一化后的) the intrinsic camera parameters (3x3)
RMatrix R; //外参:相机旋转 rotation (3x3) and
CMatrix C;</code></pre><p><code>PoseArr</code>是<code>Pose</code>类型的数组。<code>Pose</code>是类<code>Platform</code>中定义的一个结构体:</p><pre><code class="c++">struct Pose {
RMatrix R; // platform's rotation matrix
CMatrix C; // platform's translation vector in the global coordinate system
#ifdef _USE_BOOST
template <class Archive>
void serialize(Archive& ar, const unsigned int /*version*/) {
ar & R;
ar & C;
}
#endif
};
typedef CLISTDEF0IDX(Pose,uint32_t) PoseArr;</code></pre><p>从上述<code>Pose</code>中可以看出,其也包含了两个矩阵,但是这里的<code>Pose</code>中的矩阵所表示的物理意义和<code>CameraIntern</code>中外参的物理意义完全不同,简单来说,<font color=red><strong><code>CameraIntern</code>中表示的是相机本身固有或自带的属性,而<code>Pose</code>则表示整个<code>Platform</code>平台(包含相机的)在世界坐标系中位姿矩阵,针对每一张纹理图(下面介绍),这两个属性参数共同构成了对应相机的真实外参</strong></font>(后期通过源代码解释)<strong>【问题1】</strong>。</p><h4>Images</h4><p><code>images</code>是<code>ImageArr</code>类型的数组,如同源码中解释,每个<code>Image</code>对应于每个相机位姿。<code>Image</code>的结构如下:</p><pre><code class="c++">class MVS_API Image
{
public:
uint32_t platformID;//plateform相对应的ID // ID of the associated platform
uint32_t cameraID; // camer对应的ID //ID of the associated camera on the associated platform
uint32_t poseID; // 位姿ID //ID of the pose of the associated platform
uint32_t ID; // global ID of the image
String name; // 该imgage的路径 // image file name (relative path)
Camera camera; // view's pose
uint32_t width, height; // image size
Image8U3 image; //load的时候已经处理. image color pixels
ViewScoreArr neighbors; // scored neighbor images
float scale; // image scale relative to the original size
float avgDepth;
....
}</code></pre><p>如上述自己添加的中文注释,其中<font color =red><code>platformID、cameraID和poseID</code>三个参数一定要与<code>Platform</code>中的成员属性严格对应</font>,对自身而言这是最重要的三个参数,<code>Platform</code>中的成员变量本身并不携带ID属性,只是根据添加顺序默认排序,ID索引从0开始递增。</p><p>在<code>Image</code>结构中,还有不得不提的<code>camera</code>成员,如源码中所注释,它表示视角(Camera)的位姿,也就是哪个相机对应于该图片。乍一看该<code>camera</code>成员也必须要进行填充,可事实并非如此,原因后期解释<strong>【问题2】</strong>。</p><p>该结构中其他成员,如图像宽度高度、图像的name等,在调用<code>Image::LoadImage(img_path);</code>函数的的时候会自动填充;<code>neighbors</code>成员表示图像于3D点的邻接关系,在纹理贴图中非必要选项,而且不影响纹理贴图效果。</p><h4>PointCloud</h4><p>该成员表示点云,一般作为OpenMVS的稀疏重建或稠密重建的结果,对于网格不具备约束性,直接舍弃不填充。</p><h4>Mesh</h4><p>OpenMVS中的三角网格存储结构,我只需要知道其有Load函数即可。</p>
G2O与多视角点云全局配准优化
https://segmentfault.com/a/1190000041046057
2021-12-01T14:16:38+08:00
2021-12-01T14:16:38+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
2
<h2>背景</h2><p>自己手头有一堆经过初始配准后的三维散乱点云,经过粗配后,全部点云基本统一在了同一个坐标系下,如图1和图2所示。为了获取更好的全局结果,需要对粗配后的点云进行全局配准优化。</p><table><tr><td> <img src="/img/bVcWn9n" alt="全局优化前_底.png" title="全局优化前_底.png">图1 点云底面 </td>
<td> <img src="/img/bVcWn3K" alt="" title="">图2 局部细节 </td>
</tr></table><p>上述点云是基于pcl进行显示,不同的颜色代表不同的点云块,从图1可以明显看出,全局点云"融合"(其实并未融合,只是全部加载显示),效果较差,从图2可以看出,针对[耳朵]部分出现较大错位。</p><p>因为自己之前对<a href="https://link.segmentfault.com/?enc=RYEiwCh3gOZyEx41ZY4JNQ%3D%3D.OeZq%2BejdO2ffbNhbY%2BS15PYTTIYDiixR4QS3doddK1O6xcwHXiTVw%2Fj57Mb4qoeX" rel="nofollow">G2O</a>多多少少有一点了解,但是也并没有进行过多深入的研究。知道<strong>G2O可以用来解决全局优化问题</strong>,正好和自己要求解的问题非常非常相似。</p><p>因此,便毅然决然的选择使用G2O来针对自己的问题构建解决方案。</p><h2>G2O的进一步了解</h2><p>G2O是将<strong>非线性优化和图论</strong>相结合的产物,大多时候用于解决slam后端优化问题,其自身携带的例子大多也与slam有关。g2o中所谓的<strong>图</strong>,就是一堆<strong>节点</strong>和<strong>边</strong>按照一定的关系链接而成。其中,<strong>节点是要优化的目标,边是各个优化目标之间的关系</strong>(也称其误差项,在这里自己更喜欢称为 【关系】)。</p><p>基于CMake + VS2017 完成G2O库的安装,安装过程没有做详细记录,基本百度能够解决。</p><p>安装完g2o后,按照习惯去翻看其自身源码中所携带的example,以便寻找灵感,在example目录中一眼便看中了【<a href="https://link.segmentfault.com/?enc=aw7skxUHqh%2FLu%2ByJX7UL2Q%3D%3D.gdSLQ50Babyiabg5uJyw1UQ5gGP06UigC3B5tYR%2FOO6pnkdh5vbJ8RIF88IoS%2FnD9CvM3exgbCS7SYxZSNmGVCWEZ0tbnxd9hmHtsZZQmJHRnmdJyVp2FILlRhirdDQP" rel="nofollow">gicp_demo</a>】。</p><h3>G2O--gicp_demo</h3><p>g2o的使用方法基本就是:</p><ol><li>声明一个优化器;</li><li>选择求解方法;</li><li>构造图--顶点和边的关系;</li><li>优化处理。</li></ol><p><strong><code>初次看到g2o的gicp时,自认为“该gicp方法是 全局(global)的”,然而事实并非如此,事实上,其甚至不能称为是一个完整的icp问题</code></strong> </p><p>(关于上述红色字体的表示,目前仅仅是个人理解,或许有错误,也请多多留言指正,一起交流学习)</p><p>我们知道,ICP求解是一个迭代计算过程,经典ICP求解的主要步骤为:</p><ol><li>输入两片点云AB,求解对应点对(三维模型至少3个不共线的点);</li><li>基于对应点对,构造A到B的变换矩阵M;</li><li>将M作用于A,得到A*,并用A*代替A;</li><li>迭代终止条件(迭代次数或者最小误差度量);</li><li>重复步骤1--3,直到满足步骤4,终止。</li><li>输出变换矩阵M。</li></ol><p>但是细看g2o的gicp_demo,其流程并不如经典ICP求解过程一样,而更像一个ICP中步骤2的求解问题。</p><p>再来深入看看g2o中给出的gicp_demo。</p><h3>拆解gicp_demo</h3><p>首先还是直接先把g2o官方例子贴出来吧(虽然非常讨厌直接贴别人代码),便于说明。</p><p>在该Demo中,g2o首先声明并设置了优化器<code>optimizer</code>,并制作了包含1000个点的集合<code>true_points</code>作为源点云。</p><p>其次,为图添加了两个节点并设置节点ID。这里的节点类型为<code>VertexSE3</code>( <code>class G2O_TYPES_SLAM3D_API VertexSE3 : public BaseVertex<6, Isometry3></code>),也是主要的优化目标。依据节点添加到图中的顺序,将第一次添加的节点视为固定视角;<code>vc->setEstimate(cam);</code>该代码段告诉我们,真正求解的结果是存储在<code>Eigen::Isometry3d</code>类型的相机位姿(本质上是一个矩阵),这里<code>cam</code>参数的类型是<code>Eigen::Isometry3d</code>;这一步其实只是声明了两个空节点,节点参数只是单位<code>Eigen::Isometry3d</code>。</p><p>再次,为图添加了1000条边。在此过程中,首先根据节点id获取边所需要链接的两个顶点(节点),<code>vp0</code>和<code>vp1</code>,并基于<code>true_points</code> "制作" 了包含噪声的两个三维点<code>pt0</code>和 <code>pt1</code>(这一步其实已经默认<code>pt0</code>和 <code>pt1</code>为对应点对);然后 声明了一个<code>Edge_V_V_GICP</code>类型的图边结构,该边是一个g2o的二元边(<code>class G2O_TYPES_ICP_API Edge_V_V_GICP : public BaseBinaryEdge<3, EdgeGICP, VertexSE3, VertexSE3></code>),该二元边分别链接<code>vp0</code>和<code>vp1</code>;g2o还提供了<code>EdgeGICP</code>类型作为观测值,<code>EdgeGICP</code>类型可以存放对应点对。在该步骤中,一定要<strong>非常注意节点和三维坐标点的所属--对应关系</strong>。至此,基本能够将所有信息放入g2o的图中,该步骤主要关心的在于如何将自己的三维点对放入到g2o图中。</p><p>最后,初始化图关系并进行了N次迭代优化,每个节点的优化结果存储在<code>optimizer.vertices()</code>返回值类型的哈希表中,该哈希表中:键--对应节点id,值--对应节点,这里为<code>VertexSE3</code>类型,从<code>VertexSE3</code>获得的<code>Eigen::Isometry3d</code>类型是我们真正关心的结果数据。<br>该示例应该构建了如下一张超图,其中图有两个图节点,n1为固定节点,n2为变动的节点,节点之间有1000条边,每条边链接一对对应点,针对ICP问题,对应点中的固定点挂接图节点n1,变动的点挂接图节点n2:</p><table><tr><img src="/img/bVcWn0J" alt="" title=""><td> 节点--边 </td>
</tr></table><pre><code class="c++">void icp() {
double euc_noise = 0.01; // noise in position, m
//声明优化器
SparseOptimizer optimizer;
//是否打印详细信息
optimizer.setVerbose(false);
// variable-size block solver
//设定一个求解方法
g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(
g2o::make_unique<BlockSolverX>(g2o::make_unique<LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>>()));
/*g2o::OptimizationAlgorithmGaussNewton *solver = new g2o::OptimizationAlgorithmGaussNewton(
g2o::make_unique<BlockSolverX>(g2o::make_unique<LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>>()));*/
//设定优化器使用的优化方法
optimizer.setAlgorithm(solver);
//随机点坐标
vector<Vector3d> true_points;
for (size_t i = 0; i < 1000; ++i)
{
//这里从均匀分布中采样了一组数字
true_points.push_back(Vector3d((g2o::Sampler::uniformRand(0., 1.) - 0.5) * 3,
g2o::Sampler::uniformRand(0., 1.) - 0.5,
g2o::Sampler::uniformRand(0., 1.) + 10));
}
// set up two poses
//猜测: 设定两个相机位姿
int vertex_id = 0;
for (size_t i = 0; i < 2; ++i)
{
// set up rotation and translation for this node
//平移向量
Vector3d t(0, 0, i);
//四元数旋转
Quaterniond q;
q.setIdentity();
//李群(特殊欧拉群,包含旋转和平移,自己感觉和4*4矩阵类似)
Eigen::Isometry3d cam; // camera pose
cam = q;
//返回平移向量的可写表达式
cam.translation() = t;
// set up node
//李群。这里作为节点,作为优化变量
VertexSE3 *vc = new VertexSE3();
//设置顶点的估计值
vc->setEstimate(cam);
//设置该节点在 图 中的id,以便追踪
vc->setId(vertex_id); // vertex id
//打印节点的初始平移和旋转矩阵
cerr << t.transpose() << " | " << q.coeffs().transpose() << endl;
// set first cam pose fixed
if (i == 0)
vc->setFixed(true);
// add to optimizer
//优化器添加节点
optimizer.addVertex(vc);
vertex_id++;
}
// set up point matches
for (size_t i = 0; i < true_points.size(); ++i)
{
// get two poses
//获取前述添加的节点。
/* optimizer.vertices()的返回值是一个哈希表(Map)类型,本质是std::unordered_map<int, Vertex*>,
*/
VertexSE3* vp0 =
dynamic_cast<VertexSE3*>(optimizer.vertices().find(0)->second);
VertexSE3* vp1 =
dynamic_cast<VertexSE3*>(optimizer.vertices().find(1)->second);
// calculate the relative 3D position of the point
Vector3d pt0, pt1;
pt0 = vp0->estimate().inverse() * true_points[i];
pt1 = vp1->estimate().inverse() * true_points[i];
// add in noise
//添加高斯噪声
pt0 += Vector3d(g2o::Sampler::gaussRand(0., euc_noise),
g2o::Sampler::gaussRand(0., euc_noise),
g2o::Sampler::gaussRand(0., euc_noise));
pt1 += Vector3d(g2o::Sampler::gaussRand(0., euc_noise),
g2o::Sampler::gaussRand(0., euc_noise),
g2o::Sampler::gaussRand(0., euc_noise));
// form edge, with normals in varioius positions
Vector3d nm0, nm1;
nm0 << 0, i, 1;
nm1 << 0, i, 1;
nm0.normalize();
nm1.normalize();
//g20的二元边
Edge_V_V_GICP * e // new edge with correct cohort for caching
= new Edge_V_V_GICP();
e->setVertex(0, vp0); // first viewpoint
e->setVertex(1, vp1); // second viewpoint
EdgeGICP meas;
meas.pos0 = pt0;
meas.pos1 = pt1;
meas.normal0 = nm0;
meas.normal1 = nm1;
//定义观测值
e->setMeasurement(meas);
// e->inverseMeasurement().pos() = -kp;
meas = e->measurement();
// use this for point-plane
//约束信息(协方差矩阵的逆) = 点面的精度矩阵信息
e->information() = meas.prec0(0.01);
optimizer.addEdge(e);
}
// move second cam off of its true position
//变换第二个点云。
VertexSE3* vc =
dynamic_cast<VertexSE3*>(optimizer.vertices().find(1)->second);
Eigen::Isometry3d cam = vc->estimate();
cam.translation() = Vector3d(0, 0, 0.2);
vc->setEstimate(cam);
//初始化整个图结构
optimizer.initializeOptimization();
//计算所有边的误差向量
optimizer.computeActiveErrors();
//输出优化前的误差平方
cout << "Initial chi2(before opt) = " << FIXED(optimizer.chi2()) << endl;
optimizer.setVerbose(true);
optimizer.optimize(5);
//输出优化前的误差平方
cout << "Initial chi2(after opt) = " << FIXED(optimizer.chi2()) << endl;
//打印变化矩阵
cout << endl << "Second vertex should be near 0,0,1" << endl;
cout << dynamic_cast<VertexSE3*>(optimizer.vertices().find(0)->second)
->estimate().translation().transpose() << endl;
cout << dynamic_cast<VertexSE3*>(optimizer.vertices().find(1)->second)
->estimate().translation().transpose() << endl;
}</code></pre><h3>测试</h3><p>执行g2o自带的上述例子,最终能够打印出变换矩阵。然后将该例子单独摘出来,换入自己的数据(对应点对),也能输出变换矩阵,然后将变换矩阵作用于点云 ,结果如图3:</p><table><tr><img src="/img/bVcWn04" alt="aa" title="aa"><td> 图 3 g2o--gicp </td>
</tr></table><p>可能早有大神预料到会是如上结果!!不得不说,g2o的优化结果也太差强人意......</p><p>真是这样吗? ......</p><h3>小结</h3><p>如果按照上述流程,依照g2o的官方例子,结果真是那样!!!但是和pcl的icp对比,结果是真的差,问题出在哪里?</p><p>问题出在上文中红字部分,这里依然用红字提醒自己---<strong><code>g2o的gicp并不能算完全的ICP求解方案</code></strong>。</p><p>通过分析官方代码例子可以发现,其在求解前,本质上已经默认了输入点对是对应的,在此基础上进行迭代计算,本质是依据同一组对应点对,迭代计算该组对应点之间的最优变换矩阵,对于整体两片点云来说,这样其实只是完成了一次icp计算,结果当然不理想。</p><p>换句话说,该Demo遗漏(或者g2o本就如此设计,或是自己了解不够)ICP迭代方案中对应点对的计算过程,也就是缺少了<strong>步骤1</strong>,g2o--gicp只是单纯的计算了<strong>步骤2</strong>,只得到了单次的最优变换矩阵。对于整体两片点云的icp求解问题,在进行第2次求解时,对应点对的对应关系已经发生了变化,因此g2o---gicp_demo得到的矩阵只是单次的最优矩阵,所以结果也就如图3所示。</p><p>那么如何得到整体最优结果呢? 当然是将上述步骤放入大循环中,每计算完一次g2o--gicp,变换点云,重新求解对应关系,依次迭代计算。结果图省略...</p><h2>构建优化图</h2><p>在充分理解了g2o自带icp例子后,回到最初自己要求解的问题。有N片粗配后的散乱点云,想要对<strong>全局点云</strong>进行全局优化配准。</p><p>所谓粗配后的全局点云,具有以下特点:</p><ol><li>相邻两片点云之间具有较高重叠率;</li><li>相隔(至少一块点云)点云之间有或没有重叠;</li><li>每块点云可以有一个、零个(这个条件可以存在,构造g2o图时与条件1并不冲突)或者多个高重叠率的点云;</li><li>点云之间的重叠关系是对应的(即:A与B 、C、D重叠,那么BCD的重叠点云中一定也有A)。</li></ol><h3>优化目标</h3><p>上述谈到的优化目标只是我们感性上的认识,但是还必须要将优化目标转化为数学表达。口述如下:<strong>优化目标 = 求解--N片三维散乱点云,以点云A为目标点云,计算所有三维散乱点云配准到A的变换矩阵,该变换矩阵使得所有对应点对的欧式距离取得最小,或者达到指定的迭代变换次数。</strong></p><p>上述优化目标隐含:若A与C没有重叠,则C无法直接向A进行配准对齐,但是A与B,B与C有重叠,则C变换到A则需要先变换到B,由B的矩阵再变化到A,而且要保证A与B,B与C均为最优变换,换句话来说,就是B最优变换到A,C最优变换到B,则完成了A B C之间最优变换。</p><p>确认优化目标的数学表达后,还需要确认点云间的重叠率表达,口述如下<strong>:重叠率 = 两两点云对应点对的数量与该两片点云平均点数的比值。</strong></p><p>完成上述两个数学定义后,程序的总体流程应该如下:</p><ol><li>计算全局点云相互之间的对应点对与重叠率;</li><li>重叠点云筛选(重叠率较低的点云认为无重叠,不参与g2o图中边的构建);</li><li><p>g2o全局构图:</p><ul><li>图节点</li><li><strong>边</strong>(重点)</li><li>优化器与优化算法</li></ul></li><li>基于步骤3的输出矩阵,更新全部点云的坐标;</li><li>重复步骤1-4,直到满足终止条件。</li></ol><p>最终输出为优化后的全局点云及对应的变换矩阵。</p><h3>程序实现</h3><p>工程中使用了PCL点云库和g2o(废话),其中pcl主要用于计算点云重叠率。</p><p>通过上述【g2o--gicp_demo】可知,g2o中已经为ICP方案提供了定义好的图节点类<code>VertexSE3</code>和图二元边类<code>Edge_V_V_GICP</code> 以及边类<code>EdgeGICP</code>,这里直接拿来使用,省去了自定义图边、图节点麻烦(当然,若深入学习g2o,还是建议多做更多探索)。</p><h4>点云数据结构</h4><p>通过前述分析,可知,某点云结构应该包含如下必要内容:</p><ol><li>点集;</li><li>是否固定;</li><li>变换矩阵;</li><li>邻接信息;</li><li>必要方法(最近邻点云、对应点对计算)。</li></ol><p>构造如下:</p><pre><code class="c++">typedef pcl::PointCloud<pcl::PointXYZ> pointcloud;
class MyPnts
{
public:
MyPnts() ;
~MyPnts();
int id;
int v_number;
vector<Vector3d> pts;
bool fixed = false;
Isometry3d pose; //该点云的初始位姿,也是优化目标
//该点云的所有邻接信息
vector<OutgoingEdge*> neighbours;
//当前点云在所有点云序列中的k个最近邻点云(重叠率最高的前K个)
void computeKNNearPnts(vector< std::shared_ptr<MyPnts>>& frames, int k);
//对应点对计算
void computeClosestPointsToNeighbours(vector< shared_ptr<MyPnts>>& frames, float thresh);
/*
* Method: calCorrespond 计算两片点云的相互对应点对的索引
* Access: public
* Returns: std::unordered_map<int, std::vector<int>> first 0->src,1 -> tar;
* Parameter: pointcloud::Ptr src
* Parameter: pointcloud::Ptr tar
*/
std::unordered_map<int, std::vector<int>> calCorrespond(MyPnts src, MyPnts tar,double dst = 10.0);
private:
};</code></pre><p>其中,计算当前点云与全局其他点云(除当前点云外)重叠关系、对应点对均在<code>computeKNNearPnts()</code>函数中实现。</p><h4>造图</h4><p>这是最重要也是及其容易出错的地方。</p><p>前述 [粗配后全局点云的特点],决定了g2o中图结构的特点,图边应该满足 [粗配后全局点云的特点] 的描述,<strong><em>伪图如下</em></strong>:</p><table><tr><td> <img src="/img/bVcWn2T" alt="" title=""><br>全局icp 伪图</td>
</tr></table><p>如上图所示,假设:全局有5片点云,则g2o图有5个节点,n1为固定节点,n1和n2有重叠,n2和n5有重叠,n1和n5也有重叠,但n1和n3 、n1 和 n4、 n2和n4等几个节点之间不存在重叠关系。</p><p>注意:这里的重叠关系,在g2o看来是约束关系,进一步的,是两片点云之间的边的链接关系,此边可能有成百上千个对应点对构成。上图中的带箭头的边仅表示示意,并非真正的图边。</p><p>在清楚了图结构后,构造图代码如下:</p><pre><code class="c++">void G2oPCL::global_icp2(std::vector<std::shared_ptr<MyPnts>> &pnts)
{
using namespace g2o;
using namespace std;
using namespace Eigen;
g2o::SparseOptimizer optimizer;
optimizer.setVerbose(false);
g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(
g2o::make_unique<g2o::BlockSolverX>(g2o::make_unique<g2o::LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>>()));
optimizer.setAlgorithm(solver);
//节点
for (int i = 0; i < pnts.size(); ++i) {
g2o::VertexSE3 *vc = new VertexSE3();
vc->setEstimate( (*pnts[i]).pose);
vc->setId(i); // vertex id
if (i == 0) {
vc->setFixed(true);
(*pnts[i]).fixed = true;
}
optimizer.addVertex(vc);
}
//构造全局边关系
for (int i = 0; i < pnts.size(); ++i) {
std::shared_ptr<MyPnts> &current = pnts[i];
int current_nebor = current->neighbours.size();
for (int j = 0; j < current_nebor; ++j) {
OutgoingEdge *oe = current->neighbours[j];
int nearIdx = oe->neighbourIdx;
std::shared_ptr<MyPnts> &dst = pnts[nearIdx];
g2o::VertexSE3* vp0 =
dynamic_cast<g2o::VertexSE3*>(optimizer.vertices().find(nearIdx)->second); //dstCloud
g2o::VertexSE3* vp1 =
dynamic_cast<g2o::VertexSE3*>(optimizer.vertices().find(current->id)->second); //srcCloud
for (auto cor : oe->correspondances) {
g2o::Edge_V_V_GICP * e = new g2o::Edge_V_V_GICP();
e->setVertex(0, vp0);
e->setVertex(1, vp1);
g2o::EdgeGICP meas;
meas.pos0 = dst->pts[cor.second];
meas.pos1 = current->pts[cor.first];
e->setMeasurement(meas);
//use this for point-point
e->information().setIdentity();
optimizer.addEdge(e);
}
}
}
optimizer.initializeOptimization();
optimizer.computeActiveErrors();
double chiInit = optimizer.chi2(); //stop innerround if we get 100% better
optimizer.optimize(100);
cout << "round: " << "s" << " chi2: " << FIXED(chiInit) << endl;
for (int i = 0; i < pnts.size(); ++i) {
VertexSE3 *res = dynamic_cast<VertexSE3*>(optimizer.vertices().find(i)->second);
Isometry3d transAndRot = res->estimate();
MyPnts &mypnts = *pnts[i];
for (int j = 0; j < mypnts.v_number; ++j) {
mypnts.pts[j] = transAndRot * mypnts.pts[j];
}
}
}</code></pre><p>在上述代码中,不仅构造了图结构,设置了各个节点之间的相互关系,而且还更新了点的坐标,为方便下次迭代中计算对应点对提供了方便。</p><p>至此,基于g2o解决开始提到的问题框架基本完成,最终主程序代码如下:</p><pre><code class="c++">int main()
{
std::unique_ptr<G2oPCL> gp = std::make_unique<G2oPCL>();
clock_t begin, end;
begin = clock();
std::vector<shared_ptr<MyPnts>> pnts;
loadPnts(pnts, "");
//计算与目标点云重叠率最高的两片点云
computeNumbers(pnts, 2);
int N = 40;
for (int i = 0; i <N ; i++) {
std::cout << "\n===这是第" << i << "次全局优化===\n" << std::endl;
//对应点对约束距离为5.0
computeClosestPoints(pnts, 5.0);
gp->global_icp2(pnts);
}
end = clock();
double t = (end - begin) / 1000.0;
std::cout << "\n\n 执行时间是:" << t << std::endl;
std::cout << "保存结果到本地" << std::endl;
savePnts(pnts);
}</code></pre><h2>实验测试</h2><p>完成上述准备与代码编程后,将自己的数据(图1和图2所示点云),输入优化系统中。</p><h3>测试一</h3><p>在此次测试中,<code>computeNumbers(pnts, 2)</code>第二个参数为2,也就是构建了一个<strong><em>二元超图</em></strong>(所谓<strong><em>二元超图</em></strong>,只单纯是自己的定义,完全不与其他任何g2o程序或代码或教程相符合,也不具备真实数学意义,同样不适用于其他g2o学习过程,在这里所谓二元超图,只单纯表示图中每个节点有两个关系节点,同样,每个节点只有两个约束关系),最终结果如下:</p><table><tr><td> <img src="/img/bVcWnSf" alt="全局优化后_底.png" title="全局优化后_底.png"><br>图4 点云底面</td>
<td> <img src="/img/bVcWn3D" alt="全局优化后_耳朵_消除错位.png" title="全局优化后_耳朵_消除错位.png"><br>图5 局部细节</td>
</tr></table><p>将图4 图5分别与图1 图2进行对比,肉眼可见效果提升了很多。</p><p>执行过程中截图如下:</p><table><tr><td> <img src="/img/bVcWn4p" alt="第1次.png" title="第1次.png"> 图 6</td>
<td> <img src="/img/bVcWn4q" alt="第7次.png" title="第7次.png"> 图 7</td>
<td> <img src="/img/bVcWn4w" alt="第18次.png" title="第18次.png"> 图 8</td>
<td> <img src="/img/bVcWn4y" alt="第40次.png" title="第40次.png"> 图 9</td>
</tr></table><p>观察图6--图9,因为在<code>computeNumbers(pnts, 2);</code>传入的参数为2,所以这里只计算了每块点云重叠率最高的两块,打印信息可是看出,id为0的点云重叠率最高的是id为1和id为2的,id为3的点云重叠率最高的是id为2和id为4的,也就是说,当前带点云重叠率最高的是其两片相邻的点云,这是符合自己的实际情况的;继续看,随着迭代过程次数的增加,每块点云之间的重叠率是不断变化的,这也同样符合实际情况;最后,看误差参数估计<code>ch2</code>的输出变化 <code>62w-->24w-->8w-->5w</code>(全局大概20w左右的对应点对),逐步减小,也就是说对应点对之间的全局欧式距离平方和在逐步减小,同样符合实际情况。</p><p>该二元超图结构如下:</p><table><tr><td><img src="/img/bVcWn5N" alt="1638338902(1).jpg" title="1638338902(1).jpg"> 图10 二元图</td>
</tr></table><h3>测试二</h3><p>设置<code>computeNumbers(pnts, N);</code>参数N为3时,构建一张三元超图。</p><p>在三元超图下,最终效果和图4 图5类似,同样认为完成了全局优化;期间,程序执行过程中信息输出如下:</p><table><tr><td><img src="/img/bVcWn6c" alt="san1.png" title="san1.png"></td>
<td><img src="/img/bVcWn6d" alt="san2.png" title="san2.png"></td>
</tr></table><p>根据上述打印信息可知,构造的三元图结构如下所示:</p><table><tr><td> <img src="/img/bVcWn6j" alt="微信图片_20211201103100.jpg" title="微信图片_20211201103100.jpg"></td>
</tr></table><p>至此,基本完成了自己最初的目的,欣喜....</p><h2>总结&题外话</h2><p>自己目前也只是g2o新手村普通村民,且上述描述也并非真正意义上的slam问题,所述描述中只是自己针对所面临问题探求思路,不具备教学性、更不具权威性(有些夸大),仅作个人记录与兴趣交流。</p><p>上述描述的求解方案和思路,应该可以用来求解散乱点云多视角全局配准优化问题,而且也应该算是一个通用(不仅仅限于相邻点云重叠)的求解方案。</p><p>G2O在slam后端中使用的比较多,官方提供的demo大多也是slam2d、slam3d等,但又不仅限于此。因为针对自己所面临的问题,并没有将其向slam方向深入扩展,所以也并没有从其他教程所述那样,从slam2d例程入手,而是选择了自己较为熟悉的ICP领域。</p><p>不过话又说回来,自己所面的问题从整体解决思路上看,又可以作为是一个典型的slam优化问题:已知多个视角闭环的三维点云,通过求取路标点,求解全局相机位姿。</p><p>总之,我知道:<strong>g2o可以用来解决非线性优化问题</strong>。</p><p>啰里啰唆......</p><p>(顺便吐槽图片排版是真烂)</p>
路径规划
https://segmentfault.com/a/1190000038529760
2020-12-18T14:49:40+08:00
2020-12-18T14:49:40+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<p>(这里所谈路径规划并不涉及机械臂领域)<br><code>所谓路径规划,即在地图上找到从A点到B点的可通行路线.</code>这里包含两个要素:1、地图;2、“找”--搜索算法.关于地图又包含很多,如栅格地图、六边形网格地图、可视图、连通图等,"找"的算法同样也有很多,如迪杰斯特拉算法、A*算法、遗传算法、基于概率RRT、PRM等算法.</p><h2>地图</h2><p>所谓地图,本质上是一种数据存储结构。该数据结构中存储着哪些坐标可通行,哪些不可通行。目前应用较多的且最直观的莫过于图片(jpg,png等格式),这种存储方式的地图本质上是一种栅格地图,理论上任何一张jpg或png格式的图片都可以作为路径规划中的地图,图片的栅格坐标即可作为地图坐标。事实上,目前大多数研究路径规划的方案中,都是将其数据转化为栅格图,通过建立栅格图和真实数据之间的关系实现路径的规划。<br>其实,上述中以图片作为地图,这只是一种极为特殊的栅格地图,即四边形栅格。在数据以图片的方式存储过程中,默认不得不以像素坐标存储数据,而当下像素坐标不可避免的需要符合二维笛卡尔坐标系规则.在这种特殊的四边形栅格坐标表示下,相邻点的坐标只要+1或者-1即可,坐标点之间的距离自然用欧式距离表示.<br>诚然,图片方式存储的地图有其得天独厚的优势:易于理解、直观可视、计算方便,但是这种方式存储的地图结构也有不可避免的缺点...<br>聊完最常用的四边形栅格地图(其实就是一张图片)外,<a href="https://link.segmentfault.com/?enc=Qqkkypdbt7KIhEAL0ojHWw%3D%3D.LK3nS2RgiK8xr%2B081wVgaosyScN69%2BHQDYWk2uUpnOwXWQ5NH8Srd%2FL0o7CncOdG" rel="nofollow">国外大佬</a>还研究了六边形栅格地图.相比于四边形栅格地图,六边形栅格地图在并没有那么直观,如果单纯以六边形栅格存储数据,则地图不可视.所以在<a href="https://link.segmentfault.com/?enc=QXYDoNJjSxVCv%2Bwzuzf19A%3D%3D.MS7JoKnaOh7%2Fjac3Pa6Lo7vyv41T5P8yo4adG6lEU6lzaGlphyz7w05sHlV%2BS8OI" rel="nofollow">这篇</a>文章中,作者也只是将四边形网格转化为六边形网格,并提供了坐标转换公式,同时指出了六边形网格各种坐标系之间的优劣,然后进行路径规划.<br>六边形网格地图相比四边形网格地图有一定的优势和应用场景,比如在游戏领域、在军棋推演领域等.如下两图所示,上图为六边形坐标及其邻域,下图为六边形表示的栅格地图:<br><img src="/img/bVcLJ3C" alt="图1" title="图1"><img src="/img/bVcLJ6N" alt="image.png" title="image.png"><br>从上边两个图中可是看出,六边形网格坐标表示并不是那么易于理解,起码从图上看不是那么直观.在上图中,六边形网格坐标轴的夹角是120度.<br>其实无论四边形还是六边形,在表示地图时,都必须满足一个条件:多边形能够完全覆盖地图数据.在二维坐标系统中,目前能够满足此条件的只有正四边形或者正六边形.</p><h2>路径搜索算法</h2><p>路径搜索算法有很多,这里只谈谈自己对当前路劲研究现状的理解:</p><p>1、对已有优秀路径规划算法的改进。在实际应用的过程中,任何一种算法都会面对许多困难,而在具体应用方向做出针对性的改进,可以快速有效的提升算法的性能,同时解决实际问题。</p><p>2、混合算法。路径规划的混合算法即各个算法之间的有效结合。任何一个单独的算法,都不足以解决实际问题中的所有路径规划问题,尤其是在针对一些交叉学科中出现的新问题。创造出新的算法难度大,而路径规划算法之间的优势互补可以有效提供一种解决问题的新思路。一些智能算法如群体智能算法、强化学习算法、模糊控制、神经网络等渐渐引入到路径规划问题中。这种互补式的混合算法促使了各种方法的融合发展,通过取长补短,从而产生出一系类更为优秀的算法。</p><p>3、环境建模技术和路径规划算法的结合。面对复杂的二维甚至三维连续动态环境信息时,算法所能做的是有限的,好的建模技术和优秀路径规划算法相结合将成为解决这一问题的一种方法。</p>
C++之Sqlite3
https://segmentfault.com/a/1190000021141956
2019-11-28T16:22:39+08:00
2019-11-28T16:22:39+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>一.sqlite3简介</h2><h3>1.个人浅见</h3><p>sqlite3(<a href="https://link.segmentfault.com/?enc=ccLj%2FbOAwOs6KFa3itWf7w%3D%3D.ioZ5Ks9Fmlm6OTM%2FSzENc%2FB3du1JVd8XyJmLFDst1Qhf38EjEI1WRNacq78439W1" rel="nofollow">官方文档</a>)是一种轻量级且应用广泛的关系型数据库的一种。sqlite3数据库可运行在类Unix和window系统之上,其中,大部分的类Unix系统(如Linux)都默认安装了sqlite3数据库,个人使用的系统为Ubuntu 16.04,默认情况下系统已经安装了sqlite3数据库,检测方法为:在Terminal中执行<code>sqlite3</code>,即可进入sqlite3的命令编辑模式,若失败请手动编译安装sqlite3数据库。</p><p>首先明白几个概念:</p><pre><code>1.DML(Data Manipulattion Language),顾名思义--数据操作语言。所谓数据操作,是指对数据的插入(insert),删除(delete),更新(update)等。
2.DQL(Data Query Language)--数据查询语言。DQL的基本结构是由SELECT等查询语句及其子语句组成。
3.DDL(Data Definition Language)--数据定义语言。DDL是对数据库进行创建的语言。比如创建数据库(create database),创建数据表(create table),删除数据表(drop table)等。
</code></pre><h3>2.sqlite3语法</h3><p>sqlite有其独特的语法规则。</p><pre><code>1.大小写的敏感性.在sqlite中,是不区分大小写的,例如创建表语句`CREATE TABLE tablename;`;和`create table tablename;`效果是完全一样的,再比如查询语句'select * from tablename'和'SELECT * FROM tablename'也是完全一样的。
2.所有的sqlite语句可以以任何关键字开始,`select`,`insert`,`create`等,以距离最近的 **“;”** 作为本条语句的结束标志。</code></pre><h3>3.sqlite的命令</h3><p>sqlite数据库中有一些特殊的命令,这些特殊的命令都以<code> .(点)</code>开头,命令结尾<strong>不需要</strong>以 <strong>“;”</strong> 结尾,这些特殊的命令被称为sqlite的<strong><em>点命令</em></strong>。有一个特殊的点命令<code>.help</code>,它可以列出所有sqlite的点命令及其使用说明,如图所示:<img src="/img/bVbAPiA" alt="sqlietehelp.png" title="sqlietehelp.png"></p><p><a href="https://link.segmentfault.com/?enc=FOx96GFbCuZU%2BlqSSfBmug%3D%3D.76ZZrgRNpC0h6AaibeafS77lZ37BmyLW%2FOgv66O05x9zD4TkBI4w2G7PvgniaWsXM0E2pa%2BsrbZvTvH%2BDNiBJA%3D%3D" rel="nofollow">sqlite点命令的详细介绍</a></p><h2>二.sqlite3之C++接口</h2><p>sqlite提供原生态的C/C++接口,从其<a href="https://link.segmentfault.com/?enc=h0pG64SbSSdG%2F36E%2B5DHUg%3D%3D.1G3y2eFJ%2FaZTmQBnZDfkRmvXyv%2FniJtAXFWyf5%2Fh7mn7RDGHna9Ma0zJhurYdeBc" rel="nofollow">C/C++接口文档</a>可以查看所有的接口及使用方法。简单介绍几个常用的对象和接口:</p><pre><code>1.`sqliet3`.数据库对象,类似于数据库的入口。
2.`sqlite3_stmt`.状态存储对象,按照官网解释 “ An instance of this object represents a single SQL statement that has been compiled into binary form and is ready to be evaluated.” 通俗来说,就是它是一个对象实体,该实体所存储的东西是已经**预编译**好的单条SQL语句,这个实体对象可以被后续的sqlite3语句执行。`sqlite3_stmt`对象类似于C/C++语言中**预编译**的结果(在C++编程中,预编译的结果存储在二进制文件中,我们一般看不到)。
3.`sqlite3_open(const char *filename,sqlite3 **ppDb)`接口。该函数用于打开或创建(若`filename`不存在)一个数据库对象,创建的结果保存在`ppDd`所指定的`sqlite3`对象中.
4.`sqlite3_prepare_v2(sqlite3 *db,const char *zSql,int nByte,sqlite3_stmt **ppStmt,const char **pzTail);`接口,该函数主要是对SQL语句进行预编译,并将结果存在参数4指定的对象中。其中参数2`zSal`是代指一条SQL语句的字符串,参数3代指该`zSql`字符串的最大占用字节长度,参数4 `双重指针参数ppStmt`即存储经过sqlite3编译后的SQL语句的结果,最后一个参数指代参数`zSql`字符串的没有使用的地址偏移量,一般置`nullptr`即可。该函数执行成功,返回`int`类型的`SQLITE_OK`,失败返回错误代码。
5.`sqlite3_step(sqlite3_stmt *);`接口,该函数主要是用于执行`sqlite3_prepare()`函数所编译好的`sqlite3_stmt`对象。对与DDL和DML而言,该函数执行成功返回`SQLITE_DONE`,对于DQL而言,若执行成功,则返回`SQLITE_ROW`。
6.`sqlite3_finalize(sqlite3_stmt *pStmt);`接口,主要用于释放`sqlite3_stmt`对象所占用的资源,执行成功过返回`SQLITE_OK;`。
7.`sqlite3_close(sqlite3 *);`接口,该函数主要用于关闭`sqlite3`数据库对象,成功返回`SQLITE_OK`。</code></pre><p>以上两个对象(1 和 2)以及5个接口函数(3-7)应该算是SQLite c/c++编程中最重要也是最基础的,几乎每个SQLite C编程都离不开。当然还有一些其他重要的函数,比如<code>sqlite3_bind()</code>族类,<code>sqlite3_column()</code>族类,以及<code>sqlite3_exec()</code>等,这些函数的存在极大方便了<code>sqlite3</code>编程。</p><h2>三.sqlite3编程示例</h2><p>在这里创建了一个sqlite操作的类<code>Data</code>,所有对sqlite数据库的操作都作为该类的方法,该程序文件如下:</p><pre><code>#include <sqlite3.h>
#include <string.h>
#include <iostream>
#include <string>
using namespace std;
//创建操作数据库的类
class Data
{
public:
Data(){
cout<<"执行构造函数"<<endl;
};
~Data(){
cout<<"执行析构函数"<<endl;
}
//1.创建数据库和数据表
void createData() ;
//2.插入数据
void insertData();
//3.更新数据(更新第id条记录的第n列字段的值)
void updateData(const int id,const int n );
//4.显示开启事物,批量插入数据
void insertBatch() ;
//5.选择数据(选择特定行数的数据并打印出来)
void selectData(const int row,const int offset) ;
//6.关闭数据库
void close(){
sqlite3_close(conn);
return ;
}
//7.清空数据库表,以便于下次操作测试
void drop();
private:
sqlite3 *conn = nullptr;
int rc;
int count = 10;
};
void Data::updateData(const int id,const int n){
const char *field = nullptr;
const char *update ;
char str[64] = {0};
if(1 == n){
cout<<"约束主键不能修改"<<endl;
// field = "ID";
// update = "update test set ID = %d where ID = %d";
// sprintf(str,update,n,id);
}else if(2 == n){
field = "name";
update = "update test set name = '%s' where ID = %d";
sprintf(str,update,"yxg",id);
}else{
field = "width";
update = "update test set width = %f where ID = %d";
sprintf(str,update,99.8,id);
};
sqlite3_stmt *stmt = nullptr;
if(sqlite3_prepare_v2(conn,str,strlen(str),&stmt,nullptr) != SQLITE_OK){
sqlite3_finalize(stmt);
close();
return ;
}
if(sqlite3_step(stmt)!=SQLITE_DONE){
sqlite3_finalize(stmt);
close();
return;
}
sqlite3_finalize(stmt);
}
void Data::selectData(const int row,const int offset) {
int r = row,o = offset;
const char * select1 = "select * from test limit %d,%d ";
char select[64] = {0};
sprintf(select,select1,row,offset);
sqlite3_stmt *stmt = nullptr;
// const char * select = "select * from test limit 1,1";
if(sqlite3_prepare_v2(conn,select,strlen(select),&stmt,nullptr) != SQLITE_OK){
cout<<"编译select语句失败"<<endl;
close();
sqlite3_finalize(stmt);
return ;
}
if(sqlite3_step(stmt) == SQLITE_ROW){
// cout<<"查询好了:"<<endl;
int fieldcount = sqlite3_column_count(stmt);
cout<<"该表所含字段数量是:"<<fieldcount<<endl;
for(int i = 0;i < fieldcount; ++i){
int type = sqlite3_column_type(stmt,i);
if(type == SQLITE_INTEGER){
int v = sqlite3_column_int(stmt,i);
cout<<"ID is: "<<v<<endl;
}else if(type == SQLITE_TEXT){
const char *v=(const char *)sqlite3_column_text(stmt,i);
string s = v;
cout<<"Name is: "<<s<<endl;
}else if(type == SQLITE_FLOAT){
int v = sqlite3_column_int(stmt,i);
cout<<"Age is: "<<v<<endl;
}else{
cout<<"The result is nullptr!!"<<endl;
}
}
}
sqlite3_finalize(stmt);
}
void Data::drop(){
const char *drop = "drop table test";
sqlite3_stmt *stmt = nullptr;
if(sqlite3_prepare_v2(conn,drop,strlen(drop),&stmt,nullptr ) != SQLITE_OK){
close();
sqlite3_finalize(stmt);
return ;
}
if(sqlite3_step(stmt) == SQLITE_DONE){
cout<<"成功销毁数据表"<<endl;
}
sqlite3_finalize(stmt);
close();
}
void Data::insertBatch(){
//开启一个事物
const char *begin = "begin transaction";
sqlite3_stmt *stmt = nullptr;
if(sqlite3_prepare_v2(conn,begin,strlen(begin),&stmt,nullptr)!=SQLITE_OK){
close();
cout<<"预编译事物失败!!"<<endl;
return ;
}
if(sqlite3_step(stmt) != SQLITE_DONE){
close();
cout<<"执行事物失败"<<endl;
return;
}
sqlite3_finalize(stmt);
//基于绑定变量插入数据
const char *insert = "insert into test values(?,?,?)";
sqlite3_stmt *stmt2 = nullptr;
if(sqlite3_prepare_v2(conn,insert,strlen(insert),&stmt2,nullptr) != SQLITE_OK){
close();
return ;
}
// char *name = "this is name";
for(int i = 0; i < count; ++i){
//数据表最左边的索引为1
sqlite3_bind_int(stmt2,1,i);
string name = "this is name ";
name += to_string(i);
sqlite3_bind_text(stmt2,2,name.c_str(),sizeof(name),SQLITE_TRANSIENT);
sqlite3_bind_double(stmt2,3,19.1*i);
if(sqlite3_step(stmt2)!= SQLITE_DONE){
close();
sqlite3_finalize(stmt2);
return ;
}
sqlite3_reset(stmt2);
cout<<"Insert succeed!"<<endl;
}
sqlite3_finalize(stmt2);
//提交事务
const char * commit= "commit";
sqlite3_stmt *stmt3 = nullptr;
if(sqlite3_prepare_v2(conn,commit,strlen(commit),&stmt3,nullptr)!= SQLITE_OK){
close();
sqlite3_finalize(stmt3);
return;
}
if(sqlite3_step(stmt3)!=SQLITE_DONE){
close();
sqlite3_finalize(stmt3);
return;
}
sqlite3_finalize(stmt3);
}
void Data::createData() {
//在当前目录下打开(或创建)test.db数据库。
rc = sqlite3_open("test.db",&conn);
if(rc != SQLITE_OK){
close();
cout<<"创建数据库失败!!"<<endl;
return ;
}
//SQL语句
/*
const char *createTable = "create table test(" \
"ID INT PRIMARY KEY NOT NULL,"\
"name TEXT NOT NULL," \
"age INT NOT NULL," \
" );";*/
const char * createTable = "create table test(ID INT PRIMARY KEY NOT NULL,name TEXT,width REAL)";
sqlite3_stmt *stmt = nullptr;
//预编译SQL语句
if(sqlite3_prepare_v2(conn,createTable,strlen(createTable),&stmt,nullptr) != SQLITE_OK){
cout<<"预编译失败"<<endl;
sqlite3_finalize(stmt);
close();
return;
}
//执行SQL语句
if(sqlite3_step(stmt) != SQLITE_DONE){
sqlite3_finalize(stmt);
cout<<"执行失败"<<endl;
close();
return ;
}
sqlite3_finalize(stmt);
cout<<"创建数据库和数据表成功!!"<<endl;
}
int main(int argc, char **argv){
Data *base = new Data;
base->createData();
base->insertBatch();
//打印从第一条记录开始的一条记录(记录索引从0开始)
base->selectData(1,1);
base->updateData(1,2);
base->drop();
delete base;
base = nullptr;
return 0;
}</code></pre><p>从以上简单示例可以看出,<code>sqlite3</code>c++编程基本都要经过三个步骤(在不使用<code>sqlite3_exec()</code>):</p><pre><code>1.声明`SQL`语句和`sqlite3_stmt` 对象;
2.调用`sqlite3_prepare()`函数 “预编译”;
3.调用`sqlite3_step()`函数执行`SQL`操作.</code></pre><p>在上面几个类方法中,唯一有点特别的是<code>void insertBatch()</code>函数,与其他功能函数的主要区别就是:在该函数中,显示的开启了一个<strong>事务</strong>,所有的插入操作均在该<strong>事务</strong>内完成,直到该<strong>事务被显示的提交</strong>。<br>所谓事务,其实就是一块执行单元,一块代码段,在该执行单元内的sqlite操作,都不会自动的提交到数据库中,直到显示的执行<code> SQL commit</code>。在sqlite中,所有的操作本质都是“事务性”的,只不过被sqlite隐式的创建并提交,默认情况下,每一个<code>sqlite3</code>函数的调用都伴随着事务的发生。<br>一般情况下,数据插入类似下面的代码:</p><pre><code>void insert(){
const char *insertSQL= " ..... ";
sqlite3_stmt 对象;
for(int i=0;i < count;++i){
执行预编译 sqlite3_prepare();
执行插入 sqlite3_step();
....
....
}
}</code></pre><p>在方法<code>void insertBatch()</code>中,如要插入大批量数据,只需要执行一次 “预编译” 即可,然后基于<strong>数据绑定</strong>的方式在事务内部执行SQL插入语句,极大节省了<code>sqlite3_prepare_v2()</code>函数的调用次数(通常 函数<code>sqlite3_prepare_v2()</code>比函数<code>sqlite3_step()</code>执行时占用时间更多)。</p><p><strong>可以看出,在执行大批量操作时,显示的创建并提交事务,能够优化编码,并提高代码执行效率,并且各个事务之间是<code>隔离</code>状态--不同事务之间相互独立和透明的。</strong></p><p>注意:在结束数据库操作的时候,一定记得执行<code>sqlite3_close()</code>函数关闭该库,在执行关闭操作之前,也要确保每一个<code>sqlite3_stmt</code>对象的资源被释放(调用<code>sqlite3_finalize()</code>),否则容易造成内存泄露。</p>
Linux 网络编程 一
https://segmentfault.com/a/1190000019956792
2019-08-02T17:17:46+08:00
2019-08-02T17:17:46+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<h3>一、网络编程基础</h3>
<pre><code>网络编程本身是一门很大的学问,涉及到的东西也很多,尤其是各种协议。先看图:</code></pre>
<p><img src="/img/bVbvSnH?w=611&h=588" alt="网络编程" title="网络编程"></p>
<p>正如上图所示,网络编程中包含五大层面(也有区分六个层面),从应用层到物理层可以明显看出 越往下越接近计算机硬件。自己并不是专业网络编程的工程师,所以仅对这五大层面有一点点粗浅的了解,这篇文章<a href="https://link.segmentfault.com/?enc=fAbJcLh8araQ%2FHGZmQzQMA%3D%3D.87zTjrWXYSg5mTuxKSOZeXZCCq3o%2FipfWgFF3KpRI52YMorF6ASglFb0%2BBGP75Su" rel="nofollow">网络编程技巧</a>博主写的比较详细. 平时大多数所谓网络编程,其实是在<code>传输层、网络层</code>方面.</p>
<h2>二、socket编程</h2>
<p>首先,socket(套接字)编程应该属于<code>传输层</code>,主要实现的是端到端的通信,非常类似于很久很久以前的固话通信,应用程序可以通过它发送或者接受数据,可以对它进行像文件似得读写、关闭等操作。套接字允许应用程序将I/O插入网络中,并与网络中的其他应用程序进行通信。</p>
<p>其次,网络中两台或多台主机之间进行通信,必须知道对应主机的地址,也就是其IP地址,但是只知道IP地址是远远不够的,试想如你在本机A发送了一个消息,另一个台主机B也接收到到了该消息,但是到底是B主机的哪一个进程接受并处理该消息?就像你用QQ给B发送消息,但是B不可能通过陌陌收到该消息。 因此,相互通信的主机之间还必须确定一一对应的消息处理接口--端口。端口的存在,主要是为了确认消息一一对应性。另外,端口号其实就是一个从0开始的到65535之间的一个整型数字,0~1023端口,也就是常说的静态端口,已被操作系统另做它用(http,https,ftp等各种协议占用),我们自己所能使用的端口范围只能从1024开始,即动态端口取值[1024,65535].</p>
<p>可是看出,若要进行网络间通信,socket至少要包含IP+port两个方面,其实事实也是如此.还是以有线电话做为类比,socket其实就是自己家中的一部电话,其中IP就是家庭地址,port就是自己家的电话号码,当要给别人打电话时,别人家当然也必须有自己的座机和专属于该座机的号码.<br>或许我们也能猜出,socket编程是网络编程里边必不可少且及其重要的一个环节.</p>
<h2>三、Linux+socket实践</h2>
<h3>1、目的</h3>
<p>熟悉Linux(这里用Ubuntu16.04版本,其他版本类似)下socket编程基本流程,掌握socket编程基本原理,搞懂Linux下socket编程所必须的函数及其用法.</p>
<p>实验:在本地模拟两台机器,服务器和客户端,服务器监听客户端信息并能发送广播,客户端可以主动给服务器发送消息,其中消息的输入是从标准输入设备输入,并输出到标准输出--Linux 终端.</p>
<p>开始之前必须了解一点 什么是<strong>文件描述符</strong>,在Unix Linux系统中,文件描述符是一个非负整数,其存在作用更像一个索引,系统内核通过该"索引"找到对应的文件、设备、外设、安装的软件等等, 并通过<strong>描述符</strong>对它们进行操作。总而言之,文件描述符对应了系统上的所有文件,这里的<strong>文件</strong>并非"传统意义上的普通文件",而是指Linux系统内核所能管理1的一切,包含文档、文件、硬件设备、系统软件等等。这也体现了Linux系统的设计思想----把一切视作文件.</p>
<h3>2、必要接口</h3>
<h5>1)、socket函数</h5>
<p>既然<code>socket</code>这么重要,来看它到底是个什么东西.在Linux终端执行:<code>man socket</code>,出现:<br><img src="/img/bVbvSSN?w=721&h=240" alt="图片描述" title="图片描述"><br>通过Linux手册查询可以知道该函数所必须的头文件,函数声明和函数描述等信息.从[DESCRIPTION]字段可知,函数创建了一个用于通信的端点并返回该端点的<strong>描述符</strong>,若创建成功,返回创建套接字的文件描述符,否则返回一负数. <br>函数声明 <code>int socket(int domain,int type,int protocol);</code><br><img src="/img/bVbvST8?w=670&h=307" alt="图片描述" title="图片描述"><br>参数 <code>domain</code>:表示创建该socket所使用的通讯协议家族--地址族,现在一般用IPv4协议,所以通常会选择<code>AF_INET</code>;<br>参数<code>type</code>:指定所需的通信类型。包括数据流(SOCK_STREAM)<-->TCP协议、数据报(SOCK-DGRAM)<-->UDP协议和原始类型(S0CK_RAW)<-->新网格协议的开发测试.<br>参数<code>protocol</code>:说明该套接字使用的协议族中的特定协议。如果不希望特别指定使用的协议,则置为0,使用默认的连接模式.<br>若要进行 基于TCP IP的网络开发测试,则函数创建方式一般为:</p>
<pre><code>int listenfd = socket(AF_INET,SOCK_STREAM,0);
</code></pre>
<h5>2)、bind函数</h5>
<p>既然有了一部“电话”,那么就需要为该电话绑定唯一的“所属地址”,同样Linux命令行执行:<code>man bind</code>,同样函数声明为:</p>
<pre><code>int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);</code></pre>
<p>从手册的描述中可以看出,当成功创建<code>socket</code>套接字后,调用该函数可以将所创建的套接字(sockfd)和指定的地址(addr)绑定.<br>地址是由这样一个结构体指定:</p>
<pre><code>struct sockaddr {
sa_family_t sa_family; //地址族
char sa_data[14]; //14字节的协议地址
}</code></pre>
<p>上面<code>struct sockaddr</code>是通用地址,在网络编程中 internet sockaddr使用下面地址,两种地址可以互换:</p>
<pre><code>struct sockaddr_in {
short int sin_family; /* 地址族,AF_xxx 在socket编程中只能是AF_INET */
unsigned short int sin_port; /* 端口号 (使用网络字节顺序) */
struct in_addr sin_addr; /* 存储IP地址 4字节 */
unsigned char sin_zero[8]; /* 总共8个字节,实际上没有什么用,只是为了和struct sockaddr保持一样的长度 */
};
</code></pre>
<p>bind()函数的第三个参数表示地址所占字节长度,<code>socklen_t</code>本质上是一个 <code>unsigned int</code>宏定义.<br>可以通过这样方式指定地址:</p>
<pre><code> struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
//serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");</code></pre>
<p>首先声明网络接口地址结构,在给该地址赋值前必须将其清空.依次设置该地址的地址族、IP和端口(这里随便设置了一个),上边出现另一个新函数<code>htons</code>,同样终端下<code>man htons</code>,可知该函数的主要作用是将<strong>主机字节序</strong>转化为<strong>网络字节序</strong>,关于这两个字节序后续再深入研究.这里可以理解为:htons()的主要作用就是将十进制的ip地址和端口号转化为网络可以识别的"东东".</p>
<p>至此,基本可以完成座机的安装入户和号码绑定:</p>
<pre><code>bing(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));</code></pre>
<h5>3)、listen监听函数</h5>
<p>对于我们的服务器而言,它需要监听来自客户端发来的消息,Linix终端中 <code>man listen</code>可以看到详细信息. 函数声明为:</p>
<pre><code>int listen(int socdfd,int backlog);</code></pre>
<p>其中参数<code>sockfd</code>代指所要监听的套接字文件描述符,参数<code>backlog</code>表示在套接字挂起时,所能接受请求的最大队列长度.函数执行成功返回 0,否则返回 -1.</p>
<p>必须说明一点,当调用该函数后,参数<code>socdfd</code>所指定的套接字将变为<code>被动套接字</code>,所谓被动套接字,是指其只能用来接收来自其他用户的链接请求. 类似于改变了套接字的状态,使其只能用于接收.</p>
<h5>4)、accept 接收函数</h5>
<p>对于我们的服务器而言,由于其只具备接收功能,因此必须创建一个接受函数:</p>
<pre><code>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
</code></pre>
<p>函数参数不言自明,参数1sockfd表示服务器socket描述符,参数2是指客户端的协议地址,参数3为地址长度. 函数成功返回监听的等待队列中第一个套接字的描述符.</p>
<h3>3、服务器实现</h3>
<p>服务器的功能是监听客户端发来的消息,并将消息广播给客户端.因此需要一个循环实时监听客户端发来的消息,在本地构建一个简单的服务器如下:</p>
<pre><code>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(void){
int listenfd;
if(( listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0){
ERR_EXIT("socket");
}
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
//serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
ERR_EXIT("bind");
//一旦监听,则为被动套接字(只能接受连接,调用accep函数之前调用),这里随便给了一个最大队列长度
if(listen(listenfd,100)< 0)
ERR_EXIT("listen");
//声明一个地址,用于存储客户端链接时的协议地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn; //返回的一个主动套接字
if((conn= accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept");
char recvbuff[1024];
while(1){
memset(recvbuff,0,sizeof(recvbuff));
int ret = read(conn,recvbuff,sizeof(recvbuff));
fputs(recvbuff,stdout);
write(conn,recvbuff,ret);
}
close(listenfd);
return 0;
}</code></pre>
<p>其中用到了几个非socket API的函数:</p>
<pre><code>ssize_t read(int fd,void *buf,size_t count);
ssize_t write(int fd, const void *buf, size_t count);</code></pre>
<p><code>read()</code>函数:负责从fd所指定文件描述符读取字节大小为count的数据到buf中.若成功返回实际读取到的字节大小,否则返回负数,返回0表示读取到文件结束.<br><code>write()</code>:将buf中的count个字节内容写入文件描述符fd.成功时返回写的字节数.</p>
<h3>4、客户端实现</h3>
<p>客户端的实现和服务器的实现之间大同小异,同样都需要 ” 安装电话 “ ,但是客户端的功能仅在于向外”拨打电话“. 区别在于客户端是主动发起连接请求,所以它必须知道自己所要连接的目标,之后服务器才有响应.同样客户端并不需要监听,只需要接收到服务器的广播即可. 发起连接请求需要函数 <code>connect</code>:</p>
<pre><code>int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);</code></pre>
<p>在上述连接函数中,参数sockfd表示本机(客户端)的socket套接字描述符,参数addr表示服务器端的地址,参数3表示地址长度.<br>代码实现:</p>
<pre><code>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(void){
int sock;
if(( sock = socket(PF_INET,SOCK_STREAM,0)) < 0){
ERR_EXIT("socket");
}
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//发起连接
connect(sock,(struct sockadddr*)&serveraddr,sizeof(serveraddr));
char recvbuf[1024]={0};
char sendbuf[1024]={0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!= NULL){
write(sock,sendbuf,strlen(sendbuf));
read(sock,recvbuf,sizeof(recvbuf));
fputs(recvbuf,stdout);
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock);
return 0;
}</code></pre>
<p>上述函数功能就是从客户端主动向服务器发送连接请求,并在客户端机器的标准设备上如字符,服务器接受并返回. 实现两台机器通信的模拟.</p>
<h3>5、结果</h3>
<p>效果如下图:<br>用<code>gcc</code>编译上述两个文件,首先启动服务器,之后启动客户端.在客户端随便输入字符,服务器解收到并广播返回. 至此基本完成目的.<br><img src="/img/bVbvTHc?w=806&h=59" alt="图片描述" title="图片描述"></p>
<h2>三、总结</h2>
<p>目前来看,创建服务器的一般流程是:</p>
<pre><code>1.创建socket套接字(`socket`函数);
2.创建服务器地址,地址包含协议族、IP和端口号(`const struct sockaddr*`);
3.绑定套接字和服务器地址(bind函数);
4.系统监听服务器,一旦监听则该套接字变为被动套接字,只能用于接收数据(`listen`函数);
5.作为服务器,应该能接收客户端信息(`accept`函数),该函数返回一个主动套接字;</code></pre>
<p>基于以上步骤,基本能搭建一个简单的服务器.</p>
<p>客户端的搭建相比而言简单许多:</p>
<pre><code>1.创建用于连接的套接字;
2.将套接字和服务器地址连接;
3.发送消息
</code></pre>
<h6>网络编程毕竟浪大水深,毕竟初涉,慢慢填充.</h6>
osg+ActiveQT嵌入ie64位
https://segmentfault.com/a/1190000015237162
2018-06-09T19:19:18+08:00
2018-06-09T19:19:18+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>写在前面</h2>
<p>最近Boss要求将现在osgGIS效果嵌入到网页实现,折腾了一周多,终于搞定。<br>先说下自己的环境:系统为Window10,osg版本为3.4,qt版本为5.9,vs2017编译工程为64位。</p>
<p>之前看了网上好多好多博文,整体过程大同小异,但那些文章都有一个共同特点,年代较久远,大部分开发的还是32位的软件,利用vs+qt嵌入网页应该没有什么问题。但现在自己开发的全部是64位程序,相较前辈们的技术路线,也有很大不同。</p>
<p>当然,总体思路仍然是:<strong>OSG嵌入QT,QT嵌入网页,这样便实现OSG与网页(IE内核)的结合</strong>。</p>
<h2>ActiveQT server插件</h2>
<p>利用VS+QT构建Activex工程不必详述,先上一张贴图:<img src="/img/bVbb5Zv?w=955&h=582" alt="图片描述" title="图片描述"></p>
<p>一直选择默认,就能构建出一个整体的ActiveQT框架。<br>注意这里的:<img src="/img/bVbb5ZZ?w=498&h=189" alt="图片描述" title="图片描述"></p>
<p>的第一行ID号,就在 <em>html</em>文档中的<strong>[Objet]</strong>对象的值。</p>
<h2>osg嵌入QT</h2>
<p>osg与QT的结合可以查看OSG官方给出的例子 <strong>osgviewerQT</strong>。<br>(代码已经上传GitHub)</p>
<h2>难缠的IE(关键)</h2>
<p>做出来64位ActiveQT控件之后,直接用ie打开多半不会加载成功。打开ie浏览器默认的是直接打开32位的(可通过window的资源管理器,找到iexplore--打开文件位置确认),即使选择64位一般情况下也不会成功,这也是自己卡克的地方。</p>
<p>引自网上一段话 "IE 浏览器在同时打开多个选项卡后,Windows 会同时运行多个不同的 IEXPLORE.EXE 进程,这个现象并非是不正常的故障,而是 IE 浏览器在 IE 8 及后续的版本中引入的“松散耦合进程框架(Loosely Coupled IE)”进程管理技术。此技术允许 IE 浏览器将主窗口与选项卡用不同的、分离的 IEXPLORE.EXE 进程隔开。如果一个选项卡遇到了问题需要关闭,可以避免连带影响整个 IE 浏览器主窗口及其它选项卡。这样有助于提升 IE 浏览器的稳定性与安全性。"</p>
<p>也就是说,当做出<strong>html</strong>后,即使选择用64的ie打开,也会至少启动两个线程,查看方式:打开一个ie网页,选择 “任务管理器”选择 “ie转到详细信息”,如图:(一般情况下会看到两个进程,一个是打开的64位,另一个是默认打开的32的ie进程)<br><img src="/img/bVbb52p?w=500&h=237" alt="图片描述" title="图片描述"></p>
<p>现在,我们要做的就是设置IE单进程64位启动启动,方法有二:</p>
<h3>ie单进程启动方法一:</h3>
<p>运行gpedit.msc组策略管理器。在“计算机配置—》管理模板—》windows组件—》Internet Explore”中打开“选项卡进程增长”,设置为已启用,选项卡进程增长为0。应用之后IE就进入单进程模式。这时候使用C:Program FilesInternet Explore目录下的iexplore.exe(该目录下IE为64-bit),去打开调用64位控件的html文件,就能成功的调用64位控件了。</p>
<h3>ie单进程启动方法二:</h3>
<p>如果上述方法失败,则可(百度经验)<a href="https://link.segmentfault.com/?enc=uei6D8%2FPNBUhUxnKVciR7A%3D%3D.b2wqbizxZt2FVIqa5yw61dWow6VryKR3Oee4sPBc3sLRdKCzmrT4IcU6eCbyh06T4unAnmKA67xkVP8nJkilyQ%3D%3D" rel="nofollow">https://jingyan.baidu.com/art...</a></p>
<p>最后效果:<br><img src="/img/bVbb52L?w=589&h=543" alt="图片描述" title="图片描述"></p>
折腾OSGEARTH心路历程
https://segmentfault.com/a/1190000010746144
2017-08-19T16:12:08+08:00
2017-08-19T16:12:08+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
3
<h2>写在前面</h2>
<p>最近一周半的时间总结起来就几个字:折腾、折腾再折腾!!!<br>之前搞了一段时间的osg和osgearth,尤记得第一次配置osgEarth时的各种折腾,基于vs2010和cmake编译的32位版本的osg3.4和osgearth2.8(目前都是最新版本),前前后后弄了四天才搞完,当时就想,编译这一次以后再也不编译了!<br>可最近由于项目需要,boss说都用最新的吧,一路折腾过来,用了vs2010、vs2013、vs2015、vs2017几个平台几乎都编译了一遍,当然也包括osgEarhth的各种第三方库。每一个平台都能编译成功,但是运行之前自己的程序总是出现各种问题。</p>
<p><strong>注:编译完成之后,用网上好多教程的测试方法测试成功并不能算是真正的编译成功,而应当基于自己项目需要,能够运行完美运行自己项目文件才算成功。</strong><br>当然,一般针对osg的测试使用<code>osgviewer osgversion osglogo</code>(属于osgExample)+一个简单小程序测试,对osgEarth的测试一般是<code>osgearth_viewer gdal_tiff.earth和osgviewer gdal_tiff.earth</code>进行测试,这些能够测试成功的话,基本表示osg及osgEarth的编译没有太大问题,这时候就要拿自己的项目文件源码来测试,如果和之前或者看到过的显示结果有出入,那么恭喜,并没有完全安装成功。</p>
<h2>折腾之路</h2>
<p>之所以折腾,主要还在于64位osgEarth的编译过程。当然应该会有很多很多大牛应用vs2015完美编译64位的osg和osgearth,对此深表佩服,佩服的原因后续再说。</p>
<p>好吧,记录自己折腾过程中的几个关键点,以避免之后少走弯路。</p>
<h3>折腾vs2013</h3>
<p>vs2010现在可能用的比较少了吧,32位的osg和osgEarth(简称OE),能运行,64位的也能编译成功,但是并不完美。完不完美,主要看自己的需求了,编译过程不再详述。<br>用vs2013编译OE也能编译成功,但是编译osg中,freetype插件编译不成功,如果你对字体显示要求不高,当然可以忽略该插件。我们都知道,OSGEARTH编译需要基于osg,用vs2013编译完成OSGEARTH后,OSGEARTH的插件是基于osg的插件库安装在bin目录下,如下:<img src="/img/bVTfwf?w=683&h=167" alt="图片描述" title="图片描述"><br>但是用vs2013编译出的tiff连接库在OSGEARTH中并不能起作用,出现<a href="https://link.segmentfault.com/?enc=d6cf181ZcbRDz30SE2k3mg%3D%3D.YltLIcaSDX1Kg1o7%2Fo%2B1VOMAUT4af3IIqapXuPammW3tRzHej2y76HsTSxYUF%2Fjr3G1cvoNCOvRFLKzOYKeYkgBednvHo46IXoke%2BBgz9yJYW3o5oGBuf0EbKWPZfzCtLA%2BMDrv0B%2BAlJdJrYviFEO%2Ff46L7mTa2cagiTgst0fXuup5%2FxhdQ8W18qeo47bm8" rel="nofollow">http://bbs.osgchina.org/forum...</a>,错误原因很可能是自己的tiff库出了问题(32位没问题),其他测试到时没什么问题,一旦出现此问题,两种解决方法:</p>
<ol>
<li>重新生成tiff连接库,并将生成的dll覆盖掉原来的tiff.dll;</li>
<li>从cmake开始,重新编译OE。</li>
</ol>
<p>如果方案一还是不能解决问题,只好采用方案2了。</p>
<h3>折腾vs2015</h3>
<p>vs2013有问题,好吧,老师说用用vs2015试试。<br>编译过程依然不在详述,只想说:非常不建议用vs2015编译OE。<br>最主要原因:osg官网并没有提供打包好的针对vs2015的第三方插件包。也就说说,要想编译osg,前提需要编译好自己所需要的或者将会需要的第三方插件,而且在编译过程中,cmake非常容易找不到freetype和freetype2(前提自己编译成功freetype)头文件和库文件。</p>
<h3>折腾vs2017</h3>
<p>用vs2017编译osg3.4,按照网上其他教程一般都能够编译成功,需要注意几点:</p>
<ol>
<li>cmake版本不要用最新版,但也不能用较低版本。低版本无法生成针对vs2017的解决方案,最新的cmake问题多多。我使用的cmake3.8.0.</li>
<li>在将cmakelists.txt拖入到cmake时,要特别注意配置的各项内容,仔细检查自己所需要的库是否都添加到路径中,一次次config,直到不出现错误为止。</li>
</ol>
<h4>编译gdal时候(必须):</h4>
<p>找到gdal的源码文件下nmake.opt文件<img src="/img/bVTfE3?w=650&h=46" alt="图片描述" title="图片描述"><br>修改nmake.opt文件内容(可用Notepad++打开),将其中的<img src="/img/bVTfFc?w=143&h=29" alt="图片描述" title="图片描述"> 前面的 “#” 去掉。<br>用管理员身份打开vs2017命令行工具:<img src="/img/bVTfFm?w=313&h=160" alt="图片描述" title="图片描述"><br>分别执行</p>
<pre><code>nmake -f makefile.vc machine=x64
nmake -f makefile.vc install machine=x64
nmake -f makefile.vc devinstall
</code></pre>
<p>编译成功之后会在C盘根目录产生:<br><img src="/img/bVTfFx?w=698&h=188" alt="图片描述" title="图片描述"><br> 然后将该目录下的bin、lib、include、data目录复制到你的安装路径下。</p>
<h4>编译curl(必须)</h4>
<p>过程类似于编译gadl:64-debug:</p>
<pre><code>nmake /f Makefile.vc mode=static vc=15 debug=yes rtlibcfg=static machine=AMD64
nmake /f Makefile.vc mode=static vc=15 debug=no rtlibcfg=static machine=AMD64</code></pre>
<p>编译成功后,会自动在curl的根目录下生成builds文件夹,其内容如图:<br><img src="/img/bVTfF8?w=814&h=241" alt="图片描述" title="图片描述"><br>同样复制该目录下的头文件和库问价到你需要的安装路径之下。</p>
<h4>编译expat</h4>
<p>expat的作用不做过多解释,也并非OSGEARTH的必须依赖包。<br>expat的官方网站提提供了<code>expat-win32bin-2.2.3.exe</code>,然后双击安装,安装完成之后进入sources目录,双击打开<code>expat.sln</code>,如图:<br><img src="/img/bVTfHs?w=657&h=242" alt="图片描述" title="图片描述"><br>打开之后将其编译平台改为x64,<img src="/img/bVTfH0?w=697&h=494" alt="图片描述" title="图片描述"><br>然后生成expat库文件<img src="/img/bVTfIc?w=732&h=83" alt="图片描述" title="图片描述"></p>
<p>经过上述步骤之后生成的库文件在 <code>sources/win32下,如图</code><img src="/img/bVTfIo?w=676&h=141" alt="图片描述" title="图片描述"></p>
<p>最后贴一下我所用到的OSGEARTH的第三方库:<br><img src="/img/bVTfIT?w=498&h=207" alt="图片描述" title="图片描述"></p>
<p>另:自己详细书写了一份OSG和OSGEARTH的源码编译教程,相比于该手册(2011年编写)<img src="/img/bVTl7g?w=692&h=139" alt="图片描述" title="图片描述">有比较大的改进,相应的第三方依赖库也是应用了较新的版本(截止2017-8-8日)和新的编译方法。网上大多数的编译过程还是针对32位OSGEARTH的,现在计算机硬件配置水平都很高,如果再继续使用32位版本开发项目未免有些浪费。<br>继:非常感谢上述文档(网上可以下载到)提供的帮助。</p>
OSG入坑之路
https://segmentfault.com/a/1190000010506374
2017-08-04T16:13:20+08:00
2017-08-04T16:13:20+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
3
<p>所谓“入坑”,只不过为自己的不熟练找借口而已。<br>现在学习OSG已经三周,跟着书、视频做些简简单单的小demo而已,有时候真是感觉自己无从下手。<br>不晓得的自己算不算是转行,之前研究<code>点云数据处理</code>,有一点点opengl的基础,虽说工作进入某高校,项目是某沉浸式VR系统开发,目前为止也止有我一个人在搞!<br>。。。。<br>有些扯远了,上述纯属瞎扯淡,毫无逻辑。。。。。。</p>
<p>断断续续的用了大半个月的osg,给我的感觉就是:出了问题不知如何下手处理!比如对某图形节点设置状态,但是可视化出来却没有自己想要的效果,程序也不报错。<br>之所以会有上述烦恼,可能原因在于自己对opengl和计算机图形学并不深入了解,理论上的匮乏造成了实际问题无法有效解决,这是其一;其二,或许是个人经验不够,思考不多,在学校习惯了被老师同学指点,自己缺不去深入考虑,经验需要积累,在以后工作中要<code>多思考,多感悟,多总结,不怕错</code><br>坏了,又有些扯远了,还是回到osg吧!记性不好,就多记录一下自己的小收获吧!</p>
<h2>一、 渲染状态(render state)</h2>
<p><code>osg中,当设置某节点的渲染状态时,该状态会赋予当前节点及其子节点</code>,因此,若要实现多节点多状态渲染时,一定注意节点之间的父子关系,最好一个节点设置一个自己想要的状态,除非父节点及其子节点的渲染状态一样。<br>渲染状态的管理通过<code>osg::StateSet</code>管理,可以将其添加到任意的节点(node、group、geode等)和DrawAble类。如要设置渲染状态的值,需要:</p>
<ol>
<li>得到某节点的stateset实例;</li>
<li>设置该实例的渲染属性(attribute)和模式(mode)<br>例:<code>osg::StateSet *state = obj->getOrCreatStateSet() ;</code>其中,<code>obj</code>可以是节点或Drawable实例,且:<code>getOrCreatStateSet()</code>方法是在osg::Node中声明的,这就意味着geode或group都可以使用。</li>
</ol>
<pre><code>state>setMode(GL_LIGHTING,osg::StateAttribute::OFF);//关闭灯光状态
state->setTextureAttributeAndModes(0,texture,osg::StateAttribute::ON);//设置纹理
//开启shader
osg::ref_ptr<osg::Program> shaderProg = new osg::Program;
shaderProg->addShader(new osg::Shader(osg::Shader::VERTEX,vertexShader));
shaderProg->addShader(new osg::Shader(osg::Shader::FRAGMENT,fragShader));
state->setAttributeAndModes(shaderProg,osg::StateAttribute::ON); //设置渲染属性和模式
->setMode(GL_DEPTH_TEST,osg::StateAttribute::OFF);//管理深度测试
//设置渲染顺序,第一个参数越小,渲染越靠前,默认第一个参数为 -1
state->setRenderBinDetails(10, "RenderBin"); //默认渲染排序
state->setRenderBinDetails(100,"DepthSortedBin"); //由远到近
state->setRenderBinDetails(1000,"TraversalOrderBin"); //按遍历顺序
//开启混合透明度
state->setMode(GL_BLEND,osg::StateAttribute::ON); //设置渲染模式</code></pre>
<p>等等等等</p>
<h2>二、 geometry和geode</h2>
<p>显然,geode是几何节点,且是叶节点,geometry类管理osg中各种各样的几何体。<br>个人总结:在使用geode画osg<strong>自带</strong>的几何图形时,总是:</p>
<ol>
<li>声明geode节点</li>
<li>创建几何对象</li>
<li>设置几何对象的参数</li>
<li>申请一个osg::ShapeDrawable</li>
<li>geode->addDrawable<br>列子:</li>
</ol>
<pre><code>//画个圆柱
osg::TessellationHints *hins = new osg::TessellationHints;
hins->setDetailRatio(1.0f);//设置圆柱的精度为0.1,值越小,精度越小
osg::ref_ptr<osg::Cylinder> cy = new osg::Cylinder; //圆柱
osg::ref_ptr<osg::ShapeDrawable> sd = new osg::ShapeDrawable(cy); //直接用几何对象初始化shapedrawable实例
cy->setCenter(osg::Vec3(400.0,300,0));
cy->setHeight(0.1);
cy->setRadius(150);
sd->setTessellationHints(hins);
geode->addDrawable(sd); //必不可少(到这里才真正绘制)
/*以上啰嗦代码当然可以这样写:*/
geode->addDrawable(new osg::ShapeDrawable(osg::Cylinder(center),Radius,Height),teseel);
//画个盒子
osg::ref_ptr<osg::TessellationHints> hints = new osg::TessellationHints;
//设置精度
hints->setDetailRatio(0.1);
osg::ref_ptr<osg::ShapeDrawable> shape = new osg::ShapeDrawable(new osg::Box(osg::Vec3(x,y,z),长,宽,高),hints.get());
geode->addDrawable(shape);</code></pre>
<p>TessellationHints(精度)参数对圆柱几何的影响以及shapedrawable继承关系:<br><img src="/img/bVSflc?w=311&h=348" alt="参数为0.1" title="参数为0.1"><img src="/img/bVSfln?w=327&h=367" alt="参数为1" title="参数为1">!</p>
<p>自定义几何体时:</p>
<ol>
<li>申请geometry对象,自定义绘制的顶点、法向量、颜色等(如果需要的话)数组</li>
<li>将自定的各种数组传递给geode,并设置绑定方式。</li>
<li>设置各个顶点之间的关联方式(也就是绘制什么图形)</li>
<li>将geometry对象添加到geode->addDrawable(geometry);<br>在第三步骤中,geom->addPrimitiveSet();函数需要一个Drawarrays指针和原始点的连接方式</li>
</ol>
<pre><code> osg::ref_ptr<osg::Geode> geo = new osg::Geode;
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry();
osg::ref_ptr<osg::Vec3Array> vex = new osg::Vec3Array;
osg::ref_ptr<osg::Vec4Array> color = new osg::Vec4Array;
osg::ref_ptr<osg::Vec3Array> normal = new osg::Vec3Array;
osg::ref_ptr<osg::LineWidth> width = new osg::LineWidth; //线宽
//设置法向量及其绑定方式
geom->setNormalArray(normal,osg::Array::Binding::BIND_OVERALL);
normal->push_back(osg::Vec3(0.0,-1.0,0.0));
//设置顶点
vex->push_back(osg::Vec3(-10.5,5,-10.0));
vex->push_back(osg::Vec3(10.5,5,-10.0));
vex->push_back(osg::Vec3(10.0,5,10.0));
vex->push_back(osg::Vec3(-10.0,5,10.0));
geom->setVertexArray(vex.get());
//设置颜色
color->push_back(osg::Vec4(0.1,0.2,0.3,0.5));
color->push_back(osg::Vec4(1.1,0.9,0.3,0.50));
color->push_back(osg::Vec4(0.2,0.5,0.3,0.50));
color->push_back(osg::Vec4(0.4,0.2,0.7,0.50));
geom->setColorArray(color);
geom->setColorBinding(osg::Geometry::AttributeBinding::BIND_PER_VERTEX); //设置纹理绑定方式
osg::ref_ptr<osg::TessellationHints> hints = new osg::TessellationHints;
hints->setDetailRatio(0.1);
//设置透明度
geom->getOrCreateStateSet()->setMode(GL_BLEND,osg::StateAttribute::ON);
/*osg::DrawArrays相当于对opengl中glDrawarray的封装*/
geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::Mode::LINE_LOOP,0,4)); //设置顶点的关联方式(此处,以线段的方式连接)
//设置线宽
width->setWidth(20.0);
geom->getOrCreateStateSet()->setAttributeAndModes(width);
geo->addDrawable(geom.get());</code></pre>
<p>geom->addPrimitiveSet(new <code>osg::DrawArrays(osg::PrimitiveSet::Mode::LINE_LOOP,0,4));</code>的基本图元及drawable继承方式:<br><img src="/img/bVSfAF?w=322&h=522" alt="图片描述" title="图片描述"><img src="/img/bVSfl4?w=253&h=358" alt="图片描述" title="图片描述"></p>
<h2>三、Camera</h2>
<p>当然,上一篇入门文章<a href="https://segmentfault.com/a/1190000010303677">viewer::run()</a>多多少少也和camera有关。现在算是进一步吧。<br>先看看<code>camera的继承关系图</code><br><img src="/img/bVSfCX?w=412&h=476" alt="图片描述" title="图片描述"></p>
<p>很明显,camera继承自node、group、transform,也就是说他们具有的属性,camera也具有。</p>
<h3>1. 裁剪面</h3>
<p>就像<a href="https://segmentfault.com/a/1190000010303677">上篇文章</a>所述,osgviewer->run()会自动设置一个场景漫游器,该漫游器包含了透视投影矩阵、视口大小、屏幕宽高比以及远近裁剪面、摄像机位置等等参数,如果想要修改远近裁剪面应该首先关闭osg的自动判断远近裁剪面的函数:</p>
<pre><code>viewer->getCamera()->setComputeNearFarMode(osgUtil::CullVisitor::DO_NOT_COMPUTE_NEAR_FAR);
viewer->getCamera()->setProjectionMatrixAsPerspective(fovy,aspectRatio,zNear,zFarSurface);</code></pre>
<p><code>osgUtil::CullVisitor</code>是osg的场景拣选访问器。</p>
<h3>2. 获取自己设备的图形环境、HUD、RTT:</h3>
<pre><code>osg::GraphicsContext::WindowingSystemInterface *wsi = osg::GraphicsContext::getWindowingSystemInterface();
if (!wsi)
return ;
unsigned int height,width;
wsi->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0),width,height); //获取屏幕的长宽
//设置图形环境特性
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits();
traits->x = 0;
traits->y = 0;
traits->width = width;
traits->height = height;
traits->windowDecoration = false; //窗口修饰 关
traits->doubleBuffer = true; //是否支持双缓存
traits->sharedContext = false;
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits);
if (!gc.valid())
{
return ;
}
gc->setClearMask(GL_COLOR_BUFFER_BIT);
gc->setClearColor(osg::Vec4(0.5,0.5,0.5,1.0)); //设置全局上下文的背景色
osg::ref_ptr<osg::Camera> master = new osg::Camera;
master->setGraphicsContext(gc);
viewer->addSlave(master); //将相机添加到场景中。</code></pre>
<p>在HUD和RTT中,创建相机是非常重要的,HUD的相机要最后渲染,防止被覆盖<br>HUD相机:</p>
<pre><code>osg::Camera *CreatTextHUD()
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setViewMatrix(osg::Matrix::identity()); //设置视图矩阵为单位矩阵
camera->setAllowEventFocus(false); //不响应其他鼠标事件
camera->setRenderOrder(osg::Camera::POST_RENDER);//最后渲染
camera->setClearMask(GL_DEPTH_BUFFER_BIT);
camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);//设置参考帧 为绝对坐标系
camera->setProjectionMatrixAsOrtho2D(0,1024,0,768); //设置二维正交投影
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->getOrCreateStateSet()->setMode(GL_LIGHTING,osg::StateAttribute::OFF);//关闭灯光状态
osg::ref_ptr<osgText::Text> text = new osgText::Text;
geode->addDrawable(text);
text->setPosition(osg::Vec3(0,0,0));
text->setFont("simsun.ttc"); //设置为宋体
text->setText(L"宋体 测试"); //一定不要忘记加“L”字符
text->setCharacterSize(50);
camera->addChild(geode);
return camera.release();
}</code></pre>
<p>RTT相机</p>
<pre><code>void CreatRTT(osgViewer::Viewer *viewer)
{
osg::ref_ptr<osg::Group> group = new osg::Group;
osg::ref_ptr<osg::Node> node = osgDB::readNodeFile("nathan.osg");
group->addChild(node);
//设置图形上下文
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->x = 0;
traits->y = 0;
traits->width = 800;
traits->height = 600;
traits->sharedContext = false;
traits->doubleBuffer = true;
traits->windowDecoration = false;
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits);
//创建主相机
osg::ref_ptr<osg::Camera> master = new osg::Camera;
master->setGraphicsContext(gc);
master->setViewport(0,0,800,600);
viewer->addSlave(master);
//创建rtt相机
osg::ref_ptr<osg::Camera> rttCamera = new osg::Camera;
rttCamera->setRenderOrder(osg::Camera::PRE_RENDER);
rttCamera->setGraphicsContext(gc);
rttCamera->setViewport(0,0,800,600);
rttCamera->addChild(node);
//添加相机并设置瞄准镜的放大倍数为8倍,最后false表示:该添加入的相机不接受主窗口任何内容。
viewer->addSlave(rttCamera,osg::Matrix::scale(8,8,8),osg::Matrix::identity(),false);
osg::Texture2D *t2d = new osg::Texture2D;
t2d->setInternalFormat(GL_RGBA);
rttCamera->attach(osg::Camera::COLOR_BUFFER,t2d);
group->addChild(CreatHUD(t2d));
viewer->setSceneData(group);
return ;
}</code></pre>
<p>上边的函数其实是创建了一个瞄准镜的效果:<br><img src="/img/bVSfTn?w=335&h=411" alt="图片描述" title="图片描述"></p>
<p><code>自我感觉(或许并不对,仅限于目前认知水平)</code>:osg添加多相机主要功能是为了多窗口多视图显示。一个相机能够渲染多个视图,一个场景能够添加多个相机,多个相机能够实现从不同角度、方位、视角观察同时观察同一个模型。</p>
<h3>3. 两个小函数:</h3>
<p><code>viewer->setCameraManipulator(osgGA::CameraMaipulator *)</code>,和<code>viewer->addEventHandler(osgGA::EventHandler *)</code>,前者的函数参数是<code>osgGA::CameraMaipulator *</code>类型,后者参数为<code>osgGA::EventHandler *</code>类型,且<code>osgGA::CameraMaipulator *</code>是继承自<code>osgGA::EventHandler *</code>的。<br>对比:</p>
<ol>
<li>前者的主要作用是利用外部设备等影响场景中的主相机位置以及相应事件。事件相应的主要目的是为了修改相机参数矩阵,以实现漫游的目的。<code>设置漫游的本质就是修改主相机的各种参数矩阵,并将修改后的参数返回场景中的主相机</code>(影响的<strong>最顶层的相机节点</strong>).</li>
<li>后者顾名思义,就是为了相应各种键盘、鼠标等外部设备事件事件,当然可以影响主场景中的相机。<code>其实本质上)</code>:从类的继承关系来看,漫游器是从事件处理类中继承而来,如果事件处理函数个人写得足够好、足够丰富,应该能够替代漫游器的,这也是为什么两者在很多时候添加自定义类时程序并不报错,但就是达不到预想效果的原因。<br>进一步讲,漫游器是对事件处理的更进一步的丰富,其"丰富"的主要内容应该是<code>矩阵的自动返回操作</code>,从而控制相机,其矩阵自动返回的主要函数是:</li>
</ol>
<pre><code>virtual void setByMatrix(const osg::Matrixd& matrix) = 0; //设置相机的位置姿态矩阵
virtual void setByInverseMatrix(const osg::Matrixd& matrix) = 0; //设置相机的视图矩阵
virtual osg::Matrixd getMatrix() const = 0; //获取相机的姿态矩阵
virtual osg::Matrixd getInverseMatrix() const = 0; </code></pre>
<p>这也就是为什么自定义的漫游器必须重载这四个函数的原因所在。(事件处理不需要重载这几个函数)<br>关系:<img src="/img/bVSf5Y?w=941&h=396" alt="图片描述" title="图片描述"><br>如果总结为一句话,应该是:CameraManipulator是EventHanler的子类,EventHandler本身也可以实现漫游功能,只不过osg开发者为了更加丰富便捷的控制事件与漫游,从而单独设计了一个漫游操作器,以便使用者能够更加轻松方便的控制自己的场景。</p>
<p>清除camera中的深度缓存:<code>camera->setclearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH)</code>;</p>
<p><strong>注</strong>:以上内容只是自己在比较皮毛的层面的记录,并没有过多的深入源码解析,内容或许有误,恳请指正留言,不胜感激!<br><strong>另</strong>:有做点云处理研究的朋友也可多多交流。</p>
OSG:从源码看Viewer::run() 一
https://segmentfault.com/a/1190000010303677
2017-07-22T16:58:36+08:00
2017-07-22T16:58:36+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<p>刚刚入门<code>OSG</code>不久,一直弄不清楚它到底是如何显示一副图片的,简单从源码看下。<br><strong>注</strong>:所用OSG的版本为3.4 release版!</p>
<h2>1、前言</h2>
<p>在OSG程序中,简单展示一副图片用:</p>
<pre><code> osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;
osg::ref_ptr<osg::Node> node = osgDB::readNodeFile("glider.osg");
viewer->setSceneData(node);
return viewer->run();</code></pre>
<p>然后在我们的计算机显示器(屏幕中心)上就会展现滑翔机的图片,并且移动鼠标、滚轮等设备还能实现对该滑翔机的旋转、缩放、平移等一系列的操作。在OpenGL中我们知道,要实现对模型的旋转缩放等功能,需要设置相机的各种参数(相机位恣,旋转角度、平移步长、转动速率等等)。osg中简简单单的几行代码,就实现了如此之多的功能,不禁要问:<code>run()</code>函数里边到底发生了什么。</p>
<h2>2、第一步</h2>
<p>打开OSG的源码,在<code>Viewer.cpp</code>文件中找到<code>Viewer::run()</code>函数的定义:</p>
<pre><code>int Viewer::run()
{
if (!getCameraManipulator() && getCamera()->getAllowEventFocus())
{
setCameraManipulator(new osgGA::TrackballManipulator());
}
setReleaseContextAtEndOfFrameHint(false);
return ViewerBase::run();
}</code></pre>
<p>可见,将一副图片的节点读入osg的场景中后,在场景的<code>run</code>中首先会判断该场景中有没有<strong>漫游器</strong>(<code>getCameraManipulator</code>返回一个<code>osgGA::CameraManipulator</code>),如果该场景中不存在漫游器,则调用函数<code>setCameraManipulator</code>创建一个<code>跟踪球 TrackballManipulator</code>的场景漫游器。关键来看默认的漫游器<code>osgGA::TrackballManipulator()</code>做了什么!</p>
<h3>2.1 默认漫游器</h3>
<p>先看<code>osgGA::TrackballManipulator()</code>的默认构造函数:</p>
<pre><code>TrackballManipulator::TrackballManipulator( int flags )
: inherited( flags )
{
setVerticalAxisFixed( false );
}</code></pre>
<p>将该类当做参数传递给了<code>setCameraManipulator</code>,重点来看<code>setCameraManipulator</code>干了什么,源码:</p>
<pre><code>void View::setCameraManipulator(osgGA::CameraManipulator* manipulator, bool resetPosition)
{
_cameraManipulator = manipulator;
if (_cameraManipulator.valid())
{
_cameraManipulator->setCoordinateFrameCallback(new ViewerCoordinateFrameCallback(this));
if (getSceneData()) _cameraManipulator->setNode(getSceneData());
if (resetPosition)
{
osg::ref_ptr<osgGA::GUIEventAdapter> dummyEvent = _eventQueue->createEvent();
_cameraManipulator->home(*dummyEvent, *this);
}
}
}</code></pre>
<p>可以看出,该函数主要设置了当前场景的主相机的动作,设置了当前场景节点的坐标系以及场景中的数据,同时通过设置<code>home</code>--漫游器初始位置。<br>紧接着通过<code>setReleaseContextAtEndOfFrameHint()</code>设置了渲染场景的上下文关系。</p>
<h2>3、再看<code> ViewerBase::run()</code>
</h2>
<p>源码:</p>
<pre><code>int ViewerBase::run()
{
if (!isRealized())
{
realize();
}
const char* run_frame_count_str = getenv("OSG_RUN_FRAME_COUNT"); //getenv()从环境变量中去字符串
unsigned int runTillFrameNumber = run_frame_count_str==0 ? osg::UNINITIALIZED_FRAME_NUMBER : atoi(run_frame_count_str);
while(!done() && (run_frame_count_str==0 || getViewerFrameStamp()->getFrameNumber()<runTillFrameNumber))
{
double minFrameTime = _runMaxFrameRate>0.0 ? 1.0/_runMaxFrameRate : 0.0;
osg::Timer_t startFrameTick = osg::Timer::instance()->tick();
if (_runFrameScheme==ON_DEMAND)
{
if (checkNeedToDoFrame())
{
frame();
}
else
{
// we don't need to render a frame but we don't want to spin the run loop so make sure the minimum
// loop time is 1/100th of second, if not otherwise set, so enabling the frame microSleep below to
// avoid consume excessive CPU resources.
if (minFrameTime==0.0) minFrameTime=0.01;
}
}
else
{
frame();
}
// work out if we need to force a sleep to hold back the frame rate
osg::Timer_t endFrameTick = osg::Timer::instance()->tick();
double frameTime = osg::Timer::instance()->delta_s(startFrameTick, endFrameTick);
if (frameTime < minFrameTime) OpenThreads::Thread::microSleep(static_cast<unsigned int>(1000000.0*(minFrameTime-frameTime)));
}
return 0;
}</code></pre>
<p>在该函数中,实现了每一帧的渲染、计算帧率等。<br>在<code>frame()</code>中:</p>
<pre><code>void ViewerBase::frame(double simulationTime)
{
if (_done) return;
// OSG_NOTICE<<std::endl<<"CompositeViewer::frame()"<<std::endl<<std::endl;
if (_firstFrame)
{
viewerInit();
if (!isRealized())
{
realize();
}
_firstFrame = false;
}
advance(simulationTime);
eventTraversal();
updateTraversal();
renderingTraversals();
}</code></pre>
<p>可见,在osg的每一帧的绘制中,实现了对事件的遍历(<code>eventTraversal()</code>)、更新(<code> updateTraversal()</code>)和渲染(<code>renderingTraversals()</code>),同时这也是漫游器真正被添加进来起作用的地方。至于每一帧中到底如何处理事件的(<code>eventTraversal()</code>函数作用),可参看源码文件Viewer.cpp中<code>void Viewer::eventTraversal()</code>的具体定义。</p>
<p>主要参考文章:<a href="https://link.segmentfault.com/?enc=BSvYLEZ6vdkWOsh%2F1cNUIw%3D%3D.l4nl73%2FDlkSZBa0qp37aOILVtZgsF4rrq7yZKviNgEx0r97XRIxDeWkJTonO03zlXKpO5N0MpUf4Lq7uRxmmkA%3D%3D" rel="nofollow">http://blog.csdn.net/csxiaosh...</a><br><a href="https://link.segmentfault.com/?enc=QwoMhby0%2Bip0FCzbWWoeLg%3D%3D.VhFOw7uM8qP%2BWxRMzpKZwRXR1tyCxLKVaAz9eLn9Is3dnYcLCj5AQScYDBeqN9xs7nsvFW%2B2I4UHZdWmbFwINw%3D%3D" rel="nofollow">http://blog.csdn.net/popy007/...</a></p>
<p>未完 待续。。。</p>
Opengl基本知识点
https://segmentfault.com/a/1190000008520368
2017-02-28T22:25:07+08:00
2017-02-28T22:25:07+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<h2>1. GLSL语言,概念,工作原理,及如何传递数据的</h2>
<p>参考<a href="https://link.segmentfault.com/?enc=cREwr5olmNT3uk%2FPZ%2BUSkQ%3D%3D.LuJ9U5VKCavWqDwOk8JR37gXGUUQYB1i7hGKcYh3f3O0pFTA415bGhArMMOf7TXxjMOGPNyOfWeLaTgJuKkOw1Pg88ME27c79lqpudq4fIRZoJoPkxIm%2FmUusvIpgxkb" rel="nofollow">learnopengl着色器</a><br>着色器(shader)是运行在GPU上的小程序,类似于C语言,构造一个着色器在其开头必须声明版本。本质上来说,着色器是一个把输入转化为输出的程序。<br>着色器定义了<code>in</code>和<code>out</code>等关键字实现数据的输入和输出,从而实现数据的交流。如果从一个着色器向另一个着色器发送数据,则必须在发送方声明一个输出,在接收方声明一个类似的输入。当类型和名字都相同的时候,便会自动链接在一起,实现数据传递。<br>另一种从cpu向gpu发送数据的方式是<code>uniform</code>。<code>uniform</code>是全局的,无需借助其他中介实现数据传递。在着色器程序中声明<code>uniform</code>变量,在主程序中通过<code>glGetUniformLocation</code>获得其地址,从而设置着色器中uniform变量的值。</p>
<h2>2. CPU和GPU之间如何调度的</h2>
<p>如1中所述,GLSL运行在GPU,其通过接口实现和CPU之间的数据转换。<br>opengl程序涉及到两种类型的处理单元--CPU和GPU。opengl主程序由CPU调度运行,图像处理部分通过GLSL交由GPU执行。CPU与GPU之间的数据传递分三个步骤:一,首先利用内置的OpenGL函数生成一个ID号码;二,根据需要对该ID号码进行内存类型的绑定;在经过上面两个步骤之后,GPU中用于接收系统内存中数据的“标识符”就准备好了,第三部对这部分内存进行初始化,初始化的内容来自于系统内存中,这一部分功能利用glBufferData函数完成。<br>数据提交到GPU专用的内存中之后,需要根据应用场景对这些数据进行适当的分配。比如,有的数据当做顶点,有的是作为颜色,有的用于控制光照等等<br>此外,由于GPU具有高并行结构(heighly parallel structure),所以GPU在处理图形和复杂算法方面计算效率较高。CPU大部分面积为控制器和寄存器,而GPU拥有更多的ALU(Arithmetric Logic Unit,逻辑运算单云)用于数据处理,而非数据的高速缓存和流控制。</p>
<h2>3. opengl中缓冲区的概念</h2>
<p>[1] <strong>帧缓冲(frame buffer)</strong>:帧缓冲是下面几种缓冲的合集。通过帧缓冲可以将你的场景渲染到一个不同的帧缓冲中,可以使我们能够在场景中创建镜子这样的效果,或者做出一些炫酷的特效,存放显示用的数据的。<br>[2] <strong>颜色缓冲(color buffer)</strong>:存储所有片段的颜色:即视觉输出的效果。<br>[3] <strong>深度缓冲(depth buffer)</strong>:根据缓冲的z值,确定哪些面片被遮挡。由GLFW自动生成。<br>[4] <strong>模板缓冲(stencil buffer)</strong>:与深度测试类似,通过比较模板值和预设值,决定是否丢弃片段。<br>数据在opengl中处理顺序是: <strong>顶点着色器 - 片段着色器 - 模板测试 - 深度测试</strong><br>参考链接:<a href="https://link.segmentfault.com/?enc=uc4hCMLYFkgzeuVhzWHefA%3D%3D.ZcKQhUqU9TeFcPdEsV9W%2B6T%2FkdO1H87PRFgMRgFCobhVvWFH9fMwYAsbSepiROFFJ6SGR31zPJjVrvr%2Bto0Erw%3D%3D" rel="nofollow">http://blog.csdn.net/silangqu...</a></p>
<h2>4. mipmap是怎么回事</h2>
<p><a href="https://link.segmentfault.com/?enc=1p1QpAdCF5LTLbz6%2BCnjYQ%3D%3D.LtCY2Iqd871sdL7vHDZdQZoLCdAC8JA1bHJsgv6QVWgm%2FWuaY2xJQTwDkv%2FAhhxAu01YIgS7qxFEWg228B0UUBsZhzlhR%2FF%2FLY53LnyZ%2B4FAX0qgglxz7Q9%2BKnGH2%2BGk" rel="nofollow">Mipmap是多级渐远纹理</a>,也是目前应用最为广泛的纹理映射(map)技术之一。简单来说,就是实现 “实物(图片)看起来近大远小,近处清晰远处模糊”的效果。它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好</p>
<h2>5. opengl的常用坐标系</h2>
<p>局部空间(local space):或称为 物体空间.指对象所在的坐标空间<br>世界空间(world space):指顶点相对于(游戏)世界的坐标。物体变换到的最终空间就是世界坐标系<br>观察空间(view space):观察空间(View Space)经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间<br>裁剪空间(clip sapce):或称为视觉空间.在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。<br>屏幕空间(screen space):顾名思义,一般由<code>glViewPort</code>设置。<br>参考链接:<a href="https://link.segmentfault.com/?enc=qIVyDGEhgEYJ9bdII2v%2BMQ%3D%3D.bz3U3AsIctcCoDe65e3C3rOncdi7APip2WpXKcJHa6Ii98hZ47unNS1%2FFrIlazFctyH%2FuevACzqT9uR4dFh3%2Ftq0jAb8n5GlaNafhyFE4JPEm9m%2BLwq%2BEu9n0eaY9lvtnUqJiqvv9h9CRwiSqEUfGg%3D%3D" rel="nofollow">http://learnopengl-cn.readthe...</a></p>
<h2>6. opengl中的常用矩阵,各有什么作用</h2>
<p><code>model</code>:主要针对模型的平移、旋转、缩放、错切等功能,将模型由局部空间转换到世界空间<br><code>view</code>:视图矩阵。摄像机/观察者的位置等信息(设置鼠标移动、滚轮等效果),将所有世界坐标转换为观察坐标。<br><code>projection</code>:投影矩阵。裁剪坐标 转换到屏幕上</p>
<h2>7.为什么说opengl是一个状态机</h2>
<p>首先简单了解一下什么是"状态机",比如我们使用的电脑,接受各种输入(鼠标,键盘,摄像头等),然后改变自己当前的状态,但却并不知道状态的改变是如何实现的。opengl类似,接受各种参数,然后参数的改变引起当前状态的改变,达到一种新的状态(如:颜色改变,纹理变化,光照强弱变化)。<br><a href="https://link.segmentfault.com/?enc=laRD%2BRqHYj7RbWQXrCLUGQ%3D%3D.TCh2vwNbEhjsgcVPNTiK9istq0uqqqcU1%2Be7Db4vV7xCFQfImufNNFgL3y%2FCH4n519QjzL8ygABlzdtdmhBli0uaVhPUKDczh7lmJ%2BvdQhOvQTmsuWLEJFXCbLqCjVSI" rel="nofollow">opengl状态机</a></p>
<h2>8. <a href="https://link.segmentfault.com/?enc=XoyvDBMWtJlYZWycYPQP5A%3D%3D.WRt649zLSGrNtS5VhxcLmSpSob2%2FPM0kM9mfw1gG7IY8V6u3EsyllygSpFXKle4ZbDBWRS15ZMXCtKjDL%2Fqp4M%2BIa5N%2FUhd8RwvpHWkIOh4K5qp9PxeGF4YYU43J4zC6" rel="nofollow">光照</a>
</h2>
<p><code>冯式光照模型</code>:环境光照(Ambient)、漫反射(Diffuse)、镜面(Specular)<br>光源:点光、定向光、手电筒(聚光灯)</p>
<h2>9. 显示器是二维的,三维数据如何在二维屏幕上显示的。</h2>
<p>主要是通过<code>图形渲染管线(管线:实际上是指一堆原始图像数据途径一个输送管道,期间经过经过各种变换处理,最终输出在屏幕上的过程)</code>管理的,其被划分为两个过程:1. 把3D坐标转换为2D坐标(主要是通过投影矩阵完成)。 2. 把2D坐标转换为实际有颜色的像素。<br>2D坐标和像素是不同的,2D坐标精确表示一个点在空间的位置,而2D像素(好像都是整数)是这个点的近似值,2D像素受到个人屏幕/窗口 分辨率的限制。<br>由图可知,传入到片段着色器的颜色值并不是从顶点着色器传入的的,而是:顶点着色阶段(顶点、细分、几何)以及片段着色器之间有个光栅化阶段(<code>光栅化</code>:主要职责是判断屏幕的哪个部分被几何体所覆盖),也就是说传入片段着色器的结果是来自于光栅化的结果。<br><img src="/img/bVLEbS?w=776&h=401" alt="图形渲染管线的流程" title="图形渲染管线的流程"></p>
<h2>10. 透视投影和正投影、opengl摄像机</h2>
<p><code>投影</code>:在计算机图形学中,投影可以看做一种将三维坐标变换为二维坐标的方法。常用的有:正投影和透视投影。<br><code>透视投影</code>:是为了获得接近真实的三维物体效果而在二维平面上绘制、渲染的方法。类似于现实中人对事物的认识(近大远小,远处模糊近期清晰)。<br><code>正投影</code>:正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。<br><code>摄像机</code>:观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。<br><a href="https://link.segmentfault.com/?enc=Dh5D9qvdYrIj0zjWrUBKBg%3D%3D.AwXkvjYGq4ttKL9%2BSwtVfND0dXQeBTCI1lj67TU7E9NxInFwla2i83I3kFJt0yXAJ%2BftR9vl5yUtobWZan8HY6A05RDkiciKfKGro0VT3hsWQp3lH%2F%2BkFviju8BW92sq" rel="nofollow">投影、摄像机、opengl坐标系统</a></p>
pcl之MLS算法计算并统一法向量
https://segmentfault.com/a/1190000007941880
2016-12-28T10:18:45+08:00
2016-12-28T10:18:45+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>利用MLS算法计算法向量,并统一法向。<a href="https://link.segmentfault.com/?enc=7iuzpa44WPyO%2FCNbl1Ziag%3D%3D.pk81kuFaN2udI8KTHec%2Fprndj0CljgGtVPVx7P2yhxhi6xAkx2853Tk28GQicZw%2BRllQK1qQKLQzdlMdYyGzXw%3D%3D" rel="nofollow">MLS其他说明</a>
</h2>
<p>该算法比直接基于<a href="https://segmentfault.com/n/1330000005761876">SVD</a>的算法慢,但是对法向进行了统一。</p>
<pre><code>#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/surface/mls.h>
#include <pcl/console/time.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/visualization/pcl_visualizer.h> //包含基本可视化类
#include <boost/thread/thread.hpp>
#include <pcl/point_cloud.h>
using namespace std;
typedef pcl::PointXYZ point;
typedef pcl::PointCloud<point> pointcloud;
int main (int argc,char **argv)
{
pointcloud::Ptr cloud (new pointcloud);
pcl::io::loadPCDFile(argv[1],*cloud);
cout<<"points size is:"<<cloud->size()<<endl;
pcl::search::KdTree<point>::Ptr tree (new pcl::search::KdTree<point>);
//创建存储的mls对象
pcl::PointCloud<pcl::PointNormal> mls_points;
// pcl::PointCloud<point> mls_points;
//创建mls对象
pcl::MovingLeastSquares<point,pcl::PointNormal> mls;
// pcl::MovingLeastSquares<point,point> mls;
mls.setComputeNormals(true);
mls.setInputCloud(cloud);
mls.setPolynomialFit(true); //设置为true则在平滑过程中采用多项式拟合来提高精度
mls.setPolynomialOrder(2); //MLS拟合的阶数,默认是2
mls.setSearchMethod(tree);
mls.setSearchRadius(5.1); //搜索半径
mls.process(mls_points);
pcl::PointCloud<pcl::PointNormal>::Ptr mls_points_normal (new pcl::PointCloud<pcl::PointNormal>);
mls_points_normal = mls_points.makeShared();
cout<<"mls poits size is: "<<mls_points.size()<<endl;
boost::shared_ptr<pcl::visualization::PCLVisualizer> view(new pcl::visualization::PCLVisualizer ("test"));
view->setBackgroundColor(0.0,0,0);
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointNormal> v(mls_points_normal,0,250,0);
view->addPointCloud<pcl::PointNormal>(mls_points_normal,v,"sample");
view->addPointCloudNormals<pcl::PointNormal>(mls_points_normal,10,10,"normal");
view->addCoordinateSystem(1.0); //建立空间直角坐标系
view->spin();
// Save output
pcl::io::savePCDFile ("mid-mls.pcd", mls_points);
}</code></pre>
<p>第一幅图片为基于MLS计算的法向,并进行了统一。<br>第二幅图片为SVD计算,法向具有二异性。<br><img src="/img/bVHudg?w=488&h=351" alt="图片描述" title="图片描述"><img src="/img/bVHudi?w=775&h=323" alt="图片描述" title="图片描述"></p>
pcl之MLS平滑
https://segmentfault.com/a/1190000007938371
2016-12-27T21:32:14+08:00
2016-12-27T21:32:14+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>MLS移动最小二乘法</h2><p><strong><a href="https://link.segmentfault.com/?enc=eh0zBPjZWLH4qJ37XrDHCw%3D%3D.8TpcnYVOLH1xZUVg3UmjLpFb%2FRl%2Fb6UycrOa6P0xeXz4%2Ftfprp1%2FrWkf0ew0DJbR87XtksEGiReelRvShkqegEI9SZWcGNVUj%2FG9ZphM5Rwyyv%2BHxG%2FEoy6HRM7HhZ8lvknac9BJU2arPz1BBOsCGg%3D%3D" rel="nofollow">移动最小二乘法</a>对点云进行平滑处理,数据重采样,并且可以计算优化的估计法线,还可以用来曲面重建</strong></p><pre><code>#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/surface/mls.h>
#include <pcl/console/time.h>
#include <pcl/point_cloud.h>
using namespace std;
typedef pcl::PointXYZ point;
typedef pcl::PointCloud<point> pointcloud;
int main (int argc,char **argv)
{
pointcloud::Ptr cloud (new pointcloud);
pcl::io::loadPCDFile(argv[1],*cloud);
cout<<"points size is:"<<cloud->size()<<endl;
pcl::search::KdTree<point>::Ptr tree (new pcl::search::KdTree<point>);
//创建存储的mls对象
// pcl::PointCloud<pcl::PointNormal> mls_points;
pcl::PointCloud<point> mls_points;
//创建mls对象
// pcl::MovingLeastSquares<point,pcl::PointNormal> mls;
pcl::MovingLeastSquares<point,point> mls;
mls.setComputeNormals(true);
mls.setInputCloud(cloud);
mls.setPolynomialFit(true); //设置为true则在平滑过程中采用多项式拟合来提高精度
mls.setPolynomialOrder(2); //MLS拟合的阶数,默认是2
mls.setSearchMethod(tree);
mls.setSearchRadius(1.1); //这个值越大,输出的点越多
mls.process(mls_points);
cout<<"mls poits size is: "<<mls_points.size()<<endl;
// Save output
pcl::io::savePCDFile ("mid-mls.pcd", mls_points);
}
return 0;</code></pre><p><img src="/img/bVHtiw" alt="原始数据" title="原始数据"><img src="/img/bVHtiA" alt="MLS处理之后的数据" title="MLS处理之后的数据"></p>
pcl之FPFH配准
https://segmentfault.com/a/1190000007703156
2016-12-05T21:12:05+08:00
2016-12-05T21:12:05+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
3
<h2>什么是fpfh特征</h2>
<p>有关快速点云直方图(fpfh)特征的数学描述,在这里不做过多介绍,可以查看<a href="https://link.segmentfault.com/?enc=loUH8FQ5FwUrLCBBSRm3sg%3D%3D.qAm8GwsqBwDToFZe5yG57sIe8rCOpptwHwPdlqBmw5EF2SZmmFdalh4S498xxpr5iYzuCZ%2BP2s4uMjT0LnM30VhCPhv%2BEI6%2FK77pODBlqANJ%2FsJaMo2k6u7yMCbYZ5a2" rel="nofollow">fpfh</a>。也可以查看PCL的官网解释,中文版可直接搜索<a href="https://link.segmentfault.com/?enc=%2BRMDV4%2Bfm6%2FHKQJaLxrjUA%3D%3D.bFWFyKZJqC%2FQ1yeOziY%2B1bhWH0hVJ59XURgLWK6n%2FliEdSLmfcwEgPEKPsJy2%2FsWJJBY2Y6mYHYv0uEYVcgG%2Fw%3D%3D" rel="nofollow">pcl中国fpfh</a>。</p>
<h2>主程序</h2>
<p>首先还是一堆头文件(当然好多头文件在这里没用到,可自行删除)</p>
<pre><code>#include <pcl/io/pcd_io.h>
#include <ctime>
#include <Eigen/Core>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/features/fpfh.h>
#include <pcl/registration/ia_ransac.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <boost/thread/thread.hpp>
#include <pcl/features/fpfh_omp.h> //包含fpfh加速计算的omp(多核并行计算)
#include <pcl/registration/correspondence_estimation.h>
#include <pcl/registration/correspondence_rejection_features.h> //特征的错误对应关系去除
#include <pcl/registration/correspondence_rejection_sample_consensus.h> //随机采样一致性去除
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/approximate_voxel_grid.h</code></pre>
<p>为了方便记:</p>
<pre><code>using namespace std;
typedef pcl::PointCloud<pcl::PointXYZ> pointcloud;
typedef pcl::PointCloud<pcl::Normal> pointnormal;
typedef pcl::PointCloud<pcl::FPFHSignature33> fpfhFeature;</code></pre>
<p>为了使用fpfp特征匹配,声明一个计算fpfh特征点的函数:</p>
<pre><code>fpfhFeature::Ptr compute_fpfh_feature(pointcloud::Ptr input_cloud,pcl::search::KdTree<pcl::PointXYZ>::Ptr tree)
{
//法向量
pointnormal::Ptr point_normal (new pointnormal);
pcl::NormalEstimation<pcl::PointXYZ,pcl::Normal> est_normal;
est_normal.setInputCloud(input_cloud);
est_normal.setSearchMethod(tree);
est_normal.setKSearch(10);
est_normal.compute(*point_normal);
//fpfh 估计
fpfhFeature::Ptr fpfh (new fpfhFeature);
//pcl::FPFHEstimation<pcl::PointXYZ,pcl::Normal,pcl::FPFHSignature33> est_target_fpfh;
pcl::FPFHEstimationOMP<pcl::PointXYZ,pcl::Normal,pcl::FPFHSignature33> est_fpfh;
est_fpfh.setNumberOfThreads(4); //指定4核计算
// pcl::search::KdTree<pcl::PointXYZ>::Ptr tree4 (new pcl::search::KdTree<pcl::PointXYZ> ());
est_fpfh.setInputCloud(input_cloud);
est_fpfh.setInputNormals(point_normal);
est_fpfh.setSearchMethod(tree);
est_fpfh.setKSearch(10);
est_fpfh.compute(*fpfh);
return fpfh;
}</code></pre>
<p>可以看出,在计算Fpfh特征时,首先需要计算点集的法向量(法向量是点云的一个非常重要的特征,本该单独处理,仅在这里为了方便,少写两行代码,将其封装在FPFH特征的计算中),根据计算好的法向量,计算FPFH特征。计算fpfh特征时,近邻点集个数不易取得过大,,否则一则导致计算量增大,二会使得fpfh的计算失去意义(通其他特征计算一样,过大的近邻点集合不能反映局部特征)。</p>
<p>主函数:</p>
<pre><code>int main (int argc, char **argv)
{
if (argc < 3)
{
cout<<"please input two pointcloud"<<endl;
return -1;
}
clock_t start,end,time;
start = clock();
pointcloud::Ptr source (new pointcloud);
pointcloud::Ptr target (new pointcloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ());
fpfhFeature::Ptr source_fpfh = compute_fpfh_feature(source,tree);
fpfhFeature::Ptr target_fpfh = compute_fpfh_feature(target,tree);
//对齐(占用了大部分运行时间)
pcl::SampleConsensusInitialAlignment<pcl::PointXYZ, pcl::PointXYZ, pcl::FPFHSignature33> sac_ia;
sac_ia.setInputSource(source);
sac_ia.setSourceFeatures(source_fpfh);
sac_ia.setInputTarget(target);
sac_ia.setTargetFeatures(target_fpfh);
pointcloud::Ptr align (new pointcloud);
// sac_ia.setNumberOfSamples(20); //设置每次迭代计算中使用的样本数量(可省),可节省时间
sac_ia.setCorrespondenceRandomness(6); //设置计算协方差时选择多少近邻点,该值越大,协防差越精确,但是计算效率越低.(可省)
sac_ia.align(*align);
end = clock();
cout <<"calculate time is: "<< float (end-start)/CLOCKS_PER_SEC<<endl;
//可视化
boost::shared_ptr<pcl::visualization::PCLVisualizer> view (new pcl::visualization::PCLVisualizer("fpfh test"));
int v1;
int v2;
view->createViewPort(0,0.0,0.5,1.0,v1);
view->createViewPort(0.5,0.0,1.0,1.0,v2);
view->setBackgroundColor(0,0,0,v1);
view->setBackgroundColor(0.05,0,0,v2);
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sources_cloud_color(source,250,0,0);
view->addPointCloud(source,sources_cloud_color,"sources_cloud_v1",v1);
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> target_cloud_color (target,0,250,0);
view->addPointCloud(target,target_cloud_color,"target_cloud_v1",v1);
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"sources_cloud_v1");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>aligend_cloud_color(final,255,0,0);
view->addPointCloud(align,aligend_cloud_color,"aligend_cloud_v2",v2);
view->addPointCloud(target,target_cloud_color,"target_cloud_v2",v2);
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,4,"aligend_cloud_v2");
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"target_cloud_v2");
// view->addCorrespondences<pcl::PointXYZ>(source,target,*cru_correspondences,"correspondence",v1);//添加显示对应点对
while (!view->wasStopped())
{
// view->spin();
view->spinOnce(100);
boost::this_thread::sleep (boost::posix_time::microseconds (100000));
}
pcl::io::savePCDFile ("crou_output.pcd", *align);
// pcl::io::savePCDFile ("final_align.pcd", *final);
return 0;
}</code></pre>
<p>采用FPFH特征配准,效果不错,但是计算效率非常低,尤其针对大规模点云数据时。所以,很多时候,都先对原始点云进行简化,对简化后的数据做配准计算,在将所获得的配准参数应用到原始点云,以提高计算效率。</p>
<h2>体素网格简化</h2>
<p>主要程序:</p>
<pre><code> //pcl::ApproximateVoxelGrid<pcl::PointXYZ> approximate_voxel_grid;
pcl::VoxelGrid<pcl::PointXYZ> approximate_voxel_grid;
approximate_voxel_grid.setLeafSize(0.5,0.5,0.5); //网格边长.这里的数值越大,则精简的越厉害(剩下的数据少)
pointcloud::Ptr source (new pointcloud);
pointcloud::Ptr sample_sources (new pointcloud);
approximate_voxel_grid.setInputCloud(source);
approximate_voxel_grid.filter(*sample_source);
cout << "source voxel grid Filte cloud size is " << sample_source->size()<<endl;
// pcl::io::savePCDFile("voxelgrid.pcd",*out);</code></pre>
<p>针对体素网格简化,PCL提供了两种方法:其一,pcl::ApproximateVoxelGrid<pcl::PointXYZ> 类;其二, pcl::VoxelGrid<pcl::PointXYZ>类。可以看出,第二中比第一中少了“大约”approximate,也就是说第二种某些情况下比第一种更精确。原因是:<strong>第一种方法是利用体素网格的中心(长方体的中心)代替原始点,而第二种则是对体素网格中所有点求均值,以期望均值点代替原始点集</strong></p>
<h2>可视化</h2>
<p>在如上主程序中,已经包含了可视化的功能,更过可视化可看我的博客<a href="https://segmentfault.com/a/1190000006685118">pcl可视化那些事</a>,在这里,细致的讲一下如何添加对应点对的可视化功能。<br>要可视化对应关系,首先需要计算对应关系,本文配准为例:</p>
<pre><code> pcl::registration::CorrespondenceEstimation<pcl::FPFHSignature33,pcl::FPFHSignature33> crude_cor_est;
boost::shared_ptr<pcl::Correspondences> cru_correspondences (new pcl::Correspondences);
crude_cor_est.setInputSource(source_fpfh);
crude_cor_est.setInputTarget(target_fpfh);
// crude_cor_est.determineCorrespondences(cru_correspondences);
crude_cor_est.determineReciprocalCorrespondences(*cru_correspondences);
cout<<"crude size is:"<<cru_correspondences->size()<<endl;</code></pre>
<h2>效果(粗配)</h2>
<p><img src="/img/bVGt6B?w=683&h=384" alt="图片描述" title="图片描述"></p>
VS2015配置QT5.X环境
https://segmentfault.com/a/1190000007567023
2016-11-22T20:55:01+08:00
2016-11-22T20:55:01+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<h2><strong>最近VS2015支持qt-vs-addin了</strong></h2>
<p>最近在配置VS2015+QT5环境,网上教程几乎千篇一律,总结起来几乎都是:安装VS2015,安装qt5,最麻烦还要手动设置环境变量,最后好像还有安装个什么qt5Package插件。写在这里不是为了贬低别人的成果,他们当时必定都安装配置成功了,看了看他们博客的时间,都是在2016-8月前的记录。由此得出,QT官网当时必定没有出直接针对VS2015的addin插件。</p>
<p>现在来看,在VS2015中集成QT5似乎很简单。</p>
<ol>
<li>安装VS2015自不必说</li>
<li>安装QT包。进入<a href="https://link.segmentfault.com/?enc=MSzQdQ9PmbWHDdWURkMy7A%3D%3D.nIpqC%2FXD%2Ft3HOKs4cBCywe3%2BDvv5PCgCPoSOi5LDnR2dAT3PYH8S1a2ksFggLmO4" rel="nofollow">QT</a>,选择<code>QT Offine Installers</code>,下载适合自己电脑环境的QTexe安装包,下载之后直接双击安装QT。</li>
<li>VS+QT集成。上述两步完成之后,VS中是不会包含QT的环境的,解决办法进入(<a href="https://link.segmentfault.com/?enc=PSfL23CvCuga3O3FMTWT%2BA%3D%3D.MWKA8s3h8N7hXUH96fDMJx1688sm427Az%2F54h8gO0j0FlLvxQWq69NqXM5oYyddadfEMqeIhPJeSlnAZNaMaeg%3D%3D" rel="nofollow">http://download.qt.io/develop...</a>选择下载<code> qt-vs-addin-msvc2015-2.0.0-beta.vsix</code>,下载完成之后直接安装即可。(看出版时间,是10-Aug-2016,目前还是测试版,不过对于学习应用,应该是足够了)。</li>
<li>安装完成之后,如果你的QT是安装在C盘,那么VS能够自动找到QT的路径,如果没有安装在C盘,那么则需要自己添加QT的<code>bin</code>路径。</li>
</ol>
<p>测试:按照上述步骤,打开VS则会看到<img src="/img/bVFUNP?w=286&h=119" alt="图片描述" title="图片描述">。<br>点击QT选项,则会看到</p>
<p><img src="/img/bVFUNZ?w=507&h=319" alt="clipboard.png" title="clipboard.png"></p>
<p>然后点击<code>QT Options</code>,则会看到:<br><img src="/img/bVFUOa?w=469&h=353" alt="clipboard.png" title="clipboard.png"><br>其中,QT4是默认安装在C盘的,QT5是安装在D盘下的,在这里我选择使用QT5,貌似QT4有点多余了。</p>
<ol>
<li>按照步骤 1 - 4打开 <code>.pro</code>文件测试成功。</li>
<li>新建QT程序:<br><img src="/img/bVFUPj?w=956&h=582" alt="clipboard.png" title="clipboard.png">
</li>
</ol>
<h2>方法二</h2>
<p>在VS2015中使用QT5,首先安装插件,在vs2015中,依次点击<code>工具</code>-><code>扩展和更新</code>,然后搜索<code>qt</code> ,安装相关插件,之后按照上述步骤添加QT的安装路径即可。<br><img src="/img/bVFUPV?w=917&h=392" alt="clipboard.png" title="clipboard.png"></p>
pcl边界识别
https://segmentfault.com/a/1190000007315518
2016-10-28T20:54:34+08:00
2016-10-28T20:54:34+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
6
<pre><code class="c++">#include <iostream>
#include <vector>
#include <ctime>
#include <boost/thread/thread.hpp>
#include <pcl/io/pcd_io.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/console/parse.h>
#include <pcl/features/eigen.h>
#include <pcl/features/feature.h>
#include <pcl/features/normal_3d.h>
#include <pcl/impl/point_types.hpp>
#include <pcl/features/boundary.h>
#include <pcl/visualization/cloud_viewer.h>
using namespace std;
int main(int argc, char **argv)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
// if (pcl::io::loadPCDFile<pcl::PointXYZ>("/home/yxg/pcl/pcd/mid.pcd",*cloud) == -1)
if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1],*cloud) == -1)
{
PCL_ERROR("COULD NOT READ FILE mid.pcl \n");
return (-1);
}
std::cout << "points sieze is:"<< cloud->size()<<std::endl;
pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal>);
pcl::PointCloud<pcl::Boundary> boundaries;
pcl::BoundaryEstimation<pcl::PointXYZ,pcl::Normal,pcl::Boundary> est;
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
/*
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree; //创建一个快速k近邻查询,查询的时候若该点在点云中,则第一个近邻点是其本身
kdtree.setInputCloud(cloud);
int k =2;
float everagedistance =0;
for (int i =0; i < cloud->size()/2;i++)
{
vector<int> nnh ;
vector<float> squaredistance;
// pcl::PointXYZ p;
// p = cloud->points[i];
kdtree.nearestKSearch(cloud->points[i],k,nnh,squaredistance);
everagedistance += sqrt(squaredistance[1]);
// cout<<everagedistance<<endl;
}
everagedistance = everagedistance/(cloud->size()/2);
cout<<"everage distance is : "<<everagedistance<<endl;
*/
pcl::NormalEstimation<pcl::PointXYZ,pcl::Normal> normEst; //其中pcl::PointXYZ表示输入类型数据,pcl::Normal表示输出类型,且pcl::Normal前三项是法向,最后一项是曲率
normEst.setInputCloud(cloud);
normEst.setSearchMethod(tree);
// normEst.setRadiusSearch(2); //法向估计的半径
normEst.setKSearch(9); //法向估计的点数
normEst.compute(*normals);
cout<<"normal size is "<< normals->size()<<endl;
//normal_est.setViewPoint(0,0,0); //这个应该会使法向一致
est.setInputCloud(cloud);
est.setInputNormals(normals);
// est.setAngleThreshold(90);
// est.setSearchMethod (pcl::search::KdTree<pcl::PointXYZ>::Ptr (new pcl::search::KdTree<pcl::PointXYZ>));
est.setSearchMethod (tree);
est.setKSearch(20); //一般这里的数值越高,最终边界识别的精度越好
// est.setRadiusSearch(everagedistance); //搜索半径
est.compute (boundaries);
// pcl::PointCloud<pcl::PointXYZ> boundPoints;
pcl::PointCloud<pcl::PointXYZ>::Ptr boundPoints (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ> noBoundPoints;
int countBoundaries = 0;
for (int i=0; i<cloud->size(); i++){
uint8_t x = (boundaries.points[i].boundary_point);
int a = static_cast<int>(x); //该函数的功能是强制类型转换
if ( a == 1)
{
// boundPoints.push_back(cloud->points[i]);
( *boundPoints).push_back(cloud->points[i]);
countBoundaries++;
}
else
noBoundPoints.push_back(cloud->points[i]);
}
std::cout<<"boudary size is:" <<countBoundaries <<std::endl;
// pcl::io::savePCDFileASCII("boudary.pcd",boundPoints);
pcl::io::savePCDFileASCII("boudary.pcd", *boundPoints);
pcl::io::savePCDFileASCII("NoBoundpoints.pcd",noBoundPoints);
pcl::visualization::CloudViewer viewer ("test");
viewer.showCloud(boundPoints);
while (!viewer.wasStopped())
{
}
return 0;
}</code></pre>
<p><img src="/img/bVERgd?w=683&h=384" alt="图片描述" title="图片描述"></p>
pcl常用小知识
https://segmentfault.com/a/1190000007125502
2016-10-11T08:09:34+08:00
2016-10-11T08:09:34+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
10
<h3>时间计算</h3>
<p>pcl中计算程序运行时间有很多函数,其中利用控制台的时间计算是:<br>首先必须包含头文件 <code>#include <pcl/console/time.h></code>,其次,<code>pcl::console::TicToc time; time.tic(); +程序段 + cout<<time.toc()/1000<<"s"<<endl;</code>就可以以秒输出“程序段”的运行时间。</p>
<h3>如何实现类似pcl::PointCloud::Ptr和pcl::PointCloud的两个类相互转换?</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudPointer(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ> cloud;
cloud = *cloudPointer;
cloudPointer = cloud.makeShared();</code></pre>
<h3>如何查找点云的x,y,z的极值?</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/common/common.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud;
cloud = pcl::PointCloud<pcl::PointXYZ>::Ptr (new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ> ("your_pcd_file.pcd", *cloud);
pcl::PointXYZ minPt, maxPt;
pcl::getMinMax3D (*cloud, minPt, maxPt);</code></pre>
<h3>如果知道需要保存点的索引,如何从原点云中拷贝点到新点云?</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <pcl/common/impl/io.hpp>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("C:\office3-after21111.pcd", *cloud);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudOut(new pcl::PointCloud<pcl::PointXYZ>);
std::vector<int > indexs = { 1, 2, 5 };
pcl::copyPointCloud(*cloud, indexs, *cloudOut);</code></pre>
<h3>如何从点云里删除和添加点?</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <pcl/common/impl/io.hpp>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("C:\office3-after21111.pcd", *cloud);
pcl::PointCloud<pcl::PointXYZ>::iterator index = cloud->begin();
cloud->erase(index);//删除第一个
index = cloud->begin() + 5;
cloud->erase(cloud->begin());//删除第5个
pcl::PointXYZ point = { 1, 1, 1 };
//在索引号为5的位置1上插入一点,原来的点后移一位
cloud->insert(cloud->begin() + 5, point);
cloud->push_back(point);//从点云最后面插入一点
std::cout << cloud->points[5].x;//输出1</code></pre>
<p>如果删除的点太多建议用上面的方法拷贝到新点云,再赋值给原点云,如果要添加很多点,建议先resize,然后用循环向点云里的添加。</p>
<h3>如何对点云进行全局或局部变换</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <pcl/common/impl/io.hpp>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/common/transforms.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile("path/.pcd",*cloud);
//全局变化
//构造变化矩阵
Eigen::Matrix4f transform_1 = Eigen::Matrix4f::Identity();
float theta = M_PI/4; //旋转的度数,这里是45度
transform_1 (0,0) = cos (theta); //这里是绕的Z轴旋转
transform_1 (0,1) = -sin(theta);
transform_1 (1,0) = sin (theta);
transform_1 (1,1) = cos (theta);
// transform_1 (0,2) = 0.3; //这样会产生缩放效果
// transform_1 (1,2) = 0.6;
// transform_1 (2,2) = 1;
transform_1 (0,3) = 25; //这里沿X轴平移
transform_1 (1,3) = 30;
transform_1 (2,3) = 380;
pcl::PointCloud<pcl::PointXYZ>::Ptr transform_cloud1 (new pcl::PointCloud<pcl::PointXYZ>);
pcl::transformPointCloud(*cloud,*transform_cloud1,transform_1); //不言而喻
//局部
pcl::transformPointCloud(*cloud,pcl::PointIndices indices,*transform_cloud1,matrix); //第一个参数为输入,第二个参数为输入点云中部分点集索引,第三个为存储对象,第四个是变换矩阵。</code></pre>
<h3>链接两个点云字段(两点云大小必须相同)</h3>
<pre><code> pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile("/home/yxg/pcl/pcd/mid.pcd",*cloud);
pcl::NormalEstimation<pcl::PointXYZ,pcl::Normal> ne;
ne.setInputCloud(cloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>());
ne.setSearchMethod(tree);
pcl::PointCloud<pcl::Normal>::Ptr cloud_normals(new pcl::PointCloud<pcl::Normal>());
ne.setKSearch(8);
//ne.setRadisuSearch(0.3);
ne.compute(*cloud_normals);
pcl::PointCloud<pcl::PointNormal>::Ptr cloud_with_nomal (new pcl::PointCloud<pcl::PointNormal>);
pcl::concatenateFields(*cloud,*cloud_normals,*cloud_with_nomal);</code></pre>
<h3>如何从点云中删除无效点</h3>
<p>pcl中的无效点是指:点的某一坐标值为nan.</p>
<pre><code> #include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/filter.h>
#include <pcl/io/pcd_io.h>
using namespace std;
typedef pcl::PointXYZRGBA point;
typedef pcl::PointCloud<point> CloudType;
int main (int argc,char **argv)
{
CloudType::Ptr cloud (new CloudType);
CloudType::Ptr output (new CloudType);
pcl::io::loadPCDFile(argv[1],*cloud);
cout<<"size is:"<<cloud->size()<<endl;
vector<int> indices;
pcl::removeNaNFromPointCloud(*cloud,*output,indices);
cout<<"output size:"<<output->size()<<endl;
pcl::io::savePCDFile("out.pcd",*output);
return 0;
}</code></pre>
<h3>将xyzrgb格式转换为xyz格式的点云</h3>
<pre><code>#include <pcl/io/pcd_io.h>
#include <ctime>
#include <Eigen/Core>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
using namespace std;
typedef pcl::PointXYZ point;
typedef pcl::PointXYZRGBA pointcolor;
int main(int argc,char **argv)
{
pcl::PointCloud<pointcolor>::Ptr input (new pcl::PointCloud<pointcolor>);
pcl::io::loadPCDFile(argv[1],*input);
pcl::PointCloud<point>::Ptr output (new pcl::PointCloud<point>);
int M = input->points.size();
cout<<"input size is:"<<M<<endl;
for (int i = 0;i <M;i++)
{
point p;
p.x = input->points[i].x;
p.y = input->points[i].y;
p.z = input->points[i].z;
output->points.push_back(p);
}
output->width = 1;
output->height = M;
cout<< "size is"<<output->size()<<endl;
pcl::io::savePCDFile("output.pcd",*output);
}
</code></pre>
<h2>flann kdtree 查询k近邻</h2>
<pre><code> //平均密度计算
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree; //创建一个快速k近邻查询,查询的时候若该点在点云中,则第一个近邻点是其本身
kdtree.setInputCloud(cloud);
int k =2;
float everagedistance =0;
for (int i =0; i < cloud->size()/2;i++)
{
vector<int> nnh ;
vector<float> squaredistance;
// pcl::PointXYZ p;
// p = cloud->points[i];
kdtree.nearestKSearch(cloud->points[i],k,nnh,squaredistance);
everagedistance += sqrt(squaredistance[1]);
// cout<<everagedistance<<endl;
}
everagedistance = everagedistance/(cloud->size()/2);
cout<<"everage distance is : "<<everagedistance<<endl;
</code></pre>
<pre><code>#include <pcl/kdtree/kdtree_flann.h>
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree; //创建KDtree
kdtree.setInputCloud (in_cloud);
pcl::PointXYZ searchPoint; //创建目标点,(搜索该点的近邻)
searchPoint.x = 1;
searchPoint.y = 2;
searchPoint.z = 3;
//查询近邻点的个数
int k = 10; //近邻点的个数
std::vector<int> pointIdxNKNSearch(k); //存储近邻点集的索引
std::vector<float>pointNKNSquareDistance(k); //近邻点集的距离
if (kdtree.nearestKSearch(searchPoint,k,pointIdxNKNSearch,pointNKNSquareDistance)>0)
{
for (size_t i = 0; i < pointIdxNKNSearch.size (); ++i)
std::cout << " " << in_cloud->points[ pointIdxNKNSearch[i] ].x
<< " " << in_cloud->points[ pointIdxNKNSearch[i] ].y
<< " " <<in_cloud->points[ pointIdxNKNSearch[i] ].z
<< " (squared distance: " <<pointNKNSquareDistance[i] << ")" << std::endl;
}
//半径为r的近邻点
float radius = 40.0f; //其实是求的40*40距离范围内的点
std::vector<int> pointIdxRadiusSearch; //存储的对应的平方距离
std::vector<float> a;
if ( kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, a) > 0 )
{
for (size_t i = 0; i < pointIdxRadiusSearch.size (); ++i)
std::cout << " " << in_cloud->points[ pointIdxRadiusSearch[i] ].x
<< " " <<in_cloud->points[ pointIdxRadiusSearch[i] ].y
<< " " << in_cloud->points[ pointIdxRadiusSearch[i] ].z
<< " (squared distance: " <<a[i] << ")" << std::endl;
}</code></pre>
<h2>关于<code>ply</code>文件</h2>
<p>后缀命名为<code>.ply</code>格式文件,常用的点云数据文件。<code>ply</code>文件不仅可以存储<strong><code>点</code></strong>数据,而且可以存储<strong><code>网格</code></strong>数据. 用emacs打开一个<code>ply</code>文件,观察表头,如果表头<code>element face</code>的值为0,ze则表示该文件为点云文件,如果<code>element face</code>的值为某一正整数N,则表示该文件为网格文件,且包含N个网格.<br>所以利用pcl读取 ply 文件,不能一味用<code>pcl::PointCloud<PointT>::Ptr cloud (new pcl::PointCloud<PintT>)</code>来读取。<br>在读取<code>ply</code>文件时候,首先要分清该文件是点云还是网格类文件。如果是点云文件,则按照一般的点云类去读取即可,<a href="https://link.segmentfault.com/?enc=H7Gc7cfO6a2VwpXPoQ9iEw%3D%3D.WGa4Hs%2FkmqyDC4%2BnBgpJ3AoPE6eOMkA9%2Fw9OPbkMTKjmN4dgMRjDQ3pxBwTO0Mma5Q5TfJpqRUXKaKp2B0cbHo%2FJSEgUKjx4EWiLuwhEMYY%3D" rel="nofollow">官网例子</a>,就是这样。<br>如果<code>ply</code>文件是网格类,则需要</p>
<pre><code> pcl::PolygonMesh mesh;
pcl::io::loadPLYFile(argv[1],mesh);
pcl::io::savePLYFile("result.ply", mesh);
</code></pre>
<p>读取。(官网例子之所以能成功,是因为它对模型进行了细分处理,使得网格变成了点)</p>
<h2>计算点的索引</h2>
<p>例如sift算法中,pcl无法直接提供索引(主要原因是sift点是通过计算出来的,在某些不同参数下,sift点可能并非源数据中的点,而是某些点的近似),若要获取索引,则可利用以下函数:</p>
<pre><code>void getIndices (pointcloud::Ptr cloudin, pointcloud keypoints, pcl::PointIndices::Ptr indices)
{
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
kdtree.setInputCloud(cloudin);
std::vector<float>pointNKNSquareDistance; //近邻点集的距离
std::vector<int> pointIdxNKNSearch;
for (size_t i =0; i < keypoints.size();i++)
{
kdtree.nearestKSearch(keypoints.points[i],1,pointIdxNKNSearch,pointNKNSquareDistance);
// cout<<"the distance is:"<<pointNKNSquareDistance[0]<<endl;
// cout<<"the indieces is:"<<pointIdxNKNSearch[0]<<endl;
indices->indices.push_back(pointIdxNKNSearch[0]);
}
}</code></pre>
<p>其思想就是:将原始数据插入到flann的kdtree中,寻找keypoints的最近邻,如果距离等于0,则说明是同一点,提取索引即可.</p>
<h2>计算质心</h2>
<pre><code> Eigen::Vector4f centroid; //质心
pcl::compute3DCentroid(*cloud_smoothed,centroid); //估计质心的坐标</code></pre>
<h2>从网格提取顶点(将网格转化为点)</h2>
<pre><code>#include <pcl/io/io.h>
#include <pcl/io/pcd_io.h>
#include <pcl/io/obj_io.h>
#include <pcl/PolygonMesh.h>
#include <pcl/point_cloud.h>
#include <pcl/io/vtk_lib_io.h>//loadPolygonFileOBJ所属头文件;
#include <pcl/io/vtk_io.h>
#include <pcl/io/ply_io.h>
#include <pcl/point_types.h>
using namespace pcl;
int main(int argc,char **argv)
{
pcl::PolygonMesh mesh;
// pcl::io::loadPolygonFileOBJ(argv[1], mesh);
pcl::io::loadPLYFile(argv[1],mesh);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::fromPCLPointCloud2(mesh.cloud, *cloud);
pcl::io::savePCDFileASCII("result.pcd", *cloud);
return 0;
}</code></pre>
<p>以上代码可以从.obj或.ply面片格式转化为点云类型。</p>
Linux+emacs个性化定制
https://segmentfault.com/a/1190000007072935
2016-10-04T22:14:51+08:00
2016-10-04T22:14:51+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>1.最左侧显示行号</h2>
<p>在emacs的配置文件(~./.emacs.d/init.el)文件里添加如下内容:</p>
<p>(add-to-list 'load-path "/usr/share/emacs/site-lisp")<br>(require 'linum)<br>(global-linum-mode t)</p>
<h2>2. emacs无法输入中文(针对gentoo,其他linux系统可参考)</h2>
<ol>
<li><p>首先应该先检查字体配置, 然后检查是否开启eamcs的xft的USE标示符.</p></li>
<li><p>安装 font-cursor-misc 或 font-adobe-75dpi ,重启计算机测试(好像大部分是因为缺少这个语言包的问题)</p></li>
<li>
<p>安装:</p>
<pre><code>1). [ebuild N ] media-fonts/font-adobe-75dpi-1.0.0 USE="X nls" 0 kB
2). [ebuild N ] x11-apps/bdftopcf-1.0.2 USE="-debug" 0 kB
3). [ebuild N ] media-fonts/font-alias-1.0.1 USE="-debug" 0 kB
4). [ebuild N ] media-fonts/font-util-1.1.1 USE="-debug" 0 kB</code></pre>
</li>
</ol>
<h2>3. 在emacs文件的标题栏显示该文件的绝对路径: (好处是当你打开多个同名文件时,易于区分)</h2>
<p><code>(setq frame-title-format ("%S" (buffer-file-name "%f" (dired-directory dired-directory "%b"))))</code></p>
<h2>4. 如果不想显示绝对路径,只是想显示该文件的名称</h2>
<p>(setq frame-title-format "XX@%b"),其中XX为自己的电脑名,或自己可任意取名,甚至可以不需要。</p>
<h2>5. linux下为emacs定制c/c++ 和python代码的自动提示</h2>
<p>从 Emacs 24 开始,Emacs 对扩展包进行统一管理,极大程度上简化了扩展包的搜索与安装过程。打开 Emacs,然后执行 M-x list-packages ,如果网速足够快,应该很快就能看到 Emacs 官方提供的扩展包列表。</p>
<p>待软件包列表出现后,执行 C-s M-r ^ +company ,回车即可让光标跳到 company 扩展包信息所在的文本行,继而敲击 i 键(表示要安装该扩展包),再敲击 x 键(表示执行安装过程),稍等片刻,Emacs 会自动从下载 company 扩展包并安装至 $HOME/.emacs.d 目录。</p>
<p>提示:C-s 是开启 Emacs 查找模式, M-r 是将查找模式切换为正则表达式查找模式,而 C-s(ctrl +s) M-r(Alt + r) ^ + company 是用于匹配『位于行首且由多个空格与 company 字符串构成的字符串』的正则表达式。</p>
<p>如果以后要删除 company 扩展,步骤与安装步骤类似,只是将 i 键替换为 d 键(表示删除)。company 安装完毕后,在 Emacs 配置文件中做以下配置:</p>
<p>;; 代码补全<br>(add-hook 'c-mode-hook 'company-mode)<br>(add-hook 'c++-mode-hook 'company-mode)。</p>
<p>对于pcl的代码提示功能,只需要安装 company的关于C/C++的头文件即可.(目前是这样的)</p>
<p><strong>在linux下,若emacs启动较慢,可通过hostname查看自己主机的名字,然后打开/etc/hosts文件,将其中localhost改为自己主机名</strong></p>
<h2>6.最后贴一下自己的emacs配置文件</h2>
<pre><code>;; 关闭启动时的 “开机画面”,将缺省主模式设为 text-mode
(setq inhibit-startup-message t)
;; 语法高亮
(global-font-lock-mode t)
;; 以y/n代表yes/no
(fset 'yes-or-no-p 'y-or-n-p)
;; 显示括号匹配
(show-paren-mode t)
(setq show-paren-style 'parentheses)
(transient-mark-mode t)
;; 在标题栏提示你目前在什么位置
(setq frame-title-format "yxg@%b")
;;(setq frame-title-format '("%S" (buffer-file-name "%f" (dired-directory dired-directory "%b"))))
;; 默认显示100列就换行
(setq default-fill-column 100)
;; 去掉工具栏
;;(tool-bar-mode 0)
;; 去掉菜单栏
;;(menu-bar-mode 0)
(scroll-bar-mode 0)
(tooltip-mode 0)
;; 显示列号、行号
(setq column-number-mode t)
(setq line-number-mode t)
;; 回车缩进
(global-set-key "\C-m" 'newline-and-indent)
(global-set-key (kbd "C-<return>") 'newline)
;;(global-unset-key (kbd "C-SPC"))
;;(global-set-key (kbd "M-SPC") 'set-mark-command)
;;;; c mode ;;;;
(defvar xgp-cfsi-left-PAREN-old nil
"Command used to input \"(\"")
(make-variable-buffer-local 'xgp-cfsi-left-PAREN-old)
(defun xgp-cfsi-modify-alist (alist term new)
(let ((tl (assq term (symbol-value alist))))
(if tl
(setcdr tl new)
(add-to-list alist (cons term new)))))
(defun xgp-cfsi (style)
"Deciding whether using CFSI."
(interactive "Style: ")
(c-set-style style)
(setq indent-tabs-mode
nil
c-hanging-semi&comma-criteria
(quote (c-semi&comma-inside-parenlist)))
(xgp-cfsi-modify-alist 'c-hanging-braces-alist 'class-open nil)
(xgp-cfsi-modify-alist 'c-hanging-braces-alist 'class-close nil)
(local-set-key " " 'self-insert-command))
(defun xgp-cfsi-erase-blanks ()
"Erase all trivial blanks for CFSI."
(interactive)
(save-excursion
(goto-char (point-min))
(while (re-search-forward "[ \t]+$" nil t)
(replace-match "" nil nil))))
(defun linux-c-mode()
(define-key c-mode-map [return] 'newline-and-indent)
(setq imenu-sort-function 'imenu--sort-by-name)
(interactive)
(imenu-add-menubar-index)
(which-function-mode)
(c-toggle-auto-state)
(c-toggle-hungry-state)
(setq indent-tabs-mode nil)
(xgp-cfsi "linux"))
(add-hook 'c-mode-common-hook 'linux-c-mode)
;; MELPA 扩展包仓库
(when (>= emacs-major-version 24)
(require 'package)
(add-to-list
'package-archives
'("melpa" . "http://melpa.org/packages/")
t)
(package-initialize))
;; 代码补全
(add-hook 'c-mode-hook 'company-mode)
(add-hook 'c++-mode-hook 'company-mode)
;;(eval-after-load 'company
;; '(add-to-list 'company-backends 'company-irony))
;; 左侧行号显示
(add-to-list 'load-path "/usr/share/emacs/site-lisp")
(require 'linum)
(global-linum-mode t)
(custom-set-variables
;; custom-set-variables was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
'(column-number-mode t)
'(show-paren-mode t))
(custom-set-faces
;; custom-set-faces was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
'(default ((t (:family "Liberation Mono" :foundry "1ASC" :slant normal :weight normal :height 98 :width normal)))))
;;;;;;;;;;;;;;;;;;;;; for zero ;;;;;;;;;;;;;;;;;;;;;;;;;;
(add-hook 'c-mode-hook
(lambda ()
(font-lock-add-keywords nil
'(("\\(@ *[^@#
]+ *#\\) *[;]*[
]+" 1 font-lock-function-name-face t)
("^ *\\(@\\) *$" 1 font-lock-function-name-face t)))))
(defun zero-quantum ()
(interactive)
(insert "@ ")
(insert " #\n\n@")
(backward-char 5))
(define-prefix-command 'ctl-z-map)
(global-set-key (kbd "C-z") 'ctl-z-map)
(global-set-key (kbd "C-z c") 'c-mode)
(global-set-key (kbd "C-z t") 'tex-mode)
(global-set-key (kbd "C-z m") 'markdown-mode)
(global-set-key (kbd "C-z q") 'zero-quantum)
</code></pre>
python将指定点云文件(asc)转换为PCD格式
https://segmentfault.com/a/1190000007003165
2016-09-25T19:24:21+08:00
2016-09-25T19:24:21+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>起因</h2>
<p>由于自己大部分的点云文件都是<code>.asc</code>格式的,但最近用pcl做点云方面的研究,从<code>asc</code>文件到<code>pcd</code>文件手动转化太麻烦,而且效率较低,故此写一个不太成熟的python脚本实现从asc文件到pcd格式文件的转换。<br><strong><code>ps</code></strong>:<code>此脚本只适用于ASCII编码的文件,并且只适用于散乱点云</code></p>
<h2>着手写作</h2>
<p>分析pcd文件的格式可知,从asc到pcd转换最根本要求就是其文件开头符合pcd格式要求,其中最主要的问题是的是如何动态设置<code>WIDTH</code>和<code>POINTS</code>的值,对于散乱点云,这两个值都可以表示<code>点数</code>.点数的获得<code>可用asc文件的行数表示</code>.<br>代码如下:</p>
<pre><code>#coding:utf-8
import time
from sys import argv
script ,filename = argv
print ("the input file name is:%r." %filename)
start = time.time()
print ("open the file...")
file = open(filename,"r+")
count = 0
#统计源文件的点数
for line in file:
count=count+1
print ("size is %d" %count)
file.close()
#output = open("out.pcd","w+")
f_prefix = filename.split('.')[0]
output_filename = '{prefix}.pcd'.format(prefix=f_prefix)
output = open(output_filename,"w+")
list = ['# .PCD v.5 - Point Cloud Data file format\n','VERSION .5\n','FIELDS x y z\n','SIZE 4 4 4\n','TYPE F F F\n','COUNT 1 1 1\n']
output.writelines(list)
output.write('WIDTH ') #注意后边有空格
output.write(str(count))
output.write('\nHEIGHT')
output.write(str(1)) #强制类型转换,文件的输入只能是str格式
output.write('\nPOINTS ')
output.write(str(count))
output.write('\nDATA ascii\n')
file1 = open(filename,"r")
all = file1.read()
output.write(all)
output.close()
file1.close()
end = time.time()
print ("run time is: ", end-start)
</code></pre>
<h2>实例</h2>
<p>以20万左右的点云为例,该脚本运行时间大约在0.14s左右,基本可以满足自己的需求<br><img src="/img/bVDx0k?w=634&h=291" alt="转换前" title="转换前"></p>
<p><img src="/img/bVDx0o?w=632&h=422" alt="转换后" title="转换后"></p>
<p>运行以上脚本,便可自动将<code>example.asc</code>转化为<code>example.pcd</code></p>
C++之重载运算符
https://segmentfault.com/a/1190000006971869
2016-09-22T09:59:50+08:00
2016-09-22T09:59:50+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
1
<p>在C++随处可见<code>operator</code>重载运算,目前正处于学习C++中,谨以此记录。</p>
<h2>什么是函数重载</h2>
<p>函数重载是指<code>在同一作用域内</code>,可以有一组具有<code>相同函数名,不同参数列表</code>的函数,这样的函数称为重载函数。重载函数通常用来命名一些功能相似的函数,这样可减少函数命名的数量,避免命名空间的混乱不堪。</p>
<h2>函数重载的作用</h2>
<p>不言而喻, 不仅可以节省宝贵的命名空间,而且可以使得同一个函数实现多种不同功能。</p>
<h2>实例,对运算符重载</h2>
<p>对运算符的重载定义如下:<br><code>Box operator+(const Box &)</code>; 其中<code>Box</code>表示返回类型,<code>+</code>是要重载的运算符号,小括号内的为参数列表。</p>
<pre><code>#include <iostream>
using namespace std;
class complex {
private:
double real;
double image;
public:
complex():real(0.0),image(0.0) {};
complex(double a,double b):real(a),image(b){};
friend complex operator+(const complex &A,const complex &B);
friend complex operator-(const complex &A,const complex &B);
friend istream & operator>>(istream &in,complex &A);
friend ostream & operator<<(ostream &out,complex &A);
};
//重载加法运算符
complex operator+(const complex &A,const complex &B)
{
complex C;
C.real = A.real + B.real;
C.image = A.image + B.image;
return C;
}
//重载减法
complex operator-(const complex &A,const complex &B)
{
complex C;
C.real = A.real - B.real;
C.image = A.image - B.image;
return C;
}
//重载输入
istream &operator>>(istream &in,complex &A)
{
in>>A.real>>A.image;
return in;
}
ostream & operator << (ostream &out, complex &A) //如果没有重载,就不会输入复数的
{
out << A.real<<"+"<<A.image<<"i"<<endl;
return out;
}
int main ()
{
complex c1,c2,c3;
cin>>c1>>c2;
c3 = c1 + c2;
cout<<"c1 + c2="<<c3<<endl;
c3= c1 - c2;
cout<<"c1 -c2= "<<c3<<endl;
cout<<"c1 is:"<<c1<<endl;
return 0;
}
</code></pre>
<p>上面的重载运算符实现了复数的加减和输入输出功能。</p>
<p>再来看一个更好玩的(引自实验室“<code>扫地僧</code>”)</p>
<pre><code>#include <iostream>
using namespace std;
class boss {
public:
boss(const char *str) {info = str;}
boss(string s) {info = s;}
boss operator+(const boss &b) {
if (!info.compare("习大") && !b.info.compare("奥黑")) {
return boss("安倍好捉急!");
} else {
return boss(this->info + b.info);
}
}
public: //注意:这里为public成员,所以"operator<<"不必声明为类的友元函数
string info;
};
ostream & operator<<(ostream &os, const boss &b) {
os << b.info << endl;
return os;
}
int main(void)
{
boss xi_da("习大");
boss ao_hei("奥黑");
cout << xi_da + ao_hei << endl;
return 0;
}
</code></pre>
<p>如果运行上面的程序,它会输出<code>安倍好捉急</code>,是不是很有意思!</p>
<h2>总结</h2>
<p>函数重载或是运算符重载的出现,可以使得自己定制<code>个性化</code>的程序,并能使得同样函数名的程序实现不同的功能,总之,它的出现为人类进步做出了巨大贡献,感谢开发者!</p>
<p>一些相关链接:<br><a href="https://link.segmentfault.com/?enc=%2BG9vDDNqeelRRWfz5LNqBg%3D%3D.fkKMRNeTwwUuGMrRJps1MpY6fAxfyEi5NWpEgdQWGGhHXGSPOldQJfjSFVUGgGeqtZTZzFOzyuJgHa7UyA9YcQ%3D%3D" rel="nofollow">http://www.runoob.com/cpluspl...</a><br><a href="https://link.segmentfault.com/?enc=niHvg%2Bb8aJQf7DX%2FCsfJqg%3D%3D.r3NoNE%2Br%2Bv18NKwmYFdGa7kMa7laS9le7QWNSKzu2BmaVN%2BbYng0OclksuGuR9o%2FnP%2BBhUDrq2JJJ151iE9pPw%3D%3D" rel="nofollow">C语言中文网</a><br><a href="https://link.segmentfault.com/?enc=WBMyUrJbUbNMADO6a7eN6g%3D%3D.z84LncUTNtUtPoXYiWarrl4OprlpngJc5TGFxbTDznc827oPcEn0BA01MNob03SfFn%2BlJYDKkg5DQEKvKNQWLQ%3D%3D" rel="nofollow">吴秦的博客</a></p>
pcl可视化的那些事
https://segmentfault.com/a/1190000006685118
2016-08-22T23:22:38+08:00
2016-08-22T23:22:38+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
3
<h2><strong>可视化:一目了然</strong></h2>
<p>如题所示,可视化的重要性不必多说。在点云数据预处理中,要想知道点云的形状需要可视化; 要想了解精简/去噪/简化/压缩 的结果需要可视化; 配准中,对应点对的显示/对应点对的去除结果/配准变化的过程 需要可视化 ...</p>
<h2>pcl_viewer</h2>
<p>linux 下可直接在命令行输入 <code>pcl_viewr path/to/.pcd或.vtk</code>可直接显示pcl中的点云文件。</p>
<p><code>pcl_viewr</code>几个常用的命令:<br><code>r</code>键: 重现视角。如果读入文件没有在主窗口显示,不妨按下键盘的<code>r</code>键一试。<br><code>j</code>键:截图功能。<br><code>g</code>键:显示/隐藏 坐标轴。 <br><code>鼠标</code>:左键,使图像绕自身旋转; 滚轮, 按住滚轮不松,可移动图像,滚动滚轮,可放大/缩小 图像; 右键,<code>“原地” </code>放大/缩小。<br><code>-/+</code>:-(减号)可缩小点; +(加号),可放大点。<br><code>pcl_viewe -bc r,g,b /path/to/.pcd</code>:可改变背景色.<br><code>pcl_viewer</code>还可以用来直接显示pfh,fpfh(fast point feature histogram),vfh等直方图。<br>常用的<code>pcl_viewer</code> 好像就这些,其他未涉及到的功能可通过<code>pcl_viewer /path/.pcd </code>打开图像,按键盘<code>h</code>(获取帮助)的方式获得.</p>
<h2>程序中的可视化</h2>
<h3>简单可视化类</h3>
<p>所谓简单可视化类,是指直接在程序中使用,而且不支持多线程。<br>必须包含的头文件 <code>#include<pcl/visualization/cloud_viewer.h></code>,声明一个可视化类直接 <code>pcl::visualization::CloudViewer viewer ("test");</code> 即可,它的意思是说,我创建了一个CloudViewer的可视化类,这个可视化窗口的名字叫做<code>test</code>; 显示用<code>viewer.showCloud(cloud)</code> , 要想让自己所创窗口一直显示,则加上 <code>while (!viewer.wasStopped()){ };</code>即可, 或者直接 <code>viewr.spin(0);</code></p>
<h3>"复杂的"可视化类</h3>
<p>以一段程序为例:</p>
<pre><code> #include <pcl/visualization/pcl_visualizer.h> //包含基本可视化类
#include <pcl/visualization/pcl_visualizer.h>
//设置键盘交互函数,按下`space`键,某事发生
void keyboardEvent(const pcl::visualization::KeyboardEvent &event,void *nothing)
{
if(event.getKeySym() == "space" && event.keyDown())
next_iteration = true;
}
int main (int argc, char **argv)
{
1. 读入点云 source, target
2. 处理读入的数据文件
boost::shared_ptr<pcl::visualization::PCLVisualizer> view (new pcl::visualization::PCLVisualizer("test")); //创建可视化窗口,名字叫做`test`
view->setBackgroundColor(0.0,0,0); //设置背景色为黑色
viewer->addCoordinateSystem(1.0); //建立空间直角坐标系
// viewer->setCameraPosition(0,0,200); //设置坐标原点
viewer->initCameraParameters(); //初始化相机参数
***`*显示的”处理的数据文件“的具体内容*`***
view->registerKeyboardCallback(&keyboardEvent,(void*)NULL); //设置键盘回吊函数
while(!viewer->wasStopped())
{
viewer->spinOnce(100); //显示
boost::this_thread::sleep (boost::posix_time::microseconds (100000)); //随时间
}
}
</code></pre>
<p>在主程序<code>2</code>中,处理显示数据文件包含以下几种:<br> 一. 计算并显示法向量,具体在自己的笔记<a href="https://segmentfault.com/n/1330000005761876">pcl法向量的计算与显示</a><br> 二. 我的笔记<a href="https://segmentfault.com/n/1330000006645681">画线与显示</a>,可用于配准计算中对应点对的显示,不过用画线的办法<strong><em>很不好</em></strong>.<br> 三. 单纯的自定义的显示点云有如下常用函数:</p>
<ol>
<li><p>pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sources_cloud_color(source,250,0,0); //这句话的意思是:对输入为<code>pcl::PointXYZ</code>类型的点云,着色为红色。其中,source表示真正处理的点云,sources_cloud_color表示处理结果.</p></li>
<li><p>view->addPointCloud(source,sources_cloud_color,"sources_cloud_v1",v1); //将点云source,处理结果sources_cloud_color,添加到视图中,其中,双引号中的sources_cloud_v1,表示该点云的”标签“,我们依然可以称之为”名字“,之所以设置各个处理点云的名字,是为了在后续处理中易于区分; v1表是添加到哪个视图窗口(pcl中可设置多窗口模式)</p></li>
<li><p>view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,3,"sources_cloud_v1"); //设置点云属性. 其中<code>PCL_VISUALIZER_POINT_SIZE</code>表示设置点的大小为3,双引号中”sources_cloud_v1“,就是步骤<code>2</code>中所说的标签。</p></li>
<li><p>view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY,1,"sources_cloud_v1"); //主要用来设置<code>标签</code>点云的<strong><em>不透明度</em></strong>,表示对标签名字为"sources_cloud_v1"的标签点云设置不透明度为1,也就是说透明度为0. 默认情况下完全不透明。</p></li>
</ol>
<p>四. 显示配准中的对应点对关系.</p>
<p>要想显示点对之间的对应关系, 首先必须计算出对应点对, pcl中对应点对的计算可通过<code>pcl::registration::CorrespondenceEstimation<pcl::PointT,pcl::PointT> correspond_est;</code>计算,计算出对应点对后, source 和 target对应点的索引会存储在<code>vector<int> A</code>或<code>pcl::Correspondences A</code>中.要想显示点对的对应关系,只需<code> view->addCorrespondences<pcl::PointXYZ>(source,target,A,"correspond",v1);</code> 其中,<code>pcl::PointXYZ</code>表示所添加对应点对的类型为<code>PointXYZ</code>类型的,参数中的前两个表示目标点云和源点云,<code>A</code> 存储从目标点云到源点云的对应点的索引,”correspond“依然是自定义的标签,v1表示添加到哪个窗口. <br>为了使得对应点更加个性化,我们可以对它进行一下”定制“:</p>
<ol>
<li><p>view->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_LINE_WIDTH,2,"correspond"); //设置对应点连线的粗细.PCL_VISUALIZER_LINE_WIDTH,表示<code>线</code>操作,线段的宽度为2(提醒一下自己: 线段的宽度最好不要超过自定义的点的大小),"correspond"表示对 <strong>对应的标签</strong> 做处理.</p></li>
<li><p>view->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR,0,0,1,"correspond"); //设置对应点连线的颜色,范围从0-1之间。</p></li>
</ol>
<p>五. 多窗口及人机交互设置.<br>具体操作即设置请看<a href="https://segmentfault.com/a/1190000005930422">pcl之ICP实现</a><br>六. 如果用pcl的可视化类显示直方图,则可以这样做(以fpfh为例):</p>
<pre><code>#include <pcl/visualization/histogram_visualizer.h> //直方图的可视化
#include <boost/thread/thread.hpp>
#include <pcl/visualization/pcl_plotter.h>
int main (int argc, char **argv)
{
....
直方图计算
....
pcl::visualization::PCLHistogramVisualizer view;
view.setBackgroundColor(255,0,0);
view.addFeatureHistogram<pcl::FPFHSignature33> (*fpfhs,"fpfh",1000); //对下标为1000的元素可视化
//view.spinOnce(10000); //循环的次数
view.spin(); //无限循环
return 0;
}
</code></pre>
<p>也可以这样显示直方图不过需要在添加头文件<code>#include <pcl/visualization/pcl_plotter.h></code></p>
<pre><code> pcl::visualization::PCLPlotter plotter;
// We need to set the size of the descriptor beforehand.
plotter.addFeatureHistogram(*fpfhs, 300); //设置的很坐标长度,该值越大,则显示的越细致
plotter.plot();
</code></pre>
<p><img src="/img/bVCpg4?w=683&h=384" alt="图片描述" title="图片描述"></p>
k-means 之 C++ 的实现
https://segmentfault.com/a/1190000006041667
2016-07-22T12:06:07+08:00
2016-07-22T12:06:07+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>物以类聚,人以群分</h2>
<p>所谓k-means,即k均值聚类.聚类过程好比中国历史上的“春秋五霸,战国七雄”,它们同属与中国大地,同时被周王室分封。分封的过程就相当于K类的指定过程,每一个诸侯国都对应于一个聚类。五霸即五类,七雄即七类,从五霸到七雄,即相当于一个聚类生长的过程。 </p>
<p>用数学的语言来说就是,假设N个样点构成集合A,根据欧式距离需要将A划分为K个子集,则划分子集的过程就是k均值聚类实现的过程。</p>
<p>简而言之,物以类聚,人以群分,在数学中亦是如此。</p>
<h2>K均值是怎么实现的</h2>
<p>就像周王室分封诸侯,k均值聚类也需要被告知“到底要分多少诸侯”。有鉴于诸侯王们都不傻,都想要土地肥沃+物产丰绕+风调雨顺+。。。,所以周王室干脆一刀切“那就随机指定吧!”。于是,诸侯们到达封地后,为了得到更适合他们居住的地方,不断变换他们的国都,不断蚕食周围的群落,直到有一天,他们各自发现已经达到了自己理想国度--他们有无尽的子民,无数子民围绕在他们周边,他们有广阔的土地,他们就位于着土地中央! 最终,每个诸侯王不再迁都,定居过程也随之结束。</p>
<p>拿k均值来类比,总结以下几点:</p>
<ol>
<li><p>有多少诸侯要分封 -- k值</p></li>
<li><p>一开始怎么分 -- 随机</p></li>
<li><p>诸侯国迁徙 -- 距离</p></li>
<li><p>还要迁徙吗 -- 聚类最优</p></li>
<li><p>定居 -- 聚类结束</p></li>
</ol>
<h2>结构设计</h2>
<p>当然,要实现一个算法,其数据结构的设计是必不可少的!因为主要是针对三维数据的K均值计算,所以每一个样点需声明为一个结构体类型:</p>
<pre><code>typedef struct st_pointxyz
{
float x;
float y;
float z;
}st_pointxyz;
</code></pre>
<p>为了便于后续计算, 还需再设计一个结构,用于存贮某点和该点的索引号:</p>
<pre><code>typedef struct st_point
{
st_pointxyz pnt;
int groupID;
st_point()
{
}
st_point(st_pointxyz &p,int id)
{
pnt =p;
groupID= id;
}
}st_point;
</code></pre>
<p>既然是实现k均值算法,那就先定义一个<code>class KMeans</code>吧!</p>
<p>既然定义了class,就应该考虑其应该包含的具体实现函数了. 首先,聚类簇数K自不必说吧,定义<code>SetK()</code>。其次我想到的是应该包含输入输出,那就再构造一个成员输入函数:<code>SetInputCloud()</code> ,一个输出函数:<code>SaveFile()</code>。包含了输入输出,自然必须包含聚类过程的实现函数,就先定义为<code>Cluster()</code>吧!</p>
<p>接下来思考以下聚类过程是怎么实现的?哦,诸侯是被随机分封的,那我们就给它一个初始化随机函数InitKCenter(),接着,诸侯的不断迁移,就是聚类中心不断变化的过程,似乎也应该包含一个聚类中心更新的函数,那就定义为<code>UpdateGroupCenter()</code>,想起来了,他们聚类的过程是通过两点的欧式距离实现的,似乎<code>DisBetweenPoints()</code>也少不了,到这里似乎聚类过程还没有结束,我们必须再给定一个结束聚类计算的“终止函数”,就像诸侯王定居,国都不再改变,k均值聚类的中心不再变化即可认为聚类过程的结束,那就再定义一个判断中心点是否移动的函数<code>ExistCenterShift()</code>。</p>
<p>KMeans类的成员函数似乎都找齐了,但是成员变量还没说明。<code>int m_k</code>自不必说,接着再定义一个命令别名以便后用<code>typedef vector<st_point> VecPoint_t</code>(打算用<code>vector</code>存储数据),然后定义需要计算的输入点云<code>VecPoint_t mv_pntcloud</code>,还需要定义一个保存聚类结果的结构,定义为<code>vector<VecPoint_t>m_grp_pclcloud</code>,最后我们还要知道每类的聚类中心<code>vector<st_pointxyz> m_center</code>。 </p>
<p>到现在,k均值聚类整体结构已经有了,接下来就是将他们组合到一起(这里借助了pcl库,因为目前为止pcl中还没有K-means算法功能,ps:如果有谁能在pcl中找到k-means算法,请一定留言通知,不胜感激. 借助pcl只是为了省去三维点云读取与存贮的麻烦)</p>
<pre><code>class KMeans
{
public:
int m_k;
typedef vector<st_point> VecPoint_t; //定义命令别名
VecPoint_t mv_pntcloud; //要聚类的点云
vector<VecPoint_t>m_grp_pntcloud; //k类,每一类存储若干点
vector<st_pointxyz>mv_center; //每个类的中心
KMeans()
{
m_k =0;
}
inline void SetK(int k_) //设置聚类簇数
{
m_k = k_;
m_grp_pntcloud.resize(m_k);
}
//设置输入点云
bool SetInputCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr pPntCloud);
//初始化最初的k个类的中心
bool InitKCenter();
//聚类
bool Cluster();
//更新k类的中心(参数为类和中心点)
vector<st_pointxyz> UpdateGroupCenter(vector<VecPoint_t> &grp_pntcloud,vector<st_pointxyz> cer);
//计算两点欧式距离
double DistBetweenPoints(st_pointxyz &p1,st_pointxyz &p2);
//是否存在中心点转移动
bool ExistCenterShift(vector<st_pointxyz> &prev_center,vector<st_pointxyz> &cur_center);
//将聚类分别存储到各自的pcd文件中
bool SaveFile(const char *fname);
};
</code></pre>
<h2>具体实现</h2>
<p>首先设置一个判断聚类中心是否移动的阀值cosnt float DIST_NRAR = 0.001,也就是说当两次聚类中心的差值小于此值时,聚类则停止。</p>
<p>上代码:</p>
<pre><code>
bool KMeans::InitKCenter( )
{
mv_center.resize(m_k);
int size = mv_pntcloud.size();
srand(unsigned(time(NULL)));
for (int i =0; i< m_k;i++)
{
int seed = random()%(size+1);
mv_center[i].x = mv_pntcloud[seed].pnt.x;
mv_center[i].y = mv_pntcloud[seed].pnt.y;
mv_center[i].z = mv_pntcloud[seed].pnt.z;
}
return true;
}
bool KMeans::SetInputCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr pPntCloud)
{
size_t pntCount = (size_t) pPntCloud->points.size();
for (size_t i = 0; i< pntCount;++i)
{
st_point point;
point.pnt.x = pPntCloud->points[i].x;
point.pnt.y = pPntCloud->points[i].y;
point.pnt.z = pPntCloud->points[i].z;
point.groupID = 0;
mv_pntcloud.push_back(point);
}
return true;
}
bool KMeans::Cluster()
{
InitKCenter();
vector<st_pointxyz>v_center(mv_center.size());
size_t pntCount = mv_pntcloud.size();
do
{
for (size_t i = 0;i < pntCount;++i)
{
double min_dist = DBL_MAX;
int pnt_grp = 0; //聚类群组索引号
for (size_t j =0;j <m_k;++j)
{
double dist = DistBetweenPoints(mv_pntcloud[i].pnt, mv_center[j]);
if (min_dist - dist > 0.000001)
{
min_dist = dist;
pnt_grp = j;
}
}
m_grp_pntcloud[pnt_grp].push_back(st_point(mv_pntcloud[i].pnt,pnt_grp)); //将该点和该点群组的索引存入聚类中
}
//保存上一次迭代的中心点
for (size_t i = 0; i<mv_center.size();++i)
{
v_center[i] = mv_center[i];
}
mv_center=UpdateGroupCenter(m_grp_pntcloud,mv_center);
if ( !ExistCenterShift(v_center, mv_center))
{
break;
}
for (size_t i = 0; i < m_k; ++i){
m_grp_pntcloud[i].clear();
}
}while(true);
return true;
}
double KMeans::DistBetweenPoints(st_pointxyz &p1, st_pointxyz &p2)
{
double dist = 0;
double x_diff = 0, y_diff = 0, z_diff = 0;
x_diff = p1.x - p2.x;
y_diff = p1.y - p2.y;
z_diff = p1.z - p2.z;
dist = sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);
return dist;
}
vector<st_pointxyz> KMeans::UpdateGroupCenter(std::vector<VecPoint_t> &grp_pntcloud, std::vector<st_pointxyz> center)
{
for (size_t i = 0; i < m_k; ++i)
{
float x = 0, y = 0, z = 0;
size_t pnt_num_in_grp = grp_pntcloud[i].size();
for (size_t j = 0; j < pnt_num_in_grp; ++j)
{
x += grp_pntcloud[i][j].pnt.x;
y += grp_pntcloud[i][j].pnt.y;
z += grp_pntcloud[i][j].pnt.z;
}
x /= pnt_num_in_grp;
y /= pnt_num_in_grp;
z /= pnt_num_in_grp;
center[i].x = x;
center[i].y = y;
center[i].z = z;
}
return center;
}
//是否存在中心点移动
bool KMeans::ExistCenterShift(std::vector<st_pointxyz> &prev_center, std::vector<st_pointxyz> &cur_center)
{
for (size_t i = 0; i < m_k; ++i)
{
double dist = DistBetweenPoints(prev_center[i], cur_center[i]);
if (dist > DIST_NEAR_ZERO)
{
return true;
}
}
return false;
}
//将聚类的点分别存到各自的pcd文件中
bool KMeans::SaveFile(const char *prex_name)
{
for (int i = 0; i < m_k; ++i)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr p_pnt_cloud(new pcl::PointCloud<pcl::PointXYZ> ());
for (size_t j = 0, grp_pnt_count = m_grp_pntcloud[i].size(); j < grp_pnt_count; ++j)
{
pcl::PointXYZ pt;
pt.x = m_grp_pntcloud[i][j].pnt.x;
pt.y = m_grp_pntcloud[i][j].pnt.y;
pt.z = m_grp_pntcloud[i][j].pnt.z;
p_pnt_cloud->points.push_back(pt);
}
p_pnt_cloud->width = (int)m_grp_pntcloud[i].size();
p_pnt_cloud->height = 1;
char newFileName[256] = {0};
char indexStr[16] = {0};
strcat(newFileName, szFileName);
strcat(newFileName, "-");
strcat(newFileName, prex_name);
strcat(newFileName, "-");
sprintf(indexStr, "%d", i + 1);
strcat(newFileName, indexStr);
strcat(newFileName, ".pcd");
pcl::io::savePCDFileASCII(newFileName, *p_pnt_cloud);
}
return true;
}
</code></pre>
<h2>实例检测</h2>
<p><img src="/img/bVzvRf" alt="k = 2" title="k = 2"></p>
<p><img src="/img/bVzvRE" alt="k = 5" title="k = 5"></p>
PCL 之 ICP 算法实现
https://segmentfault.com/a/1190000005930422
2016-07-11T11:36:59+08:00
2016-07-11T11:36:59+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
4
<h2>ICP是什么?</h2>
<p>ICP(Iterative Closest Point),即最近点迭代算法,是最为经典的数据配准算法。其特征在于,通过求取源点云和目标点云之间的对应点对,基于对应点对构造旋转平移矩阵,并利用所求矩阵,将源点云变换到目标点云的坐标系下,估计变换后源点云与目标点云的误差函数,若误差函数值大于阀值,则迭代进行上述运算直到满足给定的误差要求.</p>
<p>ICP算法采用最小二乘估计计算变换矩阵,原理简单且具有较好的精度,但是由于采用了迭代计算,导致算法计算速度较慢,而且采用ICP进行配准计算时,其对待配准点云的初始位置有一定要求,若所选初始位置不合理,则会导致算法陷入局部最优。</p>
<h2>PCL+ICP</h2>
<p>PCL点云库已经实现了多种点云配准算法,结合pcl,本次配准的主要目的是:</p>
<ol>
<li>对PCL中ICP算法进行一些注解</li>
<li>创建可视化窗口,通过设置键盘回调函数,控制迭代过程,观察ICP算法的计算过程</li>
</ol>
<p>PCL中ICP的官方参考文档 <a href="https://link.segmentfault.com/?enc=TtYWu%2BGAuP3Qf5q8m9cC3w%3D%3D.XM8PV5GuEKhYB5kO%2FtHq9ENqry%2BvtNQ0e2m5Zxn2ywZDqA%2FqLntAfS%2FhiBLlLyIWYFYKywSx08iK7xMkabC%2B1ekwzEfHQY5PQgNlC6MSz0pp%2BrHoDBCDZoScqH8Ge4uQ" rel="nofollow">http://pointclouds.org/docume...</a></p>
<h2>具体代码实现</h2>
<p>首先看一下pcl中ICP的主要代码:</p>
<pre><code>pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp; //创建ICP的实例类
icp.setInputSource(cloud_sources);
icp.setInputTarget(cloud_target);
icp.setMaxCorrespondenceDistance(100);
icp.setTransformationEpsilon(1e-10);
icp.setEuclideanFitnessEpsilon(0.001);
icp.setMaximumIterations(100);
icc.align(final);
</code></pre>
<p>需要说明的是:</p>
<p>其一:PCL中的ICP算法是基于SVD(Singular Value Decomposition)实现的.</p>
<p>其二:使用pcl的ICP之前要set几个参数:</p>
<pre><code>1. setMaximumIterations, 最大迭代次数,icp是一个迭代的方法,最多迭代这些次(若结合可视化并逐次显示,可将次数设置为1);
2. setEuclideanFitnessEpsilon, 设置收敛条件是均方误差和小于阈值, 停止迭代;
3. setTransformtionEpsilon, 设置两次变化矩阵之间的差值(一般设置为1e-10即可);
4. setMaxCorrespondenaceDistance,设置对应点对之间的最大距离(此值对配准结果影响较大)。
</code></pre>
<p>如果仅仅运行上述代码,并设置合理的的预估计参数,便可实现利用ICP对点云数据进行配准计算.</p>
<p>其次为了更深入的了解ICP的计算过程,即本试验的第二个目的,继续添加以下代码:</p>
<pre><code>boost::shared_ptr<pcl::visualization::PCLVisualizer> view(new pcl::visualization::PCLVisualizer("icp test")); //定义窗口共享指针
int v1 ; //定义两个窗口v1,v2,窗口v1用来显示初始位置,v2用以显示配准过程
int v2 ;
view->createViewPort(0.0,0.0,0.5,1.0,v1); //四个窗口参数分别对应x_min,y_min,x_max.y_max.
view->createViewPort(0.5,0.0,1.0,1.0,v2);
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sources_cloud_color(cloud_in,250,0,0); //设置源点云的颜色为红色
view->addPointCloud(cloud_in,sources_cloud_color,"sources_cloud_v1",v1);
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> target_cloud_color (cloud_target,0,250,0); //目标点云为绿色
view->addPointCloud(cloud_target,target_cloud_color,"target_cloud_v1",v1); //将点云添加到v1窗口
view->setBackgroundColor(0.0,0.05,0.05,v1); //设着两个窗口的背景色
view->setBackgroundColor(0.05,0.05,0.05,v2);
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"sources_cloud_v1"); //设置显示点的大小
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"target_cloud_v1");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>aligend_cloud_color(Final,255,255,255); //设置配准结果为白色
view->addPointCloud(Final,aligend_cloud_color,"aligend_cloud_v2",v2);
view->addPointCloud(cloud_target,target_cloud_color,"target_cloud_v2",v2);
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"aligend_cloud_v2");
view->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE,2,"target_cloud_v2");
view->registerKeyboardCallback(&keyboardEvent,(void*)NULL); //设置键盘回调函数
int iterations = 0; //迭代次数
while(!view->wasStopped())
{
view->spinOnce(); //运行视图
if (next_iteration)
{
icp.align(*Final); //icp计算
cout <<"has conveged:"<<icp.hasConverged()<<"score:"<<icp.getFitnessScore()<<endl;
cout<<"matrix:\n"<<icp.getFinalTransformation()<<endl;
cout<<"iteration = "<<++iterations;
/*... 如果icp.hasConverged=1,则说明本次配准成功,icp.getFinalTransformation()可输出变换矩阵 ...*/
if (iterations == 1000) //设置最大迭代次数
return 0;
view->updatePointCloud(Final,aligend_cloud_color,"aligend_cloud_v2");
}
next_iteration = false; //本次迭代结束,等待触发
}
</code></pre>
<p>最后还需要设置以下键盘回调函数,用以控制迭代进程:</p>
<pre><code>bool next_iteration = false;
//设置键盘交互函数
void keyboardEvent(const pcl::visualization::KeyboardEvent &event,void *nothing)
{
if(event.getKeySym() == "space" && event.keyDown())
next_iteration = true;
}
/*... 上述函数表示当键盘空格键按下时,才可执行ICP计算 ... */
</code></pre>
<h2>实例练习</h2>
<p>将上述代码组合,添加相应头文件:</p>
<pre><code>#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/registration/icp.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <boost/thread/thread.hpp>
#include <pcl/console/parse.h> //pcl控制台解析
</code></pre>
<p>根据不同模型设置合适的参数,便可实现最初目的。实例展示:</p>
<p><img src="/img/bVy2P8?w=683&h=384" alt="初始位置" title="初始位置"></p>
<p><img src="/img/bVy2Qu?w=683&h=384" alt="配准过程" title="配准过程"></p>
<p><img src="/img/bVy2QC?w=683&h=384" alt="最终配准结果" title="最终配准结果"></p>
<h2>ICP之变种</h2>
<p>ICP有很多变种,有point-to-point的,也有point-to-plane的,一般后者的计算速度快一些,是基于法向量的,需要输入数据有较好的法向量,具体使用时建议根据自己的需要及可用的输入数据选择具体方法。</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=pLSF1wLycpiz%2B18GzzzB3A%3D%3D.wUEokqoO8OFwKl3haU0uJsBDdithvsX9r%2BtMPYcOJAmyaIA78CL7Dyqv90Tj4G0nQrNutN%2F5Chvvate%2B3ovDSj7%2Fhbt8nWS8kfHYaWtLxWjjzvfoeD%2BKA17%2F%2F0KX46fG" rel="nofollow">pcl::GeneralizedIterativeClosestPoint< PointSource, PointTarget > Class Template Reference</a></li>
<li><a href="https://link.segmentfault.com/?enc=D13XcAVbMUBWAk0iuQr7xQ%3D%3D.bdurBIU79chxar35iKRcspF0mFiV4tlKRE0Sn4jQK27p%2BjTonmlVF4UC2GWGLVbq48aIjY0UaICUeSNdwXObIW9IBdTQsFjyeuc%2FVgEMnP8%3D" rel="nofollow">pcl::IterativeClosestPoint< PointSource, PointTarget, Scalar > Class Template Reference</a></li>
<li><a href="https://link.segmentfault.com/?enc=j63V19vTu3TGOIMoBt7BTw%3D%3D.pg4SvJiHntI2SxFwP6bhw%2B0CRVwLnPZYTqcS62buLarBe0PpUuocimQwvc6E2CAJALlx3eWq%2B4O4wNffvj6%2BUWZ3xx7cg8bO8h3bDPbzy7KHLI0xpn%2B8Vh91xnDKnCfC" rel="nofollow">pcl::IterativeClosestPointWithNormals< PointSource, PointTarget, Scalar > Class Template Reference</a></li>
<li><a href="https://link.segmentfault.com/?enc=M5Yi1RZas%2FF0wLwLuAUs%2Fw%3D%3D.lvByIGYESqMjLvIc%2Bay6rekAU%2Fz9afRUKP6stdRdYxwqsTW%2BJEUOtdObKrbRCVhUObyB24Fn%2B%2F5b23GMhV%2F7RNA%2FPZ9UouAC%2BgE0xz41MRI0%2FKOJ%2B2DeIUmIHEnd3FWq" rel="nofollow">pcl::IterativeClosestPointNonLinear< PointSource, PointTarget, Scalar > Class Template Reference</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2FitdACSMfbO2E%2BcUUcKpdw%3D%3D.fCe3UksUPXPIPOu%2FsnsWJ4oLsrhpv2Waz2sKdbkRGgF88Mq%2F36434ev8fATNAdIhxGtCMcLYAPYz3MqecynfibR2H7dgVjoKveImZqOq2jTVCyZhpYzuEe0uh%2F%2BGI1Xb" rel="nofollow">pcl::JointIterativeClosestPoint< PointSource, PointTarget, Scalar > Class Template Reference</a></li>
<li><a href="https://link.segmentfault.com/?enc=KEqV3qjWHR2DpEE%2FBELKyQ%3D%3D.0vyADMAVZgKdw8YW6syDdigC3WYfCFjO%2B%2BlfXGBLrU8gZHkMZBSkZh45bARJSCUmRCGTfsXvCHKa7aZr5iPljAu%2BBWg9%2Far69i1GgNBUXU0fyhD5YQkI3O29H6mLqRq5" rel="nofollow">pcl::registration::IncrementalICP< PointT, Scalar > Class Template Reference</a></li>
</ul>
Linux下 PCL源码安装
https://segmentfault.com/a/1190000005908404
2016-07-08T10:45:34+08:00
2016-07-08T10:45:34+08:00
SimpleTriangle
https://segmentfault.com/u/yueyingyunfan
0
<h2>不得不知的PCL</h2>
<p>所谓PCL(Point Cloud Library)其实就是一个开源的c++代码库,它实现了大量点云相关的通用算法和高效的数据管理结构,不仅涉及逆向工程领域,其还在模式识别,机器人视觉,计算机图形学,虚拟现实等众多领域大显神威。</p>
<p>基于以下第三方库:Boost,Eigen,FlANN,VTK,OpenNI,QHull,实现了点云获取,滤波,分割,精简,配准,特征提取,追踪,曲面重建和可视化等功能。</p>
<h2>迈开“艰难”的第一步</h2>
<p>所谓“艰难”的第一步,其实说白了就是IDE的选择问题,只是因为自己对Linux和window都是略知一二。在选择Linux或win搭建开发平台时犯了所谓的“选择恐惧症”的问题。先说window,因为自己以前曾在window+vs2010下配置opengl开发环境,在配置opengl时,各种依赖各种手动链接然后各种错误,不胜其烦,也因为在win下各种编程的不适,让我果断选择了在linux下搭建开发平台。</p>
<p>既然选择了linux,那末接下来便是理所当然的事情--获取PCL源码包。PCL源码包可以在其官方网站获取,当获取源码包之后,顺理成章的便是将其解压并在解压出的文件新建一个空工程文档(我的习惯是<code>mkdir build && cd build</code>,以下便用build代表自己所建的工程文档吧),进入build之后,便可执行 <code>cmake..</code>,利用cmake编译源码包,用以生成和自己电脑环境相匹配的开发环境。当然,如果你只想用PCL的某个单一的功能,可在build文件中执行<code>ccmake..</code>,这时候,便会出现一个选择窗口,只对你想安装的函数选择编译即可。</p>
<p>在<code>cmake</code>阶段,<code>cmake</code>会根据CmakeLists.txt 文件生列出所需要安装的依赖库(boost,qhull,eigen等),查看<code>cmake</code>的输出,按提示安装所缺少的库即可,最终生成所需要的MakeFile文件。<br>既然现在已经生成了和自己电脑环境匹配的PCL环境,下一步自然是是依据你cmake生成的编译环境,建立PCL的链接库,这样必须用make,即 make (耗时较长)。</p>
<p>环境配置好了,PCL的链接库也已经生成,最后一步就是将我们生成的各种库文件安装在我们电脑中,<code>sudo make install</code> 可帮你完成你的意愿。 </p>
<p>建议:</p>
<ol>
<li><p>此源码安装不针对任何Linux系统,gentoo,ubantu等都适用。</p></li>
<li><p>若打算源码安装,在安装之前最好先更新一下系统,这样基本能保证所安装的包为最新包。</p></li>
</ol>
<p>其实罗嗦了这麽一大堆,归结起来就以下步骤句话:</p>
<ol>
<li><p>下载获取PCL源文件解压,并 <code>cd pcl</code></p></li>
<li><p><code>mkdir build && cd build</code></p></li>
<li><p><code>cmake ..</code> ,按照提示安装所需依赖包</p></li>
<li><p><code>make</code></p></li>
<li><p><code>sudo make install</code></p></li>
</ol>
<h2>把“浮云”抛到身后</h2>
<p>暂时先把PCL的命名规范,目录命名,结构体,类,变量名等等以后必不可少,但现在还用不到的东西统统称为“浮云”,既然归于浮云,那就先让它随风而逝吧。</p>
<p>相信必定有或多或少的“同志”和我一样,在完美构建完PCL开发平台之后,第一件事并不是拿着《PCL点云学习教程》或是在PCL官方网站上开始从头学习,而是先找个官网实例练验证一下再说。好吧,我们就是最简单的输入输出来说。<br>首先,必须有供输入的文件,PCL建议的文件格式为".pcd",可从官网上下载任意“.pcd”文件即可。 </p>
<p>既然想体验PCL,好吧,直接把输入输出代码copy过来把:</p>
<pre><code> #include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include<pcl/visualization/cloud_viewer.h>
int
main (int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
if (pcl::io::loadPCDFile<pcl::PointXYZ> (argv[1], *cloud) == -1)
{
PCL_ERROR ("Couldn't read file test_pcd.pcd \n");
return (-1);
}
std::cout << "Loaded "
<< cloud->width * cloud->height
<< " data points from test_pcd.pcd with the following fields: "
<< std::endl;
#if 0
for (size_t i = 0; i < cloud->points.size (); ++i)
std::cout << " " << cloud->points[i].x
<< " " << cloud->points[i].y
<< " " << cloud->points[i].z << std::endl;
#endif
pcl::visualization::CloudViewer viewer("test");
viewer.showCloud(cloud);
while (!viewer.wasStopped()){ }
return (0);
}</code></pre>
<p>当上面这段小程序运行后,如果能够可视化出你的<code>.pcd</code>文件,可以断定你的PCL开发平台已经搭建成功。 </p>
<p>如果是用<code>cmake</code>构建项目文件的话,添加 <code>CMakeLists.txt 文件</code>,内容如下:</p>
<pre><code>cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
project(proj)
find_package(PCL 1.2 REQUIRED)
include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})
add_executable (test test.cpp)
target_link_libraries (test ${PCL_LIBRARIES})</code></pre>
<p>上述文件是说:cmake最低版本为2.8, 我的工程名字叫做 <code>proj</code>,pcl最低版本要求为1.2,然后链接进所有<code>pcl的文件目录和库文件</code>,生成的可执行文件名字叫做 <code>test</code>,我的主程序文件叫做 test.cpp。</p>
<p>按自己的习惯,可将上述<code>cmake</code>文件和<code>test.cpp</code>文件放在同一目录之下(或不同目录,即源码与中间文件分离的原则),执行<code>cmake</code>,如果没有什么错误,再执行<code>make</code>,则会在源文件目录之下生成<code>test</code>可执行文件。</p>
<p>我想稍有一点编程经验的,应该都可以看懂。从上边代码中,是否可以发现一些PCL的特色呢? </p>
<p>下面贴出我的几个练习实例。 </p>
<p>1)体素网格精简</p>
<p><img src="/img/bVyXbK?w=683&h=384" alt="图片描述" title="图片描述"></p>
<p>2)法向量可视化</p>
<p><img src="/img/bVyXb7?w=683&h=472" alt="图片描述" title="图片描述"></p>
<p>3)曲面重建</p>
<p><img src="/img/bVyXcI?w=1301&h=744" alt="图片描述" title="图片描述"></p>