仿真跑完的那一刻,往往是Vadere使用体验里最让人犯嘀咕的环节:方案A和方案B明明只改了一个参数,可output目录里多出来的那一堆文件,到底哪些才是能写进报告、支撑结论的证据?Vadere的输出结果解读,一直是被问得最多、也最容易踩坑的部分。这篇内容想从实际使用的角度出发,把轨迹文件、测量区域数据、图形输出这几类最常见的结果捋一遍,再聊聊数据异常时怎么从结果倒推原因,以及怎样用Python在Vadere之外做二次分析。刚接触人群仿真的学生、需要把仿真结果整理成分析报告的工程师,应该都能从中找到直接能用的东西。
1. 先搞清楚Vadere输出到底生成了哪些文件
1.1 输出目录是怎么生成的
Vadere的默认做法是,每次运行模拟后把结果写到场景文件所在目录下的output子目录里。这个路径与场景文件位置绑定,换一台机器、换一个目录跑同样的场景,输出路径也会跟着变。所以拿到一批结果的第一件事,不是急着打开数据,而是先确认output里到底有哪些文件、这些文件是哪一次运行产生的。有人会把多个方案反复跑在同一个目录里,结果文件混在一起,后期根本分不清哪个对应哪个场景。
不同版本的Vadere在子目录命名上略有区别。我见过有的版本会为每次运行生成带时间戳的子目录,有的则直接把文件覆盖写在output根目录。时间戳目录的好处是多次运行不会互相覆盖,但用脚本批量处理时,目录遍历逻辑要写对。更稳妥的做法是,在每个场景文件里自定义输出文件名前缀,跑完立即把整个output目录归档改名,避免后续不必要的覆盖。
1.2 按类型认识五类常见输出
先建立一个整体印象,Vadere的输出大体可以分成下面五类:
| 输出类型 | 常见文件形式 | 内容概要 | 主要用途 |
|---|---|---|---|
| 轨迹数据 | txt / csv | 每个行人在每个输出时刻的位置 | 原始数据,几乎所有分析的基础 |
| 测量区域数据 | csv | 指定多边形或线区域内的密度、速度、流量 | 量化某条通道或出口的人群状态 |
| 标量数据 | csv / txt | 全局统计量,如疏散总时间、实时人数 | 汇总宏观指标 |
| 图像快照 | PNG序列 | 行人位置、密度热力图的可视化帧 | 展示、汇报、定性判断 |
| 日志文件 | log | 运行信息、警告、错误堆栈 | 排查异常和记录运行条件 |
轨迹文件是最底层的原始输出,精度最高但加工量也最大;测量区域数据属于已经算好的统计量,直接可用但要搞懂统计口径;标量数据适合做整体对比;PNG序列适合肉眼观察;日志文件则经常在出了问题之后才被想起来,建议运行结束顺手扫一眼。
1.3 开始解读前必做的三件事
解读任何输出之前,我会要求自己先完成三件事,这三件事能省掉后面大半的返工。第一步,回到场景配置里记录时间步长和输出频率。第二步,打开数据文件的头部,看注释行和列名,确认分隔符。第三步,挑一小段数据做手算验证,比如取相邻两行轨迹,计算速度是否和预期量级一致。
很多人会跳过第三步。举一个实际的例子:仿真时间步长是0.01秒,输出频率设置为每20步输出一次,那么轨迹数据里相邻行的时间差是0.2秒。如果在计算速度时误把0.2秒当成0.01秒,速度结果会被放大20倍。这种低级错误在报告复核里非常致命,提前用手算验证一条速度曲线就可以完全避免。
2. 轨迹文件:最原始也是最金贵的数据
2.1 轨迹文件每一列的含义
轨迹数据保存了每个行人在每个输出时刻的位置,是所有二次分析的原材料。打开文件后,先看表头和注释行。最常见的结构是四列,第一列是仿真时间,单位秒;第二列是行人编号,通常从0或1开始;第三列和第四列是行人在全局坐标系下的X、Y坐标,单位米。一个简化版的轨迹文件片段长这样:
# time pedestrianId x y 0.00 1 5.0 3.0 0.01 1 5.02 3.01 0.02 1 5.04 3.02 0.03 1 5.06 3.03较新的Vadere版本会在轨迹里附加更多字段,比如Z坐标、瞬时速度分量、行人状态标签等。不管列怎么增加,前四列的含义基本稳定,所以脚本处理时最好按列名读取,而不是按位置索引硬编码,否则版本一升级脚本就报废。
还有一点容易忽略:行人编号不一定连续,也不一定从1开始。每个行人在被创建时获得一个ID,到达出口被移除后,这个ID不会再出现。因此按行人分组统计时,要使用groupby(这就是“分组统计”的命脉),同时统计每个ID的存活时长,区分“在场景里走完全程”和“中途被移除”两种情况。
2.2 用轨迹还原速度与密度的基本算法
拿到轨迹后,速度是最容易计算的指标。对同一个行人ID,把相邻两帧的位置差除以时间差,就得到这段时间内的平均速度。写成公式就是:
[ v_i(t) = \frac{\sqrt{(x_{t+\Delta t} - x_t)^2 + (y_{t+\Delta t} - y_t)^2}}{\Delta t} ]
这里的时间差 (\Delta t) 是输出间隔,不是仿真步长。代码实现很简单:
import pandas as pd df = pd.read_csv("output/trajectories.txt", sep=r"\s+", comment="#", names=["time", "pedestrianId", "x", "y"]) df = df.sort_values(["pedestrianId", "time"]) df["dx"] = df.groupby("pedestrianId")["x"].diff() df["dy"] = df.groupby("pedestrianId")["y"].diff() df["dt"] = df.groupby("pedestrianId")["time"].diff() df["speed"] = (df["dx"] ** 2 + df["dy"] ** 2).pow(0.5) / df["dt"]实际算出来的速度曲线通常带有噪声。原因是仿真中行人会产生瞬时加减速和避让抖动,差分又放大了这些波动。想要平滑曲线,可以先对坐标做窗口平均再求速度,或者直接用Vadere测量区域处理器输出的速度统计量。
密度的计算比速度复杂。最直观的是网格法:把场景划分成较小的格子,例如0.5米乘0.5米,统计每个格子内的人数,再除以格子面积(要扣除墙和障碍物占用的面积),得到局部密度。这种方法的优点是直观、可复现,缺点是格子大小会影响结果,边界格子的面积修正也需要处理。更平滑的是核密度法,把每个行人看作一个高斯“影响源”,向周围扩散后累加。Vadere内部的核密度输出通常采用这类思路,但在具体参数上不同版本有差异,所以报告里写清楚“密度是怎么算出来的”,比写密度的数值更重要。
2.3 轨迹坐标的坑:单位、原点与旋转
轨迹坐标的单位是米,这一点几乎不会变,但原点和方向就未必了。原点不一定在场景左下角,可能会因为底图坐标而有偏移;测量区域数据使用同一套全局坐标,所以坐标原点偏移本身不是问题。
真正容易踩坑的是场景带旋转的情况。当一个大型场景被整体旋转后,轨迹输出的是旋转后的模型坐标,而你的分析底图可能是未旋转的CAD平面图,直接把轨迹画在底图上会出现方向错位。解决方法是先确认场景配置里的全局变换参数,把所有坐标统一换算到同一个参考系再做可视化。
另一个常被问到的方向问题是Y轴。GUI里一般Y轴向上,但输出文件里的坐标方向以场景属性面板为准。判断方法很简单:找一个已知坐标的固定障碍物,比较它的轨迹坐标和场景面板显示是否一致。不要默认“坐标一定是正的”,实测确认后心里才有底。
3. 测量区域输出:密度、速度、流量到底怎么算的
3.1 测量区域文件长什么样
测量区域是你在GUI里画出来的几何体,用来框定“我想知道这里的人群状态”。Vadere通过输出处理器将测量区域内的统计量写入CSV文件。比较常见的文件结构包含时间步、测量区域名称或ID、测量值等列。处理器不同,生成的列也不同,有的版本会把每个区域单独输出一个文件,有的版本则把所有区域的数据放在同一个文件里。
拿到测量区域CSV后,先别急着画折线图。把文件加载到脚本里,检查每个区域的数据行数、时间轴是否完整。两个相邻测量区域的数据长度未必相同,因为行人进入和离开区域的时间不一致,统计起止时刻自然也不同。做跨区域比较时,必须先把时间轴对齐,否则Excel里直接插入折线图会错得离谱。
3.2 三种统计口径的物理含义
密度是最直观的指标,定义为测量区域内当前人数除以区域面积,单位是人每平方米。简单密度只考虑总面积,不考虑行人分布是否均匀;核密度会因行人是否扎堆在区域边缘而给出不同结果。用于瓶颈分析时,核密度通常更能反映真实拥挤程度。
速度的定义则有讲究,这里要区分两种平均。第一种是区域内所有行人瞬时速度的算术平均,容易实现,但和高密度下的通行效率关系较弱;第二种是流率加权平均,按通过测量区域的“流量权重”进行平均,更接近“单位时间里通过多少个人、这些人的平均速度如何”。Vadere的手册会区分这两类定义,做疏散分析时如果目标是评估通道通行效率,优先使用第二种口径。
| 指标 | 单位 | 物理含义 | 常见误区 |
|---|---|---|---|
| 密度 | 人/m² | 区域内人数与面积的比值 | 把简单密度当成核密度 |
| 速度 | m/s | 区域内行人的平均速度 | 混淆算术平均与流率加权平均 |
| 流量 | 人/(m·s) | 单位时间、单位宽度通过的人数 | 测量线长度不一致导致对比失效 |
流量定义为单位时间穿过测量边界的人数,并按测量线宽度归一化,所以单位是人每米每秒。实际使用时要特别注意:线状测量区域的长度会直接影响流量数值,不同场景对比时尽量用等长测量线,否则流量曲线差异会被测量线长度污染。
3.3 从测量区域数据画佛利德曼图
佛利德曼图是行人动力学研究里最常见的基础图表,横轴常用密度,纵轴用速度或流量。从测量区域CSV里取密度列和速度列,直接画散点图即可。如果要看流量曲线,就按“流量=密度×速度”计算,但要注意这个推导出来的流量要与实际测得的流量保持一致,否则两张图表在报告里会自相矛盾。
画完图之后检查曲线形状。低密度时速度应该接近自由速度,密度增大到临界值后速度逐渐下降,流量曲线呈现先升后降的倒U形。如果看到低密度下速度也很低,或者高密度下速度突然回升,多半是数据里混入了启动阶段或边界效应。先把开头的瞬态数据剔除,再把明显异常的点过滤掉,曲线会干净很多。
4. 图形输出与可视化结果怎么看
4.1 PNG序列图的时间步对应关系
Vadere能把仿真过程输出为PNG序列,每一帧对应一个输出时间点。文件名里的数字表示“第几帧”,而不是“第几秒”。举一个实际的例子:仿真步长0.01秒,输出频率设为每50步输出一帧,那么第20帧对应的实际时间就是10秒。如果误把帧号当成秒,对“人群在第几秒到达出口”的判断就会完全出错。
为了准确对应帧号和时间,最简单的办法是同时打开日志或轨迹文件,找到帧号对应的时间戳。更省事的做法是在配置阶段让输出处理器在命名模板里携带时间信息,某些版本支持这种自定义。我个人的习惯是先用GUI播放功能定位关键时间点,再导出对应PNG帧,而不是一次性导出几百张图然后一张张翻。
4.2 热力图的数值含义
热力图本质是密度场的可视化,颜色条上的数字就是密度值。读图时第一件事不是看颜色,而是看色标范围。同样是一张黄色图,如果色标上限是8人每平方米,场景已经非常拥挤;如果上限只有1人每平方米,黄色区域只是正常行走。发布图片时如果省略色标,热度图的说服力会大打折扣。
第二件事是看热点出现的位置。出口附近容易形成半圆形的高密度堆积,这是瓶颈的典型特征;如果高密度区域出现在场景中部、周围没有障碍物,就要考虑是不是初始化分布不合理,或者人群自组织形成了局部拥堵。结合轨迹数据回放,能很快判断属于哪种情况。
4.3 可视化结果用于报告与展示的经验
跨场景对比时,务必统一色标和视角。两个方案如果色标不同,哪怕第二个方案实际上更拥挤,图片上也可能显得更清爽。我的习惯是先把两个方案的密度场标准化到同一个密度区间,再生成对比图。
还要注意导出图片的分辨率。Vadere默认输出的PNG分辨率可能在放大后出现明显锯齿,图中的文字标注也会模糊。可以把画布尺寸调大再导出,或者导出轨迹后用外部绘图工具重绘。报告里的每张图最好配一行简短的时间说明,例如“疏散开始后第30秒”,让审阅者快速理解这张图描述的是哪个时刻的状态。
5. 输出结果异常时的排查思路:从结果倒推原因
5.1 为什么测量区域文件是空的
如果你配置了测量区域处理器,运行后却得到空文件或全0数据,先别怀疑软件出了问题。第一件事是回放轨迹,确认行人是否真的穿过了你画的测量区域。很多人把区域画在障碍物后方或被墙体完全挡住的位置,行人绕路走,自然不会有数据。
第二件事是检查处理器配置。输出处理器的配置界面通常有一个下拉框,让你选择要对哪个测量区域输出。如果那里保持默认的“无”,处理器就不会生成数据。另一种情况是区域形状与处理器不匹配,例如想测流量却画了面状区域,或者想测密度却选了线状区域。每个处理器有适配的区域类型,界面往往有提示,对不上时输出自然为空。
日志文件在这里非常有价值。运行结束后打开log文件,搜索测量区域或处理器相关关键字,看有没有警告信息。某些版本在多边形自交、顶点顺序异常等边界情况下会给出警告,这些在GUI里肉眼根本看不出来,只能靠日志捕捉。
5.2 轨迹文件为空或行人数不对
轨迹文件为空,常见原因有三种。第一种是场景里行人加载源配置成了0,仿真启动了但没有任何人出现。第二种是模拟运行了极短时间就被中断,轨迹文件只写了开头几行。第三种是输出处理器确实存在,但没有被场景运行流程启用,这种情况往往伴随日志中的配置警告。
行人数变少则通常是正常现象。很多模型规定行人到达出口后被移除,模拟后期轨迹里的行人数下降,说明大部分人群已经疏散完毕。如果目标是统计疏散总时间,应该把“最后一名行人到达出口的时间”作为结束时刻,而不是等轨迹文件里完全没有任何行人——因为最后一个行人被移除的瞬间,轨迹文件就已经停止写入。
5.3 数据跳变与尖峰怎么处理
轨迹数据里偶尔会出现一个行人从位置A瞬间跳到很远的B,这是碰撞处理或行人步行模型中的局部避让造成的,并不代表真实速度。直接用原始轨迹差分时,这种跳变会生成一根很高的速度尖峰。处理方法是先对速度做中值滤波或滑动平均,或者直接设定一个距离阈值(例如2米),超过阈值的位移标记为异常记录并剔除。
还要检查输出频率是否过低。假设仿真步长是0.01秒,行人一步最多移动零点几米;但如果输出频率是每100步一次,你会看到行人每一秒位移几米甚至十几米,差分算出的速度完全不包含中间的真实过程。这种情况不是数据坏了,而是观测频率不够。把输出频率加密后重新运行,尖峰通常会自行缓解。如果加密后尖峰仍然存在,再去检查模型参数里的速度噪声范围是否被设置得过大。
6. 用Python对Vadere输出做二次分析的实用套路
6.1 加载轨迹文件的代码骨架
轨迹文件本质上是普通文本,用标准库就能读取。麻烦的是不同版本的分隔符和注释方式不同。最稳妥的做法是先用文本编辑器打开看一眼,再用pandas按实际格式读取。典型的加载写法如下:
import pandas as pd df = pd.read_csv( "output/trajectories.txt", sep=r"\s+", # 如果列间是空格 comment="#", # 跳过注释行 names=["time", "pedestrianId", "x", "y"], engine="python" )如果文件是逗号或分号分隔,把sep参数换成","或者";"即可,列名则根据真实表头调整。读取完成后先打印df.head()和后几行,确认时间列是否递增、坐标范围是否合理。这一步成本极低,但能避免后续所有基于错误数据Frame的分析白做。
6.2 批量统计多个run的数据
做参数对比时,常常需要跑几十个run。写一个脚本遍历输出目录,提取每个run的统计指标并汇总成一张表,是效率最高的做法。核心思路是先定义一个函数,输入目录路径,返回需要的指标字典,再循环调用并汇总。
import pathlib import pandas as pd def summarize_run(dir_path: pathlib.Path) -> dict: traj_file = dir_path / "trajectories.txt" df = pd.read_csv(traj_file, sep=r"\s+", comment="#", names=["time", "pedestrianId", "x", "y"]) evacuation_time = df.time.max() max_speed = df.groupby("pedestrianId")["time"].count().max() return { "evacuation_time": evacuation_time, "max_timesteps_per_ped": max_speed, } rows = [] for p in pathlib.Path("output").glob("run_*"): rows.append({"run": p.name, **summarize_run(p)}) summary = pd.DataFrame(rows)批量处理之前,务必手工确认两个run的文件结构完全一致,尤其是列名和输出频率。如果两个run的输出频率不同,直接比较它们导出的“总时间”就会失真。建议在汇总表里额外记录一个“输出间隔”字段,需要精确时间时用它做修正。
6.3 快速生成密度热力图的脚本思路
即便没有配置Vadere自带的密度输出,也可以自己用轨迹数据画热力图。思路很简单:把场景划分成网格,遍历所有轨迹点,统计每个网格里的记录次数,最后除以网格面积和总时间帧数,得到时间平均密度场。
import numpy as np import matplotlib.pyplot as plt grid_size = 0.5 # 米 x_min, y_min = df[["x", "y"]].min() x_max, y_max = df[["x", "y"]].max() nx = int((x_max - x_min) / grid_size) + 1 ny = int((y_max - y_min) / grid_size) + 1 grid = np.zeros((ny, nx)) for _, row in df.iterrows(): ix = int((row.x - x_min) / grid_size) iy = int((row.y - y_min) / grid_size) if 0 <= ix < nx and 0 <= iy < ny: grid[iy, ix] += 1 grid /= df.time.nunique() * grid_size * grid_size plt.imshow(grid, origin="lower", cmap="hot") plt.colorbar(label="people / m2") plt.show()这段代码只是骨架,实际使用时要注意两点。第一,行人进入场景前和离开后,网格会少统计一段时间,平均密度会被轻微低估,建议只用仿真中间的时间窗口做平均。第二,网格法依赖格子大小,改一个数字出来的热点位置可能看起来不一样,对定量结论要有保留。快速查看定性趋势时网格法够用,但发布正式结果时,还是建议使用测量区域处理器或核密度算法。
实际用过Vadere一段时间的人都会有同感:这个工具的输出从来不是为“让你一眼看懂”设计的,而是为“让你可信地量化”设计的。所以我自己的体会是,解读输出结果前,先想清楚这一次仿真想回答什么问题——是疏散时间、瓶颈位置,还是人群密度峰值。问题一旦明确,要读哪个文件、滤掉哪些时间、用什么口径统计,都会变得清晰。还有一个值得坚持的小习惯:每次运行前把场景配置复制一份放进output目录,和结果放在一起。将来写报告时,不需要为确认某个参数而到处翻找原始场景文件,这一份配置就是全部答案的来源。