【Python】基于 PyQt6 与 OpenCV 的实现人脸采集实验报告
·
实验目的及原理:
-
实验目的:
通过结合 cv2 与 PyQt6,实现带界面的面部图像采集与标注功能,包括界面交互、图像实时显示、手动框选人脸区域并保存图像及对应位置信息。
实验原理:
(1)PyQt6部分:
- 通过 PyQt6 的
QMainWindow搭建主窗口框架,使用QVBoxLayout/QHBoxLayout实现控件(按钮、标签、文本框)的有序布局; - 借助
QTimer定时器(30ms 间隔)触发update_frame函数,持续从摄像头读取帧数据,实现实时画面刷新; - 利用
QFileDialog提供路径选择功能,QMessageBox实现警告、成功、错误等用户反馈,提升工具易用性。 - 自定义
FaceLabel控件,重写mousePressEvent/mouseMoveEvent/mouseReleaseEvent,实现鼠标拖拽绘制红色矩形框,记录人脸区域坐标;
(2)OpenCV部分:
- OpenCV 读取的图像为
BGR格式,而 PyQt6 的QImage仅支持RGB格式,需通过cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)完成格式转换; - 界面显示的图像会因
QLabel固定尺寸(640×480)缩放,框选坐标需通过 “原始图像尺寸 / 界面标签尺寸” 的缩放比例(scale_x/scale_y)映射回原始图像,确保裁剪位置准确。 - 保存阶段分三类文件:通过
cv2.imwrite保存原始图像(face_XXXX.png)和裁剪的人脸区域(face_crop_XXXX.png),通过文件操作写入位置信息(face_XXXX.txt,含图像序号、路径、人脸坐标、尺寸)。
程序代码:
import sys
import cv2
import os
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QWidget, QFileDialog, QMessageBox, QTextEdit, QDialog
)
from PyQt6.QtCore import QTimer, Qt, QRect
from PyQt6.QtGui import QImage, QPixmap, QPainter, QPen, QMouseEvent
class FaceLabel(QLabel):
"""用于手动框选人脸的QLabel"""
def __init__(self, parent=None):
"""初始化FaceLabel控件,设置框选相关的初始变量"""
super().__init__(parent)
self.start_point = None # 框选起始点
self.end_point = None # 框选终点
self.drawing = False # 是否正在绘制状态标记
self.rect = None # 最终确定的矩形框
def mousePressEvent(self, event: QMouseEvent):
"""处理鼠标按下事件,开始框选操作"""
if event.button() == Qt.MouseButton.LeftButton:
self.start_point = event.pos() # 记录起始点
self.end_point = self.start_point
self.drawing = True # 标记为正在绘制
self.update() # 触发重绘
def mouseMoveEvent(self, event: QMouseEvent):
"""处理鼠标移动事件,更新框选区域"""
if self.drawing:
self.end_point = event.pos() # 更新终点位置
self.update() # 触发重绘以实时显示框选效果
def mouseReleaseEvent(self, event: QMouseEvent):
"""处理鼠标释放事件,完成框选操作"""
if event.button() == Qt.MouseButton.LeftButton and self.drawing:
self.end_point = event.pos() # 确定最终终点
self.drawing = False # 结束绘制状态
# 标准化矩形(确保宽高为正值)
self.rect = QRect(self.start_point, self.end_point).normalized()
self.update() # 触发重绘
def paintEvent(self, event):
"""重绘事件,绘制框选矩形"""
super().paintEvent(event) # 调用父类方法保持原有功能
if self.start_point and self.end_point:
painter = QPainter(self)
# 设置画笔样式:红色、2px宽、实线
painter.setPen(QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.SolidLine))
# 绘制标准化的矩形
rect = QRect(self.start_point, self.end_point).normalized()
painter.drawRect(rect)
def get_rect(self):
"""获取最终确定的框选矩形"""
return self.rect
def reset(self):
"""重置框选状态,清除已绘制的矩形"""
self.start_point = None
self.end_point = None
self.rect = None
self.update() # 触发重绘以清除矩形
class FaceSelectionDialog(QDialog):
"""人脸框选对话框,用于显示采集的图像并进行人脸框选"""
def __init__(self, frame, img_path, img_count, parent=None):
"""初始化对话框,接收图像数据、保存路径和图像计数"""
super().__init__(parent)
self.frame = frame # 待处理的图像帧
self.img_path = img_path # 图像保存路径
self.img_count = img_count # 图像序号
# 创建人脸裁剪保存目录
self.face_crop_dir = os.path.join(os.path.dirname(img_path), "face_crops")
os.makedirs(self.face_crop_dir, exist_ok=True)
self.setWindowTitle("框选人脸区域")
self.setFixedSize(800, 600)
self.init_ui() # 初始化界面
def init_ui(self):
"""初始化对话框界面,显示图像并添加操作按钮"""
# 转换OpenCV图像格式为PyQt可显示的格式
rgb_image = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
# 创建自定义的FaceLabel控件用于显示图像和框选
self.face_label = FaceLabel()
self.face_label.setPixmap(QPixmap.fromImage(qt_image))
self.face_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 创建确认按钮并绑定事件
btn_confirm = QPushButton("确认框选")
btn_confirm.clicked.connect(self.confirm_selection)
# 创建重新拍摄按钮并绑定事件
btn_retake = QPushButton("重新拍摄")
btn_retake.clicked.connect(self.reject)
# 布局管理
layout = QVBoxLayout()
layout.addWidget(self.face_label)
btn_layout = QHBoxLayout()
btn_layout.addWidget(btn_confirm)
btn_layout.addWidget(btn_retake)
layout.addLayout(btn_layout)
self.setLayout(layout)
def confirm_selection(self):
"""确认框选结果,保存原始图像、裁剪的人脸区域和框选信息"""
rect = self.face_label.get_rect()
if rect is None:
QMessageBox.warning(self, "警告", "请先框选人脸区域")
return
try:
# 检查保存目录是否存在,不存在则创建
save_dir = os.path.dirname(self.img_path)
if not os.path.exists(save_dir):
os.makedirs(save_dir, exist_ok=True)
# 验证图像数据有效性
if self.frame is None or self.frame.size == 0:
raise ValueError("无效的图像数据,无法保存")
# 保存原始图像文件
success = cv2.imwrite(self.img_path, self.frame)
if not success:
raise IOError(f"无法写入原始图像文件到 {self.img_path}")
# 裁剪并保存框选的人脸区域
# 注意:需要将Qt的坐标转换为OpenCV的坐标(因为图像可能被缩放)
img_height, img_width = self.frame.shape[:2]
label_width = self.face_label.width()
label_height = self.face_label.height()
# 计算缩放比例
scale_x = img_width / label_width
scale_y = img_height / label_height
# 转换框选坐标到原始图像尺寸
x = int(rect.x() * scale_x)
y = int(rect.y() * scale_y)
width = int(rect.width() * scale_x)
height = int(rect.height() * scale_y)
# 确保坐标在有效范围内
x = max(0, min(x, img_width - 1))
y = max(0, min(y, img_height - 1))
width = max(1, min(width, img_width - x))
height = max(1, min(height, img_height - y))
# 裁剪人脸区域
face_crop = self.frame[y:y + height, x:x + width]
# 保存裁剪的人脸区域
crop_img_name = f"face_crop_{self.img_count:04d}.png"
crop_img_path = os.path.join(self.face_crop_dir, crop_img_name)
success_crop = cv2.imwrite(crop_img_path, face_crop)
if not success_crop:
raise IOError(f"无法写入人脸裁剪图像到 {crop_img_path}")
# 保存人脸位置信息到文本文件
txt_path = self.img_path.replace('.png', '.txt')
with open(txt_path, 'w', encoding='utf-8') as f:
f.write(f"图像序号: {self.img_count}\n")
f.write(f"原始图像文件: {os.path.basename(self.img_path)}\n")
f.write(f"人脸裁剪文件: {os.path.basename(crop_img_path)}\n")
f.write(f"人脸位置(原始图像): x={x}, y={y}, width={width}, height={height}\n")
f.write(f"图像尺寸: {img_width}x{img_height}\n")
# 显示成功信息并关闭对话框
QMessageBox.information(self, "成功",
f"已保存以下文件:\n"
f"原始图像: {os.path.basename(self.img_path)}\n"
f"人脸裁剪: {os.path.basename(crop_img_path)}\n"
f"位置信息: {os.path.basename(txt_path)}")
self.accept()
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件时出错:{str(e)}")
class MainWindow(QMainWindow):
def __init__(self):
"""初始化主窗口,设置界面布局和初始化变量"""
super().__init__()
self.setWindowTitle("人脸采集工具")
self.setFixedSize(800, 600) # 设置窗口大小为4:3比例
# 初始化摄像头
self.cap = cv2.VideoCapture(0)
if not self.cap.isOpened():
QMessageBox.critical(self, "错误", "无法打开摄像头")
sys.exit(1)
# 初始化定时器用于更新摄像头画面
self.timer = QTimer()
self.timer.timeout.connect(self.update_frame)
# 创建界面组件
self.video_label = QLabel()
self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.video_label.setStyleSheet("border: 1px solid black;")
self.video_label.setFixedSize(640, 480) # 4:3比例的显示区域
self.path_text = QTextEdit()
self.path_text.setMaximumHeight(60)
self.path_text.setReadOnly(True) # 路径显示区域设为只读
self.btn_select_path = QPushButton("选择保存路径")
self.btn_capture = QPushButton("采集截图")
# 设置布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.addWidget(QLabel("实时摄像头画面:"), alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.video_label, alignment=Qt.AlignmentFlag.AlignCenter)
btn_layout = QHBoxLayout()
btn_layout.addWidget(self.btn_select_path)
btn_layout.addWidget(self.btn_capture)
layout.addLayout(btn_layout)
layout.addWidget(QLabel("当前保存路径:"))
layout.addWidget(self.path_text)
# 初始化变量
self.save_path = "" # 保存路径
self.img_count = 0 # 图像计数器
# 连接信号与槽函数
self.btn_select_path.clicked.connect(self.select_path)
self.btn_capture.clicked.connect(self.capture_image)
# 启动定时器,开始刷新摄像头画面
self.timer.start(30)
def select_path(self):
"""处理选择保存路径的操作"""
path = QFileDialog.getExistingDirectory(self, "选择保存路径")
if path:
self.save_path = path
# 计算已存在的图片数量并更新计数
self.img_count = self.count_existing_images()
# 更新路径显示
self.path_text.setPlainText(f"保存路径: {path}\n已保存图片数量: {self.img_count}")
def count_existing_images(self):
"""计算指定路径下已存在的符合命名规则的图像数量"""
if not self.save_path or not os.path.exists(self.save_path):
return 0
# 只统计符合"face_XXXX.png"命名规则的图片
count = 0
for f in os.listdir(self.save_path):
if f.startswith("face_") and f.endswith(".png"):
try:
# 提取文件名中的数字部分(如"face_0001.png"中的"0001")
num = int(f[5:-4])
if num > count:
count = num
except ValueError:
continue # 忽略命名不符合规则的文件
return count
def capture_image(self):
"""处理采集截图操作,获取当前摄像头帧并打开框选对话框"""
if not self.save_path:
QMessageBox.warning(self, "警告", "请先选择保存路径")
return
# 获取当前摄像头帧
ret, frame = self.cap.read()
if not ret:
QMessageBox.critical(self, "错误", "无法获取摄像头画面")
return
if frame is None or frame.size == 0:
QMessageBox.critical(self, "错误", "获取的画面为空")
return
try:
# 增加计数并生成文件名
self.img_count += 1
img_name = f"face_{self.img_count:04d}.png"
img_path = os.path.join(self.save_path, img_name)
# 弹出框选窗口
dialog = FaceSelectionDialog(frame, img_path, self.img_count, self)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
# 更新路径显示
self.path_text.setPlainText(
f"保存路径: {self.save_path}\n已保存图片数量: {self.count_existing_images()}")
QMessageBox.information(self, "成功", f"已保存第{self.img_count}张人脸图像和位置信息")
else:
# 用户取消了框选,恢复计数
self.img_count -= 1
except Exception as e:
QMessageBox.critical(self, "错误", f"处理图像时出错:{str(e)}")
if self.img_count > 0:
self.img_count -= 1 # 出错时恢复计数
def update_frame(self):
"""定时更新摄像头画面到界面"""
ret, frame = self.cap.read()
if ret:
# 转换OpenCV图像格式为PyQt可显示的格式
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
# 缩放图像并显示在界面上
self.video_label.setPixmap(QPixmap.fromImage(qt_image).scaled(
640, 480, Qt.AspectRatioMode.KeepAspectRatio))
def closeEvent(self, event):
"""窗口关闭事件处理,释放资源"""
self.cap.release() # 释放摄像头资源
self.timer.stop() # 停止定时器
event.accept() # 接受关闭事件
if __name__ == "__main__":
"""程序入口点,创建应用实例并运行"""
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
实验结果与分析:
结果:

成功运行程序后,可在界面看到实时摄像头画面;点击 “选择保存路径” 能指定图像保存目录;点击 “采集截图” 会弹出框选人脸的对话框,框选后确认可将原图像face_0001.png、框选图像(储存在face_crops内)和对应位置信息(保存于同路径下的 .txt 文件)保存(建议保存到C盘以外的其他硬盘),界面会提示保存成功并更新已保存图片数量。(多次运行以总和为准)
分析:
程序实现了任务要求的界面交互、图像采集与标注功能。cv2 与 PyQt6 结合顺畅,手动框选功能准确,文件保存逻辑正常,但在高频率采集或复杂环境下,需进一步测试性能与稳定性,比如摄像头画面延迟、大数量图像保存时的效率等。
个人收获与体会:
- 掌握了 PyQt6 与 cv2 结合进行图像采集与界面开发的方法,理解了图形界面与图像处理模块协同工作的流程。
- 深入学习了 PyQt6 中自定义控件(如 FaceLabel)、对话框(FaceSelectionDialog)的创建与事件处理,以及 cv2 图像格式与 PyQt6 图像格式的转换技巧。
- 体会到模块化编程的优势,将界面、图像采集、标注等功能分模块实现,使代码结构更清晰,便于维护与扩展。
思考:
-
能否引入自动人脸检测算法(如 dlib 或 OpenCV 的人脸检测器),替代手动框选,提高人脸标注的效率与准确性?
-
当需要采集大量面部图像时,程序在性能(如内存占用、画面刷新率)方面能否满足需求,是否需要进行优化?
更多推荐


所有评论(0)