Files
video-subtitle-remover/ui/component/video_display_component.py
Jason f78e985e1c 使用PySide6-Fluent-Widgets重构整套UI
添加任务列表组件并优化视频加载逻辑
支持可视化显示字幕区域
整理所有模型, 分别为STTN智能擦除, STTN字幕检测, LAMA, ProPainter, OpenCV
提高处理性能
新增CPU运行模式并优化多语言支持
修复Propainter模式部分视频报错

本次提交新增了CPU运行模式,适用于无GPU加速的场景。同时,优化了多语言支持,新增了日语、韩语、越南语等语言配置文件,并更新了README文档以反映新的运行模式和多语言支持。此外,修复了部分代码逻辑,提升了系统的稳定性和兼容性。
2025-05-22 08:41:59 +08:00

566 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import cv2
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from PySide6.QtCore import Qt, Signal, QRect, QRectF, QTimer, QObject, QEvent
from PySide6 import QtCore, QtWidgets, QtGui
from qfluentwidgets import qconfig, CardWidget, HollowHandleStyle
from backend.config import config, tr
class VideoDisplayComponent(QWidget):
"""视频显示组件,包含视频预览和选择框功能"""
# 定义信号
selection_changed = Signal(QRect) # 选择框变化信号
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
# 初始化变量
self.is_drawing = False
self.selection_rect = QRect()
self.drag_start_pos = None
self.resize_edge = None
self.edge_size = 10 # 调整大小的边缘区域
self.enable_mouse_events = True # 控制是否启用鼠标事件
# 获取屏幕大小
screen = QtWidgets.QApplication.primaryScreen().size()
self.screen_width = screen.width()
self.screen_height = screen.height()
# 设置视频预览区域大小(根据屏幕宽度动态调整)
self.video_preview_width = 960
self.video_preview_height = self.video_preview_width * 9 // 16
if self.screen_width // 2 < 960:
self.video_preview_width = 640
self.video_preview_height = self.video_preview_width * 9 // 16
# 视频相关参数
self.frame_width = None
self.frame_height = None
self.scaled_width = None
self.scaled_height = None
self.border_left = 0
self.border_top = 0
# 保存选择框的相对位置和大小(相对于实际视频的比例)
self.selection_ratio = None
self.__initWidget()
def __initWidget(self):
"""初始化组件"""
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
# 视频预览区域和进度条容器
self.video_container = CardWidget(self)
self.video_container.setObjectName('videoContainer')
video_layout = QVBoxLayout()
video_layout.setSpacing(0)
video_layout.setContentsMargins(2, 2, 2, 2)
video_layout.setAlignment(Qt.AlignCenter)
# 创建内部黑色背景容器
self.black_container = QWidget(self)
self.black_container.setObjectName('blackContainer')
self.black_container.setStyleSheet("""
#blackContainer {
background-color: black;
border-radius: 10px;
border: 0px solid transparent;
}
""")
black_layout = QVBoxLayout()
black_layout.setContentsMargins(0, 0, 0, 0)
black_layout.setSpacing(0)
black_layout.setAlignment(Qt.AlignCenter)
# 视频显示标签
self.video_display = QtWidgets.QLabel()
self.video_display.setStyleSheet("""
background-color: black;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: 0px solid transparent;
""")
self.video_display.setMinimumSize(self.video_preview_width, self.video_preview_height)
self.video_display.setMouseTracking(True)
self.video_display.setScaledContents(True)
self.video_display.setAlignment(Qt.AlignCenter)
self.video_display.mousePressEvent = self.selection_mouse_press
self.video_display.mouseMoveEvent = self.selection_mouse_move
self.video_display.mouseReleaseEvent = self.selection_mouse_release
# 视频滑块
self.video_slider = QtWidgets.QSlider(Qt.Horizontal)
self.video_slider.setMinimum(1)
self.video_slider.setFixedHeight(22)
self.video_slider.setMaximum(100) # 默认最大值设为100与进度百分比一致
self.video_slider.setValue(1)
self.video_slider.setStyle(HollowHandleStyle({
"handle.color": QtGui.QColor(255, 255, 255),
"handle.ring-width": 4,
"handle.hollow-radius": 6,
"handle.margin": 1
}))
# 视频预览区域
self.video_display.setObjectName('videoDisplay')
# black_layout.addWidget(self.video_display, 0, Qt.AlignCenter)
# 创建一个容器来保持比例
ratio_container = QWidget()
ratio_layout = QVBoxLayout(ratio_container)
ratio_layout.setContentsMargins(0, 0, 0, 0)
ratio_layout.addWidget(self.video_display)
# 设置固定的宽高比
ratio_container.setFixedHeight(ratio_container.width() * 9 // 16)
ratio_container.setMinimumWidth(self.video_preview_width)
# 添加到布局
black_layout.addWidget(ratio_container)
# 添加一个事件过滤器来处理大小变化
class RatioEventFilter(QObject):
def eventFilter(self, obj, event):
if event.type() == QEvent.Resize:
obj.setFixedHeight(obj.width() * 9 // 16)
return False
ratio_filter = RatioEventFilter(ratio_container)
ratio_container.installEventFilter(ratio_filter)
# 进度条和滑块容器
control_container = QWidget(self)
control_layout = QVBoxLayout()
control_layout.setContentsMargins(8, 8, 8, 8)
control_layout.addWidget(self.video_slider)
control_container.setLayout(control_layout)
control_container.setStyleSheet("""
background-color: black;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
""")
black_layout.addWidget(control_container)
self.black_container.setLayout(black_layout)
video_layout.addWidget(self.black_container)
self.video_container.setLayout(video_layout)
main_layout.addWidget(self.video_container)
def update_video_display(self, frame, draw_selection=True):
"""更新视频显示"""
if frame is None:
return
# 调整视频帧大小以适应视频预览区域
frame = cv2.resize(frame, (self.video_preview_width, self.video_preview_height))
# 将 OpenCV 帧BGR 格式)转换为 QImage 并显示在 QLabel 上
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
image = QtGui.QImage(rgb_frame.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888)
pix = QtGui.QPixmap.fromImage(image)
# 创建带圆角的图像
rounded_pix = QtGui.QPixmap(pix.size())
rounded_pix.fill(Qt.transparent) # 填充透明背景
painter = QtGui.QPainter(rounded_pix)
painter.setRenderHint(QtGui.QPainter.Antialiasing) # 抗锯齿
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
# 创建圆角路径
path = QtGui.QPainterPath()
rect = QRectF(0, 0, pix.width(), pix.height())
# 手动创建只有左上和右上圆角的路径
radius = 8
path.moveTo(radius, 0)
path.lineTo(pix.width() - radius, 0)
path.arcTo(pix.width() - radius * 2, 0, radius * 2, radius * 2, 90, -90)
path.lineTo(pix.width(), pix.height())
path.lineTo(0, pix.height())
path.lineTo(0, radius)
path.arcTo(0, 0, radius * 2, radius * 2, 180, -90)
path.closeSubpath()
painter.setClipPath(path)
painter.drawPixmap(0, 0, pix)
painter.end()
# 保存当前的pixmap用于绘制选择框
self.current_pixmap = rounded_pix.copy()
self.video_display.setPixmap(rounded_pix)
# 如果有保存的选择框比例,根据新视频尺寸重新计算选择框
if draw_selection and self.selection_ratio is not None and self.scaled_width and self.scaled_height:
x_ratio, y_ratio, w_ratio, h_ratio = self.selection_ratio
# 计算新的选择框坐标和大小
x = int(x_ratio * self.scaled_width) + self.border_left
y = int(y_ratio * self.scaled_height) + self.border_top
w = int(w_ratio * self.scaled_width)
h = int(h_ratio * self.scaled_height)
# 创建新的选择框
self.selection_rect = QRect(x, y, w, h)
# 更新视频显示
self.update_preview_with_rect()
def update_preview_with_rect(self, rect=None):
"""更新带有选择框的预览"""
if not hasattr(self, 'current_pixmap') or self.current_pixmap is None:
return
# 如果提供了新的矩形,使用它
if rect is not None:
self.selection_rect = rect
# 创建一个副本用于绘制
pixmap_copy = self.current_pixmap.copy()
painter = QtGui.QPainter(pixmap_copy)
# 设置选择框样式
pen = QtGui.QPen(QtGui.QColor(0, 255, 0)) # 绿色
pen.setWidth(2)
painter.setPen(pen)
# 绘制选择框
painter.drawRect(self.selection_rect)
painter.end()
# 更新显示
self.video_display.setPixmap(pixmap_copy)
def selection_mouse_press(self, event):
"""鼠标按下事件处理"""
if not self.enable_mouse_events:
return
pos = event.pos()
# 检测双击或三击,重置选择框
if event.type() == QtCore.QEvent.MouseButtonDblClick:
self.selection_rect = QRect(pos, pos)
self.resize_edge = None
self.is_drawing = True
self.drag_start_pos = pos
return
# 检查是否在选择框边缘(用于调整大小)
if self.selection_rect.isValid():
# 右下角
if abs(pos.x() - self.selection_rect.right()) <= self.edge_size and abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size:
self.resize_edge = "bottomright"
self.drag_start_pos = pos
return
# 右上角
elif abs(pos.x() - self.selection_rect.right()) <= self.edge_size and abs(pos.y() - self.selection_rect.top()) <= self.edge_size:
self.resize_edge = "topright"
self.drag_start_pos = pos
return
# 左下角
elif abs(pos.x() - self.selection_rect.left()) <= self.edge_size and abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size:
self.resize_edge = "bottomleft"
self.drag_start_pos = pos
return
# 左边缘
elif abs(pos.x() - self.selection_rect.left()) <= self.edge_size and self.selection_rect.top() <= pos.y() <= self.selection_rect.bottom():
self.resize_edge = "left"
self.drag_start_pos = pos
return
# 右边缘
elif abs(pos.x() - self.selection_rect.right()) <= self.edge_size and self.selection_rect.top() <= pos.y() <= self.selection_rect.bottom():
self.resize_edge = "right"
self.drag_start_pos = pos
return
# 上边缘
elif abs(pos.y() - self.selection_rect.top()) <= self.edge_size and self.selection_rect.left() <= pos.x() <= self.selection_rect.right():
self.resize_edge = "top"
self.drag_start_pos = pos
return
# 下边缘
elif abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size and self.selection_rect.left() <= pos.x() <= self.selection_rect.right():
self.resize_edge = "bottom"
self.drag_start_pos = pos
return
# 左上角
elif abs(pos.x() - self.selection_rect.left()) <= self.edge_size and abs(pos.y() - self.selection_rect.top()) <= self.edge_size:
self.resize_edge = "topleft"
self.drag_start_pos = pos
return
# 在选择框内部(用于移动)
elif self.selection_rect.contains(pos):
self.resize_edge = "move"
self.drag_start_pos = pos
return
# 开始新的选择
self.is_drawing = True
self.selection_rect = QRect(pos, pos)
self.drag_start_pos = pos
self.resize_edge = None
def selection_mouse_move(self, event):
"""鼠标移动事件处理"""
if not self.enable_mouse_events:
return
pos = event.pos()
# 根据不同的操作模式处理鼠标移动
if self.is_drawing: # 绘制新选择框
self.selection_rect.setBottomRight(pos)
self.update_preview_with_rect()
elif self.resize_edge: # 调整选择框大小或位置
if self.resize_edge == "move":
# 移动整个选择框
dx = pos.x() - self.drag_start_pos.x()
dy = pos.y() - self.drag_start_pos.y()
# 保存原始选择框尺寸
original_width = self.selection_rect.width()
original_height = self.selection_rect.height()
# 计算新位置
new_rect = self.selection_rect.translated(dx, dy)
# 获取视频显示区域
display_rect = self.video_display.rect()
# 检查是否超出边界,如果超出则调整位置但保持尺寸
if new_rect.left() < 0:
new_rect.moveLeft(0)
if new_rect.top() < 0:
new_rect.moveTop(0)
if new_rect.right() > display_rect.width():
new_rect.moveRight(display_rect.width())
if new_rect.bottom() > display_rect.height():
new_rect.moveBottom(display_rect.height())
# 确保尺寸不变
if new_rect.width() != original_width or new_rect.height() != original_height:
# 如果尺寸变了,恢复原始尺寸
if new_rect.left() == 0:
new_rect.setWidth(original_width)
if new_rect.top() == 0:
new_rect.setHeight(original_height)
if new_rect.right() == display_rect.width():
new_rect.setLeft(new_rect.right() - original_width)
if new_rect.bottom() == display_rect.height():
new_rect.setTop(new_rect.bottom() - original_height)
self.selection_rect = new_rect
self.drag_start_pos = pos
else:
# 调整选择框大小
if "left" in self.resize_edge:
self.selection_rect.setLeft(pos.x())
if "right" in self.resize_edge:
self.selection_rect.setRight(pos.x())
if "top" in self.resize_edge:
self.selection_rect.setTop(pos.y())
if "bottom" in self.resize_edge:
self.selection_rect.setBottom(pos.y())
# 确保选择框在视频显示区域内
display_rect = self.video_display.rect()
if self.selection_rect.left() < 0:
self.selection_rect.setLeft(0)
if self.selection_rect.top() < 0:
self.selection_rect.setTop(0)
if self.selection_rect.right() > display_rect.width():
self.selection_rect.setRight(display_rect.width())
if self.selection_rect.bottom() > display_rect.height():
self.selection_rect.setBottom(display_rect.height())
self.update_preview_with_rect()
else:
# 更新鼠标指针形状
self.update_cursor_shape(pos)
def selection_mouse_release(self, event):
"""鼠标释放事件处理"""
if not self.enable_mouse_events:
return
# 结束绘制或调整
self.is_drawing = False
self.resize_edge = None
# 标准化选择框(确保宽度和高度为正)
self.selection_rect = self.selection_rect.normalized()
# 保存选择框的相对位置和大小
self.save_selection_ratio()
# 发送选择框变化信号
self.selection_changed.emit(self.selection_rect)
def update_cursor_shape(self, pos):
"""根据鼠标位置更新光标形状"""
if not self.selection_rect.isValid():
self.video_display.setCursor(Qt.ArrowCursor)
return
# 检查鼠标是否在选择框边缘
if (abs(pos.x() - self.selection_rect.left()) <= self.edge_size and
self.selection_rect.top() <= pos.y() <= self.selection_rect.bottom()):
self.video_display.setCursor(Qt.SizeHorCursor)
elif (abs(pos.x() - self.selection_rect.right()) <= self.edge_size and
self.selection_rect.top() <= pos.y() <= self.selection_rect.bottom()):
self.video_display.setCursor(Qt.SizeHorCursor)
elif (abs(pos.y() - self.selection_rect.top()) <= self.edge_size and
self.selection_rect.left() <= pos.x() <= self.selection_rect.right()):
self.video_display.setCursor(Qt.SizeVerCursor)
elif (abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size and
self.selection_rect.left() <= pos.x() <= self.selection_rect.right()):
self.video_display.setCursor(Qt.SizeVerCursor)
elif (abs(pos.x() - self.selection_rect.left()) <= self.edge_size and
abs(pos.y() - self.selection_rect.top()) <= self.edge_size):
self.video_display.setCursor(Qt.SizeFDiagCursor)
elif (abs(pos.x() - self.selection_rect.right()) <= self.edge_size and
abs(pos.y() - self.selection_rect.top()) <= self.edge_size):
self.video_display.setCursor(Qt.SizeBDiagCursor)
elif (abs(pos.x() - self.selection_rect.left()) <= self.edge_size and
abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size):
self.video_display.setCursor(Qt.SizeBDiagCursor)
elif (abs(pos.x() - self.selection_rect.right()) <= self.edge_size and
abs(pos.y() - self.selection_rect.bottom()) <= self.edge_size):
self.video_display.setCursor(Qt.SizeFDiagCursor)
elif self.selection_rect.contains(pos):
self.video_display.setCursor(Qt.SizeAllCursor)
else:
self.video_display.setCursor(Qt.ArrowCursor)
def set_video_parameters(self, frame_width, frame_height, scaled_width=None, scaled_height=None, border_left=0, border_top=0):
"""设置视频参数"""
self.frame_width = frame_width
self.frame_height = frame_height
self.scaled_width = scaled_width
self.scaled_height = scaled_height
self.border_left = border_left
self.border_top = border_top
def get_selection_coordinates(self):
"""获取选择框坐标"""
return self.selection_rect
def set_selection_rect(self, rect):
"""设置选择框"""
self.selection_rect = rect
self.save_selection_ratio()
self.update_preview_with_rect()
def load_selection_ratio(self):
"""从配置中加载选择框的相对位置和大小"""
# 检查是否有有效的视频尺寸
if not self.scaled_width or not self.scaled_height:
return False
# 从配置中读取选择框的相对位置和大小
x_ratio = config.subtitleSelectionAreaX.value
y_ratio = config.subtitleSelectionAreaY.value
w_ratio = config.subtitleSelectionAreaW.value
h_ratio = config.subtitleSelectionAreaH.value
# 检查配置值是否有效
if x_ratio is None or y_ratio is None or w_ratio is None or h_ratio is None:
return False
# 检查配置值是否在有效范围内
if w_ratio <= 0.01 or h_ratio <= 0.005:
config.set(config.subtitleSelectionAreaX, config.subtitleSelectionAreaX.defaultValue)
config.set(config.subtitleSelectionAreaY, config.subtitleSelectionAreaY.defaultValue)
config.set(config.subtitleSelectionAreaW, config.subtitleSelectionAreaW.defaultValue)
config.set(config.subtitleSelectionAreaH, config.subtitleSelectionAreaH.defaultValue)
x_ratio = config.subtitleSelectionAreaX.value
y_ratio = config.subtitleSelectionAreaY.value
w_ratio = config.subtitleSelectionAreaW.value
h_ratio = config.subtitleSelectionAreaH.value
# 保存选择框比例
self.selection_ratio = (x_ratio, y_ratio, w_ratio, h_ratio)
# 计算实际像素坐标
x = int(x_ratio * self.scaled_width) + self.border_left
y = int(y_ratio * self.scaled_height) + self.border_top
w = int(w_ratio * self.scaled_width)
h = int(h_ratio * self.scaled_height)
# 创建选择框
self.selection_rect = QRect(x, y, w, h)
# 更新预览
self.update_preview_with_rect()
return True
def save_selection_ratio(self):
"""保存选择框的相对位置和大小(相对于实际视频的比例)"""
if not self.selection_rect.isValid() or not self.scaled_width or not self.scaled_height:
return
# 调整选择框坐标,考虑黑边偏移
x_adjusted = max(0, self.selection_rect.x() - self.border_left)
y_adjusted = max(0, self.selection_rect.y() - self.border_top)
# 如果选择框超出了实际视频区域,需要调整宽度和高度
w_adjusted = min(self.selection_rect.width(), self.scaled_width - x_adjusted)
h_adjusted = min(self.selection_rect.height(), self.scaled_height - y_adjusted)
# 转换为相对比例
x_ratio = x_adjusted / self.scaled_width
y_ratio = y_adjusted / self.scaled_height
w_ratio = w_adjusted / self.scaled_width
h_ratio = h_adjusted / self.scaled_height
self.selection_ratio = (x_ratio, y_ratio, w_ratio, h_ratio)
config.subtitleSelectionAreaY.value = y_ratio
config.subtitleSelectionAreaH.value = h_ratio
config.subtitleSelectionAreaX.value = x_ratio
config.subtitleSelectionAreaW.value = w_ratio
qconfig.save()
def get_original_coordinates(self):
"""获取选择框在原始视频中的坐标"""
if not self.selection_rect.isValid() or not self.scaled_width or not self.scaled_height:
return None
# 调整选择框坐标,考虑黑边偏移
x_adjusted = max(0, self.selection_rect.x() - self.border_left)
y_adjusted = max(0, self.selection_rect.y() - self.border_top)
# 如果选择框超出了实际视频区域,需要调整宽度和高度
w_adjusted = min(self.selection_rect.width(), self.scaled_width - x_adjusted)
h_adjusted = min(self.selection_rect.height(), self.scaled_height - y_adjusted)
# 转换为原始视频坐标
scale_x = self.frame_width / self.scaled_width
scale_y = self.frame_height / self.scaled_height
xmin = int(x_adjusted * scale_x)
xmax = int((x_adjusted + w_adjusted) * scale_x)
ymin = int(y_adjusted * scale_y)
ymax = int((y_adjusted + h_adjusted) * scale_y)
# 确保坐标在有效范围内
xmin = max(0, min(xmin, self.frame_width))
xmax = max(0, min(xmax, self.frame_width))
ymin = max(0, min(ymin, self.frame_height))
ymax = max(0, min(ymax, self.frame_height))
return (ymin, ymax, xmin, xmax)
def set_dragger_enabled(self, enabled):
"""设置拖动器是否可用"""
self.enable_mouse_events = enabled
self.video_display.setMouseTracking(enabled)
self.video_display.setCursor(Qt.ArrowCursor)