Linux驱动开发小结

前言

对于Linux驱动我已经是太久没有弄,已经生疏得不行了,而我当前的硬件上Linux应用项目又不可能不涉及到驱动部分,没办法只能靠自己到处查找资料,把以前忘掉的知识点补一点回来。

个人觉得通过这几天学习还是有一定的收获的,不过我还是得感谢以前在学校做过这样的东西,正应为有以前的了解,所以才让我在现在学习的过程中能够更快速全面的看清Linux驱动的流程,能够更加容易的理解哪些拗口概念。

这篇文章算是我这几天的一个小结,只是简单的介绍比较零散的Linux驱动开发的一些基础要点与理论,因为一两天的时间根本就不可能能够深入Linux驱动开发的细节。

Linux驱动开发模式

linux驱动加载有两种:一种是直接集成到内核中的,它时随内核的启动而加载;另一种是动态加载,在系统运行后,使用insmod等命令来加载。

相对的驱动开发也可以分两种,一种直接编译到内核中,另一种以模块形式编译,完成后将生成的驱动模块文件动态的加载到系统。显然后一种方法是更是和开发阶段使用,前一种更适合完成开发后release。

Linux驱动类型

Linux下驱动开发一般设备归为三类:字符设备、块设备、网络设备

1. 字符设备

所有能够像字节流一样访问的设备都可以认为字符设备,字符设备是必须串行顺序依次访问的,字符设备被映射为文件系统中的设备节点,通常在/dev/目录下,一般通过open、read、write等函数操作设备节点来控制。

2. 块设备

一般是指磁盘、内存、Flash等存储设备,它们不仅能像字符设备那样以字节流的方式访问,也可一次传输任意多的字节。

3. 网络设备

相对前两个算是特殊的设备,通过socket来操作。

设备号

设备号主要用来标示设备,由两部分组成:

  • 主设备号:一般主设备号表示与同属一类的设备,可以共用同一驱动。
  • 次设备号:当有多个设备属于同一类设备时,用次设备号区分

通过宏:MKDEV(int major, int minor),可以将主设备号和次设备号组合成一个设备号。

Linux设备驱动开发的基本流程(字符设备为例)

1. 找到内核源码

驱动程序是依赖于你的内核的,所以想面对哪个系统开发驱动,需要先找到它对应版本的源码。一般Linux系统都有将自己的内核源码保存到相应的目录。我们可以通过查看 /lib/modules目录下的内核版本信息,选择对应的版本,使用ll命令查看所选版本内核的原始路径在哪。

2. 编写Makefile

我们自己写的驱动源码不用再内核源码目录下,而可以放在我们自己指定的任意目录中,但是需要设置内核源码根目录的路径,同样也需要设置编译器,这些都可以在Makefile中设置。Makefile的写法一般都是固定格式的,可以参照别人驱动工程来写。

3. 编写驱动源码文件

驱动代码与一般的应用程序相比没有main函数,驱动模块的入口是设置被为“module_init”的函数。另外打印输出也不用printf,而使用“printk”函数,而printk函数对显示消息的重要性做了等级划分的,可以根据实际情况,设置不同的等级参数。

4. 创建设备节点

设备节点是我们Linux上层应用通过驱动操作硬件的接口,我们可以在通过通过命令手动创建,也可以在驱动中就自动创建出来。

5. 实现file_operation结构

“file_operation”是驱动程序中十分重要的结构,这个结构记录了我们在应用中操作设备所使用的open、read、write等API的对应的实际实现函数。file_operation不需要讲其成员全部都配置好,只需要配置我们驱动需要用的,不用的部分会自动赋值为NULL。

6. 编译加载

直接使用make编译驱动模块,生成.ko文件,使用“insmod”命令加载驱动,通过dmesg和tail结合显示详细的加载输出信息,如下:

1
2
insmode test.ko
dmesg | tail -5

也通过“lsmod”查看已加载成功的模块,如果不需要了的话可以通过“rmmod”命令卸载指定模块。

7. 编写应用测试

为了验证驱动是否能够正常工作,我们在通常需要自己编写一个小的Linux应用程序来使用用一下我们的驱动,测试其是否能正常工作。

其他

另外就是Linux驱动是一个很庞大的结构框架,出了上面说的三种驱动类型的基本分类外,Linux系统中还存在很多驱动子系统,这些子系统是建立在三种驱动类型的基础之上的,而很多设备驱动在需要在这些子系统的框架之下编写代码,这种的话就需要了解这些子系统的具体要求了。

Compartir

MIZ702N启动流程与开发流程

启动流程

准备好开发环境后,我们开始正式开发,第一步当然是要让开发板启动起来。之前已经按照硬件供应商给的做好的文件启动过开发板,但是那只是验证开发板是否正常,不过也是可以看到它启动后的效果的。

查阅资料可以看到ZYNQ7000有几种启动模式(由于MIZ702N是属于ZYNQ7000系列,所以以下属于ZYNQ7000的东西也适用于MIZ702N),分别是:

  • QSPI Flash启动
  • Nand Flash启动
  • Nor Flahs启动
  • JTAG启动(调试用)
  • SD卡启动

这些启动方式通过模式引脚来选择。当我们选择SD卡来启动我们Linux系统时,开发板的大致的启动流程如下:

ZYNQ7000启动流程

可以看到,当开发板上电后,它是从片内的Boot Rom开始运行的,这部分启动代码是固化在板上的,它会完成一系列板上默认的启动工作,然后通过检查模式引脚来选择启动模式。

BootRom代码执行完成后就来到FSBL阶段,这部分程序一般主要有两部分工作:

  1. 初始化板上PL部分
  2. 加载uboot或其他应用程序

通过了FSBL阶段,硬件部分就初始化完成,后面的部分就和一般Linux启动一致了。

开发流程

从上面的启动流程可以看到,其实ZYQN7000系列产品虽然是FPGA和SOC的结合体,但是它的启动还是和普通SOC启动很类似的,唯一的一个不同点就是增加了一个FSBL的过程。不过在实际开发中,正式由于FSBL阶段的引入导致ZYQN7000的开发复杂了很多,有很多都是需要软硬件相互交互的东西。

如下,是我概括的ZYQN7000大致的开发步骤的流程图:

ZYQN7000开发步骤流程图

由上图可以看到,我们的FSBL文件是被包含在BOOT.bin文件中的,为了生成BOOT.bin文件并最终启动Linux系统,我们至少需要以下三个文件:

  1. 硬件设计比输出的特流文件
  2. 与硬件设计匹配的FSBL文件
  3. 编译后的uboot

使用Vivada的SDK中的boot制作工具来创建BOOT.bin文件,文件名不能变,启动将以这个文件名的文件启动,除此之外还要根据硬件设计还要输出设备树文件,提供给Linux内核编译时使用。另外新的硬件模块,要在Linux中的应用程序中使用,还需要开发对应的Linux驱动。

根据上面的分析,软硬件模块虽然不够相互独立,但是也没有到牵一发而动全身的地步,当有硬件修改的时候,软件可能只是需要配合硬件输出一些文件即可,整个工程不会有多少改变。

Compartir

再入嵌入式坑的感想——嵌入式技术是否在进步?

这几天,开始弄MIZ702N开发板的linux系统移植部分,发现不管是uboot还是linux内核还是文件系统的移植都还是基本上保持着我当初学习嵌入式时的步骤和方法,还是一样的“配方”,还是一样的套路,但是我却完全高兴不起来,因为这意味着我无法高效而又可控的进行我的项目。

Linux系统的移植流程并不复杂,但是我一直都觉得它是个很烦人的步骤,虽然都是一样的套路。它的烦人首先在于它依赖的东西太多了,而这些依赖的资源和工具有很多往往是硬件供应商定制而不太通用的东西。为了快速进行项目,我们大多先照着硬件供应商给定的源码工具和实例来做,但即使这样,你也许仍然会遇到不少这样或者那样的问题,更糟糕的是你解决的这些问题并不能给你的下次开发带来多少好处,换句话说就是你能够能够通过本次解决的问题让你下次开发时更有经验,但是你不能保证下次开发时就不会遇到问题。所以对嵌入式开发者来说,这看似固定流程化的操作,但是你却不得不一次一次面对问题丛生的过程。

其次是它的内部的复杂性,很多时候我们仅仅靠硬件供应商给我们提供的资源是远远不够的,我们的项目可能需要更多自定制话的东西,这时候最可靠的办法就是去阅读源码了:uboot、内核源码的文件结构,启动的流程,从源码中寻找是最可靠的。但是这中间要花费的时间和精力实在是太大了。当然通过源码的学习得到的知识是通用的,因为像uboot、linux内核这些稳定后主要的主要的结构就不会经常变化了,学到后就是自己的。只是如果不是专门以linxu移植为产品的公司的话,linux移植在实际项目中顶多就是一个小小的中间过程,而在这个上面费很长的时间实在是有点轻重倒置。

linux移植仅仅是嵌入式开发的一个缩影,代表这嵌入式软件开发整体复杂而低效开发的开发技术水平。当然这是有客观原因的,由于嵌入式领域与生俱来的多样性,导致它的开发本来就很难变得简洁统一。步入嵌入式领域,开发者就不得不陷入各种硬件平台和各种软件代码相互依赖的泥潭中,无法很好的集中精力处理核心的事情。

想想从我学习起到现在已经有5年的时间了,这五年里各种技术不断的更迭,新的开发框架不断涌现,但是嵌入式软件开发技术似乎仍然停留在5年前,也许是更久的水平。我可以这样理解:一门技术不怎么改变,要不就是已经完善得不需要做什么修改了,要不就是这个领域已经没有了创新,停止了进步,显然我不认为嵌入式软件开发技术是前者。

Compartir

MIZ702N开发环境的准备2

前言

承接上一篇,前面主要是简单介绍MIZ702N的硬件配置和新建虚拟机时可能出现的问题;这一部分主要记录新开发板运行状况检查和编译器环境的搭建。

官方资料收集网页:http://www.osrc.cn/forum.php?mod=viewthread&tid=1341&highlight=%D7%CA%C1%CF

检查开发板

新开发板拿到后当然是直接开机,检查一下运行是否正常。虽然这只是一个非常简单的操作,但是也能初步判断板子是否正常。

1. 安装usb转串口驱动

通过串口与开发板通信是最基础的方法,不过当前的开发板都是通过USB转串口来实现串口功能的,因此需要安装对应的USB转串口的驱动。对MIZ702n开发板在Windows平台上需要安装的驱动名为“ft232r-usb-uart”,可以直接在网上搜到。接上设备,装好驱动后会出现新的串口,如下图中的USB Serial Port。

USB Serial Port

2. 使用串口工具进行通信

最基本就是串口调试助手,另外一些比较好用的通信工具有:SecureCRT、putty。打开通信工具,需要设置一下串口通信协议参数,对于MIZ702开发板,波特率为115200,流控为:XON/XOFF,如下图:

串口通信参数设置

3. 打开电源

打开设备电源,检查连接的串口上是否有信息输出,检查开发板是否能正常进入操作系统,并通过串口启动终端。正常启动显示如下:

设备启动输出信息

重新写入BOOT分区和Linux文件系统

当然以上的信息是在一切都是正常的情况下的结果,主要的几项注意点是:

  1. 开发板保证是SD卡启动
  2. boot文件没有问题
  3. Linux文件系统没有问题

在我第一次运行开发板时就出现了问题,开发板开机后,串口无输出。后面测试了很长一段时间才发现是原SD卡中的FAT分区和EXT分区中的文件都有问题,这个时候需要参照 官方文档来重新写一下SD开。

由于官方已经做好了boot文件和linux文件系统,所以这部操作很简单,只需要简单的将SD卡原来的文件删除,然后将官方给的压缩包解压后复制对应的分区就可以了,boot文件和linux文件系统分别为:

官方系统文件

在这个步骤中使用到了一个比较方便的文件夹同步命令:

1
sudo rsync -a ./ /tmp/sd_ext4

它实际上是实现负责的功能,第一个文件夹是源文件夹,第二个文件夹是需要同步的文件夹。

安装arm-linux-gcc编译器

为了在PC上的Ubuntu系统上编译开发板的程序,那么就需要安装交叉编译工具:arm-linux-gcc。在这里最好安装官方给定的版本,因为嵌入式的板子多种多样,太老的或太新版本的编译器都不一定支持你的开发板。我这里的版本为:“xilinx-2011.09-50-arm-xilinx-linux-gnueabi.bin”

对于当前MIZ702N开发板,在64bitUbuntu系统中安装arm-linux-gcc编译器时,需要先安装32bit的支持库,具体安装步骤如下:

  1. 安装32bit支持库

    1
    2
    sudo apt-get update
    sudo apt-get install libgtk2.0-0:i386 libxtst6:i386 gtk2-engines-murrine:i386 lib32stdc++6 libxt6:i386 libdbus-glib-1-2:i386 libasound2:i386

    有些网上说Ubuntu1604不支持以上的32bit库,但是经验证在我的虚拟机上是没有问题的。

  2. 参照官方文档安装编译器

  3. 将编译器的路径写入PATH中

其实在开发板上官方给定的linux系统中已经集成了gcc编译器了,编译出来的代码可以直接在开发板上运行,所以如果不需要经常修改Linux系统的话是可以直接在开发板上开发程序的,只不过编译效率可能不及PC机。

Compartir

MIZ702N开发环境的准备1

前言

最近由于工作需要开始接手基于MIZ702的硬件平台的Linux的开发,仔细想想,工作这么久,这好像还是我第一次接手嵌入式Liunx相关的工作。这几天拿到开发板,开始了阅读文档、安装Ubuntu虚拟机、参考官方说明着手移植Linux系统,这一切让感到熟悉又陌生,仿佛有回到了在学校学习这些东西的那段时间。说熟悉是因为虽然过了这么多年,这一系列基本的流程还是当初我学习时的那样,说陌生是因为我放了太久的时间做些东西了,面对崭新的硬件平台和对比以前更新了不知多少代的Linux版本感到一种新鲜感。

想当年嵌入式Linux开发也算当时热点领域,只不过没多久就被大红大紫的安卓、IOS开发所取代了,再到后来基于HTML5的前端开发开始风行,到现在深度学习、人工智能开始浮现,短短几年的时间热门的技术领域却不断变迁,我是深刻感受到了当前技术发展之快。

MIZ702N开发板的介绍

MIZ702N是南京米联电子科技公司设计的一款基于Xilinx(赛灵思公司)Zynq 7000系列可扩展硬件平台的开发板。它包括两个部分组成,一个是核心板,这当然就是Zyna 7000了,具体型号为:Xilinx XC7Z020-1CLG484CES Zynq-7000 AP Soc ;另一个是功能板,这上面集成了很多方便的硬件功能模块,这两者是可拆分的。

在这里要特殊说明的就是Xilinx(赛灵思公司)的Zynq 7000系列产品,它最大特点就是它将传统的SOC和FPGA很好的结合在了一起。Zynq 7000包括:

  1. 双核ARM Cortex-A9MP Core
  2. 75K可变成逻辑单元的FPGA

一般处理器系统被缩写为PS,可编程逻辑单元部分被缩写为PL。

这里简要说明一下MIZ702N的主要配置情况:

  • CPU:双核A9 667M
  • 内存:1G DDR3
  • 板上存储:8G EMMC
  • FLash:256M bit的QSPI Flash
  • USB:USB OTG2.0
  • 视屏输出:一个HDMI、一个VGA(16-bit Color)
  • 视屏输入:2个CMOS摄像头接口
  • 支持FT卡

Ubuntu虚拟机

我是使用Ubuntu1404的虚拟机作为我的开发环境,安装虚拟机的过程太简单就不写了,这里主要记录我在用虚拟机是遇到的一些问题。

1. VirtualBox默认不支持USB2.0/USB3.0

如果你是使用的VirtualBox作为你的虚拟机工具的话,你可能会发现VirtualBox是默认不支持USB2.0和USB3.0的。如果你想在VirtualBox中支持USB2.0和USB3.0,那你需要在VirtualBox的官方网站下载扩展包,注意要对应自己VirtualBox的版本。然后按照一下操作执行:“管理”–>“全局设定”–>“扩展”,找到下载的扩展包安装。

2. 虚拟机不能安装64bit系统

不管是VmWare还是VirtualBox,如果你发现你不能安装64bit的虚拟机时,那么和有可能是因为你用的Inter的CPU没有开启CPU虚拟化。这个需要在BIOS中修改,不同主板的BIOS修改的地方可能不一样,在网上搜一下照做就好了。

3. VmWare虚拟机不能连接到Internet

用VmWare创建虚拟机时按照默认的配置,是能够连接到Internet的,但是有时会突然不能上网,这可能是因为你使用一些系统优化工具时将VmWare的一些服务被优化了程序关掉了。在Windows中打开服务管理,将VmWare的服务打开即可。

Compartir

机器学习——k邻近算法和决策树算法

前言

看理论教程时总是感觉太抽象了,所以经网友推荐开始看《机器学习实践》一书。到现在读了3章内容,发现这本书确实是很不错的机器学习的入门书。它从最简单的K邻近算法开始,以python为编程语言,编写实例代码,足步深入。本文便是对《机器学习实践》一书的中前两个分类算法的学习总结。

k邻近算法

在看到这个算法前,我真没有想到这样简单的算法也是机器学习算法的一种。有这样的感想是由于之前看理论的时被一大堆数据公式符号弄得头晕老涨,结果一来看实际的实例确是如此的简单,这巨大的反差让我很吃惊。不过这正式这本书非常好得一点,从浅入深,一开始并不一下子就涉及到那些复杂得理论和公式。

1. 原理

k邻近算法是分类算法得一种,它的个工作原理是:计算待分类数据与已知数据的距离,检测待测数据主要偏向哪一边,就将待测数据分向哪边。那么距离怎么求呢?使用待测数据的各个特征和已知数据的各个特征差的平方和开根号,公式如下:

2. 执行流程

整个算法的流程相当简单:

  1. 迭代整个数据集,计算每个数据点与输入数据的距离
  2. 选出与输入点距离最小的K个数据点
  3. 统计计算这K个数据不同类别的概率
  4. 概率高的类型作为输入数据的类型
3. 基本代码

整个分类器的Python代码量相当少,我这里就直接将书上的代码复制过来了,其中函数inX为待分类的数据,dataSet为已知数据集,labels为数据集对应的类型列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
def kNearestNeighborClassify(inX, dataSet, labels, k):
dataSetSize = dataSetshape[0]
# 将输入数据与每一个数据相减
diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
# 计算数据集的距离
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances**0.5
# 找出距离最小的k个数据的所属类别的频数
sortedDistIndicies = distances.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount = classCount.get(voteIlabel, 0) + 1
sortedClassCount[voteIlabel] = sorted(classCount.iteritems(),key=operator.itemgetter(1),reverse = True)
return sortedClassCount[0][0]
4. 优化——归一化原始数据

原始数据中的每个特征的取值范围有大有小,在使用上面代码分类时,就会导致特征值大的对结果的影响大,但是我们希望每一个特征都同等重要,所以我们需要将原始数据的做一次归一化操作,将每个特征值的范围都映射到0到1的范围内,归一化操作如下:

然后使用归一化的数据作为分类器的算法输入。

5. 优缺点
  • 优点:精度高、对异常值不敏感、无数据输入假定
  • 缺点:计算复杂度高、空间复杂度高

决策树

决策树是书中介绍的第二种分类算法,相比k邻近算法决策树的名号可是响亮得多,不过实际上决策树在使用上比k邻近算法还要简单、更容易理解的,它的难点是在于决策树的构建。

决策树本质上就是一个二叉树或多叉树,在其中的每一个非叶子节点上都会对特征进行分类判断,一般越重要的特征与靠近树的跟节点,也就是说越在前面被判断,通过这一层一层的判断来决定输入数据的最终分类。

1. 构造决策树原理

正如前面所说构建决策树,是决策树算法的难点和关键,书中是以ID3算法来划分数据集来构建决策树。ID3算法是以信息论为基础,以信息熵和信息增益为衡量标准,从而实现对数据的归纳分类,这里就不得不去理解下信息熵和信息增益的这类难懂的概念了。

信息熵:信息熵被定义为离散随机事件出现的概率,一个系统越是有序,信息熵就越低,反之一个系统越是混乱,它的信息熵就越高。所以信息熵可以被认为是系统有序化程度的一个度量。

信息增益: 信息增益指的是划分前后信息熵的变化。

以ID3为划分标准的决策树,是通过计算分类系统的前后的信息增益,作为信息有序度的衡量标准,将信息增益最大的类型作为当前最优分类特征。

2. 构建决策树的步骤

构建决策数据一般会采用贪婪策略、利用递归的方法一次来构建整个决策树,其具体执行步骤如下:

  1. 对当前数据集求信息熵
  2. 尝试使用数据集种的每一个特征为标准进行分类,计算分类后的两个子系统信息熵的和
  3. 比较得到分类后信息增益最大的,作为当前的分类标准
  4. 在特征集种删除当前分类标准的特征
  5. 判断是否分类完成,如果没有,将未完成分类的数据递归重复所有操作

构建决策树的过程很费时,所以当决策树构建完成后,可以将其存储在文件中,使用时直接读取,反复使用。决策树的使用仅仅是需要做一些简单的比较就可以得到结果,相比每次分类都需要使用到使所有数据的K临近值算法,计算量极少。

3. 优缺点
  • 优点: 使用计算复杂度不高,输出结果容易理解,对中间值缺少不敏感
  • 缺点: 可能会产生过度匹配的问题
Compartir

Qt绘图的方法

简介

QPainter是Qt的画图类,在其中封装了很多底层的绘图函数。使用QPainter可以在多种类型的设备上画图,如:Qt的窗口、QPixmap或者是打印设备上。但是怎么使用它,在哪个地方使用它才能够满足我们的画图要求,却不是仅仅看一个QPainter类说明就行的。以下为在Qt中画图的一些常见方法,有些需要使用到QPainter绘制,而有些则不需要。

在paintEvent函数中绘图

在一个窗口上画图,最简单、最直接的方法就重新这个窗口的类的重绘事件的处理函数paintEvent(),在paintEvent函数中实现自己的绘制内容。

当窗口发生变化需要重绘时,paintEvent函数被会自动被调用。如果需要强制刷新,则可以调用repaint()和update()函数,不要直接使用paintEvent函数。

在paintEvent函数中画图的实例的C++代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);

// 设置画笔颜色
painter.setPen(QColor(0, 160, 230));

// 设置字体:微软雅黑、点大小50、斜体
QFont font;
font.setFamily("Microsoft YaHei");
font.setPointSize(50);
font.setItalic(true);
painter.setFont(font);

// 反走样
painter.setRenderHint(QPainter::Antialiasing, true);

// 绘制图片
painter.drawPixmap(rect(), QPixmap(":/Images/logo"));

// 绘制文本
painter.drawText(rect(), Qt::AlignCenter, "Qt");
}

利用QGraphicsView和QGraphicsScene绘图

利用paintEvent函数最基本的绘图方法,不管是主窗口还是窗口上的控件都可以重写paintEvent函数,但是这种方法却不够灵活。有很多时候我们都希望能够在窗口的一个控件上随着程序需要绘制相应的图像对象,这个时候总是要去重写一个控件是很麻烦的事情。这个时候我们可以使用到QGraphicsView和QGraphicsScene来管理和显示需要绘制的图像对象。

QGraphicsView是一个专门用来显示绘制图像的控件,但是它一般需要配合QGraphicsScene一起才能很好的使用。相比paintEvent实现绘图,QGraphicsView和QGraphicsScene则要高级得多,在这里Graphics View提供的是一种类似于Qt model-view的编程。多个views可以监视同一个场景,而场景包含多个具有多种几何外形的items。QGraphicsView提供了视图部件,它可视化场景中的内容。QGraphicsScene 表示Graphics View中的场景,它能够高效的管理在它其中的场景元素。

简而言之就是,QGraphicsView是显示的控件,QGraphicsScene是场景本身。显示的控件可以从不同角度展示同一个场景。因此我们在QGraphicsScene绘图,用QGraphicsView显示。

假定窗口里面有名graphicsView的QGraphicsView,以下为QGraphicsView和QGraphicsScene绘图python代码实例:

1
2
3
4
5
6
7
8
9
10
11
def draw(self):
scene = QGraphicsScene()

scene.addPixmap(QPixmap(r'E:\CaptureFile\test.png'))
scene.addRect(QtCore.QRectF(50,50,80,80), QtGui.QPen(QtGui.QColor(255,0,0)))
scene.addRect(QtCore.QRectF(50,200,80,80), QtGui.QPen(QtGui.QColor(0,255,0)))
t = scene.addText("scene test", QtGui.QFont("Arial", 20))

self.graphicsView.setAlignment(QtCore.Qt.AlignHorizontal_Mask)
self.graphicsView.setScene(scene)
self.graphicsView.show()

除去以上代码,QGraphicsView和QGraphicsScene还可以结合QPainter来绘制,前面就提过QPainter是可以在QPixmap上绘制的,所以我们可以先用QPainter在一个QPixmap上绘制好图像,然后将其添加到场景中显示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
def draw(self):
scene = QGraphicsScene()
mp = QPixmap(r'E:\CaptureFile\test.png')
paint = QPainter()
paint.begin(mp)
paint.drawEllipse(10,20, 80,70)
paint.drawRect(100,100,200,200)
paint.end()
scene.addPixmap(mp)
self.graphicsView_1.setScene(scene)
self.graphicsView_1.show()

其他

除开QPainter画图外,我们还可以利用其他库进行绘制图像,如python中的PIL.Image库,然后用Qt进行显示。

Compartir

使用swig将c和c++代码转化为python模块

前言

在python中使c/c++的方法有不少,最简单的一种方法就是讲c代码编译成动态连接库,在python中使用ctypes模块调用,不过这种方法对c++支持不够好。当前在实际项目中swig是将c/c++转为最常用的工具。

关于swig

swig是非常强大的开源工具,它将支持c/c++代码与绝大多数主流脚本语言相集成,使用swig将c/c++转化为python的模块很简单,由于要满足各种不同的需求,需要设置swig的参数也会变复制。

在windows下安装swig非常简单,在官方网站:http://www.swig.org/,下载安装包直接安装即可。

如何使用swig,swig官方网站提供非常详细的文档,针对python在文档中有专门的章节讲解。在本文中,我只会介绍简单的使用方法和我使用时遇到的问题。

使用swig

如果使用swig将c++转为python模块,按我自己的理解可以慨括为两个步骤:一个接口文件、两次编译过程。

  1. 接口文件(interface file)

    为了使用swig,你需要增加一个接口文件。 接口文件一般以.i为文件的后缀。接口文件的作用是,提取c/c++源文件中的接口函数或类型,以及定义一些特殊的功能,其形式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* example.i */
    %module example
    %{
    #define SWIG_FILE_WITH_INIT
    /* Put header files here or function declarations like below */
    #include "header.h"
    %}

    extern double My_variable;
    extern int fact(int n);
    extern int my_mod(int x, int y);
    extern char *get_time();

    可以把接口文件看做三个部分,第一部分是定义要生成的模块名,就是上面的第一行,第二部分就是包含的头文件信息,第三部分就是指定导出的函数。由于接口文件的存在,c/c++源文件中一般不需要像导出dll一样需要声明导出函数,只需要保持原样就可以了。

    为了简化接口文件,如果头文件中的开放函数都可以导出的化,可以导出部分也可以用头文件代替,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* example.i */
    %module example
    %{
    #define SWIG_FILE_WITH_INIT
    /* Put header files here or function declarations like below */
    #include "header.h"
    %}

    %include "header.h"
  2. swig编译

    根据写好的接口文件,用swig进行编译。vs的工程可以加入.i文件后,在.i文件上设置属性用swig编译,不过最简单还是直接用命令行来编译,一般对c++文件编译命令如下:

    1
    swig c++ -python example.i

    编译生成两个文件,一个后缀名为.cxx的c++文件,一个后缀名为.py的python文件,各自从c++和python的角度声明导出信息。

  3. 用c++编译器编译动态连接库

    在windows项目中,我直接新建vs工程,用vs进行编译。

    • 新建空的vc++工程,设置类型为dll工程

    新建dll工程

    • 将前面的.cxx文件和.i文件添加如工程,并不使用预编译头设置

    • 将在香茗居属性中加入python的文件目录和库目录

    swig工程属性

    完成以上操作,就可以编译生成release的dll文件

  4. 测试python模块

    将dll文件名字修改为下划线开头加模块名,后缀为.pyd,如:_modelname.pyd,并将其复制到第二部生成的.py文件一起,注意:两个文件名除了.pyd文件是下划线开头,都一样。在python环境下导入模块测试。

    注意:在c++代码中如果用到动态连接库,编译成python模块后同样需要这些库,一般将这些库可编译好的python模块放在同一个目录下就可以了

其他功能和注意点

  1. 数据类型定义

    类等用户自定义类型需要包含在.i文件中,不过一般使用头文件全部代替就可以了,但是有些用typedef的基本类型数据可能并没有包含进来,如有些头文件是vs的环境中包涵好的,在swig用命令行执行时,无法加入vs的环境中的头文件,所以最好手动将其定义拷贝到.i文件中。

  2. 指针类型

    python中没有指针类型,如果c/c++导出的函数的中有参数或者返回值时指针时,python中就无法使用它了,因此在swig中,专门提供生成指针的方法。

    在.i文件中,添加如下代码(生成指向int的指针),可以在python中提供生成指针的方法,具体在python中的使用方法还需参考官方文档:http://www.swig.org/Doc3.0/SWIGDocumentation.html#Library_nn3

    1
    2
    3
    %include "cpointer. i"
    /* Create some functions for working with "int *" */
    %pointer_functions(int, intp);

    除了上面的pointer_functions外,swig中还提供了很多与指针和内存分配相关操作的底层方法,可以在python中使用。

  3. typemap类型映射

    类型映射是为了解决方便c/c++到python中参数转换的问题,最方便的一个应用就是c++返回的字符串指针时,可以用typemap将其映射到python的str类型上,这样在python中调用函数时,其返回值就成了str了。在.i文件中添加如下代码:

    1
    2
    3
    4
    %typemap(out) char* 
    {
    $result = PyString_FromString($1)
    }

    注意:如果char换成uchar,我尝试以上映射并没有成功,而且在swig中很不推荐函数返回指针,因为swig在转换后调用会导致内存泄漏,因此一般需要将这类函数传回的指针改成使用参数传回

  4. 使用numpy

    当c/c++函数会传回大量数据时,在python中使用numpy接收是最方便的,但是numpy并不是python的标准自带库,所以要在swig中支持对numpy的,需要修改.i文件,具体参考numpy的文档说明:https://docs.scipy.org/doc/numpy-1.10.0/reference/swig.interface-file.html

    假定c/c++函数头为double rms(double* seq, int n),修改.i文件,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    %{
    #define SWIG_FILE_WITH_INIT
    #include "rms.h"
    %}

    %include "numpy.i"

    %init %{
    import_array();
    %}

    %apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
    %include "rms.h"

    上面的numpy.i文件可以直接在github上找到下载下来,放在源文件目录下,在vs工程属性中加入numpy头文件的路径,如我的路径为:D:\WinPython\python-3.4.4\Lib\site-packages\numpy\core\include\

    要注意的是,在python中使用该rms函数时,参数只需要传入一个整形值,表示numpy数组的长度,返回值为numpy的ndarray对象。

Compartir

python数据可视化简单操作小结

前言

python强大有一个很重要的原因是它有着数量庞大、涉及领域广泛且专业实用的第三方库,就我所接触到就有:

  • 数据分析、科学计算: numpy、scipy、pandas
  • 图像处理: OpenCV、PIL
  • 数据可视化: matplotlib
  • 网络: uillib2、scrapy、django
  • UI界面: pyqt、Thinker

知乎上更是有网友总结非常全面的总结。因此要使用python解决实际的项目,除了要熟悉python的基本语法外,还要查看一大堆第三方库的资料。每次查找资料是很费时间的,所以熟悉这些常用的第三方库的基本用法就显得很重要了,因为python很高的开发效率是它的优势,如果又因为这些原因有导致开发效率降低了,就完全达不到使用的目的了。

本篇文章仅仅对python的openCV、numpy和matplotlib这三个库的简单使用做些整理。

环境问题

  1. OpenCV的支持

    python的库多,而且又相互依赖,如OpenCV对numpy就严重依赖,所以选择发行版本WinPython或者Anacoda减少这方面的影响,但是即使是发行版一般OpenCV的也是没有带的。

    安装Python对OpenCV的支持一般方法是:将openCV库的“build\python\2.7\”目录中的cv2.pyd文件复制到python的“Lib\site-packages\”文件夹中。

    不过当前OpenCV仅仅提供2.7的支持,如果使用python3就只能下载非官方的支持,如下位Windows下python第三方库安装包整理得很全面得网站,在上面找到python和OpenCV对应的版本下载安装:

    Unofficial Windows Binaries for Python Extension Packages:http://www.lfd.uci.edu/~gohlke/pythonlibs/

    这个网站上提供的都是.whl为后缀名的文件,需要使用pip工具安装。安装方法为打开命令行界面,输入:

    1
    pip install 安装文件名
  2. pip工具的安装

    pip上python中安装第三方库最常用的工具,但是一般python也没有自带,需要手动安装。pip官方下载地址为:https://pypi.python.org/pypi/pip

    在上面下载pip的压缩包“pip-9.0.1.tar.gz”,解压后在解压后的文件夹中,打开命令行,执行:

    1
    python setup.py install

OpenCV的使用

OpenCV python版有官方文档介绍使用,是但比起C++,就显得很不不详细了。它可以提供快速入门指导,但是却无法提供其API结构更详细的说明,所以只能从C++对应的方法来推测。官方文档地址为:https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_tutorials.html

OpenCV的python版使用与在C++还是会稍有区别,其中最大不同就是使用了numpy来代替了C++中的mat。

  1. 读入图像

    使用imread函数代码,其返回的是一个numpy.ndarray类型的多维数组,记录图像的每个pixel的原始数据,使用这个返回值,可以直接使用numpy中的各种方法,很放方便的处理图像中任意像素点的原始数据,当然如果OpenCV已经又现成的方法,就可以不用手动使用numpy去处理了:

    1
    2
    import cv2
    imgdata = cv2.imread(fileName)
  2. 显示图像

    使用imshow显示图像,固定的操作,值得注意的是waitKey函数是阻塞的,在关闭窗口前程序不能向下运行。

    1
    2
    3
    4
    cv2.namedWindow('image', cv2.WINDOW_NORMAL)
    cv2.imshow('image',img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
  3. 保存图像

    使用imwrite函数保存图像到文件

    1
    cv2.imwrite('messigray.png',img)

以上为OpenCV对图像的基本操作,不过使用OpenCV的最大的好处实际上很方便的用到其强大的图像处理算法,所以对这些算法操作需要查看文档。

Numpy的使用

Numpy是python中最基础的科学计算类库,很多其他库都需要它的支持,正因为如此,Numpy的数组几乎成为了python中除基本数据结构外最重要的类型了。它最主要的目的是为python提供多维数据的类型。一般在python中,简单的序列操作,可以用list这个python内置的数据结构来实现,但是如果是大量数字类型的运算计算,使用Numpy不仅在执行速度会更高效,而且代码上会更简介。

对比list,numpy有如下特点:

  • numpy数组的size是固定的,改变其size将创建新的numpy数组
  • numpy数组的元素必须全部都是一样的
  • numpy数组是高度优化的,处理数学计算是高效且方便
  • numpy可以提供类似matlab类似的,将行或列或者是一个整体当作一个基本元素来进行计算操作
  1. ndarray类型

    ndarray是Numpy的基本对象,是表示多维数组的数据类型,它的属性有:

    • ndarray.ndim: 维度
    • ndarray.shape: 尺寸形状
    • ndarray.size: 所有元素的数量
    • ndarray.dtype: 元素的类型
    • ndarray.itemsize: 元素占字节数
    • ndarray.data: 所有元素集合
  2. 创建ndarray类型

    • 使用np.array函数: np.array([2,3,4])
    • 使用np.zeros函数,创建并将元素初始化为零: np.zeros((3,4))
    • 使用np.ones函数,创建并将元素初始化为一: np.ones((2,3,4), dtype=np.int16)
    • 使用np.empty函数,创建但不初始化: np.empty( (2,3) )
    • 使用np.arange函数,创建等差序列: np.arange( 10, 30, 5 )
  3. ndarray常用的方法

    • reshape: 改变形状
    • flatten: 将多维的数据整理成1维
    • astype: 讲多维数据中的元素的类型转换为指定类型
    • amin: 获取多维数据或者每一个行列的最小值
    • amax: 获取多维数组或者每一个行列的最大值
    • mean: 获取多维数组算数平均值或者每一个行列的平均值
    • std: 获取多维数组的标准差或每一个行列的标准差
    • var: 获取多维数组的方差或每一个行列的方差

matplotlib的使用

matplotlib是python中最常用的数据图标显示库,它类似与matlab能够画出多种类型的图标,官方网站为:http://matplotlib.org/index.html。但是由于matplotlib是能够化画的图标太多,推荐可以直接从例子上学习,地址为:http://matplotlib.org/gallery.html

  1. 导入模块

    一般使用matplotlib画图都是从matplotlib.pyplot模块开始:

    1
    import matplotlib.pyplot as plt
  2. 画曲线图

    使用plot函数可以画曲线图,plot函数有很多参数,最简单的就是只传入一个一维的数组,如下所示:

    1
    plt.plot(data)

    除了在plot函数设置参数来设置画图属性外,在plot函数后,仍然可以通过使用setp函数来设置所画图表的属性:

    1
    plt.setp(lines, color='r', linewidth=2.0)
  3. 显示图表和关闭图表

    显示图像很简单,直接使用show函数,可以显示前面所画的图表:

    1
    plt.show()
关闭图表同样简单,使用close函数即可,但是要注意的是在调用了show函数之后,程序是阻塞的状态,不会向下运行,所以在show函数使用close是没有用的。

1
plt.close()
  1. 子图表

    在一个窗口中显示多张图表,最常用的函数是subplot,使用subplot函数可以将整个窗口分割成几行几列,在每个小格子中放置一个图表,如下代码表示,将窗口分割成2行1列,并指定在第一个小格子中画图。

    1
    2
    plt.subplot(211)
    plt.plot(t2, np.cos(2*np.pi*t2), 'r--')

    如果希望子图像大小能够自己定制,如某子图像多占几个格子,则需要使用matplotlib.gridspec模块,使用GridSpec函数分割窗口,使用subplot函数传入所占格子的位置和多少,确定图表区域,后面在使用subplot函数返回值来画图。如下面的代码第三个图就占用了第二行的两列。

    1
    2
    3
    4
    5
    import matplotlib.gridspec as gridspec
    gs = gridspec.GridSpec(2,2)
    ax1 = plt.subplot(gs[0,0])
    ax2 = plt.subplot(gs[0,1])
    ax3 = plt.subplot(gs[1,0:])
  2. 调整子图表的位置和边距

    使用update函数可以调整,子图表的边距和位置

    1
    gs.update(left=0.05, right=0.98, top=0.98, bottom=0.05, wspace=0.1)

    另外一个简单的方法是,直接调用tight_layout函数,系统会根据实际情况自动调整到一个默认合适的位置。

    1
    plt.tight_layout()
  3. 画分布图

    使用hist函数,画统计分布图,如下代码,其中hist函数第二个参数表示将数组分组统计的分组数据多少,normed表示归一化;hist的返回值中n表示每个分组中数据出现的统计次数或者频率,bins表示每个分组的两个边界值。

    1
    n, bins, patches = hist(data, 256, normed=1, facecolor='g', alpha=0.75)
  4. 曲面图

    用matplotlib可以画三维图像,曲面图是一类比较简单的三维图,使用plot_surface函数可以画曲面图:

    1
    2
    3
    4
    5
    6
    from matplotlib import cm
    n, m = arrayData.shape
    x = np.arange(m)
    y = np.arange(n)
    X,Y = np.meshgrid(x,y)
    surf = self.axlist[2].plot_surface(X, Y, arrayData, cmap=cm.coolwarm, linewidth=0)

    注意以上代码,X和Y都是二维数组,表示底部平面的上的每一个点,获取方法为:通过np得到其行和列的一维数组,使用meshgrid生成。

Compartir

OpenNI使用介绍

OpenNI简介

OpenNI是PrimeSense公司联合其他公司制定的一个多语言、跨平台的规范,以此规范为基础为上层应用提供一个与视觉和音频传感器通信的统一的接口。目前主要应用在各种深度传感器中。

四大主要的接口类的简单介绍

  1. openni::OpenNI

    OpenNI类是应用使用OpenNI API的唯一入口,除此以外它还提供了访问设备、设备相关事件、版本以及出错信息的方法

  2. openni::Device

    对于一个sensor设备OpenNI会提供一个Device类,在生Device对象create前需要先对OpenNI进行initializ

  3. openni::VideoStream

    VideoStream类提供是设备中的一个视屏流封操作接口,需要依赖Device对象,而被VideoFrameRef对象所依赖。

  4. openni::VideoFrameRef

    VideoFrameRef是视频流属性和元数据的接口类

另外其他重要的类有:

  • Recorder类:提供将视屏流存储到文件中的接口
  • Listener类:检测OpneNI和视频流的个各种事件
  • DeviceInfo类:提供设备和设备事件的信息接口

OpenNI类

  1. 基本设备操作函数

    • OpenNI::initialize(), OpenNI环境的初始化,初始化成功后才能使用其他函数
    • OpenNI::enumerateDevices(), 系统中所有可用设备的枚举
    • OpenNI::shutdown(), OpenNI关闭
    • OpenNI::waitForAnyStream(), 等待指定视频流的数据更新,用于阻塞轮询
    • OpenNI::getExtendedError(), 获取错误信息
    • OpenNI::getVerion(), 获取版本信息
  2. 注册事件监听函数

    • OpenNI::addDeviceConnectedListener(), 注册监听设备连接事件
    • OpenNI::addDeviceDisconnectedListener(), 注册监听设备移除事件
    • OpenNI::addDeviceStateChangedListener(), 注册监听设备状态改变事件

    这些函数都另外有对应的remove函数,这些事件都指向一个OpenNI::DeviceInfo类。

Devices类

既可以连接的物理设备,也可以连接到ONI文件。

  1. 基本操作函数

    • Devices::open(), 打开设备
    • Devices::close(), 关闭设备
    • Devices::isValid(), 判读是否是实际的设备
    • Devices::getDeviceInfo(), 获取设备信息类
    • Devices::hasSensor(), 是否有指定的传感器
  2. 参数配置函数

    • 设置Registration标定配置: setImageRegistrationMode()
    • 设置同步FrameSync配置: setDepthColorSyncEnabled()
    • 其他配置设置: setProperty()

VideoStream类

  1. 基本操作函数

    • VideoStream::create(), 创建流,需要一个已经初始化的Devices对象
    • VideoStream::start(), 数据流传输开始
    • VideoStream::stop(), 数据流停止
    • VideoStream::readFrame(), 读取一帧数据,用于轮询阻塞读取
    • VideoStream::onNewFrame(), 新的一帧数据事件函数,需要其他设置

VideoStreamRef类

最常见的作用是作为readFrame()函数的操作,接收数据。

  1. 基本操作

    • VideoStreamRef::getData(), 获取原始数据指针
    • VideoStreamRef::getVideoMode(), 获取视屏模式
    • VideoStreamRef::getVideoMode().getResolutionX()、VideoStreamRef::getVideoMode().getResolutionY(), 获取视屏分辨率
    • VideoStreamRef::getSensorType(), 获取sensor类型

DeviceInfo类

  • DeviceInfo::getUri(), 获取设备Uri

参考

  1. 《OPENNI PROGRAMMER’S GUIDE》
Compartir