智能机器人(73):ROS的TF-transform(02)

9. tf frame

抽象层面上,变换其实就是一种“偏移”(包括平移和旋转),代表了不同坐标系之间的变换和旋转。
在ROS中坐标系是3维的,用右手表示的,即食指X轴指向前方,其余四指Y轴指向左方,拇指Z轴指向上方。
许多ROS软件包都要使用TF软件功能包,以机器人识别的变换树的形式发布。

在tf中,两个坐标系的关系(也就是转换信息),用一个6自由度的相对位姿表示:
平移量(translation) + 旋转量(rotation)。类型分别是Vector3 和Quaternion 。
简单说tf类中定义了两个刚体之间的旋转与平移矩阵,并且重载了乘法运算符,这样就可以通过相乘两个tf来沿着tf树的方向求末段执行器相对世界坐标的位置与方向.

Let’s choose the “base_link” coordinat frame as the parent. This means the transform associated with the edge connecting “base_link” and “base_laser” should be (x: 0.1m, y: 0.0m, z: 0.2m).
For example beow shows that object in base-laser frame is (x: 0.3m, y: 0.0m, z: 0.0m) ,
例如:

and same object in base-link frame is (x: 0.4m, y: 0.0m, z: 0.2m).

9.1 TF软件包,最主要的是坐标转换的生产和消费。

TF基本原理是TransformBroadcaster(tb)生产信息,TransformListener(tl)消费信息。
所有的tb会发布某一特定的parent到child的变换,而所有tl会收到所有的这些变换,然后tl利用一个 tfbuffercore的数据结构维护一个完整的树结构及其状态。
因此,只要是一个tl,就要跟所有tb建立连接,就要收取所有的tf消息,来维护一颗完整的树并且搜索这棵树从而找到一条变换的路径,然后乘呀乘,每次都要如此。

广播树:
TF树结构的建立和维护是靠tf提供的tfbroadcastor的sendtransform接口。
tb发布一个从已有的parent frame到新的child frame的坐标系变换时,这棵树就会添加一个树枝,之后就是维护。
监听树:
用tf的tflisener监听某一个指定的从一个a frame到b frame的变换即可,当然前提是树上的树杈能把a,b联通。
这个变换是a frame->到b frame的变换,也表示了b frame在a frame的描述,也代表了一个在b frame里的点的坐标变换成在a frame里的坐标的坐标变换。

还是生产和消费好:

生产:
TransformBroadcaster有一个方法sendTransform发布坐标系1到坐标系2的旋转算子。
消费:
TransformListener有一个方法transformPoint用来变换坐标。
TransformListener有一个方法lookupTransform直接使用算子。

911. 生产
TransformBroadcaster有一个方法sendTransform发布坐标系1到坐标系2的旋转算子。

tf::TransformBroadcaster br;
tf::Quaternion q;//(x,y,z,w)
tf::Vector3 p; //(x,y,z)
tf::Transform ts; //(Quaternion(x,y,z,w), tf::Vector3(x,y,z))
tf::StampedTransform(ts, ros::Time::now(), “world”,”t1″)

那么这个这个发布:
br.sendTransform( tf::StampedTransform(ts, ros::Time::now(), “world”,”t1″) );
就是表明了t1坐标系在world坐标系的信息,评议是p:xyz适量所描述的那样,旋转是q:xyzw四元数所描述的那样。
也是说,word坐标系–>变换到t1坐标系的旋转算子就是【q,p】。
或者说,已知t1坐标系中的某点pont,其在world坐标系中的坐标就是【q,p】乘point。

再例如常见的,base_laser的激光安装在base_link的前方x=0.1m、左右正中心y=0、上方z=0.2m处的发布:
br.sendTransform( tf::TransformStamped ( tf::transform( tf::Quaternion(0,0,0,1), tf::Vector3(0.1,0,0.2), ros::time()::now(), “base_link”, “base_laser”) );

那么就是表明了base_laser激光坐标系base_link基坐标系的信息,平移是(0.1, 0.0, 0.2)适量所描述的那样, 旋转是(0,0,0,1)四元数所描述的那样。
也是说,base_link坐标系–>变换到base_laser坐标系的旋转算子就是【q,p】。
或者说,由base_laser激光坐标系扫描的数据点pont,其在base_link基坐标系中的坐标就是【q,p】乘point。

=== 例子 ===
某个机器人只有一个基本的移动机体,以及,挂在机体上方的激光。
这就定义了两个坐标系:一个对应于机体中心点的坐标系,一个对应于扫描仪中心的坐标系,分别取名为“base_link”和“baser_laser。

假设已经从传感器获取了一些数据,以一种代表了障碍物体到扫描仪中心点的距离的形式给出。
换句话说,已经有了一些“base_laser”坐标系的数据。
现在,期望通过这些数据,来帮助机器人避开物理世界的障碍物。
此处成功的关键是,需要一种方式,把传感器扫描的数据,从“base_laser”坐标系转换到“base_link”坐标系中去。
本质上,就是定义一种两个坐标系的关系。

为了定义这种关系,假设激光是挂在机体中心的前方10cm,高度20cm处,可以理解成传感器到机体中心的向量(0.1,0.0,0.2),这就等于给了一种转换的偏移关系。
具体来说,就是,从传感器到机体的坐标转换关系应该为(x:0.1m,y:0.0m, z:0.2m),相反的转换即是(x:-0.1m,y:0.0m,z:0.2m)。

基于该简单的例子的需求,需要创建两个frame,一个“base_link”,一个是“base_laser”。
为了定义两者的关系,首先需要决定谁是parent,谁是child。
时刻记得,由于tf假设所有的转换都是从parent到child的,因此谁是parent是有差别的。
这里选择“base_link”坐标系作为parent,其他的传感器等,都是被添加进robot的,对于“base_link”和“base_laser”他们来说,是最适合的。
这就意味着转换关系的表达式应该是(x:0.1m,y0.0m,z:0.2m)。

关系建立后,在收到“base_laser”的数据到“base_link”的转换过程,就可以是简单的调用tf库即可完成。
机器人就可以利用这些信息,在“base_link”坐标系中,就可以推理出传感器扫描出的数据,并可安全的规划路径和避障等工作。
以上层来描述“base_laser”坐标系的点,来转换到”base_link”坐标系。

9.2 相应代码

921. 生产
TransformBroadcaster有一个方法sendTransform发布坐标系1到坐标系2的旋转算子。
922 消费方法一
TransformListener有一个方法transformPoint用来变换坐标。
923 消费方法二
TransformListener有一个方法lookupTransform直接使用算子。

921. 生产
TransformBroadcaster有一个方法sendTransform发布坐标系1到坐标系2的旋转算子。

ros::NodeHandle n;
ros::Rate r(100);

tf::TransformBroadcaster br;
//create a TransformBroadcaster object to send base_link → base_laser transform

while(n.ok()){
br.sendTransform( tf::TransformStamped ( tf::transform( tf::Quaternion(0,0,0,1), tf::Vector3(0.1,0,0.2), ros::time()::now(), “base_link”, “base_laser”) );
r.sleep();
}

HINTS:
broadcaster.sendTransform() Sending a transform with a TransformBroadcaster requires five arguments need 4 params:
PARAM1 = tf::StampedTransform( PARAM1=tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)),
PARAM2=ros::Time::now(),
PARAM3=”base_link”,
PARAM4=”base_laser”
PARAM1:
First, we pass in the rotation transform, which is specified by a btQuaternion for any rotation that needs to occur between the two coordinate frames.
In this case, we want to apply no rotation, so we send in a btQuaternion constructed from pitch, roll, and yaw values equal to zero.
Second, a btVector3 for any translation that we’d like to apply. We do, however, want to apply a translation, so we create a btVector3 corresponding to the laser’s x offset of 10cm and z offset of 20cm from the robot base.
And, we need to give the transform being published a timestamp, we’ll just stamp it with ros::Time::now().
And, We need to pass the name of the parent node of the link we’re creating, in this case “base_link.”
And, we need to pass the name of the child node of the link we’re creating, in this case “base_laser.”

922 消费方法一
TransformListener有一个方法transformPoint用来变换坐标。
这是建立了一个定时器,定时把base_laser坐标系的数据转换到base-link坐标系里使用。

#include <tf/transform_listener.h>
// 作用: 将“base_laser”坐标系的点,变换到“base_link”坐标系中。

ros::NodeHandle n;
tf::TransformListener listener(ros::Duration(10));

//transform a point once every second
ros::Timer timer = n.createTimer(ros::Duration(1.0), boost::bind(&transformPoint, boost::ref(listener)));
ros::spin();
//这个函数将会以ros::Timer定义的周期,作为一个回调函数周期调用。目前周期是1s

void transformPoint(const tf::TransformListener& listener)
//创建一个虚拟点,作为geometry_msgs::PointStamped。
//消息名字最后的”Stamped”的意义是,它包含了一个头部,允许我们去把时间戳和消息的frame_id相关关联起来。
geometry_msgs::PointStamped laser_point;

//关联frame_id也就是将node定义为frame_id
laser_point.header.frame_id = “base_laser”;

//设置laser_point的时间戳为ros::time(),即是允许我们请求TransformListener取得最新的变换数据
laser_point.header.stamp = ros::Time();

//伪造几个虚拟的点,即所需要变换过去的数据
laser_point.point.x = 1.1;
laser_point.point.y = 2.2;
laser_point.point.z = 3.3;

geometry_msgs::PointStamped base_point;

//通过TransformListener对象,调用transformPoint(),填充三个参数来进行数据变换
listener.transformPoint(“base_link”, laser_point, base_point);
//arg1,代表想要变换到的目标坐标系的名字。
//arg2,填充需要变换的原始坐标系的点对象。
//arg3,目标坐标系的点对象。
… …

ROS_INFO(“base_laser: (%.2f, %.2f. %.2f) —–> base_link: (%.2f, %.2f, %.2f) at time %.2f”,
laser_point.point.x, laser_point.point.y, laser_point.point.z,
base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec());
//THEN base_point should be (1.1, 2, 3.2).

923 消费方法二
TransformListener有一个方法lookupTransform直接使用算子。

ros::Publisher base_laser_vel = node.advertise(“base_laser/cmd_vel”, 10); // to control

tf::TransformListener listener;

ros::Rate rate(10.0);
while (node.ok()){
try {
listener.waitForTransform(“/base_link”, “/base_laser”, ros::Time(0), ros::Duration(10.0) );
listener.lookupTransform(“/base_link”, “/base_laser”, ros::Time(0), transform );
} catch (tf::TransformException &ex) {
ROS_ERROR(“%s”,ex.what());
}

geometry_msgs::Twist vel_msg;
vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(),transform.getOrigin().x());
vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) + pow(transform.getOrigin().y(), 2));
base_laser_vel.publish(vel_msg);
//while transform of t2 in t1 is iavailable then cal new linear and angular data for turtle2
//based on its distance and angle from turtle1
//New velocity is published in the topic “turtle2/cmd_vel”, t2 = base_laser
//HINTS: turtle2 is the dest child we needed, tutlr1 is the source parent we based.

rate.sleep();
}
这个是一旦等到”/base_link”, “/base_laser的变换可用,就取出这个transform,根据base_laser的数据信息控制base_link行为。

9.3. 发布变换的几种方式

931 常见方式的代码:

#include<tf/transform_broadcaster.h>
void posecallback(turtlesim::PoseConstPtr &msg)

static tf::TransformBroadcaster br;

//创建Transform存储变换相关信息,它包含旋转和位移的信息,有一个向量Vector3表示坐标,以及四元组表示方向。
tf::Transform transform;

//创建四元组
tf::Quaternion q;
//调用q的方法,绕着z轴旋转,有下面参数形式
q.setRPY(0,0,msg->theta);
//填充transform
transform.setOrigin(tf::Vector3(msg->x,msg->y,0));
transform.setRotation(q);

//创建一个transform::stamped对象,广播出去
br.sendTransform(tf::StampedTransform(transform,ros::Time::now(),”world”,”turtle1″));


}
解释直接在代码了。

932 其实变换可以直接用四元数:
transform.setOrigin( tf::Vector3(0.0, 0.0, 0.0) );
transform.setRotation( tf::Quaternion(0, 0, 0, 1) );
br.sendTransform(tf::StampedTransform(transform,ros::Time::now(), “world”, “turtle1″)
这三句话分别做了以下工作:
设置turtle1在world坐标系下的坐标原点
设置turtle1相对于world的旋转角度,这里用四元数表示
发送变换信息

933 上面这种四元数表示旋转角度的方式不太直观,写代码的时候一般不想将旋转变换换算成四元数,
大部分采用前面代码中的下方法写这个变换:
tf::Quaternion q;
q.setRPY(0, 0, msg->theta);
transform.setOrigin( tf::Vector3(msg->x, msg->y, 0.0) );
transform.setRotation(q);
br.sendTransform(tf::StampedTransform(transform,ros::Time::now(),”world”,”turtle1″));
这三句话,直接用RPY(分别对应绕XYZ轴旋转角度)来设置旋转变换了

934 还有一种使用静态变换:
static_transform_publisher工具的功能是发布两个参考系之间的静态坐标变换,两个参考系一般不发生相对位置变化。
static_transform_publisher x y z yaw pitch roll frame_id child_frame_id period_in_ms
static_transform_publisher x y z qx qy qz qw frame_id child_frame_id period_in_ms
以上两种命令格式,需要设置坐标的偏移和旋转参数。
偏移参数都使用相对于xyz三轴的坐标位移。
而旋转参数,第一种命令格式使用以弧度为单位的 yaw/pitch/roll三个角度,第二种命令格式使用四元数表达旋转角度。
发布频率以ms为单位,一般100ms比较合适。
Publish a static coordinate transform to tf using an x/y/z offset in meters and yaw/pitch/roll in radians. (yaw is rotation about Z, pitch is rotation about Y, and roll is rotation about X). The period, in milliseconds, specifies how often to send a transform. 100ms (10hz) is a good value.

935 br.sendTransform(tf::StampedTransform(transform,ros::Time::now(), “world”, “turtle1”)
这种发布都是完成一个工作, 表明t1坐标系相对于world坐标系的位置关系,是旋转角度或四元数所描述的那样。
这样,如果t1坐标系里面有激光或里程计发布数据, 位于world和t1坐标系之外的其他坐标系,即可获得任何可能的坐标系下面的激光或历程数据。

智能机器人(71):ROS的TF-transform(01)

PART I ====== 变换矩阵
PART II ====== 欧拉角
PART III ====== 四元数
PART IV ====== 坐标系:空间描述与变换

PART I ====== 变换矩阵

1. 二维的平移操作

一般,可以将平移、旋转、缩放操作用矩阵表示。但是,使用2×2的变换矩阵是没有办法描述平移、旋转变换中的二维平移操作的;同样,3×3矩阵也没法描述三维的平移操作。
所以,为了统一描述二维中的平移、旋转、缩放操作,需要引入3×3矩阵形式;同理,使用4×4的矩阵才能统一描述三维的变换。
因此,用齐次坐标(Homogeneous coordinates)描述点和向量。

对于二维平移,如下图所示:

P点经过x和y方向的平移到P’点,可以得到:
x′=x+tx
y′=y+ty
由于引入了齐次坐标,在描述二维坐标的时候,使用(x,y,w)的方式(一般w=1)。
于是,可以写成下面矩阵形式:
x′=1,0,tx—–x
y′=0,1,ty—–y
w’=0,0,1—–w
也就是说平移矩阵是
1,0,tx
0,1,ty
0,0,1

2. 二维的绕原点旋转操作

首先明确,在二维中,旋转是绕着一个点进行,在三维中,旋转是绕着某一个轴进行。

最简单的,二维中的旋转是绕着坐标原点进行的旋转,如下图所示,点v 绕坐标原点旋转θ 角,得到点v’:

假设,v点的坐标是(x, y) ,那么经过推导可以得到,v’点的坐标(x’, y’)
增加一些中间变量: 设原点到v点的距离为r,设原点到v点的向量与x轴的夹角为ϕ,则有:
x=rcosϕ
y=rsinϕ

x′=rcos(θ+ϕ)
y′=rsin(θ+ϕ)
通过三角函数展开得到
x′=rcosθcosϕ−rsinθsinϕ
y′=rsinθcosϕ+rcosθsinϕ
带入x和y的表达,得到:
x′=xcosθ−ysinθ
y′=xsinθ+ycosθ
写成矩阵形式,为:
x′ = [cosθ, -sinθ]—-x
y′ = [sinθ, cosθ]—–y
引入了齐次坐标,扩展为3×3形式,则绕原点旋转的齐次矩阵为:
cosθ, -sinθ,0
sinθ, cosθ,0
0,0,1

3. 二维的绕任意点旋转操作

绕原点旋转,是二维旋转最基本的情况。当需要进行绕任意点旋转时,可以转换到这种基本情况的绕原点旋转,思路如下:
1. 首先将这任意旋转点平移过来,到坐标原点处;
2. 执行最简单的绕坐标原点的旋转;
3. 最后将这任意旋转点平移回去,到原来的位置。
也就是说在处理绕任意点旋转的情况下,需要额外在开头和末尾执行两次平移的操作,中间才是真正的旋转。

假设平移的矩阵是T(x,y),也就是说我们需要得到的坐标 v’ = T(x,y)*R*T(-x,-y)
(采用列坐标描述点的坐标,因此是左乘:首先执行T(-x,-y)… 依次向左进行。)
这样就很容易得出二维中绕任意点旋转操作的旋转矩阵了,即只需要把三个矩阵乘起来即可:
M=
⎡⎣⎢100010txty1⎤⎦⎥∗⎡⎣⎢cosθsinθ0−sinθcosθ0001⎤⎦⎥∗⎡⎣⎢100010−tx−ty1⎤⎦⎥=⎡⎣⎢cosθsinθ0−sinθcosθ0(1−cosθ)tx+ty∗sinθ(1−cosθ)ty−tx∗sinθ1⎤⎦⎥

对照平移和旋转的矩阵可以看出:
这个3×3矩阵的前2×2部分是和旋转相关的,
第三列是与平移相关。

4. 三维的基本旋转

首先明确,在二维中旋转是绕着一个点进行,在三维中就是绕着某一个轴进行。
三维的方向,可以采用右手坐标系:同时旋转角度的正负,也遵循右手坐标系:如下图所示:

一个三维的旋转,可以转换为绕基本坐标轴的旋转。
因此,首先要讨论一下绕三个坐标值x、y、z的旋转。

4.1. 绕X轴的旋转
在三维中,一个点P(x,y,z)绕x轴旋转θ角,到点P’(x’,y’,z’)。
由于是绕x轴进行的旋转,因此x坐标保持不变,在y和z组成的yoz平面上进行一个二维旋转(y轴类似于二维旋转中的x轴,z轴类似于二维旋转中的y轴),于是降维后有:
x′=x
y′=ycosθ−zsinθ
z′=ysinθ+zcosθ

4.2. 绕Y轴旋转
绕Y轴的旋转和绕X轴的旋转类似,Y坐标保持不变,除Y轴之外,ZOX组成的平面进行一次二维的旋转(Z轴类似于二维旋转的X轴,X轴类似于二维旋转中的Y轴),同样有:
x′=zsinθ+xcosθ
y′=y
z′=zcosθ−xsinθ
注意这里是ZOX,而不是XOZ,观察右手系的图片可以了解到这一点。

4.3. 绕Z轴旋转
与上面类似,绕Z轴旋转,Z坐标保持不变,xoy组成的平面内正好进行一次二维旋转
(和上面旋转的情况完全一样)自由度

4.4. 三维旋转操作总结
可以将绕X、Y和Z坐标轴的旋转矩阵分别记为 Rx(α),Ry(β),Rz(θ),则有:

5. 三维的绕任意轴旋转

绕任意轴的三维旋转,可以使用类似二维的绕任意点旋转一样,将旋转分解为一些列基本的旋转。
对于点P,绕任意向量u,旋转角度θ,得到点Q,如果已知P点和向量u如何求Q点的坐标?如下图所示:

可以把向量u进行一些旋转,让它与z轴重合,之后旋转P到Q就作了一次绕Z轴的三维基本旋转,然后,再执行反向的旋转,将向量u变回到它原来的方向,也就是说需要进行的操作如下:
1. 将旋转轴u绕x轴旋转至xoz平面
2. 将旋转轴u绕y轴旋转至于z轴重合
3. 绕z轴旋转θ角 !
4. 执行步骤2的逆过程
5. 执行步骤1的逆过程

5.0. 原始的旋转轴u如下图所示:

5.1. 第1、2、3步骤如下图所示:



5.2.分步解析
步骤1将向量u旋转至xoz平面的操作是一个绕x轴的旋转操作,步骤2将向量u旋转到与z轴重合,
第1、2步骤的示意图如下:

作点P在yoz平面的投影点q,q的坐标是(0, b, c),原点o与q点的连线oq和z轴的夹角就是u绕x轴旋转的角度。通过这次旋转使得u向量旋转到xoz平面(图中的or向量)
【步骤1】
过r点作z轴的垂线,or与z轴的夹角为β, 这个角度就是绕Y轴旋转的角度,通过这次旋转使得u向量旋转到与z轴重合
【步骤2】

步骤1中绕x轴旋转的是一次基本的绕x轴的三维旋转,旋转矩阵是:(注意α角度是绕x旋转的正向的角度)
旋转矩阵(记作 Rx(α))为:
在完成步骤1之后,向量u被变换到了r的位置,继续步骤2的操作,绕y轴旋转负向的β角,经过这次变换之后向量u与z轴完全重合,这一步也是执行的一次绕Y轴的基本旋转,
旋转矩阵(记作 Ry(−β))为:

在完成前面两个步骤之后,u方向和z轴完全重合,因此执行旋转θ角,执行的是一次绕z轴的基本三维旋转(记作 R(θ))。

最后两步骤,是前面1和2的逆操作,也就是绕Y轴旋转β,和,绕X轴旋转−α,这两个矩阵分别记作 Ry(β) 和 Rx(−α)。

最终得到绕任意轴u旋转的旋转矩阵是:
M=Rx(−α)Ry(β)Rz(θ)Ry(−β)Rx(α)
(因为使用的列向量,因此执行的是左乘,从右往左)

(注意:上面的(u,v,w),对应向量(a,b,c) 。)

如果向量是经过单位化的(单位向量),那么有a2+b2+c2=1,可以简化上述的公式,得到:

PART II ====== 欧拉角

6. 欧拉角

上面讨论了绕三条坐标轴旋转的旋转矩阵,旋转矩阵C 一般形式为:
【c11,c21,c31】
【c12,c22.c32】
【c13,c23,c33】
(这里没有用齐次坐标)
(直角坐标系的三个坐标轴方向的单位向量,实际上是一组标准正交基,旋转矩阵C 是一个正交矩阵。
所以, 旋转矩阵表面上看起来有 9 个参数,实际上只有三个是独立的,另外6个有约束。)

该旋转矩阵C 的三个列向量,实际对应着,原坐标系三个坐标轴方向的单位向量在旋转后的新坐标系下的坐标。

为了更直接地指出这三个独立参数,欧拉(Euler)证明了如下事实:
任何一个旋转都可以由连续施行的三次绕轴旋转来实现,这三次绕轴旋转的旋转角就是三个独立参数,称为欧拉角。
欧拉角之所以可以用来描述旋转是来自于欧拉旋转定理:任何一个旋转都可以用三个绕轴旋转的参数来表示。

定义一个欧拉角,需要明确的内容包括:
1. 三个旋转角的组合方式(是xyz还是yzx还是zxy)
2. 旋转角度的参考坐标系统(旋转是相对于固定的坐标系 还是相对于自身的坐标系)
3. 使用旋转角度是左手系还是右手系
4. 三个旋转角的记法
不同人描述的欧拉角的旋转轴和旋转的顺序都可能是不一样的。
当使用其他人提供的欧拉角的实现时,需要首先搞清楚他用的是那种约定。
根据绕轴旋转的顺序不同,欧拉角的表示也不同。

关于描述坐标系{B}相对于参考坐标系{A}的姿态有两种方式。
* 第一种是绕固定(参考)坐标轴旋转:
假设开始两个坐标系重合,先将{B}绕{A}的X轴旋转γ,然后绕{A}的Y轴旋转β,最后绕{A}的Z轴旋转α,就能旋转到当前姿态。
可以称其为X-Y-Z fixed angles或RPY角(Roll, Pitch, Yaw)。
* 另一种姿态描述方式是绕自身坐标轴旋转:
假设开始两个坐标系重合,先将{B}绕自身的Z轴旋转α,然后绕Y轴旋转β,最后绕X轴旋转γ,就能旋转到当前姿态。
称其为Z-Y-X欧拉角,由于是绕自身坐标轴进行旋转。

ROS的TF是前者

6.1 常见的欧拉角表示有 Yaw-Pitch-Roll (Y-X-Z顺序),通过下面的图片可以形象地进行理解。
Yaw(偏航):欧拉角向量的y轴
Pitch(俯仰):欧拉角向量的x轴
Roll(翻滚): 欧拉角向量的z轴

6.2 ROS的TF-frame里,
采用欧拉角RPY,RPY指的是绕固定坐标系xyz旋转。
Roll(滚转角)Pitch(俯仰角)Yaw(偏航角)分别对应绕XYZ轴旋转。

Roll:横滚

Pitch: 俯仰

Yaw: 偏航

设Roll、Yaw 、Pitch 三个角度分别为 φ、ψ 、θ,那么利用欧拉角进行旋转对应的旋转变换矩阵为:
【cosψ cosθ−sinψ cosφ sinθcosψ sinθ+sinψ cosφ】
【cosθsinψ sinφ−sinψ cosθ−cosψ cosφ sinθ−sinψ sinθ+cosψ 】
【cosφ cosθcosψ sinφsinφ sinθ−sinφ cosθcosφ】
实际上 Roll/Pitch/Yaw的旋转就分别对应着前面给出的旋转矩阵 Rx(),Ry(),Rz(),上面矩阵就是这三个矩阵的复合。

6.3
旋转角的记法
顺序——-飞行器———-望远镜——符号——角速度
第一——heading——-azimuth——–θ——–yaw
第二——attitude——elevation——ϕ——–pitch
第三——bank———–tilt————-ψ——–roll

6.4
欧拉角的好处是简单、容易理解,但使用它作为旋转的工具有严重的缺陷—万向节死锁(Gimbal Lock)。万向节死锁是指物体的两个旋转轴指向同一个方向。
实际上,当两个旋转轴平行时,万向节锁现象发生了,换句话说,绕一个轴旋转可能会覆盖住另一个轴的旋转,从而失去一维自由度。

PART III ====== 四元数

7. 连续的旋转

假设对物体进行一次欧拉角描述的旋转,三个欧拉角分别是(a1,a2,a3);之后再进行一次旋转,三个欧拉角描述是(b1,b2,b3);那么能否只用一次旋转(欧拉角描述为(c1,c2,c3)),来达到这两次旋转相同的效果呢?

这样是非常困难的,不能够仅仅使用(a1+b1,a2+b2,b3+b3)来得到这三个角度。
一般来说,需要将欧拉角转换成前面的旋转矩阵或者后面的四元数来进行连续旋转的叠加计算,之后再转换回欧拉角。
但是这样做的次数多了可能会引入很大的误差导致旋转结果出错。比较好的方案是直接使用旋转矩阵或四元数来计算这类问题。

四元数的一个重要应用是用它来描述三维旋转,四元数从某种意义上来说是四维空间的旋转,难以想象,了解它的结论和使用场景更加重要。

7.1
四元数的由来和复数很很大的关系,因此首先讨论一下关于复数的内容。复数中一个比较重要的概念是共轭复数,将复数的虚部取相反数,得到它的共轭复数:
z=a+biz∗=a−bi

当使用i去乘以一个复数时,把得到的结果绘制在复平面上时,发现得到的位置正好是绕原点旋转90度的效果。

于是可以猜测,复数的乘法,和,旋转之间应该有某些关系,例如:
定义一个复数q , 使用q作为一个旋转的因子
q=cosθ+isinθ

写成矩阵的形式是:
a′–[cosθsinθ]—–[a]
b′–[−sinθcosθ]—-[b]
这个公式正好是二维空间的旋转公式,当把新的到的(a′+b′i)绘制在复平面上时,得到的正好是原来的点(a+bi)旋转θ角之后的位置

7.2
既然使用复数的乘法可以描述二维的旋转,那么拓展一个维度是否能表示三维旋转呢?这个也正是四元数发明者William Hamilton最初的想法,也就是说使用
z=a+ib+jc
i2=j2=−1
但是很遗憾 三维的复数的乘法并不是闭合的。也就是说有可能两个值相乘得到的结果并不是三维的复数。
William Hamilton终于意识到自己所需要的运算在三维空间中是不可能实现的,但在四维空间中是可以的。

四元数是另一种描述三维旋转的方式,四元数使用4个分量来描述旋转,四元数可以写成下面的方式:
q=xi+yj+zk +w
为了方便表示为
q=(x,y,z,w)=(v ,w)
其中v是向量,w是实数。
模为1的四元数称为单位四元数(Unit quaternions)。

注意:
这里四元数的表示形式和齐次坐标一样,但是它们之间没什么关系。
四元数常常用来表示旋转,将其理解为“w表示旋转角度,v表示旋转轴”是错误的,应“w与旋转角度有关,v与旋转轴有关”。

7.3
两四元数相加
A(a+bi+cj+dk) + B(e + fi + gj + hk) = C【 (a+e) + (b+f)i + (c+g)j + (d+h)k 】
两个四元数相减
(sa,va) – (sb,vb) = (sa-sb,va-vb)

7.4 构造四元数
欧拉定理告诉任意三维旋转都可以使用一个旋转向量和旋转角度来描述。因此四元数往往是使用旋转轴和旋转角来构造的:

a)绕向量u,旋转角度θ,构造四元数
u = (ux,uy,uz)=uxi+uyj+uzk
q = exp[θ/2(uxi+uyj+uzk)]
= cosθ/2 + (uxi+uyj+uzk)sinθ/2
所以,可以用一个四元数q=((x,y,z)sinθ/2, cosθ/2) 来执行一个旋转。

或者表述为,
如果有单位四元数 q = (u ⋅sinθ/2, cosθ/2) 的形式,那么该单位四元数可以表示绕轴 u 进行 θ 角的旋转。

b)从一个向量旋转到另一个向量,构造四元数

c)从四元数获取旋转矩阵和旋转角
设四元数是q = xi+yj+zk+w,那么旋转角度angle和旋转轴(a,b,c):
angle = 2 * acos(w)
a = x / sqrt(1-w*w)
b = y / sqrt(1-w*w)
c = z / sqrt(1-w*w)

PART IV ====== 坐标系:空间描述与变换

8. 刚体的姿态描述

刚体在空间中具有6个自由度,因此可以用6个变量描述一个刚体在空间中的位姿:
(x,y,z,alpha,beta,gamma)

这牵扯到两个坐标系:全局坐标系 和 固连在刚体上的本地坐标系。
而对应的坐标变换,也就是指同一空间向量相对于这两个坐标系的坐标之间的相互转换关系。

旋转矩阵与坐标系之间的坐标变换没有任何关系。
旋转矩阵所描述的,是坐标系的旋转运动,也就是怎样把全局坐标系旋转到本地坐标系上去的。
或者说,怎样把一个向量(坐标系其实就是三个向量)旋转到新的位置。
注意,这里说的向量旋转,是发生在一个坐标系下的事情。

坐标系旋转角度θ, 则等同于将目标点围绕坐标原点反方向旋转同样的角度θ。

8.1 可以推导两个坐标系之间坐标变换的方法。

对于空间向量P,在全局坐标系下坐标为(x0,y0,z0), 在局部坐标系下坐标为(x1,y1,z1),那么:
P = (i,j,k)(x0,y0,z0) = (i′,j′,k′)(x1,y1,z1) = R∗(i,j,k)(x1,y1,z1) = (i,j,k)∗R(x1,y1,z1)

对比左右两边,可以得出结论:
(x0,y0,z0)T = R∗(x1,y1,z1)T
POS_global = R_g−>l ∗ POS_local

即,点在全局的坐标,等于全局坐标系到局部坐标系的转换矩阵,乘以点在局部的坐标。

8.2 通用的空间描述与变换
更广泛的,有两个坐标系A和B,B坐标系中有一个点P,如何把B坐标系中的P映射到A坐标系呢,这涉及到空间描述与变换。
Ap表示p在A坐标系中的表达,Bp表示p在坐标系B中的表达。Aborig表达B坐标原点orig在A坐标系, 是平移量。
要想将Bp转换为A坐标系中的表达Ap,就需要乘以旋转矩阵R_A->B,是旋转量。
将B坐标系中的p点在A坐标系中进行表达:
Ap = R_A->B * Bp + Aboirg
中间的表达式可以成为旋转算子。
则各旋转矩阵可以里哟写成齐次坐标的形式如下
Ap = M * Bp

即,点在A的坐标,等于A坐标系到B坐标系的转换矩阵,乘以点在B的坐标。

8.3 具体到ROS的tf
有三个坐标系,一个世界坐标系world,已知坐标系known,一个未知坐标系query

要求得未知坐标系在世界坐标系当中的表达,可以用旋转算子来表示:

就说是: 未知坐标系在世界坐标系中的旋转算子 = 未知坐标系在已知坐标系中的旋转算子 * 已知坐标系在世界坐标系中的旋转算子

Python+Qt+OpenCV界面(03)

PyQt 作为 Python 和 Qt 的bridge, 可以运行于所有平台。

1. 绑定

使用Python的getter/setter这种property, 可以把控件属性绑定到python变量。
例如,ui界面有某edit,python代码有某变量var,怎么实现:ui界面edit内容改变则变量var即更新、变量var改变则ui界面edit内容即更新?

两个方式:
* 第一个, 界面改变更新变量可以通过signal/slot实现,例如:
myQLine.textChanged.connect(self.myVAR)
但是反过来麻烦,因为myVAR只是个plain object,python没有内建的C#那样的实现方式。

* 使用Python getter/setter这个property, 从而self.myVAR成为 Property, 例如:
class Foo(object):
@property
def xx(self):
“””This method runs whenever you try to access self.xx”””
print(“Getting self.xx”)
return self._xx
@xx.setter
def xx(self, value):
“””This method runs whenever you try to set self.xx”””
print(“Setting self.xx to %s”%(value,))
self._xx = value
# Here add code to update the control in this setter method. …
# so whenever anything modifies the value of xx, the QLineEdit will be updated.

总体上:
@yourName.setter
def yourName(self, value):
self.myQLineEdit.setText(value)
self._name = value
# Note that the name data is actually being held in an attribute _name
# because it has to differ from the name of the getter/setter.

2. 信号连接

信号与槽机制是Qt最重要特性,提供了任意两个QT对象之间的通信机制。 信号会在某个特定情况或动作下被触发,槽是用于接收并处理信号的函数。

2.1 传统的signals/slot连接方式

信号与槽机制常用的连接方式为:
connect(Object1,SIGNAL(signal),Object2,SLOT(slot))
connect函数中的Object1和Object2,两个对象,signal是Object1对象的信号,注意要用SIGNAL宏包起来。当一个特定事件发生的时候(如点击按钮)或者Object1调用emit函数的时候,signal信号被发射。slot(槽)就是一个可以被调用处理特定信号的函数(或方法),是普通的对象成员函数。

2.2 PyQt4 以后可以采用新的信号与槽方式

After PyQt4, a new customized signals can be defined as class attributes using the pyqtSignal() factory:
PyQt4.QtCore.pyqtSignal(types[, name])
valueChanged = pyqtSignal([int], [‘QString’])

稍微完整的像这样:
from PyQt4 import QtCore
class MyQObject(QtCore.QObject):

# 定义一个无参数的信号
signal1 = QtCore.pyqtSignal()
# 定义一个参数的信号,参数类型为整数,参数名称为qtSignal2
signal2 = QtCore.pyqtSignal(int, name=’qtSignal2′)

def connectSigSlot(self):
signal1.connect(self.myReceiver1)
signal2.connect(self.myReceiver2)

def myReceiver1(self):
print ‘myReceiver1 called’
def myReceiver2(self, arg):
print ‘myReceiver2 called with argument value %d’ % arg

def myEmitter(self, arg):
signal1.emit()
signal2.emit(10)

新的singal/slot的定义与使用方式是PyQT 4.5中的一大改革。可以让PyQT程序更清楚易读。PyQT 4.5以后的版本建议用这种新方式。

3. Pyqt5的

PyQt5动定义了很多QT内建信号, 但是为了灵活使用信号与槽机制,可以自定义signal.
这样通过 pyqtSignal()方法定义新的信号,新的信号作为类的属性。

3.1 信号定义
新的信号应该定义在QObject的子类中。新的信号必须作为定义类的一部分,不允许将信号作为类的属性在类定义之后通过动态的方式进行添加。通过这种方式新的信号才能自动的添加到QMetaObject类中。这就意味这新定义的信号将会出现在Qt-Designer,并且可以通过QMetaObject API实现内省。

例如:
# 定义一个“closed”信号,该信号没有参数
closed= pyqtSignal()
# 定义一个”range_changed”信号,该信号有两个int类型的参数
range_changed = pyqtSignal(int, int, name=’rangeChanged’)

helpSignal = pyqtSignal(str) # helpSignal 为str参数类型的信号
printSignal = pyqtSignal(list) # printSignal 为list参数类型的信号

# 声明一个多重载版本的信号,包括: 一个带int和str类型参数的信号 以及 带str参数的信号
previewSignal = pyqtSignal([int,str],[str])

3.2 信号和槽绑定

self.helpSignal.connect(self.showHelpMessage)
self.printSignal.connect(self.printPaper)

# 存在两个版本,从因此在绑定的时候需要显式的指定信号和槽的绑定关系。
self.previewSignal[str].connect(self.previewPaper)
self.previewSignal[int,str].connect(self.previewPaperWithArgs)

3.3 信号发射
自定义信号的发射,通过emit()方法类实现:

self.printButton.clicked.connect(self.emitPrintSignal)
self.previewButton.clicked.connect(self.emitPreviewSignal)

def emitPrintSignal(self):
pList = []
pList.append(self.numberSpinBox.value ())
pList.append(self.styleCombo.currentText())
self.printSignal.emit(pList)

def emitPreviewSignal(self):
if self.previewStatus.isChecked() == True:
self.previewSignal[int,str].emit(1080,” Full Screen”)
elif self.previewStatus.isChecked() == False:
self.previewSignal[str].emit(“Preview”)

3.4 槽函数实现
通过@PyQt4.QtCore.pyqtSlot装饰方法定义槽函数:
@PyQt4.QtCore.pyqtSlot()
def setValue_NoParameters(self):
”’无参数槽方法”’
pass
@PyQt4.QtCore.pyqtSlot(int)
def setValue_OneParameter(self,nIndex):
”’一个参数(整数)槽方法”’
pass
… …
Pyqt5:
def printPaper(self,list):
self.resultLabel.setText(“Print: “+”份数:”+ str(list[0]) +” 纸张:”+str(list[1]))

def previewPaperWithArgs(self,style,text):
self.resultLabel.setText(str(style)+text)

def previewPaper(self,text):
self.resultLabel.setText(text)

3.5 总结
自定义信号的一般流程如下:
定义信号 – 绑定信号和槽 – 定义槽函数 – 发射信号, 例如:
from PyQt5.QtCore import QObject, pyqtSignal
class NewSignal(QObject):

# 一个valueChanged的信号,该信号没有参数.
valueChanged = pyqtSignal()

def connect_and_emit_valueChanged(self):
# 绑定信号和槽函数
self.valueChanged.connect(self.handle_valueChanged)
# 发射信号.
self.trigger.emit()

def handle_valueChanged(self):
print(“trigger signal received”)

注意: signal和slot的调用逻辑,避免signal和slot出现死循环, 如在slot方法中继续发射该信号.

4. app
The pyqtSlot() decorator can be used to specify which of the signals should be connected to the slot.
For example if you were only interested in the integer variant of the signal,
then your slot definition would look like the following:

@pyqtSlot(int)
def on_spinbox_valueChanged(self, i):
# i will be an integer.
pass

If wanted to handle both variants of the signal, but with different Python methods,
then slot definitions might look like following:
@pyqtSlot(int, name=’on_spinbox_valueChanged’) # note int,
def spinbox_INT_value(self, i):
# i will be an integer.
pass
@pyqtSlot(str, name=’on_spinbox_valueChanged’) # note str,
def spinbox_QSTRING_value(self, s):
# s will be a Python string object (or a QString if they are enabled)
pass

The following shows an example using a button when you are not interested in the optional argument:
@pyqtSlot()
def on_button_clicked(self):
pass

5. 装饰符 decorator

pySlot这个装饰符号可以把一个method定义为slot, 例如:
@QtCore.pyqtSlot()
def mySlot1(self):
print ‘mySlot received a signal’)
@QtCore.pyqtSlot(int)
def mySlot2(self, arg):
print ‘mySlot2 received a signal with argument %d’ % arg)

整个slot的定义与旧的方法相较,顿时变得简单许多。

而且,如果,UI是通过pyuic4设计的,那么甚至可以通过slot的名称来指定要连接的控件和signal。
例如, UI中有一个名为myBtn的按钮,想要连接单击的clicked signal。那么只要使用装饰符定义如下slot:
@QtCore.pyqtSlot(bool)
def on_myBtn_clicked(self, checked):
print ‘myBtn clicked.’

PyQT会自动将这个slot与UI内的myBtn的clicked singal连接起来。非常省事。
REF: http://python.jobbole.com/81683/

6. pyqt bridged opencv

先要把 cv.iplimage 这个 OpenCV 图像 用Python 的 PyQt widget显示。

6.1 Image Class

从 QtGui.QImage 派生出我们CameraImage类:

$ vi myCvPyQt.py
import cv
from PyQt4 import QtGui

# image used to paint by QT frameworks,
# converted from opencv image format
class CameraImage(QtGui.QImage):

def __init__(self, opencvBgrImg):
depth, nChannels = opencvBgrImg.depth, opencvBgrImg.nChannels
if depth != cv.IPL_DEPTH_8U or nChannels != 3:
raise ValueError(“image must be 8-bit 3-channel”)
w, h = cv.GetSize(opencvBgrImg)

opencvRgbImg = cv.CreateImage((w, h), depth, nChannels)

# OpenCV images from files or camera is BGR format
# which not what PyQt wanted, so convert to RGB.
cv.CvtColor(opencvBgrImg, opencvRgbImg, cv.CV_BGR2RGB)

# NOTE: save a reference to tmp opencvRgbImg byte-content
# prevent the garbage collector delete it when __init__ returns.

self._imgData = opencvRgbImg.tostring()

# call super UI QtGui.QImage base class constructor to build img
# by byte-content, dimensions, format of image.
super(CameraImage, self).__init__(self._imgData, w, h, QtGui.QImage.Format_RGB888)

If all you want is to show an OpenCV image in a PyQt widget, that’s all you need.

6.2 CameraDevice Class

新建一个相机的类,方便后期操作,实现相机和控件的解偶。
也就是,惟一的一个相机,可以作为生产者,供许多widgets消费,而控件之间无干扰。
也就是,人一个widgets都已可完全的操作相机。

$ vi myCvPyQt.py
import cv
from PyQt4 import QtCore

class CameraDevice(QtCore.QObject):

_DEFAULT_FPS = 30
newFrame = QtCore.pyqtSignal(cv.iplimage) # define signal with args of Camera Device

def __init__(self, cameraId=0, mirrored=False, parent=None):
super(CameraDevice, self).__init__(parent)
self.mirrored = mirrored

self._cameraDevice = cv.CaptureFromCAM(cameraId) # get capturer

self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(self._queryFrame)
self._timer.setInterval(1000/self.fps)

self.paused = False

@QtCore.pyqtSlot()
def _queryFrame(self):
frame = cv.QueryFrame(self._cameraDevice) # get frame
if self.mirrored:
mirroredFrame = cv.CreateImage(cv.GetSize(frame), frame.depth, frame.nChannels)
cv.Flip(frame, mirroredFrame, 1)
frame = mirroredFrame
self.newFrame.emit(frame) # trigger signal with args of Camera Device

@property
def paused(self):
return not self._timer.isActive()

@paused.setter
def paused(self, p):
if p:
self._timer.stop()
else:
self._timer.start()

@property
def frameSize(self):
w = cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FRAME_WIDTH)
h = cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FRAME_HEIGHT)
return int(w), int(h)

@property
def fps(self):
fps = int(cv.GetCaptureProperty(self._cameraDevice, cv.CV_CAP_PROP_FPS))
if not fps > 0:
fps = self._DEFAULT_FPS
return fps

Essentially, it uses a timer to query the camera for a new frame and emits a signal passing the captured frame as parameter.
The timer is important to avoid spending CPU time with unnecessary pooling.

6.3 CameraWidget Class

The main purpose of it is to draw the frames delivered by the camera device.
But, before drawing a frame, it must allow anyone interested to process it, changing it without interfering with any other camera widget.

$ vi myCvPyQt.py
import cv
from PyQt4 import QtCore
from PyQt4 import QtGui

class CameraWidget(QtGui.QWidget):

newFrame = QtCore.pyqtSignal(cv.iplimage) # a signal of Camera Widget

def __init__(self, cameraDevice, parent=None):
super(CameraWidget, self).__init__(parent)

self._frame = None

self._cameraDevice = cameraDevice # passing into camera device
self._cameraDevice.newFrame.connect(self._onDeviceNewFrame) # connect with signal of Camera Device

w, h = self._cameraDevice.frameSize
self.setMinimumSize(w, h)
self.setMaximumSize(w, h)

@QtCore.pyqtSlot(cv.iplimage)
def _onDeviceNewFrame(self, frame):
self._frame = cv.CloneImage(frame) # make local copy
self.newFrame.emit(self._frame) # trigger signal with args of Camera Widget, processed by…
self.update() # if device update then update

def changeEvent(self, e):
if e.type() == QtCore.QEvent.EnabledChange:
if self.isEnabled():
self._cameraDevice.newFrame.connect(self._onDeviceNewFrame)
else:
self._cameraDevice.newFrame.disconnect(self._onDeviceNewFrame)

def paintEvent(self, e):
if self._frame is None:
return
painter = QtGui.QPainter(self)
painter.drawImage(QtCore.QPoint(0, 0), CameraImage(self._frame))
# paint with frame whether already processed or not

every widget saves its own version of the frame by cv.CloneImage(frame). This way, they can do whatever they want safely.

However, to process the frame is not responsibility of the widget. Thus, it emits a signal with the saved frame as parameter by emit(self._frame) and anyone connected to it can do the hard work. It happens usually inside main block code.

6.4 Application

$ vi myCvPyQt.py
import sys

def _main_():

@QtCore.pyqtSlot(cv.iplimage)
def onWidgetNewFrame(frame):
cv.CvtColor(frame, frame, cv.CV_RGB2BGR)
msg = “… processing …”
font = cv.InitFont(cv.CV_FONT_HERSHEY_DUPLEX, 1.0, 1.0)
tsize, baseline = cv.GetTextSize(msg, font)
w, h = cv.GetSize(frame)
tpt = (w – tsize[0]) / 2, (h – tsize[1]) / 2
cv.PutText(frame, msg, tpt, font, cv.RGB(255, 0, 0))

app = QtGui.QApplication(sys.argv)

cameraDevice = CameraDevice(mirrored=True) # only one camera device

cameraWidget1 = CameraWidget(cameraDevice) # 1th widget
cameraWidget1.setWindowTitle(‘Orig Img’)
cameraWidget1.show()

cameraWidget2 = CameraWidget(cameraDevice) # 2st widget
cameraWidget2.newFrame.connect(onWidgetNewFrame) # connect signal with args of Camera Widget
cameraWidget2.setWindowTitle(‘Processed img’)
cameraWidget2.show()

sys.exit(app.exec_())

if __name__ == ‘__main__’:
main()

Two CameraWidget objects share the same CameraDevice, only the first widget processes the frames .
The result is two widgets showing different images resulting from the same frame.
Now you can import CameraWidget in a PyQt application to have fresh camera preview.

6.5 Total like this:
import cv
from PyQt4 import QtCore
from PyQt4 import QtGui
import sys

class CameraImage(QtGui.QImage):
… …
class CameraDevice(QtCore.QObject):
… …
class CameraWidget(QtGui.QWidget):
… …
def main():
… …

OK

BUT, better do the video capturing and conversion in another thread firstly, and then send a signal to the GUI instead of using timers.
The advantage of this is that every time a frame is captured the thread will shoot a signal to the main (which handles the UI) to update the component, which displays the OpenCV image (in opencv2 this is the Mat object).
https://gist.github.com/saghul/1055161

Python+Qt界面设计(01)

PART – I 界面
1. 准备
2. Qt的C++界面设计
3. Qt的Python界面设计

PART – II 剥离界面代码和功能代码
1. 建立新类
2. 操作界面

PART – III 剥离主线程和任务线程
1. QThread线程
2. 线程通讯
3. 一个例子

PART – I 界面

Ubuntu系统比Windows稳定小巧免费。
OpenCV视觉项目开源免费。
Python的语言比C/Cpp效率高。
Qt界面使用简单。
SO: ubuntu+python+opencv+qt ..

整体流程是:
先用qtcreator设计界面,然后pyc转化为python界面代码,然后再单独在工作线程里应用OpenCV。

1. 准备

在Ubuntu 14.04默认给安装了Python2.7和3.4,Qt默认给安装了4.8和5.2,sip默认安装时是4.15,PyQt默认安装的是pyqt4。
qtcreator有多个版本确认使用的哪一个:
$ which qtcreator
—/usr/bin/qtcreator

确认当前使用版本:
$ qmake -version
—QMake version 3.0
—Using Qt version 5.2.1 in /usr/lib/x86_64-linux-gnu

$ qtchooser -list-versions
—4
—5
—default
—qt4-x86_64-linux-gnu
—qt4
—qt5-x86_64-linux-gnu
—qt5

$ python -V
—Python 2.7.6
$ python3 -V
—Python 3.4.3

$ sip -V
—4.15.5
—python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
>>>import sip
>>>import PyQt4
导入正常.

2. Qt的C++界面设计

Qt的C++应该是最常用的。
$ qt-creator
File–New File or project… select QT Widget Application … Choose location
例如在界面添加一个pushbutton类型的pushButton01。
然后右击pushButton01 为 clicked这个single添加一个slot, 回掉函数名字例如 CbOnClicked(),
注意!
如果是python,就不要在creator这IDE里面添加slot的回调,因为具体都在py代码中实现。
Ctrl-B to build,
Ctrl-r ro run.

总结:
QTwidgets-based-project一共4个文件:
入口文件main.cpp +mainwindow.ui文件 + mainwindow.h和mainwindow.cpp后台文件
在main.cpp -> main函数中 直接调用MainWindow类的show()方法显示主界面。
MainWindow类中有成员变量是ui,其类型是Ui::MainWindow,通过这个ui成员去访问操作界面控件

3. Qt的Python界面设计

总体是:
先用qtcreator设计界面,
然后用pyc转化为python代码。

qtcreater … File–New File or project … Applications-Qt Widgets Application … Choose … Select location
注意!
kits 选择 Qt4.8.6或5.2.1某个 … Finish

例如, 在界面添加pushbutton类型的按钮名字为pushButton01。
注意! 这里不需要在qtcreator的designer中添加slot的回调,因为具体都是在py代码中。
保存 project, ok。

界面转换成Python代码:
$ pyuic4 -x ./mainwindow.ui -o ./myGUI.py

$ nano myGUI.py
… …
self.menuBar = QtGui.QMenuBar(MainWindow)
self.menuBar.setGeometry(QtCore.QRect(0, 0, 400, 25))
self.menuBar.setObjectName(_fromUtf8(“menuBar”))
MainWindow.setMenuBar(self.menuBar)
… …
可见这个py文件储存的是ui界面信息。

以下, 是不建议的操作界面的方式。

* 在myGUI.py中直接添加回调函数:
QtCore.QObject.connect(self.pushButton01, QtCore.SIGNAL(_fromUtf8(“clicked()”)), self.CbOnClicked)
* 在myGUI.py中直接实现回调函数:
def CbOnClicked(self):
print “Hello…dehao!…”
* 整体上像这样:
$ vi myGUI.py
from PyQt4 import QtCore, QtGui
… …
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName(_fromUtf8(“MainWindow”))
MainWindow.resize(400, 300)
MainWindow.setStatusBar(self.statusBar)
… …
self.retranslateUi(MainWindow)
QtCore.QObject.connect(self.pushButton, QtCore.SIGNAL(_fromUtf8(“clicked()”)), self.CbOnClicked)
## … we conect your callback function here …
QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(_translate(“MainWindow”, “MainWindow”, None))
self.pushButton.setText(_translate(“MainWindow”, “PushOK”, None))

## … now realize your callback function here …
def CbOnClicked(self):
print “Hello…dehao!…”

if __name__ == “__main__”:
import sys
app = QtGui.QApplication(sys.argv)
MainWindow = QtGui.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())

测试:
$ python myGUI.py
这样,窗口弹出,button响应。

PART – II 剥离界面代码和功能代码

前述均是直接修改pyc导出的python界面代码文件。
但是一旦在qtcreator设计环境中修改了ui界面, 就要重新把ui界面文件重新pyc导出,
那么这个myGUI.py文件就被完全覆盖,修改的(功能)代码全部丢失。

所以, 不可以在python界面代码文件中写功能代码。
为此, 新建某个文件(myGUI.py同目录),例如main.py, 再文件中建立新类class, 例如myApp:

1. 建立新类

$ vi main.py
from PyQt4 import QtGui
import sys

# we Make one new class myApp that will combine with ui code
# so that we can use all of its features interact with GUI elements
import myGUI

# 子类调用一个自身没定义的属性的多重继承按照何种顺序到父类寻找? 尤其众多父类中有多个都包含该同名属性?
# 经典类采用, 从左到右深度优先原则匹配方法 ,新式类, 用C3算法(不同于广度优先)进行匹配
#继承方法搜索的路径是先从左到右,在选定一个Base之后一直沿着该Base的继承进行搜索直至最顶端然后再到另外一个Base。
# bujin完成了所有的父类的调用,而且保证了每一个父类的初始化函数只调用一次。
class myApp(QtGui.QMainWindow, myGUI.Ui_MainWindow):
def __init__(self):
# super(B, self)首先找到B的父类(类A),然后把类B的对象self转换为类A的对象
# 然后被转换的类A对象调用自己的__init__函数
#super机制可以保证公共父类仅被执行一次,执行顺序按照MRO:Method Resolution Order
# 混用super类和非绑定函数是危险行为
# Simple reason why we use it here is that it allows us to access variables and methods etc in myGUI.py,
# here the self.__class__ is B in above example code.
super(self.__class__, self).__init__()

# This is defined in myGUI.py file automatically. It sets up layout and widgets that are defined
self.setupUi(self)

def main():
app = QtGui.QApplication(sys.argv) # New instance of QApplication, same as myGUI.py

form = myApp() # We set the form to be our myApp (design)
form.show() # Show the form

app.exec_() # myGUI.py is sys.exit(app.exec_())

if __name__ == ‘__main__’: # if we’re running file directly and not importing it
main() # run the main function

以上即可操作界面,但是并没有响应界面元素。

2. 操作界面

例如,为pushButton01的clicked这个event建立一个connect:
self.pushButton01.clicked.connect(self.my_func01)
And add it to the __ini__ method of our myApp class so that it’s set up when the application starts.

实现事件的回调函数 my_func01 function:
def my_func01(self):
print “Hello…dehao…”

整体像这样:
$ vi main.py
from PyQt4 import QtGui
import sys
import myGUI

class myApp(QtGui.QMainWindow, myGUI.Ui_MainWindow):
def __init__(self, parent=None):
super(myApp, self).__init__(parent)
self.setupUi(self)
self.pushButton01.clicked.connect(self.my_func01)

def my_func01(self):
print “Hello…dehao…”

def main():
app = QtGui.QApplication(sys.argv)
form = myApp()
form.show()
app.exec_()

if __name__ == ‘__main__’:
main()

测试:
$ python main.py
… …

PART – III 剥离主线程和任务线程

以上,通过合理的组织,实现界面代码和功能代码的解耦。

但是:
全部功能均是在界面主线程进行,这对于某些耗时任务并不合适。

界面不更新、 不响应等界面冻结的体验恶劣,所以要把这类任务放在单独的线程。
通常, 界面处理所在线程为主线程, 执行具体工作的为任务线程。

1. QThread线程

1.1 建立一个工作线程像这样 :
from PyQt4.QtCore import QThread
class YourThreadName(QThread):
def __init__(self):
QThread.__init__(self)
def __del__(self):
self.wait()
def run(self):
# your logic here
注意不要直接用run这个method, 尽量通过start。

1.2 使用一个工作线程像这样:
self.myThread = YourThreadName()
self.myThread.start()

可以用类似 quit, start, terminate, isFinished, isRunning等method ,
QThread提供了finished, started, terminated等有用的signal。

2. 线程通讯

在后台背景运行的任务线程,需要把数据传给界面主线程,完成更新进度条之类。
The proper way to do communication between working threads and UI thread is using signals。

2.1 built-in signals

例如任务线程的暴力破解工作完成后, 界面可以得到消息并提示用户。

*-实现函数, 在界面主线程里实现响应函数:
def done(self):
QtGui.QMessageBox.information(self, “Done!”, “crack finish!”)

*-连接函数, 在界面主线程里连接信号和函数:
self.myThread = thread01(test)
self.connect(self.myThread, SIGNAL(“finished()”), self.done)
self.myThread.start()

总体上:
first make a new instance,
then connect the signal with function,
then start the thread.

查看所有可能的Qt Signals:
http://pyqt.sourceforge.net/Docs/PyQt4/qthread.html

2.2 custom signals

定制信号和内嵌信号的唯一区别,是要在“QThead类”里面定义信号,
至于界面主线程里面实现响应函数以及连接信号和响应函数的都是相同的。

*-定义信号, 在“QThead类”里面定义信号,有所种方法,例如这种:
self.emit(SIGNAL(‘myTask(QString)’), myParam)

*-连接函数, 在界面主线程里面捕捉信号,是和内嵌信号的处理相同:
self.connect(self.myThread, SIGNAL(“myTask(QString)”), self.myFunc00200)

NOTE! 但是要注意这里有个重要的定制信号和内嵌信号的不同点,就是这个信号会传递一个回调函数所需要的对象。
This signal will actually pass an object (in this case QString) to the myFunc00200 function, and we need to catch that.
If you do decide to pass something the function that will be connected to the signal must be able to accept that argument.

*-实现函数, 既然信号传递的是QString,那么myFunc00200的实现像这样:
def myFunc00200(self, text):
self.my_ui_list_controls.addItem(text)
self.my_ui_progress_bar.setValue(self.my_ui_progress_bar.value()+1)

界面主线程获得的text, 就是从任务线程传来的QString。

3. 一个例子
用户在界面输入字符串,经过耗时的暴力破解处理后,结果显示给界面,用户可以暂停,破解同时可以更新进度和结果。
为此布置界面元素如下:
输入: btn_Start,开始破解。 btn_Stop,取消破解。 edit_Control, 输入框。
显示: progress_Control,进度条。 list_Control, 列表框 破解结果。
注意列表选择 list_widget 不要用 list_view.

代码像这样:
$ vi main2.py

from PyQt4 import QtGui
from PyQt4.QtCore import QThread, SIGNAL
import sys
import myGUI2
import time

## … Threads code …
class thread01(QThread):

def __init__(self, text):
# Make a new thread instance with one text as the first argument.
# The text argument will be stored in an instance variable called text
# which then can be accessed by all other class instance functions
QThread.__init__(self)
self.text = text

def __del__(self):
self.wait()

def _get_upper_char(self, ch):
# simulate crack task by to up case
return ch.upper()

def run(self):
# simulate a time consuming by sleep
for ch in self.text:
c = self._get_upper_char(ch)
self.emit(SIGNAL(‘processing_char(QString)’), c)
self.sleep(2)

## … main code …
class myApp(QtGui.QMainWindow, myGUI2.Ui_MainWindow):

def __init__(self):
super(self.__class__, self).__init__()
self.setupUi(self)
self.btn_Start.clicked.connect(self.start_process)

def start_process(self):
text = str(self.edit_Control.text()).strip()
self.progress_Control.setMaximum( len( text ) )
self.progress_Control.setValue(0)

self.mythread = thread01(text)
self.connect(self.mythread, SIGNAL(“processing_char(QString)”), self.update_ui) #
self.connect(self.mythread, SIGNAL(“finished()”), self.done) #
self.mythread.start()

self.btn_Stop.setEnabled(True)
self.btn_Stop.clicked.connect(self.mythread.terminate)
self.btn_Start.setEnabled(False)

def update_char(self, text):
self.list_Control.addItem(text)
self.progress_Control.setValue(self.progress_Control.value()+1)

def done(self):
self.btn_Stop.setEnabled(False)
self.btn_Start.setEnabled(True)
self.progress_Control.setValue(0)
QtGui.QMessageBox.information(self, “Done!”, “process finished!”)

def main():
app = QtGui.QApplication(sys.argv)
form = myApp()
form.show()
app.exec_()

if __name__ == ‘__main__’:
main()

okay.

$ pyuic4 -x ./mainwindow.ui -o ./myGUI2.py
$ python main2.py