CBTC车载仿真实验:模式操作与OpenCV目标检测量化分析
简介:这份轨道实验内容面向轨道交通信号系统课程学习者与实验指导教师,围绕目标检测在车载系统及控制模式中的应用展开,重点解决车载软件操作、控制模式转换条件理解与实验报告撰写等问题。压缩包共1个doc文件,约27.52MB,以实验报告文档形式完整记录实验目的、器材、内容、过程、分析与总结,并附评分标准,便于按章节查阅和对照。文档详细呈现CBTC环境下区域控制器软件与车载软件的配合流程,包括ATC初始化、RM/CM/AM模式切换、ATS计划下发、运行曲线识别、到站开关车门、提前发车与跳停等操作,并给出线路设置、任务下发和驾驶台操作的分析要点。目前已有135人学习,适合需要完成同类实验、复盘操作步骤或参考报告结构的读者使用。
1. 车载CBTC仿真实验在练什么:四种模式、三台机器、一处顺序错就全重启
这份轨道实验内容包,主线是 CBTC 车载系统与控制模式的仿真操作,副线藏在报告的分析段落里——用目标检测的办法,把车载界面上的模式指示灯、运行曲线和速度数字变成可判定、可复现的图像数据。前者练的是标准作业流程,后者练的是怎么让"我看到了绿灯"这种主观描述变成一张带阈值和坐标的截图证据。
它适合两类人:一类是要交轨道交通信号系统实验报告、但卡在"操作步骤记不清、分析写不深"的在校生;另一类是带实验课、需要一套可复现操作清单和自动判定脚本的指导人员。整个流程至少三台机器协同,一台起不来,剩下两台就只能是摆设。
2. 环境与拓扑先立住:Anaconda 隔离、OpenCV 装好、四块 ROI 切开
实验失败的原因,八成不在操作手法,而在环境没对齐。主机分辨率不一样、Python 版本各装各的、OpenCV 用系统里那个不知道谁装的旧版本,后面所有的截图坐标和模板匹配全是玄学。我一般把这一步当成硬件校准来对待:先把三台机器的角色和启动顺序固定下来,再把图像处理环境单独隔出来,最后才是切分界面区域。
2.1 三台机器的角色划分与启动顺序
仿真环境里,每台列车的操作至少需要三台计算机配合,分工大致是:一台跑区域控制器(CBTCZC),负责列车的注册与进路状态;一台跑车载软件,显示驾驶台、模式按钮和运行曲线;还有一台负责计划与任务下发。角色不能互换,因为车载软件在初始化时会向区域控制器注册,注册不上,后面点什么都不亮。
启动顺序我固定成下面这样,顺序错了就得全部关掉重来:
| 顺序 | 机器角色 | 启动动作 | 判定成功的标志 |
|---|---|---|---|
| 1 | 区域控制器 | 打开 CBTCZC 主程序 | 初始界面正常渲染,无报错弹窗 |
| 2 | 车载软件 | 打开车载主程序并最大化 | 四个显示区域完整可见 |
| 3 | 计划侧 | 登录并准备线路与任务 | 列车计划表里能看到本组车号 |
加车这一步容易被跳过:先在列车初始位置处加车,车体出现在存车线或车辆段,才具备后续初始化的前提。列车从车辆段或停车场出发,要等信号开放后用 RM 模式运行到转换轨;从存车线出发,则直接在"列车初始位置"里选好位置。这里必须注意上下行——向右为上行,向左为下行,选错了车会挂到反向轨道上,后面的运行曲线全是错的。
提示:三台机器开工前对一遍分辨率。车载窗口最大化之后,界面四块的相对位置是按比例固定的,但绝对像素坐标每台机器都不同。用绝对坐标写死点击位置的脚本,换一台机器就废。
2.2 用 Anaconda 隔离环境,把 OpenCV 装干净
图像处理这部分不建议用系统自带的 Python。我一般建一个专用环境,只装这一套实验要用的包,避免跟别的项目串版本:
# 建一个独立环境,Python 3.9 对 opencv-python 4.x 的兼容最省事 conda create -n cbtc-vision python=3.9 -y conda activate cbtc-vision # 核心三件套:图像处理、数值计算、屏幕截图 pip install opencv-python==4.9.0.80 numpy pyautogui openpyxl这几行的含义要说清楚。opencv-python是主库,装 contrib 版本会多带专利算法,但这个实验用不上,反而增加体积;版本钉在 4.9 是因为再往上某些接口的默认参数有微调,模板匹配的归一化系数会差一点点。pyautogui负责截屏和模拟点击,Windows 下多显示器要显式设置主屏,否则截出来的是拼接屏的一部分。openpyxl用来把检测结果导成表格,直接贴进实验报告的附表里。
装完先验证一下,别等到实验课上才发现导入失败:
import cv2, numpy, pyautogui print(cv2.__version__) # 期望 4.9.0 print(pyautogui.size()) # 期望输出当前主屏分辨率pyautogui.size()打印出来的宽高,就是后面所有 ROI 计算的基准。如果它不是你在系统设置里看到的那个分辨率,说明 DPI 缩放没关,得先去显示设置里把缩放调成 100%,否则截出来的图会被系统二次缩放,模板匹配的相似度永远上不去。
2.3 四块 ROI 与截图脚本
车载界面最大化之后,可以按比例切成四块:左上为车载显示界面,左下为方向选择、牵引制动选择与目标速度显示,右上为运行曲线,右下为运行过程中的重要信息。这四块的信息密度完全不同,左上和左下是状态判定区,右上是要做曲线提取的连续区域,右下是文本信息区。
我先用比例而不是像素记录边界,这样换机器只要重新乘一次分辨率:
| 区域代号 | 位置 | 横向起止比例 | 纵向起止比例 | 用途 |
|---|---|---|---|---|
| display | 左上 | 0.00 ~ 0.55 | 0.00 ~ 0.50 | 车载显示、列车位置 |
| control | 左下 | 0.00 ~ 0.55 | 0.50 ~ 1.00 | 方向、牵引制动、目标速度 |
| curve | 右上 | 0.55 ~ 1.00 | 0.00 ~ 0.50 | 绿色与黄色运行曲线 |
| info | 右下 | 0.55 ~ 1.00 | 0.50 ~ 1.00 | 运行重要信息 |
截图脚本按这个比例去裁,一次跑下来把四个区域的图分目录存好:
import os, time import pyautogui SAVE_DIR = "frames" os.makedirs(SAVE_DIR, exist_ok=True) # 四个区域的相对比例 (x1, y1, x2, y2) ROI_RATIO = { "display": (0.00, 0.00, 0.55, 0.50), "control": (0.00, 0.50, 0.55, 1.00), "curve": (0.55, 0.00, 1.00, 0.50), "info": (0.55, 0.50, 1.00, 1.00), } SCREEN_W, SCREEN_H = pyautogui.size() def grab_all(tag, idx): saved = {} for name, (x1, y1, x2, y2) in ROI_RATIO.items(): # pyautogui 的 region 是 (left, top, width, height),不是两个角点 left = int(SCREEN_W * x1) top = int(SCREEN_H * y1) width = int(SCREEN_W * (x2 - x1)) height = int(SCREEN_H * (y2 - y1)) img = pyautogui.screenshot(region=(left, top, width, height)) path = os.path.join(SAVE_DIR, f"{tag}_{idx:04d}.png") img.save(path) saved[name] = path return saved if __name__ == "__main__": for i in range(5): print(grab_all("am_run", i)) time.sleep(1.0) # 采样间隔 1 秒,与运行时曲线的刷新节奏接近代码里最关键的是那行注释:region参数要的是左上角加宽高,不是两个对角点。写错的话截出来要么是左上角一小块,要么直接越界报错。采样间隔设 1 秒,是因为车载界面的曲线和速度数字刷新不是逐帧的,采太快只会拿到一堆重复帧,后面做去重反而麻烦。
跑通之后,frames/底下应该有以标签加序号命名的四类图,每类五张。打开任意一张curve_0000.png,如果能看到完整的运行曲线区域,说明比例对上了;如果边缘切断或者带进了别的区域,微调那四组比例,每次调 0.02 左右,直到切开的位置落在两个区域的分隔线上。
注意:截图前必须把车载窗口最大化并置顶。窗口一旦被遮挡或者位置偏移,比例切割出来的内容就整体错位,这种错误在单张图上很难看出来,等到批量跑模板匹配才会发现全部匹配失败。
3. RM 到 AM 的转换链路:每一次点击的触发条件与状态确认
车载实验里最容易翻车的一段,是从 RM 切到 CM 再切到 AM 的过程。教材上写的是"模式转换",实际操作是若干次点击的顺序问题,顺序错一步,车载可能不响应,也可能整个仿真进程要重启。这一章把每个模式的进入条件、点击顺序和确认信号拆开讲,最后给一个可配置的步骤执行器,把操作清单从脑子里搬到文件里。
3.1 加车与 ATC 初始化的时序约束
初始化的完整链路是:加车到列车初始位置 → ATC 钥匙接通 → ATC 初始化 → 区域控制器提示注册成功。这四步里,前两步必须严格有序,先接通钥匙再点初始化,车载才会向区域控制器发起注册;反过来点,车载认为钥匙没在,初始化请求发不出去,界面看起来"点了没反应",实际是请求根本没生成。
判定注册是否成功,不看车载界面,看区域控制器那台机器的提示。车载侧只显示当前模式,注册结果在 ZC 侧弹窗提示。这也是三台机器必须同时有人盯的原因:车载侧的同学以为自己在等,ZC 侧的同学以为车载没发请求,两边都在等对方。
注册成功之后才谈得上放模式。如果车是从存车线出发,位置选择要区分上下行,向右为上行,向左为下行;选完之后再走钥匙接通加初始化的流程,车载会落到 RM 模式。如果是车辆段或停车场出发,则要等信号开放,用 RM 模式把车开到转换轨,再进入后续流程。
3.2 RM 的三下点击与它到底在确认什么
RM 模式的操作被描述成三步:参数选择里点亮 RM、方向按钮里选向前、再点一次让 RM 熄灭。第一次点是把模式亮起来,第二次点是把确认送出去,第三次点熄灭表示这条指令已经生效。学员最容易错在中间漏了方向选择,直接点第二次,车载收到的是一个没有方向的模式切换请求,界面上模式灯亮了但车不动。
CM 的进入也有前提:RM 状态下才能点"CM 可用"完成切换。所谓"可用"是按钮变可点击状态,不是点了就切过去。我的做法是每次点击前先用截图判定按钮的可用态,避免我在不可用时反复点导致状态机混乱。
3.3 AM 模式与 ATO 的两次操作
AM 是从 CM 转过来的,操作顺序是先点 ATO 点亮,再点 ATO 发车让它熄灭。和 RM 那套逻辑同构:第一次点亮是选择自动模式,第二次熄灭是执行发车。如果车已经在站台停稳但门没关,ATO 发车不会有响应,因为车门联锁没解除。
到站之后的开关门是人工的:点开左边车门或者右边车门,再点关闭对应侧车门,之后运行曲线会刷新,需要重新以 AM 模式操作列车运行。这一步如果门没关就点发车,界面上的按钮状态不会变,说明指令被拦在联锁之外。
提示:把"模式灯是否点亮""按钮是否可用""车门是否关闭"这三类状态全部用截图留痕。出了问题回看截图,比回忆"我当时点了没点"可靠得多。
3.4 运行曲线的两条线与曲线来源
ATS 下发计划、车载接收到计划之后,界面上才出现运行曲线。曲线有两条:绿色那条是依据计划生成的站间曲线,黄色那条是依据进路开放情况生成的运行曲线。两条线不重合的地方,就是计划与现场进路状态的差异所在——黄色缺一段,说明前方进路没开放,车跑到那儿必须停。
图上红色线段表示列车当前位置。这个红色标记是判断"车是否已经进入下一区间"的直接依据,也是后面做曲线提取时用来对齐时间戳的锚点。很多报告写"列车运行平稳",不如写"红色标记在 t=45s 越过绿灯区段起点,同期黄色曲线在该区间连续无断点",这才叫分析。
3.5 把点击清单变成可执行配置
手动点几十次总有手抖的时候。我的做法是把操作步骤写成配置,脚本按顺序执行,每一步之后截一次图留证。这样即使中途出问题,回看截图序列能精确定位到是哪一步开始偏离的:
{ "steps": [ {"name": "add_train", "pos": [0.42, 0.63], "wait": 1.5}, {"name": "atc_key_on", "pos": [0.18, 0.72], "wait": 1.0}, {"name": "atc_init", "pos": [0.18, 0.78], "wait": 2.0}, {"name": "rm_on", "pos": [0.10, 0.58], "wait": 0.8}, {"name": "dir_forward", "pos": [0.08, 0.66], "wait": 0.8}, {"name": "rm_confirm", "pos": [0.10, 0.58], "wait": 1.2}, {"name": "cm_switch", "pos": [0.22, 0.58], "wait": 1.5}, {"name": "ato_on", "pos": [0.30, 0.58], "wait": 1.0}, {"name": "ato_start", "pos": [0.30, 0.58], "wait": 2.0} ] }import json, time import pyautogui W, H = pyautogui.size() def run_steps(cfg_path): with open(cfg_path, "r", encoding="utf-8") as f: cfg = json.load(f) for i, step in enumerate(cfg["steps"]): # pos 是相对比例,乘分辨率得到绝对坐标,换机器只改分辨率不改配置 x = int(W * step["pos"][0]) y = int(H * step["pos"][1]) pyautogui.click(x, y) time.sleep(step["wait"]) # wait 是给界面渲染和状态机留的响应时间 print(f"[{i:02d}] {step['name']} @ ({x},{y})") if __name__ == "__main__": run_steps("steps.json")配置里的pos用相对比例,是这套东西能跨机器复用的关键。wait是每步之后的等待,不是随便写的:初始化之后要等区域控制器回包,给 2 秒;模式切换之后界面刷新,给 1 秒到 1.5 秒。等太短,下一步点在旧界面上;等太长,整体节奏拖沓还容易误触别的区域。
注意:这个执行器只适合用在明确知道当前界面状态的场景,不适合闭眼跑全流程。中间任何一步状态不对,后面的点击都是错上加错,所以每步之后要配截图,出问题倒回看是哪一张开始偏的。
4. 把界面变成数据:目标检测的三个落点与阈值调法
实验报告里写"车载切换到 AM 模式,列车开始运行",这是操作记录,不是分析。要做分析,得有可量化的东西:模式灯是哪一刻亮的、运行曲线中黄色段占比多少、速度数字稳定在什么区间。这一章用 OpenCV 的三类典型手法,把界面截图转成表格数据,分别是模板匹配判状态、HSV 阈值提曲线、轮廓分割认数字。目标检测在这里不是训练模型,是拿现成的匹配和分割算法做状态校验,门槛低、可解释、结果稳定。
4.1 模板匹配判定模式指示灯的状态
模式指示灯是固定位置的小色块,最适合用模板匹配。做法是从一张确认处于 AM 模式的截图里,把指示灯那一小块扣出来存成模板,之后每张图都拿这个模板去滑窗,得分超过阈值就判为"亮":
import cv2 import numpy as np def match_indicator(frame_path, template_path, thr=0.85): frame = cv2.imread(frame_path) # 车载界面截图 tpl = cv2.imread(template_path) # 抠出来的指示灯小图 if frame is None or tpl is None: raise FileNotFoundError("截图或模板读取失败") res = cv2.matchTemplate(frame, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) # thr 是判定阈值,0.85 是这套配色下的经验值,低到 0.7 会出现大量误判 return { "on": max_val >= thr, "score": round(float(max_val), 4), "center": (max_loc[0] + tpl.shape[1] // 2, max_loc[1] + tpl.shape[0] // 2), } print(match_indicator("frames/control_0003.png", "tpl/am_indicator.png"))TM_CCOEFF_NORMED是对比度和亮度归一化后的相关系数,对界面整体明暗变化不敏感,适合拿来做指示灯判定。阈值 0.85 是我在这套配色下试出来的:同一状态的不同帧之间得分一般在 0.93 以上,未点亮时得分在 0.6 以下,中间那段空档很大,所以 0.85 落在一个安全的位置。如果你换分辨率导致缩放,模板要跟着重新裁,否则得分会被压到 0.7 附近,出现灰色地带。
返回的center坐标不是摆设。多帧之间对比这个坐标,如果它在界面里发生了水平漂移,说明窗口位置变了,这时候后面的曲线提取坐标也要跟着修正,不然取到的是空白区域。
4.2 HSV 双区间分离绿色与黄色运行曲线
运行曲线是两条不同颜色的细线,用颜色阈值比用边缘检测稳得多。OpenCV 里 HSV 的色相范围是 0 到 179,绿色大致落在 35 到 85 之间,黄色落在 20 到 35 之间,两者可以分开提取:
import cv2 import numpy as np def curve_masks(img_path): img = cv2.imread(img_path) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 绿色区间:色相 35~85,饱和度与明度下限 80 用来滤掉灰白背景 green = cv2.inRange(hsv, np.array([35, 80, 80]), np.array([85, 255, 255])) # 黄色区间:色相 20~35,边线更靠近红色,所以下限卡在 20 yellow = cv2.inRange(hsv, np.array([20, 80, 80]), np.array([35, 255, 255])) # 开运算去掉细碎噪点,核大小 3x3 足够,太大会把曲线本身的细段吃掉 kernel = np.ones((3, 3), np.uint8) green = cv2.morphologyEx(green, cv2.MORPH_OPEN, kernel) yellow = cv2.morphologyEx(yellow, cv2.MORPH_OPEN, kernel) return green, yellow def curve_coverage(mask): # 每一列是否存在曲线像素,用于估算曲线在横轴上的覆盖比例 cols = np.count_nonzero(mask, axis=0) return float(np.count_nonzero(cols) / mask.shape[1]) g, y = curve_masks("frames/curve_0003.png") print("green coverage:", round(curve_coverage(g), 3)) print("yellow coverage:", round(curve_coverage(y), 3))饱和度下限设 80 是为了把浅灰的坐标网格和白色背景排除掉。开运算的核用 3x3,是因为曲线本身只有两三个像素宽,核再大就会把曲线打断成若干段,覆盖率的统计会失真。
curve_coverage算的是"有多少列存在曲线像素",输出一个 0 到 1 之间的比例。黄色覆盖率明显低于绿色的时候,说明进路开放不完整,前方有区段没开放。这个数字可以直接写进报告的分析段,比"黄色曲线看起来比较短"有说服力得多。
4.3 目标速度数字的轮廓分割与模板比对
速度数字的识别,很多人第一反应是上 OCR。但这个场景下数字字体固定、位置固定、只有两三位,用轮廓分割加模板比对反而更稳,也不用额外装依赖:
import cv2 import numpy as np def split_digits(img_path, min_h=12): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 反色后二值化:数字是亮色,背景偏暗,先取反再阈值 _, bw = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY_INV) contours, _ = cv2.findContours(bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [] for c in contours: x, y, w, h = cv2.boundingRect(c) # 高度过滤掉小数点、单位符号等小连通域 if h >= min_h and w <= h * 1.2: boxes.append((x, y, w, h)) # 按横坐标排序,左到右拼数字 boxes.sort(key=lambda b: b[0]) return [img[y:y+h, x:x+w] for x, y, w, h in boxes]min_h=12是过滤条件,高度不足 12 像素的连通域丢掉,能把小数点、单位字符、噪声排掉。宽高比限制在 1.2 以内,是防止把粘连的两个数字当成一个宽块。返回的是按从左到右排好的数字图块,后面拿 0 到 9 的模板逐个比对,取相似度最高的那个作为识别结果。
这一步的价值不在于识别得多准,而在于能把"目标速度"这一列变成时间序列。有了序列,才能回答"跳停之后多久速度摸到 80km/h""提前发车之后速度曲线有没有出现台阶",这才是实验分析该有的颗粒度。
4.4 汇总成表,回到报告的分析段落
三类检测跑完,结果统一写进一张 CSV,字段设计成时间戳、模式判定、绿黄覆盖率、目标速度:
| 时间戳 | 模式得分 | 判定模式 | 绿色覆盖率 | 黄色覆盖率 | 目标速度 |
|---|---|---|---|---|---|
| 00:00:12 | 0.9612 | RM | 0.000 | 0.000 | 0 |
| 00:01:05 | 0.9487 | CM | 0.412 | 0.398 | 0 |
| 00:01:40 | 0.9731 | AM | 0.876 | 0.851 | 45 |
| 00:02:58 | 0.9550 | AM | 0.881 | 0.503 | 78 |
表格拿到手,分析段落就不用凑字了。黄色覆盖率从 0.851 掉到 0.503 的那一行,对应的就是前方进路未完全开放的区间;目标速度那一列在跳停设置之后爬到 78,正好对应"距离够长、速度能摸到 80"这个观察。写报告的时候直接引这两行,比通篇描述操作过程有力。
提示:模板文件和阈值参数一起存进实验目录,别只存脚本。换了模板,历史数据就没法复算,等于把可复现性丢了。
5. 避坑与排查:五条能把三台机器一起拖下水的操作
仿真实验的坑有个共同特点:出错的时候界面不报错,只是"点了没反应"。没有报错信息,就只能靠现象倒推。下面五条是我在实验里反复见到的,按出现频率排。
5.1 钥匙接通与初始化点了反序,车载一直注册不上
现象:车载界面上点完两个按钮,模式灯不亮,区域控制器那台机器没有任何提示,两边都在等。
原因:ATC 钥匙接通是前置条件,初始化请求依赖钥匙状态有效才会发出。顺序反了,初始化请求被本地拦下,根本没有发到 ZC 侧,所以 ZC 侧看不到任何东西。这跟网络不通的表现很像,容易误判成连接问题。
解决:先看 ZC 侧的提示,没有提示就是请求没发出去,回头检查点击顺序。重来的时候把车载软件和 ZC 都重启,不要只重启车载——ZC 可能还保留着上一次的注册状态,新会话进来会冲突。我的习惯是把顺序做成配置文件的步骤序号,脚本按序号走,人为插队的可能性就没了。
5.2 没下发任务就直接推牵引,速度一直是 0
现象:驾驶台操作了,方向也选了,牵引手柄推上去,速度显示纹丝不动。
原因:列车运行需要先对本列车下发任务。任务没下发,牵引指令不被执行,界面也不会给明显的错误提示。实验分析里专门记过这一条:没有下发任务直接操作驾驶,列车是没有运行速度的。
解决:动驾驶台之前先看计划侧的任务状态,发车任务下达之后再操作。如果确认任务已下达仍然没速度,检查车号是否匹配——三台机器里计划侧绑定的车号跟实际操作的 3 号车对不上,任务就挂到别的车上了,这是分组实验里特别常见的错位。
5.3 提前到站后 AM 无响应,以为模式坏了
现象:到站开关门之后,重新点 AM 模式、点 ATO 发车,车不动,按钮状态也没变化。
原因:计划里的站间时间点还没到,列车提前到站,系统认为还没到允许发车的时刻,所以 AM 发车被拦下。不是模式转换失败,是时间没到。
解决:在列车处点提前发车,然后再重新操作 AM 模式。这个操作的副作用是打乱了原本的站间时间,后面的运行曲线会整体前移,统计时要单独标注这一段。报告里把这个时间差算出来,反而是个不错的分析点:计划站间时间与实际运行时间差多少,说明计划设置时两点横坐标拖得太远。
5.4 截图 region 参数写成两个角点,截出来是空白或者直接报错
现象:脚本跑完,frames/curve_0000.png打开是全黑或者越界报错。
原因:pyautogui.screenshot的region参数是左上角坐标加宽高,不是两个对角点。写成(x1, y1, x2, y2)的时候,第三四个参数被当成宽高,宽高变成了远大于屏幕的值,截出来自然异常。
解决:统一用一个转换函数,把比例形式的(x1, y1, x2, y2)先算出宽高再传进去。截完立刻做一次尺寸断言,宽高必须大于 0 且小于屏幕尺寸,否则直接抛异常,不要等到后面处理阶段才发现。
5.5 模板只截一次,换机器之后匹配分数集体掉到 0.7
现象:同一套脚本,在自己机器上跑得好好的,换到实验室另一台机器上,所有指示灯判定都变成"未点亮"。
原因:模板是从某台机器的固定分辨率下截下来的,换机器之后界面缩放比例变了,指示灯像素尺寸跟着变,模板的尺度对不上,相关系数被压到阈值以下。
解决:模板跟分辨率绑定存档,文件名里带上分辨率标记。换机器先跑一遍标定:截一张已知状态的图,目视确认模板匹配得分在 0.9 以上再继续。如果拿不到高分,就现场重裁模板,别硬调阈值——把阈值从 0.85 降到 0.7 确实能让判定通过,但误判率也会跟着上来,等于用一个错误掩盖另一个错误。
注意:这五条里有四条的表现都是"点了没反应"。排查顺序固定成:先看 ZC 侧有没有提示,再看计划侧任务状态,最后才怀疑模式和脚本。倒过来查会浪费大量时间。
6. 跳停与提前发车的量化对比:用检测日志验证 80km/h 达成
跳停这个操作,实验指导里写的是"提高运行速度",但它到底提高了多少、在什么条件下才提得上去,得用数据说话。我一般会跑两组对照:一组正常停站,一组在列车处设置跳停,同样的站间距离,然后把第 4 章那张 CSV 的两份日志按时间轴对齐。
对齐之前先确认一件事:两组的速度序列必须从同一个起点开始算,也就是从 ATO 发车的那一刻归零。如果一组从初始化开始计时、另一组从发车开始计时,两条曲线根本没法比。我的做法是给ato_start那一步单独打一个时间戳标记,后续所有采样都减掉这个基准。
对比表按下面的字段整理,站间距离取计划里两点横坐标对应的预计时间做参照:
| 指标 | 正常停站组 | 跳停组 | 差异说明 |
|---|---|---|---|
| 发车到出清本站时间 | 62 s | 41 s | 省掉了站停与再次启动 |
| 峰值目标速度 | 58 km/h | 79 km/h | 跳停后无中间停站,加速段连续 |
| 达到 75 km/h 的时刻 | 未达到 | 发车后 96 s | 需要足够长的连续区间 |
| 黄色曲线最低覆盖率 | 0.841 | 0.496 | 跳停组更早进入未开放区段前 |
第三行是这份实验里最有价值的观察。达到较高速度的前提是距离够长、加速过程不被打断,所以跳停组虽然在 96 秒摸到了 75 km/h 以上,但如果区间再短一点,速度根本起不来,这也解释了为什么有的组设置了跳停但速度依然上不去——不是设置没生效,是距离不够。
验证环节我用一个很土但有效的办法:把两组的检测日志画成速度-时间折线,横轴是秒,纵轴是识别出来的目标速度,两条线放在同一张图上。如果跳停组那条线的斜率明显更陡、平台更高,说明检测和实际运行是一致的;如果两条线几乎重合,优先怀疑检测环节而不是操作环节,回去查模板和速度数字的轮廓分割是不是漏了帧。
import csv import matplotlib.pyplot as plt def load_speed(csv_path, t0_key="ato_start"): ts, vs = [], [] with open(csv_path, encoding="utf-8") as f: rows = list(csv.DictReader(f)) # 以 ato_start 行为时间基准,之前的采样全部丢弃 base = next((i for i, r in enumerate(rows) if r["event"] == t0_key), 0) for r in rows[base:]: ts.append(float(r["t_sec"])) vs.append(float(r["target_speed"] or 0)) return ts, vs t1, v1 = load_speed("log/normal.csv") t2, v2 = load_speed("log/skip_stop.csv") plt.plot(t1, v1, label="normal stop") plt.plot(t2, v2, label="skip stop") plt.xlabel("seconds from departure") plt.ylabel("target speed (km/h)") plt.legend() plt.savefig("speed_compare.png", dpi=150)t_sec是采样时刻,target_speed是识别出来的目标速度,event字段用来标记关键动作。空值统一按 0 处理,因为识别失败通常发生在界面切换的瞬间,那一帧本来也不该计入有效速度。图存成 PNG 直接贴报告,比截两张界面图并排更有说服力。
从那以后我每次进实验室,都先把三台机器的分辨率和启动顺序确认一遍,再让脚本跑一遍完整流程不带任何手动点击,能跑通才让人上手。这套习惯救过好几次场——有一次就是靠这条预跑发现了 ZC 侧残留的旧注册状态,避免了整组人卡在初始化那一步反复重启。资源里的操作清单和报告模板都可以直接拿去改,上面的检测脚本按自己机器的分辨率微调比例就能用,希望帮到你。
本文还有配套的精品资源,点击获取