回答

收藏

[项目提交] 《2025 DigiKey AI应用创意挑战赛》基于Yolo的PCB缺陷检测

2025 DigiKey AI应用创意挑战赛 2025 DigiKey AI应用创意挑战赛 418 人阅读 | 0 人回复 | 2026-01-25

本帖最后由 eefocus_3956818 于 2026-1-25 02:38 编辑

项目名称:基于Yolo的PCB缺陷检测



项目概述

本项目为基于YOLO系列模型的缺陷检测系统,支持对PCB等工业产品的缺陷自动识别。系统包含数据集处理、模型训练、推理检测、结果可视化等完整流程,并集成了Web前端界面,方便用户上传图片并查看检测结果。项目适配树莓派5环境,便于在边缘设备上部署和运行。

主要功能模块
  • 数据集处理与转换(convert.ipynb, dataset.ipynb)
  • 模型训练与测试(training.ipynb, testing.ipynb,支持yolov8n/8s/11n等权重)
  • 缺陷检测API与Web服务(app.py, app_raw.py,Flask框架)
  • 前端页面与交互(templates/index.html, static/app.js, static/style.css)
  • Docker容器化部署支持(Dockerfile, docker-build.sh, docker-run.sh)


部署环境要求(树莓派5):
  • 操作系统:Raspberry Pi OS 64位
  • Python 3.8及以上
  • 依赖库:requirements.txt中已列出(如Flask、torch、ultralytics等)
  • Docker(可选,推荐用于快速部署)
  • 推荐使用硬件加速(如树莓派5的NPU/GPU,当前未使用)以提升推理速度

作品实物图

本次的软件开发,AI的应用多集中于软件层面,对硬件并无实际的应用控制,仅仅是使用USB摄像头作为输入来到树莓派进行推理。因此故第一张图片展示树莓派和摄像头的外观,其他界面则展示对应的功能。


完整的系统正面所示


PCB缺陷检测效果1 : 文件上传推理





PCB缺陷检测效果1 : 摄像头数据流实时推理。


项目软件设计

数据集选择:项目的数据集采用的是飞浆 AI studio中北京大学开源的PCB缺陷数据集,其中可以检测:缺失孔,鼠标咬伤,开路,短路,杂散,伪铜

原生数据集采用的是JSON格式或者VOC格式,这里对VOC数据格式进行清洗和转换。

数据清洗:数据清洗的目的是主要将VOC的格式转换成Yolo支持的格式
  1. import os
  2. import xml.etree.ElementTree as ET

  3. # VOC XML 文件夹
  4. annotations_dir = "/Users/wangchong/Downloads/VOCdevkit/VOC2007/Annotations"
  5. # 输出 YOLO 格式 TXT 文件夹
  6. labels_dir = "/Users/wangchong/Downloads/VOCdevkit/VOC2007/labels"

  7. # YOLO 类别列表(顺序决定 class_id)
  8. classes = ["missing_hole", "mouse_bite", "short", "spur", "spurious_copper", "open_circuit"]

  9. os.makedirs(labels_dir, exist_ok=True)

  10. # 遍历所有 XML 文件
  11. for xml_file in os.listdir(annotations_dir):
  12.     if not xml_file.endswith(".xml"):
  13.         continue

  14.     xml_path = os.path.join(annotations_dir, xml_file)
  15.     tree = ET.parse(xml_path)
  16.     root = tree.getroot()

  17.     # 图片大小
  18.     size = root.find("size")
  19.     img_width = int(size.find("width").text)
  20.     img_height = int(size.find("height").text)

  21.     yolo_lines = []

  22.     # 遍历所有对象
  23.     for obj in root.findall("object"):
  24.         cls_name = obj.find("name").text.strip()  # 去掉空格
  25.         if cls_name not in classes:
  26.             print(f"未匹配类别: {cls_name}")  # 调试用
  27.             continue
  28.         class_id = classes.index(cls_name)

  29.         bbox = obj.find("bndbox")
  30.         xmin = int(bbox.find("xmin").text)
  31.         ymin = int(bbox.find("ymin").text)
  32.         xmax = int(bbox.find("xmax").text)
  33.         ymax = int(bbox.find("ymax").text)

  34.         # 转换为 YOLO 格式(归一化)
  35.         x_center = ((xmin + xmax) / 2) / img_width
  36.         y_center = ((ymin + ymax) / 2) / img_height
  37.         width = (xmax - xmin) / img_width
  38.         height = (ymax - ymin) / img_height

  39.         yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")

  40.     # 保存 TXT 文件
  41.     txt_filename = os.path.splitext(xml_file)[0] + ".txt"
  42.     txt_path = os.path.join(labels_dir, txt_filename)
  43.     with open(txt_path, "w") as f:
  44.         f.write("\n".join(yolo_lines))

  45. print("转换完成!TXT 文件已生成到", labels_dir)
复制代码


划分训练集和数据集
  1. import os
  2. import shutil
  3. from sklearn.model_selection import train_test_split

  4. # 原始文件夹
  5. images_dir = "/Users/wangchong/Downloads/VOCdevkit/VOC2007/JPEGImages"
  6. labels_dir = "/Users/wangchong/Downloads/VOCdevkit/VOC2007/labels"

  7. # 输出目录:dataset 根目录
  8. dataset_dir = "/Users/wangchong/Downloads/VOCdevkit/VOC2007/dataset"

  9. # 创建目录结构
  10. train_img_dir = os.path.join(dataset_dir, "images/train")
  11. val_img_dir = os.path.join(dataset_dir, "images/val")
  12. train_label_dir = os.path.join(dataset_dir, "labels/train")
  13. val_label_dir = os.path.join(dataset_dir, "labels/val")

  14. for d in [train_img_dir, val_img_dir, train_label_dir, val_label_dir]:
  15.     os.makedirs(d, exist_ok=True)

  16. # 获取所有图片文件
  17. all_images = [f for f in os.listdir(images_dir) if f.endswith((".jpg", ".png"))]

  18. # 划分训练集和验证集(80%训练, 20%验证)
  19. train_imgs, val_imgs = train_test_split(all_images, test_size=0.2, random_state=42)

  20. # 复制训练集图片和标签
  21. for img_file in train_imgs:
  22.     # 图片
  23.     shutil.copy2(os.path.join(images_dir, img_file),
  24.                  os.path.join(train_img_dir, img_file))
  25.     # 对应标签
  26.     label_file = os.path.splitext(img_file)[0] + ".txt"
  27.     src_label = os.path.join(labels_dir, label_file)
  28.     if os.path.exists(src_label):
  29.         shutil.copy2(src_label, os.path.join(train_label_dir, label_file))

  30. # 复制验证集图片和标签
  31. for img_file in val_imgs:
  32.     # 图片
  33.     shutil.copy2(os.path.join(images_dir, img_file),
  34.                  os.path.join(val_img_dir, img_file))
  35.     # 对应标签
  36.     label_file = os.path.splitext(img_file)[0] + ".txt"
  37.     src_label = os.path.join(labels_dir, label_file)
  38.     if os.path.exists(src_label):
  39.         shutil.copy2(src_label, os.path.join(val_label_dir, label_file))

  40. print(f"训练集: {len(train_imgs)} 张图片")
  41. print(f"验证集: {len(val_imgs)} 张图片")
  42. print("已完成目录整理为 YOLOv8 官方推荐结构:")
  43. print(dataset_dir)
复制代码



模型训练

  1. from ultralytics import YOLO
  2. import os


  3. # 类别列表
  4. class_names = ["missing_hole", "mouse_bite", "short", "spur", "spurious_copper", "open_circuit"]


  5. model = YOLO("yolov8s.pt")


  6. model.train(
  7.     data="/Users/wangchong/Downloads/VOCdevkit/VOC2007/dataset/pcb.yaml",
  8.     epochs=100,       # 训练轮数
  9.     imgsz=640,       # 输入图片尺寸
  10.     batch=8,         # 批量大小
  11.     device="mps",    # Mac CPU 训练
  12.     name="pcb_train" # 保存训练结果的目录名 runs/detect/pcb_train
  13. )


复制代码


一共在Mac上耗费了两个多小时,跑了100轮的训练。最终的精度到达了96% ,召回率在92%。模型表现很好。
  1. 100 epochs completed in 2.366 hours.
  2. Optimizer stripped from /Users/wangchong/DataspellProjects/defect_detection/runs/detect/pcb_train4/weights/last.pt, 22.5MB
  3. Optimizer stripped from /Users/wangchong/DataspellProjects/defect_detection/runs/detect/pcb_train4/weights/best.pt, 22.5MB

  4. Validating /Users/wangchong/DataspellProjects/defect_detection/runs/detect/pcb_train4/weights/best.pt...
  5. Ultralytics 8.3.244 🚀 Python-3.10.19 torch-2.5.1 MPS (Apple M4)
  6. Model summary (fused): 72 layers, 11,127,906 parameters, 0 gradients, 28.4 GFLOPs
  7.                  Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 9/9 5.1s/it 45.7s4.8ss
  8.                    all        139        603      0.964      0.916      0.946      0.521
  9.           missing_hole         20         92      0.987      0.989      0.986      0.612
  10.             mouse_bite         31        136      0.961      0.919      0.942      0.513
  11.                  short         26        113      0.966      0.956       0.97      0.547
  12.                   spur         19         88      0.962      0.852       0.89      0.464
  13.        spurious_copper         27        111      0.953      0.908      0.948      0.521
  14.           open_circuit         16         63      0.955      0.873      0.942      0.469
  15. Speed: 4.9ms preprocess, 154.0ms inference, 0.0ms loss, 9.3ms postprocess per image
复制代码

树莓派推理代码
  1. import os
  2. import cv2
  3. import base64
  4. import numpy as np
  5. from flask import Flask, render_template, request, jsonify, Response
  6. from ultralytics import YOLO
  7. from datetime import datetime
  8. from werkzeug.utils import secure_filename

  9. app = Flask(__name__)
  10. app.config['UPLOAD_FOLDER'] = 'static/uploads'
  11. app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size
  12. app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'bmp'}

  13. # 确保上传目录存在
  14. os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

  15. # 类别名称
  16. class_names = [
  17.     "missing_hole",
  18.     "mouse_bite",
  19.     "short",
  20.     "spur",
  21.     "spurious_copper",
  22.     "open_circuit"
  23. ]

  24. # 加载模型
  25. model = YOLO("runs/detect/pcb_train4/weights/best.pt")

  26. # 性能优化配置
  27. PERFORMANCE_CONFIG = {
  28.     'frame_skip': 2,  # 每N帧处理一次(树莓派推荐2-3)
  29.     'video_width': 640,  # 视频分辨率宽度
  30.     'video_height': 480,  # 视频分辨率高度
  31.     'inference_size': 416,  # 推理图像尺寸(越小越快,但精度可能降低)
  32.     'jpeg_quality': 70,  # JPEG压缩质量(1-100)
  33. }

  34. # 全局变量用于摄像头控制
  35. camera = None
  36. camera_active = False
  37. camera_detections = []
  38. frame_counter = 0
  39. last_annotated_frame = None


  40. def allowed_file(filename):
  41.     """检查文件扩展名是否允许"""
  42.     return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']


  43. def detect_defects(image, conf=0.3, iou=0.45, img_size=None):
  44.     """
  45.     对图像进行缺陷检测
  46.     返回:标注后的图像和检测结果列表
  47.     """
  48.     # 如果指定了推理尺寸,则调整图像大小
  49.     if img_size:
  50.         h, w = image.shape[:2]
  51.         scale = min(img_size / w, img_size / h)
  52.         if scale < 1:
  53.             new_w, new_h = int(w * scale), int(h * scale)
  54.             resized_image = cv2.resize(image, (new_w, new_h))
  55.         else:
  56.             resized_image = image
  57.     else:
  58.         resized_image = image
  59.    
  60.     results = model(
  61.         resized_image,
  62.         conf=conf,
  63.         iou=iou,
  64.         max_det=50,  # 减少最大检测数量
  65.         device="cpu",
  66.         verbose=False,
  67.         half=False  
  68.     )
  69.    
  70.     detections = []
  71.     annotated_image = image.copy()
  72.    
  73.     # 如果图像被缩放,计算缩放比例用于坐标还原
  74.     if img_size and resized_image.shape != image.shape:
  75.         scale_x = image.shape[1] / resized_image.shape[1]
  76.         scale_y = image.shape[0] / resized_image.shape[0]
  77.     else:
  78.         scale_x = scale_y = 1.0
  79.    
  80.     boxes = results[0].boxes
  81.     if boxes is not None and len(boxes) > 0:
  82.         for box in boxes:
  83.             cls_id = int(box.cls.item())
  84.             conf_score = float(box.conf.item())
  85.             x1, y1, x2, y2 = map(int, box.xyxy[0])
  86.             
  87.             # 还原到原始图像坐标
  88.             x1, y1 = int(x1 * scale_x), int(y1 * scale_y)
  89.             x2, y2 = int(x2 * scale_x), int(y2 * scale_y)
  90.             
  91.             # 记录检测结果
  92.             detections.append({
  93.                 'class': class_names[cls_id],
  94.                 'confidence': conf_score,
  95.                 'bbox': [x1, y1, x2, y2]
  96.             })
  97.             
  98.             # 绘制边界框
  99.             label = f"{class_names[cls_id]} {conf_score:.2f}"
  100.             cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 2)
  101.             
  102.             # 绘制标签背景
  103.             (label_width, label_height), _ = cv2.getTextSize(
  104.                 label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2
  105.             )
  106.             cv2.rectangle(
  107.                 annotated_image,
  108.                 (x1, y1 - label_height - 10),
  109.                 (x1 + label_width, y1),
  110.                 (0, 255, 0),
  111.                 -1
  112.             )
  113.             
  114.             # 绘制标签文本
  115.             cv2.putText(
  116.                 annotated_image,
  117.                 label,
  118.                 (x1, y1 - 6),
  119.                 cv2.FONT_HERSHEY_SIMPLEX,
  120.                 0.5,
  121.                 (0, 0, 0),
  122.                 2
  123.             )
  124.    
  125.     return annotated_image, detections


  126. @app.route('/')
  127. def index():
  128.     """主页"""
  129.     return render_template('index.html')


  130. @app.route('/upload', methods=['POST'])
  131. def upload_file():
  132.     """处理图片上传和检测"""
  133.     if 'file' not in request.files:
  134.         return jsonify({'error': '没有文件上传'}), 400
  135.    
  136.     file = request.files['file']
  137.    
  138.     if file.filename == '':
  139.         return jsonify({'error': '没有选择文件'}), 400
  140.    
  141.     if file and allowed_file(file.filename):
  142.         # 保存原始文件
  143.         filename = secure_filename(file.filename)
  144.         timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  145.         filename = f"{timestamp}_{filename}"
  146.         filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  147.         file.save(filepath)
  148.         
  149.         # 读取图像
  150.         image = cv2.imread(filepath)
  151.         if image is None:
  152.             return jsonify({'error': '无法读取图像'}), 400
  153.         
  154.         # 获取置信度阈值
  155.         conf_threshold = float(request.form.get('confidence', 0.3))
  156.         
  157.         # 进行检测
  158.         annotated_image, detections = detect_defects(image, conf=conf_threshold)
  159.         
  160.         # 保存标注后的图像
  161.         annotated_filename = f"annotated_{filename}"
  162.         annotated_filepath = os.path.join(app.config['UPLOAD_FOLDER'], annotated_filename)
  163.         cv2.imwrite(annotated_filepath, annotated_image)
  164.         
  165.         return jsonify({
  166.             'success': True,
  167.             'original_image': f'/static/uploads/{filename}',
  168.             'annotated_image': f'/static/uploads/{annotated_filename}',
  169.             'detections': detections,
  170.             'total_defects': len(detections)
  171.         })
  172.    
  173.     return jsonify({'error': '不支持的文件格式'}), 400


  174. def generate_frames():
  175.     """生成摄像头帧流"""
  176.     global camera, camera_active, camera_detections, frame_counter, last_annotated_frame
  177.    
  178.     while camera_active:
  179.         if camera is None:
  180.             break
  181.             
  182.         success, frame = camera.read()
  183.         if not success:
  184.             break
  185.         
  186.         frame_counter += 1
  187.         
  188.         # 帧跳过优化:每N帧才进行一次检测
  189.         if frame_counter % PERFORMANCE_CONFIG['frame_skip'] == 0:
  190.             # 进行检测(使用较小的推理尺寸)
  191.             annotated_frame, detections = detect_defects(
  192.                 frame,
  193.                 img_size=PERFORMANCE_CONFIG['inference_size']
  194.             )
  195.             
  196.             # 保存检测结果和标注帧
  197.             camera_detections = detections
  198.             last_annotated_frame = annotated_frame
  199.         else:
  200.             # 使用上一帧的标注结果或原始帧
  201.             annotated_frame = last_annotated_frame if last_annotated_frame is not None else frame
  202.         
  203.         # 编码为JPEG(使用压缩质量设置)
  204.         encode_params = [cv2.IMWRITE_JPEG_QUALITY, PERFORMANCE_CONFIG['jpeg_quality']]
  205.         ret, buffer = cv2.imencode('.jpg', annotated_frame, encode_params)
  206.         frame_bytes = buffer.tobytes()
  207.         
  208.         yield (b'--frame\r\n'
  209.                b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')


  210. @app.route('/video_feed')
  211. def video_feed():
  212.     """视频流路由"""
  213.     return Response(
  214.         generate_frames(),
  215.         mimetype='multipart/x-mixed-replace; boundary=frame'
  216.     )


  217. @app.route('/start_camera', methods=['POST'])
  218. def start_camera():
  219.     """启动摄像头"""
  220.     global camera, camera_active, frame_counter, last_annotated_frame
  221.    
  222.     if not camera_active:
  223.         camera = cv2.VideoCapture(0)
  224.         
  225.         # 设置摄像头分辨率(降低分辨率提升性能)
  226.         camera.set(cv2.CAP_PROP_FRAME_WIDTH, PERFORMANCE_CONFIG['video_width'])
  227.         camera.set(cv2.CAP_PROP_FRAME_HEIGHT, PERFORMANCE_CONFIG['video_height'])
  228.         camera.set(cv2.CAP_PROP_FPS, 15)  # 限制帧率到15fps
  229.         
  230.         # 重置计数器
  231.         frame_counter = 0
  232.         last_annotated_frame = None
  233.         camera_active = True
  234.         
  235.         return jsonify({'success': True, 'message': '摄像头已启动'})
  236.    
  237.     return jsonify({'success': False, 'message': '摄像头已在运行'})


  238. @app.route('/stop_camera', methods=['POST'])
  239. def stop_camera():
  240.     """停止摄像头"""
  241.     global camera, camera_active
  242.    
  243.     if camera_active:
  244.         camera_active = False
  245.         if camera is not None:
  246.             camera.release()
  247.             camera = None
  248.         return jsonify({'success': True, 'message': '摄像头已停止'})
  249.    
  250.     return jsonify({'success': False, 'message': '摄像头未运行'})


  251. @app.route('/camera_detect', methods=['POST'])
  252. def camera_detect():
  253.     """摄像头单帧检测"""
  254.     global camera, camera_detections
  255.    
  256.     if camera is None or not camera_active:
  257.         return jsonify({'error': '摄像头未启动'}), 400
  258.    
  259.     success, frame = camera.read()
  260.     if not success:
  261.         return jsonify({'error': '无法读取摄像头帧'}), 400
  262.    
  263.     # 获取置信度阈值
  264.     conf_threshold = float(request.form.get('confidence', 0.3))
  265.    
  266.     # 进行检测
  267.     annotated_frame, detections = detect_defects(frame, conf=conf_threshold)
  268.    
  269.     # 编码为base64
  270.     _, buffer = cv2.imencode('.jpg', annotated_frame)
  271.     img_base64 = base64.b64encode(buffer).decode('utf-8')
  272.    
  273.     return jsonify({
  274.         'success': True,
  275.         'image': f'data:image/jpeg;base64,{img_base64}',
  276.         'detections': detections,
  277.         'total_defects': len(detections)
  278.     })


  279. @app.route('/camera_detections', methods=['GET'])
  280. def get_camera_detections():
  281.     """获取最新的摄像头检测结果"""
  282.     global camera_detections
  283.     return jsonify({
  284.         'detections': camera_detections,
  285.         'total_defects': len(camera_detections)
  286.     })


  287. if __name__ == '__main__':
  288.     app.run(debug=True, host='0.0.0.0', port=5000)
复制代码


这里主要是对我们训练出的最好的模型进行加载,并且使用Flask应用将其运行。前端页面通过Ajax请求来访问后端的推理接口。这里启动的话比较麻烦,因此我提供了一个Docker file 用来下载依赖和打包应用。
  1. # 使用适用于ARM64架构的Python基础镜像
  2. FROM python:3.10-slim-bullseye

  3. # 设置工作目录
  4. WORKDIR /app

  5. # 安装系统依赖
  6. RUN apt-get update && apt-get install -y \
  7.     libgl1-mesa-glx \
  8.     libglib2.0-0 \
  9.     libsm6 \
  10.     libxext6 \
  11.     libxrender-dev \
  12.     libgomp1 \
  13.     libgstreamer1.0-0 \
  14.     libavcodec58 \
  15.     libavformat58 \
  16.     libswscale5 \
  17.     && rm -rf /var/lib/apt/lists/*

  18. # 复制requirements文件
  19. COPY requirements.txt ./

  20. # 安装Python依赖
  21. RUN pip install --no-cache-dir -r requirements.txt

  22. # 复制应用程序文件(只复制必要的)
  23. COPY app.py ./
  24. COPY templates/ ./templates/
  25. COPY static/style.css ./static/
  26. COPY static/app.js ./static/

  27. # 复制模型文件
  28. COPY runs/detect/pcb_train4/weights/best.pt ./runs/detect/pcb_train4/weights/

  29. # 创建必要的目录
  30. RUN mkdir -p static/uploads

  31. # 暴露端口
  32. EXPOSE 5000

  33. # 设置环境变量
  34. ENV FLASK_APP=app.py
  35. ENV PYTHONUNBUFFERED=1

  36. # 启动命令
  37. CMD ["python", "app.py"]
复制代码

并且提供了运行和部署的脚本。
  1. #!/bin/bash

  2. # 构建Docker镜像的脚本
  3. # 使用方法:chmod +x docker-build.sh && ./docker-build.sh

  4. IMAGE_NAME="pcb-defect-detection"
  5. IMAGE_TAG="latest"

  6. echo "开始构建Docker镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
  7. echo "目标平台: linux/arm64 (树莓派5)"

  8. # 构建镜像(针对ARM64架构)
  9. docker build \
  10.     --platform linux/arm64 \
  11.     -t ${IMAGE_NAME}:${IMAGE_TAG} \
  12.     .

  13. if [ $? -eq 0 ]; then
  14.     echo "✅ 镜像构建成功!"
  15.     echo ""
  16.     echo "保存镜像到文件(用于传输到树莓派):"
  17.     echo "  docker save ${IMAGE_NAME}:${IMAGE_TAG} | gzip > pcb-defect-detection.tar.gz"
  18.     echo ""
  19.     echo "在树莓派上加载镜像:"
  20.     echo "  gunzip -c pcb-defect-detection.tar.gz | docker load"
  21.     echo ""
  22.     echo "运行容器:"
  23.     echo "  docker run -d -p 5000:5000 --name pcb-detector ${IMAGE_NAME}:${IMAGE_TAG}"
  24.     echo ""
  25.     echo "如果需要使用摄像头,添加设备映射:"
  26.     echo "  docker run -d -p 5000:5000 --device=/dev/video0:/dev/video0 --name pcb-detector ${IMAGE_NAME}:${IMAGE_TAG}"
  27. else
  28.     echo "❌ 镜像构建失败"
  29.     exit 1
  30. fi
复制代码



部署脚本如下
  1. #!/bin/bash

  2. # 运行Docker容器的脚本(在树莓派上使用)
  3. # 使用方法:chmod +x docker-run.sh && ./docker-run.sh

  4. IMAGE_NAME="pcb-defect-detection"
  5. CONTAINER_NAME="pcb-detector"
  6. PORT=5000

  7. echo "启动PCB缺陷检测容器..."

  8. # 停止并删除已存在的容器
  9. docker stop ${CONTAINER_NAME} 2>/dev/null
  10. docker rm ${CONTAINER_NAME} 2>/dev/null

  11. # 运行新容器
  12. # --restart=unless-stopped: 自动重启(除非手动停止)
  13. # -p 5000:5000: 端口映射
  14. # --device=/dev/video0: 摄像头设备映射(如果使用摄像头功能)
  15. # -v ./static/uploads:/app/static/uploads: 持久化上传文件

  16. docker run -d \
  17.     --name ${CONTAINER_NAME} \
  18.     --restart=unless-stopped \
  19.     -p ${PORT}:5000 \
  20.     --device=/dev/video0:/dev/video0 \
  21.     -v "$(pwd)/static/uploads:/app/static/uploads" \
  22.     ${IMAGE_NAME}:latest

  23. if [ $? -eq 0 ]; then
  24.     echo "✅ 容器启动成功!"
  25.     echo ""
  26.     echo "访问地址: http://树莓派IP:${PORT}"
  27.     echo "本地访问: http://localhost:${PORT}"
  28.     echo ""
  29.     echo "查看日志: docker logs -f ${CONTAINER_NAME}"
  30.     echo "停止容器: docker stop ${CONTAINER_NAME}"
  31.     echo "重启容器: docker restart ${CONTAINER_NAME}"
  32. else
  33.     echo "❌ 容器启动失败"
  34.     exit 1
  35. fi
复制代码


部署方式


安装Python及依赖库:pip install -r requirements.txt
(可选)使用Docker一键部署:sh docker-build.sh && sh docker-run.sh
运行app.py启动Web服务,访问本地或树莓派IP的5000端口即可使用系统



项目文档

通过网盘分享的文件:defect_detection.zip
链接: https://pan.baidu.com/s/1RLiC4-bhgGXoWSvj0dDo0w?pwd=qavh 提取码: qavh 复制这段内容后打开百度网盘手机App,操作更方便哦


视频说明和功能演示

点击我观看

项目总结

本项目实现了一套基于 YOLO 的 PCB 缺陷检测系统,从数据处理、模型训练到实际部署进行了完整实现。项目使用北京大学开源的 PCB 缺陷数据集,对原始 VOC 数据进行清洗并转换为 YOLO 格式,训练了 YOLOv8 模型,最终在验证集上取得约 96% 的检测精度 和 92% 的召回率。

训练完成后,将模型部署到 树莓派 5,并基于 Flask 开发了 Web 检测系统,支持图片上传和摄像头实时检测。同时通过 Docker 实现一键部署,方便在边缘设备上快速运行。该项目验证了 YOLO 在 PCB 工业缺陷检测场景下的可行性和实际应用价值。








分享到:
回复

使用道具 举报

您需要登录后才可以回帖 注册/登录

本版积分规则

关闭

站长推荐上一条 /2 下一条