一、写在前面
两个月前我还在全力备战华为OD机考,刷遍了螺旋矩阵、括号匹配、打家劫舍这些经典题。考场上三道题全部零分,原因是新系统换了题型,而我连核心代码模式都没练熟。
考试失利后我决定换个方向:不再为考试而学,而是为解决实际问题而学。我选择了ADAS(高级驾驶辅助系统)作为落脚点。
今天这篇文章,就从我最熟悉的「合并区间」算法开始,看看它在ADAS中是怎么用的。
二、从一个问题开始
假设一辆车上有三个传感器:
- 前置摄像头:视野范围 -30°, 30°
- 左前角雷达:视野范围 10°, 90°
- 右前角雷达:视野范围 -90°, -10°
问:哪些角度范围被至少两个传感器同时覆盖?
这个区域叫做「重叠区」。在重叠区内,多个传感器都能看到同一个目标,可以进行传感器融合,提高检测的置信度。
三、两种不同的需求
拿到这个问题,我第一反应是「合并区间」------把有重叠的区间合并成一个更大的区间。但仔细一想,不对。
合并区间:求所有传感器覆盖的总范围(并集)。
求重叠区:求被至少两个传感器同时覆盖的范围(交集)。
两者完全不同。
用上面的数据举例:
- 合并区间结果:-90°, 90°(三个传感器合起来覆盖了整个前方半圆)
- 重叠区结果:-30°, -10° 和 10°, 30°(只有这两个区域被两个传感器同时看到)
四、算法实现:扫描线法
求重叠区最常用的方法是扫描线法,也叫差分计数。
核心思想:用一个计数器记录当前位置被多少个传感器覆盖。从左到右扫描所有区间的起点和终点,计数器增减,当计数 ≥ 2 时记录重叠区间。
from dataclasses import dataclass
@dataclass
class SensorFOV:
"""传感器视野定义"""
name: str
start_deg: int
end_deg: int
def __post_init__(self):
if self.start_deg >= self.end_deg:
raise ValueError(f"{self.name}: 起点必须小于终点")
if not (-180 <= self.start_deg <= 180 and -180 <= self.end_deg <= 180):
raise ValueError(f"{self.name}: 角度超出范围")
def calc_overlap_zones(sensors: list[SensorFOV]) -> list[tuple[float, float]]:
"""计算被至少两个传感器覆盖的重叠区"""
events: list[tuple[int, int]] = []
for sensor in sensors:
events.append((sensor.start_deg, 1)) # 进入视野
events.append((sensor.end_deg, -1)) # 离开视野
events.sort(key=lambda x: x[0])
count: int = 0
overlap_start: int | None = None
result: list[tuple[float, float]] = []
for pos, delta in events:
if count >= 2 and overlap_start is not None:
# 退出重叠区
result.append((float(overlap_start), float(pos)))
overlap_start = None
count += delta
if count >= 2 and overlap_start is None:
# 进入重叠区
overlap_start = pos
return result
五、调试过程:打印日志理解算法
刚开始看这段代码时,我对 overlap_start 的赋值时机总是搞不清楚。后来我在循环里加了打印语句,一步一步追踪 count 和 overlap_start 的变化,才真正理解了它的工作原理。
count_before:0 overlap_start:None pos:-90 → count_after:1 (未进入重叠区)
count_before:1 overlap_start:-30 pos:-30 → count_after:2 (进入重叠区,记录起点)
count_before:2 overlap_start:None pos:-10 → count_after:1 (离开重叠区,输出区间)
count_before:1 overlap_start:10 pos:10 → count_after:2 (再次进入重叠区)
count_before:2 overlap_start:None pos:30 → count_after:1 (离开重叠区,输出区间)
count_before:1 overlap_start:None pos:90 → count_after:0 (结束)
最终输出:[(-30.0, -10.0), (10.0, 30.0)]
这个过程就像发动机的起动机:计数从1变成2时「点火」,记录起点;计数从2变回1时「熄火」,输出一个完整的重叠区间。
六、工程化改进
在实际项目中,传感器参数通常不会硬编码在代码里,而是来自配置文件。我把它改成了JSON读取方式:
[
{"name": "front_camera", "start_deg": -30, "end_deg": 30},
{"name": "left_radar", "start_deg": 10, "end_deg": 90},
{"name": "right_radar", "start_deg": -90, "end_deg": -10}
]
加载代码:
import json
def load_sensors_from_json(filepath: str) -> list[SensorFOV]:
with open(filepath, 'r', encoding='utf-8') as f:
data: list[dict] = json.load(f)
sensors: list[SensorFOV] = []
for item in data:
sensors.append(SensorFOV(
name=item["name"],
start_deg=item["start_deg"],
end_deg=item["end_deg"]
))
return sensors
这样做的好处是:传感器布局调整时只需要改JSON文件,不需要改代码。
七、小结
从OD考试的「合并区间」到ADAS的「传感器重叠区计算」,算法本身并不复杂,关键是找到它在实际问题中的映射。
这次经历让我明白两件事:
- 算法是工具,不是目的。掌握一个算法最好的方式是在真实场景中使用它。
- 调试是最好的学习方式。一行打印日志,胜过十行注释。