From 9a015f2358d89fec340cb3ef3c9f0e8440c935ff Mon Sep 17 00:00:00 2001 From: Kegongteng Date: Sat, 29 Nov 2025 13:17:04 +0800 Subject: [PATCH] [feat] 3.4 --- 3-4.py | 339 +++++++++++++++++++++++++++++++ 3-control.py | 551 +++++++++++++++++++++++++++++++++++++++++++++++++++ icon.ico | Bin 7802 -> 0 bytes readme.md | 9 +- text.ico | Bin 0 -> 172175 bytes 5 files changed, 898 insertions(+), 1 deletion(-) create mode 100644 3-4.py create mode 100644 3-control.py delete mode 100644 icon.ico create mode 100644 text.ico diff --git a/3-4.py b/3-4.py new file mode 100644 index 0000000..4f605a3 --- /dev/null +++ b/3-4.py @@ -0,0 +1,339 @@ +import sys +import os +import random +import cv2 +import numpy as np +from PySide6.QtCore import QTimer, Qt +from PySide6.QtGui import QImage, QPixmap +from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QSlider, QVBoxLayout, QProgressBar + + +# ====================================================== +# 加载动画界面 +# ====================================================== +class LoadingScreen(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("Face Random Selector") + self.setFixedSize(300, 150) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + self.setStyleSheet(""" + background-color: #1E90FF; + border-radius: 10px; + """) + + # 创建布局 + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignCenter) + layout.setSpacing(15) + + # 加载提示文字 + self.loading_label = QLabel("正在加载模型...") + self.loading_label.setAlignment(Qt.AlignCenter) + self.loading_label.setStyleSheet(""" + color: white; + font-size: 18px; + font-weight: bold; + """) + layout.addWidget(self.loading_label) + + # 进度条 - 设置为不确定模式,显示加载动画 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # 设置为0,0表示不确定模式 + self.progress_bar.setFixedHeight(10) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid rgba(255, 255, 255, 100); + border-radius: 5px; + background-color: rgba(255, 255, 255, 50); + } + QProgressBar::chunk { + background-color: white; + border-radius: 4px; + } + """) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) + + +# ====================================================== +# YuNet 模型路径 +# ====================================================== +def get_yunet_model_path(): + base_dir = os.path.dirname(os.path.abspath(__file__)) + model_path = os.path.join(base_dir, "model", "face_detection_yunet_2023mar.onnx") + + if not os.path.exists(model_path): + raise FileNotFoundError(f"YuNet 模型不存在: {model_path}") + return model_path + + +# ====================================================== +# 主窗口 +# ====================================================== +class FaceRandomApp(QWidget): + def __init__(self): + super().__init__() + + # 先显示加载页面 + self.loading_screen = LoadingScreen() + self.loading_screen.show() + + # 强制立即处理GUI事件,确保加载页面能立即显示 + QApplication.processEvents() + + # 延迟初始化主界面,让加载页面先显示 + QTimer.singleShot(100, self.initialize_app) + + def initialize_app(self): + """初始化应用程序""" + # 状态 + self.state = "normal" + self.selected_face_index = -1 + self.static_frame = None + self.faces_snapshot = [] + + # 参数 + self.detection_confidence = 0.6 + self.faces = [] + + # 摄像头 + self.cap = cv2.VideoCapture(0) + self.cap.set(3, 1280) + self.cap.set(4, 720) + + # YuNet 加载 + model_path = get_yunet_model_path() + self.detector = cv2.FaceDetectorYN.create( + model_path, + "", + (640, 480), + score_threshold=self.detection_confidence, + nms_threshold=0.3, + top_k=5000 + ) + + # UI + self.setup_ui() + + # 关闭加载界面并显示主窗口 + self.loading_screen.close() + self.show() + + # Timer + self.timer = QTimer() + self.timer.timeout.connect(self.update_frame) + self.timer.start(30) + + # ====================================================== + # UI + # ====================================================== + def setup_ui(self): + self.setWindowTitle("Face Random Selector") + self.resize(1280, 720) + + self.video_label = QLabel(self) + self.video_label.setGeometry(0, 0, self.width(), self.height()) + self.video_label.setStyleSheet("background: black;") + + # 按钮 - 简约黑色半透明样式,无边框 + self.btn = QPushButton("随机", self) + self.btn.setFixedSize(140, 55) + self.btn.move(20, self.height() - 75) + self.btn.clicked.connect(self.on_random_clicked) + self.btn.setStyleSheet(""" + QPushButton { + background-color: rgba(0, 0, 0, 180); + color: white; + font-size: 20px; + font-weight: bold; + border: none; + border-radius: 8px; + } + QPushButton:hover { + background-color: rgba(30, 30, 30, 200); + } + QPushButton:pressed { + background-color: rgba(50, 50, 50, 220); + } + """) + + # 置信度滑条 - 简约半透明样式 + self.slider = QSlider(Qt.Horizontal, self) + self.slider.setRange(30, 90) + self.slider.setValue(int(self.detection_confidence * 100)) + self.slider.setFixedWidth(200) + self.slider.move(120, 20) + self.slider.valueChanged.connect(self.on_confidence_change) + self.slider.setStyleSheet(""" + QSlider::groove:horizontal { + background: rgba(0, 0, 0, 120); + height: 4px; + border-radius: 2px; + } + QSlider::handle:horizontal { + background: white; + width: 14px; + height: 14px; + border-radius: 7px; + margin: -5px 0; + } + """) + + # 置信度标签 - 简约白色文字 + self.conf_label = QLabel("置信度", self) + self.conf_label.move(20, 20) + self.conf_label.setStyleSheet(""" + color: white; + font-size: 16px; + font-weight: bold; + background: transparent; + """) + + # ====================================================== + def resizeEvent(self, event): + self.video_label.setGeometry(0, 0, self.width(), self.height()) + self.btn.move(20, self.height() - 75) + super().resizeEvent(event) + + # ====================================================== + # 置信度 + # ====================================================== + def on_confidence_change(self, value): + self.detection_confidence = value / 100.0 + self.detector.setScoreThreshold(self.detection_confidence) + + # ====================================================== + # 随机按钮 + # ====================================================== + def on_random_clicked(self): + if self.state == "normal": + self.state = "random" + self.btn.setText("重置") + + if len(self.faces) > 0: + self.selected_face_index = random.randint(0, len(self.faces) - 1) + + # 捕获静态帧 + ret, img = self.cap.read() + img = cv2.flip(img, 1) + img = self.resize_cover(img, self.video_label.width(), self.video_label.height()) + + self.static_frame = img.copy() + self.faces_snapshot = self.faces.copy() + + else: + self.state = "normal" + self.btn.setText("随机") + self.selected_face_index = -1 + self.static_frame = None + + # ====================================================== + # 等比例覆盖(无黑边) + # ====================================================== + def resize_cover(self, img, target_w, target_h): + """等比例缩放 + 居中裁切 = 无黑边""" + h, w = img.shape[:2] + + scale = max(target_w / w, target_h / h) + new_w = int(w * scale) + new_h = int(h * scale) + + resized = cv2.resize(img, (new_w, new_h)) + + # 居中裁切 + x_start = (new_w - target_w) // 2 + y_start = (new_h - target_h) // 2 + + cropped = resized[y_start:y_start + target_h, x_start:x_start + target_w] + return cropped + + # ====================================================== + # 绘制人脸框和置信度 - 优化样式 + # ====================================================== + def draw_face_with_confidence(self, img, face, color, thickness=2): + """绘制人脸框和置信度""" + x, y, w, h = map(int, face[:4]) + score = face[4] # 置信度分数,范围0-1 + + # 绘制人脸框 + cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness) + + # 绘制置信度文本 + confidence_text = f"{score:.2f}" + text_y = max(y - 10, 20) # 确保文本不超出图像顶部 + + # 绘制半透明文本背景 + text_size = cv2.getTextSize(confidence_text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0] + + # 创建半透明覆盖层 + overlay = img.copy() + cv2.rectangle(overlay, (x, text_y - text_size[1] - 5), + (x + text_size[0] + 10, text_y + 5), color, -1) + + # 应用半透明效果 + alpha = 0.7 # 透明度 + cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0, img) + + # 绘制文本 + cv2.putText(img, confidence_text, (x + 5, text_y), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) + + # ====================================================== + # 主循环 + # ====================================================== + def update_frame(self): + ret, img = self.cap.read() + if not ret: + return + + img = cv2.flip(img, 1) + img = self.resize_cover(img, self.video_label.width(), self.video_label.height()) + + if self.state == "normal": + # 设置 YuNet 输入大小 + self.detector.setInputSize((img.shape[1], img.shape[0])) + + # 检测 + _, detected = self.detector.detect(img) + self.faces = detected if detected is not None else [] + + # 画框和置信度 + for face in self.faces: + self.draw_face_with_confidence(img, face, (0, 255, 0), 2) + + else: # random 模式 + img = self.static_frame.copy() + + for i, face in enumerate(self.faces_snapshot): + color = (0, 0, 255) if i == self.selected_face_index else (0, 255, 0) + thickness = 3 if i == self.selected_face_index else 2 + self.draw_face_with_confidence(img, face, color, thickness) + + self.display(img) + + # ====================================================== + def display(self, img): + rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + h, w, c = rgb.shape + qimg = QImage(rgb.data, w, h, c * w, QImage.Format_RGB888) + self.video_label.setPixmap(QPixmap.fromImage(qimg)) + + # ====================================================== + def closeEvent(self, event): + if self.cap.isOpened(): + self.cap.release() + event.accept() + + +# ====================================================== +def main(): + app = QApplication(sys.argv) + window = FaceRandomApp() + # 注意:这里不再调用 window.show(),因为 FaceRandomApp 内部会处理显示逻辑 + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/3-control.py b/3-control.py new file mode 100644 index 0000000..fb87a1a --- /dev/null +++ b/3-control.py @@ -0,0 +1,551 @@ +import sys +import random +import cv2 +import numpy as np +from cvzone.FaceDetectionModule import FaceDetector +from PySide6.QtCore import QTimer, Qt +from PySide6.QtGui import QImage, QPixmap +from PySide6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, + QVBoxLayout, QProgressBar, QHBoxLayout, + QSlider, QGroupBox, QDoubleSpinBox) + + +class LoadingScreen(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("加载中...") + self.setFixedSize(300, 150) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + self.setStyleSheet("background-color: #1E90FF;") # 蓝色背景 + + # 创建布局 + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignCenter) + layout.setSpacing(15) + + # 加载提示文字 + self.loading_label = QLabel("人脸模型加载中") + self.loading_label.setAlignment(Qt.AlignCenter) + self.loading_label.setStyleSheet("color: white; font-size: 18px; font-weight: bold;") + layout.addWidget(self.loading_label) + + # 进度条 - 设置为不确定模式,显示加载动画 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # 设置为0,0表示不确定模式 + self.progress_bar.setFixedHeight(10) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid rgba(255, 255, 255, 100); + border-radius: 5px; + background-color: rgba(255, 255, 255, 50); + } + QProgressBar::chunk { + background-color: white; + border-radius: 4px; + } + """) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) + + +class FaceRandomApp(QWidget): + def __init__(self): + super().__init__() + + # 先显示加载页面 + self.loading_screen = LoadingScreen() + self.loading_screen.show() + + # 强制立即处理GUI事件,确保加载页面能立即显示 + QApplication.processEvents() + + # 延迟初始化主界面,让加载页面先显示 + QTimer.singleShot(100, self.initialize_app) + + def initialize_app(self): + # 设置窗口标题 + self.setWindowTitle("Face Random Selector") + + # 状态:normal → random + self.state = "normal" + self.selected_face_index = -1 + self.static_frame = None # 保存随机状态下的静态画面 + self.all_faces_snapshot = [] # 保存所有人脸信息快照 + + # 检测参数 + self.detection_confidence = 0.15 # 降低置信度阈值以提高检测率 + self.model_selection = 1 # 长距离模型 + self.control_panel_visible = False # 控制面板显示状态 + self.multi_scale_enabled = True # 启用多尺度检测 + + # ---------------- 摄像头 ---------------- + self.cap = cv2.VideoCapture(0) + + # 设置更高分辨率以捕捉更多细节 + self.cap.set(3, 1920) # 宽度 + self.cap.set(4, 1080) # 高度 + + # 设置窗口初始大小 + self.resize(1280, 720) + + # 更新加载页面文字,提示正在进行人脸模型初始化 + self.loading_screen.loading_label.setText("正在初始化人脸检测模型...") + QApplication.processEvents() + + # 使用优化的参数 + try: + self.detector = FaceDetector( + minDetectionCon=self.detection_confidence, + modelSelection=self.model_selection + ) + self.loading_screen.loading_label.setText("人脸模型加载完成!") + except Exception as e: + print(f"初始化人脸检测器时出错: {e}") + # 如果初始化失败,使用一个空的检测器 + self.detector = None + self.loading_screen.loading_label.setText("人脸模型加载失败,使用基础模式") + + # ---------------- 创建界面 ---------------- + self.setup_ui() + + self.faces = [] + + # 模拟加载完成 + QTimer.singleShot(1500, self.finish_loading) + + def finish_loading(self): + # 关闭加载页面 + self.loading_screen.close() + # 显示主窗口 + self.show() + + # 启动定时器更新视频帧 + self.timer = QTimer() + self.timer.timeout.connect(self.update_frame) + self.timer.start(30) + + def setup_ui(self): + # 视频标签 - 填充整个窗口 + self.video_label = QLabel(self) + self.video_label.setStyleSheet("background:#000;") + self.video_label.setGeometry(0, 0, self.width(), self.height()) + + # 随机按钮 - 叠加在视频上 + self.btn = QPushButton("随机", self) + self.btn.setFixedSize(140, 55) + self.btn.setStyleSheet(""" + QPushButton { + background-color: rgba(0, 0, 0, 120); + color: white; + font-size: 20px; + padding: 10px 20px; + border-radius: 8px; + } + QPushButton:hover { + background-color: rgba(30, 30, 30, 180); + } + """) + self.btn.clicked.connect(self.button_clicked) + + # 控制按钮 - 新增 + self.control_btn = QPushButton("控制", self) + self.control_btn.setFixedSize(80, 40) + self.control_btn.setStyleSheet(""" + QPushButton { + background-color: rgba(0, 0, 0, 120); + color: white; + font-size: 16px; + padding: 5px 10px; + border-radius: 6px; + } + QPushButton:hover { + background-color: rgba(30, 30, 30, 180); + } + """) + self.control_btn.clicked.connect(self.toggle_control_panel) + + # 控制面板 - 新增 + self.control_panel = QWidget(self) + self.control_panel.setFixedSize(300, 180) # 减小高度 + self.control_panel.setStyleSheet(""" + QWidget { + background-color: rgba(0, 0, 0, 180); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 100); + } + """) + self.control_panel.hide() # 初始隐藏 + + # 控制面板布局 + control_layout = QVBoxLayout(self.control_panel) + control_layout.setContentsMargins(15, 15, 15, 15) + control_layout.setSpacing(10) + + # 置信度控制 + confidence_layout = QHBoxLayout() + confidence_label = QLabel("置信度:") + confidence_label.setStyleSheet("color: white; font-size: 14px;") + self.confidence_slider = QSlider(Qt.Horizontal) + self.confidence_slider.setRange(5, 50) # 调整范围以适应教室环境 + self.confidence_slider.setValue(15) # 默认0.15 + self.confidence_slider.valueChanged.connect(self.on_confidence_changed) + self.confidence_slider.setStyleSheet(""" + QSlider::groove:horizontal { + background: rgba(255, 255, 255, 100); + height: 6px; + border-radius: 3px; + } + QSlider::handle:horizontal { + background: white; + width: 16px; + height: 16px; + border-radius: 8px; + margin: -5px 0; + } + """) + + self.confidence_value = QLabel("0.15") + self.confidence_value.setStyleSheet("color: white; font-size: 14px; min-width: 40px;") + + confidence_layout.addWidget(confidence_label) + confidence_layout.addWidget(self.confidence_slider) + confidence_layout.addWidget(self.confidence_value) + control_layout.addLayout(confidence_layout) + + # 多尺度检测选项 + multiscale_layout = QHBoxLayout() + multiscale_label = QLabel("多尺度检测:") + multiscale_label.setStyleSheet("color: white; font-size: 14px;") + + self.multiscale_check = QPushButton("启用") + self.multiscale_check.setCheckable(True) + self.multiscale_check.setChecked(True) + self.multiscale_check.setFixedHeight(30) + self.multiscale_check.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 255, 255, 50); + color: white; + border: 1px solid rgba(255, 255, 255, 100); + border-radius: 4px; + font-size: 12px; + } + QPushButton:checked { + background-color: rgba(30, 144, 255, 200); + border: 1px solid rgba(255, 255, 255, 200); + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 80); + } + """) + self.multiscale_check.clicked.connect(self.on_multiscale_changed) + + multiscale_layout.addWidget(multiscale_label) + multiscale_layout.addWidget(self.multiscale_check) + multiscale_layout.addStretch() + control_layout.addLayout(multiscale_layout) + + # 模型选择 + model_layout = QHBoxLayout() + model_label = QLabel("检测模型:") + model_label.setStyleSheet("color: white; font-size: 14px;") + + self.model_short_btn = QPushButton("短距离") + self.model_long_btn = QPushButton("长距离") + + for btn in [self.model_short_btn, self.model_long_btn]: + btn.setCheckable(True) + btn.setFixedHeight(30) + btn.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 255, 255, 50); + color: white; + border: 1px solid rgba(255, 255, 255, 100); + border-radius: 4px; + font-size: 12px; + } + QPushButton:checked { + background-color: rgba(30, 144, 255, 200); + border: 1px solid rgba(255, 255, 255, 200); + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 80); + } + """) + + self.model_short_btn.clicked.connect(lambda: self.on_model_changed(0)) + self.model_long_btn.clicked.connect(lambda: self.on_model_changed(1)) + + # 默认选择长距离模型 + self.model_long_btn.setChecked(True) + + model_layout.addWidget(model_label) + model_layout.addWidget(self.model_short_btn) + model_layout.addWidget(self.model_long_btn) + model_layout.addStretch() + control_layout.addLayout(model_layout) + + control_layout.addStretch() + + # 初始按钮位置 + self.update_button_position() + + def toggle_control_panel(self): + """切换控制面板显示状态""" + self.control_panel_visible = not self.control_panel_visible + if self.control_panel_visible: + self.control_panel.show() + else: + self.control_panel.hide() + + def on_confidence_changed(self, value): + """置信度滑块值改变""" + confidence = value / 100.0 + self.confidence_value.setText(f"{confidence:.2f}") + self.detection_confidence = confidence + if self.detector: + self.detector.minDetectionCon = confidence + + def on_multiscale_changed(self): + """多尺度检测选项改变""" + self.multi_scale_enabled = self.multiscale_check.isChecked() + + def on_model_changed(self, model_selection): + """模型选择改变""" + self.model_selection = model_selection + if model_selection == 0: + self.model_short_btn.setChecked(True) + self.model_long_btn.setChecked(False) + else: + self.model_short_btn.setChecked(False) + self.model_long_btn.setChecked(True) + + # 重新初始化检测器 + try: + self.detector = FaceDetector( + minDetectionCon=self.detection_confidence, + modelSelection=model_selection + ) + except Exception as e: + print(f"重新初始化检测器失败: {e}") + + def multi_scale_detection(self, img): + """多尺度人脸检测,提高小尺寸人脸的检测率""" + if not self.multi_scale_enabled or self.detector is None: + return self.detector.findFaces(img, draw=False) if self.detector else (img, []) + + faces = [] + scales = [1.0, 0.75, 0.5] # 多尺度因子 + + for scale in scales: + # 缩放图像 + if scale != 1.0: + h, w = img.shape[:2] + new_w, new_h = int(w * scale), int(h * scale) + scaled_img = cv2.resize(img, (new_w, new_h)) + else: + scaled_img = img.copy() + + # 在当前尺度下检测人脸 + _, scaled_faces = self.detector.findFaces(scaled_img, draw=False) + + # 将检测到的人脸坐标转换回原始尺度 + if scaled_faces: + for face in scaled_faces: + bbox = face["bbox"] + # 调整边界框坐标 + face["bbox"] = [ + int(bbox[0] / scale), + int(bbox[1] / scale), + int(bbox[2] / scale), + int(bbox[3] / scale) + ] + # 只添加新检测到的人脸(避免重复) + if not self.is_duplicate_face(face, faces): + faces.append(face) + + return img, faces + + def is_duplicate_face(self, new_face, existing_faces, threshold=0.5): + """检查是否重复检测到同一个人脸""" + if not existing_faces: + return False + + new_x, new_y, new_w, new_h = new_face["bbox"] + new_center = (new_x + new_w/2, new_y + new_h/2) + + for face in existing_faces: + x, y, w, h = face["bbox"] + center = (x + w/2, y + h/2) + + # 计算两个边界框中心点的距离 + distance = np.sqrt((new_center[0] - center[0])**2 + (new_center[1] - center[1])**2) + + # 如果距离小于阈值,认为是同一个人脸 + if distance < max(new_w, new_h) * threshold: + return True + + return False + + def resizeEvent(self, event): + """窗口大小改变时自动调整视频和按钮位置""" + self.video_label.setGeometry(0, 0, self.width(), self.height()) + self.update_button_position() + super().resizeEvent(event) + + def update_button_position(self): + """更新按钮位置""" + btn_w = 140 + btn_h = 55 + margin = 20 + + # 随机按钮位置(左下角) + self.btn.setGeometry( + margin, + self.height() - btn_h - margin, + btn_w, + btn_h + ) + + # 控制按钮位置(左上角) + control_btn_w = 80 + control_btn_h = 40 + self.control_btn.setGeometry( + margin, + margin, + control_btn_w, + control_btn_h + ) + + # 控制面板位置(控制按钮下方) + panel_width = 300 + panel_height = 180 + self.control_panel.setGeometry( + margin, + margin + control_btn_h + 10, + panel_width, + panel_height + ) + + # --------------------------------------------------------- + # 逻辑按钮:随机 ↔ 重置 + # --------------------------------------------------------- + def button_clicked(self): + if self.state == "normal": + # 切换到随机状态 + self.state = "random" + self.btn.setText("重置") + + # 随机选择一个人脸 + if len(self.faces) > 0: + self.selected_face_index = random.randint(0, len(self.faces) - 1) + + # 保存当前帧作为静态画面 + ret, img = self.cap.read() + if ret: + img = cv2.flip(img, 1) + # 调整图像大小以适应窗口 + img = cv2.resize(img, (self.video_label.width(), self.video_label.height())) + self.static_frame = img.copy() + self.all_faces_snapshot = self.faces.copy() + else: # self.state == "random" + # 切换回初始状态 + self.state = "normal" + self.selected_face_index = -1 + self.static_frame = None + self.all_faces_snapshot = [] + self.btn.setText("随机") + + # --------------------------------------------------------- + # 刷视频帧 + # --------------------------------------------------------- + def update_frame(self): + # ============================ + # 状态:normal(检测所有人脸并显示绿框) + # ============================ + if self.state == "normal": + ret, img = self.cap.read() + if not ret: + return + + img = cv2.flip(img, 1) + + # 调整图像大小以适应窗口 + img = cv2.resize(img, (self.video_label.width(), self.video_label.height())) + + # 检测人脸 - 使用多尺度检测 + if self.detector is not None: + if self.multi_scale_enabled: + img, self.faces = self.multi_scale_detection(img) + else: + img, self.faces = self.detector.findFaces(img, draw=False) + self.faces = self.faces if self.faces else [] + else: + self.faces = [] + + # 绘制所有人脸为绿色 + for face in self.faces: + x, y, w, h = face["bbox"] + score = face["score"][0] + # 根据置信度调整边框粗细 + thickness = max(1, int(3 * score)) + cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), thickness) + cv2.putText(img, f"{score:.2f}", (x, y - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) + + # 显示图像 + self.display_image(img) + + # ============================ + # 状态:random(显示静态画面,选中的人脸为红色,其他为绿色) + # ============================ + elif self.state == "random" and self.static_frame is not None: + img = self.static_frame.copy() + + # 绘制所有人脸,选中的为红色,其他为绿色 + for i, face in enumerate(self.all_faces_snapshot): + x, y, w, h = face["bbox"] + score = face["score"][0] + + if i == self.selected_face_index: + color = (0, 0, 255) # 红色 + thickness = 4 + else: + color = (0, 255, 0) # 绿色 + thickness = 2 + + cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness) + cv2.putText(img, f"{score:.2f}", (x, y - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) + + # 显示图像 + self.display_image(img) + + # --------------------------------------------------------- + # 显示图像到 QLabel + # --------------------------------------------------------- + def display_image(self, img): + rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + h, w, ch = rgb.shape + bytes_per_line = ch * w + qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888) + self.video_label.setPixmap(QPixmap.fromImage(qimg)) + + # --------------------------------------------------------- + def closeEvent(self, event): + if self.cap.isOpened(): + self.cap.release() + event.accept() + + +def main(): + app = QApplication(sys.argv) + + # 直接创建主应用窗口,它内部会处理加载页面 + window = FaceRandomApp() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/icon.ico b/icon.ico deleted file mode 100644 index 30f1d8e2067018c452941e36bbe6f3be734de4ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7802 zcmeHMX-^Yj5MKX)AN@o?KPVs=(Wn?TfFT|zhmmSRd4qv^CaI zPQoEdiwvRDG2wLWd=|;-@6qt!;6H^-Qi$2qtI^=2w5K9kD!N6pM8AKtvCg0v)Zx3(ZZzOFvK?@==mZ*kx`IZRDw%!??` zZ9Er=^m2)M0*2ks+Ggy)CE#FbZsv^6m?z%`2d4bc!wn)U#&adD+p<^tIPZ)ndZC z$NGw*d^J5T(9_U^O(p5kp{(a?^FP$z_mldnB!l4%KGPI%8~U)R1U*0>EM9eYvOLzt z8RKJPI(vzs&yaOwKY8=~tvah1>eE#A=!dAG^86fwwx}tWQs0-)2KktaaaQ~R7H^W+ zpY=~&Gl4dL?T;TY`=&e9+HMLGUH+hd7^KA&lxabp@4vZfPjF2 UfPjF2fI$dM|F2W{HZB4`0Bzx+vH$=8 diff --git a/readme.md b/readme.md index f23851d..1b2cc43 100644 --- a/readme.md +++ b/readme.md @@ -4,8 +4,13 @@ FaceRandomSelector 是一款人脸随机选择工具。软件采用opencv的人脸检测算法,能够快速准确地识别摄像头中的人脸,并提供高效的随机选择功能,帮助教师轻松实现课堂互动。 ## 2.打包指令 -` nuitka --standalone --include-package=cv2 --enable-plugin=pyside6 --windows-console-mode=disable --windows-icon-from-ico=./icon.ico --include-data-dir=F:\python310\lib\site-packages\cv2\data=cv2\data --output-dir=dist32 --remove-output 3-2.py ` +2.4版本之前: +` nuitka --standalone --include-package=cv2 --enable-plugin=pyside6 --windows-console-mode=disable --windows-icon-from-ico=./text.ico --include-data-dir=F:\python310\lib\site-packages\cv2\data=cv2\data --output-dir=dist3c --remove-output 3-control.py ` + +2.4版本之后: + +` nuitka --standalone --include-package=cv2 --include-package=numpy --enable-plugin=pyside6 --windows-console-mode=disable --windows-icon-from-ico=./text.ico --include-data-dir=F:\python310\lib\site-packages\cv2\data=cv2\data --include-data-files=model/face_detection_yunet_2023mar.onnx=model/face_detection_yunet_2023mar.onnx --output-dir=dist34 --remove-output 3-4.py` ## 3.版本说明 1. 1.py 2.py:非稳定版本 @@ -13,4 +18,6 @@ FaceRandomSelector 是一款人脸随机选择工具。软件采用opencv的人 3. 3-2.py:解决3-1.py的相关问题 4. 3-3.py:添加加载进度条 5. 3-test.py:由3-3.py改编 给出具体的[人脸识别模型无法获取]的相关报错 +6. 3-control.py:由3-3.py改编 用户可以修改相关参数 +7. 3-4.py:使用YuNet模型 加强对小人脸识别准确度 > 软件务必保存在纯英文路径中! \ No newline at end of file diff --git a/text.ico b/text.ico new file mode 100644 index 0000000000000000000000000000000000000000..c0cc6e8b39b75206e7dea4a2a14fa82338183fa0 GIT binary patch literal 172175 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 literal 0 HcmV?d00001