Scaricare i file di scansione
Questa guida spiega come scaricare gli output di scansione elaborati dalla RealityConnect API: nuvole di punti, mesh e immagini photosphere. Il flusso di lavoro usa il flusso Client Credentials e lo scope download:scan.
Prerequisiti
Sezione intitolata “Prerequisiti”- Un’applicazione OAuth configurata per Client Credentials
- Scope:
read:basic,read:hierarchy,download:scan - Il tuo
client_ideclient_secret - Accesso ai contenuti del sito che contiene i bundle che vuoi scaricare
read:twin e read:asset non sono richiesti per il download dei file di scansione.
Come sono organizzati i dati di scansione
Sezione intitolata “Come sono organizzati i dati di scansione”I dati di scansione elaborati risiedono in bundle collegati a un sito. Ogni bundle contiene uno o più componenti, dove ogni componente è un diverso formato di output:
| Tipo di componente | Formato | Descrizione |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles (standard aperto) | Dati della nuvola di punti. Punto di ingresso tileset.json con file di tile .glb |
RealityMeshHlod | Prevu3D HLOD | Dati mesh. Punto di ingresso hlod_tree.json con file di tile .pvt |
RealityPhotosphere | Immagini JPEG | Dati photosphere. Manifest stations.json con immagini .jpeg |
Flusso end-to-end
Sezione intitolata “Flusso end-to-end”flowchart LR A["1. Authenticate"] --> B["2. Find a site"] B --> C["3. List bundles"] C --> D["4. Get bundle details"] D --> E["5. Download entry files"] E --> F["6. Follow file references"]
| Passaggio | Endpoint | Cosa ottieni |
|---|---|---|
| Autenticazione | POST /oauth/token | Access token |
| Risoluzione dell’URL dell’API | GET /oauth/api-info | apiUrl regionale e ID dell’organizzazione |
| Trova il sito | GET /v1/nodes/{id}/browse | ID del sito dalla gerarchia dei contenuti |
| Elenca i bundle | GET /v1/site/{siteId}/bundles | ID dei bundle per il sito |
| Ottieni i componenti | GET /v1/site/{siteId}/bundles/{bundleId} | signedLink e roots per componente |
| Scarica i file | GET sull’URL CDN costruito | File di ingresso, tile e immagini |
Passaggio 1 — Autenticazione
Sezione intitolata “Passaggio 1 — Autenticazione”Ottieni un access token con 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_credentialsRisolvi l’URL della tua API regionale:
GET https://cloud-api.prevu3d.com/oauth/api-infoAuthorization: Bearer {access_token}La risposta contiene apiUrl (ad esempio https://api-ue1.prevu3d.com/realityconnect-api). Usa questo come URL di base per tutte le chiamate successive all’API.
Passaggio 2 — Trova un sito
Sezione intitolata “Passaggio 2 — Trova un sito”Esplora la gerarchia dei nodi partendo dalla tua organizzazione:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}Attraversa l’albero ricorsivamente finché non trovi un nodo con type: "Site". Annota il suo id.
Passaggio 3 — Elenca i bundle
Sezione intitolata “Passaggio 3 — Elenca i bundle”GET {api_url}/v1/site/{siteId}/bundlesAuthorization: Bearer {access_token}Restituisce un elenco paginato di bundle per quel sito.
Passaggio 4 — Ottieni i dettagli del bundle
Sezione intitolata “Passaggio 4 — Ottieni i dettagli del bundle”GET {api_url}/v1/site/{siteId}/bundles/{bundleId}Authorization: Bearer {access_token}Esempio di risposta:
{ "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"] } ]}Ogni componente espone due campi necessari per il download:
signedLink: un URL CDN firmato che termina con/*. Il*è un segnaposto. La firma concede l’accesso in lettura a tutti i file sotto questo prefisso, non solo a un singolo file.roots: un array di percorsi relativi al file (o ai file) di ingresso di questo componente.
Passaggio 5 — Scarica i file
Sezione intitolata “Passaggio 5 — Scarica i file”Costruisci un URL scaricabile sostituendo il * in signedLink con un percorso di file:
download_url = component["signedLink"].replace("*", root)response = requests.get(download_url)I parametri di query (Policy, Signature, Key-Pair-Id) rimangono allegati e autenticano la richiesta. Scarica i byte del file direttamente dalla CDN senza header Authorization sulla GET.
Dettagli specifici per componente
Sezione intitolata “Dettagli specifici per componente”OgcPointCloudHlod (nuvole di punti)
Sezione intitolata “OgcPointCloudHlod (nuvole di punti)”Punto di ingresso: ogni valore in roots è un percorso a un file tileset.json (uno per sessione di scansione).
for root in component["roots"]: url = component["signedLink"].replace("*", root) tileset = requests.get(url).json()Attraversamento dei tile: tileset.json segue la specifica OGC 3D Tiles. Contiene un albero di nodi tile. Ogni nodo può avere un campo content con un uri che punta a un file .glb relativo alla directory del tileset:
{ "asset": { "version": "1.0" }, "root": { "boundingVolume": { }, "content": { "uri": "cell.glb" }, "children": [ { "content": { "uri": "cell0.glb" }, "children": [ ] } ] }}Risolvi i percorsi dei tile relativi alla directory del tileset:
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).contentAttraversa l’albero ricorsivamente per scaricare tutti i tile.
RealityMeshHlod (mesh)
Sezione intitolata “RealityMeshHlod (mesh)”Punto di ingresso: roots contiene ["hlod_tree.json"].
url = component["signedLink"].replace("*", "hlod_tree.json")hlod_tree = requests.get(url).json()Struttura ad albero: hlod_tree.json contiene un albero 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" } ] }}Denominazione dei file di tile: i file di geometria e texture usano lo schema {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).contentAttraversa l’array children ricorsivamente per scoprire tutti i valori model_path a ogni livello di dettaglio.
RealityPhotosphere (photosphere)
Sezione intitolata “RealityPhotosphere (photosphere)”Punto di ingresso: roots contiene ["stations.json"].
url = component["signedLink"].replace("*", "stations.json")stations = requests.get(url).json()File aggiuntivi:
index.tsv: un file di indice separato da tabulazioni accanto astations.json- Le immagini photosphere seguono lo schema
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).contentAnalizza stations.json per scoprire tutte le directory delle stazioni e i relativi file immagine associati.
Esempio completo
Sezione intitolata “Esempio completo”Questo script si autentica, trova un sito, recupera il primo bundle, scarica i file del punto di ingresso e segue un tile o un’immagine di riferimento per componente.
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")
breakCosa c’è dopo?
Sezione intitolata “Cosa c’è dopo?”- Configura l’autenticazione dall’inizio alla fine nella guida Flusso Client Credentials.
- Carica ed elabora nuove acquisizioni con i Flussi di lavoro dei Data Bundle.
- Esplora ogni operazione nel riferimento API.