当前位置: 首页 > article >正文

从头开始开发基于虹软SDK的人脸识别考勤系统(python+RTSP开源)(四)

终于抽出时间来了,代码继续。

一、摄像头部分

摄像头服务的初始化

# ==================== 摄像头服务 ====================
class CameraWorker(QThread):
    frame_ready = pyqtSignal(np.ndarray)
    error_occurred = pyqtSignal(str)

    def __init__(self, source=0):
        super().__init__()
        self.source = source
        self.running = True
        # 确保 camera_mutex 被正确初始化
        self.camera_mutex = QMutex()

    def run(self):

        self.running = True  # 重置运行状态
        cap = cv2.VideoCapture(self.source)
        if not cap.isOpened():
            self.error_occurred.emit(f"无法打开摄像头:{self.source}")
            print(f"无法打开摄像头:{self.source}")
            return

        try:
            while self.running:
                # 加锁
                self.camera_mutex.lock()
                ret, frame = cap.read()
                # 解锁
                self.camera_mutex.unlock()
                if ret:
                    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                    self.frame_ready.emit(rgb_frame)
                else:
                    self.error_occurred.emit("视频流中断")
                    print("视频流中断")
                    break
        finally:
            # 加锁释放资源
            self.camera_mutex.lock()
            cap.release()
            # 解锁
            self.camera_mutex.unlock()

    def stop(self):
        self.running = False
        self.wait(1000)
        if hasattr(self, 'cap'):
            # 加锁释放资源
            self.camera_mutex.lock()
            self.cap.release()
            # 解锁
            self.camera_mutex.unlock()

我这次是加上了RTSP协议的监控摄像头,在配置界面上可以手工添加和启用,其实还没有写完整,应该选择启用RTSP后,自动重启程序。

def _init_camera(self):
        if self.camera_worker:
            self.camera_worker.stop()
            self.camera_worker.quit()
            self.camera_worker.wait()

        cursor = self.db.conn.cursor()
        cursor.execute('SELECT camera_type, rtsp_url, is_enabled, is_connected FROM camera_config WHERE id = 1')
        result = cursor.fetchone()
        if result:
            camera_type, rtsp_url, is_enabled, is_connected = result
            if camera_type == 'rtsp' and is_enabled and is_connected:
                self.camera_worker = CameraWorker(rtsp_url)
            else:
                self.camera_worker = CameraWorker(0)
        else:
            self.camera_worker = CameraWorker(0)

        self.camera_worker.frame_ready.connect(self.update_frame)
        self.camera_worker.error_occurred.connect(self.show_camera_error)
        if not self.camera_worker.isRunning():
            self.camera_worker.start()
def show_camera_error(self, error_msg):
        reply = QMessageBox.question(
            self,
            "摄像头错误",
            f"{error_msg}\n是否尝试重新连接?",
            QMessageBox.Yes | QMessageBox.No
        )

        if reply == QMessageBox.Yes:
            # 停止当前摄像头线程
            if hasattr(self, 'camera_worker'):
                self.camera_worker.stop()
                self.camera_worker.quit()
                self.camera_worker.wait()
            self.init_camera()  # 重新初始化摄像头
        else:
            self.camera_worker.stop()
            self.video_label.setText("摄像头已禁用")
def init_ui(self):
        self.setWindowTitle("摄像头配置")
        layout = QVBoxLayout()

        form = QFormLayout()
        self.camera_type_combo = QComboBox()
        self.camera_type_combo.addItems(['USB', 'RTSP'])
        self.rtsp_url_input = QLineEdit()
        self.enable_rtsp_checkbox = QCheckBox("启用RTSP摄像头")
        self.test_connection_button = QPushButton("测试连接")
        self.test_connection_button.clicked.connect(self.test_rtsp_connection)

        form.addRow("摄像头类型:", self.camera_type_combo)
        form.addRow("RTSP URL:", self.rtsp_url_input)
        form.addRow("", self.enable_rtsp_checkbox)
        form.addRow("", self.test_connection_button)

        btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        btn_box.accepted.connect(self.save_config)
        btn_box.rejected.connect(self.reject)

        layout.addLayout(form)
        layout.addWidget(btn_box)
        self.setLayout(layout)
        self.load_config()

读取摄像头配置

def load_config(self):
        cursor = self.db.conn.cursor()
        cursor.execute('SELECT camera_type, rtsp_url, is_enabled, is_connected FROM camera_config WHERE id = 1')
        result = cursor.fetchone()
        if result:
            camera_type, rtsp_url, is_enabled, is_connected = result
            self.camera_type_combo.setCurrentText('RTSP' if camera_type == 'rtsp' else 'USB')
            self.rtsp_url_input.setText(rtsp_url)
            self.enable_rtsp_checkbox.setChecked(is_enabled)
            self.test_connection_button.setEnabled(is_connected)

保存摄像头配置并启用

def save_config(self):
        camera_type = 'rtsp' if self.camera_type_combo.currentText() == 'RTSP' else 'usb'
        rtsp_url = self.rtsp_url_input.text() if camera_type == 'rtsp' else None
        is_enabled = self.enable_rtsp_checkbox.isChecked()
        cursor = self.db.conn.cursor()
        cursor.execute('''
            INSERT OR REPLACE INTO camera_config (id, camera_type, rtsp_url, is_enabled, is_connected)
            VALUES (1,?,?,?,?)
        ''', (camera_type, rtsp_url, is_enabled, self.test_connection_button.isEnabled()))
        self.db.conn.commit()
        self.init_camera()  # 调用初始化摄像头的方法
        self.accept()

        # 停止当前摄像头线程
        if self.camera_worker:
            self.camera_worker.stop()
            self.camera_worker.quit()
            self.camera_worker.wait()

        # 重新初始化摄像头
        self.init_camera()

        self.accept()

在RTSP摄像头界面增加测试连接功能

def test_rtsp_connection(self):
        import cv2
        rtsp_url = self.rtsp_url_input.text()
        cap = cv2.VideoCapture(rtsp_url)
        if cap.isOpened():
            cap.release()
            self.test_connection_button.setEnabled(True)
            QMessageBox.information(self, "连接成功", "RTSP摄像头连接可用")
        else:
            self.test_connection_button.setEnabled(False)
            QMessageBox.warning(self, "连接失败", "RTSP摄像头连接不可用")

    def init_camera(self):
        self.main_window._init_camera()

二、界面部分

界面部分上面也都已经介绍了,使用的是PyQt5,直接上代码吧。

1、主界面部分:

def init_ui(self):
        self.setWindowTitle("人脸识别考勤(FaceAttV2)DEV-半熟的皮皮虾")
        self.setWindowIcon(QIcon("icon.png"))
        self.resize(1280, 720)
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # 主布局,使用垂直布局
        main_layout = QVBoxLayout(central_widget)

        # 上半部分布局,使用水平布局,包含摄像头显示和考勤列表
        upper_layout = QHBoxLayout()

        # 左侧摄像头显示区域
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)

        self.video_label = QLabel()
        self.video_label.setAlignment(Qt.AlignCenter)
        self.video_label.setScaledContents(True)  # 允许按比例缩放内容
        left_layout.addWidget(self.video_label, 85)

        upper_layout.addWidget(left_widget, 7)  # 左侧占 7 份

        # 右侧已考勤人员列表区域
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setAlignment(Qt.AlignCenter)  # 设置右侧布局居中对齐

        # 添加已考勤列表标题
        self.title_label = QLabel("已考勤列表")
        self.title_label.setObjectName("titleLabel")  # 设置对象名称
        self.title_label.setFont(QFont("微软雅黑", 19, QFont.Bold))  # 设置标题字体和加粗
        self.title_label.setAlignment(Qt.AlignCenter)  # 标题居中显示
        right_layout.addWidget(self.title_label)

        # 添加伸缩空间,将考勤列表推到中间
        right_layout.addStretch()

        self.attendance_label = QLabel()
        self.attendance_label.setObjectName("attendanceLabel")  # 设置对象名称
        self.attendance_label.setFont(QFont("宋体", 15, QFont.Bold))
        right_layout.addWidget(self.attendance_label, alignment=Qt.AlignCenter)

        # 添加伸缩空间,将考勤列表推到中间
        right_layout.addStretch()

        upper_layout.addWidget(right_widget, 3)  # 右侧占 3 份

        main_layout.addLayout(upper_layout)

        # 底部布局,使用水平布局,包含日期和打卡提示
        bottom_layout = QHBoxLayout()

        # 日期显示部分,包含时间和农历日期
        date_panel = QWidget()
        date_layout = QHBoxLayout(date_panel)
        date_layout.setContentsMargins(0, 0, 0, 0)

        self.time_label = QLabel()
        self.time_label.setObjectName("timeLabel")  # 设置对象名称
        self.time_label.setFont(QFont("微软雅黑", 19, QFont.Bold))
        date_layout.addWidget(self.time_label)

        self.lunar_date_label = QLabel()  # 新增农历日期显示标签
        self.lunar_date_label.setObjectName("lunarDateLabel")  # 设置对象名称
        self.lunar_date_label.setFont(QFont("微软雅黑", 19, QFont.Bold))
        date_layout.addWidget(self.lunar_date_label)

        bottom_layout.addWidget(date_panel)

        # 打卡提示部分
        self.status_label = QLabel("等待识别...")
        self.status_label.setObjectName("statusLabel")  # 设置对象名称
        # 设置更大的字体大小
        self.status_label.setFont(QFont("微软雅黑", 30, QFont.Bold))
        self.status_label.setAlignment(Qt.AlignCenter)  # 居中显示
        bottom_layout.addStretch()  # 添加弹簧,将打卡提示推到右侧
        bottom_layout.addWidget(self.status_label, alignment=Qt.AlignRight)

        main_layout.addLayout(bottom_layout)

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_display)
        self.timer.start(1000)


        self.create_menu()
        # 修改样式表,使用对象名称选择器调整特定 QLabel 的颜色
        self.setStyleSheet("""
            QMainWindow { background-color: #2D2D2D; }
            QMenuBar { background-color: #3D3D3D; color: white; }
            QMenu { background-color: #3D3D3D; color: white; }
            QTableWidget { background-color: #404040; color: white; }
            QDialog { color: black; }
            QMessageBox { color: black; }
            #titleLabel { color: white; }
            #attendanceLabel { color: white; }
            #timeLabel { color: white; }
            #lunarDateLabel { color: white; }
            #statusLabel { color: white; font-size: 30px; font-family: 微软雅黑; font-weight: bold; }
        """)

2、菜单部分:

def create_menu(self):
        file_menu = self.menuBar().addMenu("文件(&F)")
        backup_action = QAction("数据备份", self)
        backup_action.triggered.connect(self.backup_data)
        file_menu.addAction(backup_action)

        manage_menu = self.menuBar().addMenu("管理(&M)")
        staff_action = QAction("人员管理", self)
        staff_action.triggered.connect(self.show_staff_admin)
        manage_menu.addAction(staff_action)

        view_menu = self.menuBar().addMenu("视图(&V)")
        fullscreen_action = QAction("全屏切换", self, checkable=True)
        fullscreen_action.triggered.connect(self.toggle_fullscreen)
        view_menu.addAction(fullscreen_action)

        config_menu = self.menuBar().addMenu("配置(&C)")
        algo_action = QAction("算法设置", self)
        algo_action.triggered.connect(self.show_algorithm_config)
        config_menu.addAction(algo_action)

        camera_config_action = QAction("摄像头配置", self)
        camera_config_action.triggered.connect(self.show_camera_config)
        config_menu.addAction(camera_config_action)

        # 新增考勤查询菜单
        query_menu = self.menuBar().addMenu("考勤查询(&Q)")
        query_action = QAction("考勤查询", self)
        query_action.triggered.connect(self.show_attendance_query)
        query_menu.addAction(query_action)

3、子菜单部分:

首先是识别算法的选择:

def show_algorithm_config(self):
        dialog = QDialog(self)
        layout = QFormLayout()

        # 算法选择
        algo_combo = QComboBox()
        algo_combo.addItems(['arcsoft'])
        algo_combo.setCurrentText(config.face_algorithm)

        # 虹软配置
        appid_edit = QLineEdit(config.arcsoft_appid.decode())
        sdkkey_edit = QLineEdit(config.arcsoft_sdkkey.decode())
        lib_path_edit = QLineEdit(config.arcsoft_lib_path)

        layout.addRow("识别算法:", algo_combo)
        layout.addRow("虹软APPID:", appid_edit)
        layout.addRow("虹软SDKKEY:", sdkkey_edit)
        layout.addRow("SDK路径:", lib_path_edit)

        btn_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
        btn_box.accepted.connect(lambda: self.save_config(
            algo_combo.currentText(),
            appid_edit.text(),
            sdkkey_edit.text(),
            lib_path_edit.text()
        ))
        btn_box.rejected.connect(dialog.reject)

        layout.addRow(btn_box)
        dialog.setLayout(layout)
        dialog.exec_()

摄像头的在上面说了,这里就不重复了;

考勤结果查询部分,做了个判断,加了个标识,也没啥其他的;

功能界面部分:

def init_ui(self):
        self.setWindowTitle("考勤查询")
        self.setGeometry(100, 100, 400, 300)

        layout = QFormLayout()

        self.query_type_combo = QComboBox()
        self.query_type_combo.addItems(["个人某天考勤", "个人时间区间考勤", "某天所有人员考勤", "某时间段所有人员考勤"])
        self.query_type_combo.currentIndexChanged.connect(self.update_inputs)

        self.staff_name_edit = QLineEdit()
        self.staff_name_edit.setPlaceholderText("输入员工姓名")

        self.date_edit = QDateEdit()
        self.date_edit.setDisplayFormat("yyyy-MM-dd")
        current_date = QDate.currentDate()
        self.date_edit.setDate(current_date)

        self.start_date_edit = QDateEdit()
        self.start_date_edit.setDisplayFormat("yyyy-MM-dd")
        self.start_date_edit.setDate(current_date)

        self.end_date_edit = QDateEdit()
        self.end_date_edit.setDisplayFormat("yyyy-MM-dd")
        self.end_date_edit.setDate(current_date)

        layout.addRow("查询类型:", self.query_type_combo)
        layout.addRow("员工姓名:", self.staff_name_edit)
        layout.addRow("日期:", self.date_edit)
        layout.addRow("开始日期:", self.start_date_edit)
        layout.addRow("结束日期:", self.end_date_edit)

        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.query_attendance)
        button_box.rejected.connect(self.reject)
        layout.addRow(button_box)

        self.setLayout(layout)
        self.update_inputs()
def update_inputs(self):
        query_type = self.query_type_combo.currentIndex()
        if query_type in [0, 1]:  # 个人某天考勤和个人时间区间考勤
            self.staff_name_edit.setEnabled(True)
        else:
            self.staff_name_edit.setEnabled(False)

        if query_type == 0:  # 个人某天考勤
            self.date_edit.setEnabled(True)
            self.start_date_edit.setEnabled(False)
            self.end_date_edit.setEnabled(False)
        elif query_type == 1:  # 个人时间区间考勤
            self.date_edit.setEnabled(False)
            self.start_date_edit.setEnabled(True)
            self.end_date_edit.setEnabled(True)
        elif query_type == 2:  # 某天所有人员考勤
            self.date_edit.setEnabled(True)
            self.start_date_edit.setEnabled(False)
            self.end_date_edit.setEnabled(False)
        elif query_type == 3:  # 某时间段所有人员考勤
            self.date_edit.setEnabled(False)
            self.start_date_edit.setEnabled(True)
            self.end_date_edit.setEnabled(True)

考勤的逻辑处理部分:

def show_attendance_results(self, results):
        dialog = QDialog(self)
        dialog.setWindowTitle("考勤查询结果")
        dialog.resize(640, 360)

        layout = QVBoxLayout()

        table = QTableWidget()
        table.setColumnCount(8)
        table.setHorizontalHeaderLabels(["姓名", "日期", "上午打卡时间", "上午状态", "中午打卡时间", "中午状态", "下午打卡时间", "下午状态"])
        table.setRowCount(len(results))

        from datetime import datetime

        for i, (name, date, morning_check_time, noon_check_time, night_check_time) in enumerate(results):
            table.setItem(i, 0, QTableWidgetItem(name))
            table.setItem(i, 1, QTableWidgetItem(date))

            # 处理上午打卡时间和状态
            if morning_check_time:
                try:
                    morning_check_time = datetime.strptime(morning_check_time, '%Y-%m-%d %H:%M:%S.%f')
                    morning_check_time_str = morning_check_time.strftime('%H:%M:%S')
                    morning_status = "正常"
                    if morning_check_time.time() > self.morning_end_time:
                        morning_status = "迟到"
                except ValueError:
                    morning_check_time_str = morning_check_time
                    morning_status = "异常"
            else:
                morning_check_time_str = '未打卡'
                morning_status = "未打卡"
            table.setItem(i, 2, QTableWidgetItem(morning_check_time_str))
            table.setItem(i, 3, QTableWidgetItem(morning_status))

            # 处理中午打卡时间和状态
            if noon_check_time:
                try:
                    noon_check_time = datetime.strptime(noon_check_time, '%Y-%m-%d %H:%M:%S.%f')
                    noon_check_time_str = noon_check_time.strftime('%H:%M:%S')
                    noon_status = "正常"

                except ValueError:
                    noon_check_time_str = noon_check_time
                    noon_status = "异常"
            else:
                noon_check_time_str = '未打卡'
                noon_status = "未打卡"
            table.setItem(i, 4, QTableWidgetItem(noon_check_time_str))
            table.setItem(i, 5, QTableWidgetItem(noon_status))

            # 处理下午打卡时间和状态
            if night_check_time:
                try:
                    night_check_time = datetime.strptime(night_check_time, '%Y-%m-%d %H:%M:%S.%f')
                    night_check_time_str = night_check_time.strftime('%H:%M:%S')
                    night_status = "正常"
                    if night_check_time.time() < self.night_end_time:
                        night_status = "早退"
                except ValueError:
                    night_check_time_str = night_check_time
                    night_status = "异常"
            else:
                night_check_time_str = '未打卡'
                night_status = "未打卡"
            table.setItem(i, 6, QTableWidgetItem(night_check_time_str))
            table.setItem(i, 7, QTableWidgetItem(night_status))

        layout.addWidget(table)

        button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
        button_box.accepted.connect(lambda: self.export_attendance_results(table))
        button_box.rejected.connect(dialog.reject)
        layout.addWidget(button_box)

        dialog.setLayout(layout)
        dialog.exec_()

考勤查询条件

def query_attendance(self):
        query_type = self.query_type_combo.currentIndex()
        staff_name = self.staff_name_edit.text() if self.staff_name_edit.isEnabled() else None
        date = self.date_edit.date().toString("yyyy-MM-dd") if self.date_edit.isEnabled() else None
        start_date = self.start_date_edit.date().toString("yyyy-MM-dd") if self.start_date_edit.isEnabled() else None
        end_date = self.end_date_edit.date().toString("yyyy-MM-dd") if self.end_date_edit.isEnabled() else None

        if query_type in [0, 1] and not staff_name:
            QMessageBox.warning(self, "警告", "请输入员工姓名")
            return

        if query_type == 0:  # 个人某天考勤
            if not date:
                QMessageBox.warning(self, "警告", "请输入日期")
                return
            try:
                datetime.strptime(date, '%Y-%m-%d')
            except ValueError:
                QMessageBox.warning(self, "警告", "日期格式不正确")
                return
            results = self.db.query_attendance(staff_name=staff_name, start_date=date, end_date=date)
        elif query_type == 1:  # 个人时间区间考勤
            if not start_date or not end_date:
                QMessageBox.warning(self, "警告", "请输入开始日期和结束日期")
                return
            try:
                datetime.strptime(start_date, '%Y-%m-%d')
                datetime.strptime(end_date, '%Y-%m-%d')
            except ValueError:
                QMessageBox.warning(self, "警告", "日期格式不正确")
                return
            results = self.db.query_attendance(staff_name=staff_name, start_date=start_date, end_date=end_date)
        elif query_type == 2:  # 某天所有人员考勤
            if not date:
                QMessageBox.warning(self, "警告", "请输入日期")
                return
            try:
                datetime.strptime(date, '%Y-%m-%d')
            except ValueError:
                QMessageBox.warning(self, "警告", "日期格式不正确")
                return
            results = self.db.query_attendance(start_date=date, end_date=date)
        elif query_type == 3:  # 某时间段所有人员考勤
            if not start_date or not end_date:
                QMessageBox.warning(self, "警告", "请输入开始日期和结束日期")
                return
            try:
                datetime.strptime(start_date, '%Y-%m-%d')
                datetime.strptime(end_date, '%Y-%m-%d')
            except ValueError:
                QMessageBox.warning(self, "警告", "日期格式不正确")
                return
            results = self.db.query_attendance(start_date=start_date, end_date=end_date)

        self.results = results
        self.accept()

结果导出部分:

def export_attendance_results(self, table):
        # 获取当前时间,格式化为合适的字符串
        current_time = datetime.now().strftime("%Y%m%d%H%M%S")
        # 生成默认文件名
        default_csv_name = f"考勤统计_{current_time}.csv"
        default_excel_name = f"考勤统计_{current_time}.xlsx"

        # 打开文件保存对话框,设置默认文件名
        file_path, _ = QFileDialog.getSaveFileName(
            self, 
            "导出考勤数据", 
            "{default_csv_name}".format(default_csv_name=default_csv_name), 
            "CSV Files (*.csv);;Excel Files (*.xlsx)",
            options=QFileDialog.Options()
        )


        # 如果用户没有输入文件名,根据选择的文件类型使用默认文件名
        if not file_path:
            return
        if not file_path.endswith(('.csv', '.xlsx')):
            if 'CSV' in _:
                file_path = os.path.join(os.path.dirname(file_path), default_csv_name)
            else:
                file_path = os.path.join(os.path.dirname(file_path), default_excel_name)

        data = []
        for row in range(table.rowCount()):
            row_data = []
            for col in range(table.columnCount()):
                item = table.item(row, col)
                row_data.append(item.text() if item else '')
            data.append(row_data)

        df = pd.DataFrame(data, columns=[table.horizontalHeaderItem(col).text() for col in range(table.columnCount())])
        if file_path.endswith('.csv'):
            df.to_csv(file_path, index=False)
        elif file_path.endswith('.xlsx'):
            df.to_excel(file_path, index=False)
        QMessageBox.information(self, "导出成功", f"考勤数据已导出至:{file_path}")

代码有点烟花缭乱了是吧,这篇就先到这里,慢慢来吧,我整理整理看看还有漏掉啥吧,看看再补充。


http://www.kler.cn/a/579996.html

相关文章:

  • Java本地方法根据线上地址下载图片到本地然后返回本地可以访问的地址
  • c语言笔记 一维数组与二维数组
  • python爬虫:Android自动化工具Auto.js的详细使用
  • RabbitMQ高级特性----生产者确认机制
  • craco.config.js是什么?
  • Java剪刀石头布
  • 小程序实现存储用户注册信息功能 前后端+数据库联调
  • 【2025】基于php+vue的舞蹈培训机构管理系统(源码+文档+调试+图文修改+答疑)
  • 静态网页的爬虫(以电影天堂为例)
  • 基于SpringBoot实现旅游酒店平台功能三
  • 【Academy】Web 缓存欺骗 ------ Web cache deception
  • 深入理解隐式类型转换:从原理到应用
  • FPGA|Verilog-自己写的SPI驱动
  • 我们在开发时,什么时候用到虚函数和纯虚函数?
  • MacOS安装FFmpeg和FFprobe
  • 洛谷 P1433 吃奶酪
  • Spring Cloud 负载均衡器架构选型
  • 基于51单片机多功能防盗报警系统
  • vulnhub靶场之【digitalworld.local系列】的FALL靶机
  • K8S学习之基础二十:k8s的coredns