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.
Voraussetzungen
Abschnitt betitelt „Voraussetzungen“- Eine OAuth-Anwendung, die für Client Credentials konfiguriert ist
- Scopes:
read:basic,read:hierarchy,download:scan - Ihre
client_idundclient_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.
Wie Scan-Daten organisiert sind
Abschnitt betitelt „Wie Scan-Daten organisiert sind“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:
| Komponententyp | Format | Beschreibung |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles (offener Standard) | Punktwolken-Daten. tileset.json als Einstiegspunkt mit .glb-Tile-Dateien |
RealityMeshHlod | Prevu3D HLOD | Mesh-Daten. hlod_tree.json als Einstiegspunkt mit .pvt-Tile-Dateien |
RealityPhotosphere | JPEG-Bilder | Photosphere-Daten. stations.json-Manifest mit .jpeg-Bildern |
End-to-End-Ablauf
Abschnitt betitelt „End-to-End-Ablauf“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"]
| Schritt | Endpunkt | Was Sie erhalten |
|---|---|---|
| Authentifizieren | POST /oauth/token | Access Token |
| API-URL auflösen | GET /oauth/api-info | Regionale apiUrl und Organisations-ID |
| Site finden | GET /v1/nodes/{id}/browse | Site-ID aus der Inhaltshierarchie |
| Bundles auflisten | GET /v1/site/{siteId}/bundles | Bundle-IDs für die Site |
| Komponenten abrufen | GET /v1/site/{siteId}/bundles/{bundleId} | signedLink und roots pro Komponente |
| Dateien herunterladen | GET auf die konstruierte CDN-URL | Einstiegsdateien, Tiles und Bilder |
Schritt 1 — Authentifizieren
Abschnitt betitelt „Schritt 1 — Authentifizieren“Access Token mit Client Credentials abrufen:
POST https://cloud-api.prevu3d.com/oauth/tokenAuthorization: Basic base64(client_id:client_secret)Content-Type: application/x-www-form-urlencoded
grant_type=client_credentialsRegionale API-URL auflösen:
GET https://cloud-api.prevu3d.com/oauth/api-infoAuthorization: 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.
Schritt 2 — Site finden
Abschnitt betitelt „Schritt 2 — Site finden“Durchsuchen Sie die Knotenhierarchie, beginnend bei Ihrer Organisation:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}Durchlaufen Sie den Baum rekursiv, bis Sie einen Knoten mit type: "Site" finden. Notieren Sie dessen id.
Schritt 3 — Bundles auflisten
Abschnitt betitelt „Schritt 3 — Bundles auflisten“GET {api_url}/v1/site/{siteId}/bundlesAuthorization: Bearer {access_token}Gibt eine paginierte Liste von Bundles für diese Site zurück.
Schritt 4 — Bundle-Details abrufen
Abschnitt betitelt „Schritt 4 — Bundle-Details abrufen“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.
Schritt 5 — Dateien herunterladen
Abschnitt betitelt „Schritt 5 — Dateien herunterladen“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.
Komponentenspezifische Details
Abschnitt betitelt „Komponentenspezifische Details“OgcPointCloudHlod (Punktwolken)
Abschnitt betitelt „OgcPointCloudHlod (Punktwolken)“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).contentDurchlaufen Sie den Baum rekursiv, um alle Tiles herunterzuladen.
RealityMeshHlod (Meshes)
Abschnitt betitelt „RealityMeshHlod (Meshes)“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).contentDurchlaufen Sie das children-Array rekursiv, um alle model_path-Werte auf jeder Detailstufe zu ermitteln.
RealityPhotosphere (Photospheres)
Abschnitt betitelt „RealityPhotosphere (Photospheres)“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 nebenstations.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).contentParsen Sie stations.json, um alle Stationsverzeichnisse und die zugehörigen Bilddateien zu ermitteln.
Vollständiges Beispiel
Abschnitt betitelt „Vollständiges Beispiel“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 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")
breakWie geht es weiter?
Abschnitt betitelt „Wie geht es weiter?“- Richten Sie die Authentifizierung End-to-End in der Anleitung Client-Credentials-Flow ein.
- Laden Sie neue Aufnahmen hoch und verarbeiten Sie sie mit Data Bundle Workflows.
- Durchsuchen Sie alle Operationen in der API-Referenz.