

你是否需要批量处理图片、简单检测图片状态?今天带大家从零拆解一款可视化图片批量检测系统!这款工具用 PyQt5 做美观界面、OpenCV 处理图片,支持选择文件夹、批量读图、实时显示、结果表格记录,代码完整可直接运行,新手也能轻松上手!
import sys # 系统交互,用于程序退出
import cv2 # OpenCV,负责图片读取、处理
import os # 文件路径操作
from pathlib import Path # 更简洁的文件路径管理
import threading # 多线程,防止界面检测时卡死
# PyQt5三大核心组件:窗口/控件/样式
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
✅ 知识点:
threading 必须用!GUI程序如果在主线程做耗时操作,界面会直接卡死Path 比 os.path 更简洁,支持 glob 批量查找文件classDefectDetectionWindow(QMainWindow):
def__init__(self):
super().__init__() # 调用父类初始化
self.setWindowTitle("图片批量检测系统") # 窗口标题
self.setGeometry(100, 100, 1300, 780) # 窗口位置(100,100)+大小(1300x780)
# 状态变量
self.is_running = False# 检测运行标记
self.model_folder = ""# 模型文件夹路径
self.image_folder = ""# 图片文件夹路径
# 支持的图片后缀名
self.image_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']
self.init_ui() # 初始化界面
✅ 知识点:
QMainWindow 是 PyQt5 主窗口类,自带菜单栏/状态栏self.xxx 定义全局变量,方便类内所有函数调用definit_ui(self):
# 中心控件(主窗口必须设置中心控件)
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 垂直布局:从上到下排列控件
main_layout = QVBoxLayout(central_widget)
# ========== 1. 顶部控制面板 ==========
control_frame = QFrame() # 容器框架
# CSS样式:背景色+圆角+内边距
control_frame.setStyleSheet("background-color: #F8F9FA; border-radius:8px; padding:10px;")
control_layout = QHBoxLayout(control_frame) # 水平布局
# 选择模型按钮
self.model_btn = QPushButton("📂 选择模型文件夹")
self.model_btn.setStyleSheet("padding:10px; font-size:14px;")
self.model_btn.clicked.connect(self.select_model_folder) # 点击绑定事件
control_layout.addWidget(self.model_btn)
# 选择图片按钮(逻辑同上)
self.img_btn = QPushButton("🖼 选择图片文件夹")
self.img_btn.setStyleSheet("padding:10px; font-size:14px;")
self.img_btn.clicked.connect(self.select_image_folder)
control_layout.addWidget(self.img_btn)
# 开始检测按钮
self.start_btn = QPushButton("▶ 开始检测")
self.start_btn.setStyleSheet("padding:10px; font-size:14px; background:#E8F5E8; color:#1B5E20;")
self.start_btn.clicked.connect(self.start_detection)
control_layout.addWidget(self.start_btn)
# 停止检测按钮
self.stop_btn = QPushButton("■ 停止检测")
self.stop_btn.setStyleSheet("padding:10px; font-size:14px; background:#FFEBEE; color:#C62828;")
self.stop_btn.clicked.connect(self.stop_detection)
control_layout.addWidget(self.stop_btn)
main_layout.addWidget(control_frame)
# ========== 2. 中间图片显示区域 ==========
self.video_label = QLabel() # 标签控件,用于显示图片
self.video_label.setStyleSheet("background-color:white; border-radius:8px;")
self.video_label.setAlignment(Qt.AlignCenter) # 居中显示
self.video_label.setText("选择模型文件夹 + 图片文件夹 → 开始检测")
self.video_label.setMinimumSize(800, 500) # 最小尺寸
main_layout.addWidget(self.video_label)
# ========== 3. 底部检测结果表格 ==========
result_group = QGroupBox("📋 检测结果") # 分组框
result_layout = QVBoxLayout(result_group)
self.result_table = QTableWidget() # 表格控件
self.result_table.setColumnCount(4) # 4列
self.result_table.setHorizontalHeaderLabels(["文件名", "检测状态", "尺寸", "检测时间"])
# 设置列宽
self.result_table.setColumnWidth(0, 250)
self.result_table.setColumnWidth(1, 120)
self.result_table.setColumnWidth(2, 120)
self.result_table.setColumnWidth(3, 180)
result_layout.addWidget(self.result_table)
main_layout.addWidget(result_group)
self.add_log("✅ 系统启动成功") # 打印日志
✅ 知识点:
QVBoxLayout(垂直)+QHBoxLayout(水平)是PyQt5最常用布局按钮.clicked.connect(函数) 是PyQt5核心交互机制# 选择模型文件夹
defselect_model_folder(self):
# 弹出文件夹选择框
folder = QFileDialog.getExistingDirectory(self, "选择模型文件夹")
if folder: # 如果选择了路径
self.model_folder = folder
# 修改按钮文字,显示选择的文件夹名
self.model_btn.setText(f"✅ 模型文件夹:{os.path.basename(folder)}")
self.add_log(f"📂 模型路径:{folder}")
# 选择图片文件夹(逻辑完全一致)
defselect_image_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择图片文件夹")
if folder:
self.image_folder = folder
self.img_btn.setText(f"🖼 图片文件夹:{os.path.basename(folder)}")
self.add_log(f"📂 图片路径:{folder}")
✅ 知识点:
QFileDialog.getExistingDirectory:PyQt5专用文件夹选择对话框os.path.basename(path):获取路径最后的文件夹/文件名defstart_detection(self):
# 判空:必须先选择两个文件夹
ifnot self.model_folder:
self.add_log("❌ 请先选择模型文件夹")
return
ifnot self.image_folder:
self.add_log("❌ 请先选择图片文件夹")
return
self.is_running = True# 标记运行中
self.result_table.setRowCount(0) # 清空表格
self.add_log("▶ 开始批量检测...")
# 开启子线程执行检测,daemon=True:主线程退出子线程自动退出
threading.Thread(target=self.run_detect, daemon=True).start()
✅ 知识点:
run_detect是耗时操作,放在主线程会让界面卡死daemon=True:守护线程,防止程序关闭后后台线程残留defrun_detect(self):
folder = Path(self.image_folder)
images = []
# 遍历所有支持的图片后缀,批量查找文件
for ext in self.image_exts:
images += list(folder.glob(f"*{ext}")) # 小写后缀
images += list(folder.glob(f"*{ext.upper()}")) # 大写后缀
# 无图片判断
ifnot images:
self.add_log("❌ 未找到图片")
self.is_running = False
return
total = len(images)
self.add_log(f"✅ 找到 {total} 张图片")
# 遍历所有图片
for i, img_path in enumerate(images):
ifnot self.is_running: # 点击停止则退出循环
break
name = img_path.name
self.add_log(f"[{i+1}/{total}] 读取:{name}")
# OpenCV读取图片
frame = cv2.imread(str(img_path))
if frame isNone: # 图片读取失败
self.add_table_row(name, "读取失败", "", "")
continue
# 获取图片宽高
h, w = frame.shape[:2]
self.show_image(frame) # 显示图片到界面
# 添加结果到表格
self.add_table_row(name, "已检测", f"{w}×{h}", "")
cv2.waitKey(300) # 延迟300ms,方便查看图片
self.add_log("🎉 全部检测完成!")
self.is_running = False
✅ 知识点:
Path.glob:批量查找文件,支持通配符*cv2.imread:OpenCV读取图片,失败返回Noneframe.shape:图片属性 → (高度, 宽度, 通道数)cv2.waitKey(ms):OpenCV延迟函数,单位毫秒defshow_image(self, frame):
# OpenCV默认BGR格式,转RGB才能正常显示
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = frame_rgb.shape # 获取高、宽、通道
# 转PyQt5支持的QImage格式
q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888)
# 等比例缩放显示,保持图片不变形
self.video_label.setPixmap(QPixmap.fromImage(q_img).scaled(
self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
✅ 核心坑点:
Qt.KeepAspectRatio:等比例缩放,防止图片拉伸变形defadd_table_row(self, filename, status, size, _):
row = self.result_table.rowCount() # 获取当前行数
self.result_table.insertRow(row) # 插入新行
# 获取当前时间
time_str = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
# 给每一列赋值
self.result_table.setItem(row, 0, QTableWidgetItem(filename))
self.result_table.setItem(row, 1, QTableWidgetItem(status))
self.result_table.setItem(row, 2, QTableWidgetItem(size))
self.result_table.setItem(row, 3, QTableWidgetItem(time_str))
# 停止检测:修改变量,循环自动退出
defstop_detection(self):
self.is_running = False
self.add_log("⏹ 已停止")
# 日志打印:控制台输出
defadd_log(self, text):
print(f"[日志] {text}")
if __name__ == "__main__":
app = QApplication(sys.argv) # 创建应用实例
window = DefectDetectionWindow() # 创建窗口
window.show() # 显示窗口
sys.exit(app.exec_()) # 运行程序,循环监听
import sys
import cv2
import os
from pathlib import Path
import threading
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
classDefectDetectionWindow(QMainWindow):
def__init__(self):
super().__init__()
self.setWindowTitle("图片批量检测系统")
self.setGeometry(100, 100, 1300, 780)
self.is_running = False
self.model_folder = ""
self.image_folder = ""
self.image_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']
self.init_ui()
definit_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
control_frame = QFrame()
control_frame.setStyleSheet("background-color: #F8F9FA; border-radius:8px; padding:10px;")
control_layout = QHBoxLayout(control_frame)
self.model_btn = QPushButton("📂 选择模型文件夹")
self.model_btn.setStyleSheet("padding:10px; font-size:14px;")
self.model_btn.clicked.connect(self.select_model_folder)
control_layout.addWidget(self.model_btn)
self.img_btn = QPushButton("🖼 选择图片文件夹")
self.img_btn.setStyleSheet("padding:10px; font-size:14px;")
self.img_btn.clicked.connect(self.select_image_folder)
control_layout.addWidget(self.img_btn)
self.start_btn = QPushButton("▶ 开始检测")
self.start_btn.setStyleSheet("padding:10px; font-size:14px; background:#E8F5E8; color:#1B5E20;")
self.start_btn.clicked.connect(self.start_detection)
control_layout.addWidget(self.start_btn)
self.stop_btn = QPushButton("■ 停止检测")
self.stop_btn.setStyleSheet("padding:10px; font-size:14px; background:#FFEBEE; color:#C62828;")
self.stop_btn.clicked.connect(self.stop_detection)
control_layout.addWidget(self.stop_btn)
main_layout.addWidget(control_frame)
self.video_label = QLabel()
self.video_label.setStyleSheet("background-color:white; border-radius:8px;")
self.video_label.setAlignment(Qt.AlignCenter)
self.video_label.setText("选择模型文件夹 + 图片文件夹 → 开始检测")
self.video_label.setMinimumSize(800, 500)
main_layout.addWidget(self.video_label)
result_group = QGroupBox("📋 检测结果")
result_layout = QVBoxLayout(result_group)
self.result_table = QTableWidget()
self.result_table.setColumnCount(4)
self.result_table.setHorizontalHeaderLabels(["文件名", "检测状态", "尺寸", "检测时间"])
self.result_table.setColumnWidth(0, 250)
self.result_table.setColumnWidth(1, 120)
self.result_table.setColumnWidth(2, 120)
self.result_table.setColumnWidth(3, 180)
result_layout.addWidget(self.result_table)
main_layout.addWidget(result_group)
self.add_log("✅ 系统启动成功")
defselect_model_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择模型文件夹")
if folder:
self.model_folder = folder
self.model_btn.setText(f"✅ 模型文件夹:{os.path.basename(folder)}")
self.add_log(f"📂 模型路径:{folder}")
defselect_image_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择图片文件夹")
if folder:
self.image_folder = folder
self.img_btn.setText(f"🖼 图片文件夹:{os.path.basename(folder)}")
self.add_log(f"📂 图片路径:{folder}")
defstart_detection(self):
ifnot self.model_folder:
self.add_log("❌ 请先选择模型文件夹")
return
ifnot self.image_folder:
self.add_log("❌ 请先选择图片文件夹")
return
self.is_running = True
self.result_table.setRowCount(0)
self.add_log("▶ 开始批量检测...")
threading.Thread(target=self.run_detect, daemon=True).start()
defrun_detect(self):
folder = Path(self.image_folder)
images = []
for ext in self.image_exts:
images += list(folder.glob(f"*{ext}"))
images += list(folder.glob(f"*{ext.upper()}"))
ifnot images:
self.add_log("❌ 未找到图片")
self.is_running = False
return
total = len(images)
self.add_log(f"✅ 找到 {total} 张图片")
for i, img_path in enumerate(images):
ifnot self.is_running:
break
name = img_path.name
self.add_log(f"[{i+1}/{total}] 读取:{name}")
frame = cv2.imread(str(img_path))
if frame isNone:
self.add_table_row(name, "读取失败", "", "")
continue
h, w = frame.shape[:2]
self.show_image(frame)
self.add_table_row(name, "已检测", f"{w}×{h}", "")
cv2.waitKey(300)
self.add_log("🎉 全部检测完成!")
self.is_running = False
defshow_image(self, frame):
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = frame_rgb.shape
q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888)
self.video_label.setPixmap(QPixmap.fromImage(q_img).scaled(
self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
defadd_table_row(self, filename, status, size, _):
row = self.result_table.rowCount()
self.result_table.insertRow(row)
time_str = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
self.result_table.setItem(row, 0, QTableWidgetItem(filename))
self.result_table.setItem(row, 1, QTableWidgetItem(status))
self.result_table.setItem(row, 2, QTableWidgetItem(size))
self.result_table.setItem(row, 3, QTableWidgetItem(time_str))
defstop_detection(self):
self.is_running = False
self.add_log("⏹ 已停止")
defadd_log(self, text):
print(f"[日志] {text}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = DefectDetectionWindow()
window.show()
sys.exit(app.exec_())
pip install pyqt5 opencv-python
detect_system.pyPyQt5 核心
QMainWindowQVBoxLayout/QHBoxLayoutOpenCV 核心
cv2.imread()cv2.COLOR_BGR2RGBframe.shape多线程核心
threading.Thread(target=函数, daemon=True)文件操作核心
Path.glob() 批量查找文件os.path.basename()这款工具是Python可视化+图像处理的经典案例,学会拆解后,你可以轻松改造出属于自己的批量处理工具!