说明:本文系交通攻城狮原创文章,如需转载请私信联系,侵权必究。
2020,第 30 期,编程笔记
建议直接阅读精编版:如何利用 Python 绘制酷炫的 车辆轨迹 — 速度时空图?三维数据用二维图像呈现mp.weixin.qq.com
以下为正文:
在近期的论文写作中,需要绘制轨迹-速度时空图,中间是几经波折,遇到了各种问题。这个过程也让我再次认识到利用编程解决问题的便利性,可能过程很难,但是这种可以高度自定义真是太多软件无法替代的 ......
1. 问题由来
最近阅读论文中,遇到了一类图,非常好看,并且在其他论文中也多次遇到。比如,在 Trajectory data-based traffic flow studies: A revisit 一文中的图 1 ,如下图所示[1]:
由于原图较长,这里仅引用了部分图。从上图中可以清楚的看出几个关键信息:横轴表示时间变化,说明数据需要是时间序列的
纵轴表示空间位置,可以理解为从离开某一道路截面后,车辆行驶的距离
每条轨迹线均表示一辆车的行驶路径变化,而线条的颜色则表示瞬时速度值
因此,如果想要绘制出上图,那么就需要有车辆的瞬时轨迹、速度、时间等信息的数据集。
2. 准备数据
为了尝试绘制出该图像,现在需要做两个工作。第一,找到合适的数据;第二,找到顺手的绘图工具。
基础数据
在《权威数据:交通领域科研常用数据集总结与分享》一文中,已经分享了常见的公开数据集。本次使用其中 NGSIM 的数据,这和前文中图源数据是一致的。
对于 NGSIM 数据集,作简要介绍[2]:其中包含 4 个路段的车辆轨迹数据,任何一个均是采用在路段的周边高层建筑上设置高清摄像机录像,然后通过图像处理,将每辆车的轨迹变化采集出来,进而可计算出车辆所在的车道、瞬时速度、瞬时加速度、瞬时车头时距等等。
作为与本次绘图相关的关键信息,我们仅需知道以下数据:作为演示,仅使用一个车道的数据即可
每辆车从进入摄像区域到离开,中间的瞬时速度信息,NGSIM 中对应的数据标签是 v_Vel
每辆车从进入摄像区域到离开,中间的位置坐标变化,NGSIM 中对应的数据标签是 Local_X、Local_Y
与之对应的时刻,NGSIM 中对应的数据标签是 Global_Time
好了,知道了上述三个关键数据后,就可以利用绘图工具绘制了。
绘图工具
考虑到可重复性、可移植性和方便程度,本次采用 Python 中的 Matplotlib 包来绘制,可以参考这些文章来进一步了解:《零基础如何快速入门 Python 编程?谈谈个人看法》《如何开始第一个 Python 编程实践项目?》
3. 开始绘图
在开始绘图之前,需要在 Python 环境中已经配置好以下三个包:Pandas,本文主要作用在于读取、处理数据
Numpy,本文主要用于科学计算
Matplotlib,本文主要用于绘图
导入必要的包
毫无疑问,这几个包是会被用到的,因此,我们先在程序中导入它们:
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
准备数据
然后,我们需要将包含时间、位移、速度的数据导入 Python 中。这里需要注意,按照常规思路,我们只需将这三类数据赋值给不同变量即可,但本次绘制的图则不能直接这样做。
因为,从图 1 中我们可以看出,该图是有很多轨迹线组成的,也就是说每辆车都是一组数据,并且依赖于时间变化,且存在先后顺序。
比如,同一车道内,1 号先进入研究区域,那么它的数据则被记录;随后,2 号车进入研究区域,同样被记录位置变化、瞬时速度、时间。但是,这里的时间是不同的,是逐渐推移的。
一个思路是同时将所有车辆绘制在:X 轴是时间变化,Y 轴是位置变化的图像中。但是,这个操作需要将每辆车的数据均准备好,这对于同时绘制 300~400 辆车的轨迹图是不现实的。
为了解决这个问题,一个可行的思路是:循环绘制每辆车的轨迹随时间变化。这样可以只读取一次数据,然后依据车辆 ID 来循环绘制在同一坐标象限内。由于每辆车存在先后顺序,因此无需担心每辆车之间的轨迹重叠。
于是,这一步我们需要做的是:读取基础数据集,并筛选出需要使用的某车道数据,比如车道 3
需要将该车道中所有的车辆 ID 提取出来,并且是依据时间先后顺序排序,也就是根据数据标签 Global_Time 来排序,这个是全局时间
于是,我们可写出以下代码(所有代码可以左右滑动):
# 读取数据
data = pd.read_csv(r'F:\NGSIMData.csv')
# 提取车道数据
lanedata = data[data.Lane_ID == 3]
# 提取车辆编号
x_vehID = lanedata.drop_duplicates(['Vehicle_ID'])
# 依据 Global_Time 按照时间先后顺序排序
x_vehID = x_vehID.sort_values(by='Global_Time')
# 对排序后的车辆 ID 的索引进行重置,方便索引
x_vehID = x_vehID.reset_index(drop = True)
基本设置与绘图
到这里,我们已经将某个车道的数据提取了出来,并且将其中所有的车辆编号也提取出来了,且按照时间先后顺序。此时,仅需要按照前文说的:循环绘制每辆车的轨迹即可。在开始之前,我们需要先预设一下图的大小,以及字体属性。
# 导入字体属性相关的包或类
from matplotlib.font_manager import FontProperties
# 预设字体类型、大小
font = FontProperties(fname=r"C:\Windows\Fonts\times.TTF", size=10)
#设置画布的尺寸
plt.figure(figsize=(10, 4))
接下来就是循环绘制轨迹图了。这里有几个问题需要解释一下:既然是循环绘制,自然就是每次绘制的思路完全一致,具体体现在代码中
对于单次绘图,仅需通过第一个车辆 ID 索引对应所有的数据,然后将时间、位移、速度分别赋值给 x、y、v
注意,对于时间,NGSIM 给出的是时间戳格式,需要转换
注意,对于距离和速度,NGSIM 给出的单位是英尺,需要转换
时间将体现在 X 轴,距离将体现在 Y 轴,速度将体现在轨迹线的颜色上
其中,也有一个棘手的问题。那就是,假设某一车辆进入某车道一段时间后,换道至其他车道,随后过了一段时间,该车辆又换道至当前车道。这样就会出现,在时间的前半部分有轨迹线,中间部分没有轨迹线,后半部分有轨迹线。
对于这种情况,如果我们选择折线图绘制,其便会将断开的部分直接用线段连接起来,这是折线图的特征。但是,对于这类现象,我们并不期望它们是连接的,也就是没有轨迹的时间段内,最好是空白的。
为了解决这个问题,我选择了散点图。由于 NGSIM 采集数据的频率是 0.1s,数据点非常密集,也就是说对于连续的轨迹,即便我们用散点图绘制,出来的效果也是类似于折线的。比如:
注意,右上角是有断开的区域的,如果是用折线图绘制,就不会出现这种应该出现的效果。
另一个问题是颜色的处理,也就是需要将每辆车的速度值对应到同一时刻的轨迹点上。而这一点,散点图中可以将散点的颜色映射给数值,也就是我们需要的,具体就是代码中的 c=v,但这个命令仅是将速度值映射给颜色。
如果要将某一颜色主题应用于轨迹图中,那么就需要做两个工作:定义颜色主题,也就是散点图中的命令 cmap='jet_r'
因为后边我们需要设置颜色条图例,所以需要将颜色条中的颜色数值与轨迹图中的颜色归一化,让其一一对应。
我们给出这一循环绘制轨迹-速度图的代码:
i = 0
while i < (len(x_vehID)-1):
# 循环绘制轨迹图
cardata = lanedata[lanedata.Vehicle_ID == x_vehID[i]]
# 将时间赋值给变量 x
x = cardata['Global_Time']
# 计算相对移动距离,并赋值给变量 y
y = np.sqrt(np.square(cardata['Local_Y']) + np.square(cardata['Local_X']) )
# 将速度赋值给变量 v,同时定义速度为颜色映射
v = cardata['v_Vel']
#设定每个图的colormap和colorbar所表示范围是一样的,即归一化
norm = matplotlib.colors.Normalize(vmin=0, vmax=25)
# 绘制散点图
ax = plt.scatter(x,y, marker = '.', s=1, c=v, cmap='jet_r', norm = norm)
print(i)
i = i + 1
其中,我们对 y 值求了平方和的开方,也就是根据车辆的横纵坐标计算位移。我们还将绘制的散点图 ax = plt.scatter 赋给了 ax ,方便后边对散点图的属性进行设置。
这里需要注意的是,对于散点的绘制,我们利用循环解决。而对于坐标轴等的设置,只需设置一次,无需循环。到这里,我们可以得到一个不完美的轨迹-速度图:
不知为何,初步绘制出来的图的两侧存在大量的空缺,猜测可能是由于我在数据筛选阶段出了问题,又或者这些本就没有数据点。无论怎样,这样不影响我们来学习绘制该类图像。
4. 美化图像
添加颜色条图例
此时,需要再给上图加一个颜色条图例,以表示不同颜色对应的速度数值,这里只需要两行代码搞定:
# 添加颜色条
plt.clim(0, 25)
plt.colorbar()
即可得到如下所示的图像,图中修改了一下颜色主题:
这里需要注意在循环绘图的代码内部的一行代码:
# 设定每个图的colormap和colorbar所表示范围是一样的,即归一化
norm = matplotlib.colors.Normalize(vmin=0, vmax=25)
该行代码决定了颜色条图例中的颜色和左边轨迹图中的颜色是一致的,也就是必须按照颜色条设定的色阶来绘制轨迹和呈现色彩。
如果没有该行代码,就算添加了颜色条的代码,绘制出来的颜色条和左边的轨迹图也不对应,大家可以试试看~
添加横纵坐标轴及刻度值
这个操作就比较常规了,根据自己需要添加即可,常规的代码随便网上搜一个就有,包括刻度值得设置等等。
这一步仅重点说明一点:就是 X 轴绘制时利用的是时间戳数据,由于数值太密集,所以图中我关掉了 X 轴的刻度值。如果想要显示如图 1 所示的时间格式,一个可行的办法是向图像中添加文字。
也就是说,手动将时间放置在想要防止的刻度下,我们先看一组代码:
# 设置 X 坐标轴刻度
plt.text(x, y, '8:05', fontproperties=font)
plt.text(x, y, '8:10', fontproperties=font)
plt.text(x, y, '8:15', fontproperties=font)
plt.text(x, y, '8:20', fontproperties=font)
通过这组代码,我们便可以将制定的时间放置在图中指定的位置,基于坐标(x,y)。
最后,附上一个个人认为比较完美的图像,供大家参考:
忽略缺失数据后的效果:
将坐标轴设置为汉字,是因为汉字与英文同时出现时比较难处理,大家可以尝试调一下。
说明:以上所有仅供参考,横坐标轴的时间为随意指定,无任何精度可言,具体以实际绘制的图像为准。
5. 结束语
到这里就要结束了,由于利益相关,我在其中省去了一些代码。但是核心的代码,或者说比较难解决的问题,在上文中已基本给出了必要的解释。如果你需要绘制轨迹-速度图,可以以此为出发点,开始逐步的精细化绘制吧~
这类图,说实话,在论文中放与不放个人认为区别不大,但有一个比较大的好处是:好看。
然而,今天用这么长的篇幅来解释这类图如何画,其实是想总结这种三维数据如何用二维图来展示的心得体会。
也就是说,轨迹-速度图只是一个特例,可能对你而言,你当前研究的内容更值得用这种图像来展示效果,那么借此思路,开始绘制吧!
最后啰嗦一句,上边的代码你可以按照顺序粘贴在一块,基本就是整个过程 80% 了,自己再美化一下坐标轴、刻度值,解决一下汉字问题(准备英文期刊时请忽略),那就完美了 ......
参考资料:
[1] Li L, Jiang R, He Z, et al. Trajectory data-based traffic flow studies: A revisit[J]. Transportation Research Part C: Emerging Technologies, 2020, 114: 225-240.