实验报告:基于PyQt6的面部图像采集与标注工具

实验名称:面部图像采集与标注工具的设计与实现
实验日期:2025年9月
课程名称:数字图像处理

一、实验目的

  1. 掌握使用PyQt6构建图形用户界面(GUI)的基本方法;

  2. 实现摄像头实时画面的捕获与显示;

  3. 实现图像的手动框选(ROI)功能;

  4. 完成面部图像的采集、保存与标注信息的记录。

二、实验环境

  • 操作系统:Windows 10 / 11 或 Linux

  • 编程语言:Python 3.8+

  • 依赖库:

    • PyQt6

    • OpenCV(cv2)

    • NumPy

三、实验内容与步骤

1. 界面设计

  • 使用PyQt6设计主窗口,固定大小为800×600(4:3比例);

  • 界面包含以下组件:

    • 图像显示区域(居中显示摄像头画面);

    • 路径显示标签;

    • 两个功能按钮:“选择图像保存位置”和“采集图像”;

    • 状态提示标签。

2. 摄像头初始化与实时显示

  • 使用OpenCV打开默认摄像头(索引0);

  • 设置摄像头分辨率为640×480;

  • 使用QTimer定时器实现画面的实时更新(约30fps)。

3. 图像采集与框选功能

  • 点击“采集图像”按钮后,保存当前帧并进入框选模式;

  • 用户可在图像上使用鼠标框选脸部区域;

  • 框选完成后,自动保存图像和对应的标注文件(TXT格式)。

4. 文件保存与标注信息记录

  • 图像保存为JPG格式,命名格式为:face_0001.jpg

  • 标注文件保存为TXT格式,包含图像序号、脸部区域坐标和对应图像文件名。

四、核心代码说明

1. 自定义图像显示标签(ImageLabel)

  • 继承自QLabel,支持鼠标事件;

  • 实现矩形框选功能,并计算相对于原图的ROI坐标;

  • 通过自定义信号roi_selected传递框选结果。

2. 主窗口类(FaceCaptureApp)

  • 初始化摄像头和界面布局;

  • 实现路径选择、图像采集、框选确认、文件保存等功能;

  • 使用QMessageBox进行用户交互提示。

3. 图像与标注保存逻辑

  • 图像保存使用OpenCV的imwrite函数;

  • 标注信息包括:

    • 图像序号

    • 脸部区域坐标(x, y, width, height)

    • 对应的图像文件名

五、实验结果

  • 成功实现了一个图形化面部图像采集工具;

  • 可实时显示摄像头画面,支持手动框选脸部区域;

  • 每张图像均自动保存为JPG文件,并生成对应的TXT标注文件;

  • 界面友好,操作简便,符合实验要求。

示例标注文件内容(face_0001.txt):

图像序号: 1
脸部区域: x=120, y=80, width=200, height=200
对应图像: face_0001.jpg

代码如下:

import sys
import os
import cv2
import numpy as np
from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
                             QPushButton, QLabel, QWidget, QFileDialog, QMessageBox)
from PyQt6.QtCore import QTimer, Qt, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap, QPainter, QPen


class ImageLabel(QLabel):
    # 自定义信号,用于传递框选区域
    roi_selected = pyqtSignal(tuple)

    def __init__(self):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setText("摄像头画面将显示在这里")
        self.setMinimumSize(640, 480)
        self.setStyleSheet("border: 1px solid black;")

        # 框选相关变量
        self.drawing = False
        self.start_point = None
        self.end_point = None
        self.roi = None

    def set_image(self, image):
        # 将OpenCV图像转换为QImage并显示
        h, w, ch = image.shape
        bytes_per_line = ch * w
        qt_image = QImage(image.data, w, h, bytes_per_line, QImage.Format.Format_BGR888)
        self.setPixmap(QPixmap.fromImage(qt_image).scaled(
            self.width(), self.height(), Qt.AspectRatioMode.KeepAspectRatio))

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.drawing = True
            self.start_point = event.pos()
            self.end_point = event.pos()
            self.update()

    def mouseMoveEvent(self, event):
        if self.drawing:
            self.end_point = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton and self.drawing:
            self.drawing = False
            self.end_point = event.pos()

            # 计算相对于图像实际尺寸的ROI
            pixmap = self.pixmap()
            if pixmap:
                # 获取图像在QLabel中的实际显示尺寸和位置
                label_size = self.size()
                pixmap_size = pixmap.size()

                # 计算图像在QLabel中的偏移量(居中显示)
                x_offset = (label_size.width() - pixmap_size.width()) // 2
                y_offset = (label_size.height() - pixmap_size.height()) // 2

                # 计算相对于图像的坐标
                start_x = max(0, min(self.start_point.x() - x_offset, pixmap_size.width()))
                start_y = max(0, min(self.start_point.y() - y_offset, pixmap_size.height()))
                end_x = max(0, min(self.end_point.x() - x_offset, pixmap_size.width()))
                end_y = max(0, min(self.end_point.y() - y_offset, pixmap_size.height()))

                # 确保起点在左上方,终点在右下方
                x1, x2 = sorted([start_x, end_x])
                y1, y2 = sorted([start_y, end_y])

                # 计算相对于原图的比例(假设原图是640x480)
                scale_x = 640 / pixmap_size.width()
                scale_y = 480 / pixmap_size.height()

                # 转换为原图坐标
                x1_orig = int(x1 * scale_x)
                y1_orig = int(y1 * scale_y)
                x2_orig = int(x2 * scale_x)
                y2_orig = int(y2 * scale_y)

                # 确保ROI有效
                if x2_orig > x1_orig and y2_orig > y1_orig:
                    self.roi = (x1_orig, y1_orig, x2_orig - x1_orig, y2_orig - y1_orig)
                    self.roi_selected.emit(self.roi)

            self.update()

    def paintEvent(self, event):
        super().paintEvent(event)

        if self.drawing and self.start_point and self.end_point:
            painter = QPainter(self)
            painter.setPen(QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DashLine))

            # 绘制矩形框
            rect = self.start_point.x(), self.start_point.y(), \
                self.end_point.x() - self.start_point.x(), \
                self.end_point.y() - self.start_point.y()
            painter.drawRect(*rect)


class FaceCaptureApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.save_path = ""
        self.image_count = 0
        self.current_frame = None
        self.captured_image = None
        self.is_capturing = False

        self.init_ui()
        self.init_camera()

    def init_ui(self):
        self.setWindowTitle("面部图像采集工具")
        self.setFixedSize(800, 700)  # 4:3比例

        # 中央窗口部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # 主布局
        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        # 图像显示区域
        self.image_label = ImageLabel()
        layout.addWidget(self.image_label)

        # 路径显示区域
        self.path_label = QLabel("未选择保存路径")
        self.path_label.setStyleSheet("background-color: #f0f0f0; padding: 5px;")
        self.path_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.path_label)

        # 按钮区域
        button_layout = QHBoxLayout()
        layout.addLayout(button_layout)

        # 选择路径按钮
        self.select_path_btn = QPushButton("选择图像保存位置")
        self.select_path_btn.clicked.connect(self.select_save_path)
        button_layout.addWidget(self.select_path_btn)

        # 采集按钮
        self.capture_btn = QPushButton("采集图像")
        self.capture_btn.clicked.connect(self.capture_image)
        button_layout.addWidget(self.capture_btn)

        # 状态标签
        self.status_label = QLabel("准备就绪")
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.status_label)

        # 连接ROI选择信号
        self.image_label.roi_selected.connect(self.save_roi_info)

    def init_camera(self):
        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            QMessageBox.critical(self, "错误", "无法打开摄像头")
            return

        # 设置摄像头分辨率
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

        # 定时器用于更新摄像头画面
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_frame)
        self.timer.start(30)  # 约30fps

    def update_frame(self):
        ret, frame = self.cap.read()
        if ret:
            self.current_frame = frame.copy()

            # 如果处于采集模式,显示已捕获的图像
            if self.is_capturing and self.captured_image is not None:
                display_image = self.captured_image.copy()

                # 如果已经选择了ROI,绘制矩形框
                if hasattr(self.image_label, 'roi') and self.image_label.roi:
                    x, y, w, h = self.image_label.roi
                    cv2.rectangle(display_image, (x, y), (x + w, y + h), (0, 255, 0), 2)

                self.image_label.set_image(display_image)
            else:
                self.image_label.set_image(frame)

    def select_save_path(self):
        path = QFileDialog.getExistingDirectory(self, "选择保存路径")
        if path:
            self.save_path = path
            self.path_label.setText(f"保存路径: {path}")
            self.status_label.setText("已选择保存路径")

    def capture_image(self):
        if not self.save_path:
            QMessageBox.warning(self, "警告", "请先选择保存路径")
            return

        if self.current_frame is None:
            QMessageBox.warning(self, "警告", "无法获取摄像头画面")
            return

        # 保存当前帧
        self.captured_image = self.current_frame.copy()
        self.is_capturing = True
        self.status_label.setText("请框选脸部区域")

        # 提示用户框选脸部
        QMessageBox.information(self, "提示", "请用鼠标在图像上框选脸部区域")

    def save_roi_info(self, roi):
        if not self.is_capturing or self.captured_image is None:
            return

        # 保存图像
        self.image_count += 1
        image_filename = f"face_{self.image_count:04d}.jpg"
        image_path = os.path.join(self.save_path, image_filename)
        cv2.imwrite(image_path, self.captured_image)

        # 保存ROI信息到txt文件
        txt_filename = f"face_{self.image_count:04d}.txt"
        txt_path = os.path.join(self.save_path, txt_filename)

        x, y, w, h = roi
        with open(txt_path, 'w') as f:
            f.write(f"图像序号: {self.image_count}\n")
            f.write(f"脸部区域: x={x}, y={y}, width={w}, height={h}\n")
            f.write(f"对应图像: {image_filename}\n")

        self.status_label.setText(f"已保存第 {self.image_count} 张图像和标注信息")

        # 重置状态
        self.is_capturing = False
        self.captured_image = None
        self.image_label.roi = None

        # 显示成功消息
        QMessageBox.information(self, "成功", f"图像和标注信息已保存\n图像: {image_filename}\n标注: {txt_filename}")

    def closeEvent(self, event):
        # 释放摄像头资源
        if hasattr(self, 'cap'):
            self.cap.release()
        event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = FaceCaptureApp()
    window.show()
    sys.exit(app.exec())

六、实验总结

通过本次实验,我们成功地将OpenCV与PyQt6结合,实现了一个功能完整的图像采集与标注工具。在实验过程中,我们掌握了以下技能:

  • PyQt6界面设计与事件处理;

  • OpenCV摄像头操作与图像处理;

  • 鼠标交互与坐标转换;

  • 文件读写与数据记录。

本工具可扩展用于人脸识别、表情分析等后续图像处理任务,具有良好的实用价值。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值