# -*- coding
import tkinter as tk
from tkinter import ttk, messagebox
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
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.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://tapocxy:123456@192.168.137.100/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.show_default_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=3, column=0, sticky="e", padx=5, pady=5)
self.position_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel")
self.position_value.grid(row=3, 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="打卡",
command=self.clock_in, width=15, state=tk.DISABLED)
self.clock_button.pack(side=tk.RIGHT, padx=(0, 10))
# 考勤记录标题
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 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 add_sample_records(self):
"""添加示例考勤记录"""
records = [
("08:45:22", "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 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()
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
while self.stream_active:
ret, frame = cap.read()
if not ret:
print("[调试信息] 无法读取视频帧")
continue
# 保存当前帧为临时文件供main函数处理
temp_image_path = "temp_frame.jpg"
OUT_DIR = "output"
cv2.imwrite(OUT_DIR, frame)
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")
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)
time.sleep(1) # 控制识别频率
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()
再这个代码基础上使用ttkboostrap主题包,使得界面更好看