python深度度学习库mxnet开发环境的搭建

前言

这篇文章主要是记录如何搭建一个能够运行深度学习库mxnet的python环境,需要说明的是,我是在阿里云云服务器的远程主机上安装的,系统为Ubuntu1604,不过后面我也在我自己的Win10的本地主机上安装了一次,由于conda本身是夸平台的所以安装的流程都差不多。

搭建环境

1. 安装minicoda

为了保持python环境的纯净性,我们将为mxnet专门创建一个新的python环境,使用conda来管理系统中多环境,所以安装miniconda和anaconda都是可以的。鉴于我的python环境是会重新安装的,所以我这里安装更加小巧的miniconda。

终端中执行如下命令下载miniconda,当然如果有图形界面用浏览器下载也是一样的(唯一需要注意的是选择你系统对应的版本和机器的位宽,我是Ubuntu系统 64bit的机器,自然是选择Linux 64bit版本):

1
wget -c http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh

上面是miniconda官方的下载地址,国内速度比蜗牛还慢,简直不能忍,于是切换为国内清华提供的下载源:

1
wget -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86_64.sh

下载完整后安装操作就简单了:

1
2
chmod +x Miniconda-latest-Linux-x86_64.sh
./Miniconda-latest-Linux-x86_64.sh

2. 安装mxnet的python环境

nxnet作为一个深度学习框架,是需要不少其他python库支持的,当前最重要的就是Numpy了。而conda是提供复制别人的环境的,所以从下载官方向导文件中提供的环境配置文件:

1
git clone https://github.com/mli/gluon-tuto rials-zh

当然这命令的前提是,装好了git,git安装简单就不讲了。在下载的文件夹中的environment.yml就是我们所需要的文件,使用conda复制创建一个mxnet的python环境:

1
conda env create -f environment.yml

等待一些列库的安装完成就可以了,在environment.yml文件中,将环境名命名为gluon,所以后面需要使用gluon进入该环境,关于用conda来切换管理多python环境我之间有总结过,这里就不讲了。

3. 为jupyter notebook配置其远程服务

jupyter notebook是官方推荐的开发环境,在复制创建的gluon中已经安装好了,由于我是在远程主机上安装的环境,所以我现在需要为jupyter notebook配置远程服务,操作如下:

首先,生成jupyter notebook的配置文件jupyter_notebook_config.py:

1
jupyter notebook --generate-config

然后打开jupyter_notebook_config.py:

1
vim ~/.jupyter/jupyter_notebook_config.py

修改配置如下:

1
2
3
c.NotebookApp.ip='*'
c.NotebookApp.open_browser = False
c.NotebookApp.port =8888 #随便指定一个没有被占用的端口

修改完成后保存,打开jupyter notebook,root用户打开需要加以下选项:

1
jupyter notebook --allow-root

至此jupyter notebook远程服务器部分操作完成,我们可以从本地机的浏览器中输入登陆远程服务器的jupyter的地址就可以登陆:IP:PORT。

不过很多时候由于防火墙的阻挡这样是不能正常连接jupyter的,对此最好的解决方法是在本地机上建立ssh通道访问jupyter,如果本地机是Linux系统那么只需要一条命令即可:

1
ssh -L8888:lcalhost:4562 远程主机登陆用户@远程主机IP

这样就可以将远程的8888jupyter端口映射到本地的4562端口上来,在浏览器中输入:localhost:4562,即可连接到远程的jupyter notebook。

对于本地机是Windows的用户,可能就要使用工具来建立ssh端口映射了,我是用的putty,设置Connection->SSH->Tunnels,添加从本地端口到远端端口的通道,然后通过SSH登录到服务器,截图如下:

putty设置

最后在浏览器中设置代理,建议最好用火狐,可以设置仅限于火狐自身的局部代理,而不会影响全局的上网功能,截图如下(不过我后面发现,其实是不需要设置浏览器代理的):

firefox设置

4. 配置jupyter notebook的markdown文件读写功能

使用过jupyter notebook的人都知道,jupyter notebook的默认工作文件格式为.ipynb,我们希望将其切换为markdown格式(因为mxnet的gluon向导全部使用md文件),所以需要安装notedown插件:

1
pip install notedown

说实话,notedown这个插件,不是很好装,我在gluon环境中没有安装上,退出了gluon环境后用pip装好的,很奇怪。安装好以后,再次修改配置文件jupyter_notebook_config.py如下:

1
c.NotebookApp.contents_manager_class = 'notedown.NotedownContentsManager'

5. 导入mxnet出错

我进入gluon环境,在python代码中导入mxnet,如果出现以下错入:

1
libgfortran.so.3: cannot open shared object file: No such file or directory

说明系统中缺少libgfortran库,直接使用apt-get安装即可:

32 bit系统

1
sudo apt-get install libgfortran

64 bit系统

1
sudo apt-get install gfortran-multilib

测试端口是否打开

可能在连接远程jupyter notebook服务时会遇到其他一些问题,检查网络端口的情况会给调试很大的帮助,所以这里再补充说明下测试网络端口的问题,在Linux下测试网络端口的方法很多,我这里使用netcat(简称 nc)工具来检查。首先安装(以Ubuntu为例):

sudo apt-get install netcat

测试主机端口是否打开命令如下:

nc -zv [IP] [port]

上面的命令中,这些标志是:

  • z 设置 nc 只是扫描侦听守护进程,实际上不向它们发送任何数据。
  • v 启用详细模式
Compartir

10月学习小结

这是写在前言前的前言:1个月前写的这篇总结,到现在来看已经没什么看头了,很多概念上的混淆、分类上面的混乱,而且还浅显还没什么干货。不过也证明了我这一个多月来确实进步了不少,得自我炫耀一下。只是这篇文章我本来的打算就是一篇入门性的各种概念的梳理,所以我也就修改一下明显的错误,而不作整体上的调整了。

前言

本来是打算初步学习一下SLAM的,结果没想到还没敲开SLAM大厦的大门,就被城堡外迷乱的丛密的绕晕,回头看看最近这段时间走过的迷路:从OpenCV到CMake,从图像特征到各种局部特征点算法,最后更是回头复习了一把线性代数和概率论。走着走着甚至连我自己都忘了我最初的目的只是SLAM,不由的感慨自己的基础知识实在是太差了。不过讲起来在自学新知识上,我也不是初来乍到的菜鸟,以前数次迷失在路上的经历,让我不再纠结于路边那些撤人心魄的未知的细节,不忘初心、坚持到底才是最重要的,所以这一次我很幸运的穿过了这迷雾丛丛的丛林。

稍稍卖弄了一下文艺,只是想说明,基础知识对现在我新知识学习的重要性。经过了这一路转下来,虽然对很多东西仍然是一知半解,但是也总算是对各种知识的网络关联有了一些了解,对我来说这些才是最重要的。今后再次遇到,我知道能知道它们的意义、与其他知识的相关性的联系和在实际应用中的作用。所以这里我就对前面这段时间的学习稍作总结一下(其实就是一个流水账)。

总结

从SLAM到图像匹配

总的来说,学了一大圈,但是实际的核心其他还是各种图像处理算法和应用。对于视觉SLAM,其前端本身就是各种图像处理的算法,不过在如今的研究中后端的优化才是关键,但是我对优化了解不多,就不提。以图像为主的视觉SLAM前端,大概有如下主要内容:相机的模型、空间坐标描叙和转化、图像匹配。

由于我手中没有相机,于是我就从图像匹配开始研究,在SLAM中图像的匹配一般都是通过特征点来比较的,所以第一步就是要找特征点以及特征点的描叙子。有了特征后,对比就很简单了,所以匹配的关键就是图像点特征的提取。

从图像特征到图像特征点

单说图像特征的时候我是茫然的,因为能够作为图像特征的方法太多了,各种方法各种用途,为什么SLAM中用的是图像的特征点呢?

这是由于SLAM以及其他的一些应用更关注动态的物体在图像中的表现,而局部特征就比较适合这种情况。在局部特征中,图像的特征点是当前使用最多的一类的局部特征。

各种特征点算法

虽然我将目标集中在图像的特征点上,但实际上各种不同的特征点算法也是层出不穷,对象我这样的初学者十分有必要将这些理一下。

基本上所有的特征点介绍资料都是从Harris角点开始讲解的,为什么呢?我个人觉得Harris角点的定义从图像上来,它更能够符合的我们对角点的日常认知。Harris特征点算法算是一种很经典的算法,但是他的缺点也是很明显:计算量大、参数不好配置。

相比起来FAST特征点算法,则是会更加简单易行的方法,正是由于其简单性快速,一些其他的算法就在它的基础改进而来的。

Harris和FAST算法我自认为还能看懂,但是进入到sift、surf算法我就力不从心了。这两个算法除了原理上一堆看不懂的数学推导让我印象深刻外,它们能够去除图片尺度的影响也让我感觉到很神奇。正是应为sift、surf对旋转、尺度、明亮都有很好的稳定性,所以它们在现在得到广泛的应用,从目前来看,这两个算法最大的问题可能就是计算量太大了。

部分数学基础

每种算法总会有严谨的数学推导做支撑,所以一旦要探明其原理,那相关数学知识就不可避免了,这里我就大致概括一下需要使用到的一些数学概念:矩阵、线性方程、自相关函数、卷积、矩阵的特征值和特征向量、行列式、方差与协方差等等,至于微积分中的一些知识我就不提了。

可以看到以上提到的大部分都是线性代数中的概念,所以线性代数真是很有用。

OpenCV、cmake——从理论到实践

对于前面提到的图像特征点检测与匹配的算法,即使在已经很熟悉的基础上,我想要以代码的形式实现也不是件容易的事情,不过好在我们有已经实现好的开源库——OpenCV。

在OpenCV中,常见的特征点查找、匹配都有现成的借口函数,而通过OpenCV的GUI模块有可以直观显示特征点的位置与图像点匹配的链接结果。而cmake则是项目编译管理的好工具,而实际上OpenCV的源码包,就使用cmake来管理的。

Compartir

CMake初步使用

CMake是什么

官方说明:

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice.

简单来说就是,CMake一个能够跨平台、跨编译器的编译和管理里的项目的工具集。在我目前使用过程中,最直观的感受是:通过执行cmake,它能够根据比较简单的配置文件,自动生成各种makefile或者工程的文件,而不必自己写复杂的makefile文件了。

CMake的简单使用

1. 生成CMakeLists.txt

要使用CMake首先就是需要写一个CMakeLists.txt文件了,CMakeLists.txt文件是CMake管理工程的配置文件。你对工程的所有的管理和配置都需要在CMakeLists.txt文件申明。

CMakeLists.txt就像一个简单的脚本语言一样,它也有自己的语法。它有if条件控制语句,也有foreach循环控制语句,还有 NOT、AND、OR关系运算语句,输出信息可以用message。在CMakeLists.txt中是对大小写不敏感的,你可以用大写,也可以用小写,也可以混写。具体语言使用参考官方文档:https://cmake.org/documentation/

当有CMakeLists.txt后,就可以按照以下命令执行cmake和编译的操作了:

1
2
cmake ..
make
2. 编译个一个典型的工程

在工程目录下,分文件夹来管理不同类型的文件,目录结构如下:

工程目录结构

那么我们可以将如下声明写入CMakeLists.txt中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CMAKE_MINIMUM_REQUIRED( VERSION 2.8 ) #设定版本
PROJECT( slam ) #设定工程名
SET( CMAKE_CXX_COMPILER "g++") #设定编译器

#设定可执行二进制文件的目录
SET( EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

#设定存放编译出来的库文件的目录
SET( LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
#并且把该目录设为连接目录
LINK_DIRECTORIES( ${PROJECT_SOURCE_DIR}/lib)

#设定头文件目录
INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/include)

#增加子文件夹,也就是进入源代码文件夹继续构建
ADD_SUBDIRECTORY( ${PROJECT_SOURCE_DIR}/src)

以上每一条声明都有注释有说明,当CMake读到最后ADD_SUBDIRECTORY时会,指定的进入/src子目录中,这时候我们需要在这个子目录中也要添加一个CMakeLists.txt提供CMake继续执行。

假定/src目录下只有一个main.cpp的源文件,在其中的CMakeList.txt中就可以只需指定编译的源文件即可:

1
2
# 增加一个可执行的二进制
ADD_EXECUTABLE( main main.cpp )
3. 使用选项进行条件控制

使用option的声明一个选项,如下所示:

1
2
3
4
5
6
7
option (USE_MYMATH "Use tutorial provided math implementation" ON)

if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

option选项会显示在CMake的GUI,并且其默认值为ON。需要执行cmake-gui代替cmake(需要安装cmake-qt-gui)。当用户选择了之后,这个值会被保存在CACHE中,这样就不需要每次CMAKE都进行更改了。后面用if语句检查USE_MYMATH选项开决定是否执行下面的声明语句。

4. 添加第三方库的支持

以添加OpenCV和PCL库为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 增加PCL库的依赖
FIND_PACKAGE( PCL REQUIRED COMPONENTS common io )

# 增加opencv的依赖
FIND_PACKAGE( OpenCV REQUIRED )

# 添加头文件和库文件
ADD_DEFINITIONS( ${PCL_DEFINITIONS} )
INCLUDE_DIRECTORIES( ${PCL_INCLUDE_DIRS} )
LINK_LIBRARIES( ${PCL_LIBRARY_DIRS} )

TARGET_LINK_LIBRARIES( generate_pointcloud ${OpenCV_LIBS}
${PCL_LIBRARIES} )

添加库最常用的命令就是find_package命令了,它可以被用来在系统中自动查找配置构建工程所需的程序库。在linux和unix类系统下这个命令尤其有用。CMake自带的模块文件里有大半是对各种常见开源库的find_package支持,支持库的种类非常多。REQUIRED表示如果没有找到,cmake会停止处理,并报告一个错误。最后使用TARGET_LINK_LIBRARIES高速程序需要连接的库。

Compartir

视觉SLAM介绍

前言

还记得在上家公司时,我就常常看到身边的同事拿着机器人小车在公司的走廊上跑来跑去,有时运行得很顺利,有时候却总是撞上墙壁,那是我就感到这个东西很好玩,什么时候自己也能玩一下。最近终于有机会接触到机器人的视觉问题,于是开始查询相关资料,而视觉SLAM作为机器人视觉定位和导航主流的研究方向,也是我最主要关注的方面。鉴于我自己对这方面的知识并没有一个系统的认识,所以我把我最近找到的资料做一些整理,试图梳理一下相关的知识之间的联系。

SLAM是什么?SLAM技术的对于机器人的重要性

SLAM的全称是:Simultaneous Localization and Mapping,中文翻译为即时定位与地图构建。需要理解的是,SLAM并非单一的算法而是更像是一中概念,是各种用于定位和建图的算法的集合。

视觉SLAM是通过机器视觉的方法来处理定位和建图的问题,但是SLAM并不只有视觉SLAM一种,它也可以通过其他方法来处理其相关核心问题。只是由于视觉图像携带信息的丰富性,各种视觉传感器摄像头的发展,以及各种计算机处理视觉图像方法的成熟,让使用机器视觉来处理SLAM问题,成为了当前主流SLAM研究方向。

对一个需要实现自由移动的机器人而言,它首先要解决的最基础的问题就是对它所处环境的了解,而SLAM技术可以分别从两个方面,即:机器人自身的位置和周围环境的情况,给予机器人对于自身和所处环境的:位置和地图这两个关键信息,所以对这类机器人而言,SLAM技术是它实现自由移动的关键之一。

SLAM的应用方向

  1. 自动驾驶
  2. 无人机自动返航于蔽障
  3. 工业运输机器人
  4. VR和AR设备

关键设备

在SLAM中最重要的设备就是其感知周围信息的传感器了,传感器类型的选择对SLAM的实现方式和难易程度都密切相关。当前SLAM使用的传感器主要分一下两类:

  1. 激光雷达

激光雷达的作用是提供机器人本体与周围环境障碍物的距离信息,实物如下图所示:

激光雷达

优点:

  • 扫描精度高
  • 速度快
  • 计算量相对较小
  • 研究历史久技术比较成熟

缺点:

  • 价格昂贵
  • 不易表示回环和线性误差严重
  1. 视觉摄像头

视觉SLAM采用的是各种类型的摄像头作为传感器的,当前主要的摄像头类型有以下三类:单目双目RGBD。对于以上三类,SLAM实现的难度为:单目视觉 > 双目视觉 > RGBD。

单目相机简称MonoSLAM,主要优点是成本低、装置简单,但是缺陷也是很明显:缺乏深度信息,只能购过其他手段来估测,这使得SLAM实现其他会比较困难。

双目相机通过两个摄像头拍摄物体的时差来获取深度信息,因此对比单目相机它获取深度信息相对来说会容易一些,但是双目相机的配置与标定都较为复杂,对计算量要求较高。

RGBD相机是普通的摄像头加上深度摄像头组成,他最大的优点是它可以直接获得深度图像信息,而不需要通过其他手段计算后才能获得,所以其实现SLAM是三者中最简单的一个,但是由于这种相机出现较晚,目前还存在测量范围窄、噪声大、视野小等诸多问题。

视觉SLAM的框架

针对于视觉SLAM,一般都有一个基本的框架:数据采集、VO、后端、建图、回环检测

SLAM框架

相关学习资料链接

  1. OpenSLAM:https://openslam.org/
  2. Andrew Davison的课程: http://www.doc.ic.ac.uk/~ajd/Robotics/index.html
  3. 创客智造:http://www.ncnynl.com/category/slam-learning/
  4. 半仙居士blog: http://www.cnblogs.com/gaoxiang12/
  5. 泡泡机器人课件:http://www.rosclub.cn/article/list/id/9/p/9.html
Compartir

蒙特卡洛算法简介

2017-09-22

前言

这两年随着AlphaGo等一大票围棋AI的崛起,隐藏在其背后蒙特卡洛算法也开始为大众所知,不过基本上大多都和我一样是只闻其名、不知其物。最近复习概率论,看到蒙特卡洛算法,感到很意外,没有想到威力强大的蒙特卡洛算法在原理上竟然是如此的简单。

蒙特卡洛算法简介

蒙特卡洛,英文:Monte Carlo method,蒙特卡洛方法灵活多变,但核心都是使用随机数(或伪随机数)去了解一个系统,从而来解决很多计算问题的一类方法,所以很多文档中都说的是蒙特卡洛方法而不是算法。

从概念上看,这个说明是相当抽象的,一下通过实例来说明,会发现蒙特卡洛方法实际上可以非常简单。

利用蒙特卡方法洛求π

这也许是在讲解蒙特卡洛方法时最常用的一个实例,在求π值的方法,我觉得这个方法也许是最简单的。

如何求解,我们在构造一个正方形和它的内切圆,如下图所示:

我们在正方形的面积内随机选择一个点,大量的重复该随机实现,统计点落在内切圆内的次数。从概率论的知识可以知道,这种重复独立实现的概率分布服从中几何分布,随机点在圆内的概率是正方形和内切圆的面积之比,因此就可以通过实现统计出来的结果近似的看成随机出现在圆内的概率,从而求出π值。

就是如此的简单粗暴,这种看似暴力的做法,不仅原理上很简单,而且通过计算机能够很方便的编程实现。通过在指定的空间域内产生随机数,统计验证系统的分布结果,近似的看成系统的分布概率,以此来求解,这就是蒙特卡洛方法。

利用蒙特卡方法求积分

利用蒙特卡洛方法可以求任意的定积分值。实际上求积分就是上面求π值方法的推广,一一元为例,定积分可以看成是函数曲线在和横轴的在积分域内的面积,通过构造包含该面积的一个长方形,然后使用随机数,统计在积分区域面积的概率,实际上就和上例一样了。

参考

蒙特卡罗方法入门:http://www.ruanyifeng.com/blog/2015/07/monte-carlo-method.html

Compartir

欧几里德算法求最大公约数

前言

最近看算法书籍时,发现欧几里德算法,再次对欧几里德等古代的学者深感佩服。算法简单巧妙,更重要的是,它背后所隐藏的对问题通用的解决方法——对于那些无法直接求解的问题,通过发现其潜在的规律,使用循环迭代逐步减小问题的难度,最终得到问题的结果。如今在计算机科学里无处不充斥着这样的方法,但是我们古人早在几千年前就已经使用得炉火青纯了。

欧几里德算法思路

欧几里德算法是求解两个数的最大公约数问题的方法。对于我们来说两个较小的数字,可以比较容易的推算出来他们的最大公约数,但是稍微大一点的数字,就无法直接心算求得了。

我们使用程序来解决这问题,普通解决思路的求法就是从小大一个个尝试,直到扫描完从0到两个数中的小者这个区间内的所有数字,输出一个能够同时被两个数整除的最大数字。这个方法的算法的时间复杂度是线性的。

而欧几里德算法,巧妙的利用了:两个数的公约数也是,其中任意一个数与两者模的最大公约数。

例如:求A,B的最大公约数是C,假定A>B,那么A和A%B的最大公约数也是C,B和A%B的最大公约数也是C。这样我们就将两个求两个大数A,B的最大公约数问题,转化为求B于A%B这样两个更小的数的最大公约数。经过逐层迭代,问题最终能够转化为我们可以直接求得的两个很小的数字的最大公约数问题。

当然对于计算机而言,它不能判断数字减小到什么时候可以直接看出来最大公约数,需要有一个明确的条件告诉它循环结束。这个调节就是当两个数的模等于0时,循环结束,最大公约数就是当前小的哪个数字。

证明

继承上述的例子:A = K1 x C,B = K2 x C,且K1和K2互质,A和B的模D = A % B = (K1 - K2 x n) x C,所以A、B、D有共约数C,有因为K1和K2互质,所以K2与K1-K2 x n是互质的,所以C是B、D最大公约数,所以A和B的最大共约数与B和D的最大共约数一样。

在逐步迭代过程中,D的系数K1-K2 x n会不断减小,最终等于1,这个时候D = C,整个过程结束。

使用欧几里德算法,可以让求解空间域快速收敛,算法效率大大提高。

代码

虽然使用循环会更高效,但是写为递归的形式会更容易理解,所以以下为递归实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int do_gcd(int max, int min)
{
return min?do_gcd(min, max%min):max;
}

int gcd(int a, int b)
{
if(a == 0 || b == 0)
return 0;

if(a > b)
return do_gcd(a, b);
else
return do_gcd(b, a);
}
Compartir

检查C++类私有变量的方法

前言

C++的封装是个好东西,将需要保护的数据成员定义为私有成员可以有效的防止这些数据没有意外修改,但是在程序release之前,良好的封装却成为我们调试的代码的障碍,特别是在没有IDE的情况下面。

在Linux环境下,我没有使用IDE的习惯,也不熟悉配置使用gdb,感觉就是很麻烦,因此我的调试方法就是printf/cout。直接编写调试的输出信息的代码,感觉灵活性更强,但是带来的后果就是,原始的程序代码被大量调试代码,导致代码的严重整洁性和可读性下降。

为了检查C++类中的私有数据,同时又保证尽量少的修改原有的类文件代码,我想了一个不算很好,但是也能基本上完成目标的方法。

思路

分析上面的需求,我自己设定我的目标有三个:

  1. 能够在类外访问类的私有成员变量
  2. 不用修改或是很小的修改原有类代码
  3. 调试模块对所有类通用

对于目标第一点,比较好实现,只要用指针或者引用记录类私有成员就可,不过要注意记录的指针或者引用不能修改其私有成员。

对于第二点,我实在想不出有什么方法,能够在完全不修改类代码的前提下,就能获取到它其中内部私有数据成员的地址的方法,所以我只能选择增加少量代码,负责记录类的私有数据成员。

对于第三点,我可以将这个功能模块写成一个调试类,限定这个只负责记录信息,而将具体的显示打印交给使用者。

代码

于是我使用不到40行的代码完成了我想法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <map>
#include <string>
using namespace std;

class ClassDataMonitor
{
public:
template <typename T>
const T& get_elem_by_name(string name, T smp)
{
return (T*)data_map[name];
}
bool is_elem_monitored(string name)
{
if(data_map.find(name) != data_map.end())
return true;
else
return false;
}

void add_data_monitoring(string name, char *data_addr)
{
data_map[name] = data_addr;
}
private:
map<string, char*> data_map;
};

以上我使用map来记录类中数据成员的一对信息:名字和地址,在使用时我们通过名字引索找的数据成员的地址。

另外get_elem_by_name函数返回是一个常引用,保证传出的数据不会被修改。而它的参数是数据成员的名字和类型。可以看到我了使用模板,指定一个类型,所以我可以通过char *的地址指针,转为实际类型的变量,使我可以返回该类型的常引用。

使用

在使用时,可以有两种方法添加数据监视,一是让被监视的类继承ClassDataMonitor,二是让ClassDataMonito的对象成为被监视类的共有成员。完成上步操作后还是要在被监视类中添加代码,指定监视的数据成员,如下:

1
add_data_monitoring("data1", (char*)&data1);

这个类能够帮助我在类外,也能够随时查看其中的私有变量的值,虽然不够直接,但是至少是实现了功能,可惜是它不能在不修改原来类的情况下使用。

Compartir

c和c++的区别的一些个人见解

前言

为了能够更加方便的使用到标准的高级数据结构,同时提高代码的扩展性,最近我将之前用C写的一个模块代码改写成C++ 的版本。尽管一直以来C++ 都被认为是C的一个超集,但是通常两者的编程思路是很不相同的,为尽量符合C++ 的编程思想,我却花费不少时间来做修改(或者说是重构)。

很自然的,我随手就搜了一下C和C++ 的区别与联系,于是就发现在知乎上有关于“C 与 C++ 的真正区别在哪里”的讨论,在学习了一众大佬们的讲解后,就发现有大佬谈起了面试中被问到这种问题的情况,他表示与其问这种大而广的玄学问题,还不如问一些实际项目中对于C和C++ 的使用上的心得体会,因为他觉得:我见过很多十几年以上经验的C/C++ 工程师,经历过很多大项目,他们很少提及诸如“C 和 C++ 的真正区别” 这种话题,他们通常关心 “是否稳定,是否可维护,是否扩展性好” 这种问题”,他推测会这样提问的面试官一般很可能是以下这三种情况:1. 已经早已从技术岗位走向管理岗位了 2. 面试官偏向学术 3. 所在团队比较虚、不务实。

对此我深表赞同,面对“C与C++ 的区别在哪里”这样的问题,仅仅从语法角度深入分析都可以写一本数了,更何况还可以从各种各样不同的角度来说明,面试者很难把握面试官到底想了解那一方面。不过我觉得问题既然提出来了,还是可以思考怎样说明会是一个比较好的回答。

C和C++的差异

在回答这个问题的时候,我觉得不应该直接从语法的差异来讲,因为语法的细节太多,如果没有一个主题来统一它的话,那就只能是泛泛而谈。所以我建议从设计角度,从C++ 设计程序相对C的最大特点入手,然后再衍伸出为了达到对这些设计思想良好的支持,C++ 在语法上的所添加的哪些特性。

最近在看《Effective C++ 》一书时,就看到作者对当前C++ 编程范式的说明,他提到“今天的C++ 语言已经是一个多重范式型的编程语言,一个同时支持面向过程,面向对象、函数式编程、泛型编程、元编程”,在这其中“函数式编程”和“元编程”我个人不是很清楚,但是面向对象和泛型编程是深有体会。C++语言对面向对象和泛型编程是提供语法级的支持,使得程序员可以使用很简单的代码达到目的。反观C语言,一般的C编程基本上都是面向过程的,这并不是说C就没法支持面向对象或者是泛型,而是说C没法通过它自身基本的语法特性来实现,即使能够通过复杂而繁琐的代码间接的构建了,也需要面临各种各样的难题需要处理(如性能问题、安全问题)。

首先谈一下面向对象,我们都知道面向对象有三个基本特点:封装、继承、多态。为了达到良好封装的效果,C++ 在C的struct的基础上定义了class。在用法上C的struct通常只是用来打包一群有关联的数据,而C++ 的class除了可以包含数据外,还可以拥有操作这些数据的函数,另外还有一个非常重要的特点就是:class中任何成员都能定义自己的安全属性(包括public、protect、private),这就让一个class既能有对外完全不可见得部分,也有专门用来与外界交互的部分;最后class创建和删除由它自身专门的构造函数和析构函数来控制,这使得一个class就有了自治特性。从上面的分析可以看出C++的class比起C的struct在封装特性上的支持是完全不在同一层次上的。

在说明封装的特性时,如果说C语言能看到一点影子的话,那么继承在C中是完全没有这样的概念的。继承在C++ 中的支持也是基于class的,在C++ 中允许一个class继承与另一个class,从而在不用写重复代码的情况下,从另一个class中获取到它其中的所有的成员(包括成员变量和成员函数)。

也许使用C语言的函数指针也可以牵强的看做是多态的一种体现,但是在C++ 中这种程度的多态支持就显得太弱了。首先虚函数在C++ 的引入是对多态特性强力的支持。在class继承时,C++ 可以将基类中的成员函数定义为虚函数,这样在派生类中就就可以完全重写父类中的虚函数,在调用这个函数时,C++ 会在运行时根据不同调用对象来选择具体调用哪个版本的函数,即派生类的对象调用它自己重写函数,基类对象调用基类中的函数。

以上就是C++ 对面向对象编程支持所拥有的一些主要的语法特性。再来说泛型,C++ 对于泛型的支持相对就单一些,主要就是模板了,但是模板所的作用确是巨大的,它能够让我们定义函数和类时,不用指定特定的参数类型,而将注意力关注到通用的逻辑实现上来,这使得代码的通用性大大增加。而泛型编程最著名莫过于C++ 的标准模板库STL了。

Compartir

多版本Python环境的搭建

前言

众所周知,目前主流Python仍然是2和3两个分支并行发布的,而在这两版本中,又有不同的迭代版本,虽然说新老版本之间的差别很小,但是其所对应支持的库版本可能不一致,更复杂的是各种库之间的相互依赖关系。这就导致了我们为了满足不同Python项目的要求,不得不在我们的系统中装多个版本的Python,这样一来如何在一个系统上管理多个Python版本,使其不会相互影响就显得十分重要。

使用分支版本区分Python2和Python3

当前在大多数Linux系统上(如Ubuntu),都已经默认安装上了Python2和Python3,为了区分他们,我们在终端使用“python2 xx.py”来执行用Python2写的程序,使用“python3 xx.py”来执行用Python3写的程序。同样我们使用pip2和pip3来区分安装Python2和Python3版本的库。

注意在Ubuntu系统上安装pip3的需要指定版本,安装pip3命令为:

1
sudo apt-get install python3-pip

pip2的安装命令为:

1
sudo apt-get install python-pip

在Ubuntu上可以同以下命令来查看python的路经:

1
which python

在Windows系统安装python时,可能无法使用“python2”和“python3”来区分,因为在bin目录下只有“python.exe”而没有“python2.exe”或者“python3.exe”,所有我们直接手动修改python可执行文件文件明,加入版本号即可。

Windows下使用py.exe

当Windows下同时安装了Python2和Python3时,可以使用在Python3目录下的工具py.exe来执行Python程序。在其后面执行版本号即可调用不同版本的Python。

Python2为:

1
py -2 xx.py

Python2为:

1
py -3 xx.py

如果你嫌在执行时加入参数麻烦,可以在Python程序的开头加入如下行来指定时调用python2还是python3解释器:

1
#! python3

后面就直接使用py而不用加参数了。

使用conda管理多版本

当我们不只满足区分Python2和3这两个大版本,还要区分跟细的版本如:python3.4,python3.5,以及一些库的版本时,使用上面的方法可能管理就有不方便了,特别是要避免不同版本的库相互影响很困难。这个时候最后使用那些python多版本管理工具。常用的有:

  • pyenv
  • virtualenv
  • conda

这些管理工具实际上都时重新安装独立的Python和需要的库,让将这些环境各自隔离开来,并提供相应的管理和切换和接口命令。

conda是Anaconda提供的管理工具,可以把conda看作是pip + virtualenv + PVM(Python Version Manager) + 一些必要的底层库。由于我安装了Anaconda环境,所以就直接使用它提供的conda。如果不想安装整个Anaconda而只想用conda工具,则可以直接下载其简化版Miniconda。

注意在直接在官方下载地址下载Anaconda时,可能其下载速度十分的缓慢,我们可以选在国内的清华大学 TUNA下载镜像源来下载。

1.创建和删除一个Python环境

使用conda创建一个Python版本的环境命令如下(指定名字为test_env为版本2.7),第二命令为复制其他环境的配置:

1
conda create -n test_env python=2.7


1
conda create --name test_env --clone root

移除环境命令为:

1
conda remove -n test_env --all
2.Python环境切换

进入test_env环境:

1
source activate test_env

退出test_env环境:

1
source test_env

在Windows上以上命令不需要加source

3.包管理和信息查看

通过conda安装库包,先进入某个环境,然后:

1
conda install pandas

或指定环境

1
conda install -n env_name pandas

卸载包:

1
conda remove pandas

查找包:

1
conda search pyqtgraph

查看包信息:

1
2
3
conda list
# 指定查看某环境下安装的package
conda list -n test_env

显示通过conda已经安装的环境:

1
conda env list
Compartir

一个纯C软件工程的设计

前言

说起来C语言是我的编程入门语言,但是自从工作后我就慢慢的开始从C开始转到C++,很少使用到C,而在最近半年由于工作的需要,C++也写得少了,Python成为了我的主要使用语言。伴随着这些新语言的使用,它们优秀语言特性和程序设计模式让我受益匪浅,因此现在当我再一次回到纯C开发时,我也希望能够借用上一些设计方法和思路,让C写出来的代码模块的独立性更好,通用性更强。

目标

软件需要实现的目标很简单,功能就是接受外部通信发过来的数据包,将数据包解析为已经定义好的指令,然后按照指令协议控制硬件完成指定的操作,软件工作在一个运行着Linux操作系统的嵌入式设备上。

由于软件是运行在嵌入式设备中(不过硬件性能不错),需要和专用的硬件模块进行交互且不需要界面,又是在Linux平台之上的,同时也希望尽可能的保持高效,所以我首先想到的语言是C了。为什么不用C++?也许是惯性吧,感觉在Linux平台之上写不需要UI的底层应用用C++会比较奇怪。

设计方案

分析设计需求,其实当前软件需要需要完成的功能是非常简单的。我将整个软件划分4个模块:

  • 通信数据接收模块
  • 指令解析模块
  • 指令调度
  • 硬件操作模块

以下为上面四个模块做详细的解释。

1. 通信数据接收模块

这部分的主要作用打通过外界与本机的物理通信链路,接收外部传来的数据,这部分相对独立,不管是本机与外部通信是使用哪种方式,它的都只要将传递的有效的数据信息部分传递给命令解析模块即可。

但是我们唯一需要限定的是,不管使用什么样的通信方式,不管使用怎样的协议传输数据,需要保证有效数据的数据格式一致。

2. 指令解析模块

由于数据接收模块可以提供一致的数据,所以指令解析模块就只需要参照这种固定的格式,将数据流变成结构化的数据,存放在相应的结构体中。

3. 指令调度模块

这相当于一个流程控制的模块,接收解析出的指令作为输入,对应按照指定的调度规范,调整指令执行的流程、顺序、或参数等操作,让后调用硬件操作模块来执行指令。

4. 硬件操作模块

硬件操作模块主要的作用的作用就是,对外提供实现对硬件设备控制的接口。从功能上来看,它的独立性是更强的。对于较低的层次接口来说,只需要提供通用硬件寄存器的读写操作,对于较高层次的接口来说,可以各个硬件单项功能的寄存器操作步骤集合做封装。

指令的结构设计

虽然说在设计各个模块尽可能相互独立,但是有些因素基本上对所有模块都有影响,这些因素我们应该尽量确定细节,以由于这些因素的改变对整个程序的影响。在这个工程中指令功能规范设计就是其中之一,所以我们在编写所有模块之前,首先就需要确定功能规范,围绕着此规范在去写各个模块中的逻辑实现。

为此我构造了一个专门处理指令规范的结构体,这个结构体是全局的,它为所有模块共用。它由以下几个部分组成:

  • 指令ID(int)
  • 指令名称(char *)
  • 指令参数解析函数(函数指针)
  • 指令执行函数(函数指针)

以下对结构中的每个部分进行说明:

1. 指令ID

指令都都是唯一的,我们使用ID标示这个指令,数值类型为整形。为了方便标示,我们用一个枚举型变量来代替宏来定义每个指令的ID值,利用枚举型变量内部元素常量特性,同时默认定义时,后一个定义的元素值为前一个元素值加1的特点,保证每一个指令ID值唯一,另外在所有指令定义完成后,在最后增加一个枚举元素来记录总的指令数量。

在当前的设计中,一旦结构体初始化,其指令ID期望是固定的,所以可以将其设置为const属性。

2. 指令名称

指令名称用来存储一个字符串,数据类型是一个字符串指针,虽然这并不是必须的,但是字符串的描叙对指令是个很好的说明。同样当ID确定时,名称也不应该改变,所以需要将其设置为const属性。

3. 指令参数解析函数

不同的指令所需要的参数可能不一样,有些指令设置可以不需要参数,所以将将当前指令的解析函数用函数指针保存起来,好处是:1. 不同指令直接对应各自独立的解析方法,不需混在一起要很多个if判断 2. 即使解析方法发生了变化,只要将函数指针重新设置为新的解析函数即可。不过,一般情况下相同指令的解析函数不会动态变化。

4. 指令执行函数

指令执行函数是只将指令规定的功能,通过硬件操作模块的接口,对硬件进行配置,让其完成对应工作。和指令前面类似,这里保存的也是一个函数指针。

指令数据的结构设计

前面所提到的指令解析函数,它的输入是无格式的数据流,它的输出是有意义的结构化的数据。于是,那我们就必须先完成这个数据的结构的设计。

设计思路有两种:1. 对每个不同的指令设计一个存放数据的结构体,2. 综合所有指令所带的数据,统一记录在一个结构体中。考虑到在后面指令调度模块可能会需要缓冲指令和数据,所以这样来看第二方法更加简单合适。

指令数据的结构设计如下:

  • 执行序号(int)
  • 指令ID(int)
  • 所有指令的数据元素(void *)
  • 其他数据

以下一一说明:

1. 执行序号

序号标示,当前指令在整个控制系统中的唯一编号,序号由外部控制中心产生,通过通信传递过来。

执行序号在反馈应答的应用中有很重要的作用,由于有缓存的存在,当前执行的指令可能路后于是很早前的控制中心发送过来的指令,当指令接收和指令完成后,都发送应答给控制中心,那通过执行序号就可以很好的区分它们。

2. 指令ID

与指令结构体中的指令ID一样

3. 所有指令的数据元素

由于我们采用了,用一个结构记录所有指令的数据,那么之一部分实际上是包含了一系列数据元素的。这样做是我们在缓存指令和数据后,回溯缓存的指令和数据时更方便。

4. 其他数据

其他数据是为那些可能需要传输大量数据的指令而设计的,一般不需要。

Compartir