DOD

0 downloads 0 likes on ModelScope updated

Model card

基于icraft的双模态目标检测模型端到端优化部署关键技术

1. 摘要

  1. 身份编号:fwb0052

  2. 类型与序号:

    双模态目标检测模型DOD/双模态目标检测模型端到端优化部署关键技术

  3. 进展:

    item status detail
    方案选择与硬件资源划分 1
    模型导出 1
    ~导出模型精度一致,提供python验证脚本 1
    cpu处理部分c++工程移植 1
    ~c++工程精度一致,提供验证脚本 0
    ~c++工程性能优化 1
    icraft编译通过 1
    ~存在量化精度调优工作 1
    ~完成基本pyrt/crt搭建与结果验证 1
    ~完成pyrt/crt精度测试与对齐 1
    完成上板耗时测试 1
    ~使用运行时提供的api进行基本性能优化 1
    ~存在整体方案各环节优化或优化方案的设计 1
    提出bug数 2 分别是:编译的量化阶段校准集数量有限制、yolov5的soft_nms有bug,将分别在第6节和第7.1节中说明

2. 方案简介

2.1 算法流程介绍

模型介绍

模型相关信息 本部署方案模型为Dual-modal Object Detection model(DOD),是 Cross-Modality Fusion Transformer (CFT) for Multispectral Object Detection一文中提出的CFT 模型的轻量版,CFT在处理多光谱数据时,能够获得更好的结果。CFT发表于 2022 年,以预印本形式收录于 arXiv,并投递至 Elsevier 期刊。作者来自清华大学航空航天学院 。

CFT主要研究了多光谱目标检测,并提出一种基于 Transformer 的跨模态融合方法,以提升多光谱目标检测在复杂环境下的精度与鲁棒性。在可见光摄像头受光照、雨雾、遮挡等影响时,仅凭单一模态难以获得可靠的检测结果,文中提出的 CFT 模块,旨在通过 Transformer 的自注意力机制,在特征提取阶段同时完成 RGB 与热感(Thermal)两种模态的跨模态融合,从而提升多光谱目标检测在复杂环境下的精度与健壮性。

性能对比 下表为在 FLIR 数据集上,与单模态及其他融合方案的对比(基于 YOLOv5 Backbone) :

模态 方法 参数量 (M) GFLOPs mAP₅₀ (%) mAP₇₅ (%) mAP (%)
RGB YOLOv5 (baseline) 47.06 115.6 67.8 25.9 31.8
Thermal YOLOv5 47.06 115.6 73.9 35.7 39.5
RGB+T 两流(no fusion) 73.72 190.1 73.0 32.0 37.4
RGB+T + CFT 206.03 224.4 78.7 ↑5.7 35.5 ↑3.5 40.2 ↑2.8

跨数据集表现 在 LLVIP 和 VEDAI 数据集上,CFT 同样取得显著提升:

  • LLVIP:mAP₅₀ 从 95.8 增至 97.5 (+1.7),mAP 从 62.3 增至 63.6 (+1.3)。
  • VEDAI:mAP₅₀ 从 79.7 增至 85.3 (+5.6),mAP 从 46.8 增至 56.0 (+9.2) 。

以上结果表明,CFT 模块在保证可接受计算复杂度的同时,显著增强了多光谱目标检测的精度和鲁棒性。

模型结构与创新点

原论文提出的论文模型结构如图所示,基于 YOLOv5 构建了 RGB 和热感两个模态的主干网络,并插入了 CFT 模块,用于跨模态融合。CFT 模块利用全局自注意力机制,在特征提取阶段同时完成 RGB 与热感模态的跨模态融合,从而提升多光谱目标检测在复杂环境下的精度与鲁棒性。

其主要创新点在于: (1)基于 YOLOv5 构建两流(RGB/热感)主干网络; (2)在多个尺度插入 Cross-Modality Fusion Transformer (CFT) 模块,利用全局自注意力自动执行 intra-modality 与 inter-modality特征融合; (3)为降低计算开销,先对特征图进行全局平均。

尽管其在多光谱的任务下获得更好的结果,但 CFT 模块占据了大量的参数占用,因此可以减少CFT模块的计算开销,从而在推理速度上有提升。因此我们对CFT模型进行结构化剪枝和重构,以减少参数占用,从而提高推理速度。模型结构如图所示,我们将CFT模块替换成了ADD+卷积模块,用更少的参数,实现相同的特征融合效果。

算法流程

算法流程如图所示,整个流程主要分为三部分,即数据的前后处理以及模型推理。其中,detect头用于将Prediction中的坐标从网格单元解码到输入特征图(640×640尺寸)并拼合成[1, 25200, 11]的张量,其中:

  • 25200 = (80×80 + 40×40 + 20×20) × 3(锚框数量)
  • 11 = 边界框坐标(cx, cy, w, h) + 目标置信度(obj_conf) + 6个类别置信度(cls_conf)。

前后处理介绍

该模型的前后处理与YOLOv5的流程一致。

前处理:

  1. 读取图像,调整尺寸:

    • 使用 OpenCV 或其他库加载图像。
    • DOD通常要求输入图像的尺寸为 640x640,或者其他特定的倍数(如 416x416)。
    import cv2
    im = cv2.imread("path_to_image.jpg")
    im = letterbox(im0, self.img_size, stride=self.stride, auto=self.auto)[0]  # padded resize
    
  2. 输入排布:

    • DOD的输入排布是NCHW
    • DOD输入的通道顺序是RGB
    im = im.transpose((2, 0, 1))[::-1]  # HWC to CHW, BGR to RGB
    
  3. 转换为张量:

    框架下会将图像转换为 PyTorch 张量,并以符合框架下模型输入的要求。

    im = np.ascontiguousarray(im)  # contiguous
    im = torch.from_numpy(im)
    im = im.half() if model.fp16 else im.float()  # uint8 to fp16/32
    
  4. 归一化处理:

    • 将像素值从 [0, 255] 归一化到 [0, 1] 的范围。
    img = img / 255.0  # 归一化
    if len(im.shape) == 3:
    	im = im[None]  # expand for batch dim
    

后处理:

  1. 阈值筛选:

    • 置信度阈值用于筛选检测结果中,只有超过该置信度的检测框才能被保留。通常在模型的输出后,使用 conf_thres 参数来设置这个阈值。在 DOD的代码中,这个阈值常常用于 NMS(非极大值抑制)之前。
    xc = prediction[..., 4] > conf_thres  # candidates
    
  2. 应用非极大值抑制(NMS):

    • 对于检测到的多个框,使用 NMS 来消除重叠较大的框,只保留最有可能的框。
    pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
    
  3. 可视化:

    • DOD对于每张图片进行保存检测框、写入文件、可视化结果等。
    # Process predictions
    ...
    gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
    annotator = Annotator(im0, line_width=line_thickness, example=str(names))
    if len(det):
    	# Rescale boxes from img_size to im0 size
        det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()
    ...
    

2.2 硬件资源分配

DOD 在板端硬件资源分配如下:

前处理(BGR2RGB、Resize、Pad和将特征图转换为[1, 1, 640, 640 ,4]的排布):CPU

网络主体:NPU

后处理阈值筛选(detpost):FPGA

后处理(NMS,坐标缩放):CPU

可视化:CPU

模型资源分配图如下:

4. 模型导出说明

  1. 如下图所示:在模型中,和Yolov5一样,我们将Detect头(用于解码和张量聚合)去除,放到了后处理部分。
  1. 在代码中的写法与demo中的Yolov5一样,表示如下:
     # 修改前向,去除前向中后处理操作
     def new_forward(self, x):
     for i in range(self.nl):
         x[i] = self.m[i](x[i])  # conv
     return x
    
  2. 配置好权重文件和环境后,脚本可以直接使用python 1_save.py运行

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

  1. CPU实现的功能如下:
  • 前处理中,实现RGB和IR图片的BGR转RGB:

     if (this->src_img.channels() == 3){
         :cvtColor(this->src_img, this->src_img, cv::COLOR_BGR2RGB);
     	   }
    
  • 前处理中,实现RGB和IR图片的resize和pa(以RGB为例)d:

    rgb_img.Resize({ netinfo.i_cubic[0].h, netinfo.i_cubic[0].w }, PicPre::LONG_SIDE).rPad();
    
  • 前处理中,显式地实现了RGB和IR图片的imgmake的补通道和维度变换(以RGB为例):

    cv::Mat mergedImage_rgb;
    mergedImage_rgb.create(640, 640, CV_8SC4);
    cv::parallel_for_(cv::Range(0, 640), [&](const cv::Range& range) {
        for (int r = range.start; r < range.end; ++r) {
            const uchar* src_ptr = rgb_img.dst_img.ptr<uchar>(r);
            schar* dst_ptr_rgb = mergedImage_rgb.ptr<schar>(r);
            for (int c = 0; c < 640; ++c) {
                const int offset = c * 3;
                dst_ptr_rgb[4*c]   = src_ptr[offset]   ^ 0x80;  // B: 位运算替代减法
                dst_ptr_rgb[4*c+1] = src_ptr[offset+1] ^ 0x80;  // G
                dst_ptr_rgb[4*c+2] = src_ptr[offset+2] ^ 0x80;  // R
                dst_ptr_rgb[4*c+3] = 0;                         // Alpha
                }
            }
    
  • 后处理中,实现了对output tensor的解码,nms,坐标缩放和可视化:

     void post_detpost_hard(const std::vector<Tensor>& output_tensors, PicPre& img, NetInfo& netinfo,
         float conf, float iou_thresh, bool MULTILABEL, bool fpga_nms, int N_CLASS, 
         std::vector<std::vector<std::vector<float>>> &ANCHORS,std::vector<std::string>& LABELS, 
         bool & show, bool & save , std::string &resRoot, std::string & name,icraft::xrt::Device device,bool & run_sim) {
     
         std::vector<int> id_list;
         std::vector<float> socre_list;
         std::vector<cv::Rect2f> box_list;
         std::vector<float> stride = get_stride(netinfo);
         for (size_t i = 0; i < output_tensors.size(); i++) {
             auto host_tensor = output_tensors[i].to(HostDevice::MemRegion());
             int output_tensors_bits = output_tensors[i].dtype()->element_dtype.getStorageType().bits();
             int obj_num = output_tensors[i].dtype()->shape[2];
             int anchor_length = output_tensors[i].dtype()->shape[3];
             if (output_tensors_bits == 16) {
                 auto tensor_data = (int16_t*)host_tensor.data().cptr();
                 for (size_t obj = 0; obj < obj_num; obj++) {
                     int base_addr = obj * anchor_length; 
                     Grid grid = get_grid(output_tensors_bits, tensor_data, base_addr, anchor_length);
                     get_cls_bbox(id_list, socre_list, box_list, tensor_data, base_addr,grid, netinfo.o_scale[i], stride[i],ANCHORS[i][grid.anchor_index], N_CLASS, conf, MULTILABEL);
                 }
             }
             else {
                 auto tensor_data = (int8_t*)host_tensor.data().cptr();
                 for (size_t obj = 0; obj < obj_num; obj++) {
                     int base_addr = obj * anchor_length; 
                     Grid grid = get_grid(output_tensors_bits, tensor_data, base_addr, anchor_length);
                     get_cls_bbox(id_list, socre_list, box_list, tensor_data, base_addr,grid, netinfo.o_scale[i], stride[i], ANCHORS[i][grid.anchor_index], N_CLASS, conf, MULTILABEL);
     
                 }
             }
         }
     
         auto nms_start = std::chrono::system_clock::now();
     
         std::vector<std::tuple<int, float, cv::Rect2f>> nms_res;
         if (fpga_nms&& !run_sim) {
             nms_res = nms_hard(box_list, socre_list, id_list, iou_thresh, device);
         }
         else {
             nms_res = nms_soft(id_list, socre_list, box_list, iou_thresh);   // 后处理 之 NMS
     
         }
         std::vector<std::vector<float>> output_res = coordTrans(nms_res, img);
     
     
         #ifdef _WIN32
         if (show) {
             visualize(output_res, img.ori_img, resRoot, name, LABELS);
         }
         #endif
     }
    
  1. 在移植后的C++程序中,我们对进行了如下方法验证准确性:
  • 对比了C++程序与python runtime程序中icore推理的output tensor的形状
  • 对比了C++程序与python runtime程序nms后的tensor的数值
  • 对比了可视化后多张图片的锚框位置与置信度差异
  1. 带detpost硬算子的8 bit量化模型的各环节耗如下表所示,其中前处理时间为RGB+IR时间,测试方式为推理整个M3FD数据集的验证集,取平均时间。

    环节 耗时(ms)
    BGR2RGB 6.17
    resize+pad 12.39
    imgmake 7.01
    icore推理 30.45
    output tensor解码 0.143
    nms与坐标转换 0.029

​ 我们对8bit量化模型的推理进行了速度优化和精度优化,具体优化过程将在第7点详细说明。

6. ICRAFT编译

编译通过

  • 使用运行时仿真得到的仿真效果截图或数据,需提供下述表格:

    框架动态图推理 导出静态模型推理 解析组件仿真 量化组件仿真 指令组件仿真
    1 1 1 1 1
  • 8 bit精度测试结果如下(python runtime):

Class Images Labels P R mAP@.5 mAP@.75 mAP@.5:.95
all 840 7096 0.894 0.813 0.863 0.565 0.535
People 840 2505 0.871 0.783 0.827 0.462 0.461
Car 840 3599 0.899 0.883 0.918 0.682 0.615
Bus 840 151 0.917 0.914 0.94 0.794 0.697
Lamp 840 517 0.883 0.853 0.874 0.394 0.437
Motorcycle 840 113 0.932 0.61 0.799 0.429 0.447
Truck 840 211 0.863 0.834 0.82 0.628 0.553
  • 16 bit精度测试结果如下(python runtime):
Class Images Labels P R mAP@.5 mAP@.75 mAP@.5:.95
all 840 7096 0.859 0.847 0.864 0.598 0.552
People 840 2505 0.836 0.794 0.816 0.473 0.469
Car 840 3599 0.876 0.894 0.92 0.71 0.654
Bus 840 151 0.875 0.934 0.947 0.84 0.712
Lamp 840 517 0.837 0.853 0.865 0.403 0.441
Motorcycle 840 113 0.914 0.752 0.81 0.504 0.463
Truck 840 211 0.816 0.858 0.829 0.656 0.577
  • 编译过程的一些特殊说明 编译过程中的校准集是使用验证集中的图片,通过人工随机选择的。在量化阶段,我们遇到了量化校准集数量限制的bug,在选用128张图片作为校准集时,遇到了如下报错):

​ 我们猜测应该是校准集图片数量太多导致的。因此,我们经过测试后,选用了32张图片作为校准集,绕过了bug。 ​ 此外,我们发现如果使用硬算子imgmake,会导致在板端无法运行,这是因为只有一个硬件应该没有办法支持两个imagemake,因此,我们在generate阶段只使用了硬算子detpost便于加速后处理。

7. 上板测试与优化

  1. 优化前各环节耗时统计(表格) 优化前各环节耗时统计表格如下(带detpost硬算子的8位模型):

    op_type total time(ms)
    icraft::xir::Output 0
    icraft::xir::Add 86.230381
    icraft::xir::Add 85.979921
    customop::DetPost 0.61381
    icraft::xir::AlignAxis 37.378001
    icraft::xir::Cast 102.361661
    icraft::xir::AlignAxis 36.814211
    icraft::xir::Cast 101.109751
    icraft::xir::HardOp 12.78774
    icraft::xir::HardOp 15.79851
  2. 优化后耗时统计(表格) 优化后各环节耗时统计表格如下:

    环节 耗时(ms)
    BGR2RGB 6.17
    resize+pad 12.39
    imgmake 7.01
    icore推理 30.45
    output tensor解码 0.143
    nms与坐标转换 0.029
  3. 优化项与效果

  • 速度优化:

    关键技术1: 面向双通道输入的前处理优化

    将模型部署到板端后,我们将模型进行了速度的优化。模型推理速度优化目标为减少端到端的时间,即模型从图片读到PSDDR中后,到最后后处理的坐标缩放结束的时间。使用8位量化模型,在未调优的速度表中,可以看到有3种算子占据了模型推理93%的时间,分别是Cast、Add和alignaxis,在如下模型结构图的红框中:

    而这几个在PS端运行的算子的用途仅仅是为了适应硬件,添加了一个维度和补了一个全0的通道。这个工作本可以由运行在FPGA上的imagemake硬算子快速完成,但是该模型为双输入,需要编译两个imagemake硬算子,但我们通过实验发现,3.7.1版本的套件无法实现PSin的多个imagemake硬算子推理。在这种条件下,模型端到端推理速度达到了492毫秒,FPS仅约为2帧,因此,解决这几个算子带来的速度瓶颈至关重要。我们首先重新选择使用C++重写推理代码,以便更精细地控制内存的分配和释放,以及更方便地控制线程。然后,我们在推理时使用icraft api,即viewexpect将这几个算子从模型中移除,并使用C++将这几个算子的功能写出,并在保证正确性的前提下尽可能优化速度,将原本的450ms的算子耗时变成了仅需7ms。

    然后又由于前处理的BGR2RGB、resize和pad有40ms,有较大改进空间。因此我们还并行化实现了RGB+IR前处理过程。效果很好,BGR2RGB只需6ms,resize和pad只需12ms,加上上一页7的ms的imagemake,前处理时间总共只有25ms。这为接下来的异步流水线并行化推理提供了前置条件。

    接下来是我们的第二个关键技术。我们将推理分为三级流水线3种线程,分别是输入线程,icore推理线程和后处理线程。三者是生产者/消费者模式,通过任务队列来实现。 其中输入线程有4个,负责读入RGB和IR图片,并分别实现BGR-RGB、Resize、pad和补通道的前处理,并初始化一个结构体CFT_InputForIcore,保存预处理后的 image id、rgb_img_tensor和ir_img_tensor,并将该结构体push进icore任务队列。 icore线程只有1个,这是因为100TAI处理器的NPU为单核。这个线程将从Icore任务队列中读取image id,rgb_img_tensor和ir_img_tensor,用两个张量进行推理,将image id和推理出的output tensor push进后处理队列。 后处理线程则是根据icore推理的output tensor和对应的image id进行NMS和坐标缩放的后处理,并将结果写回SD卡。

    我们的异步流水线设计如下:

​ 在时间上的并行效果图如下:

​ 在T0时刻第一帧开始从SD卡中读出;在T1时刻读取结束后第2帧由第2个线程读入,此时第1帧开始前处理;在T2时刻第1帧前处理完毕,icore线程开始推理,此时第2帧还在前处理,并将在处理结束后等待NPU资源;在T3时刻第1帧icore推理结束,此时也开始了后处理,同时第2帧开始icore推理;在T4时刻第1帧后处理结束。

​ 可以看见,由于前处理时间小于icore推理时间,因此前处理线程、icore推理线程和后处理线程是并行化的,这将模型的时间瓶颈限制在icore推理线程的30ms(其中icore推理22.5ms,从PSDDR到PLDDR的memcpy7.5ms)推理上。并且对于从摄像头取帧的真实使用场景,取帧也能并行,进而可以实现输入输出线程全并行。

​ 在空间上的并行效果图如下:

​ 其中:

​ ①:4个输入线程,每个线程在PS端对RGB和IR图像做并行的前处理,并将结果写入PLDDR;

​ ②:AI访问PLDDR中存储的数据,完成icore的推理,得到output tensor;

​ ③:将PLDDR中的output tensor写入PSDDR(自动完成),在ps端进行多线程后处理,将锚框画在图像上;

​ ④:将PSDDR中的结果写回SD卡;

​ 通过速度优化,我们将端到端FPS由2帧提升到了16帧,速度上升了8倍。

  • 精度优化: 解决了速度优化后,我们将模型进行了精度测试,发现icraft模型精度和原始模型精度相差了10%。因此首先通过对比各个阶段生成的模型,发现模型在量化阶段精度正常,在适配阶段精度则少了10%。适配阶段引入了硬算子detpost。此时的可能性有两个,detpost输出异常或者detpost对应的后处理代码异常。由于icraft提供了Yolov5的例子,我们发现带detpost的Yolov5的C++推理程序精度没问题,而python程序精度同样有问题,因此发现了是python版本的nms错误,该nms使用了类间soft_nms,导致了多个重合度很高的但是是预测不同类别的锚框被删去,降低了预测精度。因此,更正这个bug后我们实现了精度优化。

​ 实现正常精度推理后,我们来到了第三个关键技术,混合精度优化。16位模型精度高,但icore推理时间达到了53ms(其中HardOp时间45ms,memcpy 8ms)无法满足需要。因此我们继续对8位模型进行了精度测试,发现其精度为85.6,相对于原始模型精度下降了1.8,所以我们考虑混合精度量化。我们使用icraft量化分析工具研究了8位模型的各层的量化余弦相似度cos sim(指量化前的全精度(Float32)张量数据和INT8张量数据在张量展开成向量后的方向一致性度量。)以分析模型各层量化的效果,将1-cos sim和MSE作为该层的local loss,计算全模型的平均local loss,将高于均值的层设置为INT 16。此时模型为混合精度模型,精度达到了86.2,相比8位模型提升了0.6,icore部分速度为31ms,仅比8位模型慢1ms,FPS仍然约有33帧,实现了精度于速度的良好平衡。

​ 基于最新发布的icraft 3.30套件,我们进一步地做了更多的工作。在3.7.1中我们发现,在PSIN时,使用BOOT_多路_24102404位流的情况下仍然无法使用双imagemake的原因是UDMA内存空间无法初始化,而使用3.30则可以通过手动指定内存空间地址并使用fpgaDma函数便可以使用两个imagemake硬算子同时处理RGB和IR的feature map,因此,此时的mix模型的前处理速度缩减到了18ms,而icore推理速度仍然为约30ms,这是由于imagemake算子虽然运算很快,但是其承担了memcpy的作用,大部分时间在把数据从psddr复制到plddr中。因此此时的FPS仍在33帧左右。

​ 我们还讨论了icore推理速度的瓶颈,这将在决赛答辩时展示。

  1. 运行时工程的整体介绍
  • 0_MOD_CFT

    该文件夹存放了所有源码。但是不包含数据集。

  • 1_scripts

    该文件夹的0_infer.py 和2_save_infer.py可以直接运行,无需配置参数。

  • 2_compile

    可以在终端打开该文件夹,通过

    icraft compile .\config\mod_cft_8_hard_1.toml
    icraft compile .\config\mod_cft_16_hard_1.toml
    

    来分别编译8位和16位模型。

    通过:

    icraft compile .\config\mod_cft_8_soft.toml
    

    来编译8位的不带硬算子的模型。

  • 3_deploy

    • 在.\cfg中的.yaml文件的sim字段需根据硬件终端不同来配置false和true。

    • 在pyrt文件夹中是python runtime的运行脚本,可运行程序有4个,分别是:

      • mod_cft_psin.py,用于加载模型,加载并前处理图片,推理图片,后处理并存入存储。

        运行方式为:在控制台进入文件夹后,输入命令:

        python .\mod_cft_psin.py ..\cfg\mod_cft_8_hard_1.yaml
        python .\mod_cft_psin.py ..\cfg\mod_cft_16_hard_1.yaml
        python .\mod_cft_psin.py ..\cfg\mod_cft_soft.yaml
        
      • test_new_8,test_new_16,test_new_soft,分别用于测试带硬算子的8位/16位和不带硬算子的8位模型的精度,运行方式为:

        python .\test_new_8
        python .\test_new_16
        python .\test_new_soft
        
    • 在src文件夹中是c++ runtime的推理代码,分别有mod_cft_psin_ori.cpp(未优化版本)、mod_cft_psin.cpp(三级流水线)两个版本,在CMakeLists.txt中配置好编译对象名,进入build_arm文件夹,输入命令:

      cmake ..
      cmake --build . --config Release
      

      即可得到PSIN或PLIN的可执行文件。在板端进入build_arm文件夹,输入命令:

      ./mod_cft_psin_ori ../cfg/mod_cft_8_hard_1.yaml
      ./mod_cft_psin ../cfg/mod_cft_8_hard_1.yaml
      

      来运行不同推理代码和不同模型。其中,要得到7.3中优化模型的效果,需要编译mod_cft_psin.cpp,并使用mod_cft_8_hard_1.yaml配置。