ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv5与DeepLabV3Plus的指针仪表自动读数方案研究

基于YOLOv5与DeepLabV3Plus的指针仪表自动读数方案研究 简介针对仪表检测、指针表盘分割与刻度读数识别场景这份 C 工程将 YOLOv5 与 DeepLabV3Plus 结合成端到端识别流程适合计算机、人工智能、自动化等专业的毕设、课设与项目初期演示也便于初学者在已有代码上继续扩展。压缩包共 11 个文件其中 4 个 cpp 与 4 个 h 构成核心源码模块另有 gitignore、说明文档和文本文件用于辅助工程配置与结构梳理整体仅有 22KB非常轻量易上手。资源覆盖模型调用、图像处理与结果输出等关键环节核心入口与公共组件、资源目录划分清晰便于按模块阅读可对照源码理解仪表读数识别链路也能借助附带说明减少环境配置和调试弯路。已有 126 人学习下载代码经过运行验证适合希望获得可运行参考实现并深入掌握相关原理的读者。1. 为什么仪表读数要搭两级模型YOLOv5 定位与 DeepLabV3Plus 分割的真实分工化工厂的压力表、水处理站的流量表拍照后如果想让程序自动读数你会发现真正的难点不在“读”而在“找”和“分”。仪表在画面里可能只占很小一块玻璃反光、指针阴影、表盘老化都会让边缘糊成一片。只靠 YOLOv5 目标检测可以框出表盘位置但拿不到指针角度只靠 DeepLabV3Plus 语义分割能把指针和表盘从背景里抠出来但整张图上几十个表盘分割起来既慢又容易错。所以这条方案把任务拆成两级YOLOv5 先负责仪表检测DeepLabV3Plus 再负责指针表盘分割最后由几何算法把指针角度映射成刻度读数。项目里是一套完整的 C 工程加文档说明适合毕设、课程设计也适合手里有具体表型、想直接部署到工控机上的从业者。2. 项目解剖两条模型的坐标如何对齐、推理如何串联拿到资源先别急着编译我拆下来看了一圈这套工程的结构是清晰的检测、分割、读数三段式管线。核心思路是“先裁剪、再分割、后读数”而不是让两个模型各自处理整张原图。这一章先把目录结构和数据流讲透再聊坐标映射这个最容易翻车的点。2.1 目录结构与三段式数据流工程目录大致是这个样子和你手里的包基本对得上Yolov5-DeepLabV3Plus-MeterReader-main ├── CMakeLists.txt ├── src │ ├── main.cpp │ ├── common │ │ ├── Detector.h / Detector.cpp │ │ └── Segmentor.h / Segmentor.cpp │ └── meter │ └── MeterReader.h / MeterReader.cpp ├── res │ └── 模型权重、测试图片、配置 └── 说明文档数据流是一环扣一环的main.cpp 读入一张图交给 Detector 做仪表检测拿到一个矩形框把矩形框从原图上裁剪出来这块裁剪图再交给 Segmentor 做语义分割得到指针和表盘的掩码最后掩码和一组标定参数一起交给 MeterReader算出具体刻度值。这里的关键设计是分割不在整张原图上跑而是在检测框的局部区域上跑。原因很直接工业现场的表盘在画面里占比小全图分割会引入大量背景干扰而且输入尺寸固定后小表盘会被严重压缩分割精度会掉得很难看。先检测再裁剪相当于用 YOLOv5 帮分割模型做一个注意力机制。2.2 检测器到分割器的坐标映射模型之间的坐标映射是这套工程最容易出错的地方我先把手写的主循环逻辑放出来再解释坐标系怎么对齐。// main.cpp 中的核心串联逻辑 cv::Rect box detector.detect(frame)[0]; // 原图坐标系 cv::Mat roi frame(box); // 裁剪仪表区域 cv::Mat mask segmentor.predict(roi); // 在 ROI 内分割 float reading meter_reader.read(mask, config); // 在掩码上算读数 // 如果要把 mask 叠加回原图记得把 ROI 的偏移量加回去 cv::Mat overlay cv::Mat::zeros(frame.size(), CV_8UC1); mask.copyTo(overlay(box));这段代码里Detector 输出的 Rect 是原图坐标系下的frame(box) 把这块区域裁出来之后后续所有操作都发生在局部坐标系里。分割模型拿到的是裁剪图它的输出掩码尺寸对应的是裁剪图尺寸而不是原图尺寸。如果要把掩码可视化到原图上必须在 copyTo 时把 box 的 x、y 偏移加回去否则掩码会画到图的左上角。这里有个实用的细节检测框通常会比指针的最小外接圆大一点但不会大太多。实际操作中如果表盘贴边指针扫过边缘时可能被框切掉导致分割结果缺一段。我一般会对检测框做 1.05 到 1.1 倍的扩张再裁剪给指针留一圈余量。这个扩张比例不能太大太大就失去了裁剪的意义分割模型又要处理更多背景。2.3 推理方式的取舍ONNX Runtime 还是 OpenCV DNN这个项目用的是 C 工程模型推理方式值得单独说一句。检测和分割模型都导出成 ONNX 格式后有两个部署路线OpenCV DNN 模块和 ONNX Runtime。我的建议是直接用 ONNX Runtime因为 DeepLabV3Plus 里带空洞卷积部分算子组合在 OpenCV DNN 里支持得不全会莫名其妙报错而 ONNX Runtime 的算子覆盖率和优化都更稳定还能开多线程加速。Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); // 算子内部并行线程数 opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); auto session Ort::Session(env, model_path.c_str(), opts);SetIntraOpNumThreads(4) 的意思是每个算子内部的并行计算最多用 4 个线程这个值要根据工控机 CPU 核心数调整4 到 8 是比较常见的区间。GraphOptimizationLevel 建议设成 ORT_ENABLE_ALL它会把相邻算子做融合、把常量折叠掉推理速度能快不少。这个配置对检测和分割两个 session 都适用创建两个会话时各开一份即可。3. C 侧工程化ONNX Runtime 推理封装与 CMake 构建这一章直接给你一份可以照着抄的工程配置。很多下载了源码的朋友第一关就是编译不过问题基本都出在 CMake 没写清楚、OpenCV 路径不对、onnxruntime 目录指错。我把 CMakeLists 和三个核心模块的封装都过一遍你对照着自己的环境改路径就能跑起来。3.1 CMakeLists 与依赖清单先看 CMakeLists.txt这个文件决定了整个工程的构建骨架cmake_minimum_required(VERSION 3.16) project(MeterReader CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # OpenCV需要 4.x 以上版本 find_package(OpenCV REQUIRED) # onnxruntime 官方发布包不自带 CMake config手动指目录 set(ONNX_RUNTIME_DIR ${CMAKE_SOURCE_DIR}/third_party/onnxruntime CACHE PATH onnxruntime 解压目录) include_directories(${ONNX_RUNTIME_DIR}/include) link_directories(${ONNX_RUNTIME_DIR}/lib) add_executable(meter_reader src/main.cpp src/common/Detector.cpp src/common/Segmentor.cpp src/meter/MeterReader.cpp ) target_include_directories(meter_reader PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(meter_reader ${OpenCV_LIBS} onnxruntime )这段 CMake 里最需要注意的是 onnxruntime 的引入方式。它不像 OpenCV 那样能直接 find_package官方发布的 zip 包里只有 include 目录、lib 目录和 dll没有提供 CMake config 文件。所以这里用 include_directories 和 link_directories 手动指目录然后在 target_link_libraries 里直接写 onnxruntime 这个名字链接器会去 lib 目录里找 onnxruntime.lib。构建命令我一般这样执行cmake -B build -DONNX_RUNTIME_DIRD:/libs/onnxruntime-win-x64 -DOpenCV_DIRD:/libs/opencv/build cmake --build build --config ReleaseONNX_RUNTIME_DIR 指向你解压出来的 onnxruntime 目录OpenCV_DIR 指向包含 OpenCVConfig.cmake 的目录。不管你在 VS2022 还是 VS Code 配的 C 环境这套 CMake 流程都是一样的唯一的要求是 OpenCV 版本至少 4.5太老的版本对 blobFromImage 的封装和 dnn 模块的支持都不够稳。3.2 Detector 封装blobFromImage 与 YOLOv5 后处理Detector 负责把图片转成 YOLOv5 需要的输入格式跑推理再把输出解码成一组矩形框。YOLOv5 的输出是 [1, 25200, 85] 的张量85 代表 4 个框坐标 1 个置信度 80 个类别分数。这里我们只需要检测仪表这一类所以后处理时要按类别过滤。// src/common/Detector.cpp std::vectorcv::Rect Detector::detect(const cv::Mat frame) { // 1. 预处理缩放、归一化、RGB 顺序 cv::Mat input; cv::dnn::blobFromImage(frame, input, 1 / 255.0, cv::Size(640, 640), cv::Scalar(), true, false); // 2. 拷贝到 ONNX Runtime 的输入张量 Ort::Value input_tensor Ort::Value::CreateTensorfloat( mem_info, input.ptrfloat(), 1 * 3 * 640 * 640, input_shape.data(), input_shape.size()); auto outputs session.Run(Ort::RunOptions{nullptr}, input_names.data(), input_tensor, 1, output_names.data(), 1); // 3. 从输出张量里拿数据 const float* data outputs[0].GetTensorDatafloat(); auto shape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int rows static_castint(shape[1]); // 25200 int cols static_castint(shape[2]); // 85 // 4. 解码 NMS std::vectorcv::Rect boxes; std::vectorfloat scores; for (int i 0; i rows; i) { const float* row data i * cols; float obj_conf row[4]; if (obj_conf 0.45f) continue; float max_cls 0.f; for (int j 5; j cols; j) { max_cls std::max(max_cls, row[j]); } float final_conf obj_conf * max_cls; if (final_conf 0.45f) continue; float cx row[0] * 640.0f; float cy row[1] * 640.0f; float w row[2] * 640.0f; float h row[3] * 640.0f; boxes.emplace_back(cx - w / 2, cy - h / 2, w, h); scores.emplace_back(final_conf); } std::vectorint keep; cv::dnn::NMSBoxes(boxes, scores, 0.45f, 0.5f, keep); std::vectorcv::Rect result; for (int idx : keep) result.push_back(boxes[idx]); return result; }这段代码里有几个参数值得单独说。blobFromImage 的 1/255.0 是把像素值归一化到 0 到 1这是 YOLOv5 训练时的标准做法第三个参数 true 表示把 BGR 转成 RGB因为 PyTorch 训练时用的是 RGB 顺序640 是模型的输入尺寸如果你的模型导出时用的是 416 或其他尺寸这里必须跟着改。置信度阈值 0.45 和 NMS 的 IoU 阈值 0.5 是经验值。检测仪表这种大目标0.45 的置信度足够再低会把背景误检进来IoU 0.5 可以容忍两个框有部分重叠。后处理拿到的是模型输入尺寸下的像素坐标因为 blob 是整图缩放而来的所以这些坐标天然就是原图坐标系不需要再做坐标变换。3.3 Segmentor 封装argmax 得到指针掩码分割模型和检测模型的封装思路类似但输出处理完全不同。DeepLabV3Plus 的输出是 [1, num_classes, H, W] 的分数图需要逐像素取最大值所在的类别索引得到一张掩码图。// src/common/Segmentor.cpp cv::Mat Segmentor::predict(const cv::Mat image) { int w image.cols, h image.rows; // 1. 缩放到模型输入尺寸 cv::Mat resized; cv::resize(image, resized, cv::Size(input_w, input_h)); // 2. 转 RGB、归一化 cv::Mat rgb, float_img; cv::cvtColor(resized, rgb, cv::COLOR_BGR2RGB); rgb.convertTo(float_img, CV_32FC3, 1.0 / 255.0); // 3. 构造 [1,3,H,W] 的 blob推理 std::vectorfloat input_data(1 * 3 * input_h * input_w); // 这里把 float_img 按通道拆开填入 input_dataHWC 转 CHW // ... 略去通道拆分循环 Ort::Value input_tensor Ort::Value::CreateTensorfloat( mem_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); auto outputs session.Run(...); // 4. 输出是 [1, num_classes, H, W]逐像素 argmax const float* out outputs[0].GetTensorDatafloat(); cv::Mat mask(input_h, input_w, CV_8UC1); for (int i 0; i input_h * input_w; i) { int best_cls 0; float best_score out[i]; for (int c 1; c num_classes; c) { float score out[c * input_h * input_w i]; if (score best_score) { best_score score; best_cls c; } } mask.atuchar(i) static_castuchar(best_cls); } // 5. 缩放回原 ROI 尺寸 cv::Mat result; cv::resize(mask, result, cv::Size(w, h), 0, 0, cv::INTER_NEAREST); return result; }argmax 这块的索引要仔细ONNX 输出的内存布局是 NCHW即先按类别排列再按高宽排列。所以类别 c 的像素位置 i 对应的分数要从 out[c * H * W i] 取而不是 out[i * num_classes c]。这两种布局搞混了分割结果会像花屏一样全是噪点。最后一步 resize 使用 INTER_NEAREST 而不是线性插值因为掩码是类别标签线性插值会产生不存在的类别号。3.4 main.cpp 的完整调用链三个模块封装好之后main.cpp 就变得很短了。它的职责是读图、调三个模块、打印结果// src/main.cpp int main(int argc, char** argv) { std::string model_dir argv[1]; // res 目录 Detector detector(model_dir /yolov5s_meter.onnx, 640); Segmentor segmentor(model_dir /meter_seg.onnx, 512); MeterReader reader; reader.set_scale(-45.0f, 225.0f, 0.0f, 1.6f); // 参数依次是刻度起始角、终止角、量程下限、量程上限 cv::Mat frame cv::imread(argv[2]); auto boxes detector.detect(frame); for (const auto box : boxes) { cv::Mat roi frame(box); cv::Mat mask segmentor.predict(roi); float value reader.read(mask); std::cout reading: value std::endl; } return 0; }这段主流程里MeterReader 的 set_scale 四个参数是整个读数算法的灵魂-45 度和 225 度是这个表盘量程起点和终点相对表盘圆心的方位角0 和 1.6 是压力表的量程上下限。不同型号的仪表这四个参数完全不同后面第四章会专门讲怎么标定。4. 指针读数算法从分割掩码到刻度数值的四个步骤分割模型返回的掩码只是一张打了标签的图要变成最终读数需要经历四个步骤提取指针方向、做方向规整、处理跨零点、线性映射到刻度。这一章把每一步的数学原理和代码都讲清楚这是整个项目里技术含量最高的部分。4.1 指针方向提取PCA 拟合与重心法的对比拿到掩码后最简单的想法是找指针区域的质心。但质心只能代表区域的中心指针是细长结构质心位置受表盘中心点影响很大完全无法表达方向。常见做法是用 PCA 对指针像素集合拟合主轴主轴方向就是指针的指向。// src/meter/MeterReader.cpp float MeterReader::extract_angle(const cv::Mat mask) { // 1. 收集所有指针类别的像素坐标 std::vectorcv::Point2f pts; for (int y 0; y mask.rows; y) { for (int x 0; x mask.cols; x) { if (mask.atuchar(y, x) POINTER_CLASS) { pts.emplace_back(x, y); } } } if (pts.size() 50) { return -1.0f; // 像素太少认为分割失败 } // 2. 把坐标整理成 Nx2 矩阵 cv::Mat pts_mat(static_castint(pts.size()), 2, CV_32F); for (size_t i 0; i pts.size(); i) { pts_mat.atfloat(static_castint(i), 0) pts[i].x; pts_mat.atfloat(static_castint(i), 1) pts[i].y; } // 3. PCA 求主轴方向 cv::PCA pca(pts_mat, cv::Mat(), cv::PCA::DATA_AS_ROW, 2); float dx pca.eigenvectors.atfloat(0, 0); float dy pca.eigenvectors.atfloat(0, 1); float angle std::atan2(dy, dx) * 180.0f / static_castfloat(CV_PI); return angle; }PCA 里 eigenvectors 返回的是按特征值从大到小排列的方向向量第一行就是最大特征值对应的主轴。对指针这种细长结构来说主轴基本就是指针中心线的方向。atan2 得到的角度单位是度范围在 -180 到 180 之间。这个角度是相对图像 x 轴正方向的它本身不直接等于刻度角度需要通过后面的步骤转换。4.2 方向规整直线方向歧义与跨零点处理PCA 拟合出来的是直线不是射线所以 45 度和 -135 度在几何上是同一条线但指针读数完全不同。这就是方向歧义问题不处理的话读数会确定性跳变。float MeterReader::normalize_angle(float angle, float start_angle, float end_angle) { // 两个候选方向原直线方向以及反向 180 度 float cand1 angle; float cand2 angle 180.0f; if (cand2 180.0f) cand2 - 360.0f; // 取落在量程区间内的那个方向 if (is_angle_between(cand1, start_angle, end_angle)) return cand1; if (is_angle_between(cand2, start_angle, end_angle)) return cand2; return cand1; // 两个都不在区间内时默认返回第一个 }选择的标准很直接量程起点到终点覆盖一个固定的方位角区间比如 -45 度到 225 度指针的真实方向一定落在这个区间内反向的直线方向则落在对面。所以把两个候选角度分别和区间做比较落在区间内的那个就是真实方向。这个做法要求 start_angle 和 end_angle 标定得足够准否则可能两个候选都在区间外那样我就按默认方向输出同时打印一条警告。跨零点问题出在角度从 359 度变到 1 度的时候表格上指针只走了 2 度但数值上跳了 358 度。处理方式是先把角度换算成相对量程起点的偏转量再取模float MeterReader::angle_to_value(float angle, float start_angle, float end_angle, float min_value, float max_value) { // 把角度都归一化到 0 ~ 360 区间 float span end_angle - start_angle; if (span 0) span 360.0f; float offset angle - start_angle; if (offset 0) offset 360.0f; float ratio offset / span; ratio std::clamp(ratio, 0.0f, 1.0f); return min_value ratio * (max_value - min_value); }这里把差值换算成 0 到 360 的范围再除以量程跨度得到指针在量程内的百分比位置最后线性映射到实际数值。这样即使指针走过零点计算也不会跳变。4.3 角度到刻度线性映射与标定参数最终读数函数把前面三步串起来再加上一组标定参数控制。标定参数是这套系统能否落地的关键每个表型的参数都不一样参数含义示例值start_angle量程起点相对表盘圆心的方位角-45 度end_angle量程终点相对表盘圆心的方位角225 度min_value量程下限刻度值0max_value量程上限刻度值1.6 MPastart_angle 和 end_angle 的标定方法很简单找一张表盘正对着镜头的照片量程起点的刻度线相对圆心的方位角就是 start_angle终点的刻度线就是 end_angle。可以用画图工具直接量角度也可以用程序标定一次后存成配置文件。min_value 和 max_value 直接读表盘印刷的数字即可。float MeterReader::read(const cv::Mat mask) { float angle extract_angle(mask); if (angle 0.0f) return -1.0f; // 分割失败 angle normalize_angle(angle, start_angle_, end_angle_); return angle_to_value(angle, start_angle_, end_angle_, min_value_, max_value_); }这套几何算法本身的精度上限取决于分割掩码的质量。如果分割把表盘边缘误判成指针PCA 主轴会偏向表盘圆弧方向读数会明显偏大或偏小。所以训练分割模型时指针类别建议单独占一个通道表盘留白部分可以分到背景尽量不要让表盘的圆弧边缘参加 PCA。5. 避坑与排查检测漏框到读数跳变的五个典型现场这套工程我在实际跑的时候踩过不少坑下面这五条是从最容易翻车的地方里挑出来的每一条都是现实中真实发生过的现象按“现象 → 原因 → 解决”的格式给出来。5.1 检测框能出分割结果却整片背景现象YOLOv5 把仪表框出来了但 Segmentor 输出的掩码全是 0指针完全没被分割出来。第一反应通常是模型坏了重新下载权重也没用。原因检测和分割两个模型的预处理不一致。YOLOv5 用 1/255 归一化加 RGB 顺序DeepLabV3Plus 训练时如果用的是 BGR 加 0 到 1 归一化或者反过来输入数据的分布就对不上。这类问题表现得很隐蔽因为推理不报错只是结果全错。解决把两个模型各自的预处理参数打印出来对比。重点检查三件事归一化系数是 1/255 还是均值方差、通道顺序是 RGB 还是 BGR、颜色通道是 HWC 排列还是 CHW 排列。我当时查了一个下午最后发现是分割模型的 normalize 参数写成了 1.0/127.5 减 1.0改回 1/255 后立刻正常。5.2 读数稳定地差约 180 度现象表盘指在 0.5 MPa程序读出来是 1.1 MPa而且每张图都稳定地差这么大不是随机误差。原因这就是 4.2 里说的 PCA 方向歧义。直线方向相差 180 度在数学上是同一条线但映射到角度刻度时差了半个量程。解决不要直接信任 PCA 的第一个主轴方向强制做方向规整。把 normalize_angle 的两个候选方向都算出来落在量程区间内的那个才是正确的。这个坑的隐蔽之处在于如果标定好的量程区间是 0 到 180 度而实际量程是 200 度的表两个候选方向可能有一个恰好落在区间外一个落在区间内这时不写判断逻辑就会静默出错。5.3 Debug 编译时链接 onnxruntime 崩溃现象用 CMake 构建 Debug 版编译通过一运行到加载模型就崩报内存访问异常换成 Release 版就一切正常。原因onnxruntime 官方发布的 Windows 库只包含 Release 版本Debug 工程链接的 CRT 是 debug 版本两者运行时库不一致导致崩溃。这是 C 工程最常见的坑没有之一。解决构建时强制指定 Release 配置cmake --build build --config Release。如果确实需要 Debug 调试需要自己去编译开源版本的 onnxruntime Debug 库或者用 Release 版加日志输出来定位问题。我一般直接 Release 构建然后通过打印中间结果来调试。5.4 Windows 下运行提示缺少 onnxruntime.dll现象编译链接都成功但运行时弹窗提示找不到 onnxruntime.dll程序直接退出。原因链接只需要 onnxruntime.lib但运行需要 onnxruntime.dll。CMake 只处理了链接阶段dll 不会自动复制到 exe 目录。解决把 onnxruntime.dll 手动复制到 exe 同目录或者放到系统 PATH 中。最稳妥的方式是在 CMakeLists 里加一个 post-build 命令每次构建后自动拷贝add_custom_command(TARGET meter_reader POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ONNX_RUNTIME_DIR}/lib/onnxruntime.dll $TARGET_FILE_DIR:meter_reader)从这以后我每次配置新环境都先检查 dll 是否就位再跑图像。5.5 同一块表换个角度拍读数漂移 5%现象表盘正对镜头时读数很准稍微侧一点角度拍读数偏差能到 5% 以上刻度越大偏差越明显。原因透视变形。侧着拍时表盘的圆形轮廓会变成椭圆指针方向相对圆心的角度也被扭曲而我们的几何算法假设的是正俯视视角。解决最省事的方式是在训练检测模型时标注框尽量贴合表盘圆形区域同时处理 ROI 时做透视矫正把圆形的点映射回正视图像。如果只是做毕设演示建议采集数据时保持相机与表盘正面平行减少透视影响。工业场景则必须配固定机位或者先做四点透视变换后再进读数管线。6. 部署前的自检把读数误差压进 2% 以内的标定技巧最后一章不写大而全的内容只讲一个具体技巧怎么用可视化叠加和合成图回归两步把读数精度压进 2% 以内。这个精度对大多数指针仪表巡检场景是够用的也是我验收每个新表型必做的流程。6.1 用可视化叠加验证角度偏没偏读数之前先把检测框、分割掩码和计算出的指针方向线叠加到原图上保存成一张验证图。这步花不了几行代码但对排查问题价值很大。方向线画错了图上立刻就能看到不画出来的话只能看到数值偏差根本定位不到是分割问题还是角度问题。6.2 用合成表盘做回归测试对每个表型用画图程序生成几个已知角度的合成表盘图指针位置是手工确定的量程起点终点也是已知的。把这批图喂给程序看输出值和真值的绝对误差。这一步能验证映射公式和标定参数是否正确比拿真实照片去试要可控得多。真实照片有光照、反光、遮挡等干扰但合成表盘没有误差如果还大于 1%一定是代码逻辑问题先查再换真实数据。6.3 把标定参数外置成配置文件我在跑完第一个表型之后就把四个参数写进了 JSON 配置文件而不是每换一个表型就重新编译一遍。配置项包括 start_angle、end_angle、min_value、max_value以及检测和分割的模型路径。后面接新表型时只需要替换模型权重、改配置代码一行不动。这三次翻车经历教会我一个道理视觉读数项目的误差往往不是算法不够高级而是标定参数和预处理没对齐。从那以后我每次接新的表型都会强制走一遍合成图回归加可视化叠加的流程确认角度线画对了再上现场。希望帮到你。本文还有配套的精品资源点击获取
返回列表