简介:指针仪表数据集面向目标检测任务,适合机器学习研究者、算法开发者及需要仪表读数识别项目实践的中高级学习者,用于训练与评估指针仪表定位及关键读数信息提取模型。压缩包共785个文件,以783张jpg仪表图像为主,另含2个json格式的标注与数据划分文件,整体约950.3MB,可支撑训练集、测试集的组织与模型泛化能力验证。已有303人学习,说明该数据在仪表检测与读数识别方向具有一定参考价值。图像样本覆盖多种表盘与拍摄视角,配合标注文件可开展目标框、指针方向及读数关联信息的解析;训练集用于模型学习,测试集便于检验未见样本上的检测效果。包内结构清晰,适合按类别与用途检索,能为自动化仪表读数、智能巡检等场景的算法复现与实验对比提供基础数据。
1. 指针仪表数据集解压之后:训练集、测试集和 xml 哪个先看
一份指针仪表数据集通常长这样:几个按日期或设备编号命名的文件夹,里面是现场拍的 JPG,旁边躺着同名 xml,再往外一层是 train 和 test 两个目录。不少人的动作是直接找 train 丢进模型,三天后回来发现 test 里一半表盘压根没标过,或者 xml 里画的是整块表盘、模型却要在指针上出框,返工成本比从头做还高。指针仪表数据集的难度从来不在模型,而在「训练集和测试集是不是同一批工况」「xml 里的框到底代表什么」。这篇按我自己做工业巡检项目的顺序讲:先把 xml 校验清楚,再转格式、切分复核,训练完把检测框变成读数。适合手里已经有一份带 xml 的仪表数据、准备做表盘检测或指针读数识别的人。
2. 指针仪表 xml 标注拆开看:字段、类别与框边界的三个必查项
2.1 一条 VOC xml 里哪些字段真的会影响训练
先看一条典型标注,别急着写解析器,先把字段语义对上:
<annotation> <folder>gauge_0412</folder> <filename>gauge_0412_0031.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>dial</name> <difficult>0</difficult> <bndbox> <xmin>512</xmin><ymin>300</ymin><xmax>1100</xmax><ymax>890</ymax> </bndbox> </object> <object> <name>pointer</name> <bndbox> <xmin>760</xmin><ymin>430</ymin><xmax>812</xmax><ymax>640</ymax> </bndbox> </object> </annotation>真正决定训练效果的只有四个东西:filename(能不能和图片对上)、size(宽度高度是不是真实图像尺寸)、object/name(类别名,决定类别映射)、bndbox(坐标本身)。folder、path、segmented、difficult这些字段在绝大多数检测训练里是冗余的,但path里带着的绝对路径偶尔会暴露原始采集机的目录结构——如果这份数据要往外分发,先把它清掉。
三个必查项按优先级排:
第一,filename与磁盘文件是否严格一致。带空格、大小写不同、后缀写成.JPG,都会让后面的转换脚本静默跳过,最后标签数量少了你还以为是模型的问题。常见做法是在统计脚本里把filename统一strip()并按小写比对。
第二,size是否等于真实图片尺寸。这条最容易被忽略。图片被批量压缩过(比如长边从 1920 缩到 1280),xml 里的 size 没跟着改,转换出来的归一化坐标会整体偏小,框全挤向左上角。稳妥做法是转换时以cv2.imread读到的真实尺寸为准,xml 里的 size 只用来交叉验证。
第三,bndbox的坐标基准。VOC 惯例是 1-based 闭区间,即xmin从 1 开始、xmax是框内最后一个像素。有些标注工具输出的是 0-based 连续坐标。差 1 个像素对检测几乎无影响,但一套数据里两种基准混用就必须统一——最怕的是转换脚本一半减一、一半没减。
至于类别名,别假设它只叫 dial 和 pointer。我见过的命名有Dial、dial_area、gauge、meter、表盘,甚至同一个文件夹里两种写法并存。.lower()之后做一次Counter统计,比任何猜测都省事。
2.2 用 xml.etree 跑一遍全量统计:类别、框尺寸、缺图
标注看一眼就够了,全量统计必须跑脚本。下面这段是通用的体检脚本,标准库就够,不引入额外依赖:
import collections from pathlib import Path import xml.etree.ElementTree as ET XML_DIR = Path("annotations/xml") # 原始 xml 目录 IMG_DIR = Path("images/all") # 与 xml 对应的原图目录 cls_stat = collections.Counter() # 类别 -> 目标数 area_stat = collections.Counter() # 框面积分档 missing_img, bad_box, parse_err, empty_xml = [], [], [], [] for xp in sorted(XML_DIR.glob("*.xml")): try: root = ET.parse(xp).getroot() except ET.ParseError as e: # 半截文件、编码损坏都在这里暴露 parse_err.append((xp.name, str(e))) continue fname = (root.findtext("filename") or "").strip() if not fname or not (IMG_DIR / fname).exists(): missing_img.append(xp.name) continue size = root.find("size") W, H = int(size.findtext("width")), int(size.findtext("height")) objs = root.findall("object") if not objs: # 负样本 xml,要单独统计 empty_xml.append(xp.name) continue for obj in objs: name = (obj.findtext("name") or "").strip().lower() cls_stat[name] += 1 bb = obj.find("bndbox") x1, y1 = float(bb.findtext("xmin")), float(bb.findtext("ymin")) x2, y2 = float(bb.findtext("xmax")), float(bb.findtext("ymax")) if x2 <= x1 or y2 <= y1: # 坐标反向,标注时拖反了 bad_box.append((xp.name, name, "reverse")) continue if x1 < 0 or y1 < 0 or x2 > W or y2 > H: bad_box.append((xp.name, name, "out_of_range")) w, h = x2 - x1, y2 - y1 area_stat["tiny" if w * h < 32 * 32 else "normal"] += 1 print("类别分布 :", dict(cls_stat)) print("框面积档 :", dict(area_stat)) print("空 xml :", len(empty_xml)) print("缺图 :", len(missing_img), missing_img[:5]) print("坏坐标 :", len(bad_box), bad_box[:5]) print("解析失败 :", len(parse_err), parse_err[:5])逐项说明这段脚本的判断逻辑。parse_err优先看,它意味着文件根本不完整,后面所有的数量对账都会被它带偏。missing_img用的是root.findtext("filename")而不是文件名 stem,是因为有些工具的 xml 名和里面的 filename 并不一致,必须以 xml 内声明的为准才符合推理时的对应关系。area_stat里 32×32 这个阈值不是标准,只是我习惯拿它区分「正常框」和「可能是刻度线被错标成目标」的小框——指针细长的时候短边很容易掉到 10 像素以下,这类框在 640 输入下基本消失。
参数上可调的是XML_DIR和IMG_DIR,其余不用动。跑完把四个数字记下来,后面转换完要拿它对账。
2.3 表盘框、指针框与刻度区:标注边界怎么定才不返工
统计干净了,接下来是标准问题,也是返工最多的地方。同一个人隔一周标同一批图,表盘框的松紧都能差出 10 个百分点。
我的约定是这样的:表盘框(dial)贴着表盘外圈玻璃或金属边框的内沿,宁紧勿松,把背景反光、螺丝、旁边的管线全部排除在外。理由很实际——这个框的唯一用途是裁剪,下游找圆心、做二值化都在裁剪图里进行,框大了多出来的背景会让霍夫圆检测直接选错圆心。指针框(pointer)如果要做,只框指针的可视主体,从转轴到针尖,不要为了「框得好看」而往两头各扩十几像素,那会让指针方向和框的长轴方向对不上。
刻度区要不要单独标一类,取决于你的读数方案。走「检测框 + 角度法」这条路的,刻度线不标也行,用标定角度线性映射就够;走「关键点 + 刻度读数」这条路的,刻度必须标,而且要用关键点格式而不是矩形框。两种方案混着标是最麻烦的,转换脚本里还得判断哪些 object 转框、哪些转点。
还有一个坑:多人标注时,一定要先标 20 张做交叉验证,算一下同一个表盘两个人框的 IoU。IoU 低于 0.85 就说明标准没对齐,这时候直接开工,模型学到的是标注者之间的分歧,不是表盘的边界。
提示:先拿 20 张图做双人交叉标注,IoU 达标再批量开工,比标完 2000 张再返工便宜得多。
3. 把指针仪表 xml 转成 YOLO txt:归一化脚本与四个边界坑
3.1 目录怎么摆:images、labels 与 train/test 的对应关系
转换之前先把目录定死,后面所有脚本都按这个结构走,能省掉大量路径拼接的调试时间:
| 路径 | 内容 | 说明 |
|---|---|---|
dataset/images/train/ | 训练图 | 从原 train 里再切出 val 后的剩余部分 |
dataset/images/val/ | 验证图 | 从 train 按工况分层切 15% |
dataset/images/test/ | 测试图 | 原 test,全程不参与调参 |
dataset/labels/train/ | YOLO txt | 与 images/train 同名同数量 |
dataset/labels/val/ | YOLO txt | 同上 |
dataset/annotations_xml/ | 原始 xml | 只做留档,不参与训练 |
dataset/data.yaml | 数据集描述 | 类别名与路径 |
这里有个关键决定:原始数据给的 train/test 不要盲目相信。工业现场的数据集,test 往往是从同一段采集视频里连续抽帧出来的,相邻帧只差几毫秒,表盘角度几乎没变。这种 test 的指标会虚高到 0.98,上线后遇到新表型立刻掉到 0.6。我的处理是从 test 里按「设备型号 / 光照条件 / 拍摄距离」三个维度做一次分组统计,如果某一组在 train 里完全没出现,那它才是真正有价值的测试集;如果三个维度上都和 train 高度重叠,就把它降级成 val,另外从现场补一批新工况的图做 test。
val 的切分同理,按工况分层抽样,不要用random.shuffle一刀切。表盘倾斜角度、指针位置、曝光强度这三个维度各分三档,每档按比例抽,切出来的 val 才有代表性。
3.2 转换脚本逐段讲:1-based 转 0-based、越界裁剪、类别映射
下面是核心转换函数,两个参数xml_path和img_path分别指向一条标注和它对应的原图:
import math import xml.etree.ElementTree as ET from pathlib import Path import cv2 # 顺序必须和 data.yaml 的 names 完全一致,改这里就要改 yaml CLASSES = ["dial", "pointer"] def voc_to_yolo(xml_path: Path, img_path: Path): root = ET.parse(xml_path).getroot() img = cv2.imread(str(img_path)) if img is None: raise FileNotFoundError(img_path) H, W = img.shape[:2] # 以真实图像尺寸为准,不信任 xml 的 size lines = [] for obj in root.findall("object"): name = (obj.findtext("name") or "").strip().lower() if name not in CLASSES: # 未知类别显式跳过,不要静默给 0 print("skip unknown class:", name, xml_path.name) continue cid = CLASSES.index(name) bb = obj.find("bndbox") # VOC 惯例 1-based 闭区间 -> 0-based 连续坐标 x1 = float(bb.findtext("xmin")) - 1 y1 = float(bb.findtext("ymin")) - 1 x2 = float(bb.findtext("xmax")) y2 = float(bb.findtext("ymax")) # 越界裁剪:宁可切掉几像素,也不产出 >1 的归一化坐标 x1, x2 = max(0.0, x1), min(float(W), x2) y1, y2 = max(0.0, y1), min(float(H), y2) # 退化框直接丢,留着会在训练时把 loss 拉爆 if x2 - x1 < 2 or y2 - y1 < 2: continue cx = (x1 + x2) / 2.0 / W cy = (y1 + y2) / 2.0 / H bw = (x2 - x1) / W bh = (y2 - y1) / H lines.append(f"{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return lines逻辑上分四步:读图拿真实尺寸、按 name 映射类别、坐标基准转换、归一化输出。参数上只有CLASSES需要你改,顺序错了模型学出来的类就是乱的——这类错误在训练曲线上完全看不出来,loss 照降,只是预测框的类别标签反了,很难发现。所以CLASSES一定要和data.yaml里的names逐字对照一遍。
cv2.imread那一步是刻意的开销。有人为了快,直接用 xml 里的 size 做归一化,遇到图片被压缩过的批次就整体偏移。读一次图多花几十毫秒,能省掉后面半天的排查。
批量驱动部分:
for split in ["train", "val", "test"]: xml_dir = Path(f"annotations/{split}") for xml_path in sorted(xml_dir.glob("*.xml")): stem = xml_path.stem img_path = Path(f"images/{split}/{stem}.jpg") if not img_path.exists(): print("no image:", stem); continue labels = voc_to_yolo(xml_path, img_path) out = Path(f"labels/{split}/{stem}.txt") out.parent.mkdir(parents=True, exist_ok=True) # 没有目标也要写空文件,表示这是一张负样本 out.write_text("\n".join(labels), encoding="utf-8")空文件那行值得单说。YOLO 系训练里,一张图对应一个 txt,txt 为空代表「这张图确认没有目标」,是合法的负样本;但如果 txt 文件不存在,有些实现会当成「标签缺失」直接跳过这张图。含背景无表盘的图对抑制误检很有用,别顺手删。
3.3 转换后的三道自检:数量对账、坐标越界、空标签
转换跑完不算完,三道自检必须过。
第一道是数量对账,命令行就够:
# 三个数字必须完全一致,不一致就是有图片或 xml 被跳过了 ls images/train | wc -l ls labels/train | wc -l ls annotations/train/*.xml | wc -l第二道是坐标合法性,把越界和字段数错误一次扫出来:
from pathlib import Path bad = [] for p in Path("labels").rglob("*.txt"): for i, line in enumerate(p.read_text(encoding="utf-8").splitlines()): if not line.strip(): continue parts = line.split() if len(parts) != 5: # 检测格式必须是 5 列 bad.append((p.name, i, "字段数", len(parts))) continue cid = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): bad.append((p.name, i, "归一化越界")) elif cx - w / 2 < -1e-6 or cx + w / 2 > 1 + 1e-6: bad.append((p.name, i, "横向出框")) # 中心对但框超出图像边界 elif cy - h / 2 < -1e-6 or cy + h / 2 > 1 + 1e-6: bad.append((p.name, i, "纵向出框")) print("问题行:", len(bad)) for row in bad[:10]: print(row)这里特意多查了「中心合法但框出边界」的情况。裁剪逻辑写得糙的时候,cx落在 0 到 1 之间是必然的,但w可能大到让cx - w/2变成负数,训练时框会贴到图像边缘之外,回归目标本身就错了。
第三道是空标签统计,按 split 分别算空 txt 占比。全空说明路径配错了,占 30% 以上说明这批数据里有大量无表盘图,得回去看采集是不是空拍。
第三道之后还有一件事容易被跳过:抽 5 张图把转换后的框画回去看一眼。写个十行的可视化脚本,红色画 dial、绿色画 pointer,肉眼扫一遍比任何数字都直观。
4. 指针仪表检测训练:输入尺寸、增强开关与 mAP 之外的三个指标
4.1 从 train 里再切一份 val:切分比例与分层依据
前面提过 val 要分层切,具体落地是这样。先把 train 里每张图的元信息整理成一张表:表盘在图中占的短边像素数、指针角度(如果已有标注就粗算,没有就用人工抽样的 200 张估个分布)、图像平均亮度。然后按这三个维度各分三档,九个格子每格抽 15%,凑成 val。
比例上 15% 是常见起点。数据量少于 500 张时,val 占比可以压到 10%,把更多样本留给训练;数据量超过 5000 张,20% 也可以接受,因为此时 val 的统计波动已经够小。关键是 val 一旦切定就不再变动,所有超参对比都对着同一份 val,否则模型选型的依据就变成了噪声。
test 集的唯一用途是最后出一份数字。整个调参过程中不打开、不看、不参与早停,这是我自己定的规矩,破了这条规矩,最后报出去的数字就没有意义。
4.2 必须手动改的四组训练参数
默认配置在通用数据集上调得不错,但指针仪表有几个特性会让默认值翻车。下面这四组是每次都要动的:
| 参数 | 建议起点 | 为什么要改 |
|---|---|---|
imgsz | 表盘短边的 1.5~2 倍,向上取到 32 的倍数 | 表盘在整图里占比常常不到 15%,直接 resize 到 640 后指针只剩几个像素 |
mosaic | 0.0 ~ 0.3 | 四图拼接会把本来就小的表盘再缩一半,指针彻底消失 |
degrees | 5 ~ 10 | 现场手持拍摄有轻微倾斜,小角度旋转有益,大角度会把表盘转出画面 |
fliplr | 0.0 | 水平翻转后表盘上的数字全部镜像,指针与刻度字的相对关系被破坏 |
imgsz这条最容易低估。如果一份数据里表盘的平均短边是 380 像素,而原图是 1920×1080,那么 resize 到 640 之后表盘只剩 127 像素,指针宽度不到 3 像素,卷积核基本抓不到。这时候要么把imgsz提到 960 或 1280,要么改成两级方案:先训一个粗定位模型找到表盘区域,再在裁剪图上训第二个模型。后者的显存开销小很多,代价是多一次推理。
mosaic和degrees是两个此消彼长的参数。表盘在图中占比大的数据集,mosaic 开到 0.5 也无所谓;占比小的,mosaic 一定要压低,甚至直接关掉。旋转同理,倾斜 3 度以内的增强对应的是手持抖动,倾斜 30 度对应的是你在教模型认识一个现场不存在的场景。
学习率和轮次反而没那么挑。lr0从 0.01 起,小数据集(1000 张以下)用 0.005 更保险;epochs给 200,靠早停收,别指望 30 轮就收敛——表盘这种低纹理目标,前 50 轮的 box loss 下降会慢得让你怀疑数据有问题。
4.3 指标怎么读:漏检率、中心误差与分工况评估
mAP50到 0.9 以上之后,这个数字就没什么信息量了,继续盯它只会让你错过真正的问题。我一般看三个东西:
表盘检测的召回率。在读数链路里,表盘漏检等于完全无法出读数,误检还可以靠后续规则过滤。所以 dial 类的 recall 要单独拉出来看,目标是 0.99 以上。如果 recall 卡在 0.95,回去看漏检的那 5% 是什么工况——多半是强反光、表盘被遮挡一角,或者拍摄距离过远。这些样本补进训练集的收益,比调任何超参都大。
表盘中心点误差。把预测框和标注框的中心点算欧氏距离,除以标注框的短边,得到归一化中心误差。这个指标直接决定下游找圆心的难度,经验上要压到 0.05 以内。
分工况的分组指标。按前面切分 val 用的三个维度分组,分别算 recall 和中心误差。整体 0.97、但「远距离 + 强光」这一组只有 0.72,这种结论才有指导意义——你会立刻知道下一批要补采什么样的数据。
评估脚本不用写太复杂,在验证集上跑完推理,把每张图的文件名、预测框、真值框导成一张 CSV,再用 pandas 按分组标签做groupby就行。分组标签在建 val 的时候顺手存下来,后面一直能用。
5. 指针仪表数据集排查:五类翻车现场的现象、原因与解决
5.1 数据与标注侧的两类问题
训练 loss 正常下降,验证 mAP 始终是 0。现象是所有类别的 AP 都是 0,但框看起来很合理。原因基本只有一种:data.yaml里的names顺序和转换脚本里的CLASSES不一致,或者nc写成了类别数但 names 少写了一行。模型预测的类别 id 落在未定义的区间,评估时匹配不上任何真值。解决方法是转换完成后立刻打印一次CLASSES,和 yaml 逐行对照,把这一步固化成流程而不是靠记性。
提示 No labels found,训练直接不启动。现象是日志里报找不到标签,但ls labels/train | wc -l明明有几千个文件。原因通常是路径层级对不上:训练框架按「把images替换成labels」的规则找 txt,如果你的目录是images/train/xxx.jpg而标签在labels/xxx.txt(少了一层 train),替换后就找不着。还有一种情况是 txt 文件名带上了原扩展名,变成xxx.jpg.txt。解决办法是把目录结构调整成镜像关系,images/train对应labels/train,文件名保持一致且不含扩展名。
5.2 训练与评估侧的两类问题
测试集 mAP 0.97,换一批现场图掉到 0.6。现象是离线指标漂亮得不像话,上线就崩。原因是训练集和测试集来自同一段连续采集,帧间只差几毫秒,切分本质上是对同一批样本的重复采样,属于数据泄漏。解决方法是按采集批次而不是按帧切分,同一个视频片段、同一台设备、同一个时间段拍的所有图只能落在同一个 split 里。已经切错的,宁可重新划一遍,也不要拿这份虚高的指标去汇报。
指针框总是框在表盘正中间的一小块。现象是预测出来的 pointer 框又短又方,不像指针。原因是标注时多个人对「指针从哪开始」理解不同,有人从转轴标起,有人只标了针尖那一段,训练时模型学到的是两者的平均——一个居于中间的短框。解决方法是先做双人交叉标注验证,把指针框的起点固定为转轴可见边缘、终点固定为针尖,写进标注规范里,然后对已有标注做一次抽检,短边小于表盘短边 30% 的 pointer 框全部重标。
5.3 上线侧的一类问题
xml 的 size 和真实图片对不上,框整体偏移。现象是转换完画回去看,所有框都往左上角偏了一截,偏移量和图片缩放比例成正比。原因是图片在某次整理中被批量缩放过,xml 没同步更新。解决办法是转换脚本坚持用cv2.imread读到的真实尺寸做归一化,同时把 xml 里的 size 和真实尺寸比对,不一致的样本记进日志单独处理——这类样本的坐标本身可能也需要按比例换算,不能只改分母。
注意:这五类问题里有四类在训练开始前就能发现,前提是转换完一定把框画回去看五张图。
6. 从检测框到读数:把角度误差压到 1 度以内的验证习惯
检测只是中间产物,真正交付的是读数。我用得最多的是角度法:拿 dial 框裁出表盘区域,在裁剪图里找指针方向,再通过标定角度线性映射到量程。核心函数不长,但参数很挑:
import cv2, math, numpy as np def pointer_angle(crop_bgr): h, w = crop_bgr.shape[:2] cx, cy = w / 2.0, h / 2.0 gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) # 深色指针在浅色表盘上,用反相 OTSU 拿到指针与刻度的前景 _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) lines = cv2.HoughLinesP( mask, 1, np.pi / 360, threshold=30, # 累加器阈值,噪声多就往上调 minLineLength=int(min(w, h) * 0.25), # 至少四分之一表盘直径 maxLineGap=6, ) if lines is None: return None best, best_len = None, 0.0 for x1, y1, x2, y2 in lines[:, 0]: # 只保留过圆心附近的线段,滤掉刻度线和外框 num = abs((y2 - y1) * cx - (x2 - x1) * cy + x2 * y1 - y2 * x1) den = math.hypot(y2 - y1, x2 - x1) + 1e-6 if num / den > 0.08 * min(w, h): continue L = den if L > best_len: best, best_len = (x1, y1, x2, y2), L if best is None: return None x1, y1, x2, y2 = best # 屏幕坐标 y 向下,取负号换到数学坐标系 return math.degrees(math.atan2(-(y2 - y1), x2 - x1)) % 360.00.08 * min(w, h)这个容差是这条链路里最需要调的参数。表盘圆心和框中心一般差不了几个像素,容差给太小,指针线因为轻微弯曲被全滤掉;给太大,外圈的刻度线会混进来,选到最长的那根刻度,角度直接跑偏几十度。我是按表盘半径的 8% 起试,在验证集上扫一遍 0.05 到 0.15,挑角度误差中位数最低的那个。
角度拿到之后不能直接当读数,必须标定。找两张图:一张指针明确指在零位,一张明确指在满量程,分别跑出theta_zero和theta_full,之后按(theta - theta_zero) % 360 / ((theta_full - theta_zero) % 360) * range线性映射。跨 0 度的那一段一定要取模,不然会算出负值。表盘如果是非线性的(有些压力表刻度前密后疏),就在零位、25%、50%、75%、满量程各标一个点,做分段线性插值。
验证习惯上,我每次交付前一定做同一件事:抽 20 张图,人工读数写在纸上,再和脚本结果逐个比对,算绝对误差的中位数和 95 分位。中位数能压到满量程的 0.5% 以内、95 分位在 1.5% 以内,这套链路才算能用。有一次我省了这一步,客户拿回来的第一批数据里有一半读数偏了整整一格,查下来是指针尖端有个高光点,霍夫线被它拽偏了几度——这种翻车,只有拿人工读数对着看才能发现,任何离线的检测指标都照不出来。希望帮到你。
本文还有配套的精品资源,点击获取