☰
YOLOv8玻璃缺陷检测实战:从数据标注到端侧部署全流程
2026/9/29 20:55:46 网站建设 项目流程

简介:面向玻璃表面缺陷检测的YOLOv8实战源码包,适合有深度学习基础的目标检测开发者和工业质检工程师,能够直接用于点状、划痕、擦伤三类缺陷的检测任务,也适合作为毕业设计或课程项目的起步参考。压缩包体积仅10KB,共包含7个文件,其中3个Python脚本分别负责模型训练、PyQt5可视化GUI和样本数据集生成,另有data.yaml类别配置、requirements.txt依赖清单与代码仓库说明文件,方便快速搭建环境并理解工程结构。目前已有97人学习下载,资源虽然精简,但覆盖了从数据组织、模型训练到评估展示的完整链路,尤其适合作为入门级目标检测落地项目的参考模板。通过阅读train_yolov8.py可复用默认参数训练自定义数据,glass_defect_gui.py则能直观呈现检测效果,整体代码量小、结构清晰、易于修改,能有效节省从头搭建项目的时间。

1. 玻璃缺陷检测为什么绕不开YOLOv8

“玻璃缺陷检测”听起来是一个标准目标检测任务,但真跑到产线和质检机上你会发现,难点不在模型选型,而在缺陷本身不配合:透明玻璃上的划痕、气泡对比度极低,同一个缺陷换个打光角度就几乎消失。用YOLOv8做玻璃缺陷检测,等于把传统图像处理里“反复调阈值”的老路换成数据驱动方式——标注一批缺陷样本,训练一个模型,新玻璃图进来直接输出类别和位置。这套方案最大的价值在于绕开了最难缠的“换一种玻璃就得重调阈值”的死结。这篇文章面向想快速复现并投入的从业者,从环境、数据集、训练参数、部署到端侧验证,给一条可以直接照抄的链路。新手能一步步跟下来,熟手可以直接看边界和踩坑点。

2. YOLOv8环境配置:把Ubuntu 20.04 CPU版和8G显存GPU都跑通

2.1 先决定用CPU还是GPU再动手

玻璃缺陷检测的训练数据通常就几千张,类别不超过5类,网络用一个yolov8s就够。这种规模下,CPU环境不是不能训练,而是每次改个参数要等一晚上,迭代效率太低;反过来,GPU环境也不是必须的——如果你只想跑通源码、验证输出效果,CPU推理完全不亏。我的判断标准是:数据集少于500张、只做技术验证,用CPU;真要产出上线模型,至少准备一块8G显存以上的显卡,GTX 1660 Ti这类卡就能打底。

这里有个容易踩的误区:一上来就装最新版PyTorch,结果和CUDA版本对不上,训练时提示缺so文件。与其把时间花在排查驱动上,不如一开始就用一套验证过的组合。下面两套依赖分别对应CPU和GPU场景,按需选择。

2.2 CPU版最小依赖:conda + Python 3.10 + ultralytics

先装好Miniconda,然后在终端里逐行执行:

# 创建独立虚拟环境,避免污染系统 Python conda create -n glass python=3.10 -y conda activate glass # CPU 版 PyTorch,只做推理和验证足够 pip install torch==2.0.0 torchvision==0.15.0 # ultralytics 主库,自带 CLI 和 API pip install ultralytics

这里的顺序不是随便写的。先装PyTorch再装ultralytics,pip在安装ultralytics时检测到torch已经存在,就不会再拉一个别的版本;反过来装,pip会把torch当作新依赖再装一遍,导致版本冲突和大量重复下载。Python 3.10是目前ultralytics兼容性最好的版本,3.11和3.12在部分Linux机器上会碰到onnxruntime或opencv的预编译包缺失问题。

装完先别急着跑训练,用一段最短代码验证依赖是否装配完整:

python -c "from ultralytics import YOLO; model = YOLO('yolov8n.pt'); print(model.names)"

第一次运行会下载yolov8n.pt预训练权重,看到模型类别字典输出说明权重和依赖都正常。如果是在内网环境,提前把权重文件放到工作目录即可,ultralytics会优先找本地文件。这一步能拦住后面至少一半的报错,值得花两分钟确认。

2.3 GPU版依赖和显存边界

有NVIDIA显卡再走这套:

conda create -n glass-gpu python=3.10 -y conda activate glass-gpu conda install pytorch torchvision pytorch-cuda=11.8 -c pytorch -c nvidia -y pip install ultralytics

用conda而不是pip装CUDA版PyTorch,是因为conda会把CUDA runtime和cuDNN一起打理好,省掉手动下载cuDNN的环节。驱动层面只要确保nvidia-smi能显示显卡型号就行,驱动版本偏老或偏新会在运行时打印不同警告,但不影响训练主体流程。

注意显存边界。如果你手里的卡是GTX 1660 Ti这种8G卡,训练yolov8s时imgsz=640、batch=8会顶到显存红线。常见做法是把imgsz降到512、batch降到4,精度损失不大,显存占用能明显回落。这个取舍后面训练章节再展开,这里先把环境跑通再说。

2.4 拿到“可运行源码”后先做三件事

一个可运行的YOLOv8玻璃缺陷检测源码包,目录里往往有这三样东西:requirements.txt、data.yaml、best.pt或者预训练权重。拿到手不要先翻train.py,先看requirements.txt里的依赖和你刚装的环境差多少;再看data.yaml里的类别顺序和路径指向哪里;最后确认best.pt存在且能加载。

我见过最典型的翻车是把源码包里的data.yaml路径指向作者本机的绝对路径,自己机器上根本没有这个目录,训练一开始就报“AssertionError: train dataset not found”。所以拿到包后第一步就是把data.yaml里的path改成你自己的绝对路径,train和val指向实际目录。环境这块最折磨人的不是装不上,而是装了一堆后不知道是哪一步错的,所以每走完一步就验证一次,再往后推进。

3. 玻璃缺陷数据集处理:labelme标注到YOLO格式的转换脚本

3.1 缺陷分类别:不是越细越好

玻璃缺陷检测的类别定义要按产线质检标准来,不能照搬通用目标检测的类别体系。玻璃上最常见的是这几类:划痕,低角度光照射下呈细线状;气泡,玻璃内部圆形或椭圆暗斑;污渍,表面不规则的明暗斑块;裂纹,可能延伸到边缘;边损,出现在玻璃边部。起步时建议控制在5类以内,标注和训练压力都小,后期再加类。

类别标签名标注要点
划痕scratch一条连续划痕只允许一个矩形框
气泡bubble框住整个气泡暗斑,包含边缘过渡区
污渍stain框住可见明暗斑块,不框反射亮斑
裂纹crack从裂纹起点到终点整体框住
边损edge_defect包含玻璃边部缺损区域

类别ID从0开始递增,写data.yaml时顺序必须和标签txt里的第一个数字完全对应。这个对应错了不会报错,但检测结果会全部错位。玻璃缺陷检测的标注一致性比框的大小精度更关键,因为半透明缺陷的边界本身是模糊的,多人标注时容易产生分歧。

3.2 标注规范与labelme的使用方式

用labelme标注时画矩形框,不要画多边形,保证每个JSON里一个缺陷就是一个矩形。透明玻璃上的半透明缺陷边界不明确,标注原则是:如果划痕的可见光晕影响区域明显,就框完整的可见影响区;如果只能看到断断续续的亮点,就框住整条划痕的起点到终点,别只框最亮的那一小段。

另一个容易忽视的点是图像分辨率。推荐统一采集视野后resize到1920x1080或1280x720再标注,否则不同批次玻璃远近不同,缺陷的绝对尺寸差异会干扰模型收敛。还有一点:无缺陷玻璃图不需要生成标签txt,YOLO默认这张图没有目标;不要给无缺陷图硬写一个空txt,某些自定义数据加载器会报“found 0 boxes”的警告。

3.3 转换脚本:labelme JSON转YOLO txt

下面这个脚本把labelme导出的JSON转成YOLO格式的txt,读取原图宽高做归一化:

import json import os from PIL import Image def convert_labelme_json(json_path, image_dir, out_dir): with open(json_path, encoding="utf-8") as f: data = json.load(f) base = os.path.basename(json_path).rsplit(".", 1)[0] # 用原图实际宽高做归一化,避免写死分辨率 img = Image.open(os.path.join(image_dir, base + ".png")) img_w, img_h = img.size # 类别ID映射,必须和后续 data.yaml 保持一致 class_id_map = { "scratch": 0, "bubble": 1, "stain": 2, "crack": 3, "edge_defect": 4, } lines = [] for shape in data["shapes"]: if shape["shape_type"] != "rectangle": continue label = shape["label"] x1, y1 = shape["points"][0] x2, y2 = shape["points"][1] x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) w = (x2 - x1) / img_w h = (y2 - y1) / img_h if w <= 0 or h <= 0: continue cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h lines.append(f"{class_id_map[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") txt_name = base + ".txt" with open(os.path.join(out_dir, txt_name), "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": convert_labelme_json("labels_json/001.json", "images/", "labels/")

脚本核心是两件事:把矩形框坐标转成归一化的中心点加宽高,反写类别ID。img_w和img_h不是写死的,而是从原图读取,换一批不同尺寸的玻璃图也不用改代码。批量转换用一段bash命令:

mkdir -p labels for j in labels_json/*.json; do python convert_labelme.py "$j" done

转换完成后抽几个txt打开看,检查是否有类别ID超出映射范围、坐标是否出现负值或大于1的值。labelme里手滑把点拖出画布是常见操作,这类脏标签要在训练前过滤掉,否则训练时模型会被带偏。

3.4 数据集划分与data.yaml配置

缺陷数据集划分不建议直接随机打散每张图,常见做法是按玻璃片批次分组后再划分,避免同一个缺陷的不同帧同时出现在训练集和验证集,造成验证集泄漏。划分脚本可以考虑按文件名前缀分组:

python - <<'EOF' import os import random names = [n for n in os.listdir("images") if n.endswith(".png")] # 按文件名前缀分组,同一块玻璃的多帧进同一个集合 groups = {} for n in names: prefix = n.split("_")[0] groups.setdefault(prefix, []).append(n) group_names = list(groups.keys()) random.shuffle(group_names) n = len(group_names) os.makedirs("images/train", exist_ok=True) os.makedirs("images/val", exist_ok=True) os.makedirs("images/test", exist_ok=True) os.makedirs("labels/train", exist_ok=True) os.makedirs("labels/val", exist_ok=True) os.makedirs("labels/test", exist_ok=True) for i, g in enumerate(group_names): split = "train" if i < 0.7 * n else "val" if i < 0.9 * n else "test" for name in groups[g]: os.rename(f"images/{name}", f"images/{split}/{name}") txt = name.rsplit(".", 1)[0] + ".txt" if os.path.exists(f"labels/{txt}"): os.rename(f"labels/{txt}", f"labels/{split}/{txt}") EOF

data.yaml内容如下:

path: /data/glass_defect train: images/train val: images/val test: images/test names: 0: scratch 1: bubble 2: stain 3: crack 4: edge_defect

path必须改成你自己的绝对路径。names的顺序就是txt里类别ID的映射顺序,前面转换脚本里class_id_map的顺序要和这里完全一致。如果后面增删类别,需要同步改两处,不然模型训练时不会报错,推理结果全部错位。

4. YOLOv8训练自己的玻璃缺陷数据集:训练命令、参数含义与损失曲线

4.1 一条能直接用的训练命令

环境和数据都准备好了,训练命令如下:

yolo detect train \ data=glass.yaml \ model=yolov8s.pt \ epochs=200 \ batch=8 \ imgsz=640 \ patience=20 \ device=0

model=yolov8s.pt表示从官方预训练权重继续训练,比从零训练收敛快很多,也避免一开始loss就卡住不动的情况。epochs=200不是必须跑满,配合patience=20,验证集mAP连续20轮不涨就自动停。batch=8在8G显卡上跑yolov8s的imgsz=640有点紧,如果报CUDA out of memory,先把batch降到4或imgsz降到512,后面讲具体边界。

4.2 必调参数含义

参数默认值玻璃缺陷建议说明
epochs100200~300缺陷样本少,需要更多轮次收敛
batch1688G显存下保持8左右
imgsz640512~640玻璃缺陷尺寸差异大,512够用
patience10020~50早停节省时间
lr00.010.005~0.01学习率过大会导致loss震荡
workers84~8按CPU核数设置,过高易卡IO

lr0是很多人忽略的参数。玻璃缺陷数据量小,初始学习率太大时,前20轮loss会快速下降然后再反弹,看起来像训练崩了。如果你发现曲线是“跌下去又弹上来”的形状,优先把lr0降到0.005,而不是去调网络结构。precision(B)和recall(B)都能在训练日志里看到,玻璃缺陷这类小目标场景,recall比precision更需要盯紧,漏检比误检更致命。

4.3 GTX 1660 Ti这类8G卡怎么拉满

GTX 1660 Ti是8G显存。yolov8s在imgsz=640下训练,显存占用大概6G到7.5G,batch=8可能刚好卡着上限,系统里多开一个浏览器都可能触发OOM。我的建议配置是:yolov8s + imgsz=512 + batch=8,显存占用5G左右,留出余量;如果目标是小气泡这类极小的缺陷,用yolov8n + imgsz=640 + batch=16,整体显存占用反而更低。

不要通过调大batch来追求更平滑的梯度。玻璃缺陷数据集往往只有几千张,batch从8提到16并不能带来明显精度提升,却可能直接让训练进程被杀掉。OOM报错出现时看的是显存不是内存,free -g帮不上忙,用nvidia-smi看当下占用更直接。

4.4 画损失函数曲线,三步判断模型有没有学偏

ultralytics训练过程中会在runs/detect/train目录下生成results.csv,每一行对应一个epoch。画曲线用下面的脚本:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") # 列名首尾可能有空格,先清理 df.columns = [c.strip() for c in df.columns] plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(df["epoch"], df["train/box_loss"], label="box_loss") plt.plot(df["epoch"], df["train/cls_loss"], label="cls_loss") plt.plot(df["epoch"], df["train/dfl_loss"], label="dfl_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.grid(True) plt.subplot(1, 2, 2) plt.plot(df["epoch"], df["metrics/precision(B)"], label="precision") plt.plot(df["epoch"], df["metrics/recall(B)"], label="recall") plt.plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") plt.xlabel("epoch") plt.ylabel("metric") plt.legend() plt.grid(True) plt.tight_layout() plt.savefig("train_curves.png", dpi=150)

先确认results.csv的实际列名,不同版本可能有差异;如果直接按记忆里的列名画图,KeyError会让你回头再打印一次。曲线怎么判断:box_loss、cls_loss、dfl_loss整体向下走,轻微波动正常;如果某条loss掉到接近0又突然拉起,通常是学习率太大或者验证集里混入了脏标签。mAP50曲线如果上升期突然断崖,先检查验证集里有没有标注错误,而不是急着加训练epoch。

4.5 中断训练不用从头再来

训练途中断电、显存被占、手动停止,都会导致之前的时间白费?不会。ultralytics会定时保存权重,runs/detect/train/weights目录下会同时出现last.pt和best.pt。last.pt是最近一轮的权重,best.pt是验证集表现最好的权重。中断后续训用这一条命令:

yolo detect train resume=True model=runs/detect/train/weights/last.pt

resume=True会读取之前的状态,包括当前epoch、优化器状态、学习率调度,不用手动补齐参数。部署时一定要用best.pt而不是last.pt,这两个文件从文件名上容易混淆,我见过不少部署现场直接拷了last.pt,结果精度和训练时的mAP对不上。

5. 玻璃缺陷检测的5个翻车点与排查:从漏检到mAP虚高

5.1 mAP很漂亮,线上误报一堆空框

现象:训练时mAP50有0.95,一到产线测试,正常玻璃上不断输出缺陷框,误报率高到没法用。

原因:训练集里全是缺陷图,没有无缺陷玻璃样本。模型从来没学过“没有缺陷”这种状态,推理时任何没见过的纹理都会触发高置信度输出。

解决:在数据集中加入20%到30%的正常玻璃图,验证集也要保留一部分正常样本。用下面的命令在验证集上跑一遍推理:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=val_images/ \ conf=0.25 \ save_txt=True

然后统计输出的txt文件里有多少框落在正常玻璃图上。如果这个数字偏高,优先补充负样本,而不是调低置信度阈值。

5.2 长划痕被断成两截

现象:一条贯穿玻璃的划痕,模型输出两个不连续的框,中间一段漏检。

原因:标注阶段把一条划痕按可见性分成了好几段,每段单独画框,模型学到的是“划痕=短线段”,整体长划痕反而不认识;也有可能推理时conf阈值偏高,中间段置信度略低被过滤掉。

解决:标注规范里明确一条连续划痕只允许一个矩形框,哪怕中间有断裂也要从起点框到终点。推理时先用conf=0.25跑一遍,再拿conf=0.15重跑一次,对比两批结果的差异就能判断是标注问题还是阈值问题。

5.3 反光区域被识别成缺陷

现象:模型在玻璃边缘高光区输出大量缺陷框,真实缺陷区域反而漏检。

原因:玻璃表面反光形成的高亮纹理和划痕在灰度上相近,模型学到的是“亮区即缺陷”,而不是“有纹理的亮区才是缺陷”。

解决:图像采集端用偏振片或低角度漫射光压掉反光,这是根治办法。训练侧能做的有限,把ultralytics数据增强里的色亮扰动调低,hsv_v设置在0.0到0.1之间,避免把反光变化当成特征来学。这条建议在项目启动阶段就要考虑,等模型训完再补就晚了。

5.4 训练loss降得快,验证loss反复震荡

现象:训练loss前30轮降到0.1以下,验证loss却上下跳动,mAP曲线跟着起伏。

原因:数据划分时把同一个缺陷的不同帧随机拆进了train和val,验证集里混入了和训练集高度相似的图,等于提前偷看了答案,mAP看着很高但实际泛化差。

解决:按玻璃片批次或缺陷ID分组划分数据,而不是按单张图随机打散。第3章里的划分脚本已经按文件名前缀分组,如果你的缺陷图命名没有规律,先补上编号再做划分。

5.5 CPU推理慢到没法上线

现象:模型在GPU上训练没问题,部署到车间工控机上,一张1920x1080的玻璃图推理要两三秒,节拍完全跟不上。

原因:直接用训练时的best.pt做推理,imgsz=640,在纯CPU上跑逐张计算,中间还有大量冗余计算。

解决:导出ONNX时把imgsz降到384或320,推理耗时明显下降。玻璃缺陷检测追求的是“缺陷不漏”,384分辨率对大多数划痕和气泡足够;如果目标板卡带NPU,走int8量化后延迟通常能压到几十毫秒级别。

6. 部署进阶:从best.pt导出ONNX到RK3588端侧验证

6.1 导出ONNX:让推理摆脱PyTorch环境

训练完的best.pt还依赖YOLO的Python环境,不能直接拿到板子上跑。先用ultralytics自带的导出命令转成ONNX:

yolo export model=runs/detect/train/weights/best.pt \ format=onnx \ opset=12 \ imgsz=640

导出成功后会在同目录生成best.onnx。推荐用opset=12,兼容性最好,新版本默认的opset偏高,部分板卡SDK解析会报错。imgsz按照部署需求设,如果端侧推理性能紧张就导出两个版本,640留给PC验证,384留给板子。

导出的ONNX可以直接用onnxruntime在PC上跑,不依赖任何YOLO训练库,这也是验证模型是否可部署的第一步。

6.2 RK3588与其他板端工具链的差别

RK3588这类带NPU的板子,官方转换工具是RKNN-Toolkit2,把ONNX转成rknn格式后再用RKNN运行时加载。转换时最常踩的两个坑:一是ONNX里包含的姿态估计或自定义算子不支持,需要先把模型简化为检测头再加后处理;二是转int8量化时必须提供校准集,校准集建议选100张左右覆盖各类缺陷的验证图,而不是用训练集。

如果你手里的平台不是Rockchip,工具链会完全不同。有的板端SDK只接受特定格式的模型文件,有的还需要先在PC上做算子融合。做方案选型时先确认板子SDK有没有现成的YOLOv8示例,不要等到模型训练完再去看工具链,那一步的返工成本最高。

6.3 用onnxruntime写一个最小的玻璃缺陷推理脚本

下面脚本不依赖ultralytics,只依赖onnxruntime和opencv,适合在板子或工控机上直接跑:

import cv2 import numpy as np import onnxruntime as ort model_path = "best.onnx" num_classes = 5 conf_thres = 0.25 iou_thres = 0.45 sess = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name input_size = 640 def letterbox(img, size=640): h, w = img.shape[:2] scale = min(size / h, size / w) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((size, size, 3), 114, dtype=np.uint8) x0 = (size - new_w) // 2 y0 = (size - new_h) // 2 canvas[y0:y0 + new_h, x0:x0 + new_w] = resized return canvas, scale, x0, y0 img = cv2.imread("test.png") boxed, scale, x0, y0 = letterbox(img, input_size) blob = cv2.dnn.blobFromImage(boxed, 1 / 255.0, (input_size, input_size), swapRB=True) out = sess.run(None, {input_name: blob})[0] # [1, 4+num_classes, N] out = out[0].T # 转成 [N, 4+num_classes] boxes = out[:, :4] # cx, cy, w, h scores = out[:, 4:] class_ids = scores.argmax(1) confs = scores.max(1) # 置信度过滤 mask = confs > conf_thres boxes, class_ids, confs = boxes[mask], class_ids[mask], confs[mask] # cxcywh 转 xyxy,注意坐标是在 letterbox 后的 640 空间 xyxy = np.zeros_like(boxes) xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 # NMS 需要 xywh 输入 boxes_xywh = np.concatenate([xyxy[:, :2], boxes[:, 2:]], axis=1) keep = cv2.dnn.NMSBoxes( boxes_xywh.tolist(), confs.tolist(), conf_thres, iou_thres, ) if len(keep) > 0: keep = np.array(keep).reshape(-1) for idx in keep: x1, y1, x2, y2 = xyxy[idx] # 映射回原图坐标 x1 = (x1 - x0) / scale y1 = (y1 - y0) / scale x2 = (x2 - x0) / scale y2 = (y2 - y0) / scale print(class_ids[idx], confs[idx], x1, y1, x2, y2)

脚本里有两个关键点:一是YOLOv8的ONNX输出格式是[1, 4+num_classes, N],坐标是中心点加宽高,必须先转成xyxy再做NMS;二是letterbox后要把坐标映射回原图,否则画框位置整体偏移。如果推理结果都是空框,优先检查conf_thres是不是太高,玻璃缺陷的置信度整体会比通用目标低一截。

6.4 int8量化:用校准集而不是训练集

转端侧模型时,int8量化是最常掉的坑。量化过程需要统计每层激活值的分布,这个统计用的是校准集。校准集选100张左右、包含全部缺陷类别且有代表性的验证图,不要直接用训练集。训练集里图太多,校准时间长且分布容易偏向重复样本;校准集太少,量化后精度可能掉3到5个点,对小目标缺陷几乎是致命的。

量化后验证不用看整体mAP,直接对着同一张图对比fp32模型和int8模型的输出框,看漏检和错位情况。如果某个类别的小气泡全部丢失,先检查校准集里是否包含足够多的小目标样本,再用更大的校准集重新量化。这个环节偷懒,部署上去的模型大概率在产线上原形毕露。

6.5 一个常年有效的验证习惯

最后一件事不是参数,而是工作习惯。只看mAP数字很容易被蒙混过去,我现在每轮调参后都会把验证集里的漏检和误报按缺陷类型归档,然后叠加预测框重新看一遍原图。这个方法帮我拦下了好几次看似mAP没问题、一上线就翻车的改动。玻璃缺陷检测的落地难点从来不在模型结构,而在数据、标注和验证闭环上是否较真。希望这个习惯对你也有用,少走几趟我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询