スキャンファイルのダウンロード
このガイドでは、RealityConnect APIから処理済みスキャン出力(点群、メッシュ、フォトスフィア画像)をダウンロードする方法を説明します。このワークフローでは、Client Credentialsフローと download:scan スコープを使用します。
- Client Credentials 用に設定されたOAuthアプリケーション
- スコープ:
read:basic、read:hierarchy、download:scan client_idとclient_secret- ダウンロード対象のバンドルを保持するSiteへのコンテンツアクセス
スキャンファイルのダウンロードには read:twin と read:asset は不要です。
スキャンデータの構成
Section titled “スキャンデータの構成”処理済みスキャンデータは Site に添付された Data Bundle 内に格納されます。各バンドルには1つ以上の コンポーネント が含まれ、各コンポーネントは異なる出力形式を表します:
| コンポーネントタイプ | 形式 | 説明 |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles(オープン標準) | 点群データ。tileset.json エントリポイントと .glb タイルファイル |
RealityMeshHlod | Prevu3D HLOD | メッシュデータ。hlod_tree.json エントリポイントと .pvt タイルファイル |
RealityPhotosphere | JPEG画像 | フォトスフィアデータ。stations.json マニフェストと .jpeg 画像 |
エンドツーエンドのフロー
Section titled “エンドツーエンドのフロー”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}/bundles | Siteのバンドル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 — Siteを検索
Section titled “ステップ2 — Siteを検索”組織からノード階層の参照を開始します:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}type: "Site" のノードが見つかるまで、ツリーを再帰的にたどります。その id を記録してください。
ステップ3 — バンドルを一覧表示
Section titled “ステップ3 — バンドルを一覧表示”GET {api_url}/v1/site/{siteId}/bundlesAuthorization: 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)クエリパラメータ(Policy、Signature、Key-Pair-Id)はそのまま付与され、リクエストを認証します。GETリクエストには Authorization ヘッダーは不要で、CDNからファイルバイトを直接ダウンロードします。
コンポーネント別の詳細
Section titled “コンポーネント別の詳細”OgcPointCloudHlod(点群)
Section titled “OgcPointCloudHlod(点群)”エントリポイント: roots の各値は tileset.json ファイルへのパスです(スキャンセッションごとに1つ)。
for root in component["roots"]: url = component["signedLink"].replace("*", root) tileset = requests.get(url).json()タイルの走査: tileset.json は OGC 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ツリーを再帰的に走査して、すべてのタイルをダウンロードします。
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すべての詳細レベルで 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).contentstations.json を解析して、すべてのステーションディレクトリと関連する画像ファイルを発見します。
このスクリプトは認証し、Siteを検索し、最初のバンドルを取得し、エントリポイントファイルをダウンロードし、コンポーネントごとに1つの参照タイルまたは画像をたどります。
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リファレンスですべての操作を参照します。