自动驾驶安全白皮书工程化:PDF解析、需求追溯与仿真回归门禁
2026/9/18 12:22:42 网站建设 项目流程

简介:《自动驾驶安全第一白皮书》2019中文版由百度Apollo牵头,联合Aptiv、奥迪、宝马、大陆、戴姆勒、FCA、HERE、英飞凌、英特尔、大众等主机厂与供应商共同编写,面向L3、L4级自动驾驶研发人员、功能安全及预期功能安全(SOTIF)工程师与高校师生。全书149页,以“通过设计实现安全”和验证确认(V&V)为主线,先把安全原则系统分解为设计安全能力、要素与架构,再汇总V&V方法,论证自动驾驶方案相对人类平均驾驶水平具有正风险平衡,并列出ISO 26262、ISO/PAS 21448等参考标准。压缩包内含1个PDF文件,体积约4.44MB,单文件结构便于跨设备阅读、检索与归档。目前已有402人学习下载,适合用于安全概念梳理、开发流程对标与行业标准化动态跟踪。

1. 一份149页的PDF,为什么值得按工程手册来用

把自动驾驶安全第一白皮书拖进阅读器,翻到目录就关掉,是多数人的处理方式——理念段落和条款混在同一份文档里,看不出哪一句和明天下午的需求评审有关。真正影响项目节奏的不是那些开篇陈述,而是一百多页里散落的需求线索:某个安全层次怎么划、某类失效怎么验、某个场景该用仿真还是用实车。

这份材料适合三类人当手册翻。做感知与规控的工程师,需要弄清自己模块被划到哪一层安全责任、边界在哪里;做功能安全和预期功能安全的同事,要把"安全第一"拆成 ODD、危害、ASIL 和可判定的验收准则;做仿真、数据与测试的同事,得把文字条款翻译成能回放、能回归的场景。三拨人如果各读各的,最后会卡在同一件事上:条款编号对不上页码,页码对不上用例。

后面的推进路线是一条工程链:先把 PDF 解析成结构化条款并建索引,再把条款落成带来源指针的需求条目,接着用联合仿真把需求跑成可测的数据,最后接进每次提交都会执行的回归门禁。整条链上最容易丢可追溯性的地方,是"解析"和"需求"之间的那一跳。

2. 从PDF解析到可检索条款:抽取、还原章节树、建中文索引

2.1 用 pdfplumber 做 pdf解析 的最小脚本

中文技术白皮书常见的排版问题是双栏、脚注、跨页表格,以及标题字号和正文接近。直接用默认参数抓文本,得到的往往是"两栏粘一起"或者"一行断成三行"。下面这段是起步版本。

import pdfplumber, json PDF_PATH = "apollo_safety_whitepaper_2019.pdf" def extract_pages(path): pages = [] with pdfplumber.open(path) as pdf: for i, page in enumerate(pdf.pages, start=1): # x_tolerance 控制横向字符归并距离,y_tolerance 控制纵向行距 text = page.extract_text(x_tolerance=1.5, y_tolerance=3) or "" pages.append({"page": i, "text": text}) return pages if __name__ == "__main__": data = extract_pages(PDF_PATH) json.dump(data, open("pages.json", "w", encoding="utf-8"), ensure_ascii=False)

逻辑是逐页抓取后落成{页码, 正文}的列表,页码后续要当作需求条目的来源指针,所以不能省。x_tolerancey_tolerance是这段代码里唯二需要反复试的参数:中文正文一般从 1.5 和 3 起步,标题行字号明显更大的页面可以把y_tolerance调到 4,避免标题和正文粘成一行。

参数建议起点作用调错时的表现
x_tolerance1.5~2.5横向字符归并阈值偏大时相邻两栏文字串行
y_tolerance3~4纵向行距归并阈值偏小时同一行被切成多行
layoutFalse是否按版面保留空格表格页开True更整齐,正文页反而更乱
laparams默认传给 pdfminer 的版式参数目录页可用line_margin单独调

表格页单独处理更稳:page.extract_tables(table_settings={"vertical_strategy": "text", "horizontal_strategy": "text"})比全局开layout更可控。如果遇到扫描页或图片型图表说明,用page.images定位后交给 OCR 处理,别指望文本层能吐出来。

2.2 用编号正则还原章节树

条款编号是白皮书里最可靠的锚点。一级到四级通常写成11.21.2.3这样的形式,层级由点号数量决定。

import re CLAUSE_RE = re.compile(r"^(?P<num>\d+(?:\.\d+){0,3})\s+(?P<title>.{2,60})$", re.M) def find_clauses(pages): rows, seen = [], set() for p in pages: for m in CLAUSE_RE.finditer(p["text"]): key = (m.group("num"), p["page"]) if key in seen: continue seen.add(key) rows.append({ "clause": m.group("num"), "title": m.group("title").strip(), "page": p["page"], "level": m.group("num").count(".") + 1, }) return sorted(rows, key=lambda r: (r["page"], r["clause"]))

编号标题经常跨页,用(编号, 页码)去重只能解决重复抓取,不能补全跨页正文。稳妥做法是把同一编号在各页出现的位置连起来,取第一次出现到下一次出现同层级编号之间的全部文本作为正文。level字段用来在渲染时决定缩进,也用来判断"这一条属于上一节还是新起一节"。

注意:编号正则会误吃正文里的列表项,例如"3 个必调参数"这种行。收紧title长度上限、要求编号后紧跟中文或全角标点,能过滤掉大部分误命中。

2.3 用 SQLite FTS5 建中文可查的条款库

中文检索的关键是分词。FTS5 的unicode61分词器对中文按字切,召回高但精度差;先过 jieba 再写入,用空格隔开,命中率明显更好。

import sqlite3, jieba def build_index(clauses, db="safety.db"): conn = sqlite3.connect(db) conn.executescript(""" CREATE VIRTUAL TABLE IF NOT EXISTS clause_fts USING fts5( clause, title, body, page UNINDEXED, tokenize='unicode61' ); """) for c in clauses: # 标题和正文分别分词,正文可先拼接同页后续文本 title_seg = " ".join(jieba.cut(c["title"])) body_seg = " ".join(jieba.cut(c.get("body", c["title"]))) conn.execute( "INSERT INTO clause_fts(clause,title,body,page) VALUES(?,?,?,?)", (c["clause"], title_seg, body_seg, c["page"]), ) conn.commit() conn.close()

查询时用bm25()排序,这一点常被写反:

SELECT clause, title, page, bm25(clause_fts) AS score FROM clause_fts WHERE clause_fts MATCH '预期功能安全 OR 失效' ORDER BY score LIMIT 20;

FTS5 里bm25()返回的是负值,升序排列才是最相关在前。page列标了UNINDEXED,既不会污染检索,又能随手把页码带出来当来源指引用。建完索引后,把"安全测试""仿真验证""信息与数据安全"这几个词各查一遍,人工看前 20 条能不能落在同一章,能落上就说明分词和抽取都过关了。

3. 把安全原则拆成需求:ODD、HARA、ASIL 与 SOTIF 怎么落到条目

3.1 白皮书的安全分层与两个标准体系的关系

这类安全白皮书讲的是体系,不是单点技术。拆解时常见的分法是四层:整车与系统层、功能安全层、预期功能安全层、信息与数据层。功能安全回答"系统出故障时还能不能安全",对应的是危害分析与风险评估、ASIL 等级、安全机制;预期功能安全回答"系统没坏但能力不够时怎么办",对应的是 ODD 边界、性能局限、可预见误用。这两层经常被混为一谈,结果是所有需求都写了 ASIL,却没有一条描述传感器在逆光下的能力退化如何被验证。

一个实用的判断规则:需求指向"部件失效"就归功能安全,指向"功能在特定场景下不够好"就归预期功能安全。前者要故障注入,后者要场景枚举。落到条目上,两类需求共用一套字段,但验证方式必须分开写,否则测试同事无法从字段本身判断该准备故障注入台架还是仿真场景。

3.2 一条可评审的安全需求长什么样

我一般用 YAML 写需求条目,纯文本便于放进 Git 做 diff,评审时改了一行能立刻看到。字段设计比格式重要。

id: SAF-ODD-014 source: {page: 57, clause: "4.3.2"} # 指回 PDF 页码与条款号 odd: road: "城市主干道,双向四车道以上" weather: ["晴天", "小雨"] speed_kph: [0, 60] hazard: "前方静止大型车辆漏检,导致追尾" asil: B sotif_relevant: true verification: method: "场景回放 + 失效注入" scenario: "cut_in_near_static_truck" acceptance: "TTC >= 1.5s 触发减速,最大减速度 <= 4 m/s^2"
import yaml, glob REQUIRED = {"id", "source", "odd", "hazard", "asil", "verification"} VALID_ASIL = {"QM", "A", "B", "C", "D"} def check(paths): for path in glob.glob(paths): for doc in yaml.safe_load_all(open(path, encoding="utf-8")): miss = REQUIRED - doc.keys() assert not miss, f"{doc.get('id')} 缺少字段 {miss}" assert doc["asil"] in VALID_ASIL, f"{doc['id']} ASIL 取值非法" # 预期功能安全需求必须绑定具体场景,否则无法证明覆盖 if doc.get("sotif_relevant"): assert doc["verification"].get("scenario"), f"{doc['id']} 缺场景"

source字段是整条链的命脉:没有页码和条款号,后续任何评审都会退化成"我印象里白皮书说过"。acceptance必须写成可判定的数值或布尔表达式,写"应保证安全"的条目在 CI 里无法执行。ASIL 从 QM 到 D 五档,取值受控才能做统计。校验脚本放在 pre-commit 里跑,十几行代码就能挡住大部分格式错误的提交。

3.3 可追溯性矩阵:从页码到用例的一条线

需求条目写完之后,用一张矩阵检查两头是否都接上了。

需求ID来源页/条款验证方式绑定的场景或台架关联用例ID状态
SAF-ODD-01457 / 4.3.2场景回放cut_in_near_static_truckTC-1042已覆盖
SAF-ODD-02161 / 4.4.1故障注入制动回路断线注入TC-1108待补
SAF-SEC-00688 / 6.1.3数据完整性校验报文篡改注入TC-1201已覆盖

状态列里"待补"的条目就是缺口清单。把它和仿真场景库对一遍,能发现两类常见问题:需求写了场景,场景库里没有;场景库里有,需求里没写。前者要补场景,后者要么是漏需求,要么是场景过度设计,两种都得在评审上说清楚。

4. 用联合仿真跑安全测试:CarSim、VTD 与 NI 实时机的分工

4.1 三套工具在环里各管什么

常见的联合仿真方案是三方分工:CarSim 负责整车动力学,输出横摆、侧偏、轮胎力这类连续量;VTD 负责路网、场景与传感器仿真,管 OpenDRIVE 路网和 OpenSCENARIO 场景的推进,产出相机、激光雷达的感知输入;NI 的实时机跑 VeriStand 模型,承担毫秒级调度、I/O 与故障注入。感知与规控算法挂在 VTD 或实时机侧,取决于算力需求。

分工不清的典型症状是"车不动但场景在走":场景时间在推进,动力学没有反馈,因为动力学求解没接上或者步长对不齐。另一种是"传感器数据比车晚半拍",多半是传感器帧率与动力学步长没有做时间对齐。

4.2 最小启动顺序与时间同步参数

顺序不能随意调,实时机要先就绪,否则后面的场景推进拿不到反馈。

# 1) 先起实时机,1ms 步长的模型先加载完 ssh ni-rt "cd /c/ni/veristand && ./start_model.sh --rate 1000" # 2) 再起 VTD,加载路网与场景 ./vtd.sh -c config/scenario_cut_in.xml -s road/city.xodr # 3) 最后起动力学求解,用 TCP 连回实时机 ./carsim_solver -p vehicle_b.sim -t 0.001 -i tcp://127.0.0.1:48100
参数建议值说明
动力学步长1 ms与实时机保持一致,避免插值误差
传感器帧率10~20 Hz高于此值对多数场景收益递减
时间同步PTP / gPTP多机联仿必须有统一时钟源
场景格式OpenSCENARIO便于跨工具复用与版本管理
失败重试关闭仿真中断要暴露,不要静默重跑

第三步里的-t 0.001是求解步长,-i指定回连地址。第一次联调时把 VTD 的场景先换成直线跟车,确认动力学正常输出速度与位置,再上复杂场景。PTP 没配对时,最直观的现象是日志里的时间戳抖动超过一个步长,这时先别怀疑算法。

4.3 自动驾驶数据集与仿真场景的取舍

数据集补的是真实分布,仿真补的是边界与危险场景。两者不能互替。

来源擅长覆盖不适合承担
KITTI早期城区与郊区感知基线密集交互场景
nuScenes多传感器与 3D 标注长尾极端天气
Waymo Open Dataset高精标注与长时段轨迹自有车型动力学验证
ApolloScape国内道路与场景多样性整车级功能安全验证

数据集用于训练和回归感知指标,仿真用于验证决策与控制的边界行为。把数据集里的场景回放当作安全测试的全部证据,评审时会被追问"危险场景从哪来",这时候必须拿出场景枚举的依据。

4.4 仿真日志回到需求:算 KPI 与覆盖率

跑完一轮要把结果算成能对回需求条目的指标。

import json def evaluate(log_path, ttc_threshold=1.5): frames = [json.loads(l) for l in open(log_path, encoding="utf-8")] min_ttc, min_gap, collision = float("inf"), float("inf"), False for f in frames: if f.get("collision"): collision = True min_ttc = min(min_ttc, f.get("ttc", float("inf"))) min_gap = min(min_gap, f.get("gap_to_lead", float("inf"))) return { "min_ttc": round(min_ttc, 3), "min_gap": round(min_gap, 3), "collision": collision, "pass": (not collision) and min_ttc >= ttc_threshold, }

ttc取整段仿真的最小值而不是末帧值,因为风险往往出现在中段。pass用布尔量输出,方便 CI 直接判定。把min_ttc和需求里的acceptance对齐,一条需求对应一条日志里算出来的数值,可追溯性就闭合了。覆盖率另算:已执行场景数除以场景库总数,按 ODD 维度分组统计,别只报一个总数。

5. 进阶:把白皮书条款接进每次提交都跑的回归门禁

到这一步,条款已经变成需求,需求绑定了场景,场景能出数值。剩下的技巧是把它们塞进 CI,让每次改动都自动跑一遍最关键的几十条。我一般用 pytest 做参数化,需求文件直接当用例来源。

import pytest, yaml, glob def load_cases(): cases = [] for path in glob.glob("reqs/*.yaml"): for d in yaml.safe_load_all(open(path, encoding="utf-8")): if d["verification"]["method"].startswith("场景回放"): cases.append((d["id"], d["verification"]["scenario"], d["verification"]["acceptance"])) return cases @pytest.mark.parametrize("req_id,scenario,acceptance", load_cases(), ids=lambda v: v if isinstance(v, str) else "") def test_scenario(req_id, scenario, acceptance, runner): result = runner.play(scenario, seed=20240101, duration_s=30) assert not result.collision, f"{req_id} 发生碰撞" assert result.min_ttc >= 1.5, f"{req_id} 最小 TTC {result.min_ttc}"

用例 ID 直接用需求 ID,失败时 CI 日志里能一眼看到是哪一条条款没通过。seed固定是为了可复现,但固定种子会掩盖偶发问题,稳妥做法是每晚跑一轮随机种子、提交时跑固定种子。门禁命令按影响范围分层:改动感知相关目录就跑感知回归,改动规划控制就跑场景回放。

# PR 门禁:只跑与改动模块相关的需求子集 pytest tests/scenario -k "$(cat changed_modules.txt)" -x --tb=short # 夜间全量:随机种子 + 覆盖率统计 pytest tests/scenario --seed=random --cov=scenarios --cov-report=json

判断门禁该不该拦下这次提交,看两个数:一是新增需求条目里没有绑定场景或台架的比例,二是场景覆盖率相对上一主干的下降幅度。前者大于零说明有人只写条款没写验证方式,后者为负数说明这次改动动了场景库却没补需求。两个数都放进构建报告,评审时有据可查。最后一点技巧来自失效注入:把制动断线、报文篡改、传感器丢帧这几类注入做成可开关的夹具,只在夜间任务里启用,白天提交不会被它们拖慢。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询