下载扫描文件
本指南说明如何从 RealityConnect API 下载处理后的扫描输出:点云、网格和全景图像。该工作流程使用 Client Credentials 流程和 download:scan 作用域。
- 一个配置为 Client Credentials 的 OAuth 应用
- 作用域:
read:basic、read:hierarchy、download:scan - 你的
client_id和client_secret - 对持有你要下载的 Bundle 的站点具有内容访问权限
扫描文件下载不需要 read:twin 和 read:asset。
扫描数据的组织方式
Section titled “扫描数据的组织方式”处理后的扫描数据存在于附加到站点的 Bundle 中。每个 Bundle 包含一个或多个组件,每个组件是一种不同的输出格式:
| 组件类型 | 格式 | 说明 |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles(开放标准) | 点云数据。tileset.json 入口点,附带 .glb 瓦片文件 |
RealityMeshHlod | Prevu3D HLOD | 网格数据。hlod_tree.json 入口点,附带 .pvt 瓦片文件 |
RealityPhotosphere | JPEG 图像 | 全景数据。stations.json 清单,附带 .jpeg 图像 |
flowchart LR A["1. Authenticate"] --> B["2. Find a site"] B --> C["3. List bundles"] C --> D["4. Get bundle details"] D --> E["5. Download entry files"] E --> F["6. Follow file references"]
| 步骤 | 端点 | 你会得到什么 |
|---|---|---|
| 身份验证 | POST /oauth/token | 访问令牌 |
| 解析 API URL | GET /oauth/api-info | 区域 apiUrl 和组织 ID |
| 查找站点 | GET /v1/nodes/{id}/browse | 来自内容层级的站点 ID |
| 列出 Bundle | GET /v1/site/{siteId}/bundles | 站点的 Bundle ID |
| 获取组件 | GET /v1/site/{siteId}/bundles/{bundleId} | 每个组件的 signedLink 和 roots |
| 下载文件 | 对构造的 CDN URL 执行 GET | 入口文件、瓦片和图像 |
步骤 1 — 身份验证
Section titled “步骤 1 — 身份验证”使用 Client Credentials 获取访问令牌:
POST https://cloud-api.prevu3d.com/oauth/tokenAuthorization: Basic base64(client_id:client_secret)Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials解析你的区域 API URL:
GET https://cloud-api.prevu3d.com/oauth/api-infoAuthorization: Bearer {access_token}响应包含 apiUrl(例如 https://api-ue1.prevu3d.com/realityconnect-api)。将此用作所有后续 API 调用的基础 URL。
步骤 2 — 查找站点
Section titled “步骤 2 — 查找站点”从你的组织开始浏览节点层级:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}递归遍历树,直到找到一个 type: "Site" 的节点。记下它的 id。
步骤 3 — 列出 Bundle
Section titled “步骤 3 — 列出 Bundle”GET {api_url}/v1/site/{siteId}/bundlesAuthorization: Bearer {access_token}返回该站点的 Bundle 分页列表。
步骤 4 — 获取 Bundle 详情
Section titled “步骤 4 — 获取 Bundle 详情”GET {api_url}/v1/site/{siteId}/bundles/{bundleId}Authorization: Bearer {access_token}示例响应:
{ "id": "f75de673-...", "name": "My Bundle", "components": [ { "id": "abc123", "type": "OgcPointCloudHlod", "signedLink": "https://cdn.prevu3d.com/.../OgcPointCloudHlod/*?Policy=...&Signature=...&Key-Pair-Id=...", "roots": [ "sessions/0f4a60cb09a8/tileset.json", "sessions/2a5ed597dc0f/tileset.json" ] }, { "id": "def456", "type": "RealityMeshHlod", "signedLink": "https://cdn.prevu3d.com/.../RealityMeshHlod/*?Policy=...&Signature=...&Key-Pair-Id=...", "roots": ["hlod_tree.json"] }, { "id": "ghi789", "type": "RealityPhotosphere", "signedLink": "https://cdn.prevu3d.com/.../RealityPhotosphere/*?Policy=...&Signature=...&Key-Pair-Id=...", "roots": ["stations.json"] } ]}每个组件公开两个下载所需的字段:
signedLink:以/*结尾的 CDN 签名 URL。*是一个占位符。该签名授予对此前缀下所有文件的读取访问权限,而不仅仅是单个文件。roots:指向此组件入口点文件的相对路径数组。
步骤 5 — 下载文件
Section titled “步骤 5 — 下载文件”通过将 signedLink 中的 * 替换为文件路径来构造可下载的 URL:
download_url = component["signedLink"].replace("*", root)response = requests.get(download_url)查询参数(Policy、Signature、Key-Pair-Id)保持附加并对请求进行身份验证。直接从 CDN 下载文件字节,GET 请求上无需 Authorization 标头。
各组件的具体细节
Section titled “各组件的具体细节”OgcPointCloudHlod(点云)
Section titled “OgcPointCloudHlod(点云)”入口点: roots 中的每个值都是指向一个 tileset.json 文件的路径(每个扫描会话一个)。
for root in component["roots"]: url = component["signedLink"].replace("*", root) tileset = requests.get(url).json()瓦片遍历: tileset.json 遵循 OGC 3D Tiles 规范。它包含一棵瓦片节点树。每个节点可能有一个 content 字段,其 uri 指向相对于 tileset 目录的 .glb 文件:
{ "asset": { "version": "1.0" }, "root": { "boundingVolume": { }, "content": { "uri": "cell.glb" }, "children": [ { "content": { "uri": "cell0.glb" }, "children": [ ] } ] }}相对于 tileset 目录解析瓦片路径:
session_dir = root.rsplit("/tileset.json", 1)[0]tile_path = f"{session_dir}/{tile_uri}"tile_url = component["signedLink"].replace("*", tile_path)tile_data = requests.get(tile_url).content递归遍历树以下载所有瓦片。
RealityMeshHlod(网格)
Section titled “RealityMeshHlod(网格)”入口点: roots 包含 ["hlod_tree.json"]。
url = component["signedLink"].replace("*", "hlod_tree.json")hlod_tree = requests.get(url).json()树结构: hlod_tree.json 包含一棵 HLOD 树:
{ "environment_offset": [ ], "version": 1, "root": { "center": [1.17, -1.22, 5.63], "size": [50.54, 46.61, 19.63], "model_path": "cell", "density": 6.73, "children": [ { "model_path": "cell0", "children": [ ] } ], "textures": [ { "name": "color", "min_mip": 5, "max_mip": 11, "path": "cell" }, { "name": "ao", "min_mip": 5, "max_mip": 11, "path": "cell_ao" } ] }}瓦片文件命名: 几何和纹理文件使用 {model_path}_{mip_level}.pvt 模式:
model_path = node["model_path"]mip_level = texture["min_mip"]tile_file = f"{model_path}_{mip_level}.pvt"
tile_url = component["signedLink"].replace("*", tile_file)tile_data = requests.get(tile_url).content递归遍历 children 数组以发现每个细节层次上的所有 model_path 值。
RealityPhotosphere(全景图)
Section titled “RealityPhotosphere(全景图)”入口点: roots 包含 ["stations.json"]。
url = component["signedLink"].replace("*", "stations.json")stations = requests.get(url).json()其他文件:
index.tsv:与stations.json并列的制表符分隔索引文件- 全景图像遵循
station_{n}/H_{face}_{x}_{y}.jpeg模式
image_path = "station_1/H_0_0_0.jpeg"image_url = component["signedLink"].replace("*", image_path)image_data = requests.get(image_url).content解析 stations.json 以发现所有站点目录及其关联的图像文件。
此脚本进行身份验证、查找站点、获取第一个 Bundle、下载入口点文件,并为每个组件跟随一个被引用的瓦片或图像。
import base64import jsonimport osfrom pathlib import Path
import requests
CLIENT_ID = "your-client-id"CLIENT_SECRET = "your-client-secret"CLOUD_API_BASE = "https://cloud-api.prevu3d.com"OUTPUT_DIR = Path("downloads")
credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()token_response = requests.post( f"{CLOUD_API_BASE}/oauth/token", data={"grant_type": "client_credentials"}, headers={"Authorization": f"Basic {credentials}"},)token_response.raise_for_status()access_token = token_response.json()["access_token"]headers = {"Authorization": f"Bearer {access_token}"}
api_info = requests.get(f"{CLOUD_API_BASE}/oauth/api-info", headers=headers).json()api_url = api_info["apiUrl"].rstrip("/")organization_id = api_info["organization"]["id"]
def api_get(path: str) -> dict: response = requests.get(f"{api_url}{path}", headers=headers) response.raise_for_status() return response.json()
def download(url: str, dest: Path) -> bool: response = requests.get(url, timeout=30) if not response.ok: return False dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(response.content) return True
def find_tile_uri(node: dict, depth: int = 0): if depth > 5 or not isinstance(node, dict): return None content = node.get("content", {}) uri = content.get("uri") or content.get("url") if isinstance(uri, str) and not uri.startswith("http"): return uri for value in node.values(): if isinstance(value, dict): found = find_tile_uri(value, depth + 1) if found: return found elif isinstance(value, list): for item in value: found = find_tile_uri(item, depth + 1) if found: return found return None
queue = api_get(f"/v1/nodes/{organization_id}/browse").get("items", [])site_id = Nonevisited = set()
while queue and not site_id: node = queue.pop(0) if node["id"] in visited: continue visited.add(node["id"]) browse = api_get(f"/v1/nodes/{node['id']}/browse") current = browse.get("node", {}) if current.get("type", "").lower() == "site": site_id = current["id"] break queue.extend(browse.get("items", []))
if not site_id: raise RuntimeError("No site found in hierarchy")
bundles = api_get(f"/v1/site/{site_id}/bundles")bundle_id = bundles["items"][0]["id"]bundle = api_get(f"/v1/site/{site_id}/bundles/{bundle_id}")
for component in bundle.get("components", []): comp_type = component.get("type", "unknown") signed_link = component.get("signedLink", "") roots = component.get("roots", [])
if not signed_link or not roots: continue
for root in roots: entry_url = signed_link.replace("*", root) entry_dest = OUTPUT_DIR / comp_type / root.replace("/", os.sep) if not download(entry_url, entry_dest): continue
if root.endswith("tileset.json"): tileset = json.loads(entry_dest.read_bytes()) tile_uri = find_tile_uri(tileset.get("root", tileset)) if tile_uri: session_dir = root.rsplit("/tileset.json", 1)[0] tile_path = f"{session_dir}/{tile_uri}" if session_dir else tile_uri download(signed_link.replace("*", tile_path), OUTPUT_DIR / comp_type / tile_path.replace("/", os.sep))
elif root == "hlod_tree.json": tree = json.loads(entry_dest.read_bytes()) model_path = tree.get("root", {}).get("model_path", "") if model_path: tile_file = f"{model_path}_5.pvt" download(signed_link.replace("*", tile_file), OUTPUT_DIR / comp_type / tile_file)
elif root == "stations.json": download(signed_link.replace("*", "index.tsv"), OUTPUT_DIR / comp_type / "index.tsv") download(signed_link.replace("*", "station_1/H_0_0_0.jpeg"), OUTPUT_DIR / comp_type / "station_1" / "H_0_0_0.jpeg")
break下一步是什么?
Section titled “下一步是什么?”- 在 Client Credentials 流程指南中端到端地设置身份验证。
- 使用 Data Bundle 工作流上传并处理新的采集。
- 在 API 参考中浏览每个操作。