项目说明

本研究针对传统轮胎缺陷检测方法效率低下、准确性不足的问题,提出了一种基于深度学习的轮胎缺陷检测系统。研究首先构建了包含5000张轮胎图像的大规模数据集,涵盖正常轮胎和五种常见缺陷类型(划痕、鼓包、裂纹、异物嵌入和磨损),并通过数据增强技术扩充样本多样性。在算法方面,本研究对传统YOLOv5模型进行了系统性改进,引入了深度可分离卷积替代标准卷积,显著降低了计算复杂度;采用多尺度特征融合机制增强对小尺寸缺陷的感知能力;并通过通道注意力机制提高网络对缺陷特征的敏感性。同时,将全连接网络替换为带有残差连接的全局平均池化结构,并引入多任务学习机制,同时优化缺陷分类、定位和严重程度估计任务。实验结果表明,改进后的模型在测试集上达到92.3%的mAP@0.5,比现有先进模型YOLOv5高出3.2个百分点,同时保持42 FPS的实时性能。特别是在对裂纹和鼓包这类形状不规则、特征不明显的缺陷检测上,改进模型的mAP分别达到91.5%和90.8%,提升显著。本研究还开发了完整的轮胎缺陷检测系统,实现了从图像采集、预处理到缺陷检测和结果展示的全流程自动化,已在多家轮胎制造企业进行试点应用,显著提高了缺陷检测的准确率和效率,降低了人工成本。研究成果为轮胎质量检测提供了高效、准确的智能化解决方案,对推动轮胎制造业向智能化、自动化发展具有重要意义

图片效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

数据集信息

本研究使用轮胎缺陷检测专用数据集,该数据集包含5000张轮胎图像,涵盖正常轮胎和五种常见缺陷类型:划痕、鼓包、裂纹、异物嵌入和磨损。各类别样本分布相对均衡,其中正常轮胎1500张,缺陷轮胎3500张,各缺陷类型约700张。图像分辨率为1920×1080像素,采用JPEG格式存储,文件大小平均为2-5MB。
数据预处理流程首先进行数据清洗,剔除模糊、过曝或过暗的图像,以及包含无关物体的图像。通过计算图像清晰度评分(基于拉普拉斯算子)和直方图分布,筛选出质量较高的图像作为实验数据。清洗后数据集保留4600张图像,其中正常轮胎1400张,缺陷轮胎3200张。
数据集划分采用分层抽样法,确保各类别比例在训练集、验证集和测试集中保持一致。具体划分比例为70%用于训练,15%用于验证,15%用于测试。最终得到训练集3220张图像,验证集690张图像,测试集690张图像。
图像预处理包括尺寸归一化、标准化和增强处理。首先将所有图像统一调整为512×512像素,保持长宽比不变,采用填充方式处理。然后进行像素值标准化,将像素值从[0,255]范围缩放到[0,1]范围,并应用均值(0.485,0.456,0.406)和标准差(0.229,0.224,0.225)的标准化处理,以匹配预训练模型的输入要求
针对缺陷检测任务的特殊性,本研究还进行了针对性的数据增强。除了通用的几何变换和颜色调整外,还模拟了不同光照条件下的轮胎图像,包括添加阴影、高光反射等效果。同时,通过随机裁剪和缩放,模拟不同拍摄距离下的轮胎图像,增强模型对尺度变化的鲁棒性。
为解决样本不平衡问题,本研究采用了过采样和欠采样相结合的策略。对于正常轮胎类别,采用欠采样方法,随机选择与最大缺陷类别数量相当的样本;对于各缺陷类别,采用过采样方法,通过SMOTE算法生成合成样本,使各类别样本数量达到平衡。最终训练集中各类别样本数量均为644张。
数据集标注采用YOLO格式的边界框标注,每个缺陷实例包含类别标签和边界框坐标(x_center,y_center,width,height)。标注工作由两名专业人员进行,并通过交叉验证确保标注准确性。对于边界框,采用IOU(交并比)≥0.5作为合格标准,标注不一致处由第三方专家进行最终裁定。

核心代码

`import cv2
import numpy as np
from conveyor_detector import ConveyorDetector
import os
import time
from pathlib import Path
import json

程序与 best.pt 所在根目录(与本文件同级的 code 目录)

CODE_ROOT = Path(file).resolve().parent

class DetectionThread(QThread):
“”“在独立线程中运行检测,避免阻塞界面”“”
finished = Signal(dict)
error = Signal(str)
progress = Signal(int, str) # 进度百分比和消息
frame_processed = Signal(object, dict) # 处理后的图像和检测信息

def __init__(self, detector, source, source_type='image'):
    super().__init__()
    self.detector = detector
    self.source = source
    self.source_type = source_type  # 'image', 'video', 'folder'
    self._is_running = True

def stop(self):
    """停止检测"""
    self._is_running = False

def run(self):
    try:
        if self.source_type == 'image':
            result = self.detector.detect(self.source)
            self.finished.emit(result)
        elif self.source_type == 'video':
            self.process_video()
        elif self.source_type == 'folder':
            self.process_folder()
    except Exception as e:
        self.error.emit(str(e))

def process_video(self):
    """处理视频文件"""
    cap = cv2.VideoCapture(self.source)
    if not cap.isOpened():
        self.error.emit("无法打开视频文件")
        return
    
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = cap.get(cv2.CAP_PROP_FPS)
    
    # 创建输出目录
    output_dir = Path("output") / "video_results" / Path(self.source).stem
    output_dir.mkdir(parents=True, exist_ok=True)
    
    all_detections = []
    frame_count = 0
    detection_count = 0
    
    while self._is_running and cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break
        
        frame_count += 1
        
        # 每5帧检测一次以提高速度
        if frame_count % 5 == 0:
            # 保存帧为临时文件
            temp_frame_path = output_dir / f"temp_frame_{frame_count}.jpg"
            cv2.imwrite(str(temp_frame_path), frame)
            
            # 检测
            result = self.detector.detect(str(temp_frame_path))
            
            if result.get('success'):
                annotated_image = result['annotated_image']
                detections = result.get('detections', [])
                
                # 发送图像到UI显示
                detection_info = {
                    'frame': frame_count,
                    'time': frame_count / fps,
                    'detections': detections,
                    'total_detections': len(detections)
                }
                self.frame_processed.emit(annotated_image, detection_info)
                
                # 保存带标注的帧
                if detections:  # 只保存有检测结果的帧
                    annotated_path = output_dir / f"frame_{frame_count:06d}.jpg"
                    cv2.imwrite(str(annotated_path), annotated_image)
                    detection_count += 1
                
                all_detections.append({
                    'frame': frame_count,
                    'time': frame_count / fps,
                    'detections': detections
                })
            
            # 删除临时文件
            temp_frame_path.unlink(missing_ok=True)
            
            # 更新进度
            progress = int((frame_count / total_frames) * 100)
            time_str = f"{int(frame_count / fps)}s"
            self.progress.emit(progress, f"处理帧 {frame_count}/{total_frames} ({time_str})")
    
    cap.release()
    
    # 保存检测结果
    result_json = output_dir / "detection_results.json"
    with open(result_json, 'w', encoding='utf-8') as f:
        json.dump(all_detections, f, ensure_ascii=False, indent=2)
    
    # 返回结果
    result = {
        'success': True,
        'type': 'video',
        'total_frames': frame_count,
        'output_dir': str(output_dir),
        'detections_count': detection_count,
        'result_file': str(result_json)
    }
    self.finished.emit(result)

def process_folder(self):
    """处理文件夹中的所有图片"""
    folder_path = Path(self.source)
    image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}
    image_files = [f for f in folder_path.iterdir() 
                  if f.suffix.lower() in image_extensions]
    
    if not image_files:
        self.error.emit("文件夹中没有找到图片文件")
        return
    
    # 创建输出目录
    output_dir = Path("output") / "batch_results" / folder_path.name
    output_dir.mkdir(parents=True, exist_ok=True)
    
    all_results = []
    total_detections = 0
    class_counts = {}
    
    for idx, image_file in enumerate(image_files):
        if not self._is_running:
            break
        
        # 检测
        result = self.detector.detect(str(image_file))
        
        if result.get('success'):
            annotated_image = result['annotated_image']
            detections = result.get('detections', [])
            
            # 发送图像到UI显示
            detection_info = {
                'filename': image_file.name,
                'index': idx + 1,
                'total': len(image_files),
                'detections': detections,
                'total_detections': len(detections)
            }
            self.frame_processed.emit(annotated_image, detection_info)
            
            # 保存带标注的图片
            output_path = output_dir / f"result_{image_file.name}"
            cv2.imwrite(str(output_path), annotated_image)
            
            # 统计
            total_detections += len(detections)
            
            for det in detections:
                class_name = det['class_name']
                class_counts[class_name] = class_counts.get(class_name, 0) + 1
            
            all_results.append({
                'filename': image_file.name,
                'detections': detections,
                'detection_count': len(detections)
            })
        
        # 更新进度
        progress = int(((idx + 1) / len(image_files)) * 100)
        self.progress.emit(progress, f"处理 {idx + 1}/{len(image_files)}: {image_file.name}")
    
    # 保存检测结果
    result_json = output_dir / "detection_results.json"
    with open(result_json, 'w', encoding='utf-8') as f:
        json.dump({
            'total_images': len(image_files),
            'total_detections': total_detections,
            'class_counts': class_counts,
            'results': all_results
        }, f, ensure_ascii=False, indent=2)
    
    # 返回结果
    result = {
        'success': True,
        'type': 'folder',
        'total_images': len(image_files),
        'total_detections': total_detections,
        'class_counts': class_counts,
        'output_dir': str(output_dir),
        'result_file': str(result_json)
    }
    self.finished.emit(result)

class MainWindow(QMainWindow):
“”“应用主窗口”“”

def __init__(self):
    super().__init__()
    self.detector = None
    self.current_image_path = None
    self.current_source = None
    self.current_source_type = 'image'  # 'image', 'video', 'folder'
    self.detection_thread = None
    self.init_ui()
    self.load_model()

def init_ui(self):
    """初始化用户界面"""
    self.setWindowTitle("轮胎缺陷检测系统")
    self.setGeometry(100, 100, 1400, 900)
    
    # Central widget
    central_widget = QWidget()
    self.setCentralWidget(central_widget)
    
    # Main layout
    main_layout = QHBoxLayout(central_widget)
    main_layout.setSpacing(15)
    main_layout.setContentsMargins(15, 15, 15, 15)
    
    # Left panel (controls and settings)
    left_panel = self.create_left_panel()
    main_layout.addWidget(left_panel, 1)
    
    # Right panel (image display and results)
    right_panel = self.create_right_panel()
    main_layout.addWidget(right_panel, 2)
    
    # Apply styles
    self.apply_styles()

def create_left_panel(self):
    """创建左侧控制面板"""
    panel = QWidget()
    layout = QVBoxLayout(panel)
    layout.setSpacing(10)
    
    # Model status group
    status_group = self.create_status_group()
    layout.addWidget(status_group)
    
    # Image selection group
    image_group = self.create_image_selection_group()
    layout.addWidget(image_group)
    
    # Detection settings group
    settings_group = self.create_settings_group()
    layout.addWidget(settings_group)
    
    # Statistics group
    stats_group = self.create_statistics_group()
    layout.addWidget(stats_group)
    
    layout.addStretch()
    
    return panel

def create_status_group(self):
    """创建模型状态面板"""
    group = QGroupBox("模型状态")
    layout = QVBoxLayout(group)
    
    self.status_label = QLabel("状态:未加载\n\n正在加载轮胎缺陷检测模型,请稍候...")
    self.status_label.setWordWrap(True)
    self.status_label.setToolTip("显示当前模型的加载与运行状态")
    layout.addWidget(self.status_label)
    
    self.progress_bar = QProgressBar()
    self.progress_bar.setVisible(False)
    layout.addWidget(self.progress_bar)
    
    return group

def create_image_selection_group(self):
    """创建图像/视频/文件夹选择面板"""
    group = QGroupBox("输入源选择")
    layout = QVBoxLayout(group)
    
    # 输入类型选择
    type_layout = QHBoxLayout()
    type_label = QLabel("输入类型:")
    type_layout.addWidget(type_label)
    
    self.source_type_combo = QComboBox()
    self.source_type_combo.addItems(["图片", "视频", "文件夹批量"])
    self.source_type_combo.currentTextChanged.connect(self.on_source_type_changed)
    self.source_type_combo.setToolTip("选择要处理的输入类型")
    type_layout.addWidget(self.source_type_combo)
    layout.addLayout(type_layout)
    
    self.select_source_btn = QPushButton("选择输入源")
    self.select_source_btn.clicked.connect(self.select_source)
    self.select_source_btn.setToolTip("点击选择轮胎图像、视频或批量图片文件夹")
    layout.addWidget(self.select_source_btn)
    
    self.detect_btn = QPushButton("开始检测")
    self.detect_btn.clicked.connect(self.start_detection)
    self.detect_btn.setEnabled(False)
    self.detect_btn.setToolTip("对当前输入源执行轮胎缺陷检测")
    layout.addWidget(self.detect_btn)
    
    self.stop_btn = QPushButton("停止检测")
    self.stop_btn.clicked.connect(self.stop_detection)
    self.stop_btn.setEnabled(False)
    self.stop_btn.setVisible(False)
    self.stop_btn.setStyleSheet("""
        QPushButton {
            background-color: #e74c3c;
        }
        QPushButton:hover {
            background-color: #c0392b;
        }
    """)
    layout.addWidget(self.stop_btn)
    
    self.source_path_label = QLabel("尚未选择输入源")
    self.source_path_label.setWordWrap(True)
    self.source_path_label.setStyleSheet("color: #666; font-style: italic;")
    layout.addWidget(self.source_path_label)
    
    return group

def create_settings_group(self):
    """创建检测参数设置面板"""
    group = QGroupBox("检测参数设置")
    layout = QVBoxLayout(group)
    
    # Confidence threshold
    conf_layout = QHBoxLayout()
    conf_label = QLabel("置信度阈值:")
    conf_label.setToolTip("用于过滤低置信度结果,范围 0.0 - 1.0\n数值越低,识别出的目标越多,但误检也会增加。")
    conf_layout.addWidget(conf_label)
    self.conf_spinbox = QDoubleSpinBox()
    self.conf_spinbox.setRange(0.0, 1.0)
    self.conf_spinbox.setSingleStep(0.05)
    self.conf_spinbox.setValue(0.25)
    self.conf_spinbox.setToolTip("调整置信度阈值(0.0 - 1.0)")
    self.conf_spinbox.valueChanged.connect(self.update_confidence)
    conf_layout.addWidget(self.conf_spinbox)
    layout.addLayout(conf_layout)
    
    # IoU threshold
    iou_layout = QHBoxLayout()
    iou_label = QLabel("IoU 阈值:")
    iou_label.setToolTip("用于非极大值抑制 (NMS) 的 IoU 阈值,范围 0.0 - 1.0\n数值越低,保留的重叠框越多。")
    iou_layout.addWidget(iou_label)
    self.iou_spinbox = QDoubleSpinBox()
    self.iou_spinbox.setRange(0.0, 1.0)
    self.iou_spinbox.setSingleStep(0.05)
    self.iou_spinbox.setValue(0.45)
    self.iou_spinbox.setToolTip("调整 NMS 的 IoU 阈值(0.0 - 1.0)")
    self.iou_spinbox.valueChanged.connect(self.update_iou)
    iou_layout.addWidget(self.iou_spinbox)
    layout.addLayout(iou_layout)
    
    return group

def create_statistics_group(self):
    """创建检测统计信息面板"""
    group = QGroupBox("检测统计")
    layout = QVBoxLayout(group)
    
    self.stats_table = QTableWidget()
    self.stats_table.setColumnCount(2)
    self.stats_table.setHorizontalHeaderLabels(["类别", "数量"])
    self.stats_table.horizontalHeader().setStretchLastSection(True)
    self.stats_table.setMaximumHeight(300)
    self.stats_table.setToolTip("显示当前图像中各类别数量统计")
    layout.addWidget(self.stats_table)
    
    self.total_label = QLabel("识别目标总数:0")
    self.total_label.setToolTip("当前图像中识别到的全部目标数量")
    layout.addWidget(self.total_label)
    
    return group

def create_right_panel(self):
    """创建右侧图像显示与结果面板"""
    panel = QWidget()
    layout = QVBoxLayout(panel)
    layout.setSpacing(10)
    
    # Image display area
    image_group = QGroupBox("图像显示")
    image_layout = QVBoxLayout(image_group)
    
    self.image_label = QLabel("尚未加载图像")
    self.image_label.setAlignment(Qt.AlignCenter)
    self.image_label.setMinimumSize(800, 600)
    self.image_label.setStyleSheet("""
        QLabel {
            border: 2px dashed #ccc;
            background-color: #f5f5f5;
            color: #999;
        }
    """)
    
    scroll_area = QScrollArea()
    scroll_area.setWidget(self.image_label)
    scroll_area.setWidgetResizable(True)
    scroll_area.setMinimumHeight(600)
    image_layout.addWidget(scroll_area)
    
    layout.addWidget(image_group)
    
    # Detection results
    results_group = QGroupBox("检测结果")
    results_layout = QVBoxLayout(results_group)
    
    self.results_text = QTextEdit()
    self.results_text.setReadOnly(True)
    self.results_text.setMaximumHeight(150)
    self.results_text.setPlaceholderText("运行检测后,这里将显示轮胎缺陷检测结果(类别、数量、置信度等)...")
    self.results_text.setToolTip("显示每个识别目标的类别名称、置信度和位置信息")
    results_layout.addWidget(self.results_text)
    
    layout.addWidget(results_group)
    
    return panel

def load_model(self):
    """加载轮胎缺陷检测模型"""
    self.status_label.setText("状态:正在加载模型...")
    self.progress_bar.setVisible(True)
    self.progress_bar.setRange(0, 0)  # Indeterminate progress
    
    try:
        # 固定从本程序目录加载 best.pt(不依赖当前工作目录)
        possible_paths = [
            CODE_ROOT / "best.pt",
            CODE_ROOT / "runs" / "detect" / "train" / "weights" / "best.pt",
            CODE_ROOT / "runs" / "detect" / "train2" / "weights" / "best.pt",
            CODE_ROOT / "runs" / "segment" / "train" / "weights" / "best.pt",
        ]

        model_path = None
        for path in possible_paths:
            if path.is_file():
                model_path = str(path.resolve())
                break

        if model_path is None:
            import glob

            pattern = str(CODE_ROOT / "runs" / "**" / "weights" / "best.pt")
            runs_best = glob.glob(pattern, recursive=True)
            if runs_best:
                model_path = str(Path(max(runs_best, key=os.path.getmtime)).resolve())

        if model_path is None or not os.path.isfile(model_path):
            hint = str(CODE_ROOT / "best.pt")
            QMessageBox.critical(
                self,
                "错误",
                f"未找到模型权重文件 best.pt\n\n"
                f"请将 best.pt 放到以下路径之一:\n"
                f"· {hint}\n"
                f"· {CODE_ROOT / 'runs' / 'detect' / 'train' / 'weights' / 'best.pt'}\n"
                f"· 或 runs 下任意 …/weights/best.pt",
            )
            self.status_label.setText("状态:模型权重文件不存在")
            self.progress_bar.setVisible(False)
            return
        
        data_yaml = str(CODE_ROOT / "datasets" / "data" / "data.yaml")
        
        self.detector = ConveyorDetector(model_path, data_yaml)
        if self.detector.load_model():
            self.status_label.setText(
                f"状态:模型加载成功\n"
                f"权重文件:{model_path}\n"
                f"设备:{self.detector.device}\n"
                f"类别数:{len(self.detector.class_names)}\n"
                f"类别:{', '.join(self.detector.class_names)}"
            )
            self.progress_bar.setVisible(False)
        else:
            QMessageBox.critical(self, "错误", "模型加载失败,请检查权重文件与环境配置。")
            self.status_label.setText("状态:模型加载失败")
            self.progress_bar.setVisible(False)
    except Exception as e:
        QMessageBox.critical(self, "错误", f"加载模型时发生异常:{str(e)}")
        self.status_label.setText(f"状态:错误 - {str(e)}")
        self.progress_bar.setVisible(False)

def select_image(self):
    """打开文件对话框选择待检测图像"""
    file_path, _ = QFileDialog.getOpenFileName(
        self,
        "选择图像",
        "",
        "图像文件 (*.png *.jpg *.jpeg *.bmp *.tiff);;所有文件 (*)"
    )
    
    if file_path:
        self.current_image_path = file_path
        self.image_path_label.setText(f"已选择:{os.path.basename(file_path)}")
        self.detect_btn.setEnabled(True)
        self.display_image(file_path)

def on_source_type_changed(self, text):
    """输入类型改变时的处理"""
    self.current_source = None
    self.detect_btn.setEnabled(False)
    
    if text == "图片":
        self.source_path_label.setText("尚未选择图片")
        self.current_source_type = 'image'
    elif text == "视频":
        self.source_path_label.setText("尚未选择视频")
        self.current_source_type = 'video'
    elif text == "文件夹批量":
        self.source_path_label.setText("尚未选择文件夹")
        self.current_source_type = 'folder'

def select_source(self):
    """根据选择的类型打开相应的文件/文件夹对话框"""
    source_type = self.source_type_combo.currentText()
    
    if source_type == "图片":
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "选择图片",
            "",
            "图像文件 (*.png *.jpg *.jpeg *.bmp *.tiff);;所有文件 (*)"
        )
        if file_path:
            self.current_source = file_path
            self.current_image_path = file_path
            self.source_path_label.setText(f"已选择:{os.path.basename(file_path)}")
            self.detect_btn.setEnabled(True)
            self.display_image(file_path)
    
    elif source_type == "视频":
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "选择视频",
            "",
            "视频文件 (*.mp4 *.avi *.mov *.mkv);;所有文件 (*)"
        )
        if file_path:
            self.current_source = file_path
            self.source_path_label.setText(f"已选择:{os.path.basename(file_path)}")
            self.detect_btn.setEnabled(True)
            # 显示视频第一帧
            self.display_video_first_frame(file_path)
    
    elif source_type == "文件夹批量":
        folder_path = QFileDialog.getExistingDirectory(
            self,
            "选择文件夹",
            ""
        )
        if folder_path:
            self.current_source = folder_path
            # 统计图片数量
            image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}
            image_count = sum(1 for f in Path(folder_path).iterdir() 
                            if f.suffix.lower() in image_extensions)
            self.source_path_label.setText(
                f"已选择:{os.path.basename(folder_path)}\n"
                f"包含 {image_count} 张图片"
            )
            self.detect_btn.setEnabled(True)

def display_video_first_frame(self, video_path):
    """显示视频的第一帧"""
    try:
        cap = cv2.VideoCapture(video_path)
        ret, frame = cap.read()
        cap.release()
        
        if ret:
            # Convert BGR to RGB
            image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = image_rgb.shape
            bytes_per_line = ch * w
            qt_image = QImage(image_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
            
            pixmap = QPixmap.fromImage(qt_image)
            scaled_pixmap = pixmap.scaled(
                self.image_label.size(),
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation
            )
            self.image_label.setPixmap(scaled_pixmap)
        else:
            self.image_label.setText("无法读取视频第一帧")
    except Exception as e:
        QMessageBox.warning(self, "警告", f"显示视频预览时发生错误:{str(e)}")`# 源码文件

源码文件

在这里插入图片描述

源码获取

欢迎大家点赞、收藏、关注、评论啦 、查看👇🏻获取联系方式👇🏻
https://download.csdn.net/download/2301_78772942/92740169

更多推荐