This commit is contained in:
i
2026-04-22 14:52:29 +08:00
parent 64523dbea5
commit 8b2c56d3cd
2 changed files with 275 additions and 167 deletions
+255 -151
View File
@@ -1,40 +1,18 @@
# Orbbec D2C Batch Converter (软件对齐) # Orbbec 数据采集 & D2C 对齐工具
将奥比中光相机采集的深度图(Depth)批量软件对齐到彩色图(Color)坐标系,输出与 RGB 分辨率一致的对齐深度图。 基于 PyQt5 的奥比中光相机数据采集与深度图软件对齐一体化工具。支持实时预览、一键拍照、批量 D2C 转换,并提供两种 D2C 算法(正向投影 / 逆向投影)供选择。
--- ---
## 原理 ## 功能特点
软件 D2C(Depth-to-Color)对齐流程: | 功能 | 说明 |
|------|------|
``` | **数据采集** | 实时预览 RGB + 深度伪彩图,支持 D2C 预览切换;一键拍照保存多模态数据 |
深度像素 (u_d, v_d, Z) | **批量 D2C** | 对 Orbbec Viewer 导出的深度图批量软件对齐,无需连接相机 |
↓ Brown-Conrady 畸变校正 + 反投影 | **双算法支持** | 正向投影 + Splat 填充 / 逆向投影(SDK 同等),可实时切换预览 |
3D 点(深度相机坐标系)[X, Y, Z] | **自动参数获取** | 从相机实时提取内外参,自动保存为 JSON |
↓ 外参旋转 R + 平移 t | **分辨率自适应** | 支持字体缩放(0.8× ~ 2.0×),适配不同 DPI 屏幕 |
3D 点(彩色相机坐标系)[X_c, Y_c, Z_c]
↓ 彩色内参投影
彩色像素 (u_c, v_c) → 写入对齐深度图
```
多个深度点映射到同一彩色像素时,保留最近点(小 Z 值覆盖大 Z 值)。
### 前向映射的间隙问题与 3×3 Splat 修复
前向映射(forward scatter)存在固有缺陷:每个深度像素只写入 color 平面的**一个像素**。由于深度相机与彩色相机的视角差异,经外参变换 + 透视投影后,相邻深度像素在 color 平面上的落点会出现跳跃,中间留下空洞,输出图像呈现**离散小方块**而非连续深度面。
**修复方案:3×3 邻域扩散(splat)**
对每个投影后的深度像素,同时写入以其为中心的 3×3 邻域,而不是单个像素。遍历顺序仍保持**远→近**,因此当多个 splat 重叠时,Z 值较小(较近)的深度最后写入并覆盖,遮挡关系依然正确:
```
for dv in -1, 0, +1:
for du in -1, 0, +1:
aligned[v + dv, u + du] = z # 远→近顺序,近处覆盖远处
```
如果局部仍有残留空洞(如深度图有效范围边缘),可将 3×3 扩大为 5×5(`range(-2, 3)`)。
--- ---
@@ -46,43 +24,21 @@ for dv in -1, 0, +1:
| pyorbbecsdk | 2.x | | pyorbbecsdk | 2.x |
| numpy | — | | numpy | — |
| opencv-python | — | | opencv-python | — |
| PyQt5 | — |
```bash ```bash
pip install numpy opencv-python pip install numpy opencv-python PyQt5
``` ```
### 安装 pyorbbecsdk ### 安装 pyorbbecsdk
> 官方文档:https://orbbec.github.io/pyorbbecsdk/source/2_installation/install_the_package.html > 官方文档:https://orbbec.github.io/pyorbbecsdk/source/2_installation/install_the_package.html
> GitHub:https://github.com/orbbec/pyorbbecsdk
**支持平台:** Windows 10 x64 / Ubuntu 18.04·20.04·22.04 x64 / ARM64
#### 方式一:在线安装(推荐)
```bash ```bash
# Windows / Linux
pip install pyorbbecsdk2 pip install pyorbbecsdk2
``` ```
> 无论在线(`pyorbbecsdk2`)还是离线安装,最终包目录名均为 `pyorbbecsdk`,`import pyorbbecsdk` 均可正常使用。 ### 设备驱动配置
#### 方式二:离线安装(.whl)
1. 前往 [Releases](https://github.com/orbbec/pyorbbecsdk/releases) 下载与 Python 版本和平台匹配的 `.whl` 文件
(例:Python 3.11 / Windows → `pyorbbecsdk-2.x.x-cp311-cp311-win_amd64.whl`)
2. 安装:
```bash
pip install pyorbbecsdk-2.x.x-cp311-cp311-win_amd64.whl
```
#### 安装后验证
```bash
pip show pyorbbecsdk
```
#### 设备驱动配置
**Windows**(管理员 PowerShell): **Windows**(管理员 PowerShell):
```powershell ```powershell
@@ -104,121 +60,258 @@ sudo udevadm control --reload && sudo udevadm trigger
| 文件 | 说明 | | 文件 | 说明 |
|------|------| |------|------|
| `d2c_batch.py` | 主程序 | | `main.py` | GUI 主程序(数据采集 + D2C 批量转换) |
| `d2c_batch.py` | 命令行 D2C 批量转换工具(可独立使用) |
| `utils.py` | SDK 辅助函数(帧格式转换等) |
| `camera_params.json` | 相机参数缓存(自动生成,可复用) | | `camera_params.json` | 相机参数缓存(自动生成,可复用) |
| `utils.py` | 奥比中光 SDK 辅助函数(备用) |
--- ---
## 使用方法 ## GUI 使用方法
### 完整流程(首次使用)
```bash ```bash
python d2c_batch.py python main.py
``` ```
1. 列出相机所有 **RGB 配置**(分辨率 / 帧率 / 格式),输入序号选择 ### 标签页一:数据采集
2. 列出所有 **Depth 配置**,输入序号选择
3. 自动启动相机,采集一帧,提取内外参,保存到 `camera_params.json`
4. 输入**深度图目录**路径
5. 批量转换,结果保存到 `<输入目录>_d2c/`
### 仅获取相机参数(不转换) 1. **刷新设备**:点击获取相机支持的 RGB / Depth 分辨率列表
2. **流配置**:选择 RGB 分辨率、Depth 分辨率、Preset(可选)
3. **保存目录 & 采集内容**:
- 设置根目录和子目录名称,点击「创建 / 载入」
- 勾选需要采集的数据类型(RGB、Depth、Depth D2C、IR、点云等)
- 选择 **D2C 方式**:
- **逆向投影(SDK 同等)**:使用 SDK `AlignFilter`,速度快,无空洞
- **正向投影 + Splat**:纯软件实现,支持 Splat 邻域扩散(关闭 / 3×3 / 5×5 / 7×7)
4. **预览**:勾选「D2C 预览」可实时查看对齐效果,切换 D2C 方式预览即时更新
5. **拍照**:点击「拍照」或按快捷键 **S**,数据自动保存到指定目录
### 标签页二:Orbbec Viewer 深度图 D2C
1. **刷新设备**并选择 RGB / Depth 分辨率,点击「从设备获取参数」
- 或手动选择已有的 `camera_params.json`
2. **选择深度图目录**:包含 PNG 深度图的文件夹
3. **选择转换方式**:
- 正向投影 + Splat(默认)
- 逆向投影(SDK 同等)
4. 设置 **Splat 大小**(正向投影时可用)
5. 点击「开始批量 D2C 转换」
---
## 命令行使用方法(d2c_batch.py)
```bash ```bash
python d2c_batch.py --params python d2c_batch.py # 完整流程:选配置 → 获取参数 → 批量转换
``` python d2c_batch.py --params # 仅获取相机参数
python d2c_batch.py --convert # 仅批量转换(使用已有 camera_params.json)
适合先连相机标定参数,稍后离线转换。
### 仅批量转换(无需连相机)
```bash
python d2c_batch.py --convert
```
使用已有的 `camera_params.json`,直接输入深度图目录开始转换。适合相机参数已获取、需要反复处理不同数据集的场景。
### 关闭 3×3 Splat 填充
```bash
python d2c_batch.py --no-splat
```
默认开启 splat(前向映射后用 3×3 邻域扩散填补间隙)。加 `--no-splat` 可关闭,每个深度像素仅写入 color 平面的单个像素,适合对原始投影结果做对比分析。可与其他参数组合使用:
```bash
python d2c_batch.py --convert --no-splat
``` ```
--- ---
## 交互示例 ## D2C 原理详解
本工具提供两种 D2C(Depth-to-Color)对齐算法,核心差异在于**投影方向**。
### 1. 正向投影(Forward Projection)+ Splat
**思路**:从深度图每个像素出发,反投影到 3D 空间,经外参变换后投影到彩色图平面。
``` ```
Found saved camera params: camera_params.json 深度像素 (u_d, v_d, Z)
Use existing params? [y=use existing / n=re-fetch from camera]: n ↓ Brown-Conrady 畸变校正 + 反投影
3D 点(深度相机坐标系)[X, Y, Z]
=== Phase 1: Camera Parameter Acquisition === ↓ 外参旋转 R + 平移 t
3D 点(彩色相机坐标系)[X_c, Y_c, Z_c]
Available RGB (Color) profiles: ↓ 彩色内参投影
# Resolution FPS Format 彩色像素 (u_c, v_c) → 写入对齐深度图
----------------------------------------
[0 ] 1920x1080 30 MJPG
[1 ] 1280x720 30 MJPG
[2 ] 640x480 30 RGB
Select RGB (Color) profile index: 0
-> Selected: 1920x1080 @ 30fps MJPG
Available Depth profiles:
# Resolution FPS Format
----------------------------------------
[0 ] 1280x800 30 Y16
[1 ] 640x400 30 Y16
[2 ] 320x200 30 Y16
Select Depth profile index: 1
-> Selected: 640x400 @ 30fps Y16
Starting pipeline...
Pipeline stopped.
Camera params saved to: camera_params.json
--- Camera Parameters Summary ---
RGB : 1920x1080 @ 30fps MJPG
fx=1382.5 fy=1382.5 cx=959.8 cy=539.4
Depth : 640x400 @ 30fps Y16
fx=424.0 fy=424.0 cx=319.5 cy=199.5
scale=0.001000 mm/unit
Extrinsic t : [-14.82 0.12 0.03] (mm)
Enter depth images directory: D:\data\depth_raw
=== Phase 2: Batch D2C Conversion ===
Input dir : D:\data\depth_raw
Output dir : D:\data\depth_raw_d2c
Files : 120
Depth scale: 0.001000 mm/unit
Output size: 1920x1080 (color resolution)
[ 1/120] 000001.png
[ 2/120] 000002.png
...
[ 120/120] 000120.png
Done. 120/120 files converted in 8.3s
Results: D:\data\depth_raw_d2c
``` ```
--- **间隙问题**:每个深度像素只写入彩色平面的**单个像素**。由于视角差异,相邻深度像素投影后会出现跳跃,留下空洞(离散小方块)。
## 输入 / 输出格式 **Splat 修复**:对每个投影后的像素,同时写入以其为中心的 N×N 邻域,远→近顺序遍历保证遮挡正确。
| 项目 | 说明 | ```python
def software_d2c(depth_img, params, splat_radius=1):
depth_intr = params["depth"]["intrinsic"]
depth_dist = params["depth"]["distortion"]
color_intr = params["color"]["intrinsic"]
ext = params["extrinsic"]
color_w, color_h = color_intr["width"], color_intr["height"]
fx_c, fy_c = color_intr["fx"], color_intr["fy"]
cx_c, cy_c = color_intr["cx"], color_intr["cy"]
dh, dw = depth_img.shape
depth_scale = params["depth"]["scale"]
# 1. 像素网格
u_d, v_d = np.meshgrid(np.arange(dw, dtype=np.float32),
np.arange(dh, dtype=np.float32))
# 2. 深度值(mm)
Z = depth_img.astype(np.float32) * depth_scale
valid = Z > 0
# 3. 去畸变 + 反投影到深度相机 3D 空间
x_u, y_u = _undistort_points(u_d, v_d, depth_intr, depth_dist)
X = x_u * Z
Y = y_u * Z
# 4. 外参变换:深度相机 → 彩色相机
R = np.asarray(ext["rot"], dtype=np.float64).reshape(3, 3)
t = np.asarray(ext["transform"], dtype=np.float64)
pts = np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=0)
pts_c = R @ pts + t[:, np.newaxis]
Xc = pts_c[0].reshape(dh, dw).astype(np.float32)
Yc = pts_c[1].reshape(dh, dw).astype(np.float32)
Zc = pts_c[2].reshape(dh, dw).astype(np.float32)
# 5. 投影到彩色图平面
valid_c = valid & (Zc > 0)
u_c = np.where(valid_c, fx_c * Xc / Zc + cx_c, -1.0)
v_c = np.where(valid_c, fy_c * Yc / Zc + cy_c, -1.0)
u_ci = np.round(u_c).astype(np.int32)
v_ci = np.round(v_c).astype(np.int32)
in_bounds = (u_ci >= 0) & (u_ci < color_w) & (v_ci >= 0) & (v_ci < color_h)
mask = valid_c & in_bounds
u_vals = u_ci[mask]
v_vals = v_ci[mask]
z_vals = depth_img[mask]
# 6. 远→近排序,近处覆盖远处
order = np.argsort(z_vals)[::-1]
u_vals = u_vals[order]
v_vals = v_vals[order]
z_vals = z_vals[order]
aligned = np.zeros((color_h, color_w), dtype=np.uint16)
# 7. Splat 填充(N×N 邻域扩散)
if splat_radius > 0:
for dv in range(-splat_radius, splat_radius + 1):
for du in range(-splat_radius, splat_radius + 1):
us = np.clip(u_vals + du, 0, color_w - 1)
vs = np.clip(v_vals + dv, 0, color_h - 1)
aligned[vs, us] = z_vals
else:
aligned[v_vals, u_vals] = z_vals
return aligned
```
| 特点 | 说明 |
|------|------| |------|------|
| 输入深度图 | PNG,uint16,单位为传感器原始单位(乘以 `depth_scale` 得 mm) | | 优点 | 直观、可控制 Splat 大小、适合理解投影原理 |
| 输出对齐深度图 | PNG,uint16,单位与输入相同,分辨率与所选 RGB 配置一致,目录名 `<input>_d2c` | | 缺点 | 正向映射天然有间隙,必须配合 Splat;计算量较大 |
| 输出伪彩图 | PNG,uint8 BGR,JET colormap,无效像素为纯黑,目录名 `<input>_d2c_vis` | | 适用 | 对空洞容忍度有要求的场景、教学/算法验证 |
---
### 2. 逆向投影(Inverse Projection)
**思路**:遍历彩色图每个像素,反投影为射线,变换到深度相机空间后采样深度图。输出天生 dense,无正向映射空洞。
```
彩色像素 (u_c, v_c)
↓ 彩色内参反投影
射线方向(彩色相机坐标系)[xn, yn, 1]
↓ 外参逆变换 R^T
射线方向(深度相机坐标系)[ax, ay, az]
↓ 结合深度图估计 Z_c
深度相机像素 (u_d, v_d) → 采样深度值
```
采用**双 Pass 策略**:
- **Pass 0**:仅旋转(忽略平移),粗略估计深度坐标
- **Pass 1**:用 Pass 0 的估计值进行完整的 R/t 变换,精确采样
```python
def software_d2c_inv(depth_img, params):
depth_intr = params["depth"]["intrinsic"]
color_intr = params["color"]["intrinsic"]
ext = params["extrinsic"]
depth_scale = params["depth"]["scale"]
color_w, color_h = color_intr["width"], color_intr["height"]
fx_d, fy_d = depth_intr["fx"], depth_intr["fy"]
cx_d, cy_d = depth_intr["cx"], depth_intr["cy"]
fx_c, fy_c = color_intr["fx"], color_intr["fy"]
cx_c, cy_c = color_intr["cx"], color_intr["cy"]
R = np.array(ext["rot"], dtype=np.float64).reshape(3, 3)
t = np.array(ext["transform"], dtype=np.float64)
R_inv = R.T
b = R_inv @ t
# 1. 彩色像素网格
uc, vc = np.meshgrid(np.arange(color_w, dtype=np.float32),
np.arange(color_h, dtype=np.float32))
xn_c = (uc - cx_c) / fx_c
yn_c = (vc - cy_c) / fy_c
# 2. 射线方向变换到深度相机空间
N = color_w * color_h
dirs = np.stack([xn_c.ravel(), yn_c.ravel(), np.ones(N, np.float32)], axis=0)
dirs_d = (R_inv @ dirs.astype(np.float64)).astype(np.float32)
ax = dirs_d[0].reshape(color_h, color_w)
ay = dirs_d[1].reshape(color_h, color_w)
az = dirs_d[2].reshape(color_h, color_w)
depth_f32 = depth_img.astype(np.float32)
az_safe = np.where(az > 1e-6, az, 1.0)
# Pass 0:旋转-only 粗略估计
m0_u = (fx_d * ax / az_safe + cx_d).astype(np.float32)
m0_v = (fy_d * ay / az_safe + cy_d).astype(np.float32)
Z_d0 = cv2.remap(depth_f32, m0_u, m0_v,
cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
# 估计 Z_c(mm)
bx, by, bz = float(b[0]), float(b[1]), float(b[2])
Z_c_mm = np.where(az_safe > 1e-6,
(Z_d0 * depth_scale + bz) / az_safe,
0.0).astype(np.float32)
# Pass 1:完整变换精确采样
X_d = Z_c_mm * ax - bx
Y_d = Z_c_mm * ay - by
Z_d = Z_c_mm * az - bz
Z_d_safe = np.where(Z_d > 1e-6, Z_d, 1.0)
m1_u = (fx_d * X_d / Z_d_safe + cx_d).astype(np.float32)
m1_v = (fy_d * Y_d / Z_d_safe + cy_d).astype(np.float32)
valid = (Z_d0 > 0) & (az > 1e-6) & (Z_d > 1e-6)
m1_u = np.where(valid, m1_u, -1.0)
m1_v = np.where(valid, m1_v, -1.0)
aligned = cv2.remap(depth_f32, m1_u, m1_v,
cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
return aligned.astype(np.uint16)
```
| 特点 | 说明 |
|------|------|
| 优点 | 输出天生 dense 无空洞;与 SDK `AlignFilter` 算法等价;速度快 |
| 缺点 | 原理较抽象;需要深度图配合估计 Z_c |
| 适用 | 生产环境、实时预览、对空洞零容忍的场景 |
---
### 两种方法对比
| 维度 | 正向投影 + Splat | 逆向投影 |
|------|------------------|----------|
| 投影方向 | 深度 → 彩色 | 彩色 → 深度 |
| 空洞 | 有(需 Splat 修复) | 无 |
| 速度 | 较慢(numpy 密集计算) | 较快(cv2.remap 向量化) |
| 与 SDK 一致性 | 独立实现 | 与 `AlignFilter` 等价 |
| Splat 可调 | ✓ 支持(关闭/3×3/5×5/7×7) | ✗ 不需要 |
--- ---
@@ -248,9 +341,20 @@ Enter depth images directory: D:\data\depth_raw
--- ---
## 输入 / 输出格式
| 项目 | 说明 |
|------|------|
| 输入深度图 | PNG,uint16,单位为传感器原始单位(乘以 `depth_scale` 得 mm) |
| 输出对齐深度图 | PNG,uint16,单位与输入相同,分辨率与所选 RGB 配置一致 |
| 输出伪彩图 | PNG,uint8 BGR,JET colormap,无效像素为纯黑 |
---
## 注意事项 ## 注意事项
- 获取相机参数时需要相机**实际连接**;批量转换时不需要。 - 获取相机参数时需要相机**实际连接**;批量转换时不需要。
- 更换相机或修改分辨率后需重新获取参数(运行时选 `n` 或使用 `--params`)。 - 更换相机或修改分辨率后需重新获取参数。
- 输入深度图分辨率须与获取参数时选择的 Depth 分辨率一致。 - 输入深度图分辨率须与获取参数时选择的 Depth 分辨率一致。
- 对齐深度图的有效区域受深度传感器视场角限制,彩色图边缘区域可能无深度值(值为 0)。 - 对齐深度图的有效区域受深度传感器视场角限制,彩色图边缘区域可能无深度值(值为 0)。
- **正向投影**实时预览帧率较低(纯 numpy 计算),建议确认效果后切回**逆向投影**保持预览流畅。
+19 -15
View File
@@ -46,7 +46,7 @@ from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QCheckBox, QTabWidget, QLabel, QPushButton, QLineEdit, QCheckBox, QTabWidget,
QComboBox, QFileDialog, QTextEdit, QFrame, QMessageBox, QComboBox, QFileDialog, QTextEdit, QFrame, QMessageBox,
QSizePolicy, QGroupBox, QDialog, QAction, QSizePolicy, QGroupBox, QDialog, QAction, QWidgetAction
) )
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QImage, QPixmap, QFont from PyQt5.QtGui import QImage, QPixmap, QFont
@@ -1104,6 +1104,8 @@ class MainWindow(QMainWindow):
method = self.capture_d2c_method_combo.currentData() method = self.capture_d2c_method_combo.currentData()
if self._worker: if self._worker:
self._worker.set_d2c_method(method) self._worker.set_d2c_method(method)
if method != "forward":
self.capture_splat_combo.setCurrentIndex(0)
self.capture_splat_combo.setEnabled(method == "forward") self.capture_splat_combo.setEnabled(method == "forward")
def _on_capture_splat_combo_changed(self): def _on_capture_splat_combo_changed(self):
@@ -1113,6 +1115,8 @@ class MainWindow(QMainWindow):
def _on_d2c_method_changed(self): def _on_d2c_method_changed(self):
method = self.d2c_method_combo.currentData() method = self.d2c_method_combo.currentData()
if method != "forward":
self.d2c_splat_combo.setCurrentIndex(0)
self.d2c_splat_combo.setEnabled(method == "forward") self.d2c_splat_combo.setEnabled(method == "forward")
@staticmethod @staticmethod
@@ -1155,7 +1159,7 @@ class MainWindow(QMainWindow):
g2.addWidget(QLabel("RGB:")) g2.addWidget(QLabel("RGB:"))
self.color_combo = QComboBox() self.color_combo = QComboBox()
self.color_combo.setMinimumWidth(_sz(340)) self.color_combo.setMinimumWidth(_sz(380))
self.color_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.color_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.color_combo.setMaxVisibleItems(15) self.color_combo.setMaxVisibleItems(15)
self.color_combo.setStyleSheet(COMBO_CSS) self.color_combo.setStyleSheet(COMBO_CSS)
@@ -1164,7 +1168,7 @@ class MainWindow(QMainWindow):
g2.addSpacing(12) g2.addSpacing(12)
g2.addWidget(QLabel("深度:")) g2.addWidget(QLabel("深度:"))
self.depth_combo = QComboBox() self.depth_combo = QComboBox()
self.depth_combo.setMinimumWidth(_sz(340)) self.depth_combo.setMinimumWidth(_sz(380))
self.depth_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.depth_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.depth_combo.setMaxVisibleItems(15) self.depth_combo.setMaxVisibleItems(15)
self.depth_combo.setStyleSheet(COMBO_CSS) self.depth_combo.setStyleSheet(COMBO_CSS)
@@ -1173,7 +1177,7 @@ class MainWindow(QMainWindow):
g2.addSpacing(12) g2.addSpacing(12)
g2.addWidget(QLabel("Preset:")) g2.addWidget(QLabel("Preset:"))
self.preset_combo = QComboBox() self.preset_combo = QComboBox()
self.preset_combo.setMinimumWidth(_sz(220)) self.preset_combo.setMinimumWidth(_sz(260))
self.preset_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.preset_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.preset_combo.setMaxVisibleItems(12) self.preset_combo.setMaxVisibleItems(12)
self.preset_combo.setStyleSheet(COMBO_CSS) self.preset_combo.setStyleSheet(COMBO_CSS)
@@ -1204,13 +1208,13 @@ class MainWindow(QMainWindow):
row_base = QHBoxLayout() row_base = QHBoxLayout()
row_base.setSpacing(6) row_base.setSpacing(6)
lbl_base = QLabel("根目录:") lbl_base = QLabel("根目录:")
lbl_base.setFixedWidth(_sz(80)) lbl_base.setMinimumWidth(_sz(80))
row_base.addWidget(lbl_base) row_base.addWidget(lbl_base)
self.base_dir_edit = QLineEdit( self.base_dir_edit = QLineEdit(
self._app_settings.save_base_dir or str(Path.cwd())) self._app_settings.save_base_dir or str(Path.cwd()))
row_base.addWidget(self.base_dir_edit) row_base.addWidget(self.base_dir_edit)
browse_base_btn = QPushButton("浏览…") browse_base_btn = QPushButton("浏览…")
browse_base_btn.setFixedWidth(_sz(58)) browse_base_btn.setMinimumWidth(_sz(70))
browse_base_btn.setFixedHeight(_sz(26)) browse_base_btn.setFixedHeight(_sz(26))
browse_base_btn.clicked.connect(self._browse_base_dir) browse_base_btn.clicked.connect(self._browse_base_dir)
row_base.addWidget(browse_base_btn) row_base.addWidget(browse_base_btn)
@@ -1220,7 +1224,7 @@ class MainWindow(QMainWindow):
row_ses = QHBoxLayout() row_ses = QHBoxLayout()
row_ses.setSpacing(8) row_ses.setSpacing(8)
lbl_ses = QLabel("子目录:") lbl_ses = QLabel("子目录:")
lbl_ses.setFixedWidth(_sz(80)) lbl_ses.setMinimumWidth(_sz(80))
row_ses.addWidget(lbl_ses) row_ses.addWidget(lbl_ses)
today = datetime.now() today = datetime.now()
self.session_edit = QLineEdit(f"{today.year}年{today.month}月{today.day}日") self.session_edit = QLineEdit(f"{today.year}年{today.month}月{today.day}日")
@@ -1246,7 +1250,7 @@ class MainWindow(QMainWindow):
row_cap = QHBoxLayout() row_cap = QHBoxLayout()
row_cap.setSpacing(_sz(8)) row_cap.setSpacing(_sz(8))
lbl_cap = QLabel("采集内容:") lbl_cap = QLabel("采集内容:")
lbl_cap.setFixedWidth(_sz(80)) lbl_cap.setMinimumWidth(_sz(80))
row_cap.addWidget(lbl_cap) row_cap.addWidget(lbl_cap)
self._capture_checks: dict = {} self._capture_checks: dict = {}
for key, label, default in [ for key, label, default in [
@@ -1269,10 +1273,10 @@ class MainWindow(QMainWindow):
row_d2c_method = QHBoxLayout() row_d2c_method = QHBoxLayout()
row_d2c_method.setSpacing(8) row_d2c_method.setSpacing(8)
lbl_d2c_method = QLabel("D2C 方式:") lbl_d2c_method = QLabel("D2C 方式:")
lbl_d2c_method.setFixedWidth(_sz(80)) lbl_d2c_method.setMinimumWidth(_sz(80))
row_d2c_method.addWidget(lbl_d2c_method) row_d2c_method.addWidget(lbl_d2c_method)
self.capture_d2c_method_combo = QComboBox() self.capture_d2c_method_combo = QComboBox()
self.capture_d2c_method_combo.setMinimumWidth(_sz(220)) self.capture_d2c_method_combo.setMinimumWidth(_sz(260))
self.capture_d2c_method_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.capture_d2c_method_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.capture_d2c_method_combo.addItem("逆向投影(SDK 同等)", "sdk") self.capture_d2c_method_combo.addItem("逆向投影(SDK 同等)", "sdk")
self.capture_d2c_method_combo.addItem("正向投影 + Splat 填充", "forward") self.capture_d2c_method_combo.addItem("正向投影 + Splat 填充", "forward")
@@ -1282,7 +1286,7 @@ class MainWindow(QMainWindow):
lbl_splat = QLabel("Splat:") lbl_splat = QLabel("Splat:")
self.capture_splat_combo = QComboBox() self.capture_splat_combo = QComboBox()
self.capture_splat_combo.setMinimumWidth(_sz(80)) self.capture_splat_combo.setMinimumWidth(_sz(100))
self.capture_splat_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.capture_splat_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
for val, label in [(0, "关闭"), (1, "3×3"), (2, "5×5"), (3, "7×7")]: for val, label in [(0, "关闭"), (1, "3×3"), (2, "5×5"), (3, "7×7")]:
self.capture_splat_combo.addItem(label, val) self.capture_splat_combo.addItem(label, val)
@@ -1384,7 +1388,7 @@ class MainWindow(QMainWindow):
combo_row = QHBoxLayout() combo_row = QHBoxLayout()
combo_row.addWidget(QLabel("RGB:")) combo_row.addWidget(QLabel("RGB:"))
self.d2c_color_combo = QComboBox() self.d2c_color_combo = QComboBox()
self.d2c_color_combo.setMinimumWidth(_sz(340)) self.d2c_color_combo.setMinimumWidth(_sz(380))
self.d2c_color_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.d2c_color_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.d2c_color_combo.setMaxVisibleItems(15) self.d2c_color_combo.setMaxVisibleItems(15)
self.d2c_color_combo.setStyleSheet(COMBO_CSS) self.d2c_color_combo.setStyleSheet(COMBO_CSS)
@@ -1393,7 +1397,7 @@ class MainWindow(QMainWindow):
combo_row.addSpacing(12) combo_row.addSpacing(12)
combo_row.addWidget(QLabel("深度:")) combo_row.addWidget(QLabel("深度:"))
self.d2c_depth_combo = QComboBox() self.d2c_depth_combo = QComboBox()
self.d2c_depth_combo.setMinimumWidth(_sz(340)) self.d2c_depth_combo.setMinimumWidth(_sz(380))
self.d2c_depth_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.d2c_depth_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.d2c_depth_combo.setMaxVisibleItems(15) self.d2c_depth_combo.setMaxVisibleItems(15)
self.d2c_depth_combo.setStyleSheet(COMBO_CSS) self.d2c_depth_combo.setStyleSheet(COMBO_CSS)
@@ -1445,7 +1449,7 @@ class MainWindow(QMainWindow):
step4_lbl.setStyleSheet(f"color:#8ec8ff; font-weight:bold; font-size:{_fs(12)}px;") step4_lbl.setStyleSheet(f"color:#8ec8ff; font-weight:bold; font-size:{_fs(12)}px;")
step4_row.addWidget(step4_lbl) step4_row.addWidget(step4_lbl)
self.d2c_method_combo = QComboBox() self.d2c_method_combo = QComboBox()
self.d2c_method_combo.setMinimumWidth(_sz(220)) self.d2c_method_combo.setMinimumWidth(_sz(260))
self.d2c_method_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.d2c_method_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.d2c_method_combo.addItem("正向投影 + Splat 填充", "forward") self.d2c_method_combo.addItem("正向投影 + Splat 填充", "forward")
self.d2c_method_combo.addItem("逆向投影(SDK 同等)", "sdk") self.d2c_method_combo.addItem("逆向投影(SDK 同等)", "sdk")
@@ -1454,7 +1458,7 @@ class MainWindow(QMainWindow):
step4_row.addWidget(self.d2c_method_combo) step4_row.addWidget(self.d2c_method_combo)
self.d2c_splat_combo = QComboBox() self.d2c_splat_combo = QComboBox()
self.d2c_splat_combo.setMinimumWidth(_sz(80)) self.d2c_splat_combo.setMinimumWidth(_sz(100))
self.d2c_splat_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.d2c_splat_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
for val, label in [(0, "关闭"), (1, "3×3"), (2, "5×5"), (3, "7×7")]: for val, label in [(0, "关闭"), (1, "3×3"), (2, "5×5"), (3, "7×7")]:
self.d2c_splat_combo.addItem(label, val) self.d2c_splat_combo.addItem(label, val)