
简介《自动驾驶安全第一白皮书》2019中文版由百度Apollo牵头联合Aptiv、奥迪、宝马、大陆、戴姆勒、FCA、HERE、英飞凌、英特尔、大众等主机厂与供应商共同编写面向L3、L4级自动驾驶研发人员、功能安全及预期功能安全SOTIF工程师与高校师生。全书149页以“通过设计实现安全”和验证确认VV为主线先把安全原则系统分解为设计安全能力、要素与架构再汇总VV方法论证自动驾驶方案相对人类平均驾驶水平具有正风险平衡并列出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, start1): # x_tolerance 控制横向字符归并距离y_tolerance 控制纵向行距 text page.extract_text(x_tolerance1.5, y_tolerance3) or pages.append({page: i, text: text}) return pages if __name__ __main__: data extract_pages(PDF_PATH) json.dump(data, open(pages.json, w, encodingutf-8), ensure_asciiFalse)逻辑是逐页抓取后落成{页码, 正文}的列表页码后续要当作需求条目的来源指针所以不能省。x_tolerance和y_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 用编号正则还原章节树条款编号是白皮书里最可靠的锚点。一级到四级通常写成1、1.2、1.2.3这样的形式层级由点号数量决定。import re CLAUSE_RE re.compile(r^(?Pnum\d(?:\.\d){0,3})\s(?Ptitle.{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, keylambda r: (r[page], r[clause]))编号标题经常跨页用(编号, 页码)去重只能解决重复抓取不能补全跨页正文。稳妥做法是把同一编号在各页出现的位置连起来取第一次出现到下一次出现同层级编号之间的全部文本作为正文。level字段用来在渲染时决定缩进也用来判断这一条属于上一节还是新起一节。注意编号正则会误吃正文里的列表项例如3 个必调参数这种行。收紧title长度上限、要求编号后紧跟中文或全角标点能过滤掉大部分误命中。2.3 用 SQLite FTS5 建中文可查的条款库中文检索的关键是分词。FTS5 的unicode61分词器对中文按字切召回高但精度差先过 jieba 再写入用空格隔开命中率明显更好。import sqlite3, jieba def build_index(clauses, dbsafety.db): conn sqlite3.connect(db) conn.executescript( CREATE VIRTUAL TABLE IF NOT EXISTS clause_fts USING fts5( clause, title, body, page UNINDEXED, tokenizeunicode61 ); ) 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^2import 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, encodingutf-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_threshold1.5): frames [json.loads(l) for l in open(log_path, encodingutf-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, encodingutf-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(), idslambda v: v if isinstance(v, str) else ) def test_scenario(req_id, scenario, acceptance, runner): result runner.play(scenario, seed20240101, duration_s30) 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 --tbshort # 夜间全量随机种子 覆盖率统计 pytest tests/scenario --seedrandom --covscenarios --cov-reportjson判断门禁该不该拦下这次提交看两个数一是新增需求条目里没有绑定场景或台架的比例二是场景覆盖率相对上一主干的下降幅度。前者大于零说明有人只写条款没写验证方式后者为负数说明这次改动动了场景库却没补需求。两个数都放进构建报告评审时有据可查。最后一点技巧来自失效注入把制动断线、报文篡改、传感器丢帧这几类注入做成可开关的夹具只在夜间任务里启用白天提交不会被它们拖慢。本文还有配套的精品资源点击获取