Commit c94177be by Yuhaibo

merge: 保留Liuqingjun分支版本解决冲突

parents c3228c2c 25bb16b1
......@@ -16,6 +16,8 @@
!widgets/**
!handlers/
!handlers/**
!labelme/
!labelme/**
!app.py
!__main__.py
!__init__.py
......
......@@ -47,7 +47,7 @@ class CropPreviewHandler(QtCore.QObject):
# 定时器(用于轮询检测,作为文件监控的补充)
self._poll_timer = None
self._poll_interval = 2000 # 2秒轮询一次
self._poll_interval = 500 # 500毫秒轮询一次(提高实时性)
# 是否启用自动监控
self._auto_monitor_enabled = False
......@@ -98,9 +98,15 @@ class CropPreviewHandler(QtCore.QObject):
print(f"[CropPreviewHandler] 设置保存路径(不自动刷新)")
self._panel.setSavePath(save_liquid_data_path, auto_refresh=False, video_name=video_name)
# 初始化已知图片列表(扫描现有图片,避免重复显示)
print(f"[CropPreviewHandler] 扫描现有图片...")
self._initKnownImages(save_liquid_data_path, video_name)
# 初始化已知图片列表
if clear_first:
# 如果清空显示,则不扫描现有图片,所有图片都视为新增
print(f"[CropPreviewHandler] 清空模式:不扫描现有图片,所有图片将被视为新增")
self._known_images.clear()
else:
# 否则扫描现有图片,避免重复显示
print(f"[CropPreviewHandler] 扫描现有图片...")
self._initKnownImages(save_liquid_data_path, video_name)
# 设置文件系统监控
self._setupFileWatcher(save_liquid_data_path, video_name)
......@@ -113,6 +119,13 @@ class CropPreviewHandler(QtCore.QObject):
# 清除重置标志
self._is_resetting = False
# 如果是清空模式,立即执行一次检查,显示所有现有图片
if clear_first:
print(f"[CropPreviewHandler] 清空模式:立即检查并显示所有现有图片")
for i, region_path in enumerate(self._monitored_paths):
if region_path and osp.exists(region_path):
self._checkRegionForNewImages(i, region_path)
print(f"[CropPreviewHandler] === 监控已启动 ===")
def stopMonitoring(self):
......@@ -409,6 +422,12 @@ class CropPreviewHandler(QtCore.QObject):
# 更新已知图片列表
self._known_images[region_index] = current_images
# 检测到新图片后,立即触发一次额外检查(提高响应速度)
if self._poll_timer and self._poll_timer.isActive():
# 重启定时器,立即进行下一次检查
self._poll_timer.stop()
self._poll_timer.start(self._poll_interval)
except Exception as e:
print(f"[CropPreviewHandler] 检查区域{region_index+1}新图片失败: {e}")
......@@ -417,6 +436,20 @@ class CropPreviewHandler(QtCore.QObject):
if self._panel:
self._panel.refreshImages()
def forceRefresh(self):
"""
强制立即检查所有区域的新图片
用于在裁剪过程中手动触发检查,确保实时更新
"""
if not self._auto_monitor_enabled or self._is_resetting:
return
print(f"[CropPreviewHandler] 强制刷新检查")
for i, region_path in enumerate(self._monitored_paths):
if region_path and osp.exists(region_path):
self._checkRegionForNewImages(i, region_path)
def clearPanel(self):
"""清空面板显示"""
if self._panel:
......
......@@ -184,17 +184,17 @@ class DataCollectionChannelHandler:
if channel_config:
success = self._connectRealChannel(channel_config)
if not success:
error_detail = f"无法连接到配置的通道 {channel_source}\n请检查:\n 相机是否已开机并连接到网络\n2. IP地址和端口是否正确"
error_detail = f"无法连接到配置的通道 {channel_source}\n请检查:\n相机是否已开机并连接到网络\nIP地址和端口是否正确"
else:
# 如果没有配置,尝试直接连接USB通道
success = self._connectUSBChannel(channel_source)
if not success:
error_detail = f"无法连接到USB通道 {channel_source}\n请检查:\nUSB相机是否已连接\n2. 相机驱动是否已安装\n3. 相机是否被其他程序占用"
error_detail = f"无法连接到USB通道 {channel_source}\n请检查:\nUSB相机是否已连接\n相机驱动是否已安装\n相机是否被其他程序占用"
else:
# 如果是RTSP地址,直接连接
success = self._connectRTSPChannel(channel_source)
if not success:
error_detail = f"无法连接到RTSP地址\n请检查:\n1. 网络连接是否正常\n2. RTSP地址格式是否正确\n3. 相机是否支持RTSP协议\n\n地址:{channel_source}"
error_detail = f"无法连接到RTSP地址\n请检查:\n网络连接是否正常\nRTSP地址格式是否正确\n相机是否支持RTSP协议\n\n地址:{channel_source}"
if not success:
self._showDataCollectionChannelError(
......
......@@ -32,6 +32,10 @@ except Exception:
DEFAULT_CROP_SAVE_DIR = osp.join(get_project_root(), 'database', 'Corp_picture')
os.makedirs(DEFAULT_CROP_SAVE_DIR, exist_ok=True)
# 调试输出
print(f"[DataPreprocessHandler] 项目根目录: {get_project_root()}")
print(f"[DataPreprocessHandler] 默认裁剪保存目录: {DEFAULT_CROP_SAVE_DIR}")
class DrawableLabel(QtWidgets.QLabel):
"""
......@@ -2243,12 +2247,30 @@ class DataPreprocessHandler(QtCore.QObject):
# 转换为绝对路径
save_liquid_data_path = osp.abspath(save_liquid_data_path)
# 打开视频
# 输出保存路径信息(帮助用户定位图片位置)
print(f"[DataPreprocessHandler] ========== 裁剪配置 ==========")
print(f"[DataPreprocessHandler] 视频文件: {osp.basename(video_path)}")
print(f"[DataPreprocessHandler] 保存根目录: {save_liquid_data_path}")
print(f"[DataPreprocessHandler] 裁剪区域数: {len(crop_rects)}")
print(f"[DataPreprocessHandler] 裁剪频率: 每{crop_frequency}帧")
print(f"[DataPreprocessHandler] 图片格式: {image_format}")
print(f"[DataPreprocessHandler] =====================================")
# 打开视频(设置参数以提高容错性)
cap = cv2.VideoCapture(video_path)
# 设置视频读取参数,提高对损坏帧的容错性
# CAP_PROP_BUFFERSIZE: 减小缓冲区大小,加快错误恢复
try:
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
except:
pass
if not cap.isOpened():
raise Exception("无法打开视频文件")
print(f"[DataPreprocessHandler] 视频已打开: {osp.basename(video_path)}")
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# 检查是否启用了时间段选择
......@@ -2269,12 +2291,14 @@ class DataPreprocessHandler(QtCore.QObject):
# 为每个裁剪区域创建子目录(使用"视频名_区域X"的中文形式)
region_paths = []
print(f"[DataPreprocessHandler] 创建区域文件夹:")
for i in range(len(crop_rects)):
region_folder_name = f"{video_name}_区域{i+1}"
region_path = osp.join(save_liquid_data_path, region_folder_name)
region_paths.append(region_path)
try:
os.makedirs(region_path, exist_ok=True)
print(f"[DataPreprocessHandler] 区域{i+1}: {region_path}")
except Exception as mkdir_err:
raise Exception(f"无法创建区域{i+1}的保存目录: {mkdir_err}")
......@@ -2289,17 +2313,44 @@ class DataPreprocessHandler(QtCore.QObject):
# 计算实际需要处理的帧数(用于进度显示)
frames_to_process = end_frame_limit - start_frame_limit + 1
# 连续读取失败计数器
consecutive_read_failures = 0
max_consecutive_failures = 10 # 允许最多连续失败10次
# 进度更新计数器(减少processEvents调用频率)
progress_update_counter = 0
progress_update_interval = 5 # 每处理5帧更新一次进度(平衡性能和响应性)
while frame_count <= end_frame_limit:
# 检查是否取消
if progress_dialog.wasCanceled():
print("[DataPreprocessHandler] 用户取消裁剪")
break
# 读取帧
ret, frame = cap.read()
if not ret:
# 检查连续失败次数
if consecutive_read_failures >= max_consecutive_failures:
print(f"[DataPreprocessHandler] 连续读取失败{consecutive_read_failures}次,跳过剩余帧")
break
# 读取帧
try:
ret, frame = cap.read()
if not ret:
consecutive_read_failures += 1
print(f"[DataPreprocessHandler] 读取帧{frame_count}失败,跳过 (连续失败:{consecutive_read_failures})")
frame_count += 1
continue
# 读取成功,重置失败计数器
consecutive_read_failures = 0
except Exception as read_err:
consecutive_read_failures += 1
print(f"[DataPreprocessHandler] 读取帧{frame_count}异常: {read_err} (连续失败:{consecutive_read_failures})")
frame_count += 1
continue
# 根据频率决定是否裁剪
if (frame_count - start_frame_limit) % crop_frequency == 0:
# 对每个裁剪区域进行处理
......@@ -2324,20 +2375,32 @@ class DataPreprocessHandler(QtCore.QObject):
f.write(encoded_img.tobytes())
saved_counts[i] += 1
else:
print(f"[DataPreprocessHandler] 编码失败: 区域{i+1}, 帧{frame_count}")
except Exception as save_err:
pass
print(f"[DataPreprocessHandler] 保存失败: 区域{i+1}, 帧{frame_count}, 错误: {save_err}")
# 继续处理下一个区域,不中断整个流程
frame_count += 1
progress_update_counter += 1
# 更新进度(基于实际处理的帧数)
processed_frames = frame_count - start_frame_limit
progress = int((processed_frames / frames_to_process) * 100)
progress_dialog.setValue(progress)
self.cropProgress.emit(progress)
# 处理事件,保持界面响应
QtWidgets.QApplication.processEvents()
# 优化进度更新频率(减少processEvents调用)
if progress_update_counter >= progress_update_interval:
progress_update_counter = 0
# 更新进度(基于实际处理的帧数)
processed_frames = frame_count - start_frame_limit
progress = int((processed_frames / frames_to_process) * 100)
progress_dialog.setValue(progress)
self.cropProgress.emit(progress)
# 强制刷新预览面板(确保实时更新)
if self._crop_preview_handler is not None:
self._crop_preview_handler.forceRefresh()
# 处理事件,保持界面响应
QtWidgets.QApplication.processEvents()
# 释放资源
cap.release()
......@@ -2345,6 +2408,15 @@ class DataPreprocessHandler(QtCore.QObject):
# 完成
progress_dialog.setValue(100)
# 输出统计信息
print(f"[DataPreprocessHandler] ===== 裁剪完成 =====")
print(f"[DataPreprocessHandler] 处理帧数: {frame_count - start_frame_limit}/{frames_to_process}")
for i in range(len(crop_rects)):
print(f"[DataPreprocessHandler] 区域{i+1}: 保存 {saved_counts[i]} 张图片")
if consecutive_read_failures > 0:
print(f"[DataPreprocessHandler] 警告: 跳过了 {consecutive_read_failures} 个损坏/无法读取的帧")
print(f"[DataPreprocessHandler] =========================")
# 保存视频与裁剪图片的映射关系
import time
self._video_crop_mapping[video_path] = {
......
......@@ -34,6 +34,14 @@ from .widgets import ToolBar
from .widgets import UniqueLabelQListWidget
from .widgets import ZoomWidget
# Import DialogManager for styled dialogs
try:
import sys
sys.path.insert(0, osp.dirname(osp.dirname(osp.abspath(__file__))))
from widgets.style_manager import DialogManager
except ImportError:
DialogManager = None
# FIXME
# - [medium] Set max zoom value to something big enough for FitWidth/Window
......@@ -131,11 +139,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.shape_dock.setWidget(self.labelList)
self.uniqLabelList = UniqueLabelQListWidget()
self.uniqLabelList.setToolTip(
self.tr(
"点击选择标签\n或按 'Esc' 键取消"
)
)
# 隐藏提示框
# self.uniqLabelList.setToolTip(
# self.tr(
# "点击选择标签\n或按 'Esc' 键取消"
# )
# )
if self._config["labels"]:
for label in self._config["labels"]:
item = self.uniqLabelList.createItemFromLabel(label)
......@@ -725,8 +734,8 @@ class MainWindow(QtWidgets.QMainWindow):
utils.addActions(
self.canvas.menus[1],
(
action("&Copy here", self.copyShape),
action("&Move here", self.moveShape),
action("复制到此处(&C)", self.copyShape),
action("移动到此处(&M)", self.moveShape),
),
)
......@@ -1884,14 +1893,27 @@ class MainWindow(QtWidgets.QMainWindow):
return label_file
def deleteFile(self):
mb = QtWidgets.QMessageBox
msg = self.tr(
""
""
)
answer = mb.warning(self, self.tr(""), msg, mb.Yes | mb.No)
if answer != mb.Yes:
return
# Use DialogManager if available
if DialogManager:
result = DialogManager.show_question_warning(
self,
"警告",
msg,
yes_text="是",
no_text="否"
)
if not result:
return
else:
mb = QtWidgets.QMessageBox
answer = mb.warning(self, "警告", msg, mb.Yes | mb.No)
if answer != mb.Yes:
return
label_file = self.getLabelFile()
if osp.exists(label_file):
......@@ -1923,29 +1945,65 @@ class MainWindow(QtWidgets.QMainWindow):
def mayContinue(self):
if not self.dirty:
return True
mb = QtWidgets.QMessageBox
msg = self.tr('"{}"').format(
self.filename
)
answer = mb.question(
self,
self.tr(""),
msg,
mb.Save | mb.Discard | mb.Cancel,
mb.Save,
)
if answer == mb.Discard:
return True
elif answer == mb.Save:
self.saveFile()
return True
else: # answer == mb.Cancel
return False
# Use DialogManager if available
if DialogManager:
# Create custom dialog with Save/Discard/Cancel buttons
msg_box = QtWidgets.QMessageBox(self)
msg_box.setWindowTitle("提示")
msg_box.setText(msg)
msg_box.setIcon(QtWidgets.QMessageBox.Question)
# Add custom buttons with Chinese text
save_btn = msg_box.addButton("保存", QtWidgets.QMessageBox.AcceptRole)
discard_btn = msg_box.addButton("不保存", QtWidgets.QMessageBox.DestructiveRole)
cancel_btn = msg_box.addButton("取消", QtWidgets.QMessageBox.RejectRole)
msg_box.setDefaultButton(save_btn)
# Apply DialogManager styling
msg_box.setStyleSheet(DialogManager.DEFAULT_STYLE)
from widgets.style_manager import FontManager
FontManager.applyToWidgetRecursive(msg_box)
msg_box.exec_()
clicked = msg_box.clickedButton()
if clicked == discard_btn:
return True
elif clicked == save_btn:
self.saveFile()
return True
else: # cancel_btn
return False
else:
mb = QtWidgets.QMessageBox
answer = mb.question(
self,
"提示",
msg,
mb.Save | mb.Discard | mb.Cancel,
mb.Save,
)
if answer == mb.Discard:
return True
elif answer == mb.Save:
self.saveFile()
return True
else: # answer == mb.Cancel
return False
def errorMessage(self, title, message):
return QtWidgets.QMessageBox.critical(
self, title, "<p><b>%s</b></p>%s" % (title, message)
)
formatted_message = "<p><b>%s</b></p>%s" % (title, message)
# Use DialogManager if available
if DialogManager:
DialogManager.show_critical(self, title, formatted_message)
else:
return QtWidgets.QMessageBox.critical(self, title, formatted_message)
def currentPath(self):
return osp.dirname(str(self.filename)) if self.filename else "."
......@@ -1965,19 +2023,34 @@ class MainWindow(QtWidgets.QMainWindow):
self.setDirty()
def deleteSelectedShape(self):
yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
msg = self.tr(
" {} "
""
).format(len(self.canvas.selectedShapes))
if yes == QtWidgets.QMessageBox.warning(
self, self.tr(""), msg, yes | no, yes
):
self.remLabels(self.canvas.deleteSelected())
self.setDirty()
if self.noShapes():
for action in self.actions.onShapesPresent:
action.setEnabled(False)
msg = "确定删除该标签?"
# Use DialogManager if available, otherwise fallback to QMessageBox
if DialogManager:
result = DialogManager.show_question_warning(
self,
"警告",
msg,
yes_text="是",
no_text="否",
text_alignment=DialogManager.ALIGN_CENTER
)
if result:
self.remLabels(self.canvas.deleteSelected())
self.setDirty()
if self.noShapes():
for action in self.actions.onShapesPresent:
action.setEnabled(False)
else:
yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
if yes == QtWidgets.QMessageBox.warning(
self, "警告", msg, yes | no, yes
):
self.remLabels(self.canvas.deleteSelected())
self.setDirty()
if self.noShapes():
for action in self.actions.onShapesPresent:
action.setEnabled(False)
def copyShape(self):
self.canvas.endMove(copy=True)
......@@ -1994,13 +2067,19 @@ class MainWindow(QtWidgets.QMainWindow):
if not self.mayContinue():
return
defaultOpenDirPath = dirpath if dirpath else "."
# 默认打开 database\Corp_picture 目录
default_dir = osp.join(osp.dirname(osp.dirname(osp.abspath(__file__))), "database", "Corp_picture")
defaultOpenDirPath = dirpath if dirpath else default_dir
if self.lastOpenDir and osp.exists(self.lastOpenDir):
defaultOpenDirPath = self.lastOpenDir
else:
defaultOpenDirPath = (
osp.dirname(self.filename) if self.filename else "."
)
# 优先使用 database\Corp_picture,如果不存在则使用当前文件目录或当前工作目录
if osp.exists(default_dir):
defaultOpenDirPath = default_dir
else:
defaultOpenDirPath = (
osp.dirname(self.filename) if self.filename else "."
)
targetDirPath = str(
QtWidgets.QFileDialog.getExistingDirectory(
......
import PIL.Image
import PIL.ImageEnhance
import sys
import os
from qtpy.QtCore import Qt
from qtpy import QtGui
from qtpy import QtWidgets
from .. import utils
# 导入全局样式管理器
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from widgets.style_manager import FontManager
STYLE_MANAGER_AVAILABLE = True
except ImportError:
STYLE_MANAGER_AVAILABLE = False
class BrightnessContrastDialog(QtWidgets.QDialog):
def __init__(self, img, callback, parent=None):
......@@ -20,6 +33,10 @@ class BrightnessContrastDialog(QtWidgets.QDialog):
formLayout.addRow(self.tr(""), self.slider_brightness)
formLayout.addRow(self.tr(""), self.slider_contrast)
self.setLayout(formLayout)
# 应用全局字体样式
if STYLE_MANAGER_AVAILABLE:
FontManager.applyToWidgetRecursive(self)
assert isinstance(img, PIL.Image.Image)
self.img = img
......
......@@ -6,6 +6,12 @@ from labelme import QT5
from labelme.shape import Shape
import labelme.utils
# 导入标签显示名称映射
try:
from labelme.widgets.label_dialog import LABEL_DISPLAY_NAMES
except ImportError:
LABEL_DISPLAY_NAMES = {}
# TODO(unknown):
# - [maybe] Find optimal epsilon value.
......@@ -286,7 +292,7 @@ class Canvas(QtWidgets.QWidget):
# - Highlight shapes
# - Highlight vertex
# Update shape/vertex fill and tooltip value accordingly.
self.setToolTip(self.tr("Image"))
self.setToolTip(self.tr("图像"))
for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
# Look for a nearby vertex to highlight. If that fails,
# check if we happen to be inside a shape.
......@@ -301,7 +307,7 @@ class Canvas(QtWidgets.QWidget):
self.hEdge = None
shape.highlightVertex(index, shape.MOVE_VERTEX)
self.overrideCursor(CURSOR_POINT)
self.setToolTip(self.tr("Click & drag to move point"))
self.setToolTip(self.tr("点击并拖动以移动点"))
self.setStatusTip(self.toolTip())
self.update()
break
......@@ -313,7 +319,7 @@ class Canvas(QtWidgets.QWidget):
self.prevhShape = self.hShape = shape
self.prevhEdge = self.hEdge = index_edge
self.overrideCursor(CURSOR_POINT)
self.setToolTip(self.tr("Click to create point"))
self.setToolTip(self.tr("点击以创建点"))
self.setStatusTip(self.toolTip())
self.update()
break
......@@ -325,8 +331,10 @@ class Canvas(QtWidgets.QWidget):
self.prevhShape = self.hShape = shape
self.prevhEdge = self.hEdge
self.hEdge = None
# 将英文标签转换为中文显示名称
display_label = LABEL_DISPLAY_NAMES.get(shape.label, shape.label)
self.setToolTip(
self.tr("Click & drag to move shape '%s'") % shape.label
self.tr("点击并拖动以移动形状 '%s'") % display_label
)
self.setStatusTip(self.toolTip())
self.overrideCursor(CURSOR_GRAB)
......
import sys
import os
from qtpy import QtWidgets
# 导入全局样式管理器
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from widgets.style_manager import FontManager
STYLE_MANAGER_AVAILABLE = True
except ImportError:
STYLE_MANAGER_AVAILABLE = False
class ColorDialog(QtWidgets.QColorDialog):
def __init__(self, parent=None):
......@@ -14,6 +27,10 @@ class ColorDialog(QtWidgets.QColorDialog):
self.bb = self.layout().itemAt(1).widget()
self.bb.addButton(QtWidgets.QDialogButtonBox.RestoreDefaults)
self.bb.clicked.connect(self.checkRestore)
# 应用全局字体样式
if STYLE_MANAGER_AVAILABLE:
FontManager.applyToWidgetRecursive(self)
def getColor(self, value=None, title=None, default=None):
self.default = default
......
......@@ -3,6 +3,19 @@ from qtpy import QtGui
from qtpy import QtWidgets
import json
import sys
import os
# 导入全局样式管理器
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from widgets.style_manager import FontManager
STYLE_MANAGER_AVAILABLE = True
except ImportError:
STYLE_MANAGER_AVAILABLE = False
class ScrollAreaPreview(QtWidgets.QScrollArea):
......@@ -54,6 +67,10 @@ class FileDialogPreview(QtWidgets.QFileDialog):
self.setFixedSize(self.width() + 300, self.height())
self.layout().addLayout(box, 1, 3, 1, 1)
self.currentChanged.connect(self.onChange)
# 应用全局字体样式
if STYLE_MANAGER_AVAILABLE:
FontManager.applyToWidgetRecursive(self)
def onChange(self, path):
if path.lower().endswith(".json"):
......
import re
import sys
import os
from qtpy import QT_VERSION
from qtpy import QtCore
......@@ -8,17 +10,31 @@ from qtpy import QtWidgets
from labelme.logger import logger
import labelme.utils
# 导入全局样式管理器
try:
# 获取项目根目录(labelme的上级目录)
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from widgets.style_manager import FontManager, DialogManager
STYLE_MANAGER_AVAILABLE = True
except ImportError as e:
print(f"警告: 无法导入样式管理器: {e}")
STYLE_MANAGER_AVAILABLE = False
DialogManager = None
QT5 = QT_VERSION[0] == "5"
# -
FIXED_LABELS = ["liquid", "air", "foam"]
# - UI
# - UI
LABEL_DISPLAY_NAMES = {
"liquid": "",
"air": "",
"foam": ""
"liquid": "液体",
"air": "空气",
"foam": "泡沫"
}
# -
......@@ -66,7 +82,9 @@ class LabelDialog(QtWidgets.QDialog):
self._fit_to_content = fit_to_content
super(LabelDialog, self).__init__(parent)
self.setWindowTitle("")
self.setWindowTitle("请选择标签")
# 隐藏右上角的帮助按钮(?)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
#
# self.edit self.edit_group_id
......@@ -100,6 +118,10 @@ class LabelDialog(QtWidgets.QDialog):
self.labelList.setFixedHeight(150)
# self.edit.setListWidgetself.edit
layout.addWidget(self.labelList)
# 应用全局字体样式到列表控件
if STYLE_MANAGER_AVAILABLE:
FontManager.applyToWidget(self.labelList)
# label_flags
if flags is None:
flags = {}
......@@ -111,6 +133,10 @@ class LabelDialog(QtWidgets.QDialog):
#
# self.editDescription
self.setLayout(layout)
# 应用全局字体样式到整个对话框
if STYLE_MANAGER_AVAILABLE:
FontManager.applyToWidgetRecursive(self)
# completion
completer = QtWidgets.QCompleter()
if not QT5 and completion != "startswith":
......@@ -154,11 +180,10 @@ class LabelDialog(QtWidgets.QDialog):
current_item = self.labelList.currentItem()
if not current_item:
logger.warning("")
QtWidgets.QMessageBox.warning(
self,
"",
""
)
if DialogManager:
DialogManager.show_warning(self, "", "")
else:
QtWidgets.QMessageBox.warning(self, "", "")
return
#
......@@ -168,11 +193,11 @@ class LabelDialog(QtWidgets.QDialog):
#
if text not in FIXED_LABELS:
logger.warning(f" '{text}' : {', '.join(FIXED_LABELS)}")
QtWidgets.QMessageBox.warning(
self,
"",
f" '{text}' \n\n:\n" + "\n".join(FIXED_LABELS)
)
msg = f" '{text}' \n\n:\n" + "\n".join(FIXED_LABELS)
if DialogManager:
DialogManager.show_warning(self, "", msg)
else:
QtWidgets.QMessageBox.warning(self, "", msg)
return
if text:
......
......@@ -61,6 +61,25 @@ except ImportError as e:
labelme_get_config = None
class ColorPreservingDelegate(QtWidgets.QStyledItemDelegate):
"""自定义委托,保持选中状态下的文字颜色"""
def paint(self, painter, option, index):
# 获取item的前景色(文字颜色)
foreground = index.data(Qt.ForegroundRole)
# 如果item被选中,修改选项的调色板以保持文字颜色
if option.state & QtWidgets.QStyle.State_Selected:
# 设置选中背景色为灰色
option.palette.setBrush(QtGui.QPalette.Highlight, QtGui.QBrush(QtGui.QColor(208, 208, 208)))
# 如果有自定义前景色,保持它
if foreground:
option.palette.setBrush(QtGui.QPalette.HighlightedText, foreground)
# 调用父类的绘制方法
super().paint(painter, option, index)
class AnnotationTool(QtWidgets.QWidget):
"""
数据标注工具组件
......@@ -181,6 +200,10 @@ class AnnotationTool(QtWidgets.QWidget):
self.annotation_list.setAlternatingRowColors(True)
self.annotation_list.setIconSize(QtCore.QSize(80, 80))
self.annotation_list.setSpacing(5)
# 设置自定义委托以保持选中状态下的文字颜色
self.annotation_list.setItemDelegate(ColorPreservingDelegate(self.annotation_list))
self.annotation_list.setStyleSheet("""
QListWidget {
border: 1px solid #c0c0c0;
......@@ -192,8 +215,7 @@ class AnnotationTool(QtWidgets.QWidget):
border-bottom: 1px solid #e0e0e0;
}
QListWidget::item:selected {
background-color: #0078d7;
color: white;
background-color: #d0d0d0;
}
QListWidget::item:hover {
background-color: #e0e0e0;
......@@ -552,8 +574,11 @@ class AnnotationTool(QtWidgets.QWidget):
def onDirectoryChanged(self, dir_path):
"""目录变化时的UI更新(响应handler信号)"""
self.lbl_current_folder.setText(dir_path)
self.lbl_current_folder.setStyleSheet("color: #2ca02c; font-style: normal; font-weight: bold;")
# 只显示文件夹名称,不显示完整路径
import os.path as osp
folder_name = osp.basename(dir_path) if dir_path else ""
self.lbl_current_folder.setText(folder_name)
self.lbl_current_folder.setStyleSheet("color: #2ca02c; font-style: normal; font-weight: bold; font-size: 9pt;")
def onFileListUpdated(self, file_info_list):
"""文件列表更新时的UI更新(响应handler信号)"""
......@@ -634,11 +659,28 @@ class AnnotationTool(QtWidgets.QWidget):
"""列表项点击事件"""
data = item.data(Qt.UserRole)
if data:
# 强制设置选中项的颜色
if data.get('has_json'):
item.setForeground(QtGui.QBrush(QtGui.QColor(44, 160, 44))) # 绿色
else:
item.setForeground(QtGui.QBrush(QtGui.QColor(128, 128, 128))) # 灰色
image_path = data['image_path']
self.loadImageForAnnotation(image_path)
def onItemSelectionChanged(self):
"""列表项选择变化事件"""
# 重新设置所有项目的颜色(因为选中状态会改变颜色)
for i in range(self.annotation_list.count()):
item = self.annotation_list.item(i)
data = item.data(Qt.UserRole)
if data and data.get('has_json'):
# 已标注 - 保持深绿色
item.setForeground(QtGui.QBrush(QtGui.QColor(44, 160, 44))) # #2ca02c
else:
# 未标注 - 灰色
item.setForeground(QtGui.QBrush(QtGui.QColor(128, 128, 128))) # #808080
items = self.annotation_list.selectedItems()
if not items:
return
......
......@@ -41,6 +41,10 @@ except Exception:
DEFAULT_CROP_SAVE_DIR = osp.join(get_project_root(), 'database', 'Corp_picture')
os.makedirs(DEFAULT_CROP_SAVE_DIR, exist_ok=True)
# 调试输出
print(f"[CropConfigDialog模块] 项目根目录: {get_project_root()}")
print(f"[CropConfigDialog模块] 默认裁剪保存目录: {DEFAULT_CROP_SAVE_DIR}")
class CropConfigDialog(QtWidgets.QDialog):
"""
......@@ -62,8 +66,11 @@ class CropConfigDialog(QtWidgets.QDialog):
"""
super(CropConfigDialog, self).__init__(parent)
# 保存路径和频率
self._save_liquid_data_path = default_save_liquid_data_path or DEFAULT_CROP_SAVE_DIR
# 【强制修改】始终使用项目默认路径,忽略传入的参数
# 这样可以确保图片始终保存在项目目录下
self._save_liquid_data_path = DEFAULT_CROP_SAVE_DIR
print(f"[CropConfigDialog] 强制使用默认路径: {DEFAULT_CROP_SAVE_DIR}")
self._crop_frequency = default_frequency
self._file_prefix = "frame"
self._image_format = "jpg"
......@@ -321,10 +328,19 @@ class CropConfigDialog(QtWidgets.QDialog):
try:
settings = QtCore.QSettings("Detection", "CropConfigDialog")
saved_path = settings.value("save_liquid_data_path", "")
if saved_path and osp.exists(saved_path):
self._save_liquid_data_path = saved_path
self.path_edit.setText(saved_path)
# 【强制修改】清除旧的保存路径设置,不再记住保存路径
# 检查是否有旧设置
old_path = settings.value("save_liquid_data_path", "")
if old_path:
print(f"[CropConfigDialog] 检测到旧的保存路径: {old_path}")
settings.remove("save_liquid_data_path")
print(f"[CropConfigDialog] 已清除旧的保存路径设置")
# 强制使用项目默认路径
# 这样可以确保图片始终保存在项目目录下,避免用户找不到图片
self.path_edit.setText(self._save_liquid_data_path)
print(f"[CropConfigDialog] 对话框路径已设置为: {self._save_liquid_data_path}")
print(f"[CropConfigDialog] 文本框内容: {self.path_edit.text()}")
saved_freq = settings.value("crop_frequency", 1)
try:
......@@ -347,7 +363,8 @@ class CropConfigDialog(QtWidgets.QDialog):
"""保存当前设置"""
try:
settings = QtCore.QSettings("Detection", "CropConfigDialog")
settings.setValue("save_liquid_data_path", self.path_edit.text())
# 【修改】不再保存路径,每次都使用默认路径
# settings.setValue("save_liquid_data_path", self.path_edit.text())
settings.setValue("crop_frequency", self.frequency_spinbox.value())
settings.setValue("file_prefix", self.prefix_edit.text())
settings.setValue("image_format", self.format_combo.currentText())
......@@ -414,12 +431,16 @@ class CropConfigDialog(QtWidgets.QDialog):
- file_prefix: 文件名前缀
- image_format: 图片格式
"""
return {
'save_liquid_data_path': self.path_edit.text().strip(),
# 【强制修改】始终返回默认路径,忽略文本框内容
# 确保图片保存在项目目录下
config = {
'save_liquid_data_path': DEFAULT_CROP_SAVE_DIR, # 强制使用默认路径
'crop_frequency': self.frequency_spinbox.value(),
'file_prefix': self.prefix_edit.text().strip(),
'image_format': self.format_combo.currentText()
}
print(f"[CropConfigDialog] getConfig返回的保存路径: {config['save_liquid_data_path']}")
return config
def setConfig(self, config):
"""
......
......@@ -281,7 +281,7 @@ class CropPreviewPanel(QtWidgets.QWidget):
self.refreshImages()
def _findRegionPaths(self):
"""查找所有区域文件夹(支持新旧命名格式,可根据视频名称过滤)"""
"""查找所有区域文件夹(支持新旧命名格式,可根据视频名 称过滤)"""
self._region_paths = []
if not self._save_liquid_data_path or not osp.exists(self._save_liquid_data_path):
......@@ -342,9 +342,19 @@ class CropPreviewPanel(QtWidgets.QWidget):
if not self._save_liquid_data_path:
return
# 保存当前的视频名称(clearImages会清空它)
current_video_name = self._video_name
# 如果没有视频名称上下文,说明当前没有选中有效的裁剪视频,不应该刷新
if not current_video_name:
return
# 先清空所有图片
self.clearImages()
# 恢复视频名称
self._video_name = current_video_name
# 重新查找区域文件夹
self._findRegionPaths()
......@@ -485,6 +495,7 @@ class CropPreviewPanel(QtWidgets.QWidget):
# 清空缓存数据
self._region_images.clear()
self._region_paths = []
self._video_name = None # 清空视频名称,防止刷新时显示其他视频的图片
# 更新统计
self._updateStats()
......@@ -600,16 +611,16 @@ class CropPreviewPanel(QtWidgets.QWidget):
reply = DialogManager.show_question_warning(
self,
"确认删除",
f"确定要删除区域 {current_region + 1} 的所有文件吗?\n\n"
f"当前区域共有 {image_count} 张图片\n\n"
f"确定要删除区域 {current_region + 1} 的所有文件吗?\n"
f"当前区域共有 {image_count} 张图片"
f"文件夹将被移动到回收站"
)
else:
reply = QtWidgets.QMessageBox.question(
self,
"确认删除",
f"确定要删除区域 {current_region + 1} 的所有文件吗?\n\n"
f"当前区域共有 {image_count} 张图片\n\n"
f"确定要删除区域 {current_region + 1} 的所有文件吗?\n"
f"当前区域共有 {image_count} 张图片"
f"文件夹将被移动到回收站",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
......
......@@ -167,7 +167,7 @@ class DataCollectionPanel(QtWidgets.QWidget):
# 标题栏
title_layout = QtWidgets.QHBoxLayout()
title_label = QtWidgets.QLabel("目录")
title_label = QtWidgets.QLabel("数据采集")
title_label.setStyleSheet("font-size: 12pt; font-weight: bold;")
title_layout.addWidget(title_label)
......@@ -833,12 +833,25 @@ class DataCollectionPanel(QtWidgets.QWidget):
file_name_no_ext, file_ext = osp.splitext(file_name)
# 弹出输入对话框
new_name, ok = QtWidgets.QInputDialog.getText(
self, "重命名文件",
"请输入新的文件名(不含扩展名):",
QtWidgets.QLineEdit.Normal,
file_name_no_ext
)
dialog = QtWidgets.QInputDialog(self)
dialog.setWindowTitle("重命名文件")
dialog.setLabelText("请输入新的文件名(不含扩展名):")
dialog.setTextValue(file_name_no_ext)
dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
# 隐藏问号按钮
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
# 设置中文按钮文本
dialog.setOkButtonText("确定")
dialog.setCancelButtonText("取消")
# 应用全局字体管理
if DialogManager:
from ..style_manager import FontManager
FontManager.applyToWidgetRecursive(dialog)
# 应用统一按钮样式
DialogManager.applyButtonStylesToDialog(dialog)
ok = dialog.exec_()
new_name = dialog.textValue()
if not ok or not new_name.strip():
return
......@@ -990,6 +1003,14 @@ class DataCollectionPanel(QtWidgets.QWidget):
dialog.setLayout(layout)
# 应用全局字体管理和按钮样式
if DialogManager:
from ..style_manager import FontManager, TextButtonStyleManager
FontManager.applyToWidgetRecursive(dialog)
# 应用统一按钮样式
TextButtonStyleManager.applyToButton(ok_btn)
TextButtonStyleManager.applyToButton(cancel_btn)
# 连接按钮信号
ok_btn.clicked.connect(dialog.accept)
cancel_btn.clicked.connect(dialog.reject)
......@@ -1058,9 +1079,9 @@ class DataCollectionPanel(QtWidgets.QWidget):
if file_info:
file_info_text = "、".join(file_info)
message = f"确定要删除文件夹 '{folder_name}' 吗?\n\n文件夹内含有{file_info_text}\n\n所有内容将被移动到回收站"
message = f"确定要删除文件夹“{folder_name}”吗?文件夹内含有{file_info_text},所有内容将被移动到回收站。"
else:
message = f"确定要删除文件夹 '{folder_name}' 吗?\n\n文件夹为空\n\n将被移动到回收站"
message = f"确定要删除文件夹“{folder_name}”吗?文件夹为空,将被移动到回收站。"
# 确认删除
if self._showQuestionWarning("确认删除", message):
......@@ -1383,10 +1404,23 @@ def _getSelectedChannel(self):
if current_data == "custom":
# 自定义RTSP地址
rtsp_url, ok = QtWidgets.QInputDialog.getText(
self, "自定义RTSP地址",
"请输入RTSP地址:\n(格式: rtsp://username:password@ip:port/path)"
)
dialog = QtWidgets.QInputDialog(self)
dialog.setWindowTitle("自定义RTSP地址")
dialog.setLabelText("请输入RTSP地址:\n(格式: rtsp://username:password@ip:port/path)")
dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
dialog.setOkButtonText("确定")
dialog.setCancelButtonText("取消")
# 应用全局字体管理
if DialogManager:
from style_manager import FontManager
FontManager.applyToWidgetRecursive(dialog)
# 应用统一按钮样式
DialogManager.applyButtonStylesToDialog(dialog)
ok = dialog.exec_()
rtsp_url = dialog.textValue()
if ok and rtsp_url.strip():
return rtsp_url.strip()
else:
......
......@@ -172,7 +172,7 @@ class DataPreprocessPanel(QtWidgets.QWidget):
# 标题栏
title_layout = QtWidgets.QHBoxLayout()
title_label = QtWidgets.QLabel("目录")
title_label = QtWidgets.QLabel("数据预处理")
title_label.setStyleSheet("font-size: 12pt; font-weight: bold;")
title_layout.addWidget(title_label)
......@@ -615,12 +615,17 @@ class DataPreprocessPanel(QtWidgets.QWidget):
def getCropConfig(self):
"""获取裁剪配置"""
return {
'save_liquid_data_path': self.crop_path_edit.text().strip(),
# 【强制修改】始终使用项目默认路径,忽略文本框内容
# 确保图片保存在项目目录下
default_path = self._getDefaultCropFolder()
config = {
'save_liquid_data_path': default_path, # 强制使用默认路径
'crop_frequency': self.crop_frequency_spinbox.value(),
'file_prefix': self.crop_prefix_edit.text().strip(),
'image_format': self.crop_format_combo.currentText()
}
print(f"[DataPreprocessPanel] getCropConfig返回的保存路径: {config['save_liquid_data_path']}")
return config
def _createCropPreviewPanel(self):
"""创建右侧裁剪图片预览面板"""
......@@ -1124,6 +1129,7 @@ class DataPreprocessPanel(QtWidgets.QWidget):
# 创建右键菜单
menu = QtWidgets.QMenu(self)
# 只在空白处点击时显示刷新菜单
if not item:
# 在空白处点击,显示刷新菜单
action_refresh = menu.addAction(newIcon("刷新"), "刷新")
......@@ -1134,26 +1140,8 @@ class DataPreprocessPanel(QtWidgets.QWidget):
# 处理刷新动作
if action == action_refresh:
self._onRefreshVideos()
return
# 获取视频路径
video_path = item.data(Qt.UserRole)
if not video_path:
return
# 添加菜单项
action_rename = menu.addAction(newIcon("设置"), "重命名")
action_delete = menu.addAction(newIcon("关闭"), "删除")
# 显示菜单并获取选择的动作
action = menu.exec_(self.video_grid.mapToGlobal(position))
# 处理选择的动作
if action == action_rename:
self._onRenameVideo(item)
elif action == action_delete:
self._onDeleteVideo(item)
# 在视频项上右键时不显示任何菜单(已删除重命名和删除功能)
def _onRenameVideo(self, item):
"""重命名视频文件"""
......
......@@ -275,6 +275,10 @@ class DialogManager:
QMessageBox {
min-width: 400px;
}
QMessageBox QLabel {
border: none;
background: transparent;
}
"""
# 文本对齐方式常量
......@@ -323,6 +327,9 @@ class DialogManager:
# 🔥 根据文本内容自动调整对话框大小
DialogManager._adjust_dialog_size(msg_box, message)
# 🔥 应用统一按钮样式到对话框的所有按钮
DialogManager._apply_button_styles(msg_box)
return msg_box
@staticmethod
......@@ -339,6 +346,9 @@ class DialogManager:
# 只设置消息文本标签的对齐方式,不影响其他标签
if label.text() and not label.pixmap():
label.setAlignment(alignment)
# 移除任何边框样式
label.setFrameStyle(QtWidgets.QFrame.NoFrame)
label.setStyleSheet("border: none; background: transparent;")
except Exception as e:
pass
......@@ -382,6 +392,24 @@ class DialogManager:
pass
@staticmethod
def _apply_button_styles(msg_box):
"""应用统一按钮样式到对话框的所有按钮
Args:
msg_box: QMessageBox对象
"""
try:
# 查找对话框中的所有QPushButton
buttons = msg_box.findChildren(QtWidgets.QPushButton)
for button in buttons:
# 应用TextButtonStyleManager样式
TextButtonStyleManager.applyToButton(button)
except Exception as e:
pass
@staticmethod
def _set_chinese_button_texts(msg_box, button_texts=None):
"""设置按钮为中文文本"""
# 默认中文按钮文本映射
......@@ -702,6 +730,25 @@ class DialogManager:
def set_default_style(style_sheet):
"""设置默认样式"""
DialogManager.DEFAULT_STYLE = style_sheet
@staticmethod
def applyButtonStylesToDialog(dialog):
"""应用统一按钮样式到对话框的所有按钮
Args:
dialog: QDialog或QInputDialog对象
"""
try:
# 查找对话框中的所有QPushButton
buttons = dialog.findChildren(QtWidgets.QPushButton)
for button in buttons:
# 应用TextButtonStyleManager样式
from widgets.style_manager import TextButtonStyleManager
TextButtonStyleManager.applyToButton(button)
except Exception as e:
pass
# 对话框管理器便捷函数
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment