第七讲 添加回环检测

2016.11 更新

  • 把原文的SIFT替换成了ORB,这样你可以在没有nonfree模块下使用本程序了。
  • 回环检测的阈值作出了相应的调整。
  • 请以现在的github上源码为准。

简单回环检测的流程

  上一讲中,我们介绍了图优化软件g2o的使用。本讲,我们将实现一个简单的回环检测程序,利用g2o提升slam轨迹与地图的质量。本讲结束后,读者朋友们将得到一个完整的slam程序,可以跑通我们在百度云上给出的数据了。所以呢,本讲也将成为我们“一起做”系列的终点啦。

  小萝卜:这么快就要结束了吗师兄?

  师兄:嗯,因为我想要说的都教给大家了嘛。不过,尽管如此,这个教程的程序还是比较初步的,可以进一步进行效率、鲁棒性方面的优化,这就要靠大家后续的努力啦。同时我的暑假也将近结束,要开始新一轮的工作了呢。

  好的,话不多说,先来讲讲,上一讲的程序离完整的slam还有哪些距离。主要说来有两点:

  1. 关键帧的提取。把每一帧都拼到地图是去是不明智的。因为帧与帧之间距离很近,导致地图需要频繁更新,浪费时间与空间。所以,我们希望,当机器人的运动超过一定间隔,就增加一个“关键帧”。最后只需把关键帧拼到地图里就行了。
  2. 回环的检测。回环的本质是识别曾经到过的地方。最简单的回环检测策略,就是把新来的关键帧与之前所有的关键帧进行比较,不过这样会导致越往后,需要比较的帧越多。所以,稍微快速一点的方法是在过去的帧里随机挑选一些,与之进行比较。更进一步的,也可以用图像处理/模式识别的方法计算图像间的相似性,对相似的图像进行检测。

  把这两者合在一起,就得到了我们slam程序的基本流程。以下为伪码:

  1. 初始化关键帧序列:$F$,并将第一帧$f_0$放入$F$。
  2. 对于新来的一帧$I$,计算$F$中最后一帧与$I$的运动,并估计该运动的大小$e$。有以下几种可能性:
    • 若$e>E_{error}$,说明运动太大,可能是计算错误,丢弃该帧;
    • 若没有匹配上(match太少),说明该帧图像质量不高,丢弃;
    • 若$e<E_{key}$,说明离前一个关键帧很近,同样丢弃;
    • 剩下的情况,只有是特征匹配成功,运动估计正确,同时又离上一个关键帧有一定距离,则把$I$作为新的关键帧,进入回环检测程序:
  3. 近距离回环:匹配$I$与$F$末尾$m$个关键帧。匹配成功时,在图里增加一条边。
  4. 随机回环:随机在$F$里取$n$个帧,与$I$进行匹配。若匹配上,在图里增加一条边。
  5. 将$I$放入$F$末尾。若有新的数据,则回2; 若无,则进行优化与地图拼接。

  小萝卜:slam流程都是这样的吗?

  师兄:大体上如此,也可以作一些更改。例如在线跑的话呢,可以定时进行一次优化与拼图。或者,在成功检测到回环时,同时检测这两个帧附近的帧,那样得到的边就更多啦。再有呢,如果要做实用的程序,还要考虑机器人如何运动,如果跟丢了怎么进行恢复等一些实际的问题呢。  


实现代码

  代码依旧是在上一讲的代码上进行更改得来的。由于是完整的程序,稍微有些长,请大家慢慢看:

  src/slam.cpp

  1. /*************************************************************************
  2. > File Name: rgbd-slam-tutorial-gx/part V/src/visualOdometry.cpp
  3. > Author: xiang gao
  4. > Mail: gaoxiang12@mails.tsinghua.edu.cn
  5. > Created Time: 2015年08月15日 星期六 15时35分42秒
  6. * add g2o slam end to visual odometry
  7. * add keyframe and simple loop closure
  8. ************************************************************************/
  9.  
  10. #include <iostream>
  11. #include <fstream>
  12. #include <sstream>
  13. using namespace std;
  14.  
  15. #include "slamBase.h"
  16.  
  17. #include <pcl/filters/voxel_grid.h>
  18. #include <pcl/filters/passthrough.h>
  19.  
  20. #include <g2o/types/slam3d/types_slam3d.h>
  21. #include <g2o/core/sparse_optimizer.h>
  22. #include <g2o/core/block_solver.h>
  23. #include <g2o/core/factory.h>
  24. #include <g2o/core/optimization_algorithm_factory.h>
  25. #include <g2o/core/optimization_algorithm_gauss_newton.h>
  26. #include <g2o/solvers/csparse/linear_solver_csparse.h>
  27. #include <g2o/core/robust_kernel.h>
  28. #include <g2o/core/robust_kernel_factory.h>
  29. #include <g2o/core/optimization_algorithm_levenberg.h>
  30.  
  31. // 把g2o的定义放到前面
  32. typedef g2o::BlockSolver_6_3 SlamBlockSolver;
  33. typedef g2o::LinearSolverCSparse< SlamBlockSolver::PoseMatrixType > SlamLinearSolver;
  34.  
  35. // 给定index,读取一帧数据
  36. FRAME readFrame( int index, ParameterReader& pd );
  37. // 估计一个运动的大小
  38. double normofTransform( cv::Mat rvec, cv::Mat tvec );
  39.  
  40. // 检测两个帧,结果定义
  41. enum CHECK_RESULT {NOT_MATCHED=, TOO_FAR_AWAY, TOO_CLOSE, KEYFRAME};
  42. // 函数声明
  43. CHECK_RESULT checkKeyframes( FRAME& f1, FRAME& f2, g2o::SparseOptimizer& opti, bool is_loops=false );
  44. // 检测近距离的回环
  45. void checkNearbyLoops( vector<FRAME>& frames, FRAME& currFrame, g2o::SparseOptimizer& opti );
  46. // 随机检测回环
  47. void checkRandomLoops( vector<FRAME>& frames, FRAME& currFrame, g2o::SparseOptimizer& opti );
  48.  
  49. int main( int argc, char** argv )
  50. {
  51. // 前面部分和vo是一样的
  52. ParameterReader pd;
  53. int startIndex = atoi( pd.getData( "start_index" ).c_str() );
  54. int endIndex = atoi( pd.getData( "end_index" ).c_str() );
  55.  
  56. // 所有的关键帧都放在了这里
  57. vector< FRAME > keyframes;
  58. // initialize
  59. cout<<"Initializing ..."<<endl;
  60. int currIndex = startIndex; // 当前索引为currIndex
  61. FRAME currFrame = readFrame( currIndex, pd ); // 当前帧数据
  62.  
  63. string detector = pd.getData( "detector" );
  64. string descriptor = pd.getData( "descriptor" );
  65. CAMERA_INTRINSIC_PARAMETERS camera = getDefaultCamera();
  66. computeKeyPointsAndDesp( currFrame, detector, descriptor );
  67. PointCloud::Ptr cloud = image2PointCloud( currFrame.rgb, currFrame.depth, camera );
  68.  
  69. /*******************************
  70. // 新增:有关g2o的初始化
  71. *******************************/
  72. // 初始化求解器
  73. SlamLinearSolver* linearSolver = new SlamLinearSolver();
  74. linearSolver->setBlockOrdering( false );
  75. SlamBlockSolver* blockSolver = new SlamBlockSolver( linearSolver );
  76. g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg( blockSolver );
  77.  
  78. g2o::SparseOptimizer globalOptimizer; // 最后用的就是这个东东
  79. globalOptimizer.setAlgorithm( solver );
  80. // 不要输出调试信息
  81. globalOptimizer.setVerbose( false );
  82.  
  83. // 向globalOptimizer增加第一个顶点
  84. g2o::VertexSE3* v = new g2o::VertexSE3();
  85. v->setId( currIndex );
  86. v->setEstimate( Eigen::Isometry3d::Identity() ); //估计为单位矩阵
  87. v->setFixed( true ); //第一个顶点固定,不用优化
  88. globalOptimizer.addVertex( v );
  89.  
  90. keyframes.push_back( currFrame );
  91.  
  92. double keyframe_threshold = atof( pd.getData("keyframe_threshold").c_str() );
  93.  
  94. bool check_loop_closure = pd.getData("check_loop_closure")==string("yes");
  95. for ( currIndex=startIndex+; currIndex<endIndex; currIndex++ )
  96. {
  97. cout<<"Reading files "<<currIndex<<endl;
  98. FRAME currFrame = readFrame( currIndex,pd ); // 读取currFrame
  99. computeKeyPointsAndDesp( currFrame, detector, descriptor ); //提取特征
  100. CHECK_RESULT result = checkKeyframes( keyframes.back(), currFrame, globalOptimizer ); //匹配该帧与keyframes里最后一帧
  101. switch (result) // 根据匹配结果不同采取不同策略
  102. {
  103. case NOT_MATCHED:
  104. //没匹配上,直接跳过
  105. cout<<RED"Not enough inliers."<<endl;
  106. break;
  107. case TOO_FAR_AWAY:
  108. // 太近了,也直接跳
  109. cout<<RED"Too far away, may be an error."<<endl;
  110. break;
  111. case TOO_CLOSE:
  112. // 太远了,可能出错了
  113. cout<<RESET"Too close, not a keyframe"<<endl;
  114. break;
  115. case KEYFRAME:
  116. cout<<GREEN"This is a new keyframe"<<endl;
  117. // 不远不近,刚好
  118. /**
  119. * This is important!!
  120. * This is important!!
  121. * This is important!!
  122. * (very important so I've said three times!)
  123. */
  124. // 检测回环
  125. if (check_loop_closure)
  126. {
  127. checkNearbyLoops( keyframes, currFrame, globalOptimizer );
  128. checkRandomLoops( keyframes, currFrame, globalOptimizer );
  129. }
  130. keyframes.push_back( currFrame );
  131. break;
  132. default:
  133. break;
  134. }
  135.  
  136. }
  137.  
  138. // 优化
  139. cout<<RESET"optimizing pose graph, vertices: "<<globalOptimizer.vertices().size()<<endl;
  140. globalOptimizer.save("./data/result_before.g2o");
  141. globalOptimizer.initializeOptimization();
  142. globalOptimizer.optimize( ); //可以指定优化步数
  143. globalOptimizer.save( "./data/result_after.g2o" );
  144. cout<<"Optimization done."<<endl;
  145.  
  146. // 拼接点云地图
  147. cout<<"saving the point cloud map..."<<endl;
  148. PointCloud::Ptr output ( new PointCloud() ); //全局地图
  149. PointCloud::Ptr tmp ( new PointCloud() );
  150.  
  151. pcl::VoxelGrid<PointT> voxel; // 网格滤波器,调整地图分辨率
  152. pcl::PassThrough<PointT> pass; // z方向区间滤波器,由于rgbd相机的有效深度区间有限,把太远的去掉
  153. pass.setFilterFieldName("z");
  154. pass.setFilterLimits( 0.0, 4.0 ); //4m以上就不要了
  155.  
  156. double gridsize = atof( pd.getData( "voxel_grid" ).c_str() ); //分辨图可以在parameters.txt里调
  157. voxel.setLeafSize( gridsize, gridsize, gridsize );
  158.  
  159. for (size_t i=; i<keyframes.size(); i++)
  160. {
  161. // 从g2o里取出一帧
  162. g2o::VertexSE3* vertex = dynamic_cast<g2o::VertexSE3*>(globalOptimizer.vertex( keyframes[i].frameID ));
  163. Eigen::Isometry3d pose = vertex->estimate(); //该帧优化后的位姿
  164. PointCloud::Ptr newCloud = image2PointCloud( keyframes[i].rgb, keyframes[i].depth, camera ); //转成点云
  165. // 以下是滤波
  166. voxel.setInputCloud( newCloud );
  167. voxel.filter( *tmp );
  168. pass.setInputCloud( tmp );
  169. pass.filter( *newCloud );
  170. // 把点云变换后加入全局地图中
  171. pcl::transformPointCloud( *newCloud, *tmp, pose.matrix() );
  172. *output += *tmp;
  173. tmp->clear();
  174. newCloud->clear();
  175. }
  176.  
  177. voxel.setInputCloud( output );
  178. voxel.filter( *tmp );
  179. //存储
  180. pcl::io::savePCDFile( "./data/result.pcd", *tmp );
  181.  
  182. cout<<"Final map is saved."<<endl;
  183. globalOptimizer.clear();
  184.  
  185. return ;
  186. }
  187.  
  188. FRAME readFrame( int index, ParameterReader& pd )
  189. {
  190. FRAME f;
  191. string rgbDir = pd.getData("rgb_dir");
  192. string depthDir = pd.getData("depth_dir");
  193.  
  194. string rgbExt = pd.getData("rgb_extension");
  195. string depthExt = pd.getData("depth_extension");
  196.  
  197. stringstream ss;
  198. ss<<rgbDir<<index<<rgbExt;
  199. string filename;
  200. ss>>filename;
  201. f.rgb = cv::imread( filename );
  202.  
  203. ss.clear();
  204. filename.clear();
  205. ss<<depthDir<<index<<depthExt;
  206. ss>>filename;
  207.  
  208. f.depth = cv::imread( filename, - );
  209. f.frameID = index;
  210. return f;
  211. }
  212.  
  213. double normofTransform( cv::Mat rvec, cv::Mat tvec )
  214. {
  215. return fabs(min(cv::norm(rvec), *M_PI-cv::norm(rvec)))+ fabs(cv::norm(tvec));
  216. }
  217.  
  218. CHECK_RESULT checkKeyframes( FRAME& f1, FRAME& f2, g2o::SparseOptimizer& opti, bool is_loops)
  219. {
  220. static ParameterReader pd;
  221. static int min_inliers = atoi( pd.getData("min_inliers").c_str() );
  222. static double max_norm = atof( pd.getData("max_norm").c_str() );
  223. static double keyframe_threshold = atof( pd.getData("keyframe_threshold").c_str() );
  224. static double max_norm_lp = atof( pd.getData("max_norm_lp").c_str() );
  225. static CAMERA_INTRINSIC_PARAMETERS camera = getDefaultCamera();
  226. static g2o::RobustKernel* robustKernel = g2o::RobustKernelFactory::instance()->construct( "Cauchy" );
  227. // 比较f1 和 f2
  228. RESULT_OF_PNP result = estimateMotion( f1, f2, camera );
  229. if ( result.inliers < min_inliers ) //inliers不够,放弃该帧
  230. return NOT_MATCHED;
  231. // 计算运动范围是否太大
  232. double norm = normofTransform(result.rvec, result.tvec);
  233. if ( is_loops == false )
  234. {
  235. if ( norm >= max_norm )
  236. return TOO_FAR_AWAY; // too far away, may be error
  237. }
  238. else
  239. {
  240. if ( norm >= max_norm_lp)
  241. return TOO_FAR_AWAY;
  242. }
  243.  
  244. if ( norm <= keyframe_threshold )
  245. return TOO_CLOSE; // too adjacent frame
  246. // 向g2o中增加这个顶点与上一帧联系的边
  247. // 顶点部分
  248. // 顶点只需设定id即可
  249. if (is_loops == false)
  250. {
  251. g2o::VertexSE3 *v = new g2o::VertexSE3();
  252. v->setId( f2.frameID );
  253. v->setEstimate( Eigen::Isometry3d::Identity() );
  254. opti.addVertex(v);
  255. }
  256. // 边部分
  257. g2o::EdgeSE3* edge = new g2o::EdgeSE3();
  258. // 连接此边的两个顶点id
  259. edge->vertices() [] = opti.vertex( f1.frameID );
  260. edge->vertices() [] = opti.vertex( f2.frameID );
  261. edge->setRobustKernel( robustKernel );
  262. // 信息矩阵
  263. Eigen::Matrix<double, , > information = Eigen::Matrix< double, , >::Identity();
  264. // 信息矩阵是协方差矩阵的逆,表示我们对边的精度的预先估计
  265. // 因为pose为6D的,信息矩阵是6*6的阵,假设位置和角度的估计精度均为0.1且互相独立
  266. // 那么协方差则为对角为0.01的矩阵,信息阵则为100的矩阵
  267. information(,) = information(,) = information(,) = ;
  268. information(,) = information(,) = information(,) = ;
  269. // 也可以将角度设大一些,表示对角度的估计更加准确
  270. edge->setInformation( information );
  271. // 边的估计即是pnp求解之结果
  272. Eigen::Isometry3d T = cvMat2Eigen( result.rvec, result.tvec );
  273. edge->setMeasurement( T.inverse() );
  274. // 将此边加入图中
  275. opti.addEdge(edge);
  276. return KEYFRAME;
  277. }
  278.  
  279. void checkNearbyLoops( vector<FRAME>& frames, FRAME& currFrame, g2o::SparseOptimizer& opti )
  280. {
  281. static ParameterReader pd;
  282. static int nearby_loops = atoi( pd.getData("nearby_loops").c_str() );
  283.  
  284. // 就是把currFrame和 frames里末尾几个测一遍
  285. if ( frames.size() <= nearby_loops )
  286. {
  287. // no enough keyframes, check everyone
  288. for (size_t i=; i<frames.size(); i++)
  289. {
  290. checkKeyframes( frames[i], currFrame, opti, true );
  291. }
  292. }
  293. else
  294. {
  295. // check the nearest ones
  296. for (size_t i = frames.size()-nearby_loops; i<frames.size(); i++)
  297. {
  298. checkKeyframes( frames[i], currFrame, opti, true );
  299. }
  300. }
  301. }
  302.  
  303. void checkRandomLoops( vector<FRAME>& frames, FRAME& currFrame, g2o::SparseOptimizer& opti )
  304. {
  305. static ParameterReader pd;
  306. static int random_loops = atoi( pd.getData("random_loops").c_str() );
  307. srand( (unsigned int) time(NULL) );
  308. // 随机取一些帧进行检测
  309.  
  310. if ( frames.size() <= random_loops )
  311. {
  312. // no enough keyframes, check everyone
  313. for (size_t i=; i<frames.size(); i++)
  314. {
  315. checkKeyframes( frames[i], currFrame, opti, true );
  316. }
  317. }
  318. else
  319. {
  320. // randomly check loops
  321. for (int i=; i<random_loops; i++)
  322. {
  323. int index = rand()%frames.size();
  324. checkKeyframes( frames[index], currFrame, opti, true );
  325. }
  326. }
  327. }

  几点注解:

  1. 回环检测是很怕"false positive"的,即“将实际上不同的地方当成了同一处”,这会导致地图出现明显的不一致。所以,在使用g2o时,要在边里添加"robust kernel",保证一两个错误的边不会影响整体结果。
  2. 我在slambase.h里添加了一些彩色输出代码。运行此程序时,出现绿色信息则是添加新的关键帧,红色为出错。

  parameters.txt里定义了检测回环的一些参数:

  1. #part 7
  2. keyframe_threshold=0.1
  3. max_norm_lp=5.0
  4. # Loop closure
  5. check_loop_closure=yes
  6. nearby_loops=5
  7. random_loops=5

  其中,nearby_loops就是$m$,random_loops就是$n$啦。这两个数如果设大一些,匹配的帧就会多,不过太大了就会影响整体速度了呢。


回环检测的效果

  对代码进行编译,然后bin/slam即可看到程序运行啦。

  添加了回环检测之后呢,g2o文件就不会像上次那样孤零零的啦,看起来是这样子的:

  

  怎么样?是不是感觉整条轨迹“如丝般顺滑”了呢?它不再是上一讲那样一根筋通到底,而是有很多帧间的匹配数据,保证了一两帧出错能被其他匹配数据给“拉回来”。

  百度云上的数据最后拼出来是这样的哦(780帧,关键帧62张,帧率5Hz左右):

  咖啡台左侧有明显的人通过的痕迹,导致地图上出现了他的身影(帅哥你好拉风):

  嗯,这个就可以算作是基本的地图啦。至此,slam的两大目标:“轨迹”和“地图”我们都已得到了,可以算是基本上解决了这个问题了。


一些后话

  这一个“一起做rgb-d slam”系列,前前后后花了我一个多月的时间。写代码,调代码,然后写成博文。虽然讲的比较啰嗦,总体上希望能对各位slam爱好者、研究者有帮助啦!这样我既然辛苦也很开心的!

  写作期间,得到了女朋友大脸莲的不少帮助,也得到了读者和同行之间的鼓励,谢谢各位啦!等有工夫,我会把这一堆东西整理成一个pdf供slam初学者进行研究学习的。

  slam仍是一个开放的问题,尽管有人曾说“在slam领域发文章越来越难”,然而现在机器人几大期刊和会议(IJRR/TRO/RAM/JFD/ICRA/IROS...)仍有不少slam方面的文章。虽然我们“获取轨迹与地图”的目标已基本实现,但仍有许多工作等我们去做,包括:

  • 更好的数学模型(新的滤波器/图优化理论);
  • 新的视觉特征/不使用特征的直接方法;
  • 动态物体/人的处理;
  • 地图描述/点云地图优化/语义地图
  • 长时间/大规模/自动化slam
  • 等等……

  总之,大家千万别以为“slam问题已经有标准答案啦”。等我对slam有新的理解时,也会写新的博客为大家介绍的!

  如果你觉得我的博客有帮助,可以进行几块钱的小额赞助,帮助我把博客写得更好。(虽然我也是从别处学来的这招……)

  

  本讲代码:https://github.com/gaoxiang12/rgbd-slam-tutorial-gx/tree/master/part%20VII

  数据:http://yun.baidu.com/s/1i33uvw5

  交流群:374238181

一起做RGB-D SLAM(7) (完结篇)的更多相关文章

  1. 解剖SQLSERVER 完结篇 关于Internals Viewer源代码

    解剖SQLSERVER 完结篇 关于Internals Viewer源代码 大家可能都用过Internals Viewer这个软件 <查看SQLSERVER内部数据页面的小插件Internals ...

  2. (视频)《快速创建网站》 4.2 完结篇 – 应用运营vs.发射卫星,遥测(Telemetry) 技术

    本文是<快速创建网站>系列的第10篇(完结篇),如果你还没有看过之前的内容,建议你点击以下目录中的章节先阅读其他内容再回到本文.访问本系列目录,请点击:http://devopshub.c ...

  3. 不就是抽个血吗,至于么-jQuery,Linux完结篇

    hi 趁着周一去抽血化验,真开心...下午报告才出来,不过早上来了就开始各种晕菜,叫错名字,说错话.....至于么.. 还有在教研室的30天就可以肥家了,凯森凯森.今天不想干活(哪天想干过我就问问), ...

  4. 《BackboneJS框架的技巧及模式》(4)完结篇

    <BackboneJS框架的技巧及模式>(4)完结篇 本文紧接第二部分:<BackboneJS框架的技巧及模式(3)> 作者:chszs,转载需注明.博客主页:http://b ...

  5. 实现app上对csdn的文章查看,以及文章中图片的保存 (制作csdn app 完结篇)

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24022165 今天给大家带来CSDN的完结篇,即增加文章的查看和文章中图片的保存 ...

  6. (视频)《高速创建站点》 4.2 完结篇 – 应用运营vs.发射卫星,遥測(Telemetry) 技术

    本文是<高速创建站点>系列的第10篇(完结篇),假设你还没有看过之前的内容,建议你点击下面文件夹中的章节先阅读其它内容再回到本文.訪问本系列文件夹.请点击:http://anb.io/bl ...

  7. IL代码完结篇

    读懂IL代码就这么简单(三)完结篇   一 前言 写了两篇关于IL指令相关的文章,分别把值类型与引用类型在 堆与栈上的操作区别详细的写了一遍这第三篇也是最后一篇,之所以到第三篇就结束了,是因为以我现在 ...

  8. 短信发送接口被恶意访问的网络攻击事件(四)完结篇--搭建WAF清理战场

    前言 短信发送接口被恶意访问的网络攻击事件(一)紧张的遭遇战险胜 短信发送接口被恶意访问的网络攻击事件(二)肉搏战-阻止恶意请求 短信发送接口被恶意访问的网络攻击事件(三)定位恶意IP的日志分析脚本 ...

  9. Java工程师学习指南 完结篇

    Java工程师学习指南 完结篇 先声明一点,文章里面不会详细到每一步怎么操作,只会提供大致的思路和方向,给大家以启发,如果真的要一步一步指导操作的话,那至少需要一本书的厚度啦. 因为笔者还只是一名在校 ...

  10. Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息...)

    每篇一句 没有任何技术方案会是一种银弹,任何东西都是有利弊的 相关阅读 [小家Java]深入了解数据校验:Java Bean Validation 2.0(JSR303.JSR349.JSR380)H ...

随机推荐

  1. python 中datetime 和 string 转换

    dt = datetime.datetime.strptime(string_date, fmt) fmt 的格式说明如下: https://docs.python.org/2/library/dat ...

  2. 全面剖析Smarty缓存机制一[三种缓存方式]

    今天主要全面总结下Smarty模板引擎中强大的缓存机制,缓存机制有效减少了系统对服务器的压力,而这也是很多开发者喜欢Smarty的原因之一,由于篇幅较大,便于博友阅读,这篇文章将剖析Smarty缓存的 ...

  3. Linux常用命令总结--不断补充

    首先介绍一个很有用的命令:history  查看linux机器上历史命令. 在Linux下查看内存我们一般用free命令:free -m 查看硬盘状况:df -h 查看cpu信息:less /proc ...

  4. QQ空间点赞代码

    jQuery("a.qz_like_btn_v3[data-clicklog='like']").each(function(index,item){ console.log(it ...

  5. 容器平台选型的十大模式:Docker、DC/OS、K8S谁与当先?

    首先我们来谈什么情况下应该使用Docker的问题   如图,左面是经常挂在嘴边的所谓容器的优势,但是虚拟机都能一一怼回去. 如果部署的是一个传统的应用,这个应用启动速度慢,进程数量少,基本不更新,那么 ...

  6. 锐捷 rg-S2026f 学习笔记

    1.通过串口连接交换机: http://support.ruijie.com.cn/forum.php?mod=viewthread&tid=32250&extra=page%3D1& ...

  7. 【linux】centos6.5上bugzilla的搭建

    1.安装依赖包 CentOS 6.5默认安装了apche,perl ,需要安装httpd mod_ssl mysql-server mysql php-mysql gcc perl* mod_perl ...

  8. 智能家居入门DIY——【四、组合】

    前面几篇介绍了一些传感器和代码,这篇介绍一下把它们组合起来.之所以单独列出这部分,原因在于组合更多功能的时候发现使用软串口库驱动ESP8266时由于内存过小导致发送失败甚至整个系统无法工作的情况.所以 ...

  9. selenium笔记2017

    1,from time import sleep(先引入关键词) sleep(5)       (就可以使用这个命令了) 可以停止页面5秒 1-1. 等待页面元素出现的时间(即没出现时,等待元素出现) ...

  10. Java separatorChar 如何在Java里面添加 \

    Java手册 separatorChar public static final char separatorChar 与系统有关的默认名称分隔符.此字段被初始化为包含系统属性 file.separa ...