基于icraft的双模态目标检测模型端到端优化部署关键技术
1. 摘要
-
身份编号:fwb0052
-
类型与序号:
双模态目标检测模型DOD/双模态目标检测模型端到端优化部署关键技术
-
进展:
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的流程一致。
前处理:
-
读取图像,调整尺寸:
- 使用 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
-
输入排布:
- DOD的输入排布是NCHW
- DOD输入的通道顺序是RGB
im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
-
转换为张量:
框架下会将图像转换为 PyTorch 张量,并以符合框架下模型输入的要求。
im = np.ascontiguousarray(im) # contiguous im = torch.from_numpy(im) im = im.half() if model.fp16 else im.float() # uint8 to fp16/32
-
归一化处理:
- 将像素值从 [0, 255] 归一化到 [0, 1] 的范围。
img = img / 255.0 # 归一化 if len(im.shape) == 3: im = im[None] # expand for batch dim
后处理:
-
阈值筛选:
- 置信度阈值用于筛选检测结果中,只有超过该置信度的检测框才能被保留。通常在模型的输出后,使用
conf_thres参数来设置这个阈值。在 DOD的代码中,这个阈值常常用于 NMS(非极大值抑制)之前。
xc = prediction[..., 4] > conf_thres # candidates
- 置信度阈值用于筛选检测结果中,只有超过该置信度的检测框才能被保留。通常在模型的输出后,使用
-
应用非极大值抑制(NMS):
- 对于检测到的多个框,使用 NMS 来消除重叠较大的框,只保留最有可能的框。
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
-
可视化:
- 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. 模型导出说明
- 如下图所示:在模型中,和Yolov5一样,我们将Detect头(用于解码和张量聚合)去除,放到了后处理部分。
- 在代码中的写法与demo中的Yolov5一样,表示如下:
# 修改前向,去除前向中后处理操作 def new_forward(self, x): for i in range(self.nl): x[i] = self.m[i](x[i]) # conv return x
- 配置好权重文件和环境后,脚本可以直接使用python 1_save.py运行
5. cpu处理部分c++工程移植说明
- 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 }
- 在移植后的C++程序中,我们对进行了如下方法验证准确性:
- 对比了C++程序与python runtime程序中icore推理的output tensor的形状
- 对比了C++程序与python runtime程序nms后的tensor的数值
- 对比了可视化后多张图片的锚框位置与置信度差异
-
带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编译
编译通过
-
使用运行时仿真得到的仿真效果截图或数据,需提供下述表格:
框架动态图推理 导出静态模型推理 解析组件仿真 量化组件仿真 指令组件仿真 




-
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. 上板测试与优化
-
优化前各环节耗时统计(表格) 优化前各环节耗时统计表格如下(带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 -
优化后耗时统计(表格) 优化后各环节耗时统计表格如下:
环节 耗时(ms) BGR2RGB 6.17 resize+pad 12.39 imgmake 7.01 icore推理 30.45 output tensor解码 0.143 nms与坐标转换 0.029 -
优化项与效果
-
速度优化:
关键技术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推理速度的瓶颈,这将在决赛答辩时展示。
- 运行时工程的整体介绍
-
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配置。
-