Commit 5e1976e2 by liuqingjun2

1

parent 63a0675e
......@@ -6,6 +6,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] = {
......
# flake8: noqa
import logging
import sys
from qtpy import QT_VERSION
__appname__ = "labelme"
# Semantic Versioning 2.0.0: https://semver.org/
# 1. MAJOR version when you make incompatible API changes;
# 2. MINOR version when you add functionality in a backwards-compatible manner;
# 3. PATCH version when you make backwards-compatible bug fixes.
# e.g., 1.0.0a0, 1.0.0a1, 1.0.0b0, 1.0.0rc0, 1.0.0, 1.0.0.post0
__version__ = "5.2.1"
QT4 = QT_VERSION[0] == "4"
QT5 = QT_VERSION[0] == "5"
del QT_VERSION
PY2 = sys.version[0] == "2"
PY3 = sys.version[0] == "3"
del sys
from labelme.label_file import LabelFile
from labelme import testing
from labelme import utils
import argparse
import codecs
import logging
import os
import os.path as osp
import sys
import yaml
# Python
parent_dir = osp.dirname(osp.dirname(osp.abspath(__file__)))
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
from qtpy import QtCore
from qtpy import QtWidgets
from labelme import __appname__
from labelme import __version__
from labelme.app import MainWindow
from labelme.config import get_config
from labelme.logger import logger
from labelme.utils import newIcon
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--version", "-V", action="store_true", help="show version"
)
parser.add_argument(
"--reset-config", action="store_true", help="reset qt config"
)
parser.add_argument(
"--logger-level",
default="info",
choices=["debug", "info", "warning", "fatal", "error"],
help="logger level",
)
parser.add_argument("filename", nargs="?", help="image or label filename")
parser.add_argument(
"--output",
"-O",
"-o",
help="output file or directory (if it ends with .json it is "
"recognized as file, else as directory)",
)
default_config_file = os.path.join(os.path.expanduser("~"), ".labelmerc")
parser.add_argument(
"--config",
dest="config",
help="config file or yaml-format string (default: {})".format(
default_config_file
),
default=default_config_file,
)
# config for the gui
parser.add_argument(
"--nodata",
dest="store_data",
action="store_false",
help="stop storing image data to JSON file",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--autosave",
dest="auto_save",
action="store_true",
help="auto save",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--nosortlabels",
dest="sort_labels",
action="store_false",
help="stop sorting labels",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--flags",
help="comma separated list of flags OR file containing flags",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--labelflags",
dest="label_flags",
help=r"yaml string of label specific flags OR file containing json "
r"string of label specific flags (ex. {person-\d+: [male, tall], "
r"dog-\d+: [black, brown, white], .*: [occluded]})", # NOQA
default=argparse.SUPPRESS,
)
parser.add_argument(
"--labels",
help="comma separated list of labels OR file containing labels",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--validatelabel",
dest="validate_label",
choices=["exact"],
help="label validation types",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--keep-prev",
action="store_true",
help="keep annotation of previous frame",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--epsilon",
type=float,
help="epsilon to find nearest vertex on canvas",
default=argparse.SUPPRESS,
)
args = parser.parse_args()
if args.version:
print("{0} {1}".format(__appname__, __version__))
sys.exit(0)
logger.setLevel(getattr(logging, args.logger_level.upper()))
if hasattr(args, "flags"):
if os.path.isfile(args.flags):
with codecs.open(args.flags, "r", encoding="utf-8") as f:
args.flags = [line.strip() for line in f if line.strip()]
else:
args.flags = [line for line in args.flags.split(",") if line]
if hasattr(args, "labels"):
if os.path.isfile(args.labels):
with codecs.open(args.labels, "r", encoding="utf-8") as f:
args.labels = [line.strip() for line in f if line.strip()]
else:
args.labels = [line for line in args.labels.split(",") if line]
if hasattr(args, "label_flags"):
if os.path.isfile(args.label_flags):
with codecs.open(args.label_flags, "r", encoding="utf-8") as f:
args.label_flags = yaml.safe_load(f)
else:
args.label_flags = yaml.safe_load(args.label_flags)
config_from_args = args.__dict__
config_from_args.pop("version")
reset_config = config_from_args.pop("reset_config")
filename = config_from_args.pop("filename")
output = config_from_args.pop("output")
config_file_or_yaml = config_from_args.pop("config")
config = get_config(config_file_or_yaml, config_from_args)
if not config["labels"] and config["validate_label"]:
logger.error(
"--labels must be specified with --validatelabel or "
"validate_label: true in the config file "
"(ex. ~/.labelmerc)."
)
sys.exit(1)
output_file = None
output_dir = None
if output is not None:
if output.endswith(".json"):
output_file = output
else:
output_dir = output
translator = QtCore.QTranslator()
translator.load(
QtCore.QLocale.system().name(),
osp.dirname(osp.abspath(__file__)) + "/translate",
)
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName(__appname__)
app.setWindowIcon(newIcon("icon"))
app.installTranslator(translator)
win = MainWindow(
config=config,
filename=filename,
output_file=output_file,
output_dir=output_dir,
)
if reset_config:
logger.info("Resetting Qt config: %s" % win.settings.fileName())
win.settings.clear()
sys.exit(0)
win.show()
win.raise_()
sys.exit(app.exec_())
# this main block is required to generate executable by pyinstaller
if __name__ == "__main__":
main()
This diff is collapsed. Click to expand it.
# flake8: noqa
from . import draw_json
from . import draw_label_png
from . import json_to_dataset
from . import on_docker
#!/usr/bin/env python
import argparse
import sys
import imgviz
import matplotlib.pyplot as plt
from labelme.label_file import LabelFile
from labelme import utils
PY2 = sys.version_info[0] == 2
def main():
parser = argparse.ArgumentParser()
parser.add_argument("json_file")
args = parser.parse_args()
label_file = LabelFile(args.json_file)
img = utils.img_data_to_arr(label_file.imageData)
label_name_to_value = {"_background_": 0}
for shape in sorted(label_file.shapes, key=lambda x: x["label"]):
label_name = shape["label"]
if label_name in label_name_to_value:
label_value = label_name_to_value[label_name]
else:
label_value = len(label_name_to_value)
label_name_to_value[label_name] = label_value
lbl, _ = utils.shapes_to_label(
img.shape, label_file.shapes, label_name_to_value
)
label_names = [None] * (max(label_name_to_value.values()) + 1)
for name, value in label_name_to_value.items():
label_names[value] = name
lbl_viz = imgviz.label2rgb(
lbl,
imgviz.asgray(img),
label_names=label_names,
font_size=30,
loc="rb",
)
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(lbl_viz)
plt.show()
if __name__ == "__main__":
main()
import argparse
import imgviz
import matplotlib.pyplot as plt
import numpy as np
import PIL.Image
from labelme.logger import logger
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("label_png", help="label PNG file")
args = parser.parse_args()
lbl = np.asarray(PIL.Image.open(args.label_png))
logger.info("label shape: {}".format(lbl.shape))
logger.info("unique label values: {}".format(np.unique(lbl)))
lbl_viz = imgviz.label2rgb(lbl)
plt.imshow(lbl_viz)
plt.show()
if __name__ == "__main__":
main()
import argparse
import base64
import json
import os
import os.path as osp
import imgviz
import PIL.Image
from labelme.logger import logger
from labelme import utils
def main():
logger.warning(
"This script is aimed to demonstrate how to convert the "
"JSON file to a single image dataset."
)
logger.warning(
"It won't handle multiple JSON files to generate a "
"real-use dataset."
)
parser = argparse.ArgumentParser()
parser.add_argument("json_file")
parser.add_argument("-o", "--out", default=None)
args = parser.parse_args()
json_file = args.json_file
if args.out is None:
out_dir = osp.basename(json_file).replace(".", "_")
out_dir = osp.join(osp.dirname(json_file), out_dir)
else:
out_dir = args.out
if not osp.exists(out_dir):
os.mkdir(out_dir)
data = json.load(open(json_file))
imageData = data.get("imageData")
if not imageData:
imagePath = os.path.join(os.path.dirname(json_file), data["imagePath"])
with open(imagePath, "rb") as f:
imageData = f.read()
imageData = base64.b64encode(imageData).decode("utf-8")
img = utils.img_b64_to_arr(imageData)
label_name_to_value = {"_background_": 0}
for shape in sorted(data["shapes"], key=lambda x: x["label"]):
label_name = shape["label"]
if label_name in label_name_to_value:
label_value = label_name_to_value[label_name]
else:
label_value = len(label_name_to_value)
label_name_to_value[label_name] = label_value
lbl, _ = utils.shapes_to_label(
img.shape, data["shapes"], label_name_to_value
)
label_names = [None] * (max(label_name_to_value.values()) + 1)
for name, value in label_name_to_value.items():
label_names[value] = name
lbl_viz = imgviz.label2rgb(
lbl, imgviz.asgray(img), label_names=label_names, loc="rb"
)
PIL.Image.fromarray(img).save(osp.join(out_dir, "img.png"))
utils.lblsave(osp.join(out_dir, "label.png"), lbl)
PIL.Image.fromarray(lbl_viz).save(osp.join(out_dir, "label_viz.png"))
with open(osp.join(out_dir, "label_names.txt"), "w") as f:
for lbl_name in label_names:
f.write(lbl_name + "\n")
logger.info("Saved to: {}".format(out_dir))
if __name__ == "__main__":
main()
#!/usr/bin/env python
from __future__ import print_function
import argparse
import distutils.spawn
import json
import os
import os.path as osp
import platform
import shlex
import subprocess
import sys
def get_ip():
dist = platform.platform().split("-")[0]
if dist == "Linux":
return ""
elif dist == "Darwin":
cmd = "ifconfig en0"
output = subprocess.check_output(shlex.split(cmd))
if str != bytes: # Python3
output = output.decode("utf-8")
for row in output.splitlines():
cols = row.strip().split(" ")
if cols[0] == "inet":
ip = cols[1]
return ip
else:
raise RuntimeError("No ip is found.")
else:
raise RuntimeError("Unsupported platform.")
def labelme_on_docker(in_file, out_file):
ip = get_ip()
cmd = "xhost + %s" % ip
subprocess.check_output(shlex.split(cmd))
if out_file:
out_file = osp.abspath(out_file)
if osp.exists(out_file):
raise RuntimeError("File exists: %s" % out_file)
else:
open(osp.abspath(out_file), "w")
cmd = (
"docker run -it --rm"
" -e DISPLAY={0}:0"
" -e QT_X11_NO_MITSHM=1"
" -v /tmp/.X11-unix:/tmp/.X11-unix"
" -v {1}:{2}"
" -w /home/developer"
)
in_file_a = osp.abspath(in_file)
in_file_b = osp.join("/home/developer", osp.basename(in_file))
cmd = cmd.format(
ip,
in_file_a,
in_file_b,
)
if out_file:
out_file_a = osp.abspath(out_file)
out_file_b = osp.join("/home/developer", osp.basename(out_file))
cmd += " -v {0}:{1}".format(out_file_a, out_file_b)
cmd += " wkentaro/labelme labelme {0}".format(in_file_b)
if out_file:
cmd += " -O {0}".format(out_file_b)
subprocess.call(shlex.split(cmd))
if out_file:
try:
json.load(open(out_file))
return out_file
except Exception:
if open(out_file).read() == "":
os.remove(out_file)
raise RuntimeError("Annotation is cancelled.")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("in_file", help="Input file or directory.")
parser.add_argument("-O", "--output")
args = parser.parse_args()
if not distutils.spawn.find_executable("docker"):
print("Please install docker", file=sys.stderr)
sys.exit(1)
try:
out_file = labelme_on_docker(args.in_file, args.output)
if out_file:
print("Saved to: %s" % out_file)
except RuntimeError as e:
sys.stderr.write(e.__str__() + "\n")
sys.exit(1)
if __name__ == "__main__":
main()
import os.path as osp
import shutil
import yaml
from labelme.logger import logger
here = osp.dirname(osp.abspath(__file__))
def update_dict(target_dict, new_dict, validate_item=None):
for key, value in new_dict.items():
if validate_item:
validate_item(key, value)
if key not in target_dict:
logger.warn("Skipping unexpected key in config: {}".format(key))
continue
if isinstance(target_dict[key], dict) and isinstance(value, dict):
update_dict(target_dict[key], value, validate_item=validate_item)
else:
target_dict[key] = value
# -----------------------------------------------------------------------------
def get_default_config():
# labelme
config_file = osp.join(here, "default_config.yaml")
with open(config_file, encoding='utf-8') as f:
config = yaml.safe_load(f)
# save default config to ~/.labelmerc
user_config_file = osp.join(osp.expanduser("~"), ".labelmerc")
if not osp.exists(user_config_file):
try:
shutil.copy(config_file, user_config_file)
except Exception:
logger.warn("Failed to save config: {}".format(user_config_file))
return config
def validate_config_item(key, value):
if key == "validate_label" and value not in [None, "exact"]:
raise ValueError(
"Unexpected value for config key 'validate_label': {}".format(
value
)
)
if key == "shape_color" and value not in [None, "auto", "manual"]:
raise ValueError(
"Unexpected value for config key 'shape_color': {}".format(value)
)
if key == "labels" and value is not None and len(value) != len(set(value)):
raise ValueError(
"Duplicates are detected for config key 'labels': {}".format(value)
)
def get_config(config_file_or_yaml=None, config_from_args=None):
# 1. default config
config = get_default_config()
# 2. specified as file or yaml
if config_file_or_yaml is not None:
config_from_yaml = yaml.safe_load(config_file_or_yaml)
if not isinstance(config_from_yaml, dict):
with open(config_from_yaml, encoding='utf-8') as f:
logger.info(
"Loading config file from: {}".format(config_from_yaml)
)
config_from_yaml = yaml.safe_load(f)
update_dict(
config, config_from_yaml, validate_item=validate_config_item
)
# 3. command line argument or specified config file
if config_from_args is not None:
update_dict(
config, config_from_args, validate_item=validate_config_item
)
return config
auto_save: true
display_label_popup: true
store_data: true
keep_prev: false
keep_prev_scale: false
keep_prev_brightness: false
keep_prev_contrast: false
logger_level: info
flags: null
label_flags: null
labels: null
file_search: null
sort_labels: true
validate_label: null
default_shape_color: [0, 255, 0]
shape_color: auto # null, 'auto', 'manual'
shift_auto_shape_color: 0
label_colors: null
shape:
# drawing
line_color: [0, 255, 0, 128]
fill_color: [0, 255, 0, 0] # transparent
vertex_fill_color: [0, 255, 0, 255]
# selecting / hovering
select_line_color: [255, 255, 255, 255]
select_fill_color: [0, 255, 0, 155]
hvertex_fill_color: [255, 255, 255, 255]
point_size: 8
# main
flag_dock:
show: true
closable: true
movable: true
floatable: true
label_dock:
show: true
closable: true
movable: true
floatable: true
shape_dock:
show: true
closable: true
movable: true
floatable: true
file_dock:
show: true
closable: true
movable: true
floatable: true
# label_dialog
show_label_text_field: true
label_completion: startswith
fit_to_content:
column: true
row: false
# canvas
epsilon: 10.0
canvas:
# None: do nothing
# close: close polygon
double_click: close
# The max number of edits we can undo
num_backups: 10
# show crosshair
crosshair:
polygon: false
rectangle: true
circle: false
line: false
point: false
linestrip: false
shortcuts:
close: Ctrl+W
open: Ctrl+O
open_dir: Ctrl+U
quit: Ctrl+Q
save: Ctrl+S
save_as: Ctrl+Shift+S
save_to: null
delete_file: Ctrl+Delete
open_next: [D, Ctrl+Shift+D]
open_prev: [A, Ctrl+Shift+A]
zoom_in: [Ctrl++, Ctrl+=]
zoom_out: Ctrl+-
zoom_to_original: Ctrl+0
fit_window: Ctrl+F
fit_width: Ctrl+Shift+F
create_polygon: Ctrl+N
create_rectangle: Ctrl+R
create_circle: null
create_line: null
create_point: null
create_linestrip: null
edit_polygon: Ctrl+J
delete_polygon: Delete
duplicate_polygon: Ctrl+D
copy_polygon: Ctrl+C
paste_polygon: Ctrl+V
undo: Ctrl+Z
undo_last_point: Ctrl+Z
add_point_to_edge: Ctrl+Shift+P
edit_label: Ctrl+E
toggle_keep_prev_mode: Ctrl+P
remove_selected_point: [Meta+H, Backspace]
import base64
import contextlib
import io
import json
import os.path as osp
import PIL.Image
from labelme import __version__
from labelme.logger import logger
from labelme import PY2
from labelme import QT4
from labelme import utils
PIL.Image.MAX_IMAGE_PIXELS = None
@contextlib.contextmanager
def open(name, mode):
assert mode in ["r", "w"]
if PY2:
mode += "b"
encoding = None
else:
encoding = "utf-8"
yield io.open(name, mode, encoding=encoding)
return
class LabelFileError(Exception):
pass
class LabelFile(object):
suffix = ".json"
def __init__(self, filename=None):
self.shapes = []
self.imagePath = None
self.imageData = None
if filename is not None:
self.load(filename)
self.filename = filename
@staticmethod
def load_image_file(filename):
try:
image_pil = PIL.Image.open(filename)
except IOError:
logger.error("Failed opening image file: {}".format(filename))
return
# apply orientation to image according to exif
image_pil = utils.apply_exif_orientation(image_pil)
with io.BytesIO() as f:
ext = osp.splitext(filename)[1].lower()
if PY2 and QT4:
format = "PNG"
elif ext in [".jpg", ".jpeg"]:
format = "JPEG"
else:
format = "PNG"
image_pil.save(f, format=format)
f.seek(0)
return f.read()
def load(self, filename):
keys = [
"version",
"imageData",
"imagePath",
"shapes", # polygonal annotations
"flags", # image level flags
"imageHeight",
"imageWidth",
]
shape_keys = [
"label",
"points",
"group_id",
"shape_type",
"flags",
"description",
]
try:
with open(filename, "r") as f:
data = json.load(f)
version = data.get("version")
if version is None:
logger.warning(
"Loading JSON file ({}) of unknown version".format(
filename
)
)
elif version.split(".")[0] != __version__.split(".")[0]:
logger.warning(
"This JSON file ({}) may be incompatible with "
"current labelme. version in file: {}, "
"current version: {}".format(
filename, version, __version__
)
)
if data["imageData"] is not None:
imageData = base64.b64decode(data["imageData"])
if PY2 and QT4:
imageData = utils.img_data_to_png_data(imageData)
else:
# relative path from label file to relative path from cwd
imagePath = osp.join(osp.dirname(filename), data["imagePath"])
imageData = self.load_image_file(imagePath)
flags = data.get("flags") or {}
imagePath = data["imagePath"]
self._check_image_height_and_width(
base64.b64encode(imageData).decode("utf-8"),
data.get("imageHeight"),
data.get("imageWidth"),
)
shapes = [
dict(
label=s["label"],
points=s["points"],
shape_type=s.get("shape_type", "polygon"),
flags=s.get("flags", {}),
description=s.get("description"),
group_id=s.get("group_id"),
other_data={
k: v for k, v in s.items() if k not in shape_keys
},
)
for s in data["shapes"]
]
except Exception as e:
raise LabelFileError(e)
otherData = {}
for key, value in data.items():
if key not in keys:
otherData[key] = value
# Only replace data after everything is loaded.
self.flags = flags
self.shapes = shapes
self.imagePath = imagePath
self.imageData = imageData
self.filename = filename
self.otherData = otherData
@staticmethod
def _check_image_height_and_width(imageData, imageHeight, imageWidth):
img_arr = utils.img_b64_to_arr(imageData)
if imageHeight is not None and img_arr.shape[0] != imageHeight:
logger.error(
"imageHeight does not match with imageData or imagePath, "
"so getting imageHeight from actual image."
)
imageHeight = img_arr.shape[0]
if imageWidth is not None and img_arr.shape[1] != imageWidth:
logger.error(
"imageWidth does not match with imageData or imagePath, "
"so getting imageWidth from actual image."
)
imageWidth = img_arr.shape[1]
return imageHeight, imageWidth
def save(
self,
filename,
shapes,
imagePath,
imageHeight,
imageWidth,
imageData=None,
otherData=None,
flags=None,
):
if imageData is not None:
imageData = base64.b64encode(imageData).decode("utf-8")
imageHeight, imageWidth = self._check_image_height_and_width(
imageData, imageHeight, imageWidth
)
if otherData is None:
otherData = {}
if flags is None:
flags = {}
data = dict(
version=__version__,
flags=flags,
shapes=shapes,
imagePath=imagePath,
imageData=imageData,
imageHeight=imageHeight,
imageWidth=imageWidth,
)
for key, value in otherData.items():
assert key not in data
data[key] = value
try:
with open(filename, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
self.filename = filename
except Exception as e:
raise LabelFileError(e)
@staticmethod
def is_label_file(filename):
return osp.splitext(filename)[1].lower() == LabelFile.suffix
import datetime
import logging
import os
import termcolor
if os.name == "nt": # Windows
import colorama
colorama.init()
from . import __appname__
COLORS = {
"WARNING": "yellow",
"INFO": "white",
"DEBUG": "blue",
"CRITICAL": "red",
"ERROR": "red",
}
class ColoredFormatter(logging.Formatter):
def __init__(self, fmt, use_color=True):
logging.Formatter.__init__(self, fmt)
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
def colored(text):
return termcolor.colored(
text,
color=COLORS[levelname],
attrs={"bold": True},
)
record.levelname2 = colored("{:<7}".format(record.levelname))
record.message2 = colored(record.msg)
asctime2 = datetime.datetime.fromtimestamp(record.created)
record.asctime2 = termcolor.colored(asctime2, color="green")
record.module2 = termcolor.colored(record.module, color="cyan")
record.funcName2 = termcolor.colored(record.funcName, color="cyan")
record.lineno2 = termcolor.colored(record.lineno, color="cyan")
return logging.Formatter.format(self, record)
class ColoredLogger(logging.Logger):
FORMAT = (
"[%(levelname2)s] %(module2)s:%(funcName2)s:%(lineno2)s - %(message2)s"
)
def __init__(self, name):
logging.Logger.__init__(self, name, logging.INFO)
color_formatter = ColoredFormatter(self.FORMAT)
console = logging.StreamHandler()
console.setFormatter(color_formatter)
self.addHandler(console)
return
logger = logging.getLogger(__appname__)
logger.__class__ = ColoredLogger
import json
import os.path as osp
import imgviz
import labelme.utils
def assert_labelfile_sanity(filename):
assert osp.exists(filename)
data = json.load(open(filename))
assert "imagePath" in data
imageData = data.get("imageData", None)
if imageData is None:
parent_dir = osp.dirname(filename)
img_file = osp.join(parent_dir, data["imagePath"])
assert osp.exists(img_file)
img = imgviz.io.imread(img_file)
else:
img = labelme.utils.img_b64_to_arr(imageData)
H, W = img.shape[:2]
assert H == data["imageHeight"]
assert W == data["imageWidth"]
assert "shapes" in data
for shape in data["shapes"]:
assert "label" in shape
assert "points" in shape
for x, y in shape["points"]:
assert 0 <= x <= W
assert 0 <= y <= H
# flake8: noqa
from ._io import lblsave
from .image import apply_exif_orientation
from .image import img_arr_to_b64
from .image import img_b64_to_arr
from .image import img_data_to_arr
from .image import img_data_to_pil
from .image import img_data_to_png_data
from .image import img_pil_to_data
from .shape import labelme_shapes_to_label
from .shape import masks_to_bboxes
from .shape import polygons_to_mask
from .shape import shape_to_mask
from .shape import shapes_to_label
from .qt import newIcon
from .qt import newButton
from .qt import newAction
from .qt import addActions
from .qt import labelValidator
from .qt import struct
from .qt import distance
from .qt import distancetoline
from .qt import fmtShortcut
import os.path as osp
import numpy as np
import PIL.Image
def lblsave(filename, lbl):
import imgviz
if osp.splitext(filename)[1] != ".png":
filename += ".png"
# Assume label ranses [-1, 254] for int32,
# and [0, 255] for uint8 as VOC.
if lbl.min() >= -1 and lbl.max() < 255:
lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode="P")
colormap = imgviz.label_colormap()
lbl_pil.putpalette(colormap.flatten())
lbl_pil.save(filename)
else:
raise ValueError(
"[%s] Cannot save the pixel-wise class label as PNG. "
"Please consider using the .npy format." % filename
)
import base64
import io
import numpy as np
import PIL.ExifTags
import PIL.Image
import PIL.ImageOps
def img_data_to_pil(img_data):
f = io.BytesIO()
f.write(img_data)
img_pil = PIL.Image.open(f)
return img_pil
def img_data_to_arr(img_data):
img_pil = img_data_to_pil(img_data)
img_arr = np.array(img_pil)
return img_arr
def img_b64_to_arr(img_b64):
img_data = base64.b64decode(img_b64)
img_arr = img_data_to_arr(img_data)
return img_arr
def img_pil_to_data(img_pil):
f = io.BytesIO()
img_pil.save(f, format="PNG")
img_data = f.getvalue()
return img_data
def img_arr_to_b64(img_arr):
img_pil = PIL.Image.fromarray(img_arr)
f = io.BytesIO()
img_pil.save(f, format="PNG")
img_bin = f.getvalue()
if hasattr(base64, "encodebytes"):
img_b64 = base64.encodebytes(img_bin)
else:
img_b64 = base64.encodestring(img_bin)
return img_b64
def img_data_to_png_data(img_data):
with io.BytesIO() as f:
f.write(img_data)
img = PIL.Image.open(f)
with io.BytesIO() as f:
img.save(f, "PNG")
f.seek(0)
return f.read()
def apply_exif_orientation(image):
try:
exif = image._getexif()
except AttributeError:
exif = None
if exif is None:
return image
exif = {
PIL.ExifTags.TAGS[k]: v
for k, v in exif.items()
if k in PIL.ExifTags.TAGS
}
orientation = exif.get("Orientation", None)
if orientation == 1:
# do nothing
return image
elif orientation == 2:
# left-to-right mirror
return PIL.ImageOps.mirror(image)
elif orientation == 3:
# rotate 180
return image.transpose(PIL.Image.ROTATE_180)
elif orientation == 4:
# top-to-bottom mirror
return PIL.ImageOps.flip(image)
elif orientation == 5:
# top-to-left mirror
return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270))
elif orientation == 6:
# rotate 270
return image.transpose(PIL.Image.ROTATE_270)
elif orientation == 7:
# top-to-right mirror
return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90))
elif orientation == 8:
# rotate 90
return image.transpose(PIL.Image.ROTATE_90)
else:
return image
from math import sqrt
import os.path as osp
import numpy as np
from qtpy import QtCore
from qtpy import QtGui
from qtpy import QtWidgets
here = osp.dirname(osp.abspath(__file__))
def newIcon(icon):
icons_dir = osp.join(here, "../icons")
return QtGui.QIcon(osp.join(":/", icons_dir, "%s.png" % icon))
def newButton(text, icon=None, slot=None):
b = QtWidgets.QPushButton(text)
if icon is not None:
b.setIcon(newIcon(icon))
if slot is not None:
b.clicked.connect(slot)
return b
def newAction(
parent,
text,
slot=None,
shortcut=None,
icon=None,
tip=None,
checkable=False,
enabled=True,
checked=False,
):
"""Create a new action and assign callbacks, shortcuts, etc."""
a = QtWidgets.QAction(text, parent)
if icon is not None:
a.setIconText(text.replace(" ", "\n"))
a.setIcon(newIcon(icon))
if shortcut is not None:
if isinstance(shortcut, (list, tuple)):
a.setShortcuts(shortcut)
else:
a.setShortcut(shortcut)
if tip is not None:
a.setToolTip(tip)
a.setStatusTip(tip)
if slot is not None:
a.triggered.connect(slot)
if checkable:
a.setCheckable(True)
a.setEnabled(enabled)
a.setChecked(checked)
return a
def addActions(widget, actions):
for action in actions:
if action is None:
widget.addSeparator()
elif isinstance(action, QtWidgets.QMenu):
widget.addMenu(action)
else:
widget.addAction(action)
def labelValidator():
return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None)
class struct(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def distance(p):
return sqrt(p.x() * p.x() + p.y() * p.y())
def distancetoline(point, line):
p1, p2 = line
p1 = np.array([p1.x(), p1.y()])
p2 = np.array([p2.x(), p2.y()])
p3 = np.array([point.x(), point.y()])
if np.dot((p3 - p1), (p2 - p1)) < 0:
return np.linalg.norm(p3 - p1)
if np.dot((p3 - p2), (p1 - p2)) < 0:
return np.linalg.norm(p3 - p2)
if np.linalg.norm(p2 - p1) == 0:
return 0
return np.linalg.norm(np.cross(p2 - p1, p1 - p3)) / np.linalg.norm(p2 - p1)
def fmtShortcut(text):
mod, key = text.split("+", 1)
return "<b>%s</b>+<b>%s</b>" % (mod, key)
import math
import uuid
import numpy as np
import PIL.Image
import PIL.ImageDraw
from labelme.logger import logger
def polygons_to_mask(img_shape, polygons, shape_type=None):
logger.warning(
"The 'polygons_to_mask' function is deprecated, "
"use 'shape_to_mask' instead."
)
return shape_to_mask(img_shape, points=polygons, shape_type=shape_type)
def shape_to_mask(
img_shape, points, shape_type=None, line_width=10, point_size=5
):
mask = np.zeros(img_shape[:2], dtype=np.uint8)
mask = PIL.Image.fromarray(mask)
draw = PIL.ImageDraw.Draw(mask)
xy = [tuple(point) for point in points]
if shape_type == "circle":
assert len(xy) == 2, "Shape of shape_type=circle must have 2 points"
(cx, cy), (px, py) = xy
d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2)
draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1)
elif shape_type == "rectangle":
assert len(xy) == 2, "Shape of shape_type=rectangle must have 2 points"
draw.rectangle(xy, outline=1, fill=1)
elif shape_type == "line":
assert len(xy) == 2, "Shape of shape_type=line must have 2 points"
draw.line(xy=xy, fill=1, width=line_width)
elif shape_type == "linestrip":
draw.line(xy=xy, fill=1, width=line_width)
elif shape_type == "point":
assert len(xy) == 1, "Shape of shape_type=point must have 1 points"
cx, cy = xy[0]
r = point_size
draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1)
else:
assert len(xy) > 2, "Polygon must have points more than 2"
draw.polygon(xy=xy, outline=1, fill=1)
mask = np.array(mask, dtype=bool)
return mask
def shapes_to_label(img_shape, shapes, label_name_to_value):
cls = np.zeros(img_shape[:2], dtype=np.int32)
ins = np.zeros_like(cls)
instances = []
for shape in shapes:
points = shape["points"]
label = shape["label"]
group_id = shape.get("group_id")
if group_id is None:
group_id = uuid.uuid1()
shape_type = shape.get("shape_type", None)
cls_name = label
instance = (cls_name, group_id)
if instance not in instances:
instances.append(instance)
ins_id = instances.index(instance) + 1
cls_id = label_name_to_value[cls_name]
mask = shape_to_mask(img_shape[:2], points, shape_type)
cls[mask] = cls_id
ins[mask] = ins_id
return cls, ins
def labelme_shapes_to_label(img_shape, shapes):
logger.warn(
"labelme_shapes_to_label is deprecated, so please use "
"shapes_to_label."
)
label_name_to_value = {"_background_": 0}
for shape in shapes:
label_name = shape["label"]
if label_name in label_name_to_value:
label_value = label_name_to_value[label_name]
else:
label_value = len(label_name_to_value)
label_name_to_value[label_name] = label_value
lbl, _ = shapes_to_label(img_shape, shapes, label_name_to_value)
return lbl, label_name_to_value
def masks_to_bboxes(masks):
if masks.ndim != 3:
raise ValueError(
"masks.ndim must be 3, but it is {}".format(masks.ndim)
)
if masks.dtype != bool:
raise ValueError(
"masks.dtype must be bool type, but it is {}".format(masks.dtype)
)
bboxes = []
for mask in masks:
where = np.argwhere(mask)
(y1, x1), (y2, x2) = where.min(0), where.max(0) + 1
bboxes.append((y1, x1, y2, x2))
bboxes = np.asarray(bboxes, dtype=np.float32)
return bboxes
# flake8: noqa
from .brightness_contrast_dialog import BrightnessContrastDialog
from .canvas import Canvas
from .color_dialog import ColorDialog
from .file_dialog_preview import FileDialogPreview
from .label_dialog import LabelDialog
from .label_dialog import LabelQLineEdit
from .label_list_widget import LabelListWidget
from .label_list_widget import LabelListWidgetItem
from .tool_bar import ToolBar
from .unique_label_qlist_widget import UniqueLabelQListWidget
from .zoom_widget import ZoomWidget
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):
super(BrightnessContrastDialog, self).__init__(parent)
self.setModal(True)
self.setWindowTitle("/")
self.slider_brightness = self._create_slider()
self.slider_contrast = self._create_slider()
formLayout = QtWidgets.QFormLayout()
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
self.callback = callback
def onNewValue(self, value):
brightness = self.slider_brightness.value() / 50.0
contrast = self.slider_contrast.value() / 50.0
img = self.img
img = PIL.ImageEnhance.Brightness(img).enhance(brightness)
img = PIL.ImageEnhance.Contrast(img).enhance(contrast)
img_data = utils.img_pil_to_data(img)
qimage = QtGui.QImage.fromData(img_data)
self.callback(qimage)
def _create_slider(self):
slider = QtWidgets.QSlider(Qt.Horizontal)
slider.setRange(0, 150)
slider.setValue(50)
slider.valueChanged.connect(self.onNewValue)
return slider
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):
super(ColorDialog, self).__init__(parent)
self.setOption(QtWidgets.QColorDialog.ShowAlphaChannel)
# The Mac native dialog does not support our restore button.
self.setOption(QtWidgets.QColorDialog.DontUseNativeDialog)
# Add a restore defaults button.
# The default is set at invocation time, so that it
# works across dialogs for different elements.
self.default = None
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
if title:
self.setWindowTitle(title)
if value:
self.setCurrentColor(value)
return self.currentColor() if self.exec_() else None
def checkRestore(self, button):
if (
self.bb.buttonRole(button) & QtWidgets.QDialogButtonBox.ResetRole
and self.default
):
self.setCurrentColor(self.default)
from qtpy.QtCore import Qt
from qtpy import QtWidgets
class EscapableQListWidget(QtWidgets.QListWidget):
def keyPressEvent(self, event):
super(EscapableQListWidget, self).keyPressEvent(event)
if event.key() == Qt.Key_Escape:
self.clearSelection()
from qtpy import QtCore
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):
def __init__(self, *args, **kwargs):
super(ScrollAreaPreview, self).__init__(*args, **kwargs)
self.setWidgetResizable(True)
content = QtWidgets.QWidget(self)
self.setWidget(content)
lay = QtWidgets.QVBoxLayout(content)
self.label = QtWidgets.QLabel(content)
self.label.setWordWrap(True)
lay.addWidget(self.label)
def setText(self, text):
self.label.setText(text)
def setPixmap(self, pixmap):
self.label.setPixmap(pixmap)
def clear(self):
self.label.clear()
class FileDialogPreview(QtWidgets.QFileDialog):
def __init__(self, *args, **kwargs):
super(FileDialogPreview, self).__init__(*args, **kwargs)
self.setOption(self.DontUseNativeDialog, True)
#
self.setLabelText(self.LookIn, ":")
self.setLabelText(self.FileName, ":")
self.setLabelText(self.FileType, ":")
self.setLabelText(self.Accept, "")
self.setLabelText(self.Reject, "")
self.labelPreview = ScrollAreaPreview(self)
self.labelPreview.setFixedSize(300, 300)
self.labelPreview.setHidden(True)
box = QtWidgets.QVBoxLayout()
box.addWidget(self.labelPreview)
box.addStretch()
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"):
with open(path, "r") as f:
data = json.load(f)
self.labelPreview.setText(
json.dumps(data, indent=4, sort_keys=False)
)
self.labelPreview.label.setAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
)
self.labelPreview.setHidden(False)
else:
pixmap = QtGui.QPixmap(path)
if pixmap.isNull():
self.labelPreview.clear()
self.labelPreview.setHidden(True)
else:
self.labelPreview.setPixmap(
pixmap.scaled(
self.labelPreview.width() - 30,
self.labelPreview.height() - 30,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation,
)
)
self.labelPreview.label.setAlignment(QtCore.Qt.AlignCenter)
self.labelPreview.setHidden(False)
from qtpy import QtCore
from qtpy.QtCore import Qt
from qtpy import QtGui
from qtpy.QtGui import QPalette
from qtpy import QtWidgets
from qtpy.QtWidgets import QStyle
# https://stackoverflow.com/a/2039745/4158863
class HTMLDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(HTMLDelegate, self).__init__()
self.doc = QtGui.QTextDocument(self)
def paint(self, painter, option, index):
painter.save()
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
self.doc.setHtml(options.text)
options.text = ""
style = (
QtWidgets.QApplication.style()
if options.widget is None
else options.widget.style()
)
style.drawControl(QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
if option.state & QStyle.State_Selected:
ctx.palette.setColor(
QPalette.Text,
option.palette.color(
QPalette.Active, QPalette.HighlightedText
),
)
else:
ctx.palette.setColor(
QPalette.Text,
option.palette.color(QPalette.Active, QPalette.Text),
)
textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options)
if index.column() != 0:
textRect.adjust(5, 0, 0, 0)
thefuckyourshitup_constant = 4
margin = (option.rect.height() - options.fontMetrics.height()) // 2
margin = margin - thefuckyourshitup_constant
textRect.setTop(textRect.top() + margin)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
thefuckyourshitup_constant = 4
#
original_height = int(self.doc.size().height() - thefuckyourshitup_constant)
# 35
min_height = 35
item_height = max(original_height, min_height)
return QtCore.QSize(
int(self.doc.idealWidth()),
item_height,
)
class LabelListWidgetItem(QtGui.QStandardItem):
def __init__(self, text=None, shape=None):
super(LabelListWidgetItem, self).__init__()
self.setText(text or "")
self.setShape(shape)
self.setCheckable(True)
self.setCheckState(Qt.Checked)
self.setEditable(False)
self.setTextAlignment(Qt.AlignBottom)
def clone(self):
return LabelListWidgetItem(self.text(), self.shape())
def setShape(self, shape):
self.setData(shape, Qt.UserRole)
def shape(self):
return self.data(Qt.UserRole)
def __hash__(self):
return id(self)
def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self.text())
class StandardItemModel(QtGui.QStandardItemModel):
itemDropped = QtCore.Signal()
def removeRows(self, *args, **kwargs):
ret = super().removeRows(*args, **kwargs)
self.itemDropped.emit()
return ret
class LabelListWidget(QtWidgets.QListView):
itemDoubleClicked = QtCore.Signal(LabelListWidgetItem)
itemSelectionChanged = QtCore.Signal(list, list)
def __init__(self):
super(LabelListWidget, self).__init__()
self._selectedItems = []
self.setWindowFlags(Qt.Window)
self.setModel(StandardItemModel())
self.model().setItemPrototype(LabelListWidgetItem())
self.setItemDelegate(HTMLDelegate())
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.setDefaultDropAction(Qt.MoveAction)
self.doubleClicked.connect(self.itemDoubleClickedEvent)
self.selectionModel().selectionChanged.connect(
self.itemSelectionChangedEvent
)
def __len__(self):
return self.model().rowCount()
def __getitem__(self, i):
return self.model().item(i)
def __iter__(self):
for i in range(len(self)):
yield self[i]
@property
def itemDropped(self):
return self.model().itemDropped
@property
def itemChanged(self):
return self.model().itemChanged
def itemSelectionChangedEvent(self, selected, deselected):
selected = [self.model().itemFromIndex(i) for i in selected.indexes()]
deselected = [
self.model().itemFromIndex(i) for i in deselected.indexes()
]
self.itemSelectionChanged.emit(selected, deselected)
def itemDoubleClickedEvent(self, index):
self.itemDoubleClicked.emit(self.model().itemFromIndex(index))
def selectedItems(self):
return [self.model().itemFromIndex(i) for i in self.selectedIndexes()]
def scrollToItem(self, item):
self.scrollTo(self.model().indexFromItem(item))
def addItem(self, item):
if not isinstance(item, LabelListWidgetItem):
raise TypeError("item must be LabelListWidgetItem")
self.model().setItem(self.model().rowCount(), 0, item)
item.setSizeHint(self.itemDelegate().sizeHint(None, None))
def removeItem(self, item):
index = self.model().indexFromItem(item)
self.model().removeRows(index.row(), 1)
def selectItem(self, item):
index = self.model().indexFromItem(item)
self.selectionModel().select(index, QtCore.QItemSelectionModel.Select)
def findItemByShape(self, shape):
for row in range(self.model().rowCount()):
item = self.model().item(row, 0)
if item.shape() == shape:
return item
raise ValueError("cannot find shape: {}".format(shape))
def clear(self):
self.model().clear()
from qtpy import QtCore
from qtpy import QtWidgets
class ToolBar(QtWidgets.QToolBar):
def __init__(self, title):
super(ToolBar, self).__init__(title)
layout = self.layout()
m = (0, 0, 0, 0)
layout.setSpacing(0)
layout.setContentsMargins(*m)
self.setContentsMargins(*m)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
def addAction(self, action):
if isinstance(action, QtWidgets.QWidgetAction):
return super(ToolBar, self).addAction(action)
btn = QtWidgets.QToolButton()
btn.setDefaultAction(action)
btn.setToolButtonStyle(self.toolButtonStyle())
self.addWidget(btn)
# center align
for i in range(self.layout().count()):
if isinstance(
self.layout().itemAt(i).widget(), QtWidgets.QToolButton
):
self.layout().itemAt(i).setAlignment(QtCore.Qt.AlignCenter)
# -*- encoding: utf-8 -*-
import html
from qtpy import QtCore
from qtpy.QtCore import Qt
from qtpy import QtWidgets
from .escapable_qlist_widget import EscapableQListWidget
class UniqueLabelQListWidget(EscapableQListWidget):
def mousePressEvent(self, event):
super(UniqueLabelQListWidget, self).mousePressEvent(event)
if not self.indexAt(event.pos()).isValid():
self.clearSelection()
def findItemByLabel(self, label):
for row in range(self.count()):
item = self.item(row)
if item.data(Qt.UserRole) == label:
return item
def createItemFromLabel(self, label):
if self.findItemByLabel(label):
raise ValueError(
"Item for label '{}' already exists".format(label)
)
item = QtWidgets.QListWidgetItem()
item.setData(Qt.UserRole, label)
return item
def setItemLabel(self, item, label, color=None):
qlabel = QtWidgets.QLabel()
if color is None:
qlabel.setText("{}".format(label))
else:
qlabel.setText(
'{} <font color="#{:02x}{:02x}{:02x}"></font>'.format(
html.escape(label), *color
)
)
qlabel.setAlignment(Qt.AlignBottom)
#
original_size = qlabel.sizeHint()
# 35
min_height = 40
item_height = max(original_size.height(), min_height)
item.setSizeHint(QtCore.QSize(original_size.width(), item_height))
self.setItemWidget(item, qlabel)
from qtpy import QtCore
from qtpy import QtGui
from qtpy import QtWidgets
class ZoomWidget(QtWidgets.QSpinBox):
def __init__(self, value=100):
super(ZoomWidget, self).__init__()
self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
self.setRange(1, 1000)
self.setSuffix(" %")
self.setValue(value)
self.setToolTip("Zoom Level")
self.setStatusTip(self.toolTip())
self.setAlignment(QtCore.Qt.AlignCenter)
def minimumSizeHint(self):
height = super(ZoomWidget, self).minimumSizeHint().height()
fm = QtGui.QFontMetrics(self.font())
width = fm.width(str(self.maximum()))
return QtCore.QSize(width, height)
wedcerwqc
\ No newline at end of file
......@@ -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,8 +281,9 @@ 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):
return
......@@ -342,9 +343,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 +496,7 @@ class CropPreviewPanel(QtWidgets.QWidget):
# 清空缓存数据
self._region_images.clear()
self._region_paths = []
self._video_name = None # 清空视频名称,防止刷新时显示其他视频的图片
# 更新统计
self._updateStats()
......@@ -600,16 +612,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):
"""重命名视频文件"""
......
......@@ -243,6 +243,10 @@ class DialogManager:
QMessageBox {
min-width: 400px;
}
QMessageBox QLabel {
border: none;
background: transparent;
}
"""
# 文本对齐方式常量
......@@ -307,6 +311,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
......@@ -670,6 +677,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