ocsort

7 downloads 0 likes on ModelScope updated

Model card

基于OC_SORT的非线性抗遮挡多目标跟踪算法

1. 摘要

  1. 身份编号:fwb0002

  2. 类型与序号:模型1

  3. 进展:

    item status detail
    方案选择与硬件资源划分 1
    模型导出 1
    ~导出模型精度一致,提供python验证脚本 1
    cpu处理部分c++工程移植 1
    ~c++工程精度一致,提供验证脚本 1 在 Dancetrack-val 和 MOT17-train 数据集上已完成精度对齐,包括 FP32、INT8、INT16 和混合精度量化
    ~c++工程性能优化 1 进行各端侧的耗时测试,并在作者源码基础上进行耗时优化,对于后处理的 Tracking 部分速率平均提升 8.04%
    icraft编译通过 1 由于和 ByteTrack 都属于MOT类型,出于对照试验,也选择了 YOLOX 的主干,具体分别再选择s和x的模型对比,因此编译流程参考 ByteTrack 进行
    ~存在量化精度调优工作 1 针对单个算子的量化,最好的效果能让余弦相似度提升 5.8%,相较于 INT8 以 5.74% 权重增加的代价实现 8.71% 的IDF1提升以及 20.25% 的MOTA提升
    ~完成基本pyrt/crt搭建与结果验证 1 完成了crt,并修复官方部署源码精度不一致问题,完成 PC 端仿真、psin socket 与 plin axi部署与结果验证
    ~完成pyrt/crt精度测试与对齐 1 采用同一个det数据包含每一帧的检测结果,参考 python 源码修改 c++ 后处理,最终完成精度对齐工作
    完成上板耗时测试 1 分别完成 psin socket 与 plin axi 的耗时测试(使用imagemake与detpost硬算子),发现 psin 的数据传出耗时较大,plin 的数据传入耗时较大(受摄像头60Hz输入限制),icore 耗时较为合理在 9.5ms 左右(s模型)
    ~使用运行时提供的api进行基本性能优化 1 包括混合精度量化调优,其余如内存复用等方案不适合在本模型上实现
    ~存在整体方案各环节优化或优化方案的设计 1 同c++工程性能优化,代码层面实现 8.04% 的跟踪速率提升
    提出bug数 1 大概率是板子端配置问题,混合精度量化部分仿真没有任何问题,但是上板跑会在前向时跳进反汇编,没有报错只是在第一帧直接停止,由于拿库里现有的 bytetrack 同样混合精度量化也是一样问题,所以判断应该是板子配置有问题

2. 方案简介

2.1 算法流程介绍

2.1.1 算法背景

   OC-SORT(Observation-Centric SORT, from CVPR2022 )是一种改进版的SORT(Simple Online and Realtime Tracking)多目标跟踪算法, 专门设计用于解决遮挡和非线性运动场景下的跟踪问题。
   OC-SORT致力于解决:传统SORT算法在拥挤场景和非线性运动中跟踪鲁棒性不足、目标遮挡时容易丢失跟踪、非线性运动预测准确性低,这三个关键问题。
   对于性能评价,OC-SORT在保持简单、在线、实时特性的同时,显著提升了跟踪鲁棒性。在MOT17、MOT20基准测试中表现优异,特别是在处理遮挡和 非线性运动方面(DanceTrack数据集)。该算法被广泛应用于各种场景,包括行人跟踪、车辆跟踪等,在同类算法中表现出色。

2-1
图2-1 同类算法在 MOT17-test、MOT20-test 测试集上的性能对比

   根据论文所给同类算法测试结果,OC-SORT在主流多目标跟踪算法中表现出色,特别是在身份保持能力方面优势显著。在MOT17-test/MOT20-test标准行人跟踪数据集上, OC-SORT的MOTA指标分别为78.0和75.5,虽然比当前最强的ByteTrack算法低约2-3个百分点,但在IDF1指标上实现了反超,分别达到77.5和75.9,证明了其在目标身份连续性方面的优势。 更值得注意的是,在更具挑战性的DanceTrack(具有强非线性特征和遮挡问题)数据集上,OC-SORT的IDF1指标达到54.6,大幅领先包括ByteTrack(52.5)在内的所有对比算法。
   总体而言,OC-SORT展现出"以少量检测精度换取显著身份连续性提升"的技术特点。相比ByteTrack等主流算法,OC-SORT在处理非线性运动和复杂场景时表现更加稳定, 特别适合对目标身份保持要求较高的应用场景。这种性能特征使得OC-SORT成为在跟踪连续性比检测精度更重要的应用中的优选算法。

2-2
图2-2 同类算法在 DanceTrack(遮挡问题突出与显著非线性运动)测试集上性能对比

2.1.2 算法流程梳理

   算法流程框图如下:

2-3
图2-3 OC-SORT 算法流程图

   输入处理阶段:接收当前帧的目标检测结果,利用卡尔曼滤波对现有轨迹进行状态预测,为后续关联提供预测位置。
   关联匹配阶段:基于IoU相似度进行观察关联匹配,判断检测结果与预测轨迹的对应关系。对于匹配成功的执行轨迹更新,对于未匹配的检测则初始化新轨迹。
   核心算法阶段:依次执行OC-SORT的三大创新模块:ORU(观察中心重更新)回溯历史状态重新计算轨迹;OCM(观察中心动量)利用历史观察 信息平滑当前状态估计;OCR(观察中心恢复)**尝试恢复丢失的轨迹并重新建立关联。
   输出控制阶段:执行轨迹生存期管理,删除无效轨迹,输出当前帧的跟踪结果(包含目标ID、位置、置信度),并通过循环控制继续处理下一帧或结束跟踪。
   整个流程形成完整的闭环处理,通过三大核心创新模块显著提升了算法在复杂场景下的跟踪鲁棒性和目标身份保持能力。

2.1.3 模型结构上的创新与意义

   OC-SORT最重要的结构创新在于提出了"观察中心"(Observation-Centric)的设计理念,突破了传统SORT算法仅依赖当前观察进行状态更新的局限性。 该算法在保持SORT简洁高效架构的基础上,创新性地引入了历史观察信息的深度利用机制,将单帧处理扩展为多帧历史信息融合处理。通过构建观察历史缓存和虚拟轨迹重放机制, OC-SORT实现了从"被动响应"到"主动预测与恢复"的根本性转变,形成了更加鲁棒的跟踪架构。
   其结构创新集中体现在三个核心模块的有机结合上。ORU(观察中心重更新)通过构建虚拟轨迹重放系统,当目标重新被检测时能够回溯历史状态并重新计算整个轨迹序列, 有效解决了遮挡恢复时的状态不一致问题;OCM(观察中心动量)引入了观察历史加权机制,利用历史观察信息对当前状态估计进行平滑优化,显著提升了非线性运动和噪声环境下的预测精度; OCR(观察中心恢复)建立了智能轨迹恢复机制,能够在轨迹丢失后通过历史模式匹配重新建立目标关联,大幅提升了长期遮挡场景下的跟踪连续性。

2.1.4 前后处理

   本多目标跟踪算法的核心部分,实际上就算是整个模型的后处理,主干网络可以选取YOLOX、YOLO11等其他目标检测算法,这里给出原作者论文中的算法关键机制流程图。

2-4
图2-4 OC-SORT 论文中关键算法机制流程图

   上图展示了从frame t到frame t+2三帧连续处理过程中算法的关键创新机制。图中采用黄色框表示正常跟踪轨迹、红色框表示检测边界框、蓝色框表示特殊处理目标,数字标签代表目标ID。 整个演示分为上半部分的检测结果(Detections)、中间的算法处理状态(Estimates)和下半部分的跟踪轨迹结果(Tracks),清晰地展现了OC-SORT相比传统算法的处理优势。 从frame t到frame t+1展示了正常的KF Predict卡尔曼滤波预测流程,算法通过绿色箭头指示的预测过程将检测结果与轨迹进行关联匹配和状态更新。
   最关键的创新演示体现在frame t+1到frame t+2的OCR(观察中心恢复)机制工作过程中。当某个目标(如ID=1)在frame t+1中由于遮挡等原因暂时丢失, 在frame t+2中重新出现时, 图中通过对比"Tracks w/o OCR"和"Tracks w/ OCR"两种处理结果,清晰展示了OCR机制的核心价值。不使用OCR的传统方法可能导致ID切换或轨迹断裂, 而OCR机制能够记住历史轨迹特征, 当目标重新出现时正确识别并恢复原有ID,保持轨迹的连续性。右侧流程图进一步展示了完整的处理链路: KF Predict → OCR → Ass. w/ OCM(结合观察中心动量的关联) → ORU(观察中心重更新),这个四步处理流程体现了OC-SORT三大核心机制的有机结合。
   OC-SORT成功解决了多目标跟踪中的核心挑战:ID切换、轨迹断裂和状态不一致问题。该算法通过深度利用历史观察信息,实现了从"被动响应"到"主动预测与恢复"的根本转变, 显著提升了IDF1(身份保持)指标,使其在复杂场景下的跟踪性能远超传统算法。这张图完美诠释了OC-SORT"观察中心"设计理念的核心价值,展现了算法在保持计算效率的同时如何实现更加鲁棒和智能的多目标跟踪能力

2.2 硬件资源分配

NPU:目标检测算法(当前为了和模型库中 Bytetrack 进行对照,使用了 YOLOX-s)的 Backbone 部分,也就是经过 1_save.py 去除后处理后剩下的主干网络。

CPU:整个后处理部分除去使用现有的 DetPost,大部分后处理部分由于运算结构不固定,相对比较灵活复杂,但是计算量相对卷积并不大,因此目前都使用 CPU 来进行计算。

FPGA:大部分检测模型都会用到的预处理硬算子 ImageMake 与后处理硬算子 DetPost。后续根据进度以及实现成本,考虑在后处理方面适配新的硬算子加速。

4. 模型导出说明

4.1 算子适配与模型导出评估

   本 OC-SORT 算法的目标检测部分,由于需要和模型库中的 Bytetrack 形成对照,并便于后续进行精度测试与抗遮挡能力对比,采取了库中 Bytetrack 相同的主干检测网络 YOLOX-s 和 YOLOX-x,这样能够清晰地反映多目标跟踪的性能差异。
   因此,目前整套算法的YOLOX-s检测部分需要在 NPU 上进行计算,主干部分也相对前后处理架构比较固定,NPU 计算效率相对较高,而前后处理一些灵活 而计算量相对卷积并不大的部分,交由 CPU 来统一计算,并可以用现有的硬算子进行对特定的网络部分进行加速,如 ImageMake、DetPost。所以,模型导出部分应该尽量 剔除模型中的前后处理部分较为复杂的计算,而只保留目标检测部分的主干网络,包括算子如:Conv2d、Batch_norm、SiLu、MaxPool等。

4.2 导出过程修改点

  • 主要在 ByteTrack 导出脚本上进行修改,同理包括0_infer.py、1_save.py、2_infer.py,替换函数:new_forward、get_image_list、write_results、image_demo、imageflow_demo、main 以及检测头类:Predictor,全部更改为OCSORT源码的算法流程。最主要是 imageflow_demo 函数部分,下面插入关键修改代码,详细可见 1_scripts/1_save.py。
tracker = OCSort(det_thresh=0.6, iou_threshold=0.3, use_byte=True)
    timer = Timer()
    frame_id = 0
    results = []
    while True:
        if frame_id % 20 == 0:
            logger.info('Processing frame {} ({:.2f} fps)'.format(frame_id, 1. / max(1e-5, timer.average_time)))
        ret_val, frame = cap.read()
        if ret_val:
            outputs, img_info = predictor.inference(frame, timer)
            if outputs[0] is not None:
                online_targets = tracker.update(outputs[0], [img_info['height'], img_info['width']], exp.test_size)
                online_tlwhs = []
                online_ids = []
                for t in online_targets:
                    tlwh = [t[0], t[1], t[2] - t[0], t[3] - t[1]]
                    tid = t[4]
                    vertical = tlwh[2] / tlwh[3] > args.aspect_ratio_thresh
                    if tlwh[2] * tlwh[3] > args.min_box_area and not vertical:
                        online_tlwhs.append(tlwh)
                        online_ids.append(tid)
                        results.append(
                            f"{frame_id},{tid},{tlwh[0]:.2f},{tlwh[1]:.2f},{tlwh[2]:.2f},{tlwh[3]:.2f},1.0,-1,-1,-1\n"
                        )

4.3 脚本运行说明

  • 注意:exp_file 需要正确的 demo 路径,如果需要导出 x 模型就必须是 x 的检测 demo 文件,需要与模型对应。
cd 1_scripts/
python 1_save.py

# 重要参数说明,其余参数可用默认
--c           # 模型权重文件路径
--path        # 数据集路径
--trace_path  # 导出结果保存路径

5. cpu处理部分c++工程移植说明

5.1 整体运行介绍与关键算法实现

   OC-SORT 算法的后处理部分,主要是对目标检测结果进行关联匹配、轨迹更新和状态恢复等操作。由于这些操作相对灵活且计算量不大,适合在 CPU 上进行处理。 详细流程可参考 2.1.2 算法流程梳理,包含输入处理阶段、关联匹配阶段、核心算法阶段与输出控制阶段。
   OCSORT 核心算法最终融合为4个 STEP:
   STEP1: First round of association(第一轮关联)。使用 IOU (Intersection Over Union) 或 GIOU 等关联度量方法,将当前帧的检测框(dets_first)与现有轨迹(trks)进行初步匹配。 产生三种结果,matched(成功匹配的检测-轨迹对)、 unmatched_dets(未匹配的检测框)和 unmatched_trks(未匹配的轨迹)。对于匹配成功的轨迹,使用对应的检测框信息进行更新。

std::vector<Eigen::Matrix<int, 1, 2>> matched;// 数组内 元素形状是(1,2)
std::vector<int> unmatched_dets;
std::vector<int> unmatched_trks;

auto result = associate(dets_first, trks, iou_threshold, velocities, k_observations, inertia);
matched = std::get<0>(result);
unmatched_dets = std::get<1>(result);
unmatched_trks = std::get<2>(result);

for (auto m: matched) {
    Eigen::Matrix<float, 5, 1> tmp_bbox;
    tmp_bbox = dets_first.block<1, 5>(m(0), 0);
    trackers[m(1)].update(&(tmp_bbox), dets_first(m(0), 5));
}

   STEP2: Second round of association by OCR(基于 OCR 的第二轮关联)。利用 BYTE 算法思想,低置信度的检测(dets_second)与未匹配的轨迹(unmatched_trks)进行二次关联, 允许使用更宽松的匹配阈值(通常比第一轮低)来尝试恢复可能的漏匹配,并对二次匹配成功的轨迹进行更新。

if (true == use_byte && dets_second.rows() > 0 && unmatched_trks.size() > 0) {
    Eigen::MatrixXf u_trks(unmatched_trks.size(), trks.cols());
    int index_for_u_trks = 0;
    for (auto i: unmatched_trks) {
        u_trks.row(index_for_u_trks++) = trks.row(i);
    }
    Eigen::MatrixXf iou_left = asso_func(dets_second, u_trks);
    if (iou_left.maxCoeff() > iou_threshold) {
        std::vector<std::vector<float>> iou_matrix(iou_left.rows(), std::vector<float>(iou_left.cols()));
        for (int i = 0; i < iou_left.rows(); i++) {
            for (int j = 0; j < iou_left.cols(); j++) {
                iou_matrix[i][j] = -iou_left(i, j);
            }
        }
        std::vector<int> rowsol, colsol;
        float MIN_cost = execLapjv(iou_matrix, rowsol, colsol, true, 0.01, true);
        std::vector<std::vector<int>> matched_indices;
        for (int i = 0; i < rowsol.size(); i++) {
            if (rowsol.at(i) >= 0) {
                matched_indices.push_back({colsol.at(rowsol.at(i)), rowsol.at(i)});
            }
        }
        std::vector<int> to_remove_trk_indices;
        for (auto m: matched_indices) {
            int det_ind = m[0];
            int trk_ind = unmatched_trks[m[1]];
            if (iou_left(m[0], m[1]) < iou_threshold) continue;
            Eigen::Matrix<float, 5, 1> tmp_box;
            tmp_box = dets_second.block<1, 5>(det_ind, 0);
            trackers[trk_ind].update(&tmp_box, dets_second(det_ind, 5));
            to_remove_trk_indices.push_back(trk_ind);
        }
        std::vector<int> tmp_res1(unmatched_trks.size());
        sort(unmatched_trks.begin(), unmatched_trks.end());
        sort(to_remove_trk_indices.begin(), to_remove_trk_indices.end());
        auto end1 = set_difference(unmatched_trks.begin(), unmatched_trks.end(),
                                   to_remove_trk_indices.begin(), to_remove_trk_indices.end(),
                                   tmp_res1.begin());
        tmp_res1.resize(end1 - tmp_res1.begin());
        unmatched_trks = tmp_res1;
    }
}

   STEP3: Update status of second matched tracks(第二次匹配轨迹的状态更新)。在第一轮未匹配的检测(unmatched_dets)和未匹配的 轨迹(unmatched_trks)之间进行第三次匹配尝试,对这次匹配成功的轨迹进行状态更新,并更新未匹配检测和轨迹的列表。

for (auto i: rematched_indices) {
    int det_ind = unmatched_dets[i.at(0)];
    int trk_ind = unmatched_trks[i.at(1)];
    if (iou_left(i.at(0), i.at(1)) < iou_threshold) {
        continue;
    }
    Eigen::Matrix<float, 5, 1> tmp_bbox;
    tmp_bbox = dets_first.block<1, 5>(det_ind, 0);

    trackers.at(trk_ind).update(&tmp_bbox, dets_first(det_ind, 5));
    to_remove_det_indices.push_back(det_ind);
    to_remove_trk_indices.push_back(trk_ind);
}
std::vector<int> tmp_res(unmatched_dets.size());
sort(unmatched_dets.begin(), unmatched_dets.end());
sort(to_remove_det_indices.begin(), to_remove_det_indices.end());
auto end = set_difference(unmatched_dets.begin(), unmatched_dets.end(),
                          to_remove_det_indices.begin(), to_remove_det_indices.end(),
                          tmp_res.begin());
tmp_res.resize(end - tmp_res.begin());
unmatched_dets = tmp_res;// 更新
std::vector<int> tmp_res1(unmatched_trks.size());
sort(unmatched_trks.begin(), unmatched_trks.end());
sort(to_remove_trk_indices.begin(), to_remove_trk_indices.end());
auto end1 = set_difference(unmatched_trks.begin(), unmatched_trks.end(),
                           to_remove_trk_indices.begin(), to_remove_trk_indices.end(),
                           tmp_res1.begin());
tmp_res1.resize(end1 - tmp_res1.begin());
unmatched_trks = tmp_res1;

   STEP4: Initialize new tracks and remove expired tracks(初始化新轨迹和移除过期轨迹)。进行新轨迹初始化,为仍然未匹配的检测创建新轨迹, 收集满足条件的轨迹作为当前帧的跟踪结果输出,并移除长时间未更新的轨迹(time_since_update > max_age)。

for (int i: unmatched_dets) {
    Eigen::RowVectorXf tmp_bbox = dets_first.block(i, 0, 1, 5);
    int cls_ = int(output_results(i, 5));
    KalmanBoxTracker trk = KalmanBoxTracker(tmp_bbox, cls_, delta_t);
    trackers.push_back(trk);
}
int tmp_i = trackers.size();
for (int i = trackers.size() - 1; i >= 0; i--) {
    Eigen::Matrix<float, 1, 4> d;
    int last_observation_sum = trackers.at(i).last_observation.sum();
    if (last_observation_sum < 0) {
        d = trackers.at(i).get_state();
    } else {
        d = trackers.at(i).last_observation.block(0, 0, 1, 4);
    }
    if (trackers.at(i).time_since_update < 1 && ((trackers.at(i).hit_streak >= min_hits) | (frame_count <= min_hits))) {
        Eigen::RowVectorXf tracking_res(7);
        tracking_res << d(0), d(1), d(2), d(3), trackers.at(i).id + 1, trackers.at(i).cls, trackers.at(i).conf;
        ret.push_back(tracking_res);
    }
    if (trackers.at(i).time_since_update > max_age) {
        trackers.erase(trackers.begin() + i);
    }
}

5.2 精度对齐与测试流程

5.2.1 精度对齐

  • 说明:OCSORT 原作者的 deploy 代码未完全优化,与 Python 源码精度无法对齐,因此在其基础上进行了较长时间的问题排查与修改,最终真正完成精度对齐, 能够基本复现论文中给出的精度。下面依据重要程度列出主要修改点:
  1. 源码里卡尔曼滤波更新时,恢复数据不完整导致精度丢失,具体是丢失了几个关键变量如:P_post、history_obs、observed。
/* kalmanfilter.cpp */
void KalmanFilterNew::unfreeze() {
if (true == attr_saved.IsInitialized) {
    new_history = history_obs;
    /* 原始变量,后面需要加上丢失的关键变量 */
    x_prior = attr_saved.x_prior;
    P_prior = attr_saved.P_prior;
    x_post = attr_saved.x_post;
    P_post = attr_saved.P_post;
    history_obs = attr_saved.history_obs;
    observed = attr_saved.observed;

    history_obs.erase(history_obs.end() - 1);

    Eigen::VectorXf box1;
    Eigen::VectorXf box2;

    int lastNotNullIndex = -1;
    int secondLastNotNullIndex = -1;
    for (int i = new_history.size() - 1; i >= 0; i--) {
        if (new_history[i].size() > 0) {
            if (lastNotNullIndex == -1) {
                lastNotNullIndex = i;
                box2 = new_history.at(lastNotNullIndex);
            } else if (secondLastNotNullIndex == -1) {
                secondLastNotNullIndex = i;
                box1 = new_history.at(secondLastNotNullIndex);
                break;
            }
        }
    }
/* 其余代码 */
}
  1. unfreeze 函数里里换成 python 版本的 update 方式,源码中这部分会导致不对齐,这里还是使用了 python 源码中的 predict-update loop 方法。
// 同样是 unfreeze 函数中:
if (true == attr_saved.IsInitialized) {
    /* 其余代码 */
    for (int i = 0; i < time_gap; i++) {
        double x = x1 + (i + 1) * dx;
        double y = y1 + (i + 1) * dy;
        double w = w1 + (i + 1) * dw;
        double h = h1 + (i + 1) * dh;
        double s = w * h;
        double r = w / (h * 1.0);
        Eigen::VectorXf new_box(4, 1);
        new_box << x, y, s, r;

        update(new_box);
        if (i != (time_gap - 1))
            predict();
    }
}
  1. OCsort update方法里,缺失了删除历史无效跟踪器的方法,包括两种情况,超出max_age删除的跟踪器,以及预测时出现无效位置,输出为NAN的情况
/* OCsort.cpp → update 函数*/
for (int i = 0; i < trks.rows(); i++) {
    Eigen::RowVectorXf pos = trackers[i].predict();
    trks.row(i) << pos(0), pos(1), pos(2), pos(3), 0;
    // 添加了对于nan的删除
    if(any_nan(pos))
        to_del.push_back(i);
}
/* 反向删除无效行和无效跟踪器 */
{
    trks = compress_invalid_rows(trks);
    std::vector<int>::const_reverse_iterator rit;
    for(rit = to_del.rbegin(); rit != to_del.rend(); rit++)
        trackers.erase(trackers.begin() + (*rit));
}
  1. obj_tracking 函数将检测目标结果转换成 OCSORT 适配的跟踪输入格式,源码中并不是针对该平台做的跟踪,导致目标框丢失,需要更换为适配 obj_tracking 的格式
/* postprocess_ocsort_test.hpp */
inline void obj_tracking(const std::vector<std::vector<float>>& dets,
                     PicPre_ocsort& img, OCSort& tracker,
                     std::vector<std::array<float, 10>>& res_export,
                     cv::Mat& res_img,
                     int frame_id)
{
    /* 组装 OCSORT 输入 */
    std::vector<std::vector<float>> data;
    for (const auto& i : dets) {
        std::vector<float> row;
        cv::Rect2f box;
        int class_id;
        float confidence;
        box.x = i[1];
        box.y = i[2];
        box.width = i[3];
        box.height = i[4];
        class_id = 0;
        confidence = i[5];

        row.push_back(box.x);
        row.push_back(box.y);
        row.push_back(box.x + box.width);
        row.push_back(box.y + box.height);
        row.push_back(confidence);
        row.push_back(class_id);

        data.push_back(row);
        // cv::rectangle(res_img, box, cv::Scalar(0, 255, 0), 2);        // 绘制所有检测框用于对比跟踪
        }
    /* 其余代码 */
}
  1. 源码中作者可能疏忽,其每一帧向跟踪器传递的检测目标只有一个,具体是 for 循环的作用域扩展到了 tracker 导致每向 data 里 push_back 一个检测目标,都会调用一次 tracker。 现在更改为收集完所有目标并push_back后,一次性全部传递给tracker以进行位置更新。
// 同上述代码块(已修改),原始的逻辑为每向 data 里 push_back 一个检测目标,都会调用一次 tracker,导致每一帧向跟踪器传递的检测目标只有一个。
  1. 源码中当某一帧检测目标为空时,即没有检测目标时的特殊情况,直接导致程序终止停止跟踪。现在在更新前加上了检测空集判断以防止程序意外终止。
/* postprocess_ocsort_test.hpp → obj_tracking 函数 */
if (!data.empty()) {
    Eigen::Matrix<float, Eigen::Dynamic, 6> det_matrix = Vector2Matrix(data);
    std::vector<Eigen::RowVectorXf> res = tracker.update(det_matrix);
    for (auto j : res) {
        auto frame = (float)(frame_id)+1;
        auto bb_left = j[0];
        auto bb_top = j[1];
        auto bb_width = j[2] - j[0] + 1;
        auto bb_height = j[3] - j[1] + 1;
        auto conf = j[6];
        int ID = int(j[4]);
        int Class = int(j[5]);
        std::array<float, 10> each_line = { frame, ID, bb_left, bb_top, bb_width, bb_height, conf, -1.f, -1.f, -1.f };
        res_export.push_back(each_line);

        cv::Scalar s = tracker.get_color(ID);
        cv::putText(res_img, cv::format("%d", ID), cv::Point(bb_left, bb_top), 0, 0.6, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
        cv::rectangle(res_img, cv::Rect(bb_left, bb_top, bb_width, bb_height), s, 2);
    data.clear();
    }
}

5.2.2 精度对齐测试 Debug 流程

  • 整体思路: 首先测试 python 源码在 DanceTrack-val 数据集上的精度,确保正确后再根据该源码修改 C++ 版本代码以实现精度对齐。
  • 具体流程: 分别写一个 test.py 和 test.cpp (详细可见3_deploy/modelzoo/ocsort/src/)来接收同一个目标检测数据,该数据含同一数据集下的每一帧检测结果。 由于两个版本对于精度控制的规范不同,因此首先控制两个版本的精度和四舍五入方式保持相同。在此基础上,开始比较几个关键数据如:velocities、cost_matrix、last_boxes 等。 跑完整个数据集(或自定义跑一定的帧数)后,如果发现最后结果不同,则回溯到数据开始不同的那一帧,根据上面等一些关键数据来排查 tracker、kalmanfilter、association 三个主要变量, 找出主要出问题的点进行修改,如此反复迭代直到精度完全对齐。
  • 下面给出用于 Debug 的部分关键代码片段:
/* oc_debug.h */
#ifndef OC_DEBUG_H
#define OC_DEBUG_H

#define OC_DEBUG
#undef OC_DEBUG

#ifdef OC_DEBUG
#include <fstream>
#include <iostream>
#include <iomanip> // 包含 setprecision 所需的头文件
    #define OC_VAL_DEBUG        // OCSORT源码里面关键变量,比如 velocities, last_boxes, k_observations 等
    #undef OC_VAL_DEBUG

    #define OC_TRACKER_DEBUG    // 主要是跟踪器的调试信息,跟踪器的更新、预测等
    #undef OC_TRACKER_DEBUG

    #define OC_TRACKER_PREDICT_DEBUG
    #undef OC_TRACKER_PREDICT_DEBUG

    #define OC_ASSOC_DEBUG      // 关联相关的调试信息,比如 associate 函数的输入输出
    #undef OC_ASSOC_DEBUG

    #define OC_KAL_DEBUG        // 卡尔曼滤波器相关的调试信息,比如卡尔曼滤波器的状态、预测等
    #undef OC_KAL_DEBUG

    #define OC_KALBOXTRK_DEBUG  // KalmanBoxTracker 相关的调试信息,比如跟踪器的状态、预测等
    #undef OC_KALBOXTRK_DEBUG

    #define OC_UTILI_DEBUG      // 一些工具函数的调试信息,比如打印矩阵、向量等
    #undef OC_UTILI_DEBUG

    #define OC_RES_OUT_DEBUG    // 输出结果相关的调试信息,比如跟踪器的输出结果等
    #undef OC_RES_OUT_DEBUG

    #define OC_PYTHON_PREALIGN  // Python精度对齐相关的调试信息
    #undef OC_PYTHON_PREALIGN

    #define OC_DEBUG_START  {
    #define OC_DEBUG_END    }

    #define OC_TRACKER_DEBUG_NUM    17      // 跟踪器调试编号
/* 其他调试代码 */
  • 用于对照的 test.py 关键代码:
tracker = OCSort(0.6, 30, 3, 0.3, 3, "iou", 0.2, False)
tracker.enable_debug = False

def read_data_and_remove_last(file_path):
    with open(file_path, 'r') as file:
        lines = file.readlines()
    # 处理每行数据,去掉最后一个0,并转化为numpy数组
    if len(lines) != 0:
        array = np.array([list(map(float, line.strip().split()))[:-1] for line in lines])
    else:
        array = None
    return array

if __name__  == "__main__":
    print("Hello")
    np.set_printoptions(suppress=True)
    # tracker.enable_debug = True;
    for i in range(1, 1201):
        print(f"Round: {i}")
        directory = "18frames/"
        filename = f"{i}.txt"
        filename = directory + filename
        matrix = read_data_and_remove_last(filename)
        if i == 3333:
            print("-------- Diff --------")
            tracker.debug_tracker_num = 6
            tracker.enable_debug = True
        online_targets = tracker.update(matrix, [1920, 1080], [1920, 1080])
        print(f"Result {i}: \n" + f"{online_targets}\n")
  • 用于测试的 test.cpp 关键代码:
Eigen::Matrix<float, Eigen::Dynamic, 6> read_csv_to_eigen_xyxy(const std::string& filename) {
    /* 读取逐帧目标检测结果 csv 文件并解析,原理同 python,此处省略相关代码 */
    // 转换为Eigen::Matrix
    Eigen::Matrix<float, Eigen::Dynamic, 6> matrix;
    if (data.size() > 0) {
        matrix.resize(data.size(), data[0].size());
        for (int i = 0; i < data.size(); ++i) {
            for (int j = 0; j < data[0].size(); ++j) {
                matrix(i, j) = data[i][j];
            }
        }
    }
    return matrix;
}

int main(int argc, char *argv[]) {
    std::cout << "Hello" << std::endl;
    ocsort::OCSort ocsort = ocsort::OCSort(0.6, 30, 3, 0.3, 3, "iou", 0.2,false);
    std::string directory = "D:/modelzoo/oc_sort_new/3_deploy/modelzoo/ocsort/io/18frames/";

    auto start = std::chrono::high_resolution_clock::now();
    for(int i = 1; i < 1201; i++){
        std::cout << "Round :" << i << std::endl;
        std::string temp = directory;
        std::string filename = std::to_string(i).append(".txt");
        std::string filepath = temp.append(filename);

        Eigen::Matrix<float, Eigen::Dynamic, 6> matrix = read_csv_to_eigen_xyxy(filepath);   
        std::cout << matrix << std::endl;
        if(i == 3333){
            auto end = std::chrono::high_resolution_clock::now();
            auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
            std::cout << "-------- Diff --------" << duration.count() << std::endl;
            enable_debug = 1;
        }
            std::vector<Eigen::RowVectorXf> res = ocsort.update(matrix);
            std::cout << "Result " << i << ":" << "\n";
            for(auto j : res)
                std::cout  <<  j <<  "\n";
            std::cout << std::endl;
    }
}

5.2.3 中间结果与精度对齐效果展示

  • 这里展示本次精度对齐的中间结果和调试过程,充分展现了对齐调试思路。
  • 首先在 test.py 与 test.cpp 两边测试精度调整为相同的基础上,针对同一检测数据进行跟踪,对照 python 版结果来调试 C++ 版从而查找并发现问题。 首先每次调试修改完后,先自定义跑一定的帧数,发现 ID 结果与 python 版结果相比出现不同,接着回溯到数据最开始不同的那一帧结果,比如这里 图5-1 展示的是某一次调试从第 216 帧开始出现 ID 偏差或者跟踪错误。
5-1
图5-1 第 216 帧两版跟踪结果对照,
  • 根据图 5-1 来看该次调节的跟踪结果,可以发现表面上已经没有了 ID 偏差问题(这已经是 Debug 中后期较为不错的结果,初期结果基本大篇幅不同),但 实际上比如第 48 个跟踪器的坐标已经存在相对较大的偏差,其实也相当于是跟踪错误。因此针对该问题,后续需要查找使该跟踪器出现偏差的根源,Debug 上展示为 调试 velocities、trks、cost_matrix 等关键变量的值,来寻找使 results 直接出现偏差的 matches 问题。如图 5-2 所示,可以看到直接导致第 216 帧 results 出现偏差的 matches,
5-2
图5-2 第 216 帧目标匹配 matches 结果对照 ,
  • 由图 5-2 截取部分第 216 帧的 matches 结果可以看到,第 9、31 个跟踪器的匹配结果与 python 版结果相比出现了偏差,这也是导致 results 错误的 直接原因,因此需要根据此 unmatches 的结果来查找问题所在。图 5-3 展示了 velocities、trks 关键变量的结果对照,这里由于篇幅问题省去了代价矩阵 的结果展示,本质上可以从 velocities、trks 的结果中同理看出。
5-3
图5-3 第 216 帧 velocities、trks 结果对照,
  • 由图 5-3 的两个关键变量对比可以看到,在标蓝的那一行出现误差(其余行的结果的误差是由两种语言版本的自动结果四舍五入导致的,算是固定误差不用理会)。 然后需要根据这一行的结果来直接查找对应的代码块,最终发现 5.2.1 节精度对齐所展示的问题。发现问题后进行修改与不断调试,最终完成精度对齐工作, 图 5-4 展示了经过精度对齐后的结果(随机抽取较为靠后的某些帧拿来分析),这一帧结果没问题也就意味着前面都没有问题,这里给出了第 342 帧的 results 结果 记录。
5-4
图5-4 完成精度对齐后随机抽取靠后的某些帧的结果对照,
  • 上述结果可以看出,经过精度对齐后,C++ 版的跟踪结果与 python 版的结果几乎完全一致,且没有出现 ID 偏差问题。图 5-4 展示的不同,基本都是两个语言 版本的四舍五入导致的肉眼上的不同,比如一边是 988.638,一边是 988.6376,明显是四舍五入精度显示问题从而不需考虑。因此由上述结果可以验证本次精度 对齐的正确性

5.3 PC 端仿真耗时测试

  • 说明: 本耗时测试在 PC 端进行,CPU 型号是 Intel i7-14650HX,打开了 cudamode ,显卡是 NVIDIA Geforce RTX 4060Ti,但多次测试,打开 cudamode 并没有明显的加速效果(大约只提升了几倍),这里给出了打开了 cudamode 的测试结果,权重是OCSORT_S_INT8。
************************************
Total_TotalTime: 3057.8203 ms, Total_MemcpyTime : 0 ms, Total_HardTime : 0 ms, Total_OtherTime : 3057.8203 ms .
Hardop_TotalTime: 2963.611 ms, Hardop_HardTime : 0 ms.
Customop: DetPost,TotalTime: 49.0684 ms, HardTime : 0 ms.
Customop: ImageMake,TotalTime: 45.1408 ms, HardTime : 0 ms.
******************************************************
统计分析结果如下(The analysis results are as follows):
数据传入耗时(Data input time consumption):
Time(ms):0     Device:ImageMake
icore[npu]耗时(Icore [npu] time-consuming):
Time(ms):2963.61     Device:Null
数据传出耗时(Data output time consumption):
Time(ms):49.0684     Device:DetPost
cpu算子耗时(CPU operator time consumption):
Time(ms):3.05176e-05     Device:Null
******************************************************
  • 备注: 这是打开了 calctime_detail(session) 直接得到的结果,可以看到数据传入耗时这里显示为 0,与预期明显不符。而可以看出其他分支的耗时 都是基本算的 total 耗时,而这里由于是 PC 仿真导致 HardTime 是0。所以这里不清楚是有意为之还是个小问题,如果也算的总时间也比较合理。因此这里 数据传入耗时暂且认定为应该是 3057.8203 ms。

5.4 耗时性能优化

  • 说明: 耗时性能优化方面,由于测量 PC 端仿真的整体系统运行耗时较长,干扰因素比较多,每次结果上下浮动差异明显,因此本次耗时性能优化仅针对后处理部分 的 Tracking 部分,也就是仅对后处理跟踪算法进行耗时分析与优化,其余部分可能在上板调试时存在不同。这就可以正确地验证本算法的耗时性能优化效果。

  • 关键修改: OCsort.cpp 中的更新算法 update 函数部分,筛选目标时存在优化空间:源代码中,该操作每添加新的一列,就重新conservativeResize, 这个操作会导致内存分配和销毁很占时间,因此这里将其修改为,根据 bool 向量内的有效行的个数,预先分配该大小的矩阵,这样能够有效避免内存重复分配和释放的过程。 关键代码修改如下:

/* 新增 filt_by_bool_vector 函数,用于避免内存重复分配和释放 */
template<typename type, int a, int b, int c>
static inline Eigen::MatrixXf filt_by_bool_vector(const Eigen::Matrix<type, a, b>& mat, const Eigen::RowVector<bool, c>& bool_vec){
    std::vector<int> valid;
    valid.reserve(64);
    for(int i = 0; i < bool_vec.cols(); i++){
        if(true == bool_vec(i))
            valid.push_back(i);
    }
    Eigen::MatrixXf m(valid.size(), mat.cols());
    int j = 0;
    for (auto i : valid) {
        m.row(j) = mat.row(i);
        j++;
    }
    return m;
}
/* --- end --- */


std::vector<Eigen::RowVectorXf> OCSort::update(const Eigen::MatrixXf& output_results) {
    frame_count += 1;
    Eigen::Matrix<float, Eigen::Dynamic, 4> xyxys = output_results.leftCols(4);     // bboxes
    Eigen::Matrix<float, 1, Eigen::Dynamic> confs = output_results.col(4);          // confs
    Eigen::Matrix<float, 1, Eigen::Dynamic> clss = output_results.col(5);           // class

    /* 根据置信度筛选目标 */
    auto inds_low = confs.array() > 0.1;
    auto inds_high = confs.array() < det_thresh;

    Eigen::Matrix<bool, 1, Eigen::Dynamic> inds_second = inds_low && inds_high;
    Eigen::Matrix<bool, 1, Eigen::Dynamic> remain_inds = (confs.array() > det_thresh);

    /* ---- 耗时优化修改:从det筛选出inds_second和remain_inds对应的矩阵 ---- */
    Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic> dets_first = filt_by_bool_vector(output_results, remain_inds);
    Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic> dets_second = filt_by_bool_vector(output_results, inds_second);
    /* ---------------------------- end ---------------------------- */

/* 源代码会导致内存重复分配和释放,从而增加系统耗时 */
//    Eigen::Matrix<float, Eigen::Dynamic, 6> dets_second;
//    Eigen::Matrix<bool, 1, Eigen::Dynamic> remain_inds = (confs.array() > det_thresh);
//    Eigen::Matrix<float, Eigen::Dynamic, 6> dets_first;
//    for (int i = 0; i < output_results.rows(); i++) {
//        if (true == inds_second(i)) {
//            dets_second.conservativeResize(dets_second.rows() + 1, Eigen::NoChange);
//            dets_second.row(dets_second.rows() - 1) = output_results.row(i);
//        }
//        if (true == remain_inds(i)) {
//            dets_first.conservativeResize(dets_first.rows() + 1, Eigen::NoChange);
//            dets_first.row(dets_first.rows() - 1) = output_results.row(i);
//        }
//    }
/* end */

    /* get predicted locations from existing trackers */
    Eigen::MatrixXf trks = Eigen::MatrixXf::Zero(trackers.size(), 5);
    std::vector<int> to_del;
    std::vector<Eigen::RowVectorXf> ret;
    /* 其余代码 */
  • 下表记录了本次耗时优化前后的结果对照,随机抽取了 100 组互不相关的 Tracking 耗时数据,该数据是持续追踪 600 帧目标检测结果的记录(已分别剔除 最大和最小的 5 个数据)。

    Time(μs) 1 2 3 4 5 6 7 8 ... 100 Avg
    Old 144.85 149.84 149.40 142.79 145.91 148.16 143.71 152.04 ... 145.90 147.61
    New 129.49 130.98 133.06 136.97 136.62 128.48 138.08 137.57 ... 130.66 135.81
  • 绘制的耗时优化对比图如图 5-5 所示,可以看到经过优化后,针对 Tracking 的平均耗时(600 帧)从 147.61 μs 降低到了 135.81 μs,实现了 8.04% 的 耗时优化,改进效果明显,

5-5
图5-5 耗时优化结果对比

5.5 psin 与 plin 耗时测试

  • psin 耗时记录如下,模型是 OCSORT_S_INT8。可以看到主干网络推理耗时确实只有 17.5 ms,比纯 PC 仿真快了接近 170 倍,得到了较大的性能提升。
**************************************************************
Total_TotalTime: 39.178402, Total_MemcpyTime: 0, Total_HardTime: 17.093714, Total_OtherTime: 22.084684
Hardop_Total_Time: 17.47609 ms, Hardop_Hard_Time: 13.54904 ms.
Customop: DetPost, TotalTime: 17.4669 ms, HardTime: 1.5e-05 ms.
Customop: ImageMake, TotalTime: 4.2346 ms, HardTime: 3.54460 ms.
**************************************************************
统计分析结果如下 (The analysis results are as follows):
输入输入耗时(Data input time consumption):
Time(ms): 3.54486        Device: ImageMake
核心[npu]耗时(Icore [npu] time-consuming):
Time(ms): 17.47609       Device: Null
数据输出耗时(Data output time consumption):
Time(ms): 17.4669        Device: DetPost
CPU算子耗时(CPU operator time consumption):
Time(ms): 1.90735e-06    Device: Null
**************************************************************
  • plin 耗时记录如下,可以看到主干网络在 NPU 上的推理耗时占了约 9.55 ms,如果不算其他时间可以得到 100+ FPS。而这里由于数据输入耗时较大,影响了 整体 Total_time,可能是由于摄像头输入 60Hz@1080P 的限制,影响了 imagemake 环节的速度。
**************************************************************
Total_TotalTime: 26.2985, Total_MemcpyTime: 0, Total_HardTime: 26.2985, Total_OtherTime: 0
Hardop_Total_Time: 9.55321 ms, Hardop_Hard_Time: 9.07555 ms.
Customop: DetPost, TotalTime: 0.494153 ms, HardTime: 0.0988306 ms.
Customop: ImageMake, TotalTime: 16.2512 ms, HardTime: 14.6261 ms.
**************************************************************
统计分析结果如下 (The analysis results are as follows):
输入输入耗时 (Data input time consumption):
Time(ms):16.2512      Device:ImageMake
核心[npu]耗时 (Core [npu] time-consuming):
Time(ms):9.55321      Device:Null
结果输出耗时 (Data output time consumption):
Time(ms):0.494153     Device:DetPost
CPU算子耗时 (CPU operator time consumption):
Time(ms):0.129506     Device:Null
**************************************************************

6. ICRAFT编译

编译通过

6.1 过程结果数据对照

  • 由于本次算法是多目标跟踪跟踪类算法,仿真测试都是视频不便显示在文档中,因此我们放入测试的 DanceTrack-val 数据集的测试数据

  • 备注:由于数据集采用的是非线性运动数据集 DanceTrack,数据量大导致测试成本较高,而单纯的 PC 端测试耗时过高,因此我们测试时使用 psin 的 Socket 精度测试,使用板上资源测试精度效率较高。

    DanceTrack-val 框架动态图推理 导出静态模型推理 解析组件仿真 量化组件仿真 指令组件仿真
    IDF1(%) 51.6 51.2 50.7 49.5 50.3
    MOTA(%) 85.2 85.0 84.8 84.8 84.8
  • 上表可以看到,在 DanceTrack-val 数据集上,MOTA 始终比较稳定,整个编译适配阶段精度降低很小。而 IDF1 在最后阶段得到了精度回升,可能是 adapt 作了一些优化操作,或者是我们精度测试的轮数(目前每组 5 轮)还不够,还需要再进行大量测试。总的来说,本次编译适配阶段的精度测试结果比较稳定, 且 IDF1 与 MOTA 分别成功复现了原模型的 97.5% 和 99.5%,较好地复现了原模型的跟踪性能。

6.2 精度测试

  • 下表给出了本次 OCSORT_X_INT8 与 OCSORT_X_INT16 在 DanceTrack-val 数据集上精度测试的其中一组结果,其中 IDF1 达到 50.9%,MOTA 达到 84.8%。考虑篇幅和 便于比较综合性能,后续精度测试对比仅给出所有子数据集加权的总体平均结果,并只包括最重要的两个指标,IDF1 跟踪一致性指标和 MOTA 综合跟踪精度指标, 并且后续给出的总体结果是在至少 5 次测试之后取得平均值,以排除特殊情况。

  • INT8 量化结果

    OCSORT_X_INT8 IDF1 IDP IDR Rcll Prcn GT MT PT ML FP FN IDs FM MOTA MOTP IDt IDa IDm
    dancetrack0004 53.4% 55.9% 51.1% 90.1% 98.5% 4 4 0 0 62 453 42 76 87.9% 0.184 28 5 0
    dancetrack0005 62.0% 62.9% 61.1% 95.9% 98.8% 4 4 0 0 55 194 15 41 94.4% 0.148 8 3 0
    dancetrack0007 46.9% 48.9% 45.0% 91.5% 99.4% 8 8 0 0 51 803 41 114 90.6% 0.201 28 8 0
    dancetrack0010 67.2% 69.1% 65.4% 94.3% 99.7% 6 6 0 0 22 399 20 62 93.7% 0.145 13 6 1
    dancetrack0014 33.7% 34.6% 33.0% 93.5% 98.0% 12 12 0 0 236 821 150 243 90.5% 0.196 105 25 0
    dancetrack0018 91.6% 100.0% 84.5% 84.5% 100.0% 8 5 0 3 0 456 0 5 84.5% 0.109 0 0 0
    dancetrack0019 34.1% 35.3% 33.0% 91.7% 98.1% 7 7 0 0 301 1362 154 299 89.0% 0.193 103 31 0
    dancetrack0025 45.5% 51.7% 40.7% 74.8% 95.2% 9 4 5 0 269 1784 45 201 70.4% 0.228 28 14 0
    dancetrack0026 37.2% 48.1% 30.3% 59.3% 94.2% 19 0 19 0 147 1624 114 204 52.8% 0.219 92 21 6
    dancetrack0030 79.9% 80.7% 79.2% 97.1% 99.0% 6 6 0 0 78 219 28 69 95.7% 0.170 13 5 0
    dancetrack0034 44.6% 47.9% 41.7% 84.7% 97.4% 14 10 4 0 253 1703 127 252 81.3% 0.192 81 31 1
    dancetrack0035 49.7% 50.6% 48.8% 93.7% 97.1% 8 8 0 0 146 330 74 117 89.5% 0.208 61 7 1
    dancetrack0041 38.9% 41.1% 36.9% 85.2% 94.7% 22 16 6 0 793 2469 308 556 78.7% 0.226 217 41 2
    dancetrack0043 61.7% 69.5% 55.5% 76.6% 95.9% 14 5 9 0 55 392 33 52 71.3% 0.232 26 5 2
    dancetrack0047 54.6% 56.4% 53.0% 89.6% 95.3% 8 7 1 0 413 981 115 220 84.0% 0.209 76 18 0
    dancetrack0058 58.9% 59.7% 58.1% 96.8% 99.4% 7 7 0 0 61 355 31 75 96.0% 0.153 26 3 0
    dancetrack0063 25.1% 25.7% 24.6% 79.5% 82.8% 8 4 4 0 1253 1564 227 421 60.0% 0.257 111 46 3
    dancetrack0065 45.7% 46.4% 45.0% 91.4% 94.2% 5 5 0 0 196 303 27 123 85.0% 0.211 18 6 0
    dancetrack0073 54.5% 59.7% 50.1% 82.5% 98.3% 14 7 7 0 123 1479 47 111 80.5% 0.180 31 15 2
    dancetrack0077 62.7% 64.6% 61.0% 90.9% 96.3% 9 9 0 0 381 980 59 122 86.9% 0.172 34 10 0
    dancetrack0079 70.9% 72.3% 69.5% 95.2% 99.1% 18 18 0 0 124 699 84 144 93.8% 0.194 59 11 1
    dancetrack0081 46.4% 49.1% 44.0% 88.0% 98.1% 20 17 3 0 320 2264 206 439 85.2% 0.236 142 43 2
    dancetrack0090 49.4% 54.8% 45.0% 81.0% 98.6% 14 7 7 0 150 2503 110 255 79.0% 0.179 74 24 2
    dancetrack0094 48.8% 51.9% 46.1% 84.3% 95.0% 25 18 7 0 523 1840 166 284 78.5% 0.207 115 36 6
    dancetrack0097 77.0% 77.4% 76.6% 98.7% 99.6% 4 4 0 0 19 63 14 25 97.9% 0.119 8 2 0
    OVERALL 50.9% 53.4% 48.7% 88.4% 97.1% 273 198 72 3 6031 26040 2237 4510 84.8% 0.194 1497 416 29
  • INT16 量化结果

    OCSORT_X_INT16 IDF1 IDP IDR Rcll Prcn GT MT PT ML FP FN IDs FM MOTA MOTP IDt IDa IDm
    dancetrack0004 47.6% 49.7% 45.7% 90.8% 98.6% 4 4 0 0 57 422 46 72 88.6% 0.159 32 6 1
    dancetrack0005 73.4% 74.5% 72.3% 96.6% 99.5% 4 4 0 0 22 163 6 33 96.0% 0.115 4 2 0
    dancetrack0007 48.4% 50.3% 46.6% 92.3% 99.7% 8 8 0 0 23 728 41 83 91.6% 0.149 29 7 0
    dancetrack0010 72.8% 74.7% 71.0% 94.9% 99.7% 6 6 0 0 17 359 9 48 94.5% 0.106 4 4 0
    dancetrack0014 36.4% 37.1% 35.7% 94.3% 98.1% 12 12 0 0 230 726 138 202 91.4% 0.160 101 21 0
    dancetrack0018 91.9% 100.0% 85.0% 85.0% 100.0% 8 5 0 3 0 441 0 4 85.0% 0.093 0 0 0
    dancetrack0019 33.5% 34.7% 32.4% 91.7% 98.1% 7 7 0 0 292 1362 138 259 89.1% 0.151 97 28 0
    dancetrack0025 63.2% 70.4% 57.3% 77.6% 95.3% 9 5 4 0 269 1585 33 173 73.3% 0.198 18 10 0
    dancetrack0026 41.0% 51.2% 34.2% 63.4% 94.8% 19 0 19 0 140 1463 131 216 56.6% 0.189 97 28 8
    dancetrack0030 85.1% 85.8% 84.4% 97.6% 99.2% 6 6 0 0 58 184 28 59 96.4% 0.134 14 5 0
    dancetrack0034 41.2% 43.9% 38.9% 86.8% 98.1% 14 11 3 0 192 1474 123 241 83.9% 0.155 80 28 1
    dancetrack0035 42.4% 43.2% 41.6% 93.8% 97.3% 8 8 0 0 138 324 72 106 89.9% 0.163 52 8 0
    dancetrack0041 38.2% 39.9% 36.6% 87.1% 95.0% 22 18 4 0 773 2156 310 499 80.6% 0.200 233 35 1
    dancetrack0043 73.3% 80.8% 67.1% 80.4% 96.8% 14 6 8 0 44 328 37 47 75.6% 0.209 29 7 3
    dancetrack0047 42.1% 43.5% 40.8% 90.3% 96.4% 8 8 0 0 319 917 113 185 85.7% 0.177 82 15 0
    dancetrack0058 62.8% 63.5% 62.1% 97.2% 99.3% 7 7 0 0 74 312 23 60 96.3% 0.094 22 1 0
    dancetrack0063 26.2% 26.5% 25.8% 82.8% 85.1% 8 6 2 0 1100 1308 229 369 65.4% 0.226 117 40 1
    dancetrack0065 45.7% 46.4% 45.0% 93.0% 95.7% 5 5 0 0 146 246 29 102 88.0% 0.169 18 4 0
    dancetrack0073 58.8% 63.3% 54.9% 85.5% 98.6% 14 10 4 0 106 1226 46 86 83.7% 0.154 24 14 1
    dancetrack0077 63.2% 64.6% 61.8% 92.1% 96.3% 9 9 0 0 386 853 38 94 88.2% 0.133 23 6 0
    dancetrack0079 73.2% 74.7% 71.8% 95.4% 99.2% 18 18 0 0 113 674 65 127 94.1% 0.163 48 9 1
    dancetrack0081 49.4% 51.9% 47.1% 89.1% 98.3% 20 17 3 0 292 2053 190 374 86.5% 0.203 135 40 2
    dancetrack0090 39.2% 43.2% 35.8% 81.8% 98.5% 14 9 5 0 164 2395 121 250 79.6% 0.141 85 26 1
    dancetrack0094 56.0% 59.2% 53.1% 85.4% 95.2% 25 19 6 0 511 1711 135 271 79.9% 0.171 91 28 5
    dancetrack0097 65.5% 65.6% 65.4% 98.8% 99.2% 4 4 0 0 38 56 9 18 97.8% 0.086 8 0 0
    OVERALL 51.7% 53.9% 49.6% 89.6% 97.3% 273 212 58 3 5504 23466 2110 3978 86.2% 0.158 1443 372 25
  • 为了验证 OCSORT 在非线性运动特征明显的数据集 DanceTrack-val 上的先进性,这里同样测试了 ByteTrack_X_INT8 的结果,多次测试取其中较为稳定平均的结果如下表所示。 可以看到,ByteTrack 在该数据集上 IDF1 为 47.2%,MOTA 为 81.2%。如果平均所有测试结果数据,我们的 OCOSRT 在该数据集实现了 IDF1 3.1%↑ 、MOTA 3.6%↑ 的提升。

    ByteTrack_X_INT8 IDF1 IDP IDR Rcll Prcn GT MT PT ML FP FN IDs FM MOTA MOTP IDt IDa IDm
    dancetrack0004 49.1% 52.5% 46.1% 84.4% 96.1% 4 4 0 0 159 716 35 89 80.2% 0.230 24 6 0
    dancetrack0005 77.7% 78.7% 76.8% 96.9% 99.3% 4 4 0 0 32 148 3 42 96.1% 0.194 2 1 0
    dancetrack0007 45.4% 47.1% 43.8% 91.8% 98.6% 8 8 0 0 126 777 58 140 89.9% 0.230 42 11 1
    dancetrack0010 51.3% 52.6% 50.0% 93.9% 98.8% 6 6 0 0 77 431 27 72 92.4% 0.200 17 6 0
    dancetrack0014 33.1% 33.6% 32.6% 92.8% 95.9% 12 12 0 0 505 906 171 279 87.5% 0.234 118 25 0
    dancetrack0018 82.3% 89.7% 76.0% 84.7% 100.0% 8 5 0 3 0 448 2 5 84.7% 0.149 2 0 0
    dancetrack0019 28.9% 30.8% 27.3% 83.1% 93.9% 7 4 3 0 884 2787 125 442 77.0% 0.254 88 21 0
    dancetrack0025 48.9% 58.0% 42.3% 67.7% 92.8% 9 2 7 0 373 2289 41 170 61.8% 0.260 24 14 1
    dancetrack0026 36.2% 47.9% 29.1% 58.7% 96.6% 19 0 19 0 83 1649 96 171 54.2% 0.232 75 20 4
    dancetrack0030 81.2% 81.8% 80.5% 95.5% 97.1% 6 6 0 0 218 339 22 127 92.4% 0.231 18 3 0
    dancetrack0034 40.7% 43.7% 38.1% 84.6% 97.2% 14 9 5 0 275 1713 112 242 81.2% 0.226 71 30 0
    dancetrack0035 43.4% 44.0% 42.8% 94.1% 96.6% 8 8 0 0 174 308 68 111 89.5% 0.234 48 6 0
    dancetrack0041 44.2% 47.0% 41.7% 84.6% 95.5% 22 17 5 0 664 2571 244 442 79.2% 0.236 191 36 1
    dancetrack0043 63.8% 73.7% 56.3% 73.9% 96.8% 14 6 8 0 41 437 23 40 70.1% 0.238 22 3 4
    dancetrack0047 42.1% 46.0% 38.8% 79.9% 94.8% 8 3 5 0 410 1893 102 210 74.4% 0.247 75 12 0
    dancetrack0058 55.7% 56.3% 55.0% 97.0% 99.2% 7 7 0 0 90 337 34 83 95.8% 0.183 25 7 0
    dancetrack0063 33.4% 34.9% 32.1% 77.7% 84.5% 8 2 6 0 1083 1701 120 322 61.9% 0.280 69 27 4
    dancetrack0065 35.6% 36.3% 34.9% 86.0% 89.5% 5 4 1 0 354 489 27 148 75.2% 0.273 23 1 0
    dancetrack0073 55.9% 61.8% 51.0% 81.2% 98.5% 14 7 7 0 106 1593 52 76 79.3% 0.188 30 19 2
    dancetrack0077 46.3% 54.0% 40.6% 73.9% 98.4% 9 1 8 0 133 2821 29 86 72.4% 0.192 20 8 0
    dancetrack0079 65.8% 67.1% 64.4% 94.3% 98.2% 18 18 0 0 245 828 71 195 92.1% 0.219 50 8 1
    dancetrack0081 38.8% 40.9% 36.8% 85.7% 95.3% 20 17 3 0 800 2683 181 512 80.5% 0.252 124 43 3
    dancetrack0090 39.0% 43.7% 35.2% 78.7% 97.7% 14 7 7 0 242 2793 142 266 75.8% 0.217 89 34 3
    dancetrack0094 57.5% 61.3% 54.1% 83.5% 94.7% 25 17 8 0 547 1939 129 246 77.7% 0.236 93 31 8
    dancetrack0097 42.7% 42.8% 42.6% 98.9% 99.3% 4 4 0 0 33 50 19 19 97.8% 0.166 12 4 0
    OVERALL 47.2% 50.1% 44.5% 85.5% 96.2% 273 178 92 3 7654 32646 1933 4535 81.2% 0.227 1352 376 32
  • 多次精度测试取平均后的结果对比如下表所示,其中最关键的指标:IDF1 (跟踪一致性)提升了 3.1%,MOTA (综合跟踪精度)提升了 3.6%.

    跟踪性能对比(%) IDF1 MOTA IDP IDR Rcll Prcn
    ByteTrack_X_INT8 47.2 81.2 50.1 44.5 85.5 96.2
    OCSORT_X_INT8(ours) 50.3 84.8 53.4 48.7 88.4 97.1
    性能指标提升 ↑ 3.1 3.6 3.3 4.2 2.9 0.9
6-1
图6-1 OCSORT 与 ByteTrack 在 DanceTrack-val 数据集上跟踪性能对比
  • 上表不难得出结论,在 DanceTrack-val 这样的非线性运动和抗遮挡问题严重的数据集上,OCSORT 相较于 ByteTrack得到了较为明显的性能提升。

6.3 混合精度量化

  • 原始 INT8 量化分析图:
6-2
图6-2 INT8量化分析图

   上图是OC-SORT INT8 编译量化后,在icraft show中通过量化分析工具得到的波形图,可以看到从中间开始曲线的效果并不是很好。
   因此,我们在ocsort_mix_config.csv中,将op_id为5,6,9的算子的精度由int8改为了int16,可以看到波形图在一定程度上得到了优化。cosine similarity在混合精度量化中的后半段中不断与relative euclidian distance相交,而改变这几个算子的精度后,前者与后者的差距变大。 具体的提升可以具体查看这几个突出的点。

6-3
图6-3 mix_precision_csv
  • 最终我们混合精度量化后效果图:
6-4
图6-4 mix_precision
  • 当然我们也尝试修改了后面的一些关键算子量化方式,但提升都很微小,报告中就不再全部放出,而只给出了一些对结果影响很大的算子

  • 下表给出了本次量化调优过程中,对于结果影响较大的节点,其中 cosine similarity 余弦相似度最能直接反映模型量化精度,可以看到经过我们的混合精度量化,模型精度有了比较显著的提升

    index 119 119-new 145 145-new 158 158-new 162 162-new 166 166-new 171 171-new
    mean absolute error 0.121 0.109 0.154 0.138 1.000 0.818 0.141 0.127 0.165 0.147 1.000 0.886
    ↓ (%) 1.2 1.6 18.2 1.4 1.8 11.4
    relative euclidian 0.807 0.684 0.891 0.775 0.502 0.341 0.807 0.672 0.776 0.672 0.712 0.532
    ↓ (%) 12.3 11.6 16.1 13.5 10.4 18.0
    cosine similarity 0.787 0.840 0.765 0.820 0.931 0.968 0.796 0.854 0.791 0.839 0.897 0.952
    ↑ (%) 5.3 5.5 3.7 5.8 4.8 5.5
  • 最终我们给出本次混合精度量化最直观的效果对比,性能指标包括 IDF1、MOTA、IDs,以及权重的大小来衡量轻量化的程度。

    MOT17-02-DPM(%) IDF1 ↑ MOTA ↑ IDs ↓ Imodel(M) ↓
    INT8 37.9 31.6 517 9.93
    INT16 42.6 38.3 372 20.4
    MIX_AUTO 40.5 38.2 410 10.2
    MIX_5/6/9 41.3 36.6 443 10.4
    MIX_5/6/9/60/62 (Ours best) 41.2 38.0 413 10.5
    MIX_5/6/9/60/62/205/207/247/249 40.1 38.2 437 10.9
  • 量化效果对比图如下所示:

6-5
图6-5 mix_precision comparison
  • 最后相对于库里的 INT8、INT16、MIX_AUTO,优化效果如下图所示。相较于 INT8,以 5.74% 权重增加的代价,实现了 8.71% 的 IDF1 提升以及 20.25% 的 MOTA 提升;相较于 INT16,实现了 48.53% 的模型压缩;相较于 ICraft 提供的 MIX_AUTO,在 ID 跟踪一致性方面取得明显提升。

    Ours Improvement(%) IDF1 ↑ MOTA ↑ IDs ↓ Imodel(M) ↓
    with INT8 +8.71 +20.25 +20.12 -5.74
    with INT16 -3.29 -0.78 -11.02 +48.53
    with MIX_AUTO +1.73 -0.52 +0.73 -2.94

7. 上板测试与优化

  1. 优化前各环节耗时统计(表格)
  • 根据 5.5 节的 psin 与 plin 耗时测试结果,绘制下表优化前各环节耗时统计表格。

    性能指标 PSIN 耗时 (ms) PLIN 耗时 (ms)
    Total_Time 39.178 26.299
    Hard_Time 17.094 26.299
    Other_Time 22.085 0.000
    ImageMake 3.545 16.251
    NPU Core 17.476 9.553
    DetPost 17.467 0.494
    CPU Core <0.001 0.130
  • 由上表可见,PLIN 端耗时总体来看要低于 PSIN 端,主要归因于 NPU 和硬算子。此外,摄像头输入的帧率限制了 ImageMake 的耗时,如果采用更高帧率的摄像头会实现更低的数据传入耗时。

7-1
图7-1 耗时测试对数对比图
  1. 优化后耗时统计(表格)
  • 根据 5.4 节耗时性能优化部分,由于测量 PC 端仿真的整体系统运行耗时较长,干扰因素比较多,每次结果上下浮动差异明显, 因此本次耗时性能优化仅针对后处理部分的 Tracking 部分,也就是仅对后处理跟踪算法进行耗时分析与优化,其余部分可能在上板调试时存在不同。这就可以正确地验证本算法的耗时性能优化效果。

  • 下表记录了本次耗时优化前后的结果对照,随机抽取了 100 组互不相关的 Tracking 耗时数据,该数据是持续追踪 600 帧目标检测结果的记录。

    Time(μs) 1 2 3 4 5 6 7 8 ... 100 Avg
    Old 144.85 149.84 149.40 142.79 145.91 148.16 143.71 152.04 ... 145.90 147.61
    New 129.49 130.98 133.06 136.97 136.62 128.48 138.08 137.57 ... 130.66 135.81
  1. 优化项与效果
  • 核心代码 OCsort.cpp 中的更新算法 update 函数部分,筛选目标时存在优化空间:源代码中,该操作每添加新的一列,就重新conservativeResize, 这个操作会导致内存分配和销毁很占时间,因此这里将其修改为,根据 bool 向量内的有效行的个数,预先分配该大小的矩阵,这样能够有效避免内存重复分配和释放的过程。 详细可见 5.4 节耗时性能优化部分说明,图 6-6 展示了基于上表数据的耗时优化效果对比。
7-2
图7-2 耗时优化结果对比
  1. 运行时工程的整体介绍 - 参考2.1.2

    • 整体运行时工程可参考 2.1.2 算法流程,主要分为以下几个部分:
      • 数据输入:通过摄像头或视频文件获取输入数据;
      • 图像预处理:对输入图像进行缩放、归一化等处理,转换为模型所需的格式;
      • ICRAFT 编译,生成各阶段的权重文件用于模型推理;
      • 模型推理:调用量化后的模型进行目标检测;
      • 后处理:对模型输出结果进行解码、筛选和跟踪算法处理;
      • 输出结果:将跟踪结果可视化或保存到文件。

8. 硬算子设计流程

8.1 IOU 算子验证

  • 仅仅是做了 IOU 算子的一个仿真验证功能,库里是已经有了 NMS 算子的,这里相当于是做了简单的功能复现。数据输入来源于本次前文精度对齐部分,得来的 每一帧的标准检测结果。仿真里 iou 的值除以 256(因为是8bit)如果全部在 0-1 区间内,表示功能验证通过。图 8-1 展示了我们的 IOU 硬算子设计的 RTL 电路,图 8-2 展示了 IOU 硬算子设计的仿真验证结果。
8-1
图8-1 IOU 算子的 RTL 电路
8-2
图8-2 IOU 算子的功能仿真验证

8.2 Kalman Filter 硬算子设计

  • 经验上来说,一般滤波类的算子通常比较适合在 FPGA 上进行专用加速,因此这里对多目标跟踪算法的 Kalman Filter 算子进行了硬件加速设计。我们 计划将其拆解成五个主要部分进行功能实现,目前时间关系只完成了状态预测部分的仿真验证,如下图 8-3 所示:
8-3
图8-3 Kalman Filter 硬算子设计
  • 验证也就是根据 python 版的跟踪后处理部分进行结果验证,RTL 顶层和功能模块设计如图 8-4 和 8-5 所示,仿真验证结果如图 8-5 所示,波形图上半部分为上一时刻的协方差值, 下面一段是当前时刻的预测值,如果和 python 版全部对应上或误差非常小就说明可以后续真正用于我们同类型多目标跟踪算法的卡尔曼滤波硬算子加速了。
8-4
图8-4 Kalman Filter 顶层设计 RTL
8-5
图8-5 Kalman Filter 硬算子功能设计 RTL
8-6
图8-6 Kalman Filter 仿真波形图.png

9. AI 模型部署应用展示

9.1 基于边缘 AI 多目标跟踪算法的车辆拦截系统

  • 项目简介:
    针对车辆拦截系统的实时性和准确性需求,设计并实现了一套基于边缘AI的多目标跟踪算法系统,实现场目标检测到多目标跟踪的完整AI视觉算法链路, 最终完成了高精度、低延迟的拦截性能。
9-2
图9-2 AI 模型部署应用 2 展示

10. 目前存在的问题与难点

  • 混合精度量化在上板 PSIN 和 PLIN 工程上跑不通,在 PC 端仿真精度测试可行,目前没有找到原因,具体是卡在主函数 ICRAFT RUN 的 std::vector outputs = session.forward({ img_tensor }); 这一行,继续调试会进入反汇编,认为是硬件方面配置问题,因为使用库 里的 bytetrack 混合精度量化也是同样情况。