可以
This commit is contained in:
+9
-1
@@ -4,4 +4,12 @@ Log/
|
||||
camera_params.json
|
||||
**.pyc**
|
||||
data/
|
||||
.env
|
||||
.env
|
||||
|
||||
main.build/
|
||||
main.dist/
|
||||
main.onefile-build/
|
||||
main.spec
|
||||
|
||||
dist/
|
||||
build/
|
||||
@@ -166,10 +166,11 @@ class CameraWorker(QThread):
|
||||
log_msg = pyqtSignal(str)
|
||||
disconnected = pyqtSignal()
|
||||
|
||||
def __init__(self, color_cfg: dict, depth_cfg: dict):
|
||||
def __init__(self, color_cfg: dict, depth_cfg: dict, preset: str | None = None):
|
||||
super().__init__()
|
||||
self._color_cfg = color_cfg
|
||||
self._depth_cfg = depth_cfg
|
||||
self._preset = preset
|
||||
self._lock = threading.Lock()
|
||||
self._running = True
|
||||
self._d2c = False
|
||||
@@ -180,10 +181,9 @@ class CameraWorker(QThread):
|
||||
self._has_dual_ir = False
|
||||
self._ir_enabled = False
|
||||
self._last_emit_ts = 0.0
|
||||
self._last_frames = None # 最近一帧,供"按键即拍"使用
|
||||
self._capture_frames = None # 触发拍照时快照的帧
|
||||
self._consec_timeouts = 0
|
||||
self._disconnect_emitted = False
|
||||
self._sync_wait_count = 0 # 等待同步帧的计数器
|
||||
|
||||
def set_d2c(self, enabled: bool):
|
||||
with self._lock:
|
||||
@@ -191,9 +191,8 @@ class CameraWorker(QThread):
|
||||
|
||||
def trigger_capture(self, save_dir: str):
|
||||
with self._lock:
|
||||
self._capture_dir = save_dir
|
||||
self._capture_frames = self._last_frames # 快照当前帧
|
||||
self._do_capture = True
|
||||
self._capture_dir = save_dir
|
||||
self._do_capture = True
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
@@ -206,6 +205,7 @@ class CameraWorker(QThread):
|
||||
from pyorbbecsdk import (
|
||||
Pipeline, Config, AlignFilter, PointCloudFilter,
|
||||
OBStreamType, OBSensorType, OBFrameType, OBFormat,
|
||||
OBFrameAggregateOutputMode,
|
||||
save_point_cloud_to_ply,
|
||||
)
|
||||
from utils import frame_to_bgr_image
|
||||
@@ -216,7 +216,7 @@ class CameraWorker(QThread):
|
||||
pipeline = Pipeline()
|
||||
config = Config()
|
||||
|
||||
# 获取 SN + 探测 IR 类型
|
||||
# 获取 SN + 探测 IR 类型 + 应用 preset
|
||||
try:
|
||||
device = pipeline.get_device()
|
||||
try:
|
||||
@@ -228,6 +228,12 @@ class CameraWorker(QThread):
|
||||
st = sensor_list[i].get_type()
|
||||
if st in (OBSensorType.LEFT_IR_SENSOR, OBSensorType.RIGHT_IR_SENSOR):
|
||||
self._has_dual_ir = True
|
||||
if self._preset:
|
||||
try:
|
||||
device.load_preset(self._preset)
|
||||
self.log_msg.emit(f"已加载 Preset:{self._preset}")
|
||||
except Exception as exc:
|
||||
self.log_msg.emit(f"Preset 加载失败:{exc}")
|
||||
except Exception as exc:
|
||||
self.log_msg.emit(f"设备信息获取警告:{exc}")
|
||||
|
||||
@@ -245,6 +251,12 @@ class CameraWorker(QThread):
|
||||
|
||||
config.enable_stream(color_profile)
|
||||
config.enable_stream(depth_profile)
|
||||
# 要求所有启用的流都到齐才输出帧集,避免不同时刻的帧被混合返回
|
||||
try:
|
||||
config.set_frame_aggregate_output_mode(
|
||||
OBFrameAggregateOutputMode.FULL_FRAME_REQUIRE)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 启用与深度流分辨率/帧率一致的 IR 流(找不到匹配则跳过,不影响主流)
|
||||
self._ir_enabled = False
|
||||
@@ -295,6 +307,11 @@ class CameraWorker(QThread):
|
||||
self.disconnected.emit()
|
||||
return
|
||||
|
||||
try:
|
||||
pipeline.enable_frame_sync()
|
||||
except Exception:
|
||||
pass # 部分固件版本不支持,忽略
|
||||
|
||||
align_filter = AlignFilter(align_to_stream=OBStreamType.COLOR_STREAM)
|
||||
point_cloud_filter = PointCloudFilter()
|
||||
|
||||
@@ -312,14 +329,9 @@ class CameraWorker(QThread):
|
||||
self._consec_timeouts = 0
|
||||
|
||||
with self._lock:
|
||||
self._last_frames = frames # 始终保存最新帧供快照
|
||||
d2c = self._d2c
|
||||
do_cap = self._do_capture
|
||||
cap_dir = self._capture_dir
|
||||
cap_frames = self._capture_frames
|
||||
if do_cap:
|
||||
self._do_capture = False
|
||||
self._capture_frames = None
|
||||
d2c = self._d2c
|
||||
do_cap = self._do_capture
|
||||
cap_dir = self._capture_dir
|
||||
|
||||
# 首帧提取相机参数
|
||||
if self._camera_params is None:
|
||||
@@ -334,17 +346,34 @@ class CameraWorker(QThread):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 执行拍照
|
||||
# 执行拍照(等待 RGB-D 时间戳对齐后再拍)
|
||||
if do_cap and cap_dir:
|
||||
try:
|
||||
self._run_capture(
|
||||
cap_frames or frames, cap_dir,
|
||||
align_filter, point_cloud_filter,
|
||||
frame_to_bgr_image, save_point_cloud_to_ply,
|
||||
OBFrameType, OBFormat,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.log_msg.emit(f"拍照错误:{exc}")
|
||||
ts_diff = self._frame_ts_diff(frames)
|
||||
self._sync_wait_count += 1
|
||||
threshold_ms = 1000 // max(self._depth_cfg.get("fps", 30), 1)
|
||||
force = self._sync_wait_count >= 10
|
||||
if ts_diff <= threshold_ms or force:
|
||||
with self._lock:
|
||||
self._do_capture = False
|
||||
self._capture_dir = ""
|
||||
self._sync_wait_count = 0
|
||||
if force and ts_diff > threshold_ms:
|
||||
self.log_msg.emit(
|
||||
f"警告:RGB-D 时间差 {ts_diff}ms 仍未对齐,强制拍摄")
|
||||
else:
|
||||
self.log_msg.emit(f"帧同步良好(RGB-D 差 {ts_diff}ms),开始拍摄")
|
||||
try:
|
||||
self._run_capture(
|
||||
frames, cap_dir,
|
||||
align_filter, point_cloud_filter,
|
||||
frame_to_bgr_image, save_point_cloud_to_ply,
|
||||
OBFrameType, OBFormat,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.log_msg.emit(f"拍照错误:{exc}")
|
||||
else:
|
||||
self.log_msg.emit(
|
||||
f"等待帧同步…RGB-D 差 {ts_diff}ms({self._sync_wait_count}/10)")
|
||||
continue
|
||||
|
||||
# 预览
|
||||
@@ -455,6 +484,18 @@ class CameraWorker(QThread):
|
||||
|
||||
# ── Static helpers ──
|
||||
|
||||
@staticmethod
|
||||
def _frame_ts_diff(frames) -> int:
|
||||
"""返回彩色帧与深度帧时间戳差值(ms),获取失败返回 0。"""
|
||||
try:
|
||||
cf = frames.get_color_frame()
|
||||
df = frames.get_depth_frame()
|
||||
if cf and df:
|
||||
return abs(int(cf.get_timestamp()) - int(df.get_timestamp()))
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _extract_params(color_frame, depth_frame) -> dict:
|
||||
cvp = color_frame.get_stream_profile().as_video_stream_profile()
|
||||
@@ -671,6 +712,15 @@ class MainWindow(QMainWindow):
|
||||
self.depth_combo.setStyleSheet(COMBO_CSS)
|
||||
g2.addWidget(self.depth_combo)
|
||||
|
||||
g2.addSpacing(12)
|
||||
g2.addWidget(QLabel("Preset:"))
|
||||
self.preset_combo = QComboBox()
|
||||
self.preset_combo.setMinimumWidth(160)
|
||||
self.preset_combo.setMaxVisibleItems(12)
|
||||
self.preset_combo.setStyleSheet(COMBO_CSS)
|
||||
self.preset_combo.setEnabled(False)
|
||||
g2.addWidget(self.preset_combo)
|
||||
|
||||
g2.addStretch()
|
||||
self.stream_btn = QPushButton("开启")
|
||||
self.stream_btn.setCheckable(True)
|
||||
@@ -770,32 +820,37 @@ class MainWindow(QMainWindow):
|
||||
def _build_d2c_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
layout = QVBoxLayout(w)
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
layout.setSpacing(8)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(6)
|
||||
|
||||
# ── 深度图目录 ──
|
||||
row1 = QHBoxLayout()
|
||||
row1.addWidget(QLabel("深度图目录:"))
|
||||
self.d2c_dir_edit = QLineEdit()
|
||||
self.d2c_dir_edit.setPlaceholderText("选择包含 PNG 深度图的文件夹…")
|
||||
row1.addWidget(self.d2c_dir_edit)
|
||||
br1 = QPushButton("浏览…"); br1.setFixedWidth(70)
|
||||
br1.clicked.connect(lambda: self._browse_dir(self.d2c_dir_edit))
|
||||
row1.addWidget(br1)
|
||||
layout.addLayout(row1)
|
||||
COMBO_CSS = "QComboBox { combobox-popup: 0; }"
|
||||
|
||||
# ── 相机参数 ──
|
||||
params_grp = QGroupBox("相机参数")
|
||||
pg = QVBoxLayout(params_grp)
|
||||
# ── 第 1 步:刷新设备 ──
|
||||
grp1 = self._step_group(1, "刷新设备")
|
||||
g1 = QHBoxLayout(grp1)
|
||||
g1.setContentsMargins(8, 4, 8, 6)
|
||||
d2c_refresh_btn = QPushButton("刷新设备")
|
||||
d2c_refresh_btn.setFixedHeight(28)
|
||||
d2c_refresh_btn.clicked.connect(self._refresh_devices)
|
||||
g1.addWidget(d2c_refresh_btn)
|
||||
self.d2c_device_status = QLabel("未连接设备,请点击「刷新设备」")
|
||||
self.d2c_device_status.setStyleSheet("color:#aaa; font-size:12px;")
|
||||
g1.addWidget(self.d2c_device_status)
|
||||
g1.addStretch()
|
||||
layout.addWidget(grp1)
|
||||
|
||||
# ── 第 2 步:相机参数 ──
|
||||
grp2 = self._step_group(2, "相机参数(用于软件 D2C 计算)")
|
||||
pg = QVBoxLayout(grp2)
|
||||
pg.setContentsMargins(8, 4, 8, 6)
|
||||
pg.setSpacing(6)
|
||||
|
||||
# 从分辨率自动查找
|
||||
combo_row = QHBoxLayout()
|
||||
combo_row.addWidget(QLabel("RGB:"))
|
||||
self.d2c_color_combo = QComboBox()
|
||||
self.d2c_color_combo.setMinimumWidth(240)
|
||||
self.d2c_color_combo.setMaxVisibleItems(12)
|
||||
self.d2c_color_combo.setStyleSheet("QComboBox { combobox-popup: 0; }")
|
||||
self.d2c_color_combo.setStyleSheet(COMBO_CSS)
|
||||
combo_row.addWidget(self.d2c_color_combo)
|
||||
|
||||
combo_row.addSpacing(12)
|
||||
@@ -803,7 +858,7 @@ class MainWindow(QMainWindow):
|
||||
self.d2c_depth_combo = QComboBox()
|
||||
self.d2c_depth_combo.setMinimumWidth(240)
|
||||
self.d2c_depth_combo.setMaxVisibleItems(12)
|
||||
self.d2c_depth_combo.setStyleSheet("QComboBox { combobox-popup: 0; }")
|
||||
self.d2c_depth_combo.setStyleSheet(COMBO_CSS)
|
||||
combo_row.addWidget(self.d2c_depth_combo)
|
||||
|
||||
combo_row.addSpacing(12)
|
||||
@@ -814,13 +869,11 @@ class MainWindow(QMainWindow):
|
||||
combo_row.addStretch()
|
||||
pg.addLayout(combo_row)
|
||||
|
||||
# 分隔线提示
|
||||
sep = QLabel("── 或直接指定 JSON 文件 ──")
|
||||
sep.setAlignment(Qt.AlignCenter)
|
||||
sep.setStyleSheet("color:#666; font-size:11px;")
|
||||
pg.addWidget(sep)
|
||||
|
||||
# 手动选 JSON
|
||||
json_row = QHBoxLayout()
|
||||
self.params_path_edit = QLineEdit()
|
||||
self.params_path_edit.setPlaceholderText("留空则使用上方自动查找结果…")
|
||||
@@ -830,19 +883,33 @@ class MainWindow(QMainWindow):
|
||||
json_row.addWidget(br2)
|
||||
pg.addLayout(json_row)
|
||||
|
||||
# 当前选中的参数文件
|
||||
self.params_selected_lbl = QLabel("(尚未选定参数文件)")
|
||||
self.params_selected_lbl.setStyleSheet("color:#888; font-size:12px;")
|
||||
pg.addWidget(self.params_selected_lbl)
|
||||
layout.addWidget(grp2)
|
||||
|
||||
layout.addWidget(params_grp)
|
||||
# ── 第 3 步:深度图目录 ──
|
||||
grp3 = self._step_group(3, "深度图目录")
|
||||
g3 = QHBoxLayout(grp3)
|
||||
g3.setContentsMargins(8, 4, 8, 6)
|
||||
g3.setSpacing(8)
|
||||
self.d2c_dir_edit = QLineEdit()
|
||||
self.d2c_dir_edit.setPlaceholderText("选择包含 PNG 深度图的文件夹…")
|
||||
g3.addWidget(self.d2c_dir_edit)
|
||||
br1 = QPushButton("浏览…"); br1.setFixedWidth(70)
|
||||
br1.clicked.connect(lambda: self._browse_dir(self.d2c_dir_edit))
|
||||
g3.addWidget(br1)
|
||||
layout.addWidget(grp3)
|
||||
|
||||
# ── Splat + 运行 ──
|
||||
opt_run = QHBoxLayout()
|
||||
# ── 第 4 步:开始转换 ──
|
||||
step4_row = QHBoxLayout()
|
||||
step4_lbl = QLabel("第 4 步 · 开始转换")
|
||||
step4_lbl.setStyleSheet("color:#8ec8ff; font-weight:bold; font-size:12px;")
|
||||
step4_row.addWidget(step4_lbl)
|
||||
self.splat_check = QCheckBox("Splat 3×3 填充(减少 D2C 空洞,默认开启)")
|
||||
self.splat_check.setChecked(True)
|
||||
opt_run.addWidget(self.splat_check)
|
||||
opt_run.addStretch()
|
||||
step4_row.addWidget(self.splat_check)
|
||||
step4_row.addStretch()
|
||||
self.run_d2c_btn = QPushButton("▶ 开始批量 D2C 转换")
|
||||
self.run_d2c_btn.setFixedHeight(36)
|
||||
self.run_d2c_btn.setStyleSheet(
|
||||
@@ -851,8 +918,8 @@ class MainWindow(QMainWindow):
|
||||
"QPushButton:disabled{ background:#444; color:#777; }"
|
||||
)
|
||||
self.run_d2c_btn.clicked.connect(self._run_batch_d2c)
|
||||
opt_run.addWidget(self.run_d2c_btn)
|
||||
layout.addLayout(opt_run)
|
||||
step4_row.addWidget(self.run_d2c_btn)
|
||||
layout.addLayout(step4_row)
|
||||
|
||||
# ── 日志 ──
|
||||
layout.addWidget(QLabel("输出日志:"))
|
||||
@@ -875,6 +942,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.color_combo.clear()
|
||||
self.depth_combo.clear()
|
||||
self.preset_combo.clear()
|
||||
self.d2c_color_combo.clear()
|
||||
self.d2c_depth_combo.clear()
|
||||
self._color_profiles_data.clear()
|
||||
@@ -916,14 +984,39 @@ class MainWindow(QMainWindow):
|
||||
self.depth_combo.addItem(label, cfg)
|
||||
self.d2c_depth_combo.addItem(label, cfg)
|
||||
|
||||
# 枚举 preset
|
||||
try:
|
||||
preset_list = p.get_device().get_available_preset_list()
|
||||
current_preset = p.get_device().get_current_preset_name()
|
||||
if preset_list and len(preset_list) > 0:
|
||||
self.preset_combo.addItem("── 不切换 ──", None)
|
||||
for name in preset_list:
|
||||
self.preset_combo.addItem(name, name)
|
||||
# 选中当前 preset
|
||||
idx = self.preset_combo.findData(current_preset)
|
||||
if idx >= 0:
|
||||
self.preset_combo.setCurrentIndex(idx)
|
||||
self.preset_combo.setEnabled(True)
|
||||
else:
|
||||
self.preset_combo.addItem("(设备不支持)", None)
|
||||
self.preset_combo.setEnabled(False)
|
||||
except Exception:
|
||||
self.preset_combo.addItem("(不支持)", None)
|
||||
self.preset_combo.setEnabled(False)
|
||||
|
||||
nc, nd = len(self._color_profiles_data), len(self._depth_profiles_data)
|
||||
self.device_status.setText(
|
||||
f"已连接 SN: {sn} 彩色 {nc} 项 / 深度 {nd} 项")
|
||||
msg = f"已连接 SN: {sn} 彩色 {nc} 项 / 深度 {nd} 项"
|
||||
self.device_status.setText(msg)
|
||||
self.device_status.setStyleSheet("color:#4aaa4a; font-size:12px;")
|
||||
self.d2c_device_status.setText(msg)
|
||||
self.d2c_device_status.setStyleSheet("color:#4aaa4a; font-size:12px;")
|
||||
|
||||
except Exception as exc:
|
||||
self.device_status.setText(f"未找到设备:{exc}")
|
||||
msg = f"未找到设备:{exc}"
|
||||
self.device_status.setText(msg)
|
||||
self.device_status.setStyleSheet("color:#cc6666; font-size:12px;")
|
||||
self.d2c_device_status.setText(msg)
|
||||
self.d2c_device_status.setStyleSheet("color:#cc6666; font-size:12px;")
|
||||
|
||||
# ─────────────────────────────────
|
||||
# Stream Toggle
|
||||
@@ -947,7 +1040,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _start_stream(self, color_cfg: dict, depth_cfg: dict):
|
||||
self._stop_stream() # 确保旧 worker 已停止
|
||||
self._worker = CameraWorker(color_cfg, depth_cfg)
|
||||
preset = self.preset_combo.currentData() # None 表示不切换
|
||||
self._worker = CameraWorker(color_cfg, depth_cfg, preset=preset)
|
||||
self._worker.preview_ready.connect(self._update_preview)
|
||||
self._worker.capture_done.connect(self._on_capture_done)
|
||||
self._worker.params_ready.connect(self._on_params_ready)
|
||||
|
||||
Reference in New Issue
Block a user