Ga naar inhoud

Scanbestanden downloaden

Deze gids legt uit hoe u verwerkte scanuitvoer downloadt van de RealityConnect API: puntenwolken, meshes en photosphere-afbeeldingen. De workflow gebruikt de Client Credentials-flow en de download:scan-scope.


  • Een OAuth-applicatie geconfigureerd voor Client Credentials
  • Scopes: read:basic, read:hierarchy, download:scan
  • Uw client_id en client_secret
  • Inhoudstoegang tot de site die de bundles bevat die u wilt downloaden

read:twin en read:asset zijn niet vereist voor het downloaden van scanbestanden.

Verwerkte scangegevens bevinden zich in bundles die aan een site zijn gekoppeld. Elke bundle bevat een of meer componenten, waarbij elke component een ander uitvoerformaat is:

ComponenttypeFormaatBeschrijving
OgcPointCloudHlodOGC 3D Tiles (open standaard)Puntenwolkgegevens. tileset.json-toegangspunt met .glb-tegelbestanden
RealityMeshHlodPrevu3D HLODMeshgegevens. hlod_tree.json-toegangspunt met .pvt-tegelbestanden
RealityPhotosphereJPEG-afbeeldingenPhotosphere-gegevens. stations.json-manifest met .jpeg-afbeeldingen
flowchart LR
  A["1. Authenticeren"] --> B["2. Een site vinden"]
  B --> C["3. Bundles opsommen"]
  C --> D["4. Bundledetails ophalen"]
  D --> E["5. Toegangsbestanden downloaden"]
  E --> F["6. Bestandsverwijzingen volgen"]
StapEndpointWat u krijgt
AuthenticerenPOST /oauth/tokenAccess token
API-URL oplossenGET /oauth/api-infoRegionale apiUrl en organisatie-ID
Site vindenGET /v1/nodes/{id}/browseSite-ID uit de inhoudshiërarchie
Bundles opsommenGET /v1/site/{siteId}/bundlesBundle-ID’s voor de site
Componenten ophalenGET /v1/site/{siteId}/bundles/{bundleId}signedLink en roots per component
Bestanden downloadenGET op de samengestelde CDN-URLToegangsbestanden, tegels en afbeeldingen

Verkrijg een access token met 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

Los uw regionale API-URL op:

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

De response bevat apiUrl (bijvoorbeeld https://api-ue1.prevu3d.com/realityconnect-api). Gebruik dit als de basis-URL voor alle volgende API-aanroepen.

Blader door de node-hiërarchie, beginnend bij uw organisatie:

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

Doorloop de boom recursief totdat u een node met type: "Site" vindt. Noteer de id.

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

Retourneert een gepagineerde lijst met bundles voor die site.

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

Voorbeeldresponse:

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

Elke component stelt twee velden beschikbaar die u nodig hebt om te downloaden:

  • signedLink: een ondertekende CDN-URL die eindigt op /*. De * is een tijdelijke aanduiding. De handtekening verleent leestoegang tot alle bestanden onder dit prefix, niet slechts één bestand.
  • roots: een array met relatieve paden naar het (de) toegangspuntbestand(en) voor deze component.

Stel een downloadbare URL samen door de * in signedLink te vervangen door een bestandspad:

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

De queryparameters (Policy, Signature, Key-Pair-Id) blijven behouden en authenticeren het verzoek. Download bestandsbytes rechtstreeks van de CDN zonder Authorization-header op de GET.

Toegangspunt: elke waarde in roots is een pad naar een tileset.json-bestand (één per scansessie).

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

Tegeltraversal: tileset.json volgt de OGC 3D Tiles-specificatie. Het bevat een boom van tegelnodes. Elke node kan een content-veld hebben met een uri die verwijst naar een .glb-bestand ten opzichte van de tileset-directory:

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

Los tegelpaden op ten opzichte van de tileset-directory:

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

Doorloop de boom recursief om alle tegels te downloaden.

Toegangspunt: roots bevat ["hlod_tree.json"].

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

Boomstructuur: hlod_tree.json bevat een HLOD-boom:

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

Naamgeving van tegelbestanden: geometrie- en textuurbestanden gebruiken het patroon {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

Doorloop de children-array recursief om alle model_path-waarden op elk detailniveau te ontdekken.

Toegangspunt: roots bevat ["stations.json"].

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

Aanvullende bestanden:

  • index.tsv: een tab-gescheiden indexbestand naast stations.json
  • Photosphere-afbeeldingen volgen het patroon 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

Parseer stations.json om alle stationdirectory’s en de bijbehorende afbeeldingsbestanden te ontdekken.

Dit script authenticeert, vindt een site, haalt de eerste bundle op, downloadt toegangspuntbestanden en volgt één verwezen tegel of afbeelding per component.

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