把一段 egocentric 视频变成 3D 标注:一步一步
用 Holi-Spatial pipeline 处理 HD-EPIC 里一段 60 秒的厨房视频,从原始鱼眼画面一直做到带描述的 3D box 和空间问答题。下面是每一步做了什么、怎么做的、结果长什么样。
选片段、选帧
HD-EPIC · Aria MPS SLAM
从 28 分钟的视频里取 2–62 秒,选出 100 帧清晰且互相有重叠的关键帧。
怎么做的
- 把 60 秒切成 100 个 0.6 秒的 bin。
- 从 SLAM 轨迹读出每一帧的角速度和线速度。
- 每个 bin 里选速度最小的一帧,运动模糊最少。
选这一段是因为人在厨房里走动,相机位移大(约 2.4 m × 1.5 m),视角有重叠。

相机位姿 + 鱼眼去畸变
自写 adapter:hdepic_to_dl3dv.py
HD-EPIC 是 Aria 鱼眼视频,pipeline 只吃 pinhole 图 + 相机参数。这一步把视频转成它认识的 DL3DV 布局。
怎么做的
- 位姿:用数据集自带的 Aria MPS closed-loop SLAM 轨迹(1 kHz,单位米),通过
mp4_to_vrs_time_ns.csv对齐到每一帧,再乘上标定里的T_Device_Camera。 - 去畸变:标定是 2816 分辨率的 Fisheye624 模型,而 mp4 是 1408 且相对传感器顺时针转了 90°。对输出 pinhole 图的每个像素反算它在鱼眼图里的位置,再
cv2.remap。 - 输出:1024×1024、f=560 的 pinhole 图,每帧一个
.npz(intrinsic3×3,pose4×4 camera-to-world)。
验证:在相邻帧上做 SIFT 匹配,用给定位姿算对极误差。旋转方向假设对了是 3.2 px,假设反了是 76 px。
python hdepic_to_dl3dv.py --video P08-20240620-180825.mp4 \
--time-csv P08-20240620-180825_mp4_to_vrs_time_ns.csv \
--slam-dir slam_P08/10/slam --out-scene pilot/dl3dv/<scene> \
--t-start 2 --t-end 62 --n-frames 100dl3dv 字样,几个脚本靠路径字符串判断数据集类型。
深度和初始点云
Depth Anything 3 · DA3NESTED-GIANT-LARGE
给每一帧估计深度,并融合成一个带颜色的初始点云。
怎么做的
100 帧连同内外参一次性送进 DA3。align_to_input_ext_scale=True 让预测深度对齐到 SLAM 位姿的尺度,所以深度单位是米。深度按置信度 0.3 过滤后反投影,下采样到 400 万点。
验证:把一帧的深度重投影到相邻帧,和相邻帧自己的深度比较,相对差异中位数 1.8%。
SCENE_DIR="$HS_DATA/$HS_SCENE" bash run_da3.shrefs/main 才能离线按 repo id 加载。

Gaussian 几何优化
PGSR(Holi-Spatial 自带的修改版)
以 DA3 点云为初始化、DA3 深度为监督,优化出一个几何更干净的 Gaussian 场景。
怎么做的
训练 30000 iter。除了常规的 photometric loss,PGSR 还有单视图法向一致、多视图几何一致两类约束,所以渲染出来的深度和法向比普通 3DGS 平整。
python PGSR/train.py -s "$HS_DATA/$HS_SCENE" -i rgb \
-m "$HS_RUN/gs/$HS_SCENE" --resolution 1dense/ 下找。不加 symlink 的话训练照样能跑,只是静默地没有深度监督。第一次就这样白跑了一遍。另外 gcc 13 编译 rasterizer 要补 #include <cstdint>。
Mesh 和一致性 mask
TSDF fusion(Open3D)
从 Gaussian 里提取 mesh,后面用 mesh 的深度把 2D mask 抬到 3D。
怎么做的
- 渲染每一帧的 plane depth。
- TSDF fusion:voxel 6 mm,最大深度 4 m。
- 只保留最大连通块,去掉漂浮碎片。
- 再把 mesh 深度和 Gaussian 深度逐帧比较,差异大的像素标成不可信,得到每帧一张一致性 mask。
DATA_ROOT="$HS_DATA" OUTPUT_ROOT="$HS_RUN/gs" NUM_GPUS=1 MAX_PARALLEL=1 bash mesh.sh

发现物体类别
Qwen3-VL-30B-A3B-Instruct · vLLM
让 VLM 看每一帧,列出里面有哪些物体类别,作为下一步分割的 text prompt。
怎么做的
逐帧提问。每次把前面帧已经出现的类别一起给它,要求同一种东西复用同一个词,避免 sofa / couch 这样的同义词分裂。wall / floor / ceiling 固定加入。
100 帧合并后 43 类:
vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct --port 8000
python classic_vllm.py --data-root "$HS_DATA" --scene "$HS_SCENE" \
--max-images 100 --api-base http://127.0.0.1:8000/v1 \
--output-dir "$HS_RUN/classes"2D 分割
SAM3(text-prompted)
对每一帧,用上一步该帧的类别列表逐个作为文本提示,分割出所有实例。
怎么做的
置信度阈值 0.6。每个 mask 记录 label、score、bbox 和 RLE 编码,汇总在 mask_index.json。数量最多的是 container 645、bottle 202、wall 148、shelf 140、bowl 122。
python sam3.py --data-root "$HS_DATA" \
--scene-json "$HS_RUN/classes/$HS_SCENE.json" \
--sam3-checkpoint "$HS_SAM_CKPT" --save-mask-images \
--output-dir "$HS_RUN/masks"sam3/ 缺 train 子包,要从官方 SAM3 仓库拷过来;BPE 词表要放到 assets/。
抬到 3D + 复核
mesh depth · Gemini 3 Flash verifier
把每个 2D mask 变成 3D 点,跨帧合并成 instance,拟合 3D box,再让 VLM 复核拿不准的那些。
怎么做的
- 反投影:mask 先腐蚀 15 px 去掉边缘,再用 mesh 深度把像素变成 3D 点。
- 合并:同一类别、不同帧的点云如果 3D 重叠够大(交集 / 较小 box 体积 ≥ 0.30),就认为是同一个物体。
- 分三档:按该 instance 最高的 mask 分数,≥ 0.9 直接保留,< 0.75 丢弃,中间的送去复核。
- 复核:给 VLM 三张图(原图、mask 叠加图、放大图),问它「这个 mask 是不是一个完整的、类别正确的单个物体」,返回结构化 JSON。
- 重力对齐:用 floor 的法向把 box 摆正。
复核的例子(Gemini 的原话):
| 类别 | 判定 | 理由 |
|---|---|---|
| cabinet | REJECT | The mask only covers a small triangular fragment of the cabinet door and is cut off by the image crop. |
| cabinet | ACCEPT | The mask correctly identifies a single, complete upper kitchen cabinet door. |
| picture | ACCEPT | The mask correctly identifies a single, complete picture attached to the refrigerator. |
| shelf | REJECT | The mask only covers the front edge of the shelf rather than the entire visible surface. |
| shelf | ACCEPT | The mask correctly identifies a single complete refrigerator door shelf. |
| bottle | REJECT | The mask only covers a thin vertical sliver of the bottle rather than its full visible silhouette. |
| bottle | ACCEPT | The mask correctly identifies a single, complete soap dispenser bottle. |
| bowl | REJECT | The mask only covers a small fragment of the bowl's rim and is highly incomplete. |
| container | REJECT | The mask only covers the rim of the container and excludes the base and the contents. |
| container | ACCEPT | The mask correctly identifies a single, complete jar container on the refrigerator shelf. |
关于 verifier:原 pipeline 用 Gemini 2.5 Flash,该模型对新 key 已下线,这里用的是 gemini-3-flash-preview(112 次调用,约 40 万 tokens)。同一批候选也用本地 Qwen3-VL 跑过一遍:它接受 55、拒绝 57,和 Gemini 只在 59/112(53%)个候选上判定一致,分歧最多的是 hand、paper、container。
python 3d_bounding_instance_gs_rerun_da3.py --scene "$HS_SCENE" \
--data-root "$HS_DATA" --mask-root "$HS_RUN/masks" \
--scene-json-dir "$HS_RUN/classes" --model-path "$HS_RUN/gs/$HS_SCENE" \
--depth-source mesh --output-dir "$HS_RUN/boxes" --rerun false
python postprocess_3d_bbox_aabb.py --input_dir "$HS_RUN/boxes" \
--output_dir "$HS_RUN/boxes_aligned" --floor_label floor
给每个物体写描述
Qwen3-VL
为每个 instance 生成一句外观描述,后面出题时用它来指代物体。
怎么做的
取该 instance 置信度最高的那一帧,把 mask 标出来,让 Qwen3-VL 写一句包含颜色、材质、形状、位置的描述。
| label | 尺寸 (m) | 描述 |
|---|---|---|
| cabinet | 0.68 × 0.50 × 0.99 | white cabinet with vertical handle, smooth surface, rectangular shape |
| light | 0.06 × 0.05 × 0.01 | Circular ceiling light fixture, white, with a smooth surface and a red outline marking its boundary |
| refrigerator | 0.96 × 1.46 × 2.23 | white refrigerator with glass door and internal shelves, open with contents visible |
| sofa | 1.37 × 2.44 × 0.46 | dark grey sofa with rectangular backrest and armrests, upholstered fabric texture |
| paper | 0.00 × 0.08 × 0.04 | rectangular paper with red border, dark background, and small white text, attached to a white surface |
| paper | 0.17 × 0.02 × 0.09 | white paper with red border, rectangular shape, attached to surface |
| bowl | 0.16 × 0.12 × 0.03 | small clear plastic container with red contents, next to a blue bowl |
| container | 0.06 × 0.05 × 0.09 | white plastic container with red lid, on top shelf of refrigerator door |
| container | 0.07 × 0.07 × 0.12 | white plastic container with rounded top and smooth surface |
| container | 0.06 × 0.07 × 0.01 | small clear glass jar with dark contents, next to a white cylindrical object |
| pepper | 0.07 × 0.16 × 0.11 | yellow bell pepper, smooth glossy surface, heart-shaped cut, green stem attached |
| knife | 0.01 × 0.01 × 0.11 | dark grey knife with smooth handle, metallic blade, and rounded tip |
python qa_generation/generate_3d_instance_description.py \
--input_dir "$HS_RUN/boxes_aligned" --output_dir "$HS_RUN/described" \
--dataset_root "$HS_DATA" --scene_id_list "$HS_SCENE" \
--vllm-api-url http://127.0.0.1:8000/v1/chat/completions生成 spatial QA
模板 + 3D 几何 + Qwen3-VL 过滤
用 3D box 和相机位姿直接算出答案,生成两视图的空间问答题。
怎么做的
- Covisibility:先算哪些帧对看到的是同一片区域。把一帧的深度重投影到另一帧,深度一致(5% 以内)的像素比例就是 covisibility。9900 个帧对里 1583 个 > 0.25。
- 出题:采样帧对,套模板。相机运动题直接用两帧位姿算;物体题用 box 中心和尺寸算。物体用上一步的描述来指代。
- 过滤:如果一句描述在图里能对上不止一个物体(比如冰箱里好几个红罐子),题目就有歧义。让 Qwen3-VL 看图判断,丢掉有歧义的。
- 转格式:转成 LLaMA-Factory 的
dataset.jsonl。
出题前去掉了 21 个 hand instance,手是动的,不能当静态物体。
| 题型 | 生成 | 过滤后 |
|---|---|---|
| object_distance | 489 | 135 |
| cam_translation | 121 | 121 |
| object_direction | 193 | 65 |
| object_height_or_length | 133 | 48 |
| object_cross_direction | 133 | 48 |
| cam_rotation | 44 | 44 |
| object_relpos | 210 | 29 |
| object_proxy_direction | 296 | 28 |
python compute_covisibility.py "$HS_DATA/$HS_SCENE" "$HS_RUN/wai"
python qa_generation/generate_two_view_qa.py --scene-id "$HS_SCENE" \
--data-root "$HS_DATA" --wai-root "$HS_RUN/wai" \
--bbox-json-folder "$HS_RUN/described_nohand" \
--marker-types language_description --output "$HS_RUN/qa"
python qa_generation/filter_qa_repeat_descriptions.py --data-root "$HS_DATA" \
--bbox-json-folder "$HS_RUN/described_nohand" --output "$HS_RUN/qa" \
--output-filter-repeat "$HS_RUN/qa_filtered" --scenes "$HS_SCENE" \
--vllm-api-url http://127.0.0.1:8000/v1/chat/completions
python qa_generation/convert_qa_to_llamafactory.py --image-root "$HS_DATA" \
--qa-json-dir "$HS_RUN/qa_filtered" --output-dir "$HS_RUN/llamafactory".jpg 和 DL3DV 路径,各补了一行。
每种题型一个例子


What is the primary camera motion direction from view A to view B in view A's coordinate?
A) forward
B) backward
C) right
D) left
Reply with only the option letter (A/B/C/D).
答案:A


Given view A and view B, consider the relative rotation from A to B expressed in view A's camera frame. Which option best describes the rotation direction using TWO components: yaw and pitch?
A. turn left and tilt down
B. turn right and tilt up
C. turn right and tilt down
D. turn left and tilt up
答案:A


In image A, the object described as "white refrigerator with glass door and internal shelves, open with contents visible" is the refrigerator.
Locate the same physical object in image B.
Estimate the 3D metric distance (in meters)
from the camera position of image B (camera center)
to the "refrigerator" (to the object surface/center point).
This is NOT pixel distance to the image center.
Return only one number in meters (e.g., 0.7).
Output format: <answer>NUMBER</answer>.
答案:0.7 m


In image A, the object described as "green-lidded container with white label, cylindrical shape, on countertop" is the container.
Which direction is the "container" relative to you when taking image B?
A. Front
B. Back
C. Left
D. Right
答案:A. Front


In image A, the object described as "dark grey metallic faucet with curved spout and smooth finish" is the faucet.
In image B, the object described as "dark grey metallic sink with rectangular basin and visible drain, integrated into black countertop" is the sink.
Which object is taller (consider the top of the objects)? faucet or sink?
A. Both are about the same height
B. sink
C. Cannot tell
D. faucet
答案:B. sink


In image A, the object described as "white plastic spray bottle with blue cap and red outline" is the bottle.
In image B, the object described as "white plate with red circular design in center" is the plate.
The direction of bottle region relative to image A is east.
What is the direction of plate relative to image B?
A. South-West
B. South
C. South-East
D. North-East
答案:C. South-East


In image B, the object described as "white plastic bottle with narrow neck and smooth surface, next to red sauce bottle" is the container.
In image B, the object described as "blue ceramic bowl containing dark berries" is the bowl.
In image A, the object described as "dark blue rug with smooth texture, rectangular shape, and slightly curved edge" is the rug.
The direction of container relative to bowl is south.
What is the direction of rug relative to bowl?
A. North
B. East
C. West
D. South-East
答案:A. North


In image A, the object described as "dark grey sofa with rectangular backrest and armrests, upholstered fabric texture" is the sofa.
In image B, the object described as "Clear plastic cup with orange contents, cylindrical shape, smooth surface" is the container.
In image A, the object described as "rectangular paper with handwritten text and photo, attached to refrigerator door" is the paper.
You are positioned at sofa and face container.
In which direction is paper relative to you?
A. Left
B. Right
C. Front-Left
D. Front
答案:D. (Front)
和 ground truth 对比
HD-EPIC object movement 标注
HD-EPIC 对每次物体被拿起或放下都标了物体名、2D 框和 3D 世界坐标。这 60 秒里有 23 个这样的 GT 点(11 个物体),拿来检查预测的 3D box 对不对。
怎么做的
- 确认坐标系一致:把 GT 的 3D 点用我们的相机模型投影回它自己那一帧,19/23 落在 GT 的 2D 框里,说明 GT 和预测在同一个 SLAM 世界坐标系。
- 打分:对每个 GT 点,找类别正确的预测 box 里离它最近的一个,量 GT 点到 box 表面的距离。
- 逐步加入 GT 信息,看每种标注能换来多少对齐。
| 档位 | 用了哪些 GT 信息 | 5 cm 内 | 15 cm 内 | 没有同类 instance |
|---|---|---|---|---|
| E0 baseline | 无 | 4 | 7 | 11 |
| E1 词表 | 物体名(GT 名 + narration 名词),去掉 hand | 9 | 12 | 4 |
| E2 词表 + 事件帧 | 再加 GT 拿起/放下时刻的 45 帧 | 9 | 12 | 4 |
| E3 同上,DA3 深度 | 换成逐帧 DA3 深度 | 10 | 12 | 4 |
| E4 oracle | 时间 + 2D 框 + 名字,单帧直接 lift | 18 | 20 | 3(视野外) |
看出了什么
- 物体名最划算。只补词表,5 cm 命中从 4 到 9。baseline 里 air fryer、baking tray、kitchen scale 这些根本没被列为类别。
- 只加帧没用。事件帧上 SAM3 分出了 mask,但跨帧合并和 verifier 又把它们丢了。air fryer 有 8 个 mask,合并成 3 个候选,全被拒绝。
- 物体被移动后跟不上。tupperware、baking tray 等始终差 26–59 cm:同类 box 存在,但在物体的另一个停留位置。
- Oracle 剩下的差距。3 个是去畸变时裁掉了鱼眼画面底部,GT 框落在视野外;2 个是 cucumber,差 8–9 cm。
- 换 verifier 不影响对齐。Gemini 版和 Qwen 版在这 23 个点上的得分相同。
数据集最少要有什么标注才能对齐
- 标定好的相机 + 每帧 metric 位姿,且和 GT 在同一个世界坐标系。
- 物体名(开放词表即可)。
- 时间锚点:物体停在某个位置的时刻。
- 该时刻的 2D 框或 mask:名字分割不出来时兜底,同类多个时指明是哪一个。
- 跨时间的 instance ID:同一物体多次移动要能关联。
- 3D 参考:至少一个点;要评 box 大小和朝向则需要 3D box。
样本只有 23 个点、一段 60 秒,且只覆盖被移动过的物体,数字是方向性的。
已知问题
- 场景不是静态的。这 60 秒里冰箱门、烤箱门在开关,手和被拿的物体在动,而 pipeline 假设场景静止。153 个 instance 里 21 个是 hand;cucumber、pepper、meat 这类被拿过的物体,box 只代表某一时刻。
- 有明显错误的 box。sofa、refrigerator 被拉得很大,extent 最大的一个有 5.25 m。
- 类别漏检。oven、kettle、coffee machine、air fryer 没有 box。
- Verifier 版本。用的是 Gemini 3 Flash,不是原 pipeline 的 2.5 Flash;它和 Qwen3-VL 的判定只有 53% 一致,说明这一步本身不稳定。
- 没跑 functional-region 分支(区域级标注和对应的 QA)。
- QA 的距离是到 box 中心,不是到最近表面。
- 这些是 pseudo-label,不是测量得到的 ground truth。
版本
| holi-spatial | 606dbb3 + 本地补丁 |
| Depth-Anything-3 | 3d835ec,权重 DA3NESTED-GIANT-LARGE@8615eef |
| SAM3 | facebook/sam3@3c879f3 |
| Qwen3-VL | Qwen3-VL-30B-A3B-Instruct@9c4b90e,vLLM 0.11.0 |
| 环境 | Python 3.12,torch 2.8.0+cu128,open3d 0.19.0,RTX PRO 6000 Blackwell 96 GB |