Salta ai contenuti

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.


  • Un’applicazione OAuth configurata per Client Credentials
  • Scope: read:basic, read:hierarchy, download:scan
  • Il tuo client_id e client_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.

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 componenteFormatoDescrizione
OgcPointCloudHlodOGC 3D Tiles (standard aperto)Dati della nuvola di punti. Punto di ingresso tileset.json con file di tile .glb
RealityMeshHlodPrevu3D HLODDati mesh. Punto di ingresso hlod_tree.json con file di tile .pvt
RealityPhotosphereImmagini JPEGDati photosphere. Manifest stations.json con immagini .jpeg
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"]
PassaggioEndpointCosa ottieni
AutenticazionePOST /oauth/tokenAccess token
Risoluzione dell’URL dell’APIGET /oauth/api-infoapiUrl regionale e ID dell’organizzazione
Trova il sitoGET /v1/nodes/{id}/browseID del sito dalla gerarchia dei contenuti
Elenca i bundleGET /v1/site/{siteId}/bundlesID dei bundle per il sito
Ottieni i componentiGET /v1/site/{siteId}/bundles/{bundleId}signedLink e roots per componente
Scarica i fileGET sull’URL CDN costruitoFile di ingresso, tile e immagini

Ottieni un access token con 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

Risolvi l’URL della tua API regionale:

GET https://cloud-api.prevu3d.com/oauth/api-info
Authorization: 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.

Esplora la gerarchia dei nodi partendo dalla tua organizzazione:

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

Attraversa l’albero ricorsivamente finché non trovi un nodo con type: "Site". Annota il suo id.

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

Restituisce un elenco paginato di bundle per quel sito.

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.

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.

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).content

Attraversa l’albero ricorsivamente per scaricare tutti i tile.

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).content

Attraversa l’array children ricorsivamente per scoprire tutti i valori model_path a ogni livello di dettaglio.

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 a stations.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).content

Analizza stations.json per scoprire tutte le directory delle stazioni e i relativi file immagine associati.

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 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