1. 基于AIS数据的会遇局面识别与分类
任务拆解:从AIS报文到会遇局面标签
本案例的输入是一段包含本船(OS)与目标船(TS)的AIS轨迹记录,每条记录至少包含MMSI、UTC时间、经度、纬度、对地航向COG、对地航速SOG。输出是每对船舶在指定时刻的会遇局面分类(对遇、交叉、追越)以及支撑该分类的量化依据:相对方位、DCPA、TCPA和相对舷角。整个处理链可以拆成四步:轨迹对齐、相对运动解算、规则判据计算、分类与依据输出。
关键判据的量化定义
规则条文是定性的,落到代码里必须转成可计算的阈值。本案例采用以下约定,并在正文、代码与图中保持一致:
- 相对方位 Brg:从本船指向目标船的真北方位角,范围 0°~360°,以真北为 0°、顺时针为正。
- 相对舷角 RelBearing:目标船相对本船船首的夹角,右舷为正、左舷为负,范围 -180°~+180°。计算式为
RelBearing = normalize180(Brg - COG_OS)。 - 对遇:|RelBearing| ≤ 5° 且两船航向差在 180°±5° 内,且目标船位于本船前方(相对距离在缩小)。
- 追越:|RelBearing| > 112.5°,即目标船位于本船正横后方扇区。
- 交叉:不满足对遇与追越条件,且 |RelBearing| 在 5°~112.5° 之间。
DCPA/TCPA 用于辅助判断会遇是否紧迫,但不直接决定局面类别;它们回答的是“会不会撞、多久撞”,而局面分类回答的是“谁该让路”。两者在输出中并列给出,便于后续决策模块使用。
可运行的分类实现
下面给出一个自包含的 Python 实现。输入是一段简化的 AIS 轨迹(两船各若干条记录),输出每个采样时刻的会遇局面分类与判定依据。代码不依赖第三方库,可直接运行。
import math
R_NM = 3440.065 # 地球平均半径,海里
def normalize180(deg):
"""把角度归一化到 (-180, 180]"""
while deg <= -180: deg += 360
while deg > 180: deg -= 360
return deg
def normalize360(deg):
"""把角度归一化到 [0, 360)"""
return deg % 360
def haversine_nm(lat1, lon1, lat2, lon2):
"""两点大圆距离,单位海里"""
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2
return 2 * R_NM * math.asin(math.sqrt(a))
def bearing_deg(lat1, lon1, lat2, lon2):
"""从点1指向点2的真北方位角"""
p1, p2 = math.radians(lat1), math.radians(lat2)
dl = math.radians(lon2 - lon1)
x = math.sin(dl) * math.cos(p2)
y = math.cos(p1)*math.sin(p2) - math.sin(p1)*math.cos(p2)*math.cos(dl)
return normalize360(math.degrees(math.atan2(x, y)))
def dctpa(lat1, lon1, cog1, sog1, lat2, lon2, cog2, sog2):
"""基于局部平面近似的 DCPA(nm) 与 TCPA(min)"""
# 以本船为原点的局部东-北坐标(nm)
def to_en(lat, lon):
e = math.radians(lon - lon1) * math.cos(math.radians(lat1)) * R_NM
n = math.radians(lat - lat1) * R_NM
return e, n
e2, n2 = to_en(lat2, lon2)
# 速度分量(kn -> nm/min)
v1e = sog1 * math.sin(math.radians(cog1)) / 60.0
v1n = sog1 * math.cos(math.radians(cog1)) / 60.0
v2e = sog2 * math.sin(math.radians(cog2)) / 60.0
v2n = sog2 * math.cos(math.radians(cog2)) / 60.0
# 相对位置与相对速度
re, rn = e2, n2
ve, vn = v2e - v1e, v2n - v1n
v2 = ve*ve + vn*vn
if v2 < 1e-12:
return math.hypot(re, rn), float('inf')
tcpa = -(re*ve + rn*vn) / v2
dcpa = math.hypot(re + ve*tcpa, rn + vn*tcpa)
return dcpa, tcpa
def classify(brg, cog_os, cog_ts, dist_nm, dcpa, tcpa):
"""返回 (局面, 判定依据字典)"""
rel = normalize180(brg - cog_os)
hdg_diff = abs(normalize180(cog_ts - cog_os))
evidence = {
"相对方位": round(brg, 1),
"相对舷角": round(rel, 1),
"航向差": round(hdg_diff, 1),
"DCPA_nm": round(dcpa, 3),
"TCPA_min": round(tcpa, 1) if math.isfinite(tcpa) else None,
"距离_nm": round(dist_nm, 3),
}
if abs(rel) <= 5 and abs(hdg_diff - 180) <= 5:
return "对遇", evidence
if abs(rel) > 112.5:
return "追越", evidence
return "交叉", evidence
# ---- 教学示例数据(示例,非实测)----
# 每条: (mmsi, utc_sec, lat, lon, cog, sog)
os_track = [
(111111111, 0, 30.0000, 122.0000, 0.0, 12.0),
(111111111, 60, 30.0100, 122.0000, 0.0, 12.0),
(111111111, 120, 30.0200, 122.0000, 0.0, 12.0),
]
ts_track = [
(222222222, 0, 30.0600, 122.0000, 180.0, 10.0), # 对遇
(222222222, 60, 30.0500, 122.0000, 180.0, 10.0),
(222222222, 120, 30.0400, 122.0000, 180.0, 10.0),
]
# 按时间对齐后逐点分类
for i in range(len(os_track)):
_, t, lat1, lon1, c1, s1 = os_track[i]
_, _, lat2, lon2, c2, s2 = ts_track[i]
d = haversine_nm(lat1, lon1, lat2, lon2)
b = bearing_deg(lat1, lon1, lat2, lon2)
dcpa, tcpa = dctpa(lat1, lon1, c1, s1, lat2, lon2, c2, s2)
label, ev = classify(b, c1, c2, d, dcpa, tcpa)
print(f"t={t:>3}s 局面={label} 依据={ev}")
运行后预期输出(示例数据直接推出,非实测):
t= 0s 局面=对遇 依据={'相对方位': 0.0, '相对舷角': 0.0, '航向差': 180.0, 'DCPA_nm': 0.0, 'TCPA_min': 18.0, '距离_nm': 3.6}
t= 60s 局面=对遇 依据={'相对方位': 0.0, '相对舷角': 0.0, '航向差': 180.0, 'DCPA_nm': 0.0, 'TCPA_min': 12.0, '距离_nm': 2.4}
t=120s 局面=对遇 依据={'相对方位': 0.0, '相对舷角': 0.0, '航向差': 180.0, 'DCPA_nm': 0.0, 'TCPA_min': 6.0, '距离_nm': 1.2}
三类局面的验证用例
仅用一组对遇数据无法验证分类器是否真的区分了三类局面。下面补充交叉与追越的构造用例,并给出预期标签,便于回归测试。
| 用例 | 本船 COG/SOG | 目标船 COG/SOG | 目标船相对位置 | 预期局面 | 关键判据 |
|---|---|---|---|---|---|
| 对遇 | 000°/12kn | 180°/10kn | 正北 3.6nm | 对遇 | 相对舷角 0°,航向差 180° |
| 右舷交叉 | 000°/12kn | 270°/10kn | 正东 3.0nm | 交叉 | 相对舷角 +90°,航向差 90° |
| 追越 | 000°/12kn | 000°/8kn | 正南 2.0nm | 追越 | 相对舷角 180°,航向差 0° |
验证时先看相对舷角是否落在对应扇区,再看航向差是否满足对遇的 180°±5° 条件。若两者冲突(例如舷角接近 0° 但航向差只有 90°),应判为交叉而非对遇——这正是规则中“对遇需航向相反”的约束。
常见误判与排查
- 把追越误判为交叉:多因相对舷角用了 0°~360° 而非 -180°~180°,导致船尾方向被算成 180° 附近的正角。检查
normalize180是否在比较前调用。 - 对遇漏判:航向差阈值过严。规则要求“几乎相反”,工程上常用 ±5°,若数据噪声大需先对 COG 做平滑。
- DCPA 为负或 TCPA 为负:说明最近会遇点已过去,此时局面分类仍有效,但紧迫性判断应改用当前距离。
- 坐标单位混用:经纬度差值直接当海里用会引入随纬度变化的误差,务必用 haversine 或局部平面近似并乘 cos(lat)。
本案例的 DCPA/TCPA 基于匀速直线假设,仅用于局面分类的辅助判据。实际避碰决策还需考虑船舶操纵性、风流压差和规则中的让路责任划分,不能仅凭 DCPA 数值下结论。