1. 基于AIS数据的会遇局面识别与分类

任务拆解:从AIS报文到会遇局面标签

本案例的输入是一段包含本船(OS)与目标船(TS)的AIS轨迹记录,每条记录至少包含MMSI、UTC时间、经度、纬度、对地航向COG、对地航速SOG。输出是每对船舶在指定时刻的会遇局面分类(对遇、交叉、追越)以及支撑该分类的量化依据:相对方位、DCPA、TCPA和相对舷角。整个处理链可以拆成四步:轨迹对齐、相对运动解算、规则判据计算、分类与依据输出。

AIS轨迹 → 会遇局面分类 处理链 AIS轨迹记录 MMSI / UTC 经度 / 纬度 COG / SOG 轨迹时间对齐 按UTC插值到同一时刻 剔除跳变/重复点 统一WGS-84坐标 相对运动解算 相对距离 D 相对方位 Brg 相对速度 Vr DCPA TCPA 最近会遇点 时间/距离 《国际海上避碰规则》判据(以本船为参照) 对遇 Head-on 相对方位 ≈ 船首正前方 |相对舷角| ≤ 5° 两船航向差 ≈ 180° 交叉 Crossing 相对方位在舷侧 5° < |相对舷角| < 112.5° 航向差介于 0°~180° 追越 Overtaking 相对方位在船尾方向 |相对舷角| > 112.5° 后船追上前船 分类输出与判定依据 局面标签 对遇 / 交叉 / 追越 相对方位 Brg 0°~360°(真北起算) DCPA / TCPA nm / min 判定依据 舷角 + 航向差 + 距离 注:相对舷角以本船船首为0°,右舷为正、左舷为负;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°/12kn180°/10kn正北 3.6nm对遇相对舷角 0°,航向差 180°
右舷交叉000°/12kn270°/10kn正东 3.0nm交叉相对舷角 +90°,航向差 90°
追越000°/12kn000°/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 数值下结论。