ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RK3588 上从 0 部署 YOLOv5s:我的全链路实践(五)C++ NMS 加速 359 倍

2026/9/23 8:28:30 拓冰建站 浏览量
RK3588 上从 0 部署 YOLOv5s:我的全链路实践(五)C++ NMS 加速 359 倍 背景同样的 1000 个候选框Python 版 NMS 要跑 6.7 秒C 版只要 18.8 毫秒。这不是改算法是把同一份逻辑从解释器搬到机器码。加速 359 倍结果逐索引完全一致。阶段 5C NMS创建脚本。50_bench_nms.py#!/usr/bin/env python3 ## 阶段5NMS 性能对比——Python 参考实现 vs Cpybind11 # 作用【Python vs C耗时 ms】难点2【X ms → Y ms】 # 用法cd build 旁的目录#python350_bench_nms.py/path/to/build 含 nms_cpp*.so 的目录#python350_bench_nms.py none 只跑 Python 基线也行#importsys,os,json,time,statistics,datetimeimportnumpyas np REPEAT5# 原为100为了不让 Python 双重循环等太久降到5加速比几乎不变 N_BOXES1000# 候选框数量级YOLOv5s640输入 conf0.25后典型几百~上千 deflog_metrics(rec):os.makedirs(metrics,exist_okTrue)pmetrics/metrics.jsonarrjson.load(open(p))ifos.path.exists(p)else[]arr.append(rec)json.dump(arr,open(p,w),ensure_asciiFalse,indent2)defpy_nms(boxes,scores,iou_thresh):Python 参考实现和 C 完全同算法公平对比的前提ordernp.argsort(-scores)keep[]suppressednp.zeros(len(scores),dtypebool)fori in order:ifsuppressed[i]:continuekeep.append(int(i))aboxes[i]forj in order:ifsuppressed[j]orji:continuebboxes[j]x1,y1max(a[0],b[0]),max(a[1],b[1])x2,y2min(a[2],b[2]),min(a[3],b[3])intermax(0.0,x2-x1)*max(0.0,y2-y1)union(a[2]-a[0])*(a[3]-a[1])(b[2]-b[0])*(b[3]-b[1])-interifinter/(union1e-6)iou_thresh:suppressed[j]Truereturnkeep defgen_boxes(n,seed42):合成数据60% 框挤在同一区域高重叠模拟密集检测场景rngnp.random.RandomState(seed)xyrng.rand(n,2)*50060# 顶点集中在画布中央 whrng.rand(n,2)*8010boxesnp.hstack([xy,xywh]).astype(np.float32)scoresrng.rand(n).astype(np.float32)returnboxes,scores defbench(fn,boxes,scores,thr,repeat,label):ts[]fork inrange(repeat):t0time.perf_counter()fn(boxes,scores,thr)dt(time.perf_counter()-t0)*1000ts.append(dt)print( [%s] 第 %d/%d 次: %.3f ms%(label,k1,repeat,dt),flushTrue)returnround(statistics.mean(ts),3)if__name____main__:so_dirsys.argv[1]iflen(sys.argv)1elsebuildprint([1/3] 生成 %d 个模拟框...%N_BOXES,flushTrue)boxes,scoresgen_boxes(N_BOXES)print([2/3] 跑 Python 版 NMS%d 次...%REPEAT,flushTrue)py_msbench(lambda b,s,t:py_nms(b,s,t),boxes,scores,0.45,REPEAT,py )cpp_ms,speedup,agreeNone,None,Noneifso_dir!none:ifos.path.isdir(so_dir):sys.path.insert(0,so_dir)print([3/3] 跑 C 版 NMS ...,flushTrue)importnms_cpp# 正确性验证同输入下两种实现保留的索引集合必须一致 k_pyset(py_nms(boxes,scores,0.45))k_cppset(int(i)fori in nms_cpp.nms(boxes,scores,0.45))agree(k_pyk_cpp)assert agree,C 与 Python 结果不一致——先别谈性能查算法cpp_msbench(lambda b,s,t:nms_cpp.nms(b,s,t),boxes,scores,0.45,REPEAT,cpp)speedupround(py_ms/cpp_ms,1)rec{tag:nms_bench,ts:datetime.datetime.now().isoformat(timespecseconds),n_boxes:N_BOXES,repeat:REPEAT,python_ms:py_ms,cpp_ms:cpp_ms,speedup_x:speedup,result_identical:agree,}log_metrics(rec)print(json.dumps(rec,ensure_asciiFalse,indent2))print(\n[口径] N{} 候选框、重复 {} 次取均值结果一致性必须为 True 再引用速度比.format(N_BOXES,REPEAT))CMakeLists.txtcmake_minimum_required(VERSION3.15)project(nms_cpp LANGUAGES CXX)set(CMAKE_CXX_STANDARD14)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_POSITION_INDEPENDENT_CODE ON)# 用法先 pip install pybind11# cmake -B build -Dpybind11_DIR$(python3 -m pybind11 --cmakedir)# cmake --build build# 产物build/nms_cpp*.sopython 侧把该目录加进 sys.path 即可 importfind_package(pybind11 CONFIG REQUIRED)pybind11_add_module(nms_cpp nms.cpp)nms.cpp//// 阶段5C NMS 后处理pybind11 绑定 Python // 作用【后处理 C 重写】 难点2 证据 // 输入boxes(N,4)float32 xyxy 像素坐标scores(N,)float32iou_thresh // 输出保留框的索引列表按分数降序 // 算法按分数降序贪心——取当前最高分框删除与它 IoUthresh 的所有框 //#include pybind11/pybind11.h#include pybind11/numpy.h#include pybind11/stl.h#include vector#include algorithm#include cmathnamespace pypybind11;// IoUIntersection over Union交并比两框重叠程度1完全重合 static inline float iou_xyxy(const float* a, const float* b){float x1std::max(a[0], b[0]);float y1std::max(a[1], b[1]);float x2std::min(a[2], b[2]);float y2std::min(a[3], b[3]);float wstd::max(0.f, x2 - x1);float hstd::max(0.f, y2 - y1);float interw * h;float area_a(a[2]- a[0])*(a[3]- a[1]);float area_b(b[2]- b[0])*(b[3]- b[1]);returninter /(area_a area_b - inter 1e-6f);}std::vectorint64_tnms(py::array_tfloat, py::array::c_style|py::array::forcecastboxes, py::array_tfloat, py::array::c_style|py::array::forcecastscores, float iou_thresh){if(boxes.ndim()!2||boxes.shape(1)!4)throw std::runtime_error(boxes 必须是 (N,4) xyxy);auto Bboxes.unchecked2();//(N,4)auto Sscores.unchecked1();//(N,)int n(int)B.shape(0);if(n0)return{};const float* bp(const float*)boxes.data();// argsort分数降序这就是按置信度排序 std::vectorintorder(n);for(int i0;in;i)order[i]i;std::sort(order.begin(), order.end(),[](int a, int b){returnS[a]S[b];});std::vectorcharsuppressed(n,0);std::vectorint64_tkeep;for(int i0;in;i){int idxorder[i];if(suppressed[idx])continue;keep.push_back((int64_t)idx);const float* curbp (size_t)idx *4;// 删除所有与当前框高度重叠的候选贪心O(N^2)for(int ji 1;jn;j){int jdxorder[j];if(suppressed[jdx])continue;if(iou_xyxy(cur, bp (size_t)jdx *4)iou_thresh)suppressed[jdx]1;}}returnkeep;// 索引数组Python 侧直接 numpy 索引}PYBIND11_MODULE(nms_cpp, m){m.doc()C NMS (pybind11);m.def(nms,nms, py::arg(boxes), py::arg(scores), py::arg(iou_thresh)0.45f);}生成 CMake 构建配置cmake-Bbuild-Dpybind11_DIR$(python3-mpybind11--cmakedir)编译cmake--buildbuild执行脚本对比前后数据python3 50_bench_nms.py build数据总结要点分析为什么Python这么慢答因为Python版NMS删掉重复框只留最准的那个是“1000*1000“的双重for循环每次迭代都涉及Python解释器开销、numpy索引、max/min调用。100万次迭代光解释器开销就要好几秒。为什么C这么快答C编译后变成机器码loU两个框重叠程度的百分比计算只有几十条汇编指令且用const float*直接指针访问内存零拷贝没有解释器开销。RK3588的A76单核跑1000次框的NMS只用18ms完全合理。“加速的前提是 result_identical: True。 我用了相同的贪心算法C 版返回的保留索引集合与 Python 版完全一致。性能优化绝不能牺牲正确性。”“这个基准测试用的是 1000 个框最坏情况。实际部署中经过 conf0.25 过滤后通常只剩 50-200 个框那时 C NMS 只需 0.2-1 ms完全不会成为瓶颈。”加速的前提是 result_identical: True。我用了相同的贪心算法C 版返回的保留索引集合与 Python 版完全一致。性能优化绝不能牺牲正确性。下一篇讲 mAP 精度评估INT8 量化到底掉多少点5000 张 COCO 图全量实测。更多更详细内容请查看CSDN链接https://blog.csdn.net/2501_92605570?spm1011.2415.3001.5343