Sign in

    mbmot

    Detector-agnostic tracking and spatial event analytics in MoonBit

    tracking
    computer-vision
    mot
    analytics
    counting
    Download zip
    Version
    0.2.0
    License
    MIT
    Last updated
    last month
    Downloads
    75

    #MBMOT

    MBMOT 是一个用 MoonBit 编写的在线多目标跟踪库。它不读取图片,也不绑定某个检测模型;调用方逐帧交给它检测框、置信度和类别,得到按 track_id 排序的可见轨迹,以及本帧进入 lost 或 removed 的编号。仓库中的 analytics 包还可以把这条身份流转成越线、区域进出、占用、唯一目标数和停留帧数。

    在线体验 会在浏览器中加载 MoonBit 的 JavaScript 产物,重放 90 帧检测流。页面可以调整有向线和多边形区域、查看统计,并导出轨迹与空间事件 NDJSON;默认录像的检测已经预先生成,浏览器不执行检测模型推理。

    跟踪器采用八维 xyah + velocity 运动状态。高分检测先与活动、暂定和仍在保留期内的 lost 轨迹关联,未匹配的活动轨迹再尝试低分检测。低分框能维持正在活动的身份,但不会新建编号或恢复已经 lost 的轨迹。

    #安装与源码运行

    在 MoonBit 模块中加入 0.2.0:

    moon add python123-ops/mbmot@0.2.0

    这个版本包含跟踪、空间事件、NDJSON 编解码、MOT 文件读写与评测,以及网站使用的 JavaScript 桥接源码。

    需要安装 MoonBit 工具链。检出仓库后可以运行四个稳定后端的检查和测试:

    moon fmt --check moon check --target all --deny-warn moon test --target all --deny-warn

    核心包的基本调用如下:

    let tracker = @mbmot.Tracker::new(@mbmot.TrackerConfig::default())
    let box = @mbmot.BoundingBox::new(12.0, 20.0, 52.0, 80.0)
    let detection = @mbmot.Detection::new(box, 0.91, 0)
    let frame = tracker.update(1, [detection])

    let track = frame.tracks()[0]
    assert_eq(track.track_id(), 1)
    assert_eq(track.hits(), 1)

    BoundingBox::from_xywh 接受左上角加宽高,BoundingBox::from_cxcywh 接受中心点加宽高,后者可直接承接 YOLO 常见的坐标顺序。两种构造都保留输入尺度:像素坐标和归一化坐标可以使用,但同一条流必须保持一致,库不会读取图像尺寸替调用方缩放。

    #逐帧契约

    边界框使用连续坐标 xyxy,面积不采用像素端点的 +1 约定。坐标必须有限且满足 x2 > x1、y2 > y1;置信度位于 [0, 1],类别编号非负。不同类别永不关联。

    frame_id 必须严格递增。跳帧按实际帧差推进运动和失踪时间,因此一次跳过若干帧与逐帧提交空检测会到达相同的内部状态。任何非法帧都会整帧拒绝,不消耗编号,也不部分更新已有轨迹。

    公开的 Track::bbox() 始终是最近一次真实检测框;预测框只参与关联。Tracker::status() 可读取活动、暂定和 lost 数量,但不会暴露协方差或预测位置。reset() 会清空流状态,并让下一个身份重新从 1 开始。

    默认参数如下:

    参数值作用
    high_score_threshold0.25第一阶段检测下限
    low_score_threshold0.10第二阶段检测下限
    new_track_threshold0.25新建身份的最低分数
    first_match_max_cost0.80高分关联最大代价
    second_match_max_cost0.50低分关联最大代价
    max_lost_frames30lost 身份的保留帧数
    min_hits1轨迹可见前所需命中数
    fuse_scoretrue第一阶段是否融合检测分数

    #从轨迹到空间事件

    python123-ops/mbmot/analytics 接受 MBMOT 的 FrameTracks,也接受由其他跟踪器构造的 TrackSample。下面的组合调用使用检测框底边中心作为规则判定点:

    let analyzer = @analytics.SpatialAnalyzer::new(
    @analytics.AnalyticsConfig::default(),
    [
    @analytics.DirectedLine::new(
    4,
    @analytics.Point::new(320.0, 0.0),
    @analytics.Point::new(320.0, 720.0),
    ),
    ],
    [
    @analytics.PolygonRegion::new(
    7,
    [
    @analytics.Point::new(100.0, 100.0),
    @analytics.Point::new(540.0, 100.0),
    @analytics.Point::new(540.0, 620.0),
    @analytics.Point::new(100.0, 620.0),
    ],
    ),
    ],
    )

    let tracked = tracker.update(frame_id, detections)
    let spatial = analyzer.update_mbmot(frame_id, tracked)
    for event in spatial.events() {
    // LineCrossed / RegionEntered / RegionExited
    consume(event)
    }

    有向线从 start 指向 end,越线方向是 LeftToRight 或 RightToLeft。区域初次观测就在内部时,RegionEntered 的 initial 为 true;从外部进入时为 false。RegionExited 的 dwell_frames 使用真实帧号差,因此跳帧不会被压缩成一帧。

    默认需要连续两次明确位于新一侧才确认状态变化。点落在线或多边形边界时不推进证据。lost 身份不计入当前占用,但保留区域归属和进入帧;恢复后不会重复报告进入,也不会根据未观测路径推断越线。removed 清理身份的临时状态,累计统计仍保留。

    规则与轨迹必须使用同一坐标尺度。分析器不读取图像尺寸,也不会在像素和归一化坐标之间自动转换。帧号不递增、轨迹编号重复或冲突、类别变化和已移除编号的复用都会整帧拒绝。

    仓库内的六帧样例依次展示进入、越线、短暂丢失、恢复和退出:

    moon run src/analytics_demo --target native

    命令的六行输出与 examples/analytics.expected.txt 逐字比较。样例为了在六帧中展示完整事件链,显式使用 stable_frames = 1。

    外部跟踪器也可以通过 NDJSON 使用同一分析逻辑。原生命令先读取一行规则配置,后续每行接收一帧可见轨迹与生命周期编号:

    {"config":{"anchor":"bottom_center","stable_frames":1},"lines":[{"id":4,"start":[5,0],"end":[5,10]}],"regions":[{"id":7,"vertices":[[0,0],[10,0],[10,10],[0,10]]}]} {"frame":1,"tracks":[{"track_id":1,"xyxy":[-3,2,-1,5],"class_id":0}],"lost":[],"removed":[]}

    在 Bash 中重放完整样例:

    moon run src/analytics_replay --target native < examples/analytics.ndjson

    配置行不产生输出,每个帧行产生一个 JSON 对象。其中 events 是本帧确认的越线或区域状态变化,line_counts 和 region_counts 是截至当前帧的累计值,occupants 列出当前可见的区域内身份及停留帧数。完整输出保存在 examples/analytics.expected.ndjson,CI 会逐字比较两者。

    输入可以来自 MBMOT、其他跟踪器或已经保存的轨迹文件,只需提供正整数 track_id、合法 xyxy、非负 class_id,并保证 lost、removed 与可见集合不冲突。空白行会被忽略;其他错误携带物理行号并以非零状态退出,错误前已经输出的完整帧仍然有效。

    #录像演示

    tools/video_demo/video_demo.py 把录像拆成“检测、MoonBit 跟踪、MoonBit 空间分析、画面渲染”四段。默认检测器是 OpenCV 自带的全身行人 HOG,不需要下载模型;已有 YOLO 或其他检测器时,可直接传入与检测流重放相同的 NDJSON。

    PowerShell 中建立隔离环境并处理一段录像:

    py -3 -m venv .venv-video-demo .\.venv-video-demo\Scripts\python.exe -m pip install -r tools\video_demo\requirements.txt .\.venv-video-demo\Scripts\python.exe tools\video_demo\video_demo.py ` --source input.mp4 ` --output input.mbmot.mp4 ` --config examples\video-demo-config.json

    也可以先录制摄像头 300 帧,再自动完成后续跟踪和渲染:

    .\.venv-video-demo\Scripts\python.exe tools\video_demo\video_demo.py ` --source 0 ` --max-frames 300 ` --output camera.mbmot.mp4 ` --config examples\video-demo-config.json

    渲染画面包含检测框、track_id、类别和最近分数,并叠加有向线、多边形、双向计数、区域占用、唯一目标数和最近事件。同名 -artifacts 目录保留 detections.ndjson、tracks.ndjson、analytics.ndjson 和转换为像素坐标后的规则,可以单独检查每一段输出。

    外部检测结果的每行格式为:

    {"frame":1,"detections":[{"xyxy":[120,80,260,410],"score":0.91,"class_id":0}]}

    传入时增加 --detections detections.ndjson,演示程序便不再运行 HOG。这个接入面不限制 YOLO 版本或推理框架。examples/video-demo-config.json 使用归一化规则坐标,运行时按录像宽高转成像素坐标。

    当前演示是离线双遍处理:第一遍生成检测和 MoonBit 结果,第二遍渲染录像。它不保留原视频音轨;OpenCV HOG 只用于全身行人演示,车辆、工件或更复杂视角应提供外部检测流。

    仓库中保留了一段可直接查看的 90 帧输出:

    MBMOT 录像演示预览

    播放 MBMOT 空间分析录像。原始画面来源和样例口径记录在 examples/video/README.md。

    #NDJSON 重放

    原生重放命令从标准输入逐行读取:

    {"frame":1,"detections":[{"xyxy":[0,0,10,10],"score":0.95,"class_id":0}]}

    在 Bash 中运行仓库内的四帧样例:

    moon run src/replay --target native < examples/replay.ndjson

    PowerShell 可以通过 cmd 使用同一个输入文件:

    cmd /c "moon run src/replay --target native < examples\replay.ndjson"

    每个成功输入行产生一个输出行:

    {"frame":1,"tracks":[{"track_id":1,"xyxy":[0,0,10,10],"score":0.95,"class_id":0,"first_frame":1,"last_frame":1,"hits":1}],"lost":[],"removed":[]}

    字段顺序和轨迹顺序固定,仓库中的 examples/replay.expected.ndjson 是完整样例输出。遇到 JSON、检测值或帧号错误时,命令把物理输入行号写到标准错误并以非零状态退出;此前已经写出的完整输出行仍然有效。

    MBMOT 处理单摄像头、轴对齐检测框,不使用外观特征。身份延续由类别、运动预测、IoU、检测分数和生命周期共同决定。

    #MOT 序列评测

    仓库中的 evaluation 包读取 MOTChallenge 的逗号分隔二维框。标注文件接受九列或十列,第七列为零的标注不参与计数;跟踪结果要求十列,并可由 encode_tracker_results 生成。默认按 IoU >= 0.5 匹配,报告 TP、FP、FN、身份切换、MOTA、平均匹配 IoU(MOTP)以及 IDP、IDR、IDF1。

    原生命令接收标注和跟踪结果两个文件:

    moon run src/evaluate --target native examples/mot/ground-truth.txt examples/mot/tracker-results.txt

    已有 MOTChallenge 检测文件时,可以先由 MBMOT 生成十列轨迹结果。检测行接受常见的七列或十列形式;文件无需预先按帧排序,同一帧内仍保留原检测顺序:

    moon run src/mot_track --target native --release \ examples/mot/detections.txt tracks.txt

    仓库样例会输出:

    {"match_iou_threshold":0.5,"frames":3,"ground_truth_detections":3,"tracker_detections":4,"true_positives":3,"false_positives":1,"false_negatives":0,"identity_switches":1,"mota":0.3333333333333333,"motp":1,"id_true_positives":2,"id_false_positives":2,"id_false_negatives":1,"id_precision":0.5,"id_recall":0.6666666666666666,"idf1":0.5714285714285714}

    这里的评测用于自有序列回归,不执行 MOTChallenge 对行人类别、遮挡区域和 distractor 类别的官方预处理。需要提交排行榜时,应再用 TrackEval 复核同一份轨迹结果。

    仓库中的 benchmarks/MOT17-02-FRCNN.md 记录了一次完整训练序列运行的输入哈希、参数、原始计数和 TrackEval 复核结果。该记录得到 HOTA 34.895、MOTA 32.404、IDF1 39.606 和 111 次身份切换;它是可复算的训练序列结果,不是测试集排行榜成绩。

    #License

    TrackerError

    pub(all) suberror TrackerError {
    InvalidBoundingBox(String)
    InvalidScore(Double)
    InvalidClassId(Int)
    InvalidConfig(String)
    NonIncreasingFrame(previous~ : Int, received~ : Int)
    InvalidDetection(index~ : Int)
    } derive(
    Debug
    )

    Errors raised when an MBMOT value or frame violates its input contract.

    TrackerError::message

    fn TrackerError::message(self : TrackerError) -> String

    Returns a stable human-readable description of an input error.

    BoundingBox

    pub struct BoundingBox {
    x1 : Double
    y1 : Double
    x2 : Double
    y2 : Double
    } derive(Eq,
    Debug
    )

    An axis-aligned rectangle in continuous xyxy coordinates.

    BoundingBox::from_cxcywh

    fn BoundingBox::from_cxcywh(center_x : Double, center_y : Double, width : Double, height : Double) -> BoundingBox raise TrackerError

    Creates a box from its center, width, and height.

    This is the coordinate order commonly emitted by YOLO-family detectors.

    BoundingBox::from_xywh

    fn BoundingBox::from_xywh(x : Double, y : Double, width : Double, height : Double) -> BoundingBox raise TrackerError

    Creates a box from its top-left corner, width, and height.

    BoundingBox::new

    fn BoundingBox::new(x1 : Double, y1 : Double, x2 : Double, y2 : Double) -> BoundingBox raise TrackerError

    Creates a valid continuous-coordinate bounding box.

    BoundingBox::to_cxcywh

    fn BoundingBox::to_cxcywh(self : BoundingBox) -> (Double, Double, Double, Double)

    Returns the box as its center, width, and height.

    BoundingBox::to_xywh

    fn BoundingBox::to_xywh(self : BoundingBox) -> (Double, Double, Double, Double)

    Returns the box as its top-left corner, width, and height.

    BoundingBox::x1

    fn BoundingBox::x1(self : BoundingBox) -> Double

    Returns the left coordinate.

    BoundingBox::x2

    fn BoundingBox::x2(self : BoundingBox) -> Double

    Returns the right coordinate.

    BoundingBox::y1

    fn BoundingBox::y1(self : BoundingBox) -> Double

    Returns the top coordinate.

    BoundingBox::y2

    fn BoundingBox::y2(self : BoundingBox) -> Double

    Returns the bottom coordinate.

    Detection

    pub struct Detection {
    bbox : BoundingBox
    score : Double
    class_id : Int
    } derive(Eq,
    Debug
    )

    One detector observation supplied to a tracker update.

    Detection::bbox

    fn Detection::bbox(self : Detection) -> BoundingBox

    Returns the detection box.

    Detection::class_id

    fn Detection::class_id(self : Detection) -> Int

    Returns the non-negative detector class identifier.

    Detection::new

    fn Detection::new(bbox : BoundingBox, score : Double, class_id : Int) -> Detection raise TrackerError

    Creates a detection after validating its score and class identifier.

    Detection::score

    fn Detection::score(self : Detection) -> Double

    Returns the detector confidence in the closed interval [0, 1].

    FrameTracks

    type FrameTracks derive(Eq,
    Debug
    )

    The visible tracks and lifecycle events produced by one update.

    FrameTracks::lost

    fn FrameTracks::lost(self : FrameTracks) -> Array[Int]

    Returns identities that entered the recoverable lost state in this frame.

    FrameTracks::removed

    fn FrameTracks::removed(self : FrameTracks) -> Array[Int]

    Returns identities permanently removed in this frame.

    FrameTracks::tracks

    fn FrameTracks::tracks(self : FrameTracks) -> Array[Track]

    Returns visible tracks sorted by track_id.

    Track

    pub struct Track {
    track_id : Int
    bbox : BoundingBox
    class_id : Int
    score : Double
    first_frame : Int
    last_frame : Int
    hits : Int
    } derive(Eq,
    Debug
    )

    A visible tracked object for one processed frame.

    Track::bbox

    fn Track::bbox(self : Track) -> BoundingBox

    Returns the box observed in the current frame.

    Track::class_id

    fn Track::class_id(self : Track) -> Int

    Returns the class identifier fixed when the track was created.

    Track::first_frame

    fn Track::first_frame(self : Track) -> Int

    Returns the frame in which the identity was created.

    Track::hits

    fn Track::hits(self : Track) -> Int

    Returns the number of detector observations assigned to the identity.

    Track::last_frame

    fn Track::last_frame(self : Track) -> Int

    Returns the most recent frame associated with the identity.

    Track::score

    fn Track::score(self : Track) -> Double

    Returns the most recent detector confidence.

    Track::track_id

    fn Track::track_id(self : Track) -> Int

    Returns the stable identity assigned by this tracker instance.

    Tracker

    type Tracker

    Stateful online association for one ordered frame stream.

    Tracker::new

    fn Tracker::new(config : TrackerConfig) -> Tracker

    Creates an empty tracker with a validated configuration.

    Tracker::reset

    fn Tracker::reset(self : Tracker) -> Unit

    Clears every identity and restarts numbering from 1.

    Tracker::status

    fn Tracker::status(self : Tracker) -> TrackerStatus

    Summarizes retained identity state without exposing motion internals.

    Tracker::update

    fn Tracker::update(self : Tracker, frame_id : Int, detections : Array[Detection]) -> FrameTracks raise TrackerError

    Associates one strictly newer frame and returns its visible identities.

    The entire frame is validated before tracker state changes.

    TrackerConfig

    pub struct TrackerConfig {
    high_score_threshold : Double
    low_score_threshold : Double
    new_track_threshold : Double
    first_match_max_cost : Double
    second_match_max_cost : Double
    max_lost_frames : Int
    min_hits : Int
    fuse_score : Bool
    } derive(Eq,
    Debug
    )

    Settings that determine detection selection and identity association.

    TrackerConfig::default

    fn TrackerConfig::default() -> TrackerConfig

    Returns MBMOT's default association settings.

    TrackerConfig::first_match_max_cost

    fn TrackerConfig::first_match_max_cost(self : TrackerConfig) -> Double

    Returns the maximum accepted first-stage assignment cost.

    TrackerConfig::fuse_score

    fn TrackerConfig::fuse_score(self : TrackerConfig) -> Bool

    Reports whether detector scores are fused into first-stage costs.

    TrackerConfig::high_score_threshold

    fn TrackerConfig::high_score_threshold(self : TrackerConfig) -> Double

    Returns the high-confidence cutoff used by the first association stage.

    TrackerConfig::low_score_threshold

    fn TrackerConfig::low_score_threshold(self : TrackerConfig) -> Double

    Returns the low-confidence cutoff reserved for the second association stage.

    TrackerConfig::max_lost_frames

    fn TrackerConfig::max_lost_frames(self : TrackerConfig) -> Int

    Returns the configured number of frames retained after a track is lost.

    TrackerConfig::min_hits

    fn TrackerConfig::min_hits(self : TrackerConfig) -> Int

    Returns the number of hits required before a track becomes visible.

    TrackerConfig::new

    fn TrackerConfig::new(high_score_threshold~ : Double, low_score_threshold~ : Double, new_track_threshold~ : Double, first_match_max_cost~ : Double, second_match_max_cost~ : Double, max_lost_frames~ : Int, min_hits~ : Int, fuse_score~ : Bool) -> TrackerConfig raise TrackerError

    Creates a complete tracker configuration.

    TrackerConfig::new_track_threshold

    fn TrackerConfig::new_track_threshold(self : TrackerConfig) -> Double

    Returns the minimum score allowed to create a new track.

    TrackerConfig::second_match_max_cost

    fn TrackerConfig::second_match_max_cost(self : TrackerConfig) -> Double

    Returns the maximum second-stage cost reserved for low-score recovery.

    TrackerStatus

    type TrackerStatus

    A read-only summary of identities retained by a tracker.

    TrackerStatus::active_count

    fn TrackerStatus::active_count(self : TrackerStatus) -> Int

    Returns the number of confirmed identities visible in the latest frame.

    TrackerStatus::last_frame

    fn TrackerStatus::last_frame(self : TrackerStatus) -> Int?

    Returns the most recently accepted frame, or None before the first update.

    TrackerStatus::lost_count

    fn TrackerStatus::lost_count(self : TrackerStatus) -> Int

    Returns the number of confirmed identities retained for recovery.

    TrackerStatus::tentative_count

    fn TrackerStatus::tentative_count(self : TrackerStatus) -> Int

    Returns the number of active identities still below min_hits.