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

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言:

       最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,干脆自己花了两天功夫随手写了个视频标注工具,在此分享。

Git仓库:

wlf728050719/BehaviLabel: Lightweight Python-based video behavior annotation toolhttps://github.com/wlf728050719/BehaviLabelexe下载链接:(v1.0.0的release版本)

https://github.com/wlf728050719/BehaviLabel/releases/download/v1.0.0/dist.ziphttps://github.com/wlf728050719/BehaviLabel/releases/download/v1.0.0/dist.zipexe百度网盘:

behaviLabel.zip_免费高速下载|百度网盘-分享无限制https://pan.baidu.com/s/1z6yIqkyXBOKejv_sAQ7QJw?pwd=6666补充:

行为检测模型其实也放我的Github仓库里面了,在kinetic-400的准确率有77%,不过还没来的及做readme.md以及相关博客介绍,倒是发了个b站视频,感兴趣的小伙伴可以看下,v1.0.0版本是原论文交叉熵版本的,v2.0.0改成了用三元组损失+中心损失的度量学习方法,用来发paper肯定是远远不及的,但是做本科毕设还是绰绰有余的(虽然这个点也不是做毕设的时间)

基于YOLOPOSE+STGCN的行为检测模型_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1GTTxzFECi/?share_source=copy_web&vd_source=b884e0d6e76b4660d8d391e57bbf6a80


使用:

0.运行我们打包好的exe文件或者在python环境下执行python main.py

1.点击初始化按钮,分别设置标注视频所在根目录,行为类型txt,以及标记文件存放目录,设置后程序右侧列表会显示设置目录的所有视频文件,选中视频的标注记录以及设定的行为类型

行为类型txt应该类似下面格式,即每行一种行为

2.可选择使用按钮完成上下视频切换,快进后退以及切换标注类型,设置标注起始帧并确认标注,同时提供了进度条帮助你快速定位到你想要的位置。

当然正常都会使用快捷键,不出错误的情况下整个标注行为都不需要鼠标的介入

正常一个行为标注的按键应该如下:

(1)W设置起始帧,同时视频暂停

(2)(如果暂停慢或快了按方向键左/右或直接拉进度条再重新按W)

(3)按空格继续播放视频

(4)S设置终止帧,同时视频暂停

(5)(如果暂停慢或快了按方向键左/右或直接拉进度条再重新按S)

(6)按方向上下键切换需要标注的行为

(7)enter完成标注,不会有弹窗,但右边标注记录的列表会多一条以及起始和终止帧设为空

默认设置的倍速切换为1,2,3,4,8,16,当然也可改源码中allowed_speed列表自定义你喜欢的速度,不支持小数,如果想设置0.5倍速,则需要把delay设置为当前两倍,不过本质起始相当于帧切换速度变成两倍,帧的数目没有发生变化,不如暂停后通过方向键到对应的帧。

3.当标记出错时,你也可以右键标注的记录快速定位到标注记录的起始帧或者将这条标注删除掉。

4.标记的文件会在你设置的保存目录里,命名和对应视频同名。内容如下:

5.如果需要将视频对应帧截取出来,在工具里提供了视频裁剪功能,设置好原视频根目录和标注记录保存目录以及视频输出目录后会自动裁剪。

6.以及工具提供了标注统计的功能,帮助你快速掌握当前标注情况。

7.右上角的计时记录你这次标注总用时长,总标注了多少记录,方便记录你的kpi。


源码:

后续源码更新此博客不会同步,如需要最新版本代码还请移步上述github链接

import webbrowser
import cv2
import tkinter as tk
from tkinter import filedialog
from tkinter import ttk
import os
from PIL import Image, ImageTk
import timeclass BehaviLabel:def __init__(self, root,mode):self.root = rootself.label_file = Noneself.labels = []self.video_dir = Noneself.video_index = 0self.video_list = []self.save_dir = Noneself.start_frame = Noneself.end_frame = Noneself.mode = modeself.cap = Noneself.paused = Trueself.allowed_speed = [1,2,3,4,8,16]self.speed_index = 0self.current_frame = 0self.total_frames = 0self.delay = 10self.video_width = 0self.video_height = 0self.current_photo = None  # 用于保持当前图像的引用self.selected_behavior = tk.StringVar()  # 存储选中的行为self.annotation_records = {}self.start_time = time.time()self.label_count = 0# 固定视频显示区域尺寸self.display_width = 1000  # 固定宽度self.display_height = 600  # 固定高度self.setup_ui()# 修改绑定方式,使用bind_all确保全局捕获空格键self.root.bind_all('<space>', self.pause_continue)self.root.bind_all('<a>', self.last_video)self.root.bind_all('<d>', self.next_video)self.root.bind_all('<w>', self.set_start_frame)self.root.bind_all('<s>', self.set_end_frame)self.root.bind_all('<Return>', self.confirm_annotation)self.root.bind_all('<Up>', self.select_prev_behavior)  # 添加上箭头绑定self.root.bind_all('<Down>', self.select_next_behavior)  # 添加下箭头绑定self.root.bind_all('<Left>', self.last_frame)self.root.bind_all('<Right>', self.next_frame)self.update()def update_working_time(self):self.lb_time.config(text=time.strftime('%H:%M:%S', time.gmtime(time.time() - self.start_time)))def setup_ui(self):self.root.title("BehaviLabel")self.root.geometry("1500x800")filename_frame = tk.Frame(self.root)filename_frame.pack(side=tk.TOP, pady=5, fill=tk.X)self.lb_time = tk.Label(filename_frame, text="", font=("Arial", 10))self.lb_time.pack(side=tk.RIGHT, padx=5)self.lb_count = tk.Label(filename_frame, text="标记数目:(0)", font=("Arial", 10))self.lb_count.pack(side=tk.RIGHT, padx=5)self.filename_label = tk.Label(filename_frame, text="未选择文件", font=("Arial", 10), fg="blue")self.filename_label.pack(side=tk.RIGHT, padx=5)# 主framemain_frame = tk.Frame(self.root)main_frame.pack(fill=tk.BOTH, expand=True)# 左侧frameleft_frame = tk.Frame(main_frame)left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)# 右侧frameright_frame = tk.Frame(main_frame, width=300)right_frame.pack(side=tk.RIGHT, fill=tk.Y)# 左上角按钮区button_frame = tk.Frame(left_frame)button_frame.pack(side=tk.TOP, anchor='nw', pady=5, padx=5, fill=tk.X)#初始化菜单self.btn_init = tk.Button(button_frame, text="初始化 ▼",command=lambda: self.show_menu(self.init_menu, self.btn_init))self.btn_init.pack(side=tk.LEFT, padx=5)self.init_menu = tk.Menu(self.root, tearoff=0)self.init_menu.add_command(label="设置视频目录", command=self.load_video_directory)self.init_menu.add_command(label="选择标签txt", command=self.load_label_file)self.init_menu.add_command(label="设置保存目录", command=self.load_save_dir)#基础操作菜单self.btn_base_use = tk.Button(button_frame, text="基础使用 ▼",command=lambda: self.show_menu(self.base_menu, self.btn_base_use))self.btn_base_use.pack(side=tk.LEFT, padx=5)self.base_menu = tk.Menu(self.root, tearoff=0)self.base_menu.add_command(label="上一个视频(A)",command=self.last_video)self.base_menu.add_command(label="下一个视频(D)", command=self.next_video)self.base_menu.add_separator()self.base_menu.add_command(label="快进(right)", command=self.next_frame)self.base_menu.add_command(label="后退(left)", command=self.last_frame)self.base_menu.add_separator()self.base_menu.add_command(label="设置起始帧(W)", command=self.set_start_frame)self.base_menu.add_command(label="设置结束帧(S)", command=self.set_end_frame)self.base_menu.add_separator()self.base_menu.add_command(label="切换上一行为类型(up)",command=self.select_prev_behavior)self.base_menu.add_command(label="切换下一行为类型(down)", command=self.select_next_behavior)self.base_menu.add_separator()self.base_menu.add_command(label="确认标注(Enter)",command=self.confirm_annotation)#设置菜单self.btn_setting = tk.Button(button_frame, text="设置 ▼",command=lambda: self.show_menu(self.setting_menu, self.btn_setting))self.btn_setting.pack(side=tk.LEFT, padx=5)self.setting_menu = tk.Menu(self.root, tearoff=0)self.setting_menu.add_command(label="连续播放", command=self.load_video_directory)# 关于菜单self.btn_util = tk.Button(button_frame, text="工具 ▼",command=lambda: self.show_menu(self.util_menu, self.btn_util))self.btn_util.pack(side=tk.LEFT, padx=5)self.util_menu = tk.Menu(self.root, tearoff=0)self.util_menu.add_command(label="视频分片", command=self.slice)self.util_menu.add_command(label="标记统计", command=self.show_statistics)#关于菜单self.btn_about = tk.Button(button_frame, text="关于 ▼",command=lambda: self.show_menu(self.about_menu, self.btn_about))self.btn_about.pack(side=tk.LEFT, padx=5)self.about_menu = tk.Menu(self.root, tearoff=0)self.about_menu.add_command(label="作者",command=self.author)self.about_menu.add_command(label="邮箱",command=self.mail)self.about_menu.add_command(label="项目地址",command=self.project)self.about_menu.add_command(label="检查更新",command=self.check_update)#倍速按钮self.btn_change_speed = tk.Button(button_frame, text="1倍速", command=self.change_speed)self.btn_change_speed.pack(side=tk.LEFT, padx=5)# 视频播放区域 - 固定大小的黑色背景self.video_canvas = tk.Canvas(left_frame,width=self.display_width,height=self.display_height,bg='black',highlightthickness=0)self.video_canvas.pack(side=tk.TOP, pady=10, padx=10)# 进度条progress_frame = tk.Frame(left_frame)progress_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)self.frame_label = tk.Label(progress_frame, text="0/0")self.frame_label.pack(side=tk.LEFT, padx=5)self.progress = ttk.Scale(progress_frame, from_=0, to=100, orient=tk.HORIZONTAL)self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True)self.progress.bind("<B1-Motion>", self.on_progress_drag)  # 拖动# 右上空白区域 - 现在添加帧信息和行为选择right_top_frame = tk.Frame(right_frame, bg='#f0f0f0')right_top_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)# 第一行:显示起始帧和结束帧frame_info_frame = tk.Frame(right_top_frame)frame_info_frame.pack(fill=tk.X, pady=5)tk.Label(frame_info_frame, text="起始帧:").pack(side=tk.LEFT)self.start_frame_label = tk.Label(frame_info_frame, text="未设置")self.start_frame_label.pack(side=tk.LEFT, padx=5)tk.Label(frame_info_frame, text="结束帧:").pack(side=tk.LEFT)self.end_frame_label = tk.Label(frame_info_frame, text="未设置")self.end_frame_label.pack(side=tk.LEFT, padx=5)# 第二行:行为选择下拉框behavior_frame = tk.Frame(right_top_frame)behavior_frame.pack(fill=tk.X, pady=5)tk.Label(behavior_frame, text="行为:").pack(side=tk.LEFT)self.behavior_combobox = ttk.Combobox(behavior_frame, textvariable=self.selected_behavior, state="readonly")self.behavior_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True)# 第三行:确认按钮confirm_frame = tk.Frame(right_top_frame)confirm_frame.pack(fill=tk.X, pady=5)self.confirm_button = tk.Button(confirm_frame, text="确认标注", command=self.confirm_annotation)self.confirm_button.pack(fill=tk.X)# 右下区域 - 分成两个列表bottom_frame = tk.Frame(right_frame)bottom_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=5, pady=5)# 视频列表框架video_list_frame = tk.Frame(bottom_frame)video_list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)self.lb_video_list = (tk.Label(video_list_frame, text="未设置视频目录"))self.lb_video_list.pack(side=tk.TOP)self.video_listbox = tk.Listbox(video_list_frame)self.video_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)video_scrollbar = tk.Scrollbar(video_list_frame, orient=tk.VERTICAL, command=self.video_listbox.yview)video_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.video_listbox.config(yscrollcommand=video_scrollbar.set)# 标注记录列表框架annotation_frame = tk.Frame(bottom_frame)annotation_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)self.lb_label_list = tk.Label(annotation_frame, text="未设置保存目录")self.lb_label_list.pack(side=tk.TOP)self.annotation_listbox = tk.Listbox(annotation_frame)self.annotation_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)annotation_scrollbar = tk.Scrollbar(annotation_frame, orient=tk.VERTICAL, command=self.annotation_listbox.yview)annotation_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.annotation_listbox.config(yscrollcommand=annotation_scrollbar.set)self.annotation_listbox.bind('<Button-3>', self.operate_record)  # 右键点击self.video_listbox.bind('<<ListboxSelect>>', self.on_video_select)self.video_listbox.bind('<FocusIn>', lambda e: self.root.focus_set())def operate_record(self, event):"""右键点击标注记录时弹出提示框"""# 获取点击位置的索引index = self.annotation_listbox.nearest(event.y)if index < 0:returnrecord = self.annotation_listbox.get(index)# 确保选中状态更新self.annotation_listbox.selection_clear(0, tk.END)self.annotation_listbox.selection_set(index)self.annotation_listbox.activate(index)# 创建弹出菜单popup = tk.Menu(self.root, tearoff=0)popup.add_command(label=f"记录详情: {record}")popup.add_separator()popup.add_command(label="定位记录", command=lambda: self.set_record_start_frame(record))popup.add_command(label="删除记录",command=lambda: self.delete_annotation_record(index))try:popup.tk_popup(event.x_root, event.y_root)finally:popup.grab_release()def set_record_start_frame(self, record):frame_range, behavior = record.split(": ")start_frame, end_frame = map(int, frame_range.split("-"))self.current_frame = start_frameself.paused = Trueself.show_frame(self.video_list[self.video_index])self.update_progress()def delete_annotation_record(self, index):"""从TXT文件中删除指定的标注记录"""if not self.video_list or self.video_index >= len(self.video_list):return# 获取当前视频文件名(不带扩展名)video_path = self.video_list[self.video_index]video_name = os.path.splitext(os.path.basename(video_path))[0]# 确保保存目录已设置if not self.save_dir:return# 构建标注文件路径record_file = os.path.join(self.save_dir, f"{video_name}.txt")# 获取要删除的记录内容record_to_delete = self.annotation_listbox.get(index)try:# 读取所有记录with open(record_file, 'r', encoding='utf-8') as f:lines = f.readlines()# 过滤掉要删除的记录new_lines = []for line in lines:line = line.strip()if line:parts = line.split()if len(parts) >= 3:start_frame = parts[0]end_frame = parts[1]behavior = ' '.join(parts[2:])current_record = f"{start_frame}-{end_frame}: {behavior}"if current_record != record_to_delete:new_lines.append(line + '\n')# 重新写入文件with open(record_file, 'w', encoding='utf-8') as f:f.writelines(new_lines)# 更新界面显示self.load_records()print(f"已删除记录: {record_to_delete}")except Exception as e:self.show_custom_message(f"删除记录失败: {str(e)}")self.load_records()def show_menu(self, menu, button):"""通用显示菜单方法"""try:menu.tk_popup(button.winfo_rootx(),button.winfo_rooty() + button.winfo_height())finally:menu.grab_release()def update(self):if len(self.video_list) > 0 and not self.paused:self.show_frame(self.video_list[self.video_index])self.update_progress()self.update_working_time()self.root.after(self.delay, self.update)def load_records(self):"""从当前视频文件对应的标注txt中加载标注记录"""# 确保有视频文件被选中if not self.video_list or self.video_index >= len(self.video_list):return# 获取当前视频文件名(不带扩展名)video_path = self.video_list[self.video_index]video_name = os.path.splitext(os.path.basename(video_path))[0]# 确保保存目录已设置if not self.save_dir:return# 构建标注文件路径record_file = os.path.join(self.save_dir, f"{video_name}.txt")# 清空当前记录self.annotation_listbox.delete(0, tk.END)self.annotation_records[video_name] = []# 检查标注文件是否存在if os.path.exists(record_file):try:with open(record_file, 'r', encoding='utf-8') as f:for line in f:line = line.strip()if line:# 解析标注记录 (格式: 起始帧 结束帧 行为)parts = line.split()if len(parts) >= 3:start_frame = parts[0]end_frame = parts[1]behavior = ' '.join(parts[2:])  # 处理行为名称中可能包含空格的情况record_str = f"{start_frame}-{end_frame}: {behavior}"self.annotation_records[video_name].append(record_str)self.annotation_listbox.insert(tk.END, record_str)except Exception as e:self.show_custom_message(f"加载标注记录失败: {str(e)}")def change_speed(self):self.speed_index+=1if self.speed_index>=len(self.allowed_speed):self.speed_index=0self.info("current speed:" + str(self.allowed_speed[self.speed_index]))self.btn_change_speed.config(text=str(self.allowed_speed[self.speed_index])+'倍速')def on_progress_drag(self, event):"""拖动进度条时实时输出当前值(带防抖)"""self.paused = Trueif not hasattr(self, 'last_drag') or time.time() - self.last_drag > 0.1:  # 0.1秒防抖self.last_drag = time.time()# 计算点击位置对应的帧数if self.total_frames > 0:# 获取进度条宽度width = self.progress.winfo_width()# 计算点击位置百分比click_pos = event.x / width# 计算对应的帧数new_frame = int(click_pos * self.total_frames)# 确保帧数在有效范围内new_frame = max(0, min(new_frame, self.total_frames - 1))# 更新当前帧self.current_frame = new_frame# 更新显示self.show_frame(self.video_list[self.video_index])self.frame_label.config(text=f"{self.current_frame}/{self.total_frames}")def on_video_select(self, event):selection = self.video_listbox.curselection()if selection:index = selection[0]# 如果切换的是不同的视频才重置current_frameif index != self.video_index:self.current_frame = 0filepath = self.video_list[index]  # 取完整路径total = len(self.video_list)abs_path = os.path.abspath(filepath)self.filename_label.config(text=f"{abs_path}({index + 1}/{total})")self.video_index = indexself.root.focus_set()self.paused = Trueself.show_frame(self.video_list[self.video_index])self.update_progress()self.load_records()def confirm_annotation(self, event=None):"""确认标注按钮的回调函数"""behavior = self.selected_behavior.get()if not behavior:self.show_custom_message("请先选择一个行为")returnif self.start_frame is None or self.end_frame is None:self.show_custom_message("请先设置起始帧和结束帧")return# 确保保存目录已设置if not self.save_dir:self.show_custom_message("请先设置保存目录")return# 确保有视频文件被选中if not self.video_list or self.video_index >= len(self.video_list):self.show_custom_message("没有视频文件被选中")return# 获取当前视频文件名(不带扩展名)video_path = self.video_list[self.video_index]video_name = os.path.splitext(os.path.basename(video_path))[0]# 构建保存路径save_path = os.path.join(self.save_dir, f"{video_name}.txt")try:# 写入标注信息(追加模式)with open(save_path, 'a', encoding='utf-8') as f:f.write(f"{self.start_frame} {self.end_frame} {behavior}\n")except Exception as e:self.show_custom_message(f"保存标注失败: {str(e)}")return# 重置帧标记self.start_frame = Noneself.end_frame = Noneself.start_frame_label.config(text="未设置")self.end_frame_label.config(text="未设置")self.load_records()self.label_count+=1self.lb_count.config(text="标记数目("+str(self.label_count)+")")def last_frame(self,event=None):self.paused = Trueself.current_frame -= self.allowed_speed[self.speed_index]if self.current_frame < 0:self.current_frame = 0self.show_frame(self.video_list[self.video_index])self.update_progress()def next_frame(self,event=None):self.paused = Trueself.current_frame += self.allowed_speed[self.speed_index]if self.current_frame >= self.total_frames:self.current_frame = self.total_frames-1self.show_frame(self.video_list[self.video_index])self.update_progress()def select_prev_behavior(self, event=None):"""选择上一个行为"""if not self.labels:returncurrent = self.selected_behavior.get()if current in self.labels:index = self.labels.index(current)if index > 0:self.selected_behavior.set(self.labels[index - 1])elif self.labels:self.selected_behavior.set(self.labels[-1])return "break"  # 阻止事件继续传播def select_next_behavior(self, event=None):"""选择下一个行为"""if not self.labels:returncurrent = self.selected_behavior.get()if current in self.labels:index = self.labels.index(current)if index < len(self.labels) - 1:self.selected_behavior.set(self.labels[index + 1])elif self.labels:self.selected_behavior.set(self.labels[0])return "break"  # 阻止事件继续传播def pause_continue(self, event=None):self.paused = not self.pausedreturn "break"  # 阻止事件继续传播def next_video(self,event=None):if self.video_index < len(self.video_list) - 1:self.video_index += 1filepath = self.video_list[self.video_index]  # 取完整路径total = len(self.video_list)abs_path = os.path.abspath(filepath)self.filename_label.config(text=f"{abs_path}({self.video_index + 1}/{total})")self.current_frame = 0self.root.focus_set()self.paused = Trueself.show_frame(self.video_list[self.video_index])self.update_progress()self.load_records()else:msg = f"已经是最后一个视频"self.show_custom_message(msg)def last_video(self,event=None):if self.video_index >= 1:self.video_index -= 1filepath = self.video_list[self.video_index]  # 取完整路径total = len(self.video_list)abs_path = os.path.abspath(filepath)self.filename_label.config(text=f"{abs_path}({self.video_index + 1}/{total})")self.current_frame = 0self.root.focus_set()self.paused = Trueself.show_frame(self.video_list[self.video_index])self.update_progress()self.load_records()else:msg = f"已经是第一个视频"self.show_custom_message(msg)def set_start_frame(self,event=None):self.start_frame = self.current_frameself.start_frame_label.config(text=str(self.start_frame))self.paused = Trueself.info('set start frame:' + str(self.start_frame))def set_end_frame(self,event=None):self.end_frame = self.current_frameself.end_frame_label.config(text=str(self.end_frame))self.paused = Trueself.info('set end frame:' + str(self.end_frame))def load_label_file(self):"""加载标签文件"""file_path = filedialog.askopenfilename(title="选择标签文件", filetypes=[("文本文件", "*.txt")])if file_path:self.label_file = file_pathwith open(file_path, 'r', encoding='utf-8') as f:self.labels = [line.strip() for line in f.readlines() if line.strip()]# 更新下拉框选项self.behavior_combobox['values'] = self.labelsif self.labels:self.selected_behavior.set(self.labels[0])msg = f"已加载 {len(self.labels)} 个行为标签"self.show_custom_message(msg)def load_save_dir(self):"""设置保存目录"""dir_path = filedialog.askdirectory(title="选择保存目录")if dir_path:self.save_dir = dir_pathmsg = f"标注文件将保存到: {dir_path}"self.show_custom_message(msg)self.lb_label_list.config(text='标注记录')def load_video_directory(self):"""设置视频目录"""directory = filedialog.askdirectory()if directory:self.video_dir = directoryself.video_list.clear()self.video_listbox.delete(0, tk.END)for filename in os.listdir(directory):if filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):path = os.path.join(directory, filename)self.video_list.append(path)self.video_listbox.insert(tk.END, filename)  # 只插入文件名msg = f"找到 {len(self.video_list)} 个视频文件"self.show_custom_message(msg)self.lb_video_list.config(text="视频目录")def show_custom_message(self, message, links=None):"""显示自定义消息框,支持超链接和文本复制"""top = tk.Toplevel(self.root)top.title("提示")top.resizable(False, False)# 使用Text控件实现可复制文本和超链接text = tk.Text(top, wrap=tk.WORD, height=10, width=50,padx=10, pady=10, font=('Arial', 10))text.pack()# 解析消息文本for line in message.split('\n'):# 查找行中是否包含链接url_found = Falseif links:for url in links:if url in line:# 为每个链接创建唯一tagtag_name = f"hyperlink_{url}"# 配置当前链接样式text.tag_config(tag_name, foreground="blue", underline=1)text.tag_bind(tag_name, "<Enter>",lambda e, t=text: t.config(cursor="hand2"))text.tag_bind(tag_name, "<Leave>",lambda e, t=text: t.config(cursor=""))# 分割普通文本和URLparts = line.split(url)text.insert(tk.END, parts[0])text.insert(tk.END, url, tag_name)if len(parts) > 1:text.insert(tk.END, parts[1])text.insert(tk.END, "\n")# 绑定点击事件(使用默认参数捕获当前url值)text.tag_bind(tag_name, "<Button-1>",lambda e, u=url: webbrowser.open(links[u]))url_found = Truebreakif not url_found:text.insert(tk.END, line + "\n")# 使文本只读但可选择复制text.config(state=tk.DISABLED)# 确定按钮btn = tk.Button(top, text="确定", command=top.destroy)btn.pack(pady=5)# 窗口居中top.update_idletasks()width = top.winfo_width()height = top.winfo_height()x = (top.winfo_screenwidth() // 2) - (width // 2)y = (top.winfo_screenheight() // 2) - (height // 2)top.geometry(f'+{x}+{y}')def show_frame(self, video_path):self.debug(self.current_frame)# 释放之前的资源if self.cap is not None:self.cap.release()# 清除画布上的内容self.video_canvas.delete("all")self.cap = cv2.VideoCapture(video_path)if not self.cap.isOpened():return# 获取视频原始尺寸self.video_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))self.video_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)ret, frame = self.cap.read()if ret:# 转换颜色空间frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)img = Image.fromarray(frame)# 计算保持比例的缩放因子ratio = min(self.display_width / self.video_width,self.display_height / self.video_height)new_width = int(self.video_width * ratio)new_height = int(self.video_height * ratio)# 调整图像大小img = img.resize((new_width, new_height), Image.LANCZOS)# 计算居中位置x_offset = (self.display_width - new_width) // 2y_offset = (self.display_height - new_height) // 2# 创建并显示图像self.current_photo = ImageTk.PhotoImage(image=img)self.video_canvas.create_image(x_offset, y_offset,anchor=tk.NW,image=self.current_photo)# 只有在播放状态下才前进到下一帧if not self.paused:self.current_frame += self.allowed_speed[self.speed_index]# 确保current_frame不超过视频总帧数if self.current_frame >= self.total_frames:self.current_frame = self.total_frames - 1def update_progress(self):"""更新进度条和帧数显示"""if self.total_frames > 0:progress_value = (self.current_frame / self.total_frames) * 100self.progress.set(progress_value)self.frame_label.config(text=f"{self.current_frame}/{self.total_frames}")def author(self):"""显示作者信息,带可点击链接"""msg = (f"name: 汪洛飞(Luofei Wang)\n"f"blog: https://blog.csdn.net/wlf2030\n"f"github: https://github.com/wlf728050719\n")links = {"https://blog.csdn.net/wlf2030": "https://blog.csdn.net/wlf2030","https://github.com/wlf728050719": "https://github.com/wlf728050719"}self.show_custom_message(msg, links)def mail(self):"""显示邮箱,可点击发送邮件"""email = "18086270070@163.com"msg = f"邮箱: {email}"# 创建mailto链接mailto = f"mailto:{email}"links = {email: mailto}self.show_custom_message(msg, links)def project(self):"""显示项目链接,可点击打开"""url = "https://github.com/wlf728050719/BehaviLabel"msg = f"项目地址: {url}"links = {url: url}self.show_custom_message(msg, links)def check_update(self):"""检查更新"""# 这里可以添加实际的更新检查逻辑self.show_custom_message("正在检查更新...\n暂未实现自动更新功能")def show_statistics(self):"""统计标记信息功能"""# 创建统计窗口stat_window = tk.Toplevel(self.root)stat_window.title("标记统计")stat_window.attributes('-topmost', True)stat_window.grab_set()# 主框架main_frame = tk.Frame(stat_window, padx=10, pady=10)main_frame.pack()# 选择TXT目录txt_dir_var = tk.StringVar(value=self.save_dir)def select_txt_dir():stat_window.attributes('-topmost', False)dir_path = filedialog.askdirectory(title="选择TXT目录",initialdir=self.save_dir,parent=stat_window)stat_window.attributes('-topmost', True)if dir_path:txt_dir_var.set(dir_path)dir_frame = tk.Frame(main_frame)dir_frame.pack(fill=tk.X, pady=5)tk.Label(dir_frame, text="TXT目录:").pack(side=tk.LEFT)tk.Entry(dir_frame, textvariable=txt_dir_var, width=40).pack(side=tk.LEFT, padx=5)tk.Button(dir_frame, text="浏览...", command=select_txt_dir).pack(side=tk.LEFT)# 进度条progress_var = tk.DoubleVar()progress_var.set(0)progress_frame = tk.Frame(main_frame)progress_frame.pack(fill=tk.X, pady=10)tk.Label(progress_frame, text="进度:").pack(side=tk.LEFT)progress_bar = ttk.Progressbar(progress_frame, variable=progress_var, maximum=100)progress_bar.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)# 状态标签status_label = tk.Label(main_frame, text="准备统计...", fg="blue")status_label.pack()# 结果文本框result_text = tk.Text(main_frame, wrap=tk.WORD, height=15, width=60,padx=5, pady=5, font=('Consolas', 10))result_text.pack(pady=5)# 开始统计按钮def start_statistics():txt_dir = txt_dir_var.get()if not txt_dir:self.show_custom_message("请选择TXT目录")return# 禁用按钮stat_btn.config(state=tk.DISABLED)try:# 执行统计total_marks = 0total_frames = 0action_stats = {}# 获取所有txt文件txt_files = [f for f in os.listdir(txt_dir) if f.endswith('.txt')]total_files = len(txt_files)for i, txt_file in enumerate(txt_files):# 更新进度progress = (i + 1) / total_files * 100progress_var.set(progress)status_label.config(text=f"正在统计 {txt_file} ({i + 1}/{total_files})")stat_window.update_idletasks()# 读取txt文件内容txt_path = os.path.join(txt_dir, txt_file)with open(txt_path, 'r') as f:lines = f.readlines()# 统计每行标记for line in lines:parts = line.strip().split()if len(parts) < 3:continuestart_frame = int(parts[0])end_frame = int(parts[1])action = parts[2]# 统计总数total_marks += 1total_frames += (end_frame - start_frame + 1)# 统计行为if action not in action_stats:action_stats[action] = {'count': 0,'frames': 0}action_stats[action]['count'] += 1action_stats[action]['frames'] += (end_frame - start_frame + 1)# 显示统计结果result_text.config(state=tk.NORMAL)result_text.delete(1.0, tk.END)result_text.insert(tk.END, f"=== 标记统计结果 ===\n\n")result_text.insert(tk.END, f"总标记数: {total_marks}\n")result_text.insert(tk.END, f"总帧数: {total_frames}\n\n")result_text.insert(tk.END, f"=== 按行为统计 ===\n")for action, stats in sorted(action_stats.items()):result_text.insert(tk.END,f"{action}: {stats['count']} 条, {stats['frames']} 帧\n")result_text.config(state=tk.DISABLED)status_label.config(text="统计完成", fg="green")except Exception as e:self.show_custom_message(f"统计出错: {str(e)}")finally:stat_btn.config(state=tk.NORMAL)stat_btn = tk.Button(main_frame, text="开始统计", command=start_statistics)stat_btn.pack(pady=10)# 窗口关闭处理def on_closing():stat_window.grab_release()stat_window.destroy()stat_window.protocol("WM_DELETE_WINDOW", on_closing)# 窗口居中stat_window.update_idletasks()width = stat_window.winfo_width()height = stat_window.winfo_height()x = (stat_window.winfo_screenwidth() // 2) - (width // 2)y = (stat_window.winfo_screenheight() // 2) - (height // 2)stat_window.geometry(f'+{x}+{y}')def slice(self):"""视频分片功能主方法"""# 创建选择窗口并设置为顶级窗口top = tk.Toplevel(self.root)top.title("视频分片设置")top.resizable(False, False)top.attributes('-topmost', True)  # 设置为最顶层top.grab_set()  # 独占焦点# 存储选择的路径selected_paths = {'video_dir': tk.StringVar(value=self.video_dir),'txt_dir': tk.StringVar(value=self.save_dir),'output_dir': tk.StringVar()}# 创建进度条变量progress_var = tk.DoubleVar()progress_var.set(0)# 创建主框架main_frame = tk.Frame(top, padx=10, pady=10)main_frame.pack()# 视频目录选择def select_video_dir():top.attributes('-topmost', False)  # 临时取消最顶层属性dir_path = filedialog.askdirectory(title="选择视频目录",initialdir=self.video_dir,parent=top  # 指定父窗口)top.attributes('-topmost', True)  # 恢复最顶层属性if dir_path:selected_paths['video_dir'].set(dir_path)video_frame = tk.Frame(main_frame)video_frame.pack(fill=tk.X, pady=5)tk.Label(video_frame, text="视频目录:").pack(side=tk.LEFT)tk.Entry(video_frame, textvariable=selected_paths['video_dir'], width=40).pack(side=tk.LEFT, padx=5)tk.Button(video_frame, text="浏览...", command=select_video_dir).pack(side=tk.LEFT)# TXT目录选择def select_txt_dir():top.attributes('-topmost', False)dir_path = filedialog.askdirectory(title="选择TXT目录",initialdir=self.save_dir,parent=top)top.attributes('-topmost', True)if dir_path:selected_paths['txt_dir'].set(dir_path)txt_frame = tk.Frame(main_frame)txt_frame.pack(fill=tk.X, pady=5)tk.Label(txt_frame, text="TXT目录:").pack(side=tk.LEFT)tk.Entry(txt_frame, textvariable=selected_paths['txt_dir'], width=40).pack(side=tk.LEFT, padx=5)tk.Button(txt_frame, text="浏览...", command=select_txt_dir).pack(side=tk.LEFT)# 输出目录选择def select_output_dir():top.attributes('-topmost', False)dir_path = filedialog.askdirectory(title="选择输出目录",parent=top)top.attributes('-topmost', True)if dir_path:selected_paths['output_dir'].set(dir_path)output_frame = tk.Frame(main_frame)output_frame.pack(fill=tk.X, pady=5)tk.Label(output_frame, text="输出目录:").pack(side=tk.LEFT)tk.Entry(output_frame, textvariable=selected_paths['output_dir'], width=40).pack(side=tk.LEFT, padx=5)tk.Button(output_frame, text="浏览...", command=select_output_dir).pack(side=tk.LEFT)# 进度条progress_frame = tk.Frame(main_frame)progress_frame.pack(fill=tk.X, pady=10)tk.Label(progress_frame, text="进度:").pack(side=tk.LEFT)progress_bar = ttk.Progressbar(progress_frame, variable=progress_var, maximum=100)progress_bar.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)# 状态标签status_label = tk.Label(main_frame, text="", fg="blue")status_label.pack()# 确认按钮def start_processing():# 验证输入if not selected_paths['output_dir'].get():self.show_custom_message("请选择输出目录")return# 禁用按钮confirm_btn.config(state=tk.DISABLED)# 开始处理try:success = self._process_videos(video_dir=selected_paths['video_dir'].get(),txt_dir=selected_paths['txt_dir'].get(),output_dir=selected_paths['output_dir'].get(),progress_var=progress_var,status_label=status_label,top_window=top)# 处理完成后关闭进度窗口top.grab_release()top.destroy()# 显示完成消息(会自动置顶)self.show_custom_message("视频分片完成!")except Exception as e:# 出错时也关闭进度窗口top.grab_release()top.destroy()self.show_custom_message(f"处理出错: {str(e)}")confirm_btn = tk.Button(main_frame, text="开始分片", command=start_processing)confirm_btn.pack(pady=10)# 窗口关闭时的处理def on_closing():top.grab_release()top.destroy()top.protocol("WM_DELETE_WINDOW", on_closing)# 窗口居中top.update_idletasks()width = top.winfo_width()height = top.winfo_height()x = (top.winfo_screenwidth() // 2) - (width // 2)y = (top.winfo_screenheight() // 2) - (height // 2)top.geometry(f'+{x}+{y}')def _process_videos(self, video_dir, txt_dir, output_dir, progress_var, status_label, top_window):"""实际处理视频的方法"""# 确保输出目录存在if not os.path.exists(output_dir):os.makedirs(output_dir)# 获取所有txt文件txt_files = [f for f in os.listdir(txt_dir) if f.endswith('.txt')]total_files = len(txt_files)for i, txt_file in enumerate(txt_files):# 更新进度和状态progress = (i + 1) / total_files * 100progress_var.set(progress)status_label.config(text=f"正在处理 {txt_file} ({i + 1}/{total_files})")top_window.update_idletasks()  # 使用传入的窗口对象更新UI# 获取对应的视频文件路径video_name = os.path.splitext(txt_file)[0]video_path = os.path.join(video_dir, video_name)# 检查是否有对应的视频文件(支持多种视频格式)video_extensions = ['.mp4', '.avi', '.mov', '.mkv']found_video = Falsefor ext in video_extensions:if os.path.exists(video_path + ext):video_path += extfound_video = Truebreakif not found_video:continue# 读取视频cap = cv2.VideoCapture(video_path)if not cap.isOpened():continuefps = cap.get(cv2.CAP_PROP_FPS)total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 读取txt文件内容txt_path = os.path.join(txt_dir, txt_file)with open(txt_path, 'r') as f:lines = f.readlines()# 处理每一行标记for line in lines:parts = line.strip().split()if len(parts) < 3:continuestart_frame = int(parts[0])end_frame = int(parts[1])action = parts[2]# 确保行为文件夹存在action_folder = os.path.join(output_dir, action)if not os.path.exists(action_folder):os.makedirs(action_folder)# 创建输出视频文件名output_name = f"{video_name}_{start_frame}_{end_frame}_{action}.mp4"output_path = os.path.join(action_folder, output_name)# 设置视频写入器fourcc = cv2.VideoWriter_fourcc(*'mp4v')frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))out = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height))# 跳转到起始帧cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)# 读取并写入指定范围内的帧for frame_num in range(start_frame, end_frame + 1):ret, frame = cap.read()if not ret:breakout.write(frame)out.release()cap.release()return True  # 返回成功状态def debug(self, string):if self.mode == 'debug':print(string)def info(self, string):if self.mode == 'info' or self.mode == 'debug':print(string)if __name__ == "__main__":root = tk.Tk()app = BehaviLabel(root, 'debug')root.mainloop()

最后:

        之后会做行为检测的模型以及对kinetic-400细粒度标记的数据集分享,可以关注一手期待后续。

相关文章:

  • GC1808:高性能音频ADC的卓越之选
  • goreplay
  • iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
  • Kafka主题运维全指南:从基础配置到故障处理
  • glb/gltf格式批量转换fbx/obj,材质贴图在,批量转换stl/dae等其他格式,无需一个个打开
  • 消息队列系统设计与实践全解析
  • 面试高频问题
  • Docker环境下安装 Elasticsearch + IK 分词器 + Pinyin插件 + Kibana(适配7.10.1)
  • 大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
  • 【Linux】Linux安装并配置RabbitMQ
  • Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
  • 企业数据备份与恢复管理制度
  • 【 java 虚拟机知识 第一篇 】
  • 融智学本体论体系全景图
  • linux常用基础命令_新
  • Linux信号保存与处理机制详解
  • MySQL 主从同步异常处理
  • 【PySpark安装配置】01 搭建单机模式的PySpark开发环境(Windows系统)
  • 【C++】unordered_set和unordered_map
  • 生信服务器 | 做生信为什么推荐使用Linux服务器?
  • 广州 网站建设/网页设计主要做什么
  • 电子商务网站开发实例/下载百度app下载
  • 做网站被用作非法用途/网站优化排名公司哪家好
  • 网站建设好后的手续交接/google国外入口
  • 网站建设方案编写人/站长工具站长之家官网
  • 怎么免费制作一个企业网站/深圳全网推广