# coding=UTF-8
"""
theme: pyqt5实现动作起始帧和结束帧的定位,将定位到的帧数保存json文件
time: 2024-6-27
author: cong
"""
import json
import os
import re
import sys
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from functools import partial
# 使用 QMediaPlayer 可以进行视频文件解码,视频播放必须将视频帧在某个界面组件上显示,
# 有 QVideoWidget 和 QGraphicsVideoItem 两种视频显示组件,也可以从这两个类继承,自定义视频显示组件。
# QMediaPlayer 也可以结合 QMediaPlaylist 实现视频文件列表播放。
class VideoWin(QWidget):
save_flag = 0
save_start_count = 1
save_end_count = 2
len_load_json = 0
def __init__(self):
super(VideoWin, self).__init__()
self.num_end = None
self.setWindowTitle("MediaPlayer")
# 播放画面
self.player = QMediaPlayer()
self.video_widget = QVideoWidget(self) # 定义视频显示的widget,界面组件
self.video_widget.setFixedSize(1280, 720)
self.player.setVideoOutput(self.video_widget) # 视频播放输出的widget,就是上面定义的
self.video_path = ''
# 当前播放的进度,显示调整视频进度条
self.label_time = QLabel()
self.timeSlider = QSlider()
self.timeSlider.setOrientation(Qt.Horizontal)
self.timeSlider.setValue(0)
self.timeSlider.setMinimum(0)
self.player.positionChanged.connect(self.get_time)
self.timeSlider.sliderPressed.connect(self.player.pause)
self.timeSlider.sliderMoved.connect(self.change_time)
self.timeSlider.sliderReleased.connect(self.player.play)
# 打开视频
self.open_button = QPushButton('打开视频')
self.open_button.clicked.connect(self.open_file)
# 快进
self.right_button = QPushButton('快进')
self.right_button.clicked.connect(self.up_time)
# play
self.play_button = QPushButton('播放')
self.play_button.clicked.connect(self.player.play)
# pause
self.mid_button = QPushButton('暂停')
self.mid_button.clicked.connect(self.player.pause)
# 快退
self.left_button = QPushButton('快退')
self.left_button.clicked.connect(self.down_time)
# 保存开始时间
self.start_button = QPushButton('保存动作开始时间')
self.start_button.clicked.connect(self.save_start_time)
# 保存结束时间
self.end_button = QPushButton('保存动作结束时间')
self.end_button.clicked.connect(self.save_end_time)
# # 进度条跳转
jump_layout = QVBoxLayout()
for i in range(30):
button = QPushButton(f'跳转 {i + 1}')
button.setFixedSize(50, 21)
jump_layout.addWidget(button)
# 给每个按钮连接一个槽函数(示例中没有实际的槽函数)
button.clicked.connect(partial(self.jump, i))
# 加载json
self.load_json_button = QPushButton('加载json')
self.load_json_button.setFixedSize(80, 40)
self.load_json_button.clicked.connect(self.load_json)
# 所有时间选定,最终保存按钮
self.done_button = QPushButton('完成并保存')
self.done_button.setFixedSize(80, 40)
self.done_button.clicked.connect(self.save_json)
# label_names布局
self.labels_names_entry = QLineEdit()
labels_names =['一道', '七道', '三道', '九道', '二道', '五道', '停车信号', '八道', '六道', '减速信号',
'十一道', '十三道', '十二道', '十五道', '四道', '指挥机车向显示人反方向去的信号', '指挥机车向显示人方向来的信号',
'指挥机车向显示人方向稍行移动的信号', '道岔开通信号']
self.labels_names_entry.setText(str(labels_names))
# 视频路径 entry 布局
self.path_entry = QLineEdit()
# 创建一个网格布局
grid_layout = QGridLayout()
self.grid_num = 90
self.entry_names = [f'entry_{i + 1}' for i in range(self.grid_num)]
for i in range(self.grid_num):
self.entry_names[i] = QLineEdit(self)
self.entry_names[i].setFixedWidth(60)
if i % 3 == 0:
label = QLabel(f"start_time_d{int((i + 3) // 3)}:")
grid_layout.addWidget(label, i // 3, (i % 3) * 3)
grid_layout.addWidget(self.entry_names[i], i // 3, (i % 3) * 3 + 1)
elif i % 3 == 1:
label = QLabel(f"end_time_d{int((i + 2) // 3)}:")
grid_layout.addWidget(label, i // 3, (i % 3) + 1)
grid_layout.addWidget(self.entry_names[i], i // 3, (i % 3) + 2)
else:
label = QLabel(f"label_name_d{int((i + 1) // 3)}:")
grid_layout.addWidget(label, i // 3, (i % 3) + 2)
grid_layout.addWidget(self.entry_names[i], i // 3, (i % 3) + 3)
# label.setFixedSize(60,20)
# self.entry_names[i].setFixedWidth(50)
# grid_layout.addWidget(label, i // 3, (i % 3)*3)
# grid_layout.addWidget(self.entry_names[i], i // 3,(i % 3)*3+1)
# 上述按钮布局
button_layout = QHBoxLayout()
button_layout.addWidget(self.open_button)
button_layout.addWidget(self.right_button)
button_layout.addWidget(self.play_button)
button_layout.addWidget(self.mid_button)
button_layout.addWidget(self.left_button)
button_layout.addWidget(self.start_button)
button_layout.addWidget(self.end_button)
# 左侧布局
left_layout = QVBoxLayout()
left_layout.addWidget(self.video_widget)
left_layout.addWidget(self.label_time, alignment=Qt.AlignRight)
left_layout.addWidget(self.timeSlider)
left_layout.addLayout(button_layout)
# left_layout.addSpacing(100)
left_layout.addWidget(QLabel("动作标签:"))
left_layout.addWidget(self.labels_names_entry)
left_layout.addWidget(QLabel("视频路径:"))
left_layout.addWidget(self.path_entry)
json_layout = QHBoxLayout()
json_layout.addWidget(self.load_json_button)
json_layout.addWidget(QLabel("路径:"))
self.json_path_entry = QLineEdit()
json_layout.addWidget(self.json_path_entry)
left_layout.addLayout(json_layout)
# 中间布局
middle_layout = QHBoxLayout()
middle_layout.addLayout(grid_layout)
middle_layout.addLayout(jump_layout)
# 最右侧布局
right_layout = QVBoxLayout()
right_layout.addWidget(self.done_button)
# 总布局
all_layout = QHBoxLayout()
all_layout.addLayout(left_layout)
all_layout.addLayout(middle_layout)
all_layout.addLayout(right_layout)
self.setLayout(all_layout)
self.showMaximized()
# 设置播放进度检查定时器
self.position_check_timer = QTimer(self)
self.position_check_timer.setInterval(10) # 设置定时器间隔为100毫秒
# 打开视频
def open_file(self):
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
a = QFileDialog.getOpenFileUrl()[0]
if a.toString() == '' or not any(a.toString().endswith(ext) for ext in video_extensions):
QMessageBox.information(self, '提示', "请打开符合格式的视频文件")
return
self.video_path = re.split('file:///', a.toString())[1]
self.player.setMedia(QMediaContent(a)) # 选取视频文件
msg = QMessageBox.information(self, '提示', "已经打开视频文件")
self.path_entry.setText(self.video_path)
# 自动调用json文件
file_path = re.split('/', self.video_path)[-1]
json_path_name = file_path.replace(file_path.split('.')[-1], 'json')
json_names = os.listdir('json_output')
if json_path_name in json_names:
json_path = os.path.join('json_output', json_path_name)
self.json_path_entry.setText(json_path)
with open(json_path, encoding='utf-8') as f:
data = json.load(f)
# print('data', data)
# self.path_entry.setText(data['video_path'])
split_result = data['split_result']
for i in range(len(split_result)):
self.entry_names[3 * i].setText(str(split_result[i]['beginTime']))
self.entry_names[3 * i + 1].setText(str(split_result[i]['endTime']))
self.entry_names[3 * i + 2].setText(str(split_result[i]['label']))
self.len_load_json = len(split_result)
self.save_start_count_load = self.len_load_json
self.save_end_count_load = self.len_load_json
QMessageBox.information(self, '提示', "已经自动加载json文件")
else:
QMessageBox.information(self, '提示', "未找到对应json文件")
# 调节播放进度
def change_time(self, num):
self.player.setPosition(num)
# 快进
def up_time(self):
# print(self.player.duration())
# num = self.player.position() + int(self.player.duration() / 20)
num = self.player.position() + 200
self.player.setPosition(num)
# 快退
def down_time(self):
# num = self.player.position() - int(self.player.duration() / 20)
num = self.player.position() - 200
self.player.setPosition(num)
# 获取进度条进度
def get_time(self, num):
self.timeSlider.setMaximum(self.player.duration())
self.timeSlider.setValue(num)
# frame_count = int(num / 1000 * 30)
# d = QDateTime.fromMSecsSinceEpoch(num).toString("mm:ss")
# print(d)
all = self.player.duration()
# total_count = int(all / 1000 * 30)
# all_d = QDateTime.fromMSecsSinceEpoch(all).toString("mm:ss")
self.label_time.setText(str(self.player.position()) + '/' + str(all))
def jump(self, jump_button_index):
# print(jump_button_index)
num_start = int(self.entry_names[jump_button_index * 3].text())
self.num_end = int(self.entry_names[jump_button_index * 3 + 1].text())
if num_start == '' or self.num_end == '':
QMessageBox.information(self, "提示", f"请先保存动作时间")
return
self.position_check_timer.stop()
self.position_check_timer.timeout.connect(self.check_position)
# 停止播放进度检查定时器(如果之前有启动的话)
# 设置播放起始位置
self.player.setPosition(num_start) # 转换为毫秒
# 启动播放进度检查定时器
self.position_check_timer.start()
# 开始播放
self.player.play()
def check_position(self):
# 获取当前播放的位置(毫秒)
current_position = self.player.position()
print('current_position', current_position)
# 如果达到设置的结束时间,停止播放
if current_position >= self.num_end:
self.player.pause()
self.position_check_timer.stop()
def closeEvent(self, event): # 关闭前需要self.player.pause()操作,否则报错
self.player.pause()
reply = QMessageBox.question(self, '提示',
"是否退出",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
def save_start_time(self):
if self.save_flag == 0:
self.save_flag = 1
start_time = self.player.position()
# print('start time:', start_time)
# start_frame = int(start_time / 1000 * 30)
if self.len_load_json != 0:
self.entry_names[self.save_start_count_load * 3].setText(str(start_time))
self.save_start_count_load += 1
else:
self.entry_names[self.save_start_count - 1].setText(str(start_time))
self.save_start_count += 3
# QMessageBox.information(self, "保存成功", f"已保存当前时间点:第{start_frame}帧 ")
else:
QMessageBox.information(self, "保存失败", f"请先保存动作结束时间 ")
def save_end_time(self):
if self.save_flag == 1:
self.save_flag = 0
end_time = self.player.position()
# end_frame = int(end_time / 1000 * 30)
if self.len_load_json != 0:
self.entry_names[self.save_end_count_load * 3 + 1].setText(str(end_time))
self.save_end_count_load += 1
else:
self.entry_names[self.save_end_count - 1].setText(str(end_time))
self.save_end_count += 3
# QMessageBox.information(self, "保存成功", f"已保存当前时间点:第{end_frame}帧 ")
else:
QMessageBox.information(self, "保存失败", f"请先保存动作开始时间 ")
def load_json(self):
os.makedirs('json_output', exist_ok=True)
# 默认打开'json_output'
json_path = QFileDialog.getOpenFileName(self, '打开json文件', 'json_output', 'json文件(*.json)')
if json_path[0] == '':
return
print(json_path)
self.json_path = os.path.basename(json_path[0])
self.json_path = os.path.join('json_output', self.json_path)
if not self.json_path.endswith('.json'):
QMessageBox.information(self, "加载失败", f"请先打开json文件")
return
self.json_path_entry.setText(self.json_path)
with open(self.json_path, encoding='utf-8') as f:
data = json.load(f)
print('data', data)
# self.path_entry.setText(data['video_path'])
split_result = data['split_result']
for i in range(len(split_result)):
self.entry_names[3*i].setText(str(split_result[i]['beginTime']))
self.entry_names[3*i+1].setText(str(split_result[i]['endTime']))
self.entry_names[3*i+2].setText(str(split_result[i]['label']))
# self.len_load_json = len(split_result)
# print('len_load_json', self.len_load_json)
def save_json(self):
result = {}
single_part = {}
if self.video_path == '':
QMessageBox.information(self, "保存失败", f"请先打开视频文件")
return
video_path = self.video_path
print('当前保存结果来源于视频文件', video_path)
result['video_path'] = video_path
result['split_result'] = []
file_path = re.split('/', video_path)[-1]
# print('file_path', file_path)
# json_path_name = file_path.split('.mkv')[0] + '.json'
json_path_name = file_path.replace(file_path.split('.')[-1], 'json')
json_path_name = os.path.join('json_output', os.path.basename(json_path_name))
os.makedirs('json_output', exist_ok=True)
for i in range(len(self.entry_names)):
if self.entry_names[i].text() == '':
continue
if i % 3 == 0:
label_key = f"beginTime"
single_part[label_key] = int(self.entry_names[i].text())
# print('ssssssssssssssssss')
elif i % 3 == 1:
label_key = f"endTime"
single_part[label_key] = int(self.entry_names[i].text())
# print('########################')
else:
# print('labels_names:',self.labels_names_entry.text())
if self.entry_names[i].text() not in self.labels_names_entry.text():
QMessageBox.information(self, "保存失败", f"标签名字不在标签列表中")
break
else:
label_key = f"label"
single_part[label_key] = self.entry_names[i].text()
result['split_result'].append(single_part)
single_part = {}
# print('single_part:', single_part)
if len(single_part) % 3 != 0:
QMessageBox.information(self, "保存失败", f"请检查是否有漏缺,数据长度不对")
with open(json_path_name, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=4)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
win = VideoWin()
win.show()
sys.exit(app.exec())