从头开始开发基于虹软SDK的人脸识别考勤系统(python+RTSP开源)(五)补充剩余内容
本篇是对照之前代码剩余的部分代码做补充,分享给大家,便于对照运行测试。
人脸识别抽象层,这个大家应该都知道,就是为了方便使用其他的人脸识别算法设置的。
# 人脸识别服务抽象层
class FaceServiceBase:
def __init__(self, db_service):
self.db = db_service
self.known_features = []
self.known_ids = []
def refresh_data(self):
"""从数据库加载特征数据"""
raise NotImplementedError
def recognize(self, frame):
"""识别图像中的人脸"""
raise NotImplementedError
@staticmethod
def get_face_feature(image_path):
"""从图片提取特征"""
raise NotImplementedError
为了增强识别效果,对图片进行处理。效果怎么样这个还真不明显。水平有限,走这步有点画蛇添足
def preprocess_image(frame):
# 灰度化
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 直方图均衡化
equalized = cv2.equalizeHist(gray)
# 高斯模糊
blurred = cv2.GaussianBlur(equalized, (5, 5), 0)
# 将单通道灰度图转换为三通道图像
blurred = cv2.cvtColor(blurred, cv2.COLOR_GRAY2BGR)
return blurred
绘制识别人脸框,原来是个蓝色的框框太扎眼,换了个橙色半透明的。
# 提取绘制人脸框的代码为独立函数,修改为半透明橙色
def draw_face_boxes(pixmap, face_rects):
painter = QPainter(pixmap)
pen = QPen(QColor(255, 165, 0, 128), 2) # 半透明橙色
painter.setPen(pen)
for rect in face_rects:
if isinstance(rect, MRECT):
painter.drawRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top)
else:
top, right, bottom, left = rect
painter.drawRect(left, top, right - left, bottom - top)
painter.end()
return pixmap
看方法名就知道了!
def update_frame(self, frame):
if not self.camera_worker.running: # 仅在运行状态下更新
return
try:
self.frame_mutex.lock()
self.current_frame = frame.copy()
# 显示帧(添加空指针检查)
if self.current_frame is not None:
h, w, ch = self.current_frame.shape
bytes_per_line = ch * w
q_img = QImage(
self.current_frame.data,
w, h, bytes_per_line,
QImage.Format_RGB888
)
pixmap = QPixmap.fromImage(q_img)
# 人脸检测与绘制人脸框
face_rects = self._detect_faces_arcsoft(self.current_frame)
pixmap = draw_face_boxes(pixmap, face_rects)
self.current_pixmap = pixmap # 存储当前的 QPixmap
# print("Updating video label pixmap")
self._update_video_label_pixmap(pixmap)
else:
print("接收到的帧数据为空")
finally:
self.frame_mutex.unlock()
调整摄像头随同窗口大小按比例改变
#调整摄像头随同窗口大小按比例改变
def _update_video_label_pixmap(self, pixmap):
label_size = self.video_label.size()
if pixmap.width() > label_size.width() or pixmap.height() > label_size.height():
pixmap = pixmap.scaled(label_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.video_label.setPixmap(pixmap)
def resizeEvent(self, event):
width = self.width()
if width > 1300:
self.num_records = 20
else:
self.num_records = 15
self.update_attendance_list(self.num_records)
self.attendance_label.adjustSize() # 调整 QLabel 大小
super().resizeEvent(event)
识别时候的提示框,忽闪忽闪的。
def reset_status_label(self):
default_style = f"color: white; font-size: 30px; font-weight: bold; background-color: transparent;"
self.status_label.setStyleSheet(default_style)
self.status_label.setText("等待识别...")
人脸识别逻辑,不是考勤规则,注释里有详细的说明,比较啰嗦一点
def get_check_type(self):
current_time = datetime.now()
current_hour = current_time.hour
current_minute = current_time.minute
# 第一次打卡时间:每天的11点30分之前(不包括11点30)只记录最早的一次打卡
if current_hour < 11 or (current_hour == 11 and current_minute < 30):
return'morning'
# 第二次打卡时间:每天的11点30分之后,14点之前(包括11点30和14点)时间区间内只记录最早的一次,后续不更新
elif (current_hour > 11 or (current_hour == 11 and current_minute >= 30)) and current_hour <= 14:
return 'noon'
# 第三次打卡时间:每天的14点之后均可打卡,多次打卡,自动更新为最后一次打卡时间
elif current_hour > 14:
return 'night'
else:
return 'other'
配套的数据库写入
def record_attendance(self, staff_id, check_type, is_holiday=False):
with self.lock:
try:
current_date = datetime.now().strftime('%Y-%m-%d')
cursor = self.conn.cursor()
# 检查该员工在当前日期、当前考勤时间段内是否已经有考勤记录
cursor.execute('''
SELECT id, check_time
FROM attendance
WHERE staff_id =? AND check_type =? AND date =?
''', (staff_id, check_type, current_date))
existing_record = cursor.fetchone()
if existing_record:
record_id, existing_check_time = existing_record
existing_check_time = datetime.strptime(existing_check_time, "%Y-%m-%d %H:%M:%S.%f")
current_time = datetime.now()
if check_type in ('morning', 'noon'):
# 第一次和第二次打卡取首次打卡时间,不更新
pass
elif check_type == 'night':
# 第三次打卡更新为最新时间
cursor.execute('''
UPDATE attendance
SET check_time =?
WHERE id =?
''', (current_time, record_id))
self.conn.commit()
else:
# 没有记录则插入新记录
cursor.execute('''
INSERT INTO attendance (staff_id, check_time, date, check_type, is_holiday)
VALUES (?,?,?,?,?)
''', (staff_id, datetime.now(), current_date, check_type, is_holiday))
self.conn.commit()
except sqlite3.Error as e:
print(f"记录考勤失败: {e}")
raise
显示倒序排列的考勤列表,就是摄像头画面旁边的那个白色框框。
def update_attendance_list(self,num_records):
# 从数据库中获取最新的 num_records 条考勤记录
last_attendances = self.db.get_last_attendances(num_records)
# 构建表格的 HTML 内容,添加白色单细线边框样式和自适应宽度样式
table_html = '<table style="width: 100%; height: auto;border-collapse: collapse; border: 1px solid white; table-layout: fixed;">'
table_html += '<tr><th style="text-align: center; border: 1px solid white; padding: 8px; word-wrap: break-word;">倒序</th><th style="text-align: center; border: 1px solid white; padding: 8px; word-wrap: break-word;">姓 名</th><th style="text-align: center; border: 1px solid white; padding: 8px; word-wrap: break-word;">识别时间</th></tr>'
for index, (name, check_time) in enumerate(last_attendances, start=1):
time_str = check_time.strftime("%Y-%m-%d %H:%M:%S")
table_html += f'<tr><td style="border: 1px solid white; padding: 8px; word-wrap: break-word;">{index}</td><td style="border: 1px solid white; padding: 8px; word-wrap: break-word;">{name}</td><td style="border: 1px solid white; padding: 8px; word-wrap: break-word;">{time_str}</td></tr>'
table_html += '</table>'
# 设置 QLabel 以富文本格式显示
self.attendance_label.setTextFormat(Qt.RichText)
# 设置考勤记录内容
self.attendance_label.setText(table_html)
# 手动刷新界面
self.attendance_label.repaint()
这里才是考勤规则,在查询中设置
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_()
异步人脸识别
# ==================== 异步人脸识别处理 ====================
class RecognitionWorker(QThread):
result_ready = pyqtSignal(list)
def __init__(self, frame, face_service):
super().__init__()
self.frame = frame.copy()
self.face_service = face_service
def run(self):
try:
# print("RecognitionWorker开始运行")
results = self.face_service.recognize(self.frame)
# print(f"识别结果: {results}")
self.result_ready.emit(results)
# print("RecognitionWorker运行结束")
except Exception as e:
print(f"RecognitionWorker运行出错: {e}")
增加人员时的写入数据库和同步复制图片到目录
def validate_input(self):
if not self.name_input.text().strip():
QMessageBox.warning(self, "输入错误", "请输入姓名")
return
if not self.photo_path:
QMessageBox.warning(self, "输入错误", "请选择照片")
return
feature = ArcSoftService.get_face_feature(self.photo_path)
if feature is None:
QMessageBox.warning(self, "错误", "未检测到人脸")
return
try:
# 保存照片到指定目录
os.makedirs(config.face_img_dir, exist_ok=True)
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}.jpg"
save_path = os.path.join(config.face_img_dir, filename)
shutil.copy(self.photo_path, save_path)
# 添加记录到数据库
self.db.add_staff(
self.name_input.text().strip(),
self.dept_input.text().strip(),
save_path,
feature
)
self.accept()
except Exception as e:
QMessageBox.critical(self, "保存失败", f"发生错误:{str(e)}")
大约差不多就这些了吧
程序入口
# ==================== 应用程序入口 ====================
if __name__ == "__main__":
# 初始化应用程序
app = QApplication(sys.argv)
app.setStyle(QStyleFactory.create("Fusion"))
# 主窗口
main_window = MainWindow()
main_window.show()
# 运行应用程序
sys.exit(app.exec_())
项目结束。欢迎讨论。
目前程序在低端机器上运行就是感觉识别的时候卡顿,大家有什么优化建议都来说哈,我是小白,欢迎大家指导,谢谢。