コンテンツにスキップ

スキャンファイルのダウンロード

このガイドでは、RealityConnect APIから処理済みスキャン出力(点群、メッシュ、フォトスフィア画像)をダウンロードする方法を説明します。このワークフローでは、Client Credentialsフローdownload:scan スコープを使用します。


  • Client Credentials 用に設定されたOAuthアプリケーション
  • スコープ: read:basicread:hierarchydownload:scan
  • client_idclient_secret
  • ダウンロード対象のバンドルを保持するSiteへのコンテンツアクセス

スキャンファイルのダウンロードには read:twinread:asset不要です。

処理済みスキャンデータは Site に添付された Data Bundle 内に格納されます。各バンドルには1つ以上の コンポーネント が含まれ、各コンポーネントは異なる出力形式を表します:

コンポーネントタイプ形式説明
OgcPointCloudHlodOGC 3D Tiles(オープン標準)点群データ。tileset.json エントリポイントと .glb タイルファイル
RealityMeshHlodPrevu3D HLODメッシュデータ。hlod_tree.json エントリポイントと .pvt タイルファイル
RealityPhotosphereJPEG画像フォトスフィアデータ。stations.json マニフェストと .jpeg 画像
flowchart LR
  A["1. 認証"] --> B["2. Siteを検索"]
  B --> C["3. バンドルを一覧表示"]
  C --> D["4. バンドル詳細を取得"]
  D --> E["5. エントリファイルをダウンロード"]
  E --> F["6. ファイル参照をたどる"]
ステップエンドポイント取得内容
認証POST /oauth/tokenアクセストークン
API URLの解決GET /oauth/api-infoリージョンの apiUrl と組織ID
Siteの検索GET /v1/nodes/{id}/browseコンテンツ階層からのSite ID
バンドルの一覧GET /v1/site/{siteId}/bundlesSiteのバンドルID
コンポーネントの取得GET /v1/site/{siteId}/bundles/{bundleId}コンポーネントごとの signedLinkroots
ファイルのダウンロード構築したCDN URLへの GETエントリファイル、タイル、画像

Client Credentialsでアクセストークンを取得します:

POST https://cloud-api.prevu3d.com/oauth/token
Authorization: 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-info
Authorization: Bearer {access_token}

レスポンスには apiUrl(例: https://api-ue1.prevu3d.com/realityconnect-api)が含まれます。以降のすべてのAPI呼び出しのベースURLとして使用してください。

組織からノード階層の参照を開始します:

GET {api_url}/v1/nodes/{organizationId}/browse
Authorization: Bearer {access_token}

type: "Site" のノードが見つかるまで、ツリーを再帰的にたどります。その id を記録してください。

ステップ3 — バンドルを一覧表示

Section titled “ステップ3 — バンドルを一覧表示”
GET {api_url}/v1/site/{siteId}/bundles
Authorization: Bearer {access_token}

そのSiteのバンドルのページネーション付きリストを返します。

ステップ4 — バンドル詳細を取得

Section titled “ステップ4 — バンドル詳細を取得”
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"]
}
]
}

各コンポーネントには、ダウンロードに必要な2つのフィールドが公開されています:

  • signedLink: /* で終わるCDN署名付きURL。* はプレースホルダーです。署名により、このプレフィックス配下のすべてのファイルへの読み取りアクセスが付与されます(単一ファイルだけではありません)。
  • roots: このコンポーネントのエントリポイントファイルへの相対パスの配列。

ステップ5 — ファイルをダウンロード

Section titled “ステップ5 — ファイルをダウンロード”

signedLink 内の * をファイルパスに置き換えて、ダウンロード可能なURLを構築します:

download_url = component["signedLink"].replace("*", root)
response = requests.get(download_url)

クエリパラメータ(PolicySignatureKey-Pair-Id)はそのまま付与され、リクエストを認証します。GETリクエストには Authorization ヘッダーは不要で、CDNからファイルバイトを直接ダウンロードします。

エントリポイント: roots の各値は tileset.json ファイルへのパスです(スキャンセッションごとに1つ)。

for root in component["roots"]:
url = component["signedLink"].replace("*", root)
tileset = requests.get(url).json()

タイルの走査: tileset.jsonOGC 3D Tiles仕様に従います。タイルノードのツリーが含まれます。各ノードには content フィールドがあり、uri でタイルセットディレクトリからの相対パスとして .glb ファイルを指します:

{
"asset": { "version": "1.0" },
"root": {
"boundingVolume": { },
"content": { "uri": "cell.glb" },
"children": [
{
"content": { "uri": "cell0.glb" },
"children": [ ]
}
]
}
}

タイルパスをタイルセットディレクトリからの相対パスとして解決します:

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

ツリーを再帰的に走査して、すべてのタイルをダウンロードします。

エントリポイント: 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

すべての詳細レベルで model_path 値を発見するには、children 配列を再帰的に走査します。

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 を解析して、すべてのステーションディレクトリと関連する画像ファイルを発見します。

このスクリプトは認証し、Siteを検索し、最初のバンドルを取得し、エントリポイントファイルをダウンロードし、コンポーネントごとに1つの参照タイルまたは画像をたどります。

import base64
import json
import os
from 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 = None
visited = 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