import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from PIL import Image, ImageTk, ImageDraw, ImageFont
import vlc
import os
import cv2
import json
import time
import threading
import queue
import random
import subprocess
import sqlite3
from datetime import datetime
import pandas as pd
import pymysql
from threading import Thread, Event
OUT_DIR = "./output"
class EmployeeClockSystem:
def __init__(self, root):
self.root = root
self.root.title("员工工牌识别打卡系统")
self.root.geometry("1200x700")
self.root.configure(bg="#f0f0f0")
self.idle = Event() # 创建事件对象
self.idle.set() # 初始设置为空闲状态(允许采集)
self.capture_thread = None
self.cap = None
# 设置输出目录
self.OUT_DIR = "./output"
os.makedirs(self.OUT_DIR, exist_ok=True)
# 创建样式
self.style = ttk.Style()
self.style.configure("Title.TLabel", font=("微软雅黑", 18, "bold"), foreground="#2c3e50")
self.style.configure("Subtitle.TLabel", font=("微软雅黑", 14), foreground="#34495e")
self.style.configure("Info.TLabel", font=("微软雅黑", 12), foreground="#2c3e50")
self.style.configure("Card.TFrame", background="#ffffff", borderwidth=1, relief="raised", padding=10)
self.style.configure("Control.TFrame", background="#e0e0e0", borderwidth=1, relief="sunken", padding=10)
# 主布局框架 - 使用PanedWindow实现可调整的分割
main_paned = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=4)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧视频区域 (50%)
left_frame = ttk.Frame(main_paned)
main_paned.add(left_frame, stretch="always")
# 右侧员工信息区域 (50%)
right_frame = ttk.Frame(main_paned)
main_paned.add(right_frame, stretch="always")
# 视频流标题
ttk.Label(left_frame, text="实时视频监控", style="Title.TLabel").pack(pady=(0, 10), anchor=tk.W, padx=10)
# 视频显示区域
video_card = ttk.Frame(left_frame, style="Card.TFrame")
video_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.video_container = ttk.Frame(video_card)
self.video_container.pack(fill=tk.BOTH, expand=True)
# 视频控制面板
control_frame = ttk.Frame(left_frame, style="Control.TFrame")
control_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
# URL输入框
ttk.Label(control_frame, text="RTSP地址:").pack(side=tk.LEFT, padx=(0, 5))
self.url_entry = ttk.Entry(control_frame, width=40)
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
self.url_entry.insert(0, "rtsp://192.168.1.101/stream1")
# 连接按钮
self.connect_button = ttk.Button(control_frame, text="启动监控",
command=self.toggle_stream, width=12)
self.connect_button.pack(side=tk.LEFT, padx=(0, 5))
# 截图按钮
self.snapshot_button = ttk.Button(control_frame, text="抓拍",
command=self.take_snapshot, width=8, state=tk.DISABLED)
self.snapshot_button.pack(side=tk.LEFT)
# 员工信息标题
ttk.Label(right_frame, text="员工信息识别", style="Title.TLabel").pack(pady=(0, 10), anchor=tk.W, padx=10)
# 员工信息卡片
info_card = ttk.Frame(right_frame, style="Card.TFrame")
info_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 员工照片和基本信息
info_frame = ttk.Frame(info_card)
info_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧员工照片区域
avatar_frame = ttk.Frame(info_frame, width=180, height=200)
avatar_frame.pack(side=tk.LEFT, padx=(0, 20), fill=tk.Y)
self.avatar_label = ttk.Label(avatar_frame)
self.avatar_label.pack(fill=tk.BOTH, expand=True)
# 默认头像
self.update_avatar()
# 右侧员工详细信息
detail_frame = ttk.Frame(info_frame)
detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
ttk.Label(detail_frame, text="员工基本信息", style="Subtitle.TLabel").pack(anchor=tk.W, pady=(0, 10))
# 信息标签 - 使用Grid布局更精确控制
label_frame = ttk.Frame(detail_frame)
label_frame.pack(fill=tk.X, pady=5)
ttk.Label(label_frame, text="姓名:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=0, column=0, sticky="e", padx=5, pady=5)
self.name_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel")
self.name_value.grid(row=0, column=1, sticky="w", padx=5, pady=5)
ttk.Label(label_frame, text="工号:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=1, column=0, sticky="e", padx=5, pady=5)
self.id_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel")
self.id_value.grid(row=1, column=1, sticky="w", padx=5, pady=5)
ttk.Label(label_frame, text="部门:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=2, column=0, sticky="e", padx=5, pady=5)
self.dept_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel")
self.dept_value.grid(row=2, column=1, sticky="w", padx=5, pady=5)
# ttk.Label(label_frame, text="打卡状态:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=4, column=0, sticky="e", padx=5, pady=5)
# self.status_value = ttk.Label(label_frame, text="未识别", width=20, anchor=tk.W, style="Info.TLabel")
# self.status_value.grid(row=4, column=1, sticky="w", padx=5, pady=5)
# 打卡按钮
# 打卡按钮
button_frame = ttk.Frame(detail_frame)
button_frame.pack(fill=tk.X, pady=10)
self.clock_button = ttk.Button(button_frame, text="导出打卡EXCEL",
command=self.export_attendance_to_excel, width=15)
self.clock_button.pack(side=tk.RIGHT, padx=(0, 20))
self.search_button = ttk.Button(button_frame, text="查询",
command=self.open_search_window, width=15)
self.search_button.pack(side=tk.RIGHT, padx=(0, 30))
# 考勤记录标题
ttk.Label(right_frame, text="今日考勤记录", style="Title.TLabel").pack(pady=(10, 10), anchor=tk.W, padx=10)
# 考勤记录表格
record_card = ttk.Frame(right_frame, style="Card.TFrame")
record_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 创建表格
columns = ("time", "id", "name", "dept", "status")
self.record_tree = ttk.Treeview(record_card, columns=columns, show="headings", height=8)
# 设置列标题
self.record_tree.heading("time", text="时间", anchor=tk.W)
self.record_tree.heading("id", text="工号", anchor=tk.W)
self.record_tree.heading("name", text="姓名", anchor=tk.W)
self.record_tree.heading("dept", text="部门", anchor=tk.W)
self.record_tree.heading("status", text="状态", anchor=tk.W)
# 设置列宽
self.record_tree.column("time", width=150, anchor=tk.W)
self.record_tree.column("id", width=100, anchor=tk.W)
self.record_tree.column("name", width=100, anchor=tk.W)
self.record_tree.column("dept", width=120, anchor=tk.W)
self.record_tree.column("status", width=80, anchor=tk.W)
# 添加滚动条
scrollbar = ttk.Scrollbar(record_card, orient="vertical", command=self.record_tree.yview)
self.record_tree.configure(yscrollcommand=scrollbar.set)
# 布局
self.record_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 状态栏
self.status_var = tk.StringVar(value="系统就绪")
status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 初始化变量
self.stream_active = False
self.instance = vlc.Instance("--no-xlib")
self.player = self.instance.media_player_new()
# 模拟员工数据库
self.employees = {
"1001": {"name": "张明", "dept": "技术部", "position": "高级工程师", "avatar": "avatar1.jpg"},
"1002": {"name": "李华", "dept": "市场部", "position": "市场经理", "avatar": "avatar2.jpg"},
"1003": {"name": "王芳", "dept": "财务部", "position": "会计", "avatar": "avatar3.jpg"},
"1004": {"name": "赵刚", "dept": "人力资源", "position": "招聘主管", "avatar": "avatar4.jpg"},
"1005": {"name": "陈晓", "dept": "产品部", "position": "产品经理", "avatar": "avatar5.jpg"},
}
# 考勤记录
self.attendance_records = []
self.add_sample_records()
# 初始化线程队列
self.gui_queue = queue.Queue()
self.root.after(100, self.process_queue)
def process_queue(self):
"""处理队列中的GUI更新任务"""
while not self.gui_queue.empty():
try:
task = self.gui_queue.get_nowait()
if task["type"] == "update_employee_info":
self.name_value.config(text=task["name"])
self.id_value.config(text=task["id"])
self.dept_value.config(text=task["dept"])
self.position_value.config(text=task["position"])
self.status_value.config(text=task["status"])
self.show_employee_avatar(task["avatar"])
self.clock_button.config(state=tk.NORMAL)
elif task["type"] == "update_status":
self.status_var.set(task["message"])
elif task["type"] == "clock_in":
self.clock_in_task(task["emp_id"], task["emp_name"], task["emp_dept"])
except queue.Empty:
pass
self.root.after(100, self.process_queue)
def update_avatar(self, image_path=None):
if image_path and os.path.exists(image_path):
try:
# 尝试加载图片
print("opennnnnnnnnnnn")
img = Image.open(image_path)
print("opennnnnnnnnnnn")
# 调整图片大小以适应显示区域
img = self.resize_image(img, target_width=180, target_height=200)
# 转换为Tkinter PhotoImage
photo = ImageTk.PhotoImage(img)
# 更新显示
self.avatar_label.config(image=photo)
self.avatar_label.image = photo # 保持引用
print(f"成功加载头像: {image_path}")
return True
except Exception as e:
print(f"加载图片失败: {e}")
# 加载失败时显示默认头像
self.show_default_avatar(error=True)
return False
else:
# 没有提供图片路径或路径无效
self.show_default_avatar()
return False
def show_default_avatar(self):
"""显示默认头像"""
default_img = Image.new('RGB', (180, 200), color='#3498db')
draw = ImageDraw.Draw(default_img)
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
draw.text((40, 85), "无数据", fill="white", font=font)
default_photo = ImageTk.PhotoImage(default_img)
self.avatar_label.config(image=default_photo)
self.avatar_label.image = default_photo
def resize_image(self, img, target_width, target_height):
# """调整图片大小,保持宽高比并填充背景"""
# 计算缩放比例
width, height = img.size
scale = min(target_width / width, target_height / height)
# 计算新尺寸
new_width = int(width * scale)
new_height = int(height * scale)
# 调整图片大小
img = img.resize((new_width, new_height), Image.LANCZOS)
# 创建新图片并粘贴调整后的图片到中心
new_img = Image.new('RGB', (target_width, target_height), color='white')
position = (
(target_width - new_width) // 2,
(target_height - new_height) // 2
)
new_img.paste(img, position)
return new_img
def open_search_window(self):
# """打开查找员工记录的弹窗"""
search_window = tk.Toplevel(self.root)
search_window.title("查找员工考勤记录")
search_window.geometry("600x400")
search_window.resizable(False, False)
# 输入框
input_frame = ttk.Frame(search_window)
input_frame.pack(pady=10, padx=10, fill=tk.X)
ttk.Label(input_frame, text="姓名或工号:", width=12, anchor=tk.E).pack(side=tk.LEFT)
search_entry = ttk.Entry(input_frame, width=30)
search_entry.pack(side=tk.LEFT, padx=5)
# 查询按钮
search_btn = ttk.Button(input_frame, text="查询", command=lambda: self.perform_search(search_entry, tree))
search_btn.pack(side=tk.LEFT)
# 表格
tree_frame = ttk.Frame(search_window)
tree_frame.pack(padx=10, pady=5, fill=tk.BOTH, expand=True)
tree = ttk.Treeview(tree_frame, columns=("工号", "姓名", "部门","时间", "次数"), show="headings", height=15)
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 设置列标题
for col in tree["columns"]:
tree.heading(col, text=col)
tree.column(col, width=100, anchor=tk.CENTER)
# 滚动条
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
tree.configure(yscrollcommand=scrollbar.set)
# 提示信息
self.search_status_label = ttk.Label(search_window, text="", foreground="red")
self.search_status_label.pack(pady=5)
# 保存状态
self.current_search_tree = tree
def perform_search(self, entry_widget, tree_widget):
keyword = entry_widget.get().strip()
if not keyword:
print("//////////////")
self.search_status_label.config(text="请输入姓名或工号")
return
try:
conn = pymysql.connect(
host='localhost',
user='root',
password='139800',
database='employeeinfomation'
)
print("**************")
cursor = conn.cursor()
print("**************")
query = """
SELECT * FROM workcard
WHERE NAME LIKE %s
"""
cursor.execute(query, (f"%{keyword}%"))
results = cursor.fetchall()
print(results)
# 清空表格
for item in tree_widget.get_children():
tree_widget.delete(item)
# 插入查询结果
for record in results:
tree_widget.insert("", tk.END, values=record)
if not results:
self.search_status_label.config(text=f"未找到包含“{keyword}”的记录")
else:
self.search_status_label.config(text=f"找到 {len(results)} 条记录")
conn.close()
except Exception as e:
self.search_status_label.config(text=f"查询失败:{str(e)}")
def add_sample_records(self):
"""添加示例考勤记录"""
records = [
("09:00:24", "1001", "张明", "技术部", "正常"),
("09:01:35", "1002", "李华", "市场部", "迟到"),
("09:05:47", "1003", "王芳", "财务部", "正常"),
("12:01:15", "1001", "张明", "技术部", "外出"),
("13:30:08", "1004", "赵刚", "人力资源", "正常"),
]
for record in records:
self.attendance_records.append(record)
self.record_tree.insert("", tk.END, values=record)
def add_new_records(self, record):
if (record.get('id') and record.get('name') and record.get('dept')):
new_record = (
datetime.now().strftime("%H:%M:%S"), # 当前时间
record.get('id'), # 员工ID
record.get('name'), # 姓名
record.get('dept'), # 部门
"正常" # 考勤状态
)
self.attendance_records.append(new_record)
self.record_tree.insert("", tk.END, values=new_record)
else:
# 可选:添加错误处理(如日志记录或弹窗提示)
print("错误:缺少必要的字段信息,记录未添加")
def export_attendance_to_excel(self):
# """从本地数据库导出考勤记录到Excel文件"""
# 弹出文件保存路径
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
title="保存考勤记录为Excel"
)
if not file_path:
return # 用户取消了保存
try:
# 1. 连接 MySQL 数据库(根据你的实际配置填写)
# conn = mysql.connector.connect(
# host="localhost", # 数据库地址
# user="root", # 用户名
# password="yourpass", # 密码
# database="attendance_db" # 数据库名称
# )
conn = pymysql.connect(
host='localhost',
user='root',
password='139800',
database='employeeinfomation'
)
# 2. 使用 pandas 直接读取 SQL 查询结果
query = "SELECT * FROM workcard"
df = pd.read_sql_query(query, conn)
# 3. 关闭数据库连接
conn.close()
# 4. 如果没有数据
if df.empty:
messagebox.showwarning("导出失败", "数据库中没有可导出的考勤记录!")
return
# 5. 导出为 Excel 文件
df.to_excel(file_path, index=False)
messagebox.showinfo("导出成功", f"考勤记录已成功导出到:\n{file_path}")
except Exception as e:
messagebox.showerror("导出失败", f"导出考勤记录时发生错误:\n{str(e)}")
def search_count(self, name):
conn = pymysql.connect(
host='localhost',
user='root',
password='139800',
database='employeeinfomation'
)
# 2. 使用 pandas 直接读取 SQL 查询结果
cursor = conn.cursor()
query = "SELECT COUNT FROM workcard WHERE NAME LIKE %s "
cursor.execute(query, (f"%{name}%"))
count = cursor.fetchall()
# 3. 关闭数据库连接
conn.close()
return count
def toggle_stream(self):
"""切换视频流状态"""
if self.stream_active:
self.stop_stream()
else:
self.start_stream()
def start_stream(self):
"""启动视频流"""
url = self.url_entry.get().strip()
if not url:
messagebox.showerror("错误", "请输入有效的视频流URL")
return
try:
media = self.instance.media_new(url)
self.player.set_media(media)
win_id = self.video_container.winfo_id()
if os.name == 'nt':
win_id = int(win_id)
self.player.set_hwnd(win_id)
else:
self.player.set_xwindow(win_id)
self.player.play()
self.stream_active = True
self.connect_button.config(text="停止监控")
self.snapshot_button.config(state=tk.NORMAL)
self.status_var.set(f"正在播放: {url}")
# 启动视频流线程
threading.Thread(target=self.video_thread, daemon=True).start()
# 启动识别线程
# threading.Thread(target=self.recognition_thread, daemon=True).start()
# 启动采集线程
self.capture_thread = Thread(target=self.capture_loop, daemon=True)
self.capture_thread.start()
except Exception as e:
messagebox.showerror("连接错误", f"无法连接到视频流: {str(e)}")
self.status_var.set("连接失败")
def video_thread(self):
"""视频流播放线程"""
pass # 视频流由VLC内部处理,无需额外操作
# def recognition_thread(self, out_dir: str = OUT_DIR):
# """使用实际工牌识别代码进行识别的线程"""
# cap = cv2.VideoCapture(self.url_entry.get().strip())
# if not cap.isOpened():
# print("[错误] 无法打开视频流")
# return
# # 加载预训练的人脸检测模型
# face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
# while self.stream_active:
# ret, frame = cap.read()
# if not ret:
# print("[调试信息] 无法读取视频帧")
# continue
# # 转为灰度图,提高检测效率
# gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# faces = 0
# # 检测人脸
# faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
# if len(faces) > 0:
# print(f"[调试信息] 检测到 {len(faces)} 张人脸")
# # 保存当前帧供后续处理
# temp_image_path = "temp_frame.jpg"
# cv2.imwrite(temp_image_path, frame)
# print(f"[调试信息] 图像已保存至 {temp_image_path}")
# # 调用main.py 进行图像识别
# try:
# print(f"[调试信息] 正在调用 main.py 处理 {temp_image_path}")
# subprocess.run(["python", "main.py", temp_image_path], check=True)
# print("[调试信息] main.py 执行完成")
# except subprocess.CalledProcessError as e:
# print(f"[错误信息] main.py 执行失败: {e}")
# # 读取main.py 输出的JSON文件
# stem = os.path.splitext(os.path.basename(temp_image_path))[0]
# final_json = os.path.join(OUT_DIR, f"{stem}_face_result.json")
# final_picture = os.path.join(OUT_DIR, f"{stem}_face.jpg")
# img = Image.open(final_picture)
# print(img)
# self.update_avatar(final_picture)
# if os.path.exists(final_json):
# print(f"[调试信息] JSON文件已找到: {final_json}")
# with open(final_json, "r", encoding="utf-8") as f:
# result = json.load(f)
# ocr_info = result.get("OCR", {})
# emp_id = ocr_info.get("id")
# emp_name = ocr_info.get("name")
# emp_dept = ocr_info.get("department")
# # 打印识别结果
# print(f"[调试信息] 识别结果: ID={emp_id}, 姓名={emp_name}, 部门={emp_dept}")
# # 将识别结果发送到GUI队列
# task = {
# "type": "update_employee_info",
# "name": emp_name,
# "id": emp_id,
# "dept": emp_dept,
# "position": "未知",
# "avatar": os.path.join(OUT_DIR, f"{stem}_face.jpg")
# }
# self.gui_queue.put(task)
# # 添加示例记录
# count = self.search_count(emp_name)
# if(count == 1):
# self.add_new_records(task)
# messagebox.showinfo("打卡成功", f"{emp_name} 已成功打卡!")
# else:
# messagebox.showinfo("Warnning!", f"{emp_name} 重复打卡!")
# # time.sleep(10) # 控制识别频率
# cap.release()
def capture_loop(self):
# """视频采集主循环,使用事件控制采集频率"""
self.cap = cv2.VideoCapture(self.url_entry.get().strip())
if not self.cap.isOpened():
print("[错误] 无法打开视频流")
return
# 加载预训练的人脸检测模型
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
while self.stream_active:
# 等待空闲状态(允许采集)
self.idle.wait()
# time.sleep(5)
# 读取一帧
ret, frame = self.cap.read()
if not ret:
print("[调试信息] 无法读取视频帧")
time.sleep(0.1)
continue
# 转为灰度图,提高检测效率
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 检测人脸
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30)
)
if len(faces) > 0:
print(f"[调试信息] 检测到 {len(faces)} 张人脸")
# 立即设置为忙碌状态(禁止进一步采集)
self.idle.clear()
# 启动处理线程
Thread(
target=self.process_frame,
args=(frame.copy(),), # 复制帧以避免后续修改
daemon=True
).start()
else:
# 未检测到人脸时短暂休眠,控制抽帧频率
time.sleep(0.05)
def process_frame(self, frame):
# """处理检测到人脸的帧"""
try:
# 保存当前帧
temp_image_path = "temp_frame.jpg"
cv2.imwrite(temp_image_path, frame)
print(f"[调试信息] 图像已保存至 {temp_image_path}")
# 调用main.py进行图像识别
print(f"[调试信息] 正在调用 main.py 处理 {temp_image_path}")
subprocess.run(["python", "main.py", temp_image_path], check=True)
print("[调试信息] main.py 执行完成")
# 读取main.py输出的JSON文件
stem = os.path.splitext(os.path.basename(temp_image_path))[0]
final_json = os.path.join(OUT_DIR, f"{stem}_face_result.json")
final_picture = os.path.join(OUT_DIR, f"{stem}_face.jpg")
if os.path.exists(final_json):
print(f"[调试信息] JSON文件已找到: {final_json}")
with open(final_json, "r", encoding="utf-8") as f:
result = json.load(f)
ocr_info = result.get("OCR", {})
emp_id = ocr_info.get("id")
emp_name = ocr_info.get("name")
emp_dept = ocr_info.get("department")
# 打印识别结果
print(f"[调试信息] 识别结果: ID={emp_id}, 姓名={emp_name}, 部门={emp_dept}")
# 更新GUI
img = Image.open(final_picture)
self.update_avatar(final_picture)
# 将识别结果发送到GUI队列
task = {
"type": "update_employee_info",
"name": emp_name,
"id": emp_id,
"dept": emp_dept,
"position": "未知",
"avatar": final_picture
}
self.gui_queue.put(task)
# 检查打卡记录
count = self.search_count(emp_name)
if count == 0:
self.add_new_records(task)
messagebox.showinfo("打卡成功", f"{emp_name} 已成功打卡!")
else:
messagebox.showinfo("警告", f"{emp_name} 重复打卡!")
else:
print(f"[错误] 未找到JSON文件: {final_json}")
# 处理失败时也恢复采集
except subprocess.CalledProcessError as e:
print(f"[错误] main.py 执行失败: {e}")
except Exception as e:
print(f"[错误] 处理过程中发生异常: {str(e)}")
finally:
# 无论处理成功与否,都恢复空闲状态
self.idle.set()
def stop(self):
# """停止采集和处理"""
self.stream_active = False
self.idle.set() # 确保线程可以退出
if self.cap and self.cap.isOpened():
self.cap.release()
def stop_stream(self):
"""停止视频流"""
if self.player:
self.player.stop()
self.stream_active = False
self.connect_button.config(text="启动监控")
self.snapshot_button.config(state=tk.DISABLED)
self.clock_button.config(state=tk.DISABLED)
self.status_var.set("已停止视频流")
# 清空员工信息
self.name_value.config(text="")
self.id_value.config(text="")
self.dept_value.config(text="")
self.position_value.config(text="")
self.status_value.config(text="未识别")
self.show_default_avatar()
def take_snapshot(self):
"""抓拍当前帧"""
if self.stream_active:
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"snapshot_{timestamp}.png"
self.player.video_take_snapshot(0, filename, 0, 0)
messagebox.showinfo("抓拍成功", f"已保存截图: {filename}")
self.status_var.set(f"截图已保存: {filename}")
def show_employee_avatar(self, avatar_path):
"""显示员工头像"""
try:
colors = ["#3498db", "#2ecc71", "#e74c3c", "#f39c12", "#9b59b6"]
color = random.choice(colors)
img = Image.new('RGB', (180, 200), color=color)
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("arial.ttf", 20)
except:
font = ImageFont.load_default()
draw.text((40, 85), "员工照片", fill="white", font=font)
photo = ImageTk.PhotoImage(img)
self.avatar_label.config(image=photo)
self.avatar_label.image = photo
except Exception as e:
print(f"头像加载错误: {e}")
def clock_in(self):
"""员工打卡"""
emp_id = self.id_value.cget("text")
emp_name = self.name_value.cget("text")
emp_dept = self.dept_value.cget("text")
task = {"type": "clock_in", "emp_id": emp_id, "emp_name": emp_name, "emp_dept": emp_dept}
self.gui_queue.put(task)
def clock_in_task(self, emp_id, emp_name, emp_dept):
"""执行打卡逻辑"""
current_time = time.strftime("%H:%M:%S")
hour = int(time.strftime("%H"))
minute = int(time.strftime("%M"))
status = "迟到" if (hour > 9 or (hour == 9 and minute > 0)) else "正常"
record = (current_time, emp_id, emp_name, emp_dept, status)
self.attendance_records.append(record)
self.record_tree.insert("", tk.END, values=record)
self.status_value.config(text=f"已打卡 ({status})")
self.status_var.set(f"{emp_name} 打卡成功! 时间: {current_time}")
self.clock_button.config(state=tk.DISABLED)
self.record_tree.see(self.record_tree.get_children()[-1])
if __name__ == "__main__":
root = tk.Tk()
app = EmployeeClockSystem(root)
root.mainloop()
为什么使用了event还是一直截取同一个时刻的照片,而不是隔一段时间再截图