forked from YikaiFu-cart/acAubo
Refactor control parameters and enhance depth handling for tomato harvesting
- Updated YOLO model path to a relative path. - Introduced new parameters for depth handling, including minimum and maximum depth, fallback settings, and pixel validation thresholds. - Increased maximum pick angle to accommodate wider fruit stems. - Added cut pull assist feature with configurable options. - Removed unused resource path resolution functions from control_core.py. - Simplified resource path handling in main.py. - Updated UI colors and styles for improved aesthetics. - Removed outdated requirements.txt file.
This commit is contained in:
60
README.md
60
README.md
@ -113,56 +113,6 @@ start_tomatopick.bat
|
|||||||
|
|
||||||
如果需要桌面入口,可以自行在桌面创建快捷方式,并把目标指向 `start_tomatopick.bat`。
|
如果需要桌面入口,可以自行在桌面创建快捷方式,并把目标指向 `start_tomatopick.bat`。
|
||||||
|
|
||||||
## PyInstaller 打包
|
|
||||||
|
|
||||||
本项目已经附带 PyInstaller 配置文件:
|
|
||||||
|
|
||||||
- `TomatoPick.spec`
|
|
||||||
- `build_windows_exe.bat`
|
|
||||||
- `build_linux.sh`
|
|
||||||
|
|
||||||
打包时会一起带上:
|
|
||||||
|
|
||||||
- `models/` 下的 YOLO 模型文件
|
|
||||||
- `tools/1.png` 背景图
|
|
||||||
- `ultralytics` 的运行数据
|
|
||||||
- `pyrealsense2`、`pyaubo_sdk`、`pyaubo_agvc_sdk` 的动态库
|
|
||||||
|
|
||||||
### Windows 打包 exe
|
|
||||||
|
|
||||||
需要在 Windows 机器上执行,PyInstaller 不能在 Linux 上直接交叉生成可运行的 Windows `.exe`。
|
|
||||||
|
|
||||||
```bat
|
|
||||||
build_windows_exe.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
生成结果:
|
|
||||||
|
|
||||||
```text
|
|
||||||
dist\TomatoPick\TomatoPick.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux 打包可执行文件
|
|
||||||
|
|
||||||
在 Linux 上执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x build_linux.sh
|
|
||||||
./build_linux.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
生成结果:
|
|
||||||
|
|
||||||
```text
|
|
||||||
dist/TomatoPick/TomatoPick
|
|
||||||
```
|
|
||||||
|
|
||||||
### 打包后资源路径说明
|
|
||||||
|
|
||||||
- 默认模型路径已调整为 `models/best.pt`
|
|
||||||
- 背景图会优先在打包资源目录中查找 `tools/1.png`
|
|
||||||
- `ui_settings.json` 在打包后会保存到可执行文件所在目录,而不是 PyInstaller 的临时解包目录
|
|
||||||
|
|
||||||
界面中主要按钮:
|
界面中主要按钮:
|
||||||
|
|
||||||
- `启动程序`:运行完整采摘流程,会连接机械臂、AGV、RealSense 并加载 YOLO。
|
- `启动程序`:运行完整采摘流程,会连接机械臂、AGV、RealSense 并加载 YOLO。
|
||||||
@ -234,6 +184,16 @@ best.pt
|
|||||||
|
|
||||||
模型类别、关键点顺序和训练定义需要与上述逻辑一致。
|
模型类别、关键点顺序和训练定义需要与上述逻辑一致。
|
||||||
|
|
||||||
|
### 真实果梗的适配
|
||||||
|
|
||||||
|
采摘流程仍保持原来的两关键点和单次剪刀动作:第 0 点用于定位采摘点,第 1 点只用于计算末端角度。为适应真实串番茄,控制参数新增了以下处理:
|
||||||
|
|
||||||
|
- 剪切点深度无效时,先扩大邻域取中位深度;仍失败时沿第 0 点到第 1 点的果梗方向取有限次数的邻近深度,并将该深度用于剪切点反投影。
|
||||||
|
- 关键点连线按无方向直线处理,方向点在剪切点的上方、下方或侧方均可;默认最大末端旋转已提高到 $70^\circ$。
|
||||||
|
- `CUT_PULL_ASSIST_ENABLED` 默认关闭。没有“已剪断”传感反馈时,程序不能可靠判断剪刀是否剪断了老梗。确认撤离方向、碰撞空间和末端承载能力后,才可开启该项,以在原有撤离前增加一个小的 Y 向拉拽动作。
|
||||||
|
|
||||||
|
首次调试建议保持辅助拉拽关闭,先以低速确认深度回退输出、关键点角度和机械臂姿态均正确。
|
||||||
|
|
||||||
## 运行前检查
|
## 运行前检查
|
||||||
|
|
||||||
完整采摘流程会控制真实硬件。启动前请确认:
|
完整采摘流程会控制真实硬件。启动前请确认:
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs, collect_submodules
|
|
||||||
|
|
||||||
|
|
||||||
project_dir = Path(SPECPATH).resolve().parent
|
|
||||||
|
|
||||||
datas = [
|
|
||||||
(str(project_dir / "acaubo" / "models"), "models"),
|
|
||||||
(str(project_dir / "acaubo" / "tools" / "1.png"), "tools"),
|
|
||||||
]
|
|
||||||
|
|
||||||
binaries = []
|
|
||||||
hiddenimports = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
hiddenimports += collect_submodules(
|
|
||||||
"ultralytics",
|
|
||||||
filter=lambda name: not name.startswith("ultralytics.trackers"),
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
hiddenimports.append("ultralytics")
|
|
||||||
|
|
||||||
for package_name in ("pyrealsense2", "pyaubo_sdk", "pyaubo_agvc_sdk"):
|
|
||||||
try:
|
|
||||||
hiddenimports += collect_submodules(package_name)
|
|
||||||
except Exception:
|
|
||||||
hiddenimports.append(package_name)
|
|
||||||
|
|
||||||
for package_name in ("pyrealsense2", "pyaubo_sdk", "pyaubo_agvc_sdk"):
|
|
||||||
try:
|
|
||||||
binaries += collect_dynamic_libs(package_name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
datas += collect_data_files("ultralytics", include_py_files=False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
[str(project_dir / "acaubo" / "main.py")],
|
|
||||||
pathex=[str(project_dir / "acaubo")],
|
|
||||||
binaries=binaries,
|
|
||||||
datas=datas,
|
|
||||||
hiddenimports=hiddenimports,
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[
|
|
||||||
"lap",
|
|
||||||
"ultralytics.trackers",
|
|
||||||
"ultralytics.trackers.basetrack",
|
|
||||||
"ultralytics.trackers.bot_sort",
|
|
||||||
"ultralytics.trackers.byte_tracker",
|
|
||||||
"ultralytics.trackers.track",
|
|
||||||
"ultralytics.trackers.utils",
|
|
||||||
"ultralytics.trackers.utils.gmc",
|
|
||||||
"ultralytics.trackers.utils.kalman_filter",
|
|
||||||
"ultralytics.trackers.utils.matching",
|
|
||||||
],
|
|
||||||
noarchive=False,
|
|
||||||
optimize=0,
|
|
||||||
)
|
|
||||||
pyz = PYZ(a.pure)
|
|
||||||
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
[],
|
|
||||||
exclude_binaries=True,
|
|
||||||
name="TomatoPick",
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=False,
|
|
||||||
console=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
coll = COLLECT(
|
|
||||||
exe,
|
|
||||||
a.binaries,
|
|
||||||
a.datas,
|
|
||||||
strip=False,
|
|
||||||
upx=False,
|
|
||||||
upx_exclude=[],
|
|
||||||
name="TomatoPick",
|
|
||||||
)
|
|
||||||
33
control.py
33
control.py
@ -27,10 +27,22 @@ AGV_RUN_DISTANCE = 5.0
|
|||||||
TOTAL_DURATION = 300
|
TOTAL_DURATION = 300
|
||||||
AGV_STOP_TIMEOUT = 10
|
AGV_STOP_TIMEOUT = 10
|
||||||
AGV_PICK_SETTLE_DELAY = 1.5
|
AGV_PICK_SETTLE_DELAY = 1.5
|
||||||
YOLO_MODEL_PATH = "models/best.pt"
|
YOLO_MODEL_PATH = "best.pt"
|
||||||
YOLO_DETECT_CONF = 0.5
|
YOLO_DETECT_CONF = 0.5
|
||||||
PICK_CONFIDENCE_THRESHOLD = 0.7
|
PICK_CONFIDENCE_THRESHOLD = 0.7
|
||||||
|
|
||||||
|
# 真实果梗较细时,剪切点像素可能没有直接深度。先使用剪切点邻域深度,
|
||||||
|
# 失败后可沿关键点 0 -> 关键点 1 的方向寻找同一果串的可靠深度。
|
||||||
|
PICK_DEPTH_MIN_M = 0.18
|
||||||
|
PICK_DEPTH_MAX_M = 1.20
|
||||||
|
PICK_DEPTH_WINDOW_RADIUS = 5
|
||||||
|
PICK_DEPTH_FALLBACK_WINDOW_RADIUS = 14
|
||||||
|
PICK_DEPTH_MIN_VALID_PIXELS = 5
|
||||||
|
PICK_DEPTH_MEDIAN_TOLERANCE_M = 0.05
|
||||||
|
PICK_DEPTH_LINE_FALLBACK_ENABLED = True
|
||||||
|
PICK_DEPTH_LINE_FALLBACK_STEPS = 5
|
||||||
|
PICK_DEPTH_LINE_FALLBACK_STEP_PX = 8
|
||||||
|
|
||||||
|
|
||||||
# -------------------- 位姿与轨迹参数 --------------------
|
# -------------------- 位姿与轨迹参数 --------------------
|
||||||
# Home 是机械臂的初始/复位关节位。
|
# Home 是机械臂的初始/复位关节位。
|
||||||
@ -45,7 +57,13 @@ PICKUP_X_OFFSET = 0.0
|
|||||||
PICKUP_Y_OFFSET = -0.013
|
PICKUP_Y_OFFSET = -0.013
|
||||||
PICK_ANGLE_ORIENTATION_INDEX = 4
|
PICK_ANGLE_ORIENTATION_INDEX = 4
|
||||||
PICK_ANGLE_SIGN = 1.0
|
PICK_ANGLE_SIGN = 1.0
|
||||||
MAX_PICK_ANGLE_DEG = 30.0
|
# 真实果梗可能接近横向;仍受限于此值,避免因姿态变化过大造成不可达或碰撞。
|
||||||
|
MAX_PICK_ANGLE_DEG = 70.0
|
||||||
|
|
||||||
|
# 默认关闭。剪刀缺少“已剪断”反馈时,不能自动判断是否应强拉。
|
||||||
|
# 现场确认撤离方向安全后,才可启用小距离辅助拉拽。
|
||||||
|
CUT_PULL_ASSIST_ENABLED = False
|
||||||
|
CUT_PULL_ASSIST_Y_OFFSET = 0.03
|
||||||
|
|
||||||
# 当前项目只使用一个放置位,仍保留列表结构以兼容 control_core。
|
# 当前项目只使用一个放置位,仍保留列表结构以兼容 control_core。
|
||||||
place_positions = [[-0.6203, 0.026, 0.1078, 2.9923, 0.0366, -1.4157]]
|
place_positions = [[-0.6203, 0.026, 0.1078, 2.9923, 0.0366, -1.4157]]
|
||||||
@ -78,6 +96,15 @@ _CONFIG_NAMES = (
|
|||||||
"YOLO_MODEL_PATH",
|
"YOLO_MODEL_PATH",
|
||||||
"YOLO_DETECT_CONF",
|
"YOLO_DETECT_CONF",
|
||||||
"PICK_CONFIDENCE_THRESHOLD",
|
"PICK_CONFIDENCE_THRESHOLD",
|
||||||
|
"PICK_DEPTH_MIN_M",
|
||||||
|
"PICK_DEPTH_MAX_M",
|
||||||
|
"PICK_DEPTH_WINDOW_RADIUS",
|
||||||
|
"PICK_DEPTH_FALLBACK_WINDOW_RADIUS",
|
||||||
|
"PICK_DEPTH_MIN_VALID_PIXELS",
|
||||||
|
"PICK_DEPTH_MEDIAN_TOLERANCE_M",
|
||||||
|
"PICK_DEPTH_LINE_FALLBACK_ENABLED",
|
||||||
|
"PICK_DEPTH_LINE_FALLBACK_STEPS",
|
||||||
|
"PICK_DEPTH_LINE_FALLBACK_STEP_PX",
|
||||||
"HOME_JOINTS",
|
"HOME_JOINTS",
|
||||||
"APPROACH_Y_OFFSET",
|
"APPROACH_Y_OFFSET",
|
||||||
"LIFT_Z_OFFSET",
|
"LIFT_Z_OFFSET",
|
||||||
@ -86,6 +113,8 @@ _CONFIG_NAMES = (
|
|||||||
"PICK_ANGLE_ORIENTATION_INDEX",
|
"PICK_ANGLE_ORIENTATION_INDEX",
|
||||||
"PICK_ANGLE_SIGN",
|
"PICK_ANGLE_SIGN",
|
||||||
"MAX_PICK_ANGLE_DEG",
|
"MAX_PICK_ANGLE_DEG",
|
||||||
|
"CUT_PULL_ASSIST_ENABLED",
|
||||||
|
"CUT_PULL_ASSIST_Y_OFFSET",
|
||||||
"GRIPPER_CLOSE_IO",
|
"GRIPPER_CLOSE_IO",
|
||||||
"GRIPPER_OPEN_IO",
|
"GRIPPER_OPEN_IO",
|
||||||
"GRIPPER_ACTION_DELAY",
|
"GRIPPER_ACTION_DELAY",
|
||||||
|
|||||||
142
control_core.py
142
control_core.py
@ -6,7 +6,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -40,13 +39,6 @@ PICK_ENDPOINT_KEYPOINT_INDEX = 1 # YOLO pose 第 1 个点为方向端点 endpoi
|
|||||||
PICK_KEYPOINT_BOX_MARGIN_RATIO = 0.15 # 关键点允许略超出 bbox 的比例。
|
PICK_KEYPOINT_BOX_MARGIN_RATIO = 0.15 # 关键点允许略超出 bbox 的比例。
|
||||||
PICK_CUTPOINT_MAX_REL_Y = 0.70 # 采摘点在 bbox 内的最大相对高度,防止误取下方点。
|
PICK_CUTPOINT_MAX_REL_Y = 0.70 # 采摘点在 bbox 内的最大相对高度,防止误取下方点。
|
||||||
PICK_KEYPOINT_MIN_DISTANCE_PX = 8.0 # cutpoint 与 endpoint 的最小像素距离。
|
PICK_KEYPOINT_MIN_DISTANCE_PX = 8.0 # cutpoint 与 endpoint 的最小像素距离。
|
||||||
PICK_ENDPOINT_Y_TOLERANCE_PX = 4 # endpoint 允许比 cutpoint 略高的像素容差。
|
|
||||||
PICK_DEPTH_WINDOW_RADIUS = 5 # 首次稳健取深度的窗口半径,5 表示 11x11。
|
|
||||||
PICK_DEPTH_FALLBACK_WINDOW_RADIUS = 10 # 首次深度不足时的扩大窗口半径,10 表示 21x21。
|
|
||||||
PICK_DEPTH_MIN_VALID_PIXELS = 5 # 深度窗口内至少需要的有效深度点数量。
|
|
||||||
PICK_DEPTH_MIN_M = 0.25 # 有效深度下限,单位 m。
|
|
||||||
PICK_DEPTH_MAX_M = 0.6 # 有效深度上限,单位 m。
|
|
||||||
PICK_DEPTH_MEDIAN_TOLERANCE_M = 0.035 # 深度中位数滤波容差,单位 m。
|
|
||||||
|
|
||||||
# R_tc = np.array(
|
# R_tc = np.array(
|
||||||
# [
|
# [
|
||||||
@ -106,61 +98,6 @@ ui_callback: Callable[[np.ndarray], None] = lambda frame: None
|
|||||||
robot_rpc_client = pyaubo_sdk.RpcClient()
|
robot_rpc_client = pyaubo_sdk.RpcClient()
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_resource_dir() -> Path:
|
|
||||||
"""返回运行时资源目录,兼容 PyInstaller 解包目录。"""
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
return Path(sys._MEIPASS).resolve()
|
|
||||||
return Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_app_dir() -> Path:
|
|
||||||
"""返回应用目录,打包后优先使用可执行文件所在目录。"""
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
return Path(sys.executable).resolve().parent
|
|
||||||
return Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_resource_candidates(filename: str) -> List[Path]:
|
|
||||||
"""返回模型/图片等资源的候选路径。"""
|
|
||||||
resource_dir = get_runtime_resource_dir()
|
|
||||||
app_dir = get_runtime_app_dir()
|
|
||||||
return [
|
|
||||||
resource_dir / filename,
|
|
||||||
resource_dir / "models" / filename,
|
|
||||||
resource_dir / "tools" / filename,
|
|
||||||
app_dir / filename,
|
|
||||||
app_dir / "models" / filename,
|
|
||||||
app_dir / "tools" / filename,
|
|
||||||
Path.cwd() / filename,
|
|
||||||
Path.cwd() / "models" / filename,
|
|
||||||
Path.cwd() / "tools" / filename,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_resource_path(path_value: Optional[str], fallback_name: Optional[str] = None) -> Optional[Path]:
|
|
||||||
"""解析运行时资源路径,兼容源码运行与打包运行。"""
|
|
||||||
if path_value:
|
|
||||||
candidate = Path(path_value).expanduser()
|
|
||||||
if candidate.is_absolute():
|
|
||||||
candidate = candidate.resolve()
|
|
||||||
if candidate.exists():
|
|
||||||
return candidate
|
|
||||||
else:
|
|
||||||
for relative_candidate in (
|
|
||||||
get_runtime_app_dir() / candidate,
|
|
||||||
get_runtime_resource_dir() / candidate,
|
|
||||||
Path.cwd() / candidate,
|
|
||||||
):
|
|
||||||
resolved = relative_candidate.resolve()
|
|
||||||
if resolved.exists():
|
|
||||||
return resolved
|
|
||||||
if fallback_name:
|
|
||||||
for candidate in get_resource_candidates(fallback_name):
|
|
||||||
if candidate.exists():
|
|
||||||
return candidate.resolve()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def configure(config: Dict[str, Any]) -> None:
|
def configure(config: Dict[str, Any]) -> None:
|
||||||
"""接收 control.py 同步过来的可调参数。"""
|
"""接收 control.py 同步过来的可调参数。"""
|
||||||
_runtime_config.update(config)
|
_runtime_config.update(config)
|
||||||
@ -804,7 +741,21 @@ class RobotArmController:
|
|||||||
|
|
||||||
self.close_scissors_for_pick()
|
self.close_scissors_for_pick()
|
||||||
|
|
||||||
retreat_joints = self.move_pose(approach_pose, pickup_joints, description="采摘后返回预抓取点")
|
retreat_reference_joints = pickup_joints
|
||||||
|
if _cfg("CUT_PULL_ASSIST_ENABLED"):
|
||||||
|
pull_pose = pickup_pose.copy()
|
||||||
|
pull_pose[1] += _cfg("CUT_PULL_ASSIST_Y_OFFSET")
|
||||||
|
print(
|
||||||
|
"机械臂:执行剪切后辅助拉拽 "
|
||||||
|
f"Y 偏移 {_cfg('CUT_PULL_ASSIST_Y_OFFSET'):.3f} m"
|
||||||
|
)
|
||||||
|
pull_joints = self.move_pose(pull_pose, pickup_joints, description="剪切后辅助拉拽")
|
||||||
|
if pull_joints is None:
|
||||||
|
self.return_home()
|
||||||
|
return False
|
||||||
|
retreat_reference_joints = pull_joints
|
||||||
|
|
||||||
|
retreat_joints = self.move_pose(approach_pose, retreat_reference_joints, description="采摘后返回预抓取点")
|
||||||
if retreat_joints is None:
|
if retreat_joints is None:
|
||||||
self.return_home()
|
self.return_home()
|
||||||
return False
|
return False
|
||||||
@ -854,9 +805,7 @@ def init_camera() -> Tuple[Any, Any, Any]:
|
|||||||
|
|
||||||
def init_tomato_detector(model_path: Optional[str] = None) -> Any:
|
def init_tomato_detector(model_path: Optional[str] = None) -> Any:
|
||||||
"""初始化 YOLO 模型。"""
|
"""初始化 YOLO 模型。"""
|
||||||
configured_path = model_path or _cfg("YOLO_MODEL_PATH") or "models/best.pt"
|
use_path = model_path or _cfg("YOLO_MODEL_PATH") or "best.pt"
|
||||||
resolved_path = resolve_resource_path(configured_path, fallback_name="best.pt")
|
|
||||||
use_path = str(resolved_path) if resolved_path is not None else configured_path
|
|
||||||
print(f"加载 YOLO 模型 | 路径: {use_path}")
|
print(f"加载 YOLO 模型 | 路径: {use_path}")
|
||||||
|
|
||||||
if not os.path.exists(use_path):
|
if not os.path.exists(use_path):
|
||||||
@ -986,7 +935,13 @@ def calculate_keypoint_line_angle_rad(
|
|||||||
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
|
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
# 果梗是一条无方向直线;endpoint 在剪切点的任一侧都应表示同一倾角。
|
||||||
|
# 归一化后,横向或斜向果梗不会因关键点标注顺序而得到 180°旋转。
|
||||||
angle_rad = math.atan2(dx, dy)
|
angle_rad = math.atan2(dx, dy)
|
||||||
|
if angle_rad > math.pi / 2:
|
||||||
|
angle_rad -= math.pi
|
||||||
|
elif angle_rad <= -math.pi / 2:
|
||||||
|
angle_rad += math.pi
|
||||||
max_angle_rad = math.radians(float(_cfg("MAX_PICK_ANGLE_DEG")))
|
max_angle_rad = math.radians(float(_cfg("MAX_PICK_ANGLE_DEG")))
|
||||||
return max(-max_angle_rad, min(max_angle_rad, angle_rad))
|
return max(-max_angle_rad, min(max_angle_rad, angle_rad))
|
||||||
|
|
||||||
@ -1131,10 +1086,6 @@ class VisionController:
|
|||||||
print(f"Pick candidate skipped: keypoints too close ({distance:.1f}px)")
|
print(f"Pick candidate skipped: keypoints too close ({distance:.1f}px)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if end_point[1] + PICK_ENDPOINT_Y_TOLERANCE_PX < cutpoint[1]:
|
|
||||||
print(f"Pick candidate skipped: endpoint appears above cutpoint @ cut={cutpoint}, end={end_point}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def extract_candidate_keypoints(
|
def extract_candidate_keypoints(
|
||||||
@ -1271,7 +1222,7 @@ class VisionController:
|
|||||||
for sample_y in range(y_start, y_end + 1):
|
for sample_y in range(y_start, y_end + 1):
|
||||||
for sample_x in range(x_start, x_end + 1):
|
for sample_x in range(x_start, x_end + 1):
|
||||||
depth = float(depth_frame.get_distance(sample_x, sample_y))
|
depth = float(depth_frame.get_distance(sample_x, sample_y))
|
||||||
if PICK_DEPTH_MIN_M <= depth <= PICK_DEPTH_MAX_M:
|
if _cfg("PICK_DEPTH_MIN_M") <= depth <= _cfg("PICK_DEPTH_MAX_M"):
|
||||||
samples.append(depth)
|
samples.append(depth)
|
||||||
return samples
|
return samples
|
||||||
|
|
||||||
@ -1283,25 +1234,27 @@ class VisionController:
|
|||||||
frame_shape: Tuple[int, int, int],
|
frame_shape: Tuple[int, int, int],
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
center_depth = float(depth_frame.get_distance(pixel_x, pixel_y))
|
center_depth = float(depth_frame.get_distance(pixel_x, pixel_y))
|
||||||
for radius in (PICK_DEPTH_WINDOW_RADIUS, PICK_DEPTH_FALLBACK_WINDOW_RADIUS):
|
for radius in (_cfg("PICK_DEPTH_WINDOW_RADIUS"), _cfg("PICK_DEPTH_FALLBACK_WINDOW_RADIUS")):
|
||||||
samples = self.collect_depth_samples(depth_frame, pixel_x, pixel_y, frame_shape, radius)
|
samples = self.collect_depth_samples(depth_frame, pixel_x, pixel_y, frame_shape, radius)
|
||||||
if len(samples) < PICK_DEPTH_MIN_VALID_PIXELS:
|
if len(samples) < _cfg("PICK_DEPTH_MIN_VALID_PIXELS"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sample_array = np.array(samples, dtype=float)
|
sample_array = np.array(samples, dtype=float)
|
||||||
median_depth = float(np.median(sample_array))
|
median_depth = float(np.median(sample_array))
|
||||||
stable_samples = sample_array[np.abs(sample_array - median_depth) <= PICK_DEPTH_MEDIAN_TOLERANCE_M]
|
stable_samples = sample_array[
|
||||||
if len(stable_samples) >= PICK_DEPTH_MIN_VALID_PIXELS:
|
np.abs(sample_array - median_depth) <= _cfg("PICK_DEPTH_MEDIAN_TOLERANCE_M")
|
||||||
|
]
|
||||||
|
if len(stable_samples) >= _cfg("PICK_DEPTH_MIN_VALID_PIXELS"):
|
||||||
depth = float(np.median(stable_samples))
|
depth = float(np.median(stable_samples))
|
||||||
else:
|
else:
|
||||||
depth = median_depth
|
depth = median_depth
|
||||||
|
|
||||||
if not (PICK_DEPTH_MIN_M <= center_depth <= PICK_DEPTH_MAX_M):
|
if not (_cfg("PICK_DEPTH_MIN_M") <= center_depth <= _cfg("PICK_DEPTH_MAX_M")):
|
||||||
print(
|
print(
|
||||||
"采摘点中心深度无效,使用邻域稳健深度 "
|
"采摘点中心深度无效,使用邻域稳健深度 "
|
||||||
f"{depth:.3f}m @ ({pixel_x}, {pixel_y}), radius={radius}"
|
f"{depth:.3f}m @ ({pixel_x}, {pixel_y}), radius={radius}"
|
||||||
)
|
)
|
||||||
elif abs(center_depth - depth) > PICK_DEPTH_MEDIAN_TOLERANCE_M:
|
elif abs(center_depth - depth) > _cfg("PICK_DEPTH_MEDIAN_TOLERANCE_M"):
|
||||||
print(
|
print(
|
||||||
"采摘点中心深度跳变,使用邻域稳健深度 "
|
"采摘点中心深度跳变,使用邻域稳健深度 "
|
||||||
f"{depth:.3f}m 替代 {center_depth:.3f}m @ ({pixel_x}, {pixel_y})"
|
f"{depth:.3f}m 替代 {center_depth:.3f}m @ ({pixel_x}, {pixel_y})"
|
||||||
@ -1314,6 +1267,37 @@ class VisionController:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_cutpoint_depth(
|
||||||
|
self,
|
||||||
|
depth_frame: Any,
|
||||||
|
cutpoint: Tuple[int, int],
|
||||||
|
end_point: Optional[Tuple[int, int]],
|
||||||
|
frame_shape: Tuple[int, int, int],
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""获取剪切点深度;细果梗无深度时沿原有关键点连线做有限回退。"""
|
||||||
|
depth = self.get_robust_depth(depth_frame, cutpoint[0], cutpoint[1], frame_shape)
|
||||||
|
if depth is not None:
|
||||||
|
return depth
|
||||||
|
if not _cfg("PICK_DEPTH_LINE_FALLBACK_ENABLED") or end_point is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
direction = np.array(end_point, dtype=float) - np.array(cutpoint, dtype=float)
|
||||||
|
norm = float(np.linalg.norm(direction))
|
||||||
|
if norm < 1e-6:
|
||||||
|
return None
|
||||||
|
direction /= norm
|
||||||
|
for step in range(1, int(_cfg("PICK_DEPTH_LINE_FALLBACK_STEPS")) + 1):
|
||||||
|
offset = direction * float(_cfg("PICK_DEPTH_LINE_FALLBACK_STEP_PX")) * step
|
||||||
|
sample_x, sample_y = self.clamp_pixel(cutpoint[0] + offset[0], cutpoint[1] + offset[1], frame_shape)
|
||||||
|
depth = self.get_robust_depth(depth_frame, sample_x, sample_y, frame_shape)
|
||||||
|
if depth is not None:
|
||||||
|
print(
|
||||||
|
"剪切点无有效深度,使用果梗方向邻近深度 "
|
||||||
|
f"{depth:.3f}m @ ({sample_x}, {sample_y}) 估计剪切点"
|
||||||
|
)
|
||||||
|
return depth
|
||||||
|
return None
|
||||||
|
|
||||||
def build_detected_tomato(
|
def build_detected_tomato(
|
||||||
self,
|
self,
|
||||||
candidate: TomatoCandidate,
|
candidate: TomatoCandidate,
|
||||||
@ -1329,7 +1313,7 @@ class VisionController:
|
|||||||
return None
|
return None
|
||||||
pixel_x, pixel_y = candidate.cutpoint
|
pixel_x, pixel_y = candidate.cutpoint
|
||||||
|
|
||||||
depth = self.get_robust_depth(depth_frame, pixel_x, pixel_y, frame.shape)
|
depth = self.get_cutpoint_depth(depth_frame, candidate.cutpoint, candidate.end_point, frame.shape)
|
||||||
if depth is None:
|
if depth is None:
|
||||||
print(f"无效深度值 @ ({pixel_x}, {pixel_y})")
|
print(f"无效深度值 @ ({pixel_x}, {pixel_y})")
|
||||||
return None
|
return None
|
||||||
|
|||||||
418
main.py
418
main.py
@ -8,7 +8,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
import math
|
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -34,7 +33,6 @@ import contextlib
|
|||||||
# Windows 控制台缓冲区结构体。
|
# Windows 控制台缓冲区结构体。
|
||||||
# 当程序在 Windows 上运行时,会用它读取后台线程输出的控制台内容,
|
# 当程序在 Windows 上运行时,会用它读取后台线程输出的控制台内容,
|
||||||
# 再把这些内容转发到 Tkinter 的日志面板中。
|
# 再把这些内容转发到 Tkinter 的日志面板中。
|
||||||
if sys.platform == "win32":
|
|
||||||
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("dwSize", wintypes._COORD),
|
("dwSize", wintypes._COORD),
|
||||||
@ -46,28 +44,9 @@ if sys.platform == "win32":
|
|||||||
|
|
||||||
STD_OUTPUT_HANDLE = -11
|
STD_OUTPUT_HANDLE = -11
|
||||||
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||||
else:
|
|
||||||
CONSOLE_SCREEN_BUFFER_INFO = None
|
|
||||||
STD_OUTPUT_HANDLE = None
|
|
||||||
kernel32 = None
|
|
||||||
|
|
||||||
MAX_LOG_LINES = 1000
|
MAX_LOG_LINES = 1000
|
||||||
MAX_TERMINAL_LINES = 1500
|
MAX_TERMINAL_LINES = 1500
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_resource_dir():
|
|
||||||
"""返回运行时资源目录,兼容 PyInstaller 临时解包目录。"""
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
return Path(sys._MEIPASS).resolve()
|
|
||||||
return Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_app_dir():
|
|
||||||
"""返回应用写入目录,打包后优先使用 exe 所在目录。"""
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
return Path(sys.executable).resolve().parent
|
|
||||||
return Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
class TomatoHarvestingUI:
|
class TomatoHarvestingUI:
|
||||||
"""番茄采摘系统的主界面类。"""
|
"""番茄采摘系统的主界面类。"""
|
||||||
|
|
||||||
@ -84,39 +63,31 @@ class TomatoHarvestingUI:
|
|||||||
self.root.minsize(max(980, int(screen_w * 0.5)), max(640, int(screen_h * 0.55)))
|
self.root.minsize(max(980, int(screen_w * 0.5)), max(640, int(screen_h * 0.55)))
|
||||||
self.base_window_width = window_w
|
self.base_window_width = window_w
|
||||||
self.base_window_height = window_h
|
self.base_window_height = window_h
|
||||||
self.base_dir = get_runtime_resource_dir()
|
self.base_dir = Path(__file__).resolve().parent
|
||||||
self.app_dir = get_runtime_app_dir()
|
self.settings_file = self.base_dir / "ui_settings.json"
|
||||||
self.settings_file = self.app_dir / "ui_settings.json"
|
|
||||||
self._font_resize_job = None
|
self._font_resize_job = None
|
||||||
self._is_applying_font_scale = False
|
self._is_applying_font_scale = False
|
||||||
self.colors = {
|
self.colors = {
|
||||||
"bg": "#FFF4EC",
|
"bg": "#F3F7FB",
|
||||||
"bg_deep": "#F7E0D0",
|
"panel": "#FFFFFF",
|
||||||
"panel": "#FFFDFB",
|
"panel_soft": "#F8FBFD",
|
||||||
"panel_soft": "#FFF2E8",
|
"border": "#D8E3EC",
|
||||||
"panel_alt": "#FBE4D7",
|
"text": "#16324A",
|
||||||
"border": "#E8C9B8",
|
"muted": "#6B7C93",
|
||||||
"text": "#452315",
|
"accent": "#0F766E",
|
||||||
"muted": "#8A5E50",
|
"accent_soft": "#D7F3EE",
|
||||||
"accent": "#D84A32",
|
"danger": "#B45309",
|
||||||
"accent_deep": "#A92F22",
|
"danger_soft": "#FDE7D7",
|
||||||
"accent_soft": "#FFD9CF",
|
"info": "#0F4C81",
|
||||||
"accent_gold": "#F4B860",
|
"info_soft": "#DCEBFA",
|
||||||
"leaf": "#3F8C53",
|
"terminal_bg": "#0F172A",
|
||||||
"leaf_soft": "#D8F0D8",
|
"terminal_fg": "#DCFCE7",
|
||||||
"danger": "#A63E2A",
|
"terminal_muted": "#93C5FD",
|
||||||
"danger_soft": "#FFE0D6",
|
"terminal_err": "#FCA5A5",
|
||||||
"info": "#8F3B2E",
|
"metric_1": "#E6FFFB",
|
||||||
"info_soft": "#FFE7D9",
|
"metric_2": "#EEF2FF",
|
||||||
"terminal_bg": "#2A1410",
|
"metric_3": "#FFF7ED",
|
||||||
"terminal_fg": "#FFE8C9",
|
"metric_4": "#F0FDF4",
|
||||||
"terminal_muted": "#FFB66E",
|
|
||||||
"terminal_err": "#FF9B8F",
|
|
||||||
"metric_1": "#FFE4D8",
|
|
||||||
"metric_2": "#FFF1CF",
|
|
||||||
"metric_3": "#E7F6DF",
|
|
||||||
"metric_4": "#FDE6EF",
|
|
||||||
"shadow": "#EFD6C7",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 设置中文字体
|
# 设置中文字体
|
||||||
@ -145,13 +116,6 @@ class TomatoHarvestingUI:
|
|||||||
self.camera_image = None
|
self.camera_image = None
|
||||||
self.camera_bg_image = None # 保存背景图引用(关键:防止回收)
|
self.camera_bg_image = None # 保存背景图引用(关键:防止回收)
|
||||||
self.raw_bg_image = None # 新增:保存原始1.png,用于后续尺寸调整
|
self.raw_bg_image = None # 新增:保存原始1.png,用于后续尺寸调整
|
||||||
self.startup_window = None
|
|
||||||
self.startup_overlay = None
|
|
||||||
self.startup_animation_started = False
|
|
||||||
self.startup_transparent_ready = False
|
|
||||||
self.startup_bg_color = self.colors["bg"]
|
|
||||||
|
|
||||||
self.root.withdraw()
|
|
||||||
|
|
||||||
# 创建界面
|
# 创建界面
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
@ -164,7 +128,6 @@ class TomatoHarvestingUI:
|
|||||||
|
|
||||||
# 启动日志处理线程
|
# 启动日志处理线程
|
||||||
self.root.after(200, self.process_log_queue)
|
self.root.after(200, self.process_log_queue)
|
||||||
self.root.after(20, self.play_startup_animation)
|
|
||||||
|
|
||||||
# 信号处理
|
# 信号处理
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
@ -175,7 +138,7 @@ class TomatoHarvestingUI:
|
|||||||
# 设置主窗口背景
|
# 设置主窗口背景
|
||||||
self.set_background_image()
|
self.set_background_image()
|
||||||
|
|
||||||
main_frame = ttk.Frame(self.root, padding=(18, 16, 18, 18), style="App.TFrame")
|
main_frame = ttk.Frame(self.root, padding=(16, 12, 16, 16), style="App.TFrame")
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
header_card = tk.Frame(
|
header_card = tk.Frame(
|
||||||
@ -185,26 +148,16 @@ class TomatoHarvestingUI:
|
|||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground=self.colors["border"]
|
highlightbackground=self.colors["border"]
|
||||||
)
|
)
|
||||||
header_card.pack(fill=tk.X, pady=(0, 12))
|
header_card.pack(fill=tk.X, pady=(0, 10))
|
||||||
header_top = tk.Frame(header_card, bg=self.colors["panel"])
|
header_top = tk.Frame(header_card, bg=self.colors["panel"])
|
||||||
header_top.pack(fill=tk.X, padx=20, pady=(16, 10))
|
header_top.pack(fill=tk.X, padx=18, pady=10)
|
||||||
hero_badge = tk.Label(
|
|
||||||
header_top,
|
|
||||||
text="TomatoPick Studio",
|
|
||||||
font=self.fonts["chip"],
|
|
||||||
bg=self.colors["leaf_soft"],
|
|
||||||
fg=self.colors["leaf"],
|
|
||||||
padx=12,
|
|
||||||
pady=6
|
|
||||||
)
|
|
||||||
hero_badge.pack(side=tk.LEFT, padx=(0, 10))
|
|
||||||
self.status_badge = tk.Label(
|
self.status_badge = tk.Label(
|
||||||
header_top,
|
header_top,
|
||||||
text="系统就绪",
|
text="系统就绪",
|
||||||
font=self.fonts["button"],
|
font=self.fonts["button"],
|
||||||
bg=self.colors["accent_soft"],
|
bg=self.colors["accent_soft"],
|
||||||
fg=self.colors["accent_deep"],
|
fg=self.colors["accent"],
|
||||||
padx=16,
|
padx=14,
|
||||||
pady=6
|
pady=6
|
||||||
)
|
)
|
||||||
self.status_badge.pack(side=tk.RIGHT)
|
self.status_badge.pack(side=tk.RIGHT)
|
||||||
@ -212,7 +165,7 @@ class TomatoHarvestingUI:
|
|||||||
title_wrap.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
title_wrap.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
self.title_label = tk.Label(
|
self.title_label = tk.Label(
|
||||||
title_wrap,
|
title_wrap,
|
||||||
text="番茄采摘控制台",
|
text="番茄采摘系统",
|
||||||
font=self.fonts["title"],
|
font=self.fonts["title"],
|
||||||
bg=self.colors["panel"],
|
bg=self.colors["panel"],
|
||||||
fg=self.colors["text"]
|
fg=self.colors["text"]
|
||||||
@ -220,27 +173,12 @@ class TomatoHarvestingUI:
|
|||||||
self.title_label.pack(side=tk.LEFT)
|
self.title_label.pack(side=tk.LEFT)
|
||||||
self.subtitle_label = tk.Label(
|
self.subtitle_label = tk.Label(
|
||||||
title_wrap,
|
title_wrap,
|
||||||
text="参数配置 · 视觉联调 · 设备控制 · 终端回显",
|
text="参数配置 · 日志 · 相机 · 终端",
|
||||||
font=self.fonts["subtitle"],
|
font=self.fonts["subtitle"],
|
||||||
bg=self.colors["panel"],
|
bg=self.colors["panel"],
|
||||||
fg=self.colors["muted"]
|
fg=self.colors["muted"]
|
||||||
)
|
)
|
||||||
self.subtitle_label.pack(side=tk.LEFT, padx=(14, 0), pady=(4, 0))
|
self.subtitle_label.pack(side=tk.LEFT, padx=(14, 0), pady=(4, 0))
|
||||||
header_bottom = tk.Frame(header_card, bg=self.colors["panel"])
|
|
||||||
header_bottom.pack(fill=tk.X, padx=20, pady=(0, 16))
|
|
||||||
self.create_info_chip(header_bottom, "视觉模式", "YOLO + RealSense", self.colors["metric_1"], self.colors["accent_deep"]).pack(side=tk.LEFT, padx=(0, 8))
|
|
||||||
self.create_info_chip(header_bottom, "运行姿态", "机械臂 / AGV 联动", self.colors["metric_2"], "#9A5417").pack(side=tk.LEFT, padx=8)
|
|
||||||
self.create_info_chip(header_bottom, "当前风格", "卡通番茄主题", self.colors["metric_3"], self.colors["leaf"]).pack(side=tk.LEFT, padx=8)
|
|
||||||
self.header_decoration = tk.Canvas(
|
|
||||||
header_bottom,
|
|
||||||
height=54,
|
|
||||||
width=220,
|
|
||||||
bd=0,
|
|
||||||
highlightthickness=0,
|
|
||||||
bg=self.colors["panel"]
|
|
||||||
)
|
|
||||||
self.header_decoration.pack(side=tk.RIGHT)
|
|
||||||
self.draw_header_decoration()
|
|
||||||
|
|
||||||
body_frame = ttk.Frame(main_frame, style="App.TFrame")
|
body_frame = ttk.Frame(main_frame, style="App.TFrame")
|
||||||
body_frame.pack(fill=tk.BOTH, expand=True)
|
body_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
@ -417,7 +355,7 @@ class TomatoHarvestingUI:
|
|||||||
# 日志文本框
|
# 日志文本框
|
||||||
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=4, bd=0, relief="flat", highlightthickness=0)
|
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=4, bd=0, relief="flat", highlightthickness=0)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True, pady=(0, 2))
|
self.log_text.pack(fill=tk.BOTH, expand=True, pady=(0, 2))
|
||||||
self.log_text.config(state=tk.DISABLED, bg="#FFF7F2", fg=self.colors["text"], insertbackground=self.colors["text"])
|
self.log_text.config(state=tk.DISABLED, bg="#F0F0F0", fg="#333333", insertbackground="black")
|
||||||
|
|
||||||
# 相机画面区域(下方)
|
# 相机画面区域(下方)
|
||||||
camera_card = ttk.LabelFrame(center_frame, text="相机画面", padding="8", style="Card.TLabelframe")
|
camera_card = ttk.LabelFrame(center_frame, text="相机画面", padding="8", style="Card.TLabelframe")
|
||||||
@ -446,9 +384,9 @@ class TomatoHarvestingUI:
|
|||||||
bd=0,
|
bd=0,
|
||||||
relief="flat",
|
relief="flat",
|
||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
bg=self.colors["terminal_bg"],
|
bg="#111111",
|
||||||
fg=self.colors["terminal_fg"],
|
fg="#D8F3DC",
|
||||||
insertbackground=self.colors["terminal_fg"]
|
insertbackground="#D8F3DC"
|
||||||
)
|
)
|
||||||
self.terminal_text.pack(fill=tk.BOTH, expand=True, pady=5)
|
self.terminal_text.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
self.terminal_text.config(state=tk.DISABLED)
|
self.terminal_text.config(state=tk.DISABLED)
|
||||||
@ -462,28 +400,23 @@ class TomatoHarvestingUI:
|
|||||||
|
|
||||||
def init_fonts(self):
|
def init_fonts(self):
|
||||||
"""创建一组可随窗口缩放的字体对象。"""
|
"""创建一组可随窗口缩放的字体对象。"""
|
||||||
primary_family = self.pick_font_family(["Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Sans"])
|
|
||||||
title_family = self.pick_font_family(["Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Sans"])
|
|
||||||
mono_family = self.pick_font_family(["JetBrains Mono", "Consolas", "DejaVu Sans Mono", primary_family])
|
|
||||||
self.font_specs = {
|
self.font_specs = {
|
||||||
"default": {"family": primary_family, "size": 10},
|
"default": {"family": "微软雅黑", "size": 9},
|
||||||
"title": {"family": title_family, "size": 20, "weight": "bold"},
|
"title": {"family": "微软雅黑", "size": 18, "weight": "bold"},
|
||||||
"subtitle": {"family": primary_family, "size": 10},
|
"subtitle": {"family": "微软雅黑", "size": 10},
|
||||||
"section": {"family": primary_family, "size": 10, "weight": "bold"},
|
"section": {"family": "微软雅黑", "size": 9, "weight": "bold"},
|
||||||
"button": {"family": primary_family, "size": 10, "weight": "bold"},
|
"button": {"family": "微软雅黑", "size": 9, "weight": "bold"},
|
||||||
"button_normal": {"family": primary_family, "size": 10},
|
"button_normal": {"family": "微软雅黑", "size": 9},
|
||||||
"mono": {"family": mono_family, "size": 9},
|
"mono": {"family": "微软雅黑", "size": 9},
|
||||||
"chip": {"family": primary_family, "size": 9, "weight": "bold"},
|
|
||||||
}
|
}
|
||||||
self.font_min_sizes = {
|
self.font_min_sizes = {
|
||||||
"default": 8,
|
"default": 7,
|
||||||
"title": 15,
|
"title": 14,
|
||||||
"subtitle": 8,
|
"subtitle": 8,
|
||||||
"section": 8,
|
"section": 8,
|
||||||
"button": 8,
|
"button": 8,
|
||||||
"button_normal": 8,
|
"button_normal": 8,
|
||||||
"mono": 8,
|
"mono": 8,
|
||||||
"chip": 8,
|
|
||||||
}
|
}
|
||||||
self.fonts = {}
|
self.fonts = {}
|
||||||
for name, spec in self.font_specs.items():
|
for name, spec in self.font_specs.items():
|
||||||
@ -496,14 +429,6 @@ class TomatoHarvestingUI:
|
|||||||
self.root.option_add("*Font", self.fonts["default"])
|
self.root.option_add("*Font", self.fonts["default"])
|
||||||
self.root.bind("<Configure>", self.on_root_resize)
|
self.root.bind("<Configure>", self.on_root_resize)
|
||||||
|
|
||||||
def pick_font_family(self, candidates):
|
|
||||||
"""在当前系统中优先选择更适合中文界面的字体。"""
|
|
||||||
available = set(tkfont.families(self.root))
|
|
||||||
for family in candidates:
|
|
||||||
if family in available:
|
|
||||||
return family
|
|
||||||
return "TkDefaultFont"
|
|
||||||
|
|
||||||
def setup_styles(self):
|
def setup_styles(self):
|
||||||
"""集中配置 ttk 控件样式,避免样式定义散落在各个方法中。"""
|
"""集中配置 ttk 控件样式,避免样式定义散落在各个方法中。"""
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
@ -517,47 +442,33 @@ class TomatoHarvestingUI:
|
|||||||
style.configure("App.TFrame", background=self.colors["bg"])
|
style.configure("App.TFrame", background=self.colors["bg"])
|
||||||
style.configure("Transparent.TFrame", background=self.colors["bg"])
|
style.configure("Transparent.TFrame", background=self.colors["bg"])
|
||||||
style.configure("TPanedwindow", background=self.colors["bg"])
|
style.configure("TPanedwindow", background=self.colors["bg"])
|
||||||
style.configure("TPanedwindow.Sash", background=self.colors["bg_deep"], sashthickness=10)
|
|
||||||
style.configure("TLabel", background=self.colors["panel"], foreground=self.colors["text"], font=self.fonts["default"])
|
style.configure("TLabel", background=self.colors["panel"], foreground=self.colors["text"], font=self.fonts["default"])
|
||||||
style.configure(
|
style.configure(
|
||||||
"TEntry",
|
"TEntry",
|
||||||
fieldbackground=self.colors["panel"],
|
fieldbackground=self.colors["panel"],
|
||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
foreground=self.colors["text"],
|
foreground=self.colors["text"],
|
||||||
padding=7,
|
padding=5,
|
||||||
font=self.fonts["default"],
|
font=self.fonts["default"],
|
||||||
relief="flat",
|
relief="flat",
|
||||||
borderwidth=1,
|
borderwidth=1
|
||||||
insertcolor=self.colors["accent_deep"]
|
|
||||||
)
|
)
|
||||||
style.map("TEntry", bordercolor=[("focus", self.colors["accent"]), ("!focus", self.colors["border"])])
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"TCombobox",
|
"TCombobox",
|
||||||
fieldbackground=self.colors["panel"],
|
fieldbackground=self.colors["panel"],
|
||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
foreground=self.colors["text"],
|
foreground=self.colors["text"],
|
||||||
arrowsize=14,
|
arrowsize=14,
|
||||||
padding=4,
|
padding=3,
|
||||||
font=self.fonts["default"]
|
font=self.fonts["default"]
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"TButton",
|
"TButton",
|
||||||
font=self.fonts["button_normal"],
|
font=self.fonts["button_normal"],
|
||||||
padding=(12, 8),
|
padding=(10, 6),
|
||||||
relief="flat",
|
relief="flat",
|
||||||
borderwidth=0
|
borderwidth=0
|
||||||
)
|
)
|
||||||
style.configure(
|
|
||||||
"TCheckbutton",
|
|
||||||
background=self.colors["panel"],
|
|
||||||
foreground=self.colors["text"],
|
|
||||||
font=self.fonts["default"]
|
|
||||||
)
|
|
||||||
style.map(
|
|
||||||
"TCheckbutton",
|
|
||||||
background=[("active", self.colors["panel_soft"])],
|
|
||||||
foreground=[("active", self.colors["accent_deep"])]
|
|
||||||
)
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"SidebarShell.TLabelframe",
|
"SidebarShell.TLabelframe",
|
||||||
background=self.colors["panel_soft"],
|
background=self.colors["panel_soft"],
|
||||||
@ -576,12 +487,12 @@ class TomatoHarvestingUI:
|
|||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
borderwidth=1,
|
borderwidth=1,
|
||||||
relief="solid",
|
relief="solid",
|
||||||
padding=8
|
padding=6
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"SidebarCard.TLabelframe.Label",
|
"SidebarCard.TLabelframe.Label",
|
||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
foreground=self.colors["accent_deep"],
|
foreground=self.colors["text"],
|
||||||
font=self.fonts["section"]
|
font=self.fonts["section"]
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
@ -589,33 +500,33 @@ class TomatoHarvestingUI:
|
|||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
borderwidth=1,
|
borderwidth=1,
|
||||||
relief="solid",
|
relief="solid",
|
||||||
padding=8
|
padding=6
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"Card.TLabelframe.Label",
|
"Card.TLabelframe.Label",
|
||||||
background=self.colors["panel"],
|
background=self.colors["panel"],
|
||||||
foreground=self.colors["accent_deep"],
|
foreground=self.colors["text"],
|
||||||
font=self.fonts["section"]
|
font=self.fonts["section"]
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"TerminalCard.TLabelframe",
|
"TerminalCard.TLabelframe",
|
||||||
background=self.colors["panel_alt"],
|
background=self.colors["panel"],
|
||||||
borderwidth=1,
|
borderwidth=1,
|
||||||
relief="solid",
|
relief="solid",
|
||||||
padding=8
|
padding=6
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"TerminalCard.TLabelframe.Label",
|
"TerminalCard.TLabelframe.Label",
|
||||||
background=self.colors["panel_alt"],
|
background=self.colors["panel"],
|
||||||
foreground=self.colors["accent_deep"],
|
foreground=self.colors["text"],
|
||||||
font=self.fonts["section"]
|
font=self.fonts["section"]
|
||||||
)
|
)
|
||||||
style.configure("Primary.TButton", background=self.colors["accent"], foreground="#FFF8F4", font=self.fonts["button"])
|
style.configure("Primary.TButton", background=self.colors["accent"], foreground="#FFFFFF", font=self.fonts["button"])
|
||||||
style.map("Primary.TButton", background=[("active", self.colors["accent_deep"]), ("disabled", "#F1B5AA")], foreground=[("disabled", "#FFF6F2")])
|
style.map("Primary.TButton", background=[("active", "#0B5E58"), ("disabled", "#A8D7D2")], foreground=[("disabled", "#F7FAFC")])
|
||||||
style.configure("Danger.TButton", background="#B84531", foreground="#FFF8F4", font=self.fonts["button"])
|
style.configure("Danger.TButton", background="#C2410C", foreground="#FFFFFF", font=self.fonts["button"])
|
||||||
style.map("Danger.TButton", background=[("active", "#8D3022"), ("disabled", "#EDB9AE")], foreground=[("disabled", "#FEF2F2")])
|
style.map("Danger.TButton", background=[("active", "#9A3412"), ("disabled", "#F3B99A")], foreground=[("disabled", "#FEF2F2")])
|
||||||
style.configure("Secondary.TButton", background=self.colors["info_soft"], foreground=self.colors["info"], font=self.fonts["button_normal"])
|
style.configure("Secondary.TButton", background=self.colors["info_soft"], foreground=self.colors["info"], font=self.fonts["button_normal"])
|
||||||
style.map("Secondary.TButton", background=[("active", self.colors["accent_gold"]), ("disabled", "#F5E6DA")], foreground=[("active", self.colors["text"]), ("disabled", self.colors["muted"])])
|
style.map("Secondary.TButton", background=[("active", "#C8DFF7"), ("disabled", "#E7EEF5")])
|
||||||
|
|
||||||
def on_root_resize(self, event):
|
def on_root_resize(self, event):
|
||||||
"""窗口尺寸变化时,延迟刷新字体缩放。"""
|
"""窗口尺寸变化时,延迟刷新字体缩放。"""
|
||||||
@ -645,41 +556,6 @@ class TomatoHarvestingUI:
|
|||||||
finally:
|
finally:
|
||||||
self._is_applying_font_scale = False
|
self._is_applying_font_scale = False
|
||||||
|
|
||||||
def create_info_chip(self, parent, title, value, bg_color, fg_color):
|
|
||||||
"""创建头部信息胶囊,用于提升首页层次感。"""
|
|
||||||
chip = tk.Frame(parent, bg=bg_color, bd=0, highlightthickness=0)
|
|
||||||
tk.Label(
|
|
||||||
chip,
|
|
||||||
text=title,
|
|
||||||
font=self.fonts["chip"],
|
|
||||||
bg=bg_color,
|
|
||||||
fg=fg_color,
|
|
||||||
).pack(anchor=tk.W, padx=12, pady=(8, 0))
|
|
||||||
tk.Label(
|
|
||||||
chip,
|
|
||||||
text=value,
|
|
||||||
font=self.fonts["default"],
|
|
||||||
bg=bg_color,
|
|
||||||
fg=self.colors["text"],
|
|
||||||
).pack(anchor=tk.W, padx=12, pady=(0, 8))
|
|
||||||
return chip
|
|
||||||
|
|
||||||
def draw_header_decoration(self):
|
|
||||||
"""绘制头部右侧的番茄主题装饰。"""
|
|
||||||
if not hasattr(self, "header_decoration"):
|
|
||||||
return
|
|
||||||
canvas = self.header_decoration
|
|
||||||
canvas.delete("all")
|
|
||||||
canvas.create_oval(10, 16, 38, 44, fill="#FFD9CF", outline="")
|
|
||||||
canvas.create_oval(48, 6, 102, 52, fill=self.colors["accent"], outline="")
|
|
||||||
canvas.create_polygon(67, 12, 74, 0, 81, 12, 90, 4, 86, 18, 70, 18, fill=self.colors["leaf"], outline="")
|
|
||||||
canvas.create_oval(63, 22, 68, 28, fill=self.colors["text"], outline="")
|
|
||||||
canvas.create_oval(80, 22, 85, 28, fill=self.colors["text"], outline="")
|
|
||||||
canvas.create_arc(67, 24, 82, 36, start=200, extent=140, style=tk.ARC, outline="#FFF8F4", width=2)
|
|
||||||
canvas.create_oval(112, 20, 142, 50, fill="#FFE6B4", outline="")
|
|
||||||
canvas.create_oval(150, 10, 208, 46, fill="#E3F5DA", outline="")
|
|
||||||
canvas.create_text(180, 28, text="Fresh", fill=self.colors["leaf"], font=self.fonts["chip"])
|
|
||||||
|
|
||||||
def set_status_badge(self, text, tone="idle"):
|
def set_status_badge(self, text, tone="idle"):
|
||||||
"""更新顶部状态徽标。"""
|
"""更新顶部状态徽标。"""
|
||||||
tone_map = {
|
tone_map = {
|
||||||
@ -719,168 +595,12 @@ class TomatoHarvestingUI:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def play_startup_animation(self):
|
|
||||||
"""播放一个轻量级的卡通番茄启动动画。"""
|
|
||||||
if self.startup_animation_started or self.startup_overlay is not None:
|
|
||||||
return
|
|
||||||
self.startup_animation_started = True
|
|
||||||
screen_w = self.root.winfo_screenwidth()
|
|
||||||
screen_h = self.root.winfo_screenheight()
|
|
||||||
width = min(560, max(420, int(screen_w * 0.34)))
|
|
||||||
height = min(440, max(320, int(screen_h * 0.34)))
|
|
||||||
pos_x = max(0, (screen_w - width) // 2)
|
|
||||||
pos_y = max(0, (screen_h - height) // 2)
|
|
||||||
transparent_key = "#010203"
|
|
||||||
|
|
||||||
splash = tk.Toplevel(self.root)
|
|
||||||
splash.overrideredirect(True)
|
|
||||||
splash.geometry(f"{width}x{height}+{pos_x}+{pos_y}")
|
|
||||||
splash.configure(bg=transparent_key)
|
|
||||||
try:
|
|
||||||
splash.wm_attributes("-topmost", True)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
splash.wm_attributes("-transparentcolor", transparent_key)
|
|
||||||
self.startup_transparent_ready = True
|
|
||||||
self.startup_bg_color = transparent_key
|
|
||||||
except tk.TclError:
|
|
||||||
self.startup_transparent_ready = False
|
|
||||||
self.startup_bg_color = self.colors["bg"]
|
|
||||||
splash.configure(bg=self.startup_bg_color)
|
|
||||||
try:
|
|
||||||
splash.wm_attributes("-alpha", 0.96)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
overlay = tk.Canvas(
|
|
||||||
splash,
|
|
||||||
width=width,
|
|
||||||
height=height,
|
|
||||||
bg=self.startup_bg_color,
|
|
||||||
bd=0,
|
|
||||||
highlightthickness=0,
|
|
||||||
)
|
|
||||||
overlay.place(x=0, y=0, relwidth=1, relheight=1)
|
|
||||||
self.startup_window = splash
|
|
||||||
self.startup_overlay = overlay
|
|
||||||
self._animate_tomato_frame(frame_index=0, total_frames=44)
|
|
||||||
|
|
||||||
def _animate_tomato_frame(self, frame_index, total_frames):
|
|
||||||
"""逐帧绘制番茄跳出动画。"""
|
|
||||||
canvas = self.startup_overlay
|
|
||||||
if canvas is None:
|
|
||||||
return
|
|
||||||
width = max(canvas.winfo_width(), 960)
|
|
||||||
height = max(canvas.winfo_height(), 640)
|
|
||||||
progress = frame_index / max(total_frames - 1, 1)
|
|
||||||
center_x = width * 0.5
|
|
||||||
base_y = height * 0.58
|
|
||||||
rise = math.sin(progress * math.pi) * 110
|
|
||||||
bounce = abs(math.sin(progress * math.pi * 2.2))
|
|
||||||
body_scale_x = 1.0 + (0.12 if progress < 0.18 else 0.0) - bounce * 0.05
|
|
||||||
body_scale_y = 1.0 - (0.16 if progress < 0.18 else 0.0) + bounce * 0.08
|
|
||||||
eye_offset = math.sin(progress * math.pi * 4) * 1.2
|
|
||||||
|
|
||||||
canvas.delete("all")
|
|
||||||
if not self.startup_transparent_ready:
|
|
||||||
canvas.create_oval(-40, height * 0.48, width + 40, height + 120, fill="#FFF1E6", outline="")
|
|
||||||
canvas.create_oval(center_x - 150, 20, center_x + 150, 220, fill="#FFE7DA", outline="")
|
|
||||||
canvas.create_text(
|
|
||||||
center_x,
|
|
||||||
height * 0.20,
|
|
||||||
text="TomatoPick",
|
|
||||||
fill=self.colors["accent_deep"],
|
|
||||||
font=self.fonts["title"],
|
|
||||||
)
|
|
||||||
canvas.create_text(
|
|
||||||
center_x,
|
|
||||||
height * 0.29,
|
|
||||||
text="准备进入番茄采摘控制台",
|
|
||||||
fill=self.colors["muted"],
|
|
||||||
font=self.fonts["subtitle"],
|
|
||||||
)
|
|
||||||
|
|
||||||
y = base_y - rise
|
|
||||||
shadow_w = 120 - rise * 0.22
|
|
||||||
canvas.create_oval(
|
|
||||||
center_x - shadow_w,
|
|
||||||
base_y + 88,
|
|
||||||
center_x + shadow_w,
|
|
||||||
base_y + 112,
|
|
||||||
fill="#E6B9A7",
|
|
||||||
outline="",
|
|
||||||
)
|
|
||||||
self.draw_cartoon_tomato(canvas, center_x, y, 118 * body_scale_x, 112 * body_scale_y, eye_offset)
|
|
||||||
|
|
||||||
if progress > 0.55:
|
|
||||||
info_alpha_color = self.colors["leaf"] if progress > 0.72 else self.colors["muted"]
|
|
||||||
canvas.create_text(
|
|
||||||
center_x,
|
|
||||||
y + 126,
|
|
||||||
text="视觉 ready · 日志 ready · 参数 ready",
|
|
||||||
fill=info_alpha_color,
|
|
||||||
font=self.fonts["default"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if frame_index + 1 < total_frames:
|
|
||||||
self.root.after(28, lambda: self._animate_tomato_frame(frame_index + 1, total_frames))
|
|
||||||
else:
|
|
||||||
self.root.after(220, self.finish_startup_animation)
|
|
||||||
|
|
||||||
def draw_cartoon_tomato(self, canvas, center_x, center_y, body_w, body_h, eye_offset):
|
|
||||||
"""在 Canvas 上绘制卡通番茄。"""
|
|
||||||
left = center_x - body_w / 2
|
|
||||||
top = center_y - body_h / 2
|
|
||||||
right = center_x + body_w / 2
|
|
||||||
bottom = center_y + body_h / 2
|
|
||||||
canvas.create_oval(left + 8, top + 10, right + 8, bottom + 14, fill="#D9A18F", outline="")
|
|
||||||
canvas.create_oval(left, top, right, bottom, fill=self.colors["accent"], outline="")
|
|
||||||
canvas.create_oval(left + 16, top + 14, center_x - 6, center_y + 12, fill="#FF8B72", outline="")
|
|
||||||
canvas.create_oval(left + 20, top + 18, left + 42, top + 42, fill="#FFEDE7", outline="")
|
|
||||||
canvas.create_polygon(
|
|
||||||
center_x - 10, top - 6,
|
|
||||||
center_x, top - 24,
|
|
||||||
center_x + 10, top - 6,
|
|
||||||
center_x + 22, top - 20,
|
|
||||||
center_x + 16, top + 2,
|
|
||||||
center_x - 16, top + 2,
|
|
||||||
center_x - 22, top - 20,
|
|
||||||
fill=self.colors["leaf"],
|
|
||||||
outline="",
|
|
||||||
)
|
|
||||||
canvas.create_line(center_x, top - 4, center_x + 6, top - 26, fill="#7B512F", width=3, smooth=True)
|
|
||||||
canvas.create_oval(center_x - 24, center_y - 8 + eye_offset, center_x - 14, center_y + 4 + eye_offset, fill=self.colors["text"], outline="")
|
|
||||||
canvas.create_oval(center_x + 14, center_y - 8 - eye_offset, center_x + 24, center_y + 4 - eye_offset, fill=self.colors["text"], outline="")
|
|
||||||
canvas.create_oval(center_x - 21, center_y - 5 + eye_offset, center_x - 18, center_y - 2 + eye_offset, fill="#FFF8F4", outline="")
|
|
||||||
canvas.create_oval(center_x + 17, center_y - 5 - eye_offset, center_x + 20, center_y - 2 - eye_offset, fill="#FFF8F4", outline="")
|
|
||||||
canvas.create_arc(center_x - 18, center_y + 4, center_x + 18, center_y + 28, start=200, extent=140, style=tk.ARC, outline="#FFF8F4", width=3)
|
|
||||||
canvas.create_oval(center_x - 42, center_y + 14, center_x - 26, center_y + 24, fill="#F48B8E", outline="")
|
|
||||||
canvas.create_oval(center_x + 26, center_y + 14, center_x + 42, center_y + 24, fill="#F48B8E", outline="")
|
|
||||||
|
|
||||||
def finish_startup_animation(self):
|
|
||||||
"""移除启动动画遮罩。"""
|
|
||||||
if self.startup_overlay is not None:
|
|
||||||
self.startup_overlay.destroy()
|
|
||||||
self.startup_overlay = None
|
|
||||||
if self.startup_window is not None:
|
|
||||||
self.startup_window.destroy()
|
|
||||||
self.startup_window = None
|
|
||||||
self.root.deiconify()
|
|
||||||
self.root.lift()
|
|
||||||
self.root.focus_force()
|
|
||||||
|
|
||||||
def get_resource_candidates(self, filename):
|
def get_resource_candidates(self, filename):
|
||||||
"""返回项目内可能存放资源文件的候选路径。"""
|
"""返回项目内可能存放资源文件的候选路径。"""
|
||||||
return [
|
return [
|
||||||
self.base_dir / filename,
|
self.base_dir / filename,
|
||||||
self.base_dir / "models" / filename,
|
|
||||||
self.base_dir / "tools" / filename,
|
self.base_dir / "tools" / filename,
|
||||||
self.app_dir / filename,
|
|
||||||
self.app_dir / "models" / filename,
|
|
||||||
self.app_dir / "tools" / filename,
|
|
||||||
Path.cwd() / filename,
|
Path.cwd() / filename,
|
||||||
Path.cwd() / "models" / filename,
|
|
||||||
Path.cwd() / "tools" / filename,
|
Path.cwd() / "tools" / filename,
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -888,20 +608,11 @@ class TomatoHarvestingUI:
|
|||||||
"""解析绝对路径或相对项目根目录的路径。"""
|
"""解析绝对路径或相对项目根目录的路径。"""
|
||||||
if path_value:
|
if path_value:
|
||||||
candidate = Path(path_value).expanduser()
|
candidate = Path(path_value).expanduser()
|
||||||
if candidate.is_absolute():
|
if not candidate.is_absolute():
|
||||||
|
candidate = self.base_dir / candidate
|
||||||
candidate = candidate.resolve()
|
candidate = candidate.resolve()
|
||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
return candidate
|
return candidate
|
||||||
else:
|
|
||||||
relative_candidates = [
|
|
||||||
self.app_dir / candidate,
|
|
||||||
self.base_dir / candidate,
|
|
||||||
Path.cwd() / candidate,
|
|
||||||
]
|
|
||||||
for relative_candidate in relative_candidates:
|
|
||||||
resolved = relative_candidate.resolve()
|
|
||||||
if resolved.exists():
|
|
||||||
return resolved
|
|
||||||
if fallback_name:
|
if fallback_name:
|
||||||
for candidate in self.get_resource_candidates(fallback_name):
|
for candidate in self.get_resource_candidates(fallback_name):
|
||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
@ -1019,8 +730,7 @@ class TomatoHarvestingUI:
|
|||||||
self.agv_speed_stop.insert(0, "0.0")
|
self.agv_speed_stop.insert(0, "0.0")
|
||||||
self.total_duration.insert(0, "300")
|
self.total_duration.insert(0, "300")
|
||||||
self.agv_stop_timeout.insert(0, "10")
|
self.agv_stop_timeout.insert(0, "10")
|
||||||
default_model_path = self.resolve_path("models/best.pt", fallback_name="best.pt")
|
self.yolo_model_path.insert(0, "best.pt")
|
||||||
self.yolo_model_path.insert(0, str(default_model_path) if default_model_path else "models/best.pt")
|
|
||||||
default_bg_path = self.resolve_path("", fallback_name="1.png")
|
default_bg_path = self.resolve_path("", fallback_name="1.png")
|
||||||
if default_bg_path:
|
if default_bg_path:
|
||||||
self.camera_bg_path.insert(0, str(default_bg_path))
|
self.camera_bg_path.insert(0, str(default_bg_path))
|
||||||
@ -1047,9 +757,7 @@ class TomatoHarvestingUI:
|
|||||||
self.agv_stop_timeout.delete(0, tk.END)
|
self.agv_stop_timeout.delete(0, tk.END)
|
||||||
self.agv_stop_timeout.insert(0, str(settings.get("AGV_STOP_TIMEOUT", "10")))
|
self.agv_stop_timeout.insert(0, str(settings.get("AGV_STOP_TIMEOUT", "10")))
|
||||||
self.yolo_model_path.delete(0, tk.END)
|
self.yolo_model_path.delete(0, tk.END)
|
||||||
saved_model_path = settings.get("YOLO_MODEL_PATH", "models/best.pt")
|
self.yolo_model_path.insert(0, str(settings.get("YOLO_MODEL_PATH", "best.pt")))
|
||||||
resolved_model_path = self.resolve_path(saved_model_path, fallback_name="best.pt")
|
|
||||||
self.yolo_model_path.insert(0, str(resolved_model_path) if resolved_model_path else str(saved_model_path))
|
|
||||||
self.camera_bg_path.delete(0, tk.END)
|
self.camera_bg_path.delete(0, tk.END)
|
||||||
saved_bg_path = settings.get("CAMERA_BG_PATH", "")
|
saved_bg_path = settings.get("CAMERA_BG_PATH", "")
|
||||||
resolved_bg_path = self.resolve_path(saved_bg_path, fallback_name="1.png")
|
resolved_bg_path = self.resolve_path(saved_bg_path, fallback_name="1.png")
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
# TomatoPick runtime dependencies for Windows packaging.
|
|
||||||
# If you need CUDA acceleration, install a matching torch/torchvision/torchaudio
|
|
||||||
# build first, then run: pip install -r requirements.txt
|
|
||||||
# tkinter is not listed here because it is bundled with the standard Windows
|
|
||||||
# CPython installer.
|
|
||||||
|
|
||||||
Pillow==11.2.1
|
|
||||||
opencv-python==4.10.0.84
|
|
||||||
numpy==2.0.0
|
|
||||||
pyrealsense2==2.55.1.6486
|
|
||||||
pyaubo_agvc_sdk==0.2.0
|
|
||||||
pyaubo_sdk==0.24.1
|
|
||||||
ultralytics==8.3.112
|
|
||||||
pyinstaller>=6,<7
|
|
||||||
Reference in New Issue
Block a user