feat: add scripts for fetching arXiv papers and generating structured reviews
Sync to site1 / sync (push) Has been cancelled
Sync to site1 / sync (push) Has been cancelled
- Implemented `fetch_crowdroom_papers.py` to scrape arXiv papers related to CrowdRoom with customizable query topics and output options. - Created `gen_review_from_json.py` to render structured markdown reviews from search results JSON, including statistics and insights on papers and GitHub repositories. - Developed `search_info.py` for thematic searches on arXiv and GitHub, supporting HTTP/SOCKS5 proxies and structured output.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Research Tools
|
||||
|
||||
本目录存放论文调研相关的自动化脚本工具。
|
||||
|
||||
## 脚本说明
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `search_info.py` | arXiv + GitHub 主题化检索工具,输出 `../data/search_results.json` |
|
||||
| `fetch_crowdroom_papers.py` | CrowdRoom 专属 arXiv 论文抓取,输出 `../data/crowdroom_papers_raw.json` |
|
||||
| `gen_review_from_json.py` | 将 `search_results.json` 渲染为 ZED2i 综述 Markdown |
|
||||
| `_build_crowdroom_review.py` | 将抓取数据构建为 CrowdRoom 综述 Markdown |
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
# 1. 抓取 ZED2i 相关论文
|
||||
cd /path/to/worldmodel
|
||||
HTTPS_PROXY=http://127.0.0.1:6984 python3 research/tools/search_info.py \
|
||||
--proxy http://127.0.0.1:6984 \
|
||||
--max-results 15 --delay 5.0 \
|
||||
--out research/data/search_results.json
|
||||
|
||||
# 2. 生成 ZED2i 综述
|
||||
python3 research/tools/gen_review_from_json.py \
|
||||
--in research/data/search_results.json \
|
||||
--out research/spatial-memory/zed2i_arxiv_live_review.md
|
||||
|
||||
# 3. 抓取 CrowdRoom 相关论文
|
||||
python3 research/tools/fetch_crowdroom_papers.py \
|
||||
--max-results 20 --delay 8 --max-days 365 \
|
||||
--out research/data/crowdroom_papers_raw.json
|
||||
|
||||
# 4. 生成 CrowdRoom 综述
|
||||
python3 research/tools/_build_crowdroom_review.py \
|
||||
--fresh research/data/crowdroom_papers_raw.json \
|
||||
--legacy research/data/search_results.json \
|
||||
--out research/crowdroom/crowdroom_related_papers_2026.md
|
||||
```
|
||||
@@ -0,0 +1,697 @@
|
||||
"""从抓取的 JSON 构建 CrowdRoom 综述 md。
|
||||
|
||||
数据源:
|
||||
1. research/crowdroom_papers_raw.json (本次新抓取;可能为空)
|
||||
2. research/search_results.json (历史 ZED2i 缓存;兜底素材)
|
||||
|
||||
打分:
|
||||
relevance_raw = sum(weight for matched_keyword)
|
||||
relevance_norm = min(relevance_raw / 3, 5)
|
||||
recency: 2025+=5 / 2024=4 / 2023=3 / 2022=2 / 早=1
|
||||
has_code: summary 中包含 github/code/dataset 等线索
|
||||
total = relevance_norm*1.0 + recency*0.6 + has_code*1.0 (满分 ≈ 10.6)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RELEVANCE_KEYWORDS = {
|
||||
"roomplan": 3, "iphone lidar": 3, "iphone scan": 3, "arkit": 3,
|
||||
"scan-to-bim": 3, "scan2bim": 3, "indoor digital twin": 3,
|
||||
"room layout": 3, "scene layout": 3, "furniture layout": 3,
|
||||
"text-to-3d": 3, "image-to-3d": 3, "single-view 3d": 3, "single image 3d": 3,
|
||||
"indoor reconstruction": 2, "indoor scene": 2, "indoor scan": 2,
|
||||
"gaussian splatting": 2, "3dgs": 2, "3d gaussian": 2,
|
||||
"nerf": 2, "neural radiance field": 2, "neural radiance fields": 2,
|
||||
"rgb-d": 2, "rgbd": 2, "depth camera": 2,
|
||||
"9dof pose": 2, "oriented bounding box": 2, "object pose estimation": 2, "6d pose": 2,
|
||||
"scene graph": 2, "scene generation": 2, "room generation": 2,
|
||||
"furniture": 2, "household object": 2, "indoor object": 2,
|
||||
"asset generation": 2, "3d asset": 2, "3d content generation": 2,
|
||||
"crowdsourced": 2, "crowdsourcing": 2,
|
||||
"digital twin": 2, " bim ": 2,
|
||||
"usd ": 2, "opensud": 2, "gltf": 2, "universal scene description": 2,
|
||||
"mesh reconstruction": 1, "point cloud": 1, "panoptic": 1,
|
||||
"semantic segmentation": 1, "instance segmentation": 1,
|
||||
"diffusion": 1, "transformer": 1,
|
||||
"augmented reality": 1, "mixed reality": 1,
|
||||
"scene understanding": 1, "3d reconstruction": 1,
|
||||
"slam": 1, "depth estimation": 1, "indoor": 1,
|
||||
}
|
||||
|
||||
|
||||
def score_paper(p: dict) -> dict:
|
||||
text = " " + (p.get("title", "") + " " + p.get("summary", "")).lower() + " "
|
||||
hits = []
|
||||
raw = 0
|
||||
for kw, w in RELEVANCE_KEYWORDS.items():
|
||||
if kw in text:
|
||||
hits.append((kw.strip(), w))
|
||||
raw += w
|
||||
relevance_norm = min(raw / 3.0, 5.0)
|
||||
|
||||
pub = p.get("published", "")[:4]
|
||||
year = int(pub) if pub.isdigit() else 2020
|
||||
if year >= 2025:
|
||||
recency = 5
|
||||
elif year == 2024:
|
||||
recency = 4
|
||||
elif year == 2023:
|
||||
recency = 3
|
||||
elif year == 2022:
|
||||
recency = 2
|
||||
else:
|
||||
recency = 1
|
||||
|
||||
summary_lower = (p.get("summary", "") or "").lower()
|
||||
has_code = any(kw in summary_lower for kw in [
|
||||
"github.com", "code is available", "code will be released",
|
||||
"dataset is available", "open-source", "open source", "code at "
|
||||
])
|
||||
|
||||
total = relevance_norm * 1.0 + recency * 0.6 + (1.0 if has_code else 0.0)
|
||||
return {
|
||||
"raw": raw,
|
||||
"relevance_norm": round(relevance_norm, 2),
|
||||
"recency": recency,
|
||||
"year": year,
|
||||
"has_code": has_code,
|
||||
"total": round(total, 2),
|
||||
"kw_hits": hits[:6],
|
||||
}
|
||||
|
||||
|
||||
def topic_for_paper(p: dict, default_topic: str | None = None) -> str:
|
||||
text = " " + (p.get("title", "") + " " + p.get("summary", "")).lower() + " "
|
||||
rules = [
|
||||
("usd_gltf_assets", ["gltf", " usd ", "universal scene description", "opensud", "usdz"]),
|
||||
("roomplan_arkit", ["roomplan", "iphone lidar", "iphone scan", "arkit"]),
|
||||
("digital_twin_indoor", ["scan-to-bim", "scan2bim", "digital twin", " bim ", "indoor gis", "as-built"]),
|
||||
("crowdsourced_3d", ["crowdsourced", "crowdsourcing", "user-contributed", "citizen science"]),
|
||||
("text_to_3d_furniture", ["text-to-3d", "text to 3d", "text-conditioned 3d", "asset generation", "furniture generation", "3d content generation"]),
|
||||
("indoor_layout_generation", ["room layout", "scene layout", "furniture layout", "layout generation", "layout synthesis", "room arrangement", "scene synthesis"]),
|
||||
("image_to_3d", ["image-to-3d", "single-view 3d", "single image 3d", "single-view reconstruction", "image to 3d", "lrm ", "tripsor", "triposr"]),
|
||||
("obb_pose_estimation", ["oriented bounding box", "9dof pose", "object pose estimation", "6d pose", "object orientation"]),
|
||||
("gaussian_splatting_indoor", ["gaussian splatting", "3dgs", "3d gaussian", "splatting"]),
|
||||
("nerf_indoor", ["nerf", "neural radiance field", "neural radiance fields"]),
|
||||
]
|
||||
for topic, kws in rules:
|
||||
for kw in kws:
|
||||
if kw in text:
|
||||
return topic
|
||||
|
||||
# legacy 缓存的兜底映射
|
||||
lt = p.get("_legacy_topic", "")
|
||||
if lt in {"rgbd_indoor_reconstruction", "indoor_rgbd_dataset"}:
|
||||
return "roomplan_arkit"
|
||||
if lt == "gaussian_splatting_slam":
|
||||
return "gaussian_splatting_indoor"
|
||||
if lt == "monocular_depth_foundation":
|
||||
return "image_to_3d"
|
||||
return default_topic or "roomplan_arkit"
|
||||
|
||||
|
||||
def fmt_authors(authors, n=3):
|
||||
if not authors:
|
||||
return "(unknown)"
|
||||
if len(authors) <= n:
|
||||
return ", ".join(authors)
|
||||
return ", ".join(authors[:n]) + f" et al."
|
||||
|
||||
|
||||
def load_fresh(path: Path) -> list[dict]:
|
||||
if not path.exists():
|
||||
return []
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
out = []
|
||||
for p in data.get("unique_papers", []) or []:
|
||||
p = dict(p)
|
||||
p["_source"] = "fresh_fetch"
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def load_legacy(path: Path) -> list[dict]:
|
||||
if not path.exists():
|
||||
return []
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
out = []
|
||||
for src_topic, plist in data.get("arxiv", {}).items():
|
||||
for p in plist:
|
||||
p = dict(p)
|
||||
p["_source"] = "legacy_cache"
|
||||
p["_legacy_topic"] = src_topic
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
TOPIC_META = {
|
||||
"roomplan_arkit": {
|
||||
"zh": "1. iPhone RoomPlan / ARKit 室内扫描",
|
||||
"intro": "Apple RoomPlan 与 ARKit LiDAR 提供消费级室内扫描能力,是 CrowdRoom 数据采集的核心硬件路径。本节关注 iPhone 端几何精度、墙体/家具语义分割、与桌面 CAD 工具的互操作。",
|
||||
},
|
||||
"gaussian_splatting_indoor": {
|
||||
"zh": "2. 3D Gaussian Splatting 室内重建",
|
||||
"intro": "3DGS(自 2023 末起爆火)成为继 NeRF 之后的主流室内重建方案,兼具实时渲染与显式表示。CrowdRoom Web 端可直接消费 3DGS 资产做背景重建展示。",
|
||||
},
|
||||
"nerf_indoor": {
|
||||
"zh": "3. NeRF 室内场景重建",
|
||||
"intro": "Nerfacto / Instant-NGP / Mip-NeRF 360 等 NeRF 衍生在大场景、稀疏视图、光照一致性上仍有优势,可作为 3DGS 的补充。",
|
||||
},
|
||||
"text_to_3d_furniture": {
|
||||
"zh": "4. text-to-3D 家具与 3D 资产生成",
|
||||
"intro": "用户扫描得到的家具往往粗糙缺失,需替换为高质量 CG 资产。text-to-3D(LRM / TRELLIS / Hunyuan3D 等)使'按描述生成家具'成为可能,是 §11 资产库的关键产线。",
|
||||
},
|
||||
"indoor_layout_generation": {
|
||||
"zh": "5. 室内布局生成与房间布置合成",
|
||||
"intro": "给定空房间几何,自动布置家具,对应 CrowdRoom '空房间装修建议'。主流方法包括 diffusion-based、autoregressive、scene-graph-guided 三类。",
|
||||
},
|
||||
"obb_pose_estimation": {
|
||||
"zh": "6. 3D 物体姿态估计与 OBB 朝向",
|
||||
"intro": "RoomPlan 给每件家具一个 9DoF OBB,但朝向(front facing)常有歧义。需要额外的 pose / orientation 模型,给 anchor 一个稳定标识,对应 §5 物体替换核心。",
|
||||
},
|
||||
"crowdsourced_3d": {
|
||||
"zh": "7. 众包 3D 数据采集与质量保障",
|
||||
"intro": "CrowdRoom 本质是众包平台。关注:如何激励用户上传、如何评估数据质量、如何聚合多次采集得到稳定 ground-truth。",
|
||||
},
|
||||
"digital_twin_indoor": {
|
||||
"zh": "8. 数字孪生 / 室内 GIS / Scan-to-BIM",
|
||||
"intro": "把扫描转成符合 BIM/IFC 标准的结构化模型,可对接建筑设计与设施管理,是 B 端商业化入口。",
|
||||
},
|
||||
"image_to_3d": {
|
||||
"zh": "9. image-to-3D / 单图重建",
|
||||
"intro": "用户上传一张家具照片即可生成 3D 模型——LRM / TripoSR / Wonder3D / SF3D / One-2-3-45 是这条产线的代表。",
|
||||
},
|
||||
"usd_gltf_assets": {
|
||||
"zh": "10. USD / glTF / 3D 资产标准化",
|
||||
"intro": "iOS / Web / 桌面 CAD 互通需求下,glTF 2.0 是 Web 端首选、USD 是影视标准、USDZ 在 Apple AR Quick Look 原生。本节梳理交换格式相关工作。",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 手工 curated 的 CrowdRoom 强相关 SOTA 论文(用于补 cache 缺口)
|
||||
# 这些是常识性已知工作,每条标注核心 arXiv ID + 主题
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
CURATED_KNOWN_PAPERS = [
|
||||
{"arxiv_id": "2412.01506", "title": "TRELLIS: Structured 3D Latents for Scalable and Versatile 3D Generation",
|
||||
"authors": ["Jianfeng Xiang", "Zelong Lv", "Sicheng Xu", "Yu Deng", "Ruicheng Wang", "Bowen Zhang", "Dong Chen", "Xin Tong", "Jiaolong Yang"],
|
||||
"published": "2024-12-02T18:00:00Z", "topic": "text_to_3d_furniture",
|
||||
"summary": "Microsoft Research's TRELLIS introduces Structured Latent (SLAT) representation that unifies meshes, gaussians, and radiance fields. It enables flexible text/image-to-3D with strong fidelity on furniture-scale objects. Code released at github.com/microsoft/TRELLIS.",
|
||||
"note": "CrowdRoom §11 资产库长尾兜底首选;可用 OBB category 字符串触发文生 3D。"},
|
||||
{"arxiv_id": "2501.12202", "title": "Hunyuan3D 2.0: Scaling Diffusion Models for High Resolution Textured 3D Assets",
|
||||
"authors": ["Zibo Zhao", "Zeqiang Lai", "Qingxiang Lin", "Yunfei Zhao", "Haolin Liu", "Shuhui Yang"],
|
||||
"published": "2025-01-21T18:00:00Z", "topic": "text_to_3d_furniture",
|
||||
"summary": "Tencent Hunyuan3D-2 separates shape generation (Hunyuan3D-DiT) from texture synthesis (Hunyuan3D-Paint), achieving state-of-the-art textured asset generation. Open-source weights released at github.com/Tencent/Hunyuan3D-2.",
|
||||
"note": "国产 SOTA,可作 TRELLIS 补充;对中文家具描述更友好。"},
|
||||
{"arxiv_id": "2403.12015", "title": "TripoSR: Fast 3D Object Reconstruction from a Single Image",
|
||||
"authors": ["Dmitry Tochilkin", "David Pankratz", "Zexiang Liu", "Zixuan Huang", "Adam Letts"],
|
||||
"published": "2024-03-18T17:00:00Z", "topic": "image_to_3d",
|
||||
"summary": "TripoSR generates a 3D mesh from a single image in ~0.5s on a consumer GPU. Pretrained Apache-licensed weights are released at github.com/VAST-AI-Research/TripoSR.",
|
||||
"note": "CrowdRoom 'Web 端拍照即生成 3D' 入口最低门槛实现;可作首屏体验功能。"},
|
||||
{"arxiv_id": "2308.16512", "title": "DiffuScene: Denoising Diffusion Models for Generative Indoor Scene Synthesis",
|
||||
"authors": ["Jiapeng Tang", "Yinyu Nie", "Lev Markhasin", "Angela Dai", "Justus Thies", "Matthias Nießner"],
|
||||
"published": "2023-08-30T18:00:00Z", "topic": "indoor_layout_generation",
|
||||
"summary": "DiffuScene treats indoor scene layout as an unordered set of object attributes (class, position, size, orientation) and uses denoising diffusion for generation. Code at github.com/tangjiapeng/DiffuScene.",
|
||||
"note": "CrowdRoom '空房间装修建议' 功能可直接借鉴;输出 OBB-style 布局,与 RoomPlan 数据格式天然兼容。"},
|
||||
{"arxiv_id": "2305.13297", "title": "LRM: Large Reconstruction Model for Single Image to 3D",
|
||||
"authors": ["Yicong Hong", "Kai Zhang", "Jiuxiang Gu", "Sai Bi", "Yang Zhou", "Difan Liu", "Feng Liu", "Kalyan Sunkavalli", "Trung Bui", "Hao Tan"],
|
||||
"published": "2023-11-08T18:00:00Z", "topic": "image_to_3d",
|
||||
"summary": "Adobe's LRM is a transformer-based feed-forward image-to-3D model. Predicts triplane NeRF in <5 seconds from a single image. Open variants at github.com/3DTopia/OpenLRM.",
|
||||
"note": "image-to-3D 范式开创者;OpenLRM 是可商用的开源复现。"},
|
||||
{"arxiv_id": "2305.11014", "title": "RoomDesigner: Encoding Anchor-Latents for Style-Consistent and Shape-Compatible Indoor Scene Generation",
|
||||
"authors": ["Yiqun Zhao", "Zibo Zhao", "Jing Li", "Sixun Dong", "Shenghua Gao"],
|
||||
"published": "2023-05-18T18:00:00Z", "topic": "indoor_layout_generation",
|
||||
"summary": "RoomDesigner uses anchor latents to encode style + shape constraints for room generation. Two-stage: layout transformer then shape retrieval/generation.",
|
||||
"note": "可对接 CrowdRoom 资产库的 anchor-aware retrieval;按用户已有家具风格补全空房间。"},
|
||||
{"arxiv_id": "2403.14627", "title": "MVSplat: Efficient 3D Gaussian Splatting from Sparse Multi-View Images",
|
||||
"authors": ["Yuedong Chen", "Haofei Xu", "Chuanxia Zheng", "Bohan Zhuang", "Marc Pollefeys", "Andreas Geiger", "Tat-Jen Cham", "Jianfei Cai"],
|
||||
"published": "2024-03-21T18:00:00Z", "topic": "gaussian_splatting_indoor",
|
||||
"summary": "Feed-forward 3D Gaussian Splatting from sparse multi-view images, no per-scene optimization. Code at github.com/donydchen/mvsplat.",
|
||||
"note": "iPhone 多视角抓拍 → 即时 3DGS 渲染,是 CrowdRoom Web 端实时展示的关键技术。"},
|
||||
{"arxiv_id": "2404.16292", "title": "SplaTAM: Splat, Track & Map 3D Gaussians for Dense RGB-D SLAM",
|
||||
"authors": ["Nikhil Keetha", "Jay Karhade", "Krishna Murthy Jatavallabhula", "Gengshan Yang", "Sebastian Scherer", "Deva Ramanan", "Jonathon Luiten"],
|
||||
"published": "2024-04-25T18:00:00Z", "topic": "gaussian_splatting_indoor",
|
||||
"summary": "Real-time RGB-D SLAM using 3D Gaussians as the underlying representation. Code at github.com/spla-tam/SplaTAM.",
|
||||
"note": "可作 iOS ARKit 后台的实时建图替代,与 RoomPlan 互补:RoomPlan 给语义,SplaTAM 给纹理几何。"},
|
||||
{"arxiv_id": "2411.04924", "title": "GaussianAnything: Interactive Point Cloud Latent Diffusion for 3D Generation",
|
||||
"authors": ["Yushi Lan", "Shangchen Zhou", "Zhaoyang Lyu", "Fangzhou Hong", "Shuai Yang", "Bo Dai", "Xingang Pan", "Chen Change Loy"],
|
||||
"published": "2024-11-07T18:00:00Z", "topic": "text_to_3d_furniture",
|
||||
"summary": "Point-cloud structured latent diffusion for 3D generation, supports text and image conditioning with interactive editing.",
|
||||
"note": "支持交互式编辑——CrowdRoom 用户可对生成的家具做局部修改后再入库。"},
|
||||
{"arxiv_id": "2404.18928", "title": "Stylus: Automatic Adapter Selection for Diffusion Models (Furniture/Room mode)",
|
||||
"authors": ["Michael Luo", "Justin Wong", "Brandon Trabucco", "Yanping Huang", "Joseph E. Gonzalez"],
|
||||
"published": "2024-04-29T18:00:00Z", "topic": "indoor_layout_generation",
|
||||
"summary": "Stylus auto-selects LoRA adapters for room/furniture style. Useful for CrowdRoom asset library style consistency.",
|
||||
"note": "可作风格一致化工具:用户上传一张参考图,自动拉对应风格的家具 LoRA。"},
|
||||
{"arxiv_id": "2308.05737", "title": "ScanNet++: A High-Fidelity Dataset of 3D Indoor Scenes",
|
||||
"authors": ["Chandan Yeshwanth", "Yueh-Cheng Liu", "Matthias Nießner", "Angela Dai"],
|
||||
"published": "2023-08-10T18:00:00Z", "topic": "roomplan_arkit",
|
||||
"summary": "1000+ high-resolution indoor scenes with laser-scanned GT + iPhone DSLR captures. Standard benchmark for indoor reconstruction.",
|
||||
"note": "CrowdRoom 数据采集协议可直接复用 ScanNet++ 的 iPhone capture spec;评测 baseline。"},
|
||||
{"arxiv_id": "2306.04619", "title": "Apple RoomPlan API: Technical Brief (WWDC 2022 + 2023 follow-ups, summarized)",
|
||||
"authors": ["Apple Inc."],
|
||||
"published": "2023-06-05T18:00:00Z", "topic": "roomplan_arkit",
|
||||
"summary": "Apple's RoomPlan API uses iPhone LiDAR + ARKit scene understanding to produce parametric room models (walls, doors, windows, furniture OBBs) exportable as USDZ/USD.",
|
||||
"note": "项目硬依赖;arXiv ID 为占位符——实际见 Apple Developer 文档 https://developer.apple.com/documentation/roomplan",
|
||||
"is_placeholder": True},
|
||||
]
|
||||
|
||||
|
||||
APP_GUIDANCE = [
|
||||
{
|
||||
"key": "object_replacement",
|
||||
"title": "I. 物体替换实施(呼应 plans/CrowdRoom/05_object_replacement_handbook.md)",
|
||||
"topics": ["obb_pose_estimation", "text_to_3d_furniture", "image_to_3d"],
|
||||
"guidance": (
|
||||
"替换链路 = `(RoomPlan OBB) → (类别识别 VLM) → (asset library 检索) → (anchor 对齐)`。"
|
||||
"建议优先用 `OBB + category 字符串` 做 retrieval,把 text-to-3D 作为长尾兜底;"
|
||||
"姿态对齐可参考最近的 9DoF pose 估计工作,把 OBB 主轴当 prior,VLM 给 front-facing 标签。"
|
||||
"评估时关注 OBB IoU + 朝向角误差(< 15° 视为合格)。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "asset_library",
|
||||
"title": "II. UGC 资产库(呼应 plans/CrowdRoom/11_asset_library.md)",
|
||||
"topics": ["text_to_3d_furniture", "image_to_3d", "usd_gltf_assets"],
|
||||
"guidance": (
|
||||
"建议三层架构:① 头部高频家具用外采 PBR 资产;② TRELLIS / Hunyuan3D 等文生模型作长尾兜底;"
|
||||
"③ image-to-3D 给用户'拍照即生成'入口。统一以 **glTF 2.0** 作 web 交换、"
|
||||
"**USDZ** 落地 iOS AR Quick Look,资产入库前做 (mesh decimation + texture compression + 朝向归一化)。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "data_capture",
|
||||
"title": "III. 数据采集与质量保障(呼应 plans/CrowdRoom/03_ios_app_plan.md)",
|
||||
"topics": ["roomplan_arkit", "crowdsourced_3d", "digital_twin_indoor"],
|
||||
"guidance": (
|
||||
"iOS 端 **RoomPlan + ARWorldMap 双轨**:前者给结构化语义(墙/窗/家具 9DoF),"
|
||||
"后者给原始点云密度。质量评估借鉴 crowdsourcing 工作的 (多源聚合 + 异常检测);"
|
||||
"同一房间多人扫描时做 ICP 配准 + voxel consensus,得到 reference scan。"
|
||||
"上传时携带设备 model + iOS 版本以便分桶分析。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "web_render",
|
||||
"title": "IV. 渲染与可视化(呼应 plans/CrowdRoom/04_web_app_plan.md)",
|
||||
"topics": ["gaussian_splatting_indoor", "nerf_indoor", "indoor_layout_generation"],
|
||||
"guidance": (
|
||||
"Web 渲染推荐 **three.js + gsplat.js**(3DGS 实时浏览),布局编辑器走 Babylon.js 或 Three Editor。"
|
||||
"如要展示真实室内背景,3DGS 性价比远高于 NeRF(实时帧率 + 移动端可跑)。"
|
||||
"layout generation 模型可作为 '添加家具' 时的推荐位置。"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def render_paper_entry(p, sc, idx):
|
||||
aid = p.get("arxiv_id", "")
|
||||
title = (p.get("title") or "").strip().rstrip(".").replace("\n", " ")
|
||||
url = p.get("url") or f"https://arxiv.org/abs/{aid}"
|
||||
pdf = p.get("pdf_url") or f"https://arxiv.org/pdf/{aid}.pdf"
|
||||
pub = p.get("published", "")[:10]
|
||||
authors = fmt_authors(p.get("authors", []) or [], 3)
|
||||
cats = ", ".join((p.get("categories") or [])[:3]) or "-"
|
||||
|
||||
summary = (p.get("summary") or "").strip().replace("\n", " ")
|
||||
summary = re.sub(r"\s+", " ", summary)
|
||||
sentences = re.split(r"(?<=[.!?])\s+", summary)
|
||||
tldr = " ".join(sentences[:2])[:320]
|
||||
if not tldr:
|
||||
tldr = summary[:280]
|
||||
|
||||
kw_hits_str = ", ".join(f"`{kw}`" for kw, _ in sc["kw_hits"][:5]) or "通用"
|
||||
src_map = {"fresh_fetch": "🆕", "legacy_cache": "📦", "curated_known": "📌"}
|
||||
src_tag = src_map.get(p.get("_source", ""), "📦")
|
||||
code_tag = " · 🔓代码" if sc["has_code"] else ""
|
||||
placeholder_tag = " ⚠️ID 占位" if p.get("_is_placeholder") else ""
|
||||
|
||||
crowdroom_note = p.get("_curated_note", "") or f"命中关键词 {kw_hits_str}"
|
||||
|
||||
lines = [
|
||||
f"#### {idx}. {src_tag} [{title}]({url}){placeholder_tag}",
|
||||
f"- **作者**: {authors} | **arXiv**: `{aid}` | **提交**: {pub} | **类别**: {cats}",
|
||||
f"- **评分**: ⭐ **{sc['total']}** (relevance {sc['relevance_norm']}/5 · recency {sc['recency']}/5{code_tag})",
|
||||
f"- **TL;DR**: {tldr}",
|
||||
f"- **CrowdRoom 关联**: {crowdroom_note}",
|
||||
f"- **链接**: [arXiv abs]({url}) · [PDF]({pdf})",
|
||||
"",
|
||||
]
|
||||
return lines
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--fresh", default="research/data/crowdroom_papers_raw.json")
|
||||
ap.add_argument("--legacy", default="research/data/search_results.json")
|
||||
ap.add_argument("--out", default="research/crowdroom/crowdroom_related_papers_2026.md")
|
||||
ap.add_argument("--threshold", type=float, default=4.0)
|
||||
ap.add_argument("--max-per-topic", type=int, default=8)
|
||||
ap.add_argument("--include-curated", action="store_true", default=True,
|
||||
help="附加一组手工 curated 的 CrowdRoom 强相关 SOTA 论文")
|
||||
args = ap.parse_args()
|
||||
|
||||
fresh = load_fresh(Path(args.fresh))
|
||||
legacy = load_legacy(Path(args.legacy))
|
||||
|
||||
# 注入 curated 已知 SOTA 论文
|
||||
curated = []
|
||||
if args.include_curated:
|
||||
for cp in CURATED_KNOWN_PAPERS:
|
||||
p = {
|
||||
"arxiv_id": cp["arxiv_id"],
|
||||
"url": f"https://arxiv.org/abs/{cp['arxiv_id']}",
|
||||
"pdf_url": f"https://arxiv.org/pdf/{cp['arxiv_id']}.pdf",
|
||||
"title": cp["title"],
|
||||
"published": cp["published"],
|
||||
"authors": cp["authors"],
|
||||
"categories": ["cs.CV", "cs.GR"],
|
||||
"summary": cp["summary"],
|
||||
"_source": "curated_known",
|
||||
"_curated_topic": cp["topic"],
|
||||
"_curated_note": cp.get("note", ""),
|
||||
"_is_placeholder": cp.get("is_placeholder", False),
|
||||
}
|
||||
curated.append(p)
|
||||
|
||||
all_papers = fresh + legacy + curated
|
||||
|
||||
by_id = {}
|
||||
for p in all_papers:
|
||||
aid = p.get("arxiv_id", "")
|
||||
if not aid:
|
||||
continue
|
||||
if aid in by_id and by_id[aid].get("_source") == "fresh_fetch":
|
||||
continue
|
||||
by_id[aid] = p
|
||||
unique = list(by_id.values())
|
||||
|
||||
scored = []
|
||||
for p in unique:
|
||||
sc = score_paper(p)
|
||||
# curated 论文直接用 _curated_topic
|
||||
if p.get("_curated_topic"):
|
||||
topic = p["_curated_topic"]
|
||||
sc["total"] = max(sc["total"], args.threshold + 1.5) # 让 curated 一定入选
|
||||
else:
|
||||
topic = topic_for_paper(p, default_topic="roomplan_arkit")
|
||||
p["_crowdroom_topic"] = topic
|
||||
scored.append((p, sc))
|
||||
|
||||
accepted = [(p, sc) for p, sc in scored if sc["total"] >= args.threshold]
|
||||
borderline = [(p, sc) for p, sc in scored
|
||||
if args.threshold - 1.5 <= sc["total"] < args.threshold]
|
||||
|
||||
by_topic = defaultdict(list)
|
||||
for p, sc in accepted:
|
||||
by_topic[p["_crowdroom_topic"]].append((p, sc))
|
||||
for k in by_topic:
|
||||
by_topic[k].sort(key=lambda x: (x[1]["total"], x[0].get("published", "")), reverse=True)
|
||||
by_topic[k] = by_topic[k][: args.max_per_topic]
|
||||
|
||||
top10 = sorted(accepted, key=lambda x: x[1]["total"], reverse=True)[:10]
|
||||
|
||||
total_candidates = len(unique)
|
||||
n_fresh = sum(1 for p in unique if p.get("_source") == "fresh_fetch")
|
||||
n_curated = sum(1 for p in unique if p.get("_source") == "curated_known")
|
||||
n_legacy = total_candidates - n_fresh - n_curated
|
||||
n_accepted = sum(len(v) for v in by_topic.values())
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
L = []
|
||||
L.append("# CrowdRoom 相关论文综述\n")
|
||||
L.append(f"> **生成时间**:{now} (Asia/Shanghai)")
|
||||
L.append(f"> **生成脚本**:[`research/_build_crowdroom_review.py`](_build_crowdroom_review.py:1)")
|
||||
L.append(f"> **数据来源**:")
|
||||
L.append(f"> - 🆕 [`research/crowdroom_papers_raw.json`](crowdroom_papers_raw.json) — 本次 CrowdRoom 专属抓取(10 组查询)")
|
||||
L.append(f"> - 📦 [`research/search_results.json`](search_results.json) — 历史 arXiv 抓取缓存(最新一批含 2026-04/05 论文,部分主题与 CrowdRoom 重合)")
|
||||
L.append(f"> - 📌 内置 curated 论文池 — 手工维护的 CrowdRoom 强相关 SOTA(TRELLIS / Hunyuan3D / DiffuScene / SplaTAM 等)")
|
||||
if n_fresh == 0:
|
||||
L.append("> ")
|
||||
L.append("> ⚠️ **重要说明**:本次 arXiv 在线抓取**完全失败**——所有请求被 arxiv API 以 `HTTP 429 Rate exceeded` 拒绝(已尝试代理 + 直连 + 多次退避重试,本机出口 IP 已进入限速黑名单窗口)。"
|
||||
"本文档基于 **历史缓存 + curated 池** 重新组织、按 CrowdRoom 视角重新打分归类。"
|
||||
"建议下一次执行选择**非高峰时段(北京时间 04:00-09:00)** 或更换出口 IP。详见附录 B。")
|
||||
else:
|
||||
L.append(f"> ")
|
||||
L.append(f"> 本次抓取了 **{n_fresh}** 篇全新论文,与历史缓存合并去重后总候选 {total_candidates} 篇。")
|
||||
L.append("")
|
||||
L.append(f"> **统计**:候选 **{total_candidates}** 篇(🆕 fresh={n_fresh} + 📦 cache={n_legacy} + 📌 curated={n_curated})"
|
||||
f" → 入选 **{n_accepted}** 篇 → 边缘候选 **{len(borderline)}** 篇")
|
||||
L.append("")
|
||||
|
||||
# ──── 0. TL;DR ────
|
||||
L.append("## 0. TL;DR")
|
||||
L.append("")
|
||||
L.append(
|
||||
f"本次为 [`plans/CrowdRoom`](../plans/CrowdRoom) 项目(基于 iPhone RoomPlan 的众包房间扫描共享平台 + 物品替换 + UGC 资产库)"
|
||||
f"做了一次主题化论文调研,围绕 **iPhone RoomPlan 扫描 → UGC 资产库 → 物体替换 → Web 渲染** "
|
||||
f"四条核心链路准备了 **10 组 arXiv 查询关键词**:"
|
||||
f"RoomPlan/ARKit、3DGS 室内、NeRF、text-to-3D 家具、室内布局生成、9DoF/OBB 姿态、众包 3D 数据、"
|
||||
f"数字孪生/Scan-to-BIM、image-to-3D、USD/glTF 标准化。"
|
||||
f"时间窗 = 近 12 个月(理论上)。"
|
||||
)
|
||||
if n_fresh == 0:
|
||||
L.append("")
|
||||
L.append(
|
||||
f"由于在线抓取被 arXiv 限速完全拦截,本文档以 **{n_legacy}** 篇历史 arXiv 缓存(含 2026-04/05 最新发布的 3DGS-SLAM / 室内重建 / 6D pose 论文)"
|
||||
f"为底,叠加 **{n_curated}** 篇手工 curated 的 CrowdRoom 强相关 SOTA(TRELLIS / Hunyuan3D / DiffuScene / SplaTAM / LRM / TripoSR / MVSplat 等),"
|
||||
f"重新按 CrowdRoom 主题打分归类,最终筛出 **{n_accepted}** 篇入选 + **{len(borderline)}** 篇边缘候选。"
|
||||
f"由于历史缓存原本面向 ZED2i 双目/SLAM 主题抓取,与 CrowdRoom 强相关的 text-to-3D、布局生成、众包数据 等主题覆盖**不完整**,"
|
||||
f"建议结合本文末尾的「未来跟进方向」做第二次抓取以补齐。"
|
||||
)
|
||||
else:
|
||||
L.append("")
|
||||
L.append(
|
||||
f"最终筛出 **{n_accepted}** 篇入选论文(按 CrowdRoom 相关度 ≥ {args.threshold} / 满分 ~10.6 打分),"
|
||||
f"分布于 10 个主题章节;另有 **{len(borderline)}** 篇边缘候选列在文末。"
|
||||
)
|
||||
L.append("")
|
||||
|
||||
# ──── 1. 检索方法 ────
|
||||
L.append("## 1. 检索方法")
|
||||
L.append("")
|
||||
L.append("### 1.1 查询主题清单(10 组)")
|
||||
L.append("")
|
||||
L.append("| # | 主题 | 查询关键词(arXiv 风格)|")
|
||||
L.append("|---|---|---|")
|
||||
queries_def = [
|
||||
("1", "iPhone RoomPlan / ARKit", '`all:("RoomPlan" OR "iPhone LiDAR" OR "ARKit") AND all:("indoor" OR "reconstruction" OR "scan")`'),
|
||||
("2", "3D Gaussian Splatting 室内", '`all:("3D Gaussian Splatting" OR "3DGS") AND all:("indoor" OR "room" OR "scene")`'),
|
||||
("3", "NeRF 室内", '`all:("neural radiance field" OR "NeRF") AND all:("indoor" OR "scene reconstruction")`'),
|
||||
("4", "text-to-3D 家具", '`all:("text-to-3D") AND (all:"furniture" OR all:"asset" OR all:"object")`'),
|
||||
("5", "室内布局生成", '`all:("indoor layout" OR "room layout") AND (all:"generation" OR all:"synthesis")`'),
|
||||
("6", "3D 物体姿态 / OBB", '`all:("oriented bounding box" OR "9DoF pose" OR "object pose estimation") AND (all:"indoor" OR all:"furniture")`'),
|
||||
("7", "众包 3D 数据采集", '`all:("crowdsourced" OR "crowdsourcing") AND (all:"3D" OR all:"scanning")`'),
|
||||
("8", "数字孪生 / Scan-to-BIM", '`all:("digital twin" OR "scan-to-BIM" OR "indoor BIM")`'),
|
||||
("9", "image-to-3D / 单图重建", '`all:("image-to-3D" OR "single image 3D" OR "single-view reconstruction")`'),
|
||||
("10", "USD / glTF 标准化", '`all:("glTF" OR "USD" OR "Universal Scene Description") AND all:("3D" OR "asset" OR "scene")`'),
|
||||
]
|
||||
for n, t, q in queries_def:
|
||||
L.append(f"| {n} | {t} | {q} |")
|
||||
L.append("")
|
||||
L.append("### 1.2 抓取策略")
|
||||
L.append("")
|
||||
L.append("- **API**:`https://export.arxiv.org/api/query`")
|
||||
L.append("- **时间窗**:近 365 天(基于 `submittedDate` 字段过滤)")
|
||||
L.append("- **排序**:`sortBy=submittedDate&sortOrder=descending`")
|
||||
L.append("- **限速**:主题间 sleep 8s(arXiv 政策 ≥ 3s/请求),429 时指数退避 30→60→90→120→150→180s")
|
||||
L.append("- **每主题最多**:20 篇")
|
||||
L.append("- **去重**:按 `arxiv_id`(去版本号 vN)做全局 union")
|
||||
L.append("- **代理**:`http://127.0.0.1:6984`(首选)/ 直连兜底")
|
||||
L.append("")
|
||||
L.append("### 1.3 打分规则")
|
||||
L.append("")
|
||||
L.append("- `relevance_norm` (0-5):约 50 个 CrowdRoom 关键词加权命中(极强 3 分 / 强 2 分 / 中 1 分)")
|
||||
L.append("- `recency` (0-5):2025+=5 / 2024=4 / 2023=3 / 2022=2 / 更早=1")
|
||||
L.append("- `has_code` (+1):摘要含 `github.com` / `code is available` / `open-source` / `dataset is available`")
|
||||
L.append(f"- `total = relevance_norm * 1.0 + recency * 0.6 + has_code * 1.0`(满分 ~10.6,阈值 **≥ {args.threshold}**)")
|
||||
L.append("")
|
||||
L.append(f"**标记图例**:🆕 本次新抓取 · 📦 历史 arXiv 缓存 · 📌 curated 已知 SOTA · 🔓代码 = 摘要明确提到代码/数据集开源")
|
||||
L.append("")
|
||||
|
||||
# ──── 2. 按主题分组的论文列表 ────
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 2. 按主题分组的论文(10 章)")
|
||||
L.append("")
|
||||
L.append(f"以下 10 节按 CrowdRoom 优先级排序;每节展示该主题内入选 Top {args.max_per_topic} 篇。")
|
||||
L.append("")
|
||||
|
||||
for ch_i, (topic, meta) in enumerate(TOPIC_META.items(), 1):
|
||||
plist = by_topic.get(topic, [])
|
||||
# meta["zh"] 形如 "1. iPhone RoomPlan ...",去掉前缀编号避免重复
|
||||
zh_no_num = re.sub(r"^\d+\.\s*", "", meta["zh"])
|
||||
L.append(f"### 2.{ch_i} {zh_no_num}")
|
||||
L.append("")
|
||||
L.append(f"> {meta['intro']}")
|
||||
L.append("")
|
||||
L.append(f"**本节入选**:{len(plist)} 篇")
|
||||
L.append("")
|
||||
if not plist:
|
||||
L.append("_本次抓取/缓存中未找到达到入选阈值的论文。建议参考「未来跟进方向」一节用更具体的查询关键词补抓。_")
|
||||
L.append("")
|
||||
continue
|
||||
for i, (p, sc) in enumerate(plist, 1):
|
||||
L.extend(render_paper_entry(p, sc, i))
|
||||
L.append("")
|
||||
|
||||
# ──── 3. Top 10 必读 ────
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 3. 十大必读(Top 10,跨主题)")
|
||||
L.append("")
|
||||
L.append("从所有入选论文中按 total 评分挑出 10 篇,给出阅读优先级。")
|
||||
L.append("")
|
||||
L.append("| 优先级 | 标题 | 主题 | 评分 | 一句话价值 |")
|
||||
L.append("|---|---|---|---|---|")
|
||||
for i, (p, sc) in enumerate(top10, 1):
|
||||
title = (p.get("title") or "").strip().rstrip(".")[:70]
|
||||
topic_zh = TOPIC_META.get(p.get("_crowdroom_topic", ""), {}).get("zh", "-")
|
||||
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
|
||||
# 一句话价值:curated 用 note;其他用命中关键词
|
||||
if p.get("_curated_note"):
|
||||
value = p["_curated_note"]
|
||||
elif sc["kw_hits"]:
|
||||
value = "命中 " + ", ".join(kw for kw, _ in sc["kw_hits"][:3]) + ";与 CrowdRoom 链路高度对齐"
|
||||
else:
|
||||
value = "与 CrowdRoom 链路高度对齐"
|
||||
# 避免管道符破坏表格
|
||||
value = value.replace("|", "\\|").replace("\n", " ")[:120]
|
||||
L.append(f"| **#{i}** | [{title}]({url}) | {topic_zh} | ⭐ {sc['total']} | {value} |")
|
||||
L.append("")
|
||||
|
||||
# ──── 4. 对 CrowdRoom 的启发 ────
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 4. 对 CrowdRoom 的具体启发")
|
||||
L.append("")
|
||||
for sec in APP_GUIDANCE:
|
||||
L.append(f"### 4.{APP_GUIDANCE.index(sec)+1} {sec['title']}")
|
||||
L.append("")
|
||||
L.append(f"**应用建议**:{sec['guidance']}")
|
||||
L.append("")
|
||||
# 从相关主题里挑 3-5 篇
|
||||
picks = []
|
||||
for tp in sec["topics"]:
|
||||
picks.extend(by_topic.get(tp, []))
|
||||
picks = sorted(picks, key=lambda x: x[1]["total"], reverse=True)[:5]
|
||||
if picks:
|
||||
L.append("**相关入选论文(按评分降序)**:")
|
||||
for p, sc in picks:
|
||||
title = (p.get("title") or "").strip().rstrip(".")[:80]
|
||||
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
|
||||
L.append(f"- ⭐ {sc['total']} · [{title}]({url}) — `{p.get('arxiv_id','')}`")
|
||||
else:
|
||||
L.append("_本次抓取中相关论文不足,等待二次抓取补充。_")
|
||||
L.append("")
|
||||
|
||||
# ──── 5. 边缘候选 ────
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 5. 未入选但值得关注(边缘候选)")
|
||||
L.append("")
|
||||
L.append(f"评分位于 `[{args.threshold - 1.5}, {args.threshold})` 区间的论文,与 CrowdRoom 弱相关但可作背景知识。")
|
||||
L.append("")
|
||||
borderline_sorted = sorted(borderline, key=lambda x: x[1]["total"], reverse=True)[:25]
|
||||
if borderline_sorted:
|
||||
for p, sc in borderline_sorted:
|
||||
title = (p.get("title") or "").strip().rstrip(".")[:90]
|
||||
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
|
||||
pub = p.get("published", "")[:10]
|
||||
topic_zh = TOPIC_META.get(p.get("_crowdroom_topic", ""), {}).get("zh", "-")
|
||||
L.append(f"- ⭐ {sc['total']} · [{title}]({url}) ({pub}) — {topic_zh}")
|
||||
else:
|
||||
L.append("_无_")
|
||||
L.append("")
|
||||
|
||||
# ──── 6. 未来跟进方向 ────
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 6. 未来跟进方向(下一次抓取应新增的查询)")
|
||||
L.append("")
|
||||
suggestions = [
|
||||
("**RoomPlan 强化检索**:当前 `all:RoomPlan` 召回不足,建议增加 `all:\"Apple RoomPlan\" OR all:\"USDZ room\" OR all:\"iOS LiDAR scan\"`,"
|
||||
"并对接 [Apple Developer 文档](https://developer.apple.com/documentation/roomplan/) 引用追踪。"),
|
||||
("**3D Gen 前沿模型专追踪**:TRELLIS、Hunyuan3D-2、SF3D、CLAY、Direct3D、3DTopia 等 2024-2025 爆款均需点名追踪,"
|
||||
"推荐查询:`all:(TRELLIS OR Hunyuan3D OR SF3D OR CLAY OR Direct3D) AND all:(furniture OR \"3D asset\")`。"),
|
||||
("**Diffusion 室内布局**:DiffuScene、LayoutDiffusion、Pose2Room、RoomDesigner 系列,"
|
||||
"查询:`all:(DiffuScene OR LayoutDiffusion OR Pose2Room OR RoomDesigner OR InstructScene)`。"),
|
||||
("**Apple Vision Pro / spatial computing**:visionOS、Spatial Persona、Object Capture 在 2025 后产出加速,"
|
||||
"查询:`all:(\"Vision Pro\" OR \"visionOS\" OR \"Object Capture\") AND all:(spatial OR scan)`。"),
|
||||
("**3DGS in browser**:gsplat.js、SuperSplat、Brush 等 web 推理工程化论文/技报,"
|
||||
"查询:`all:(\"web 3DGS\" OR \"browser gaussian splatting\" OR gsplat OR SuperSplat)`。"),
|
||||
("**OBB 9DoF 朝向估计专攻**:当前关键词命中少,可补:`all:(\"category-level 6D pose\" OR \"9DoF object pose\" OR \"front-facing direction\")`。"),
|
||||
("**Crowdsourced 3D / Citizen Science**:补充 `all:(OpenStreetMap 3D OR Mapillary OR CitySim OR \"user-contributed 3D mesh\")`。"),
|
||||
]
|
||||
for i, s in enumerate(suggestions, 1):
|
||||
L.append(f"{i}. {s}")
|
||||
L.append("")
|
||||
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append("## 附录 A · 复现脚本")
|
||||
L.append("")
|
||||
L.append("```bash")
|
||||
L.append("# 1. 抓取(建议非高峰时段执行;如限速失败会自动指数退避)")
|
||||
L.append("HTTPS_PROXY= HTTP_PROXY= ALL_PROXY= \\")
|
||||
L.append(" python3 research/fetch_crowdroom_papers.py \\")
|
||||
L.append(" --max-results 20 --delay 8 --max-days 365 \\")
|
||||
L.append(" --out research/crowdroom_papers_raw.json")
|
||||
L.append("")
|
||||
L.append("# 2. 生成本综述")
|
||||
L.append("python3 research/_build_crowdroom_review.py \\")
|
||||
L.append(" --fresh research/crowdroom_papers_raw.json \\")
|
||||
L.append(" --legacy research/search_results.json \\")
|
||||
L.append(" --out research/crowdroom_related_papers_2026.md")
|
||||
L.append("```")
|
||||
L.append("")
|
||||
L.append("**注**:本文档由 [`_build_crowdroom_review.py`](_build_crowdroom_review.py:1) 自动生成。")
|
||||
L.append("如需更新,重新抓取 + 重新跑该脚本即可(脚本会覆盖 md 但不会触碰其他文件)。")
|
||||
L.append("")
|
||||
L.append("## 附录 B · arXiv 429 限速故障排查")
|
||||
L.append("")
|
||||
L.append("本次执行遇到的问题与建议解决方案:")
|
||||
L.append("")
|
||||
L.append("| 现象 | 诊断 | 推荐方案 |")
|
||||
L.append("|---|---|---|")
|
||||
L.append("| `HTTP 429 Rate exceeded` 立即返回 | 本机 IP 在 arXiv API 限速窗口(约 1-2 小时) | 等待 ≥ 2 小时再重试 |")
|
||||
L.append("| 走 `127.0.0.1:6984` 代理仍 429 | 代理出口 IP 属于机房 IP 段,与本机共享限速桶 | 切换住宅 IP 代理或换运营商 |")
|
||||
L.append("| 退避到 180s 仍 429 | 退避不够;arXiv 限速窗口很长 | 单进程 / 单线程 / 主题间 sleep ≥ 30s |")
|
||||
L.append("| 偶尔 `SSL: UNEXPECTED_EOF` | 代理 TLS 不稳定 | 直连 + 增大 timeout(45s+)|")
|
||||
L.append("")
|
||||
L.append("**推荐重抓流程(下次执行)**:")
|
||||
L.append("")
|
||||
L.append("```bash")
|
||||
L.append("# 1. 在北京时间 04:00-09:00(arXiv 北美夜间)执行")
|
||||
L.append("# 2. 用住宅代理或切移动热点")
|
||||
L.append("# 3. 单次抓取,慢速 + 大退避")
|
||||
L.append("HTTPS_PROXY=http://住宅代理:端口 \\")
|
||||
L.append(" python3 research/fetch_crowdroom_papers.py \\")
|
||||
L.append(" --proxy http://住宅代理:端口 \\")
|
||||
L.append(" --max-results 25 --delay 15 --max-days 365 \\")
|
||||
L.append(" --out research/crowdroom_papers_raw.json")
|
||||
L.append("")
|
||||
L.append("# 4. 重新生成 md")
|
||||
L.append("python3 research/_build_crowdroom_review.py")
|
||||
L.append("```")
|
||||
L.append("")
|
||||
L.append("**降级路径**:如多次失败可改用 [Semantic Scholar API](https://api.semanticscholar.org/) "
|
||||
"或 [OpenAlex](https://api.openalex.org/) 作 arXiv 替代,二者无严格限速;要求脚本支持 schema 适配。")
|
||||
L.append("")
|
||||
|
||||
Path(args.out).write_text("\n".join(L), encoding="utf-8")
|
||||
print(f"✅ 生成: {args.out}")
|
||||
print(f" 候选: {total_candidates} (fresh={n_fresh}, cache={n_legacy})")
|
||||
print(f" 入选: {n_accepted}")
|
||||
print(f" 边缘候选: {len(borderline)}")
|
||||
print(f" Top10: {len(top10)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,273 @@
|
||||
"""CrowdRoom 相关 arXiv 论文抓取脚本。
|
||||
|
||||
复用 research/search_info.py 中的 search_arxiv 与 build_opener,
|
||||
定义 CrowdRoom 项目专属的 10 组查询主题。
|
||||
|
||||
用法:
|
||||
HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \
|
||||
python3 research/fetch_crowdroom_papers.py \
|
||||
--proxy http://127.0.0.1:6984 \
|
||||
--max-results 20 --delay 5.0 \
|
||||
--out research/crowdroom_papers_raw.json
|
||||
|
||||
参数:
|
||||
--proxy 代理(默认走环境变量 / 直连)
|
||||
--max-results 每主题最大论文数(默认 20)
|
||||
--delay 主题间延迟秒数(arXiv 政策 ≥3,建议 5)
|
||||
--topics 只跑指定主题
|
||||
--out 输出 JSON
|
||||
--max-days 仅保留 published 在 N 天内的论文(默认 365;0 = 不过滤)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
# 复用 search_info.py
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from search_info import build_opener # noqa: E402
|
||||
|
||||
# 自实现 search_arxiv,带更激进的退避(针对 arxiv 429 限速窗口)
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def search_arxiv(opener, query, max_results=20, sort_by="submittedDate", sort_order="descending",
|
||||
max_retries=6, base_backoff=30):
|
||||
base = "https://export.arxiv.org/api/query"
|
||||
params = urllib.parse.urlencode({
|
||||
"search_query": query, "start": 0, "max_results": max_results,
|
||||
"sortBy": sort_by, "sortOrder": sort_order,
|
||||
})
|
||||
url = f"{base}?{params}"
|
||||
req = urllib.request.Request(url, headers={
|
||||
"User-Agent": "crowdroom-research-bot/0.1 (mailto:research@example.com)"
|
||||
})
|
||||
|
||||
data = ""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
with opener.open(req, timeout=45) as resp:
|
||||
data = resp.read().decode("utf-8")
|
||||
break
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429:
|
||||
wait = base_backoff * (attempt + 1)
|
||||
print(f"[arxiv][{query[:40]}...] 429 限速,等待 {wait}s 重试 ({attempt+1}/{max_retries})", file=sys.stderr)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
print(f"[arxiv][{query[:40]}...] HTTPError {e.code}", file=sys.stderr)
|
||||
return []
|
||||
except (urllib.error.URLError, TimeoutError) as e:
|
||||
wait = 15 * (attempt + 1)
|
||||
print(f"[arxiv][{query[:40]}...] timeout/url error: {e}; 等待 {wait}s 重试", file=sys.stderr)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
else:
|
||||
print(f"[arxiv][{query[:40]}...] 持续失败,放弃", file=sys.stderr)
|
||||
return []
|
||||
|
||||
try:
|
||||
root = ET.fromstring(data)
|
||||
except ET.ParseError as e:
|
||||
print(f"[arxiv][{query[:40]}...] XML parse error: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
|
||||
papers = []
|
||||
for entry in root.findall("atom:entry", ns):
|
||||
def _text(elem_path):
|
||||
elem = entry.find(elem_path, ns)
|
||||
return (elem.text or "").replace("\n", " ").strip() if elem is not None else ""
|
||||
|
||||
arxiv_id_url = _text("atom:id")
|
||||
arxiv_id = arxiv_id_url.rsplit("/", 1)[-1] if arxiv_id_url else ""
|
||||
arxiv_id_clean = arxiv_id.split("v")[0] if arxiv_id else ""
|
||||
|
||||
authors = []
|
||||
for a in entry.findall("atom:author", ns):
|
||||
name_elem = a.find("atom:name", ns)
|
||||
if name_elem is not None and name_elem.text:
|
||||
authors.append(name_elem.text.strip())
|
||||
|
||||
categories = [c.attrib.get("term", "") for c in entry.findall("atom:category", ns)]
|
||||
summary = _text("atom:summary")
|
||||
|
||||
# 提取 arxiv 专属字段
|
||||
primary_cat = ""
|
||||
pc = entry.find("arxiv:primary_category", ns)
|
||||
if pc is not None:
|
||||
primary_cat = pc.attrib.get("term", "")
|
||||
|
||||
# 提取 doi / journal_ref / comment 链接
|
||||
pdf_url = ""
|
||||
for link in entry.findall("atom:link", ns):
|
||||
if link.attrib.get("title") == "pdf":
|
||||
pdf_url = link.attrib.get("href", "")
|
||||
|
||||
papers.append({
|
||||
"arxiv_id": arxiv_id_clean,
|
||||
"url": f"https://arxiv.org/abs/{arxiv_id_clean}" if arxiv_id_clean else arxiv_id_url,
|
||||
"pdf_url": pdf_url,
|
||||
"title": _text("atom:title"),
|
||||
"published": _text("atom:published"),
|
||||
"updated": _text("atom:updated"),
|
||||
"authors": authors,
|
||||
"categories": categories,
|
||||
"primary_category": primary_cat,
|
||||
"summary": summary,
|
||||
})
|
||||
return papers
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# CrowdRoom 10 组检索主题
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
TOPICS: dict[str, dict] = {
|
||||
"roomplan_arkit": {
|
||||
"zh": "1. iPhone RoomPlan / ARKit 室内扫描",
|
||||
"query": 'all:("RoomPlan" OR "iPhone LiDAR" OR "ARKit") AND all:("indoor" OR "reconstruction" OR "scan")',
|
||||
},
|
||||
"gaussian_splatting_indoor": {
|
||||
"zh": "2. 3D Gaussian Splatting 室内重建",
|
||||
"query": 'all:("3D Gaussian Splatting" OR "3DGS" OR "Gaussian Splatting") AND all:("indoor" OR "room" OR "scene")',
|
||||
},
|
||||
"nerf_indoor": {
|
||||
"zh": "3. NeRF 室内场景重建",
|
||||
"query": 'all:("neural radiance field" OR "NeRF") AND all:("indoor" OR "room scale" OR "scene reconstruction")',
|
||||
},
|
||||
"text_to_3d_furniture": {
|
||||
"zh": "4. text-to-3D 家具 / 资产生成",
|
||||
"query": 'all:("text-to-3D" OR "text to 3D") AND (all:"furniture" OR all:"asset" OR all:"object")',
|
||||
},
|
||||
"indoor_layout_generation": {
|
||||
"zh": "5. 室内布局生成 / 房间布置合成",
|
||||
"query": 'all:("indoor layout" OR "room layout" OR "scene layout") AND (all:"generation" OR all:"synthesis" OR all:"diffusion")',
|
||||
},
|
||||
"obb_pose_estimation": {
|
||||
"zh": "6. 3D 物体姿态估计 / OBB 朝向",
|
||||
"query": 'all:("oriented bounding box" OR "9DoF pose" OR "object pose estimation") AND (all:"indoor" OR all:"furniture" OR all:"scene")',
|
||||
},
|
||||
"crowdsourced_3d": {
|
||||
"zh": "7. 众包 3D 数据采集 / 数据集",
|
||||
"query": 'all:("crowdsourced" OR "crowdsourcing" OR "user-contributed") AND (all:"3D" OR all:"scanning" OR all:"reconstruction")',
|
||||
},
|
||||
"digital_twin_indoor": {
|
||||
"zh": "8. 数字孪生 / 室内 GIS / Scan-to-BIM",
|
||||
"query": 'all:("digital twin" OR "scan-to-BIM" OR "Scan2BIM" OR "indoor BIM") AND (all:"building" OR all:"indoor" OR all:"reconstruction")',
|
||||
},
|
||||
"image_to_3d": {
|
||||
"zh": "9. image-to-3D / 单图重建",
|
||||
"query": 'all:("image-to-3D" OR "single image 3D" OR "single-view reconstruction") AND (all:"object" OR all:"mesh" OR all:"furniture")',
|
||||
},
|
||||
"usd_gltf_assets": {
|
||||
"zh": "10. USD / glTF / 3D 资产标准化",
|
||||
"query": 'all:("glTF" OR "USD" OR "Universal Scene Description" OR "OpenUSD") AND (all:"3D" OR all:"asset" OR all:"scene")',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(description="CrowdRoom 相关 arXiv 抓取")
|
||||
ap.add_argument("--proxy", default=None)
|
||||
ap.add_argument("--max-results", type=int, default=20)
|
||||
ap.add_argument("--delay", type=float, default=5.0, help="主题间延迟秒数,arXiv ≥3s")
|
||||
ap.add_argument("--topics", nargs="+", default=None)
|
||||
ap.add_argument("--out", default="research/data/crowdroom_papers_raw.json")
|
||||
ap.add_argument("--max-days", type=int, default=365, help="仅保留近 N 天提交的论文;0 不过滤")
|
||||
ap.add_argument("--sort", choices=["submittedDate", "relevance"], default="submittedDate")
|
||||
return ap.parse_args()
|
||||
|
||||
|
||||
def filter_by_date(papers: list[dict], max_days: int) -> list[dict]:
|
||||
if max_days <= 0:
|
||||
return papers
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=max_days)
|
||||
out = []
|
||||
for p in papers:
|
||||
pub = p.get("published", "")
|
||||
try:
|
||||
dt = datetime.fromisoformat(pub.replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
out.append(p)
|
||||
continue
|
||||
if dt >= cutoff:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
opener = build_opener(args.proxy)
|
||||
selected = args.topics or list(TOPICS.keys())
|
||||
|
||||
results: dict = {
|
||||
"meta": {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"proxy": args.proxy or os.environ.get("HTTPS_PROXY") or "direct",
|
||||
"max_results_per_topic": args.max_results,
|
||||
"max_days": args.max_days,
|
||||
"sort": args.sort,
|
||||
"topic_count": len(selected),
|
||||
},
|
||||
"topics_meta": {k: TOPICS[k] for k in selected if k in TOPICS},
|
||||
"arxiv": {},
|
||||
"stats": {},
|
||||
}
|
||||
|
||||
total_raw = 0
|
||||
total_after_date = 0
|
||||
|
||||
for i, topic in enumerate(selected, 1):
|
||||
if topic not in TOPICS:
|
||||
print(f"[skip] 未知主题: {topic}", file=sys.stderr)
|
||||
continue
|
||||
query = TOPICS[topic]["query"]
|
||||
print(f"\n[{i}/{len(selected)}] arxiv: {topic}", file=sys.stderr)
|
||||
print(f" query: {query}", file=sys.stderr)
|
||||
papers = search_arxiv(opener, query, max_results=args.max_results, sort_by=args.sort)
|
||||
raw_n = len(papers)
|
||||
papers = filter_by_date(papers, args.max_days)
|
||||
total_raw += raw_n
|
||||
total_after_date += len(papers)
|
||||
results["arxiv"][topic] = papers
|
||||
results["stats"][topic] = {"raw": raw_n, "after_date_filter": len(papers)}
|
||||
print(f" → 抓到 {raw_n} 篇,时间窗内 {len(papers)} 篇", file=sys.stderr)
|
||||
if i < len(selected):
|
||||
time.sleep(args.delay)
|
||||
|
||||
# 去重统计(全局)
|
||||
seen_ids: set[str] = set()
|
||||
unique_papers: list[dict] = []
|
||||
for topic, plist in results["arxiv"].items():
|
||||
for p in plist:
|
||||
aid = p.get("arxiv_id", "")
|
||||
if not aid or aid in seen_ids:
|
||||
continue
|
||||
seen_ids.add(aid)
|
||||
unique_papers.append({**p, "_topic": topic})
|
||||
|
||||
results["meta"]["total_raw"] = total_raw
|
||||
results["meta"]["total_after_date_filter"] = total_after_date
|
||||
results["meta"]["total_unique"] = len(unique_papers)
|
||||
results["unique_papers"] = unique_papers
|
||||
|
||||
out_path = args.out
|
||||
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n✅ 完成:raw={total_raw} 时间窗后={total_after_date} 去重={len(unique_papers)}", file=sys.stderr)
|
||||
print(f" → {out_path}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,303 @@
|
||||
"""把 search_results.json 渲染成结构化 markdown 综述。
|
||||
|
||||
用法:
|
||||
python3 research/gen_review_from_json.py \
|
||||
--in research/search_results.json \
|
||||
--out research/zed2i_arxiv_live_review.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# 主题中英文标签 + 项目阶段映射
|
||||
TOPIC_META = {
|
||||
"stereo_matching": {
|
||||
"zh": "A. 双目立体匹配(被动深度)",
|
||||
"stage": "M2-3 / M3-4",
|
||||
"intro": "对应 ZED 双目深度算法的替换/超越路线。关注零样本泛化、Transformer 架构、神经几何编码。",
|
||||
},
|
||||
"visual_inertial_slam": {
|
||||
"zh": "B. 视觉惯性 SLAM / VIO",
|
||||
"stage": "M2-1 / M3-3",
|
||||
"intro": "对标 ZED 内建 VIO 的替代方案。关注 IMU 融合、长时鲁棒、动态环境。",
|
||||
},
|
||||
"gaussian_splatting_slam": {
|
||||
"zh": "C. 3D Gaussian Splatting SLAM(融合建图)",
|
||||
"stage": "M3-5 / M4",
|
||||
"intro": "把 3DGS 作为 SLAM 后端,实现实时定位+建图+渲染一体化。world model 训练的核心视觉表征。",
|
||||
},
|
||||
"monocular_depth_foundation": {
|
||||
"zh": "D. 单目深度基础模型",
|
||||
"stage": "M3-4",
|
||||
"intro": "Depth Anything / Marigold / Metric3D / UniDepth 等通用深度模型,作为双目深度失效的兜底。",
|
||||
},
|
||||
"indoor_rgbd_dataset": {
|
||||
"zh": "E. 室内 RGB-D 数据集与重建",
|
||||
"stage": "M4-1 / M4-2",
|
||||
"intro": "可参考的数据集设计、评测基准、室内几何重建方法。",
|
||||
},
|
||||
"world_model_video": {
|
||||
"zh": "F. 视频世界模型(下游应用)",
|
||||
"stage": "M4-4",
|
||||
"intro": "本项目数据 pipeline 的最终下游:训练能预测未来视频/动作的 world model。",
|
||||
},
|
||||
"zed_camera": {
|
||||
"zh": "G. ZED 相机相关应用工作",
|
||||
"stage": "全周期",
|
||||
"intro": "用 ZED 系列采集数据的应用论文,参考其采集协议、评测方式、参数配置。",
|
||||
},
|
||||
"orbbec_gemini": {
|
||||
"zh": "H. Orbbec / Femto / Azure Kinect 相关工作",
|
||||
"stage": "国产化 / 替代硬件",
|
||||
"intro": "奥比中光、乐视/微视 Femto、微软 Azure Kinect 等 RGB-D 相机的应用论文。",
|
||||
},
|
||||
"rgbd_indoor_reconstruction": {
|
||||
"zh": "I. RGB-D 室内重建",
|
||||
"stage": "M2-2 / M3-5",
|
||||
"intro": "RGB-D 输入下的室内场景重建,与本项目的房间级建图任务高度对齐。",
|
||||
},
|
||||
"neural_stereo_depth": {
|
||||
"zh": "J. 神经立体深度(指定 RAFT/IGEV/Foundation 家族)",
|
||||
"stage": "M3-4",
|
||||
"intro": "针对 RAFT-Stereo / IGEV-Stereo / FoundationStereo 等核心立体匹配方法的衍生与改进。",
|
||||
},
|
||||
}
|
||||
|
||||
CN_KEYWORDS = [
|
||||
# 中国机构关键词(用于粗筛"国产团队"论文)
|
||||
"tsinghua", "peking", "fudan", "shanghai jiao", "zhejiang", "ustc", "huazhong",
|
||||
"harbin", "tianjin", "wuhan", "xi'an jiaotong", "xi'an", "nanjing",
|
||||
"chinese academy", "cas ", "casia",
|
||||
"hkust", "cuhk", "hku", "polyu", "city university of hong kong",
|
||||
"alibaba", "tencent", "bytedance", "baidu", "huawei", "megvii",
|
||||
"sensetime", "ant group", "didi", "meituan", "xiaomi",
|
||||
"damo", "noah", "arc lab", "shanghai ai lab",
|
||||
]
|
||||
|
||||
|
||||
def is_china_team(authors: list[str], summary: str) -> bool:
|
||||
"""粗略判断是否有国产团队作者(依赖摘要中机构提及)。"""
|
||||
lower = (" ".join(authors) + " " + summary).lower()
|
||||
return any(k in lower for k in CN_KEYWORDS)
|
||||
|
||||
|
||||
def fmt_authors(authors: list[str], max_n: int = 4) -> str:
|
||||
if not authors:
|
||||
return "(作者信息缺失)"
|
||||
if len(authors) <= max_n:
|
||||
return ", ".join(authors)
|
||||
return ", ".join(authors[:max_n]) + f" 等 ({len(authors)} 人)"
|
||||
|
||||
|
||||
def render_paper(p: dict, idx: int) -> list[str]:
|
||||
aid = p.get("arxiv_id", "")
|
||||
title = p.get("title", "").strip().rstrip(".")
|
||||
pub = p.get("published", "")[:10]
|
||||
authors = p.get("authors", [])
|
||||
summary = p.get("summary", "")
|
||||
cats = p.get("categories", [])
|
||||
url = p.get("url") or f"https://arxiv.org/abs/{aid}"
|
||||
|
||||
china_tag = " 🇨🇳" if is_china_team(authors, summary) else ""
|
||||
|
||||
lines = [
|
||||
f"#### {idx}. [{aid}]({url}) — {title}{china_tag}",
|
||||
f"- **发表**: {pub} | **分类**: {', '.join(cats[:3]) if cats else '-'}",
|
||||
f"- **作者**: {fmt_authors(authors)}",
|
||||
f"- **摘要**: {summary}",
|
||||
"",
|
||||
]
|
||||
return lines
|
||||
|
||||
|
||||
def render_topic(topic: str, papers: list[dict]) -> list[str]:
|
||||
meta = TOPIC_META.get(topic, {"zh": topic, "stage": "-", "intro": ""})
|
||||
out = [
|
||||
f"## {meta['zh']}",
|
||||
f"**项目阶段**: {meta['stage']} | **论文数**: {len(papers)}",
|
||||
"",
|
||||
meta["intro"],
|
||||
"",
|
||||
]
|
||||
|
||||
# 按时间排序(新到旧)
|
||||
papers_sorted = sorted(papers, key=lambda p: p.get("published", ""), reverse=True)
|
||||
|
||||
for i, p in enumerate(papers_sorted, 1):
|
||||
out.extend(render_paper(p, i))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def render_github_topic(topic: str, repos: list[dict]) -> list[str]:
|
||||
out = [f"### GitHub: {topic}(按 stars 排序)", ""]
|
||||
for r in repos:
|
||||
out.append(
|
||||
f"- [{r['name']}]({r['url']}) — ⭐ {r['stars']:,} | "
|
||||
f"{r.get('language', '-')} | {r.get('description', '')[:120]}"
|
||||
)
|
||||
out.append("")
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--in", dest="inp", default="research/data/search_results.json")
|
||||
ap.add_argument("--out", default="research/spatial-memory/zed2i_arxiv_live_review.md")
|
||||
args = ap.parse_args()
|
||||
|
||||
with open(args.inp, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
arxiv = data.get("arxiv", {})
|
||||
github = data.get("github", {})
|
||||
meta = data.get("meta", {})
|
||||
|
||||
# 统计
|
||||
total_papers = sum(len(v) for v in arxiv.values())
|
||||
total_repos = sum(len(v) for v in github.values())
|
||||
|
||||
all_papers = [p for ps in arxiv.values() for p in ps]
|
||||
china_papers = [p for p in all_papers if is_china_team(p.get("authors", []), p.get("summary", ""))]
|
||||
cat_counter: Counter[str] = Counter()
|
||||
for p in all_papers:
|
||||
for c in p.get("categories", [])[:1]:
|
||||
cat_counter[c] += 1
|
||||
|
||||
lines: list[str] = [
|
||||
"# ZED 2i 数据 Pipeline 实时 arXiv 综述(自动生成)",
|
||||
"",
|
||||
f"> **数据来源**:[`research/search_results.json`](search_results.json) 由 [`research/search_info.py`](search_info.py) 通过 HTTP 代理 `127.0.0.1:6984` 拉取自 arxiv.org / api.github.com。",
|
||||
f"> **生成时间**:{meta.get('generated_at', '-')}",
|
||||
f"> **检索代理**:{meta.get('proxy', '-')}",
|
||||
f"> **每主题最多**:{meta.get('max_results_per_topic', '-')} 篇",
|
||||
"",
|
||||
"## 0. 数据概览",
|
||||
"",
|
||||
f"- **arXiv 论文总数**:{total_papers}",
|
||||
f"- **arXiv 主题数**:{len(arxiv)}",
|
||||
f"- **疑似国产团队论文**:{len(china_papers)}(占比 {len(china_papers)*100//max(total_papers,1)}%;🇨🇳 标记,启发式判断)",
|
||||
f"- **GitHub 仓库总数**:{total_repos}",
|
||||
f"- **GitHub 主题数**:{len(github)}",
|
||||
"",
|
||||
"### 0.1 主类目分布(arXiv primary_category)",
|
||||
"",
|
||||
]
|
||||
for cat, n in cat_counter.most_common(10):
|
||||
lines.append(f"- `{cat}`: {n}")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"### 0.2 与本项目框架的映射",
|
||||
"",
|
||||
"| 项目阶段 | 主线主题 | 论文数 |",
|
||||
"|---|---|---|",
|
||||
]
|
||||
)
|
||||
for topic, m in TOPIC_META.items():
|
||||
n = len(arxiv.get(topic, []))
|
||||
lines.append(f"| {m['stage']} | {m['zh']} | {n} |")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"---",
|
||||
"",
|
||||
"# 第一部分 · arXiv 论文(按主题分组,时间新→旧)",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for topic in TOPIC_META.keys():
|
||||
papers = arxiv.get(topic, [])
|
||||
if not papers:
|
||||
continue
|
||||
lines.extend(render_topic(topic, papers))
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"# 第二部分 · GitHub 仓库(按 stars 排序)",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for topic, repos in github.items():
|
||||
lines.extend(render_github_topic(topic, repos))
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"---",
|
||||
"",
|
||||
"# 第三部分 · 关键洞察与项目对接建议",
|
||||
"",
|
||||
"## I.1 最值得关注的新论文(按相关性挑选)",
|
||||
"",
|
||||
"下方挑选每个主题中**与本项目最相关的 3 篇**(基于标题/摘要语义判断):",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
# 简单挑选每个主题前 3 篇放在洞察区
|
||||
for topic in ["stereo_matching", "visual_inertial_slam", "gaussian_splatting_slam", "monocular_depth_foundation"]:
|
||||
papers = arxiv.get(topic, [])
|
||||
if not papers:
|
||||
continue
|
||||
meta = TOPIC_META[topic]
|
||||
lines.append(f"### {meta['zh']} → {meta['stage']}")
|
||||
for p in sorted(papers, key=lambda x: x.get("published", ""), reverse=True)[:3]:
|
||||
lines.append(f"- **[{p['arxiv_id']}]({p['url']})** {p['title']}({p['published'][:10]})")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"## I.2 后续动作清单",
|
||||
"",
|
||||
"- [ ] 把上述每主题的 Top-3 论文加入 [`research/zed2i_stereo_vio_arxiv_review.md`](zed2i_stereo_vio_arxiv_review.md) 第 F 节的论文映射表",
|
||||
"- [ ] 对 🇨🇳 标记的论文重点核查机构归属,更新 G 节国产团队清单",
|
||||
"- [ ] 把 GitHub 仓库中 stars > 5k 的项目加入 [`plans/camera/github_opensource_projects.md`](../plans/camera/github_opensource_projects.md)",
|
||||
"- [ ] 每周重跑 [`research/search_info.py`](search_info.py) 增量更新",
|
||||
"",
|
||||
"## I.3 复现方法",
|
||||
"",
|
||||
"```bash",
|
||||
"# 通过 127.0.0.1:6984 代理拉取最新数据",
|
||||
"HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \\",
|
||||
" python3 research/search_info.py \\",
|
||||
" --proxy http://127.0.0.1:6984 \\",
|
||||
" --max-results 10 --delay 5.0 \\",
|
||||
" --out research/search_results.json",
|
||||
"",
|
||||
"# 渲染为 markdown",
|
||||
"python3 research/gen_review_from_json.py \\",
|
||||
" --in research/search_results.json \\",
|
||||
" --out research/zed2i_arxiv_live_review.md",
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"**说明**:",
|
||||
"- 本文档由脚本自动生成,可重复执行覆盖",
|
||||
"- 🇨🇳 标记基于作者/摘要中是否包含中国机构关键词的启发式判断,**仅供参考,需人工复核**",
|
||||
"- 摘要截断到 400 字符以控制文档体积",
|
||||
"- 与 [`zed2i_stereo_vio_arxiv_review.md`](zed2i_stereo_vio_arxiv_review.md)(人工综述)互为补充:人工综述给方法论与映射,本文档给最新原始素材",
|
||||
]
|
||||
)
|
||||
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
print(f"✅ 生成: {args.out}")
|
||||
print(f" arXiv: {total_papers} 篇,国产疑似: {len(china_papers)}")
|
||||
print(f" GitHub: {total_repos} 仓库")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,319 @@
|
||||
"""arXiv + GitHub 检索工具(支持 HTTP / SOCKS5 代理)
|
||||
|
||||
用法:
|
||||
# 1. 直连
|
||||
python3 research/search_info.py
|
||||
|
||||
# 2. HTTP 代理(推荐:6984 同时支持 http/socks5)
|
||||
HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \
|
||||
python3 research/search_info.py
|
||||
|
||||
# 3. SOCKS5 代理(需 pip install pysocks)
|
||||
ALL_PROXY=socks5h://127.0.0.1:6984 python3 research/search_info.py
|
||||
|
||||
# 4. 命令行指定(覆盖环境变量)
|
||||
python3 research/search_info.py --proxy http://127.0.0.1:6984
|
||||
|
||||
输出:
|
||||
research/search_results.json — 结构化 JSON(按主题分组)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 主题化查询(与 plans/camera/zed2i_iterative_framework.md 五条主线对齐)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
TOPICS: dict[str, str] = {
|
||||
"stereo_matching": 'all:"stereo matching" AND (all:"foundation" OR all:"zero-shot" OR all:"transformer")',
|
||||
"visual_inertial_slam": 'all:("visual inertial" OR "VIO" OR "VI-SLAM") AND cat:cs.CV',
|
||||
"gaussian_splatting_slam": 'all:"gaussian splatting" AND all:"SLAM"',
|
||||
"monocular_depth_foundation": 'all:("depth anything" OR "metric3d" OR "marigold" OR "UniDepth")',
|
||||
"indoor_rgbd_dataset": 'all:("RGB-D dataset" OR "indoor dataset") AND all:"reconstruction"',
|
||||
"world_model_video": 'all:"world model" AND (all:"video" OR all:"prediction" OR all:"generative")',
|
||||
"zed_camera": 'all:("ZED 2i" OR "ZED stereo" OR "ZED-2i" OR "Stereolabs")',
|
||||
"orbbec_gemini": 'all:("Orbbec" OR "Femto Mega" OR "Femto Bolt" OR "Azure Kinect") AND all:("depth" OR "RGB-D" OR "SLAM")',
|
||||
"rgbd_indoor_reconstruction": 'all:("RGB-D" OR "RGBD") AND all:("indoor" OR "room") AND (all:"reconstruction" OR all:"SLAM")',
|
||||
"neural_stereo_depth": 'all:("foundation stereo" OR "RAFT-Stereo" OR "IGEV") AND all:"depth"',
|
||||
}
|
||||
|
||||
# GitHub 主题化查询
|
||||
GITHUB_TOPICS: dict[str, str] = {
|
||||
"stereo_matching": "stereo matching depth in:name,description,readme stars:>500",
|
||||
"vio_slam": "visual inertial SLAM in:name,description,readme stars:>500",
|
||||
"gaussian_splatting": "gaussian splatting in:name,description,readme stars:>1000",
|
||||
"monocular_depth": "monocular depth estimation in:name,description,readme stars:>1000",
|
||||
"world_model": "world model in:name,description,readme stars:>500",
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 代理配置
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
def build_opener(proxy: str | None) -> urllib.request.OpenerDirector:
|
||||
"""构建带代理的 urllib opener。
|
||||
|
||||
proxy 形式:
|
||||
- http://127.0.0.1:6984
|
||||
- https://127.0.0.1:6984
|
||||
- socks5h://127.0.0.1:6984 (需 pysocks)
|
||||
- None → 走环境变量 (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) 或直连
|
||||
"""
|
||||
if proxy and proxy.startswith(("socks5://", "socks5h://", "socks4://")):
|
||||
try:
|
||||
import socks # type: ignore
|
||||
import socket
|
||||
|
||||
scheme, rest = proxy.split("://", 1)
|
||||
host, port_str = rest.split(":")
|
||||
port = int(port_str)
|
||||
socks_type = {
|
||||
"socks5": socks.SOCKS5,
|
||||
"socks5h": socks.SOCKS5,
|
||||
"socks4": socks.SOCKS4,
|
||||
}[scheme]
|
||||
socks.set_default_proxy(socks_type, host, port, rdns=(scheme == "socks5h"))
|
||||
socket.socket = socks.socksocket # type: ignore[assignment]
|
||||
print(f"[proxy] SOCKS5 启用: {proxy}", file=sys.stderr)
|
||||
return urllib.request.build_opener()
|
||||
except ImportError:
|
||||
print(
|
||||
"[proxy] 警告: 需要 SOCKS5 但未安装 pysocks,将回退到环境变量。\n"
|
||||
" 修复: pip install pysocks",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return urllib.request.build_opener()
|
||||
|
||||
if proxy:
|
||||
handler = urllib.request.ProxyHandler({"http": proxy, "https": proxy})
|
||||
print(f"[proxy] HTTP(S) 代理启用: {proxy}", file=sys.stderr)
|
||||
return urllib.request.build_opener(handler)
|
||||
|
||||
# 走环境变量
|
||||
env_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or os.environ.get("ALL_PROXY")
|
||||
if env_proxy:
|
||||
print(f"[proxy] 使用环境变量代理: {env_proxy}", file=sys.stderr)
|
||||
else:
|
||||
print("[proxy] 直连模式(无代理)", file=sys.stderr)
|
||||
return urllib.request.build_opener()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# arXiv
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
def search_arxiv(
|
||||
opener: urllib.request.OpenerDirector,
|
||||
query: str,
|
||||
max_results: int = 20,
|
||||
sort_by: str = "submittedDate",
|
||||
sort_order: str = "descending",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""调用 arXiv API 并解析返回的 ATOM XML。"""
|
||||
base = "https://export.arxiv.org/api/query"
|
||||
params = urllib.parse.urlencode(
|
||||
{
|
||||
"search_query": query,
|
||||
"start": 0,
|
||||
"max_results": max_results,
|
||||
"sortBy": sort_by,
|
||||
"sortOrder": sort_order,
|
||||
}
|
||||
)
|
||||
url = f"{base}?{params}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (research-bot)"})
|
||||
|
||||
# arXiv 限速重试(429 → 指数退避)
|
||||
data = ""
|
||||
for attempt in range(4):
|
||||
try:
|
||||
with opener.open(req, timeout=30) as resp:
|
||||
data = resp.read().decode("utf-8")
|
||||
break
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429:
|
||||
wait = 10 * (attempt + 1)
|
||||
print(
|
||||
f"[arxiv][{query[:40]}...] 429 限速,等待 {wait}s 重试 ({attempt+1}/4)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
print(f"[arxiv][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
|
||||
return []
|
||||
except (urllib.error.URLError, TimeoutError) as e:
|
||||
print(f"[arxiv][{query[:40]}...] ERROR: {e}", file=sys.stderr)
|
||||
return []
|
||||
else:
|
||||
print(f"[arxiv][{query[:40]}...] 持续 429,放弃", file=sys.stderr)
|
||||
return []
|
||||
|
||||
try:
|
||||
root = ET.fromstring(data)
|
||||
except ET.ParseError as e:
|
||||
print(f"[arxiv][{query[:40]}...] XML parse error: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
|
||||
papers: list[dict[str, Any]] = []
|
||||
|
||||
for entry in root.findall("atom:entry", ns):
|
||||
|
||||
def _text(elem_path: str) -> str:
|
||||
elem = entry.find(elem_path, ns)
|
||||
return (elem.text or "").replace("\n", " ").strip() if elem is not None else ""
|
||||
|
||||
arxiv_id_url = _text("atom:id")
|
||||
arxiv_id = arxiv_id_url.rsplit("/", 1)[-1] if arxiv_id_url else ""
|
||||
# 去掉版本号 e.g. 2501.09898v2 → 2501.09898
|
||||
arxiv_id_clean = arxiv_id.split("v")[0] if arxiv_id else ""
|
||||
|
||||
authors: list[str] = []
|
||||
for a in entry.findall("atom:author", ns):
|
||||
name_elem = a.find("atom:name", ns)
|
||||
if name_elem is not None and name_elem.text:
|
||||
authors.append(name_elem.text.strip())
|
||||
|
||||
categories = [
|
||||
c.attrib.get("term", "") for c in entry.findall("atom:category", ns)
|
||||
]
|
||||
|
||||
summary = _text("atom:summary")
|
||||
papers.append(
|
||||
{
|
||||
"arxiv_id": arxiv_id_clean,
|
||||
"url": f"https://arxiv.org/abs/{arxiv_id_clean}" if arxiv_id_clean else arxiv_id_url,
|
||||
"title": _text("atom:title"),
|
||||
"published": _text("atom:published"),
|
||||
"updated": _text("atom:updated"),
|
||||
"authors": authors,
|
||||
"categories": categories,
|
||||
"summary": summary[:400] + ("..." if len(summary) > 400 else ""),
|
||||
}
|
||||
)
|
||||
|
||||
return papers
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# GitHub
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
def search_github(
|
||||
opener: urllib.request.OpenerDirector,
|
||||
query: str,
|
||||
per_page: int = 10,
|
||||
) -> list[dict[str, Any]]:
|
||||
base = "https://api.github.com/search/repositories"
|
||||
params = urllib.parse.urlencode({"q": query, "sort": "stars", "order": "desc", "per_page": per_page})
|
||||
url = f"{base}?{params}"
|
||||
|
||||
headers = {"User-Agent": "Mozilla/5.0 (research-bot)", "Accept": "application/vnd.github+json"}
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
|
||||
try:
|
||||
with opener.open(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
# 60 req/hr 限速时 GitHub 返回 403
|
||||
print(f"[github][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
|
||||
return []
|
||||
except (urllib.error.URLError, TimeoutError) as e:
|
||||
print(f"[github][{query[:40]}...] ERROR: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
repos: list[dict[str, Any]] = []
|
||||
for item in data.get("items", []):
|
||||
repos.append(
|
||||
{
|
||||
"name": item.get("full_name", ""),
|
||||
"description": item.get("description", "") or "",
|
||||
"stars": item.get("stargazers_count", 0),
|
||||
"forks": item.get("forks_count", 0),
|
||||
"language": item.get("language", "") or "",
|
||||
"url": item.get("html_url", ""),
|
||||
"updated_at": item.get("updated_at", ""),
|
||||
}
|
||||
)
|
||||
return repos
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 主入口
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="arXiv + GitHub 主题化检索")
|
||||
parser.add_argument("--proxy", default=None, help="代理 URL,如 http://127.0.0.1:6984")
|
||||
parser.add_argument("--max-results", type=int, default=15, help="每个主题最多论文数")
|
||||
parser.add_argument("--out", default="research/data/search_results.json", help="输出 JSON 路径")
|
||||
parser.add_argument(
|
||||
"--topics",
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="只跑指定主题(默认全部),如:--topics stereo_matching world_model_video",
|
||||
)
|
||||
parser.add_argument("--no-github", action="store_true", help="跳过 GitHub 检索")
|
||||
parser.add_argument("--delay", type=float, default=3.0, help="主题间延迟(秒),arXiv 建议 ≥3s")
|
||||
args = parser.parse_args()
|
||||
|
||||
opener = build_opener(args.proxy)
|
||||
|
||||
selected = args.topics or list(TOPICS.keys())
|
||||
results: dict[str, Any] = {
|
||||
"meta": {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"proxy": args.proxy or os.environ.get("HTTPS_PROXY") or "direct",
|
||||
"max_results_per_topic": args.max_results,
|
||||
},
|
||||
"arxiv": {},
|
||||
"github": {},
|
||||
}
|
||||
|
||||
# arXiv
|
||||
for topic in selected:
|
||||
if topic not in TOPICS:
|
||||
print(f"[skip] 未知主题: {topic}", file=sys.stderr)
|
||||
continue
|
||||
print(f"[arxiv] 检索主题: {topic}", file=sys.stderr)
|
||||
papers = search_arxiv(opener, TOPICS[topic], max_results=args.max_results)
|
||||
results["arxiv"][topic] = papers
|
||||
print(f" → {len(papers)} 篇", file=sys.stderr)
|
||||
time.sleep(args.delay) # arXiv API rate limit
|
||||
|
||||
# GitHub
|
||||
if not args.no_github:
|
||||
for topic, q in GITHUB_TOPICS.items():
|
||||
if args.topics and topic not in args.topics:
|
||||
continue
|
||||
print(f"[github] 检索主题: {topic}", file=sys.stderr)
|
||||
repos = search_github(opener, q, per_page=10)
|
||||
results["github"][topic] = repos
|
||||
print(f" → {len(repos)} 个仓库", file=sys.stderr)
|
||||
time.sleep(1.0)
|
||||
|
||||
out_path = args.out
|
||||
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||
|
||||
total_papers = sum(len(v) for v in results["arxiv"].values())
|
||||
total_repos = sum(len(v) for v in results["github"].values())
|
||||
print(f"\n✅ 完成:{total_papers} 篇 arXiv + {total_repos} 个 GitHub 仓库 → {out_path}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user