Zum Inhalt springen

Scan-Dateien herunterladen

Diese Anleitung erklärt, wie Sie verarbeitete Scan-Ausgaben über die RealityConnect API herunterladen: Punktwolken, Meshes und Photosphere-Bilder. Der Workflow nutzt den Client-Credentials-Flow und den Scope download:scan.


  • Eine OAuth-Anwendung, die für Client Credentials konfiguriert ist
  • Scopes: read:basic, read:hierarchy, download:scan
  • Ihre client_id und client_secret
  • Inhaltszugriff auf die Site, die die Bundles enthält, die Sie herunterladen möchten

read:twin und read:asset sind für den Download von Scan-Dateien nicht erforderlich.

Verarbeitete Scan-Daten liegen in Bundles, die an eine Site angehängt sind. Jedes Bundle enthält eine oder mehrere Komponenten, wobei jede Komponente ein anderes Ausgabeformat darstellt:

KomponententypFormatBeschreibung
OgcPointCloudHlodOGC 3D Tiles (offener Standard)Punktwolken-Daten. tileset.json als Einstiegspunkt mit .glb-Tile-Dateien
RealityMeshHlodPrevu3D HLODMesh-Daten. hlod_tree.json als Einstiegspunkt mit .pvt-Tile-Dateien
RealityPhotosphereJPEG-BilderPhotosphere-Daten. stations.json-Manifest mit .jpeg-Bildern
flowchart LR
  A["1. Authentifizieren"] --> B["2. Site finden"]
  B --> C["3. Bundles auflisten"]
  C --> D["4. Bundle-Details abrufen"]
  D --> E["5. Einstiegsdateien herunterladen"]
  E --> F["6. Dateireferenzen folgen"]
SchrittEndpunktWas Sie erhalten
AuthentifizierenPOST /oauth/tokenAccess Token
API-URL auflösenGET /oauth/api-infoRegionale apiUrl und Organisations-ID
Site findenGET /v1/nodes/{id}/browseSite-ID aus der Inhaltshierarchie
Bundles auflistenGET /v1/site/{siteId}/bundlesBundle-IDs für die Site
Komponenten abrufenGET /v1/site/{siteId}/bundles/{bundleId}signedLink und roots pro Komponente
Dateien herunterladenGET auf die konstruierte CDN-URLEinstiegsdateien, Tiles und Bilder

Access Token mit Client Credentials abrufen:

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

Regionale API-URL auflösen:

GET https://cloud-api.prevu3d.com/oauth/api-info
Authorization: Bearer {access_token}

Die Antwort enthält apiUrl (zum Beispiel https://api-ue1.prevu3d.com/realityconnect-api). Verwenden Sie diese als Basis-URL für alle nachfolgenden API-Aufrufe.

Durchsuchen Sie die Knotenhierarchie, beginnend bei Ihrer Organisation:

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

Durchlaufen Sie den Baum rekursiv, bis Sie einen Knoten mit type: "Site" finden. Notieren Sie dessen id.

GET {api_url}/v1/site/{siteId}/bundles
Authorization: Bearer {access_token}

Gibt eine paginierte Liste von Bundles für diese Site zurück.

GET {api_url}/v1/site/{siteId}/bundles/{bundleId}
Authorization: Bearer {access_token}

Beispielantwort:

{
"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"]
}
]
}

Jede Komponente stellt zwei Felder bereit, die Sie für den Download benötigen:

  • signedLink: eine signierte CDN-URL, die mit /* endet. Das * ist ein Platzhalter. Die Signatur gewährt Lesezugriff auf alle Dateien unter diesem Präfix, nicht nur auf eine einzelne Datei.
  • roots: ein Array relativer Pfade zu den Einstiegsdatei(en) für diese Komponente.

Konstruieren Sie eine herunterladbare URL, indem Sie das * in signedLink durch einen Dateipfad ersetzen:

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

Die Query-Parameter (Policy, Signature, Key-Pair-Id) bleiben angehängt und authentifizieren die Anfrage. Laden Sie Dateibytes direkt vom CDN herunter, ohne Authorization-Header beim GET.

Einstiegspunkt: jeder Wert in roots ist ein Pfad zu einer tileset.json-Datei (eine pro Scan-Session).

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

Tile-Traversierung: tileset.json folgt der OGC 3D Tiles-Spezifikation. Es enthält einen Baum von Tile-Knoten. Jeder Knoten kann ein content-Feld mit einer uri haben, die auf eine .glb-Datei relativ zum Tileset-Verzeichnis verweist:

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

Lösen Sie Tile-Pfade relativ zum Tileset-Verzeichnis auf:

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

Durchlaufen Sie den Baum rekursiv, um alle Tiles herunterzuladen.

Einstiegspunkt: roots enthält ["hlod_tree.json"].

url = component["signedLink"].replace("*", "hlod_tree.json")
hlod_tree = requests.get(url).json()

Baumstruktur: hlod_tree.json enthält einen HLOD-Baum:

{
"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" }
]
}
}

Tile-Dateibenennung: Geometrie- und Texturdateien verwenden das Muster {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

Durchlaufen Sie das children-Array rekursiv, um alle model_path-Werte auf jeder Detailstufe zu ermitteln.

Einstiegspunkt: roots enthält ["stations.json"].

url = component["signedLink"].replace("*", "stations.json")
stations = requests.get(url).json()

Zusätzliche Dateien:

  • index.tsv: eine tabulatorgetrennte Indexdatei neben stations.json
  • Photosphere-Bilder folgen dem Muster 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

Parsen Sie stations.json, um alle Stationsverzeichnisse und die zugehörigen Bilddateien zu ermitteln.

Dieses Skript authentifiziert, findet eine Site, ruft das erste Bundle ab, lädt Einstiegsdateien herunter und folgt einer referenzierten Tile oder einem Bild pro Komponente.

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