Aller au contenu

Téléchargement des fichiers de scan

Ce guide explique comment télécharger les sorties de scan traitées depuis la RealityConnect API : nuages de points, maillages et images photosphère. Le flux de travail utilise le flux Client Credentials et le scope download:scan.


  • Une application OAuth configurée pour Client Credentials
  • Scopes : read:basic, read:hierarchy, download:scan
  • Votre client_id et client_secret
  • Accès au contenu du Site qui contient les bundles que vous souhaitez télécharger

read:twin et read:asset ne sont pas requis pour le téléchargement des fichiers de scan.

Les données de scan traitées se trouvent dans des bundles attachés à un Site. Chaque bundle contient un ou plusieurs composants, chaque composant correspondant à un format de sortie différent :

Type de composantFormatDescription
OgcPointCloudHlodOGC 3D Tiles (norme ouverte)Données de nuage de points. Point d’entrée tileset.json avec des fichiers de tuiles .glb
RealityMeshHlodPrevu3D HLODDonnées de maillage. Point d’entrée hlod_tree.json avec des fichiers de tuiles .pvt
RealityPhotosphereImages JPEGDonnées photosphère. Manifeste stations.json avec des images .jpeg
flowchart LR
  A["1. S'authentifier"] --> B["2. Trouver un Site"]
  B --> C["3. Lister les bundles"]
  C --> D["4. Obtenir les détails du bundle"]
  D --> E["5. Télécharger les fichiers d'entrée"]
  E --> F["6. Suivre les références de fichiers"]
ÉtapePoint de terminaisonCe que vous obtenez
S’authentifierPOST /oauth/tokenJeton d’accès
Résoudre l’URL APIGET /oauth/api-infoapiUrl régionale et ID d’organisation
Trouver un SiteGET /v1/nodes/{id}/browseID du Site depuis la hiérarchie de contenu
Lister les bundlesGET /v1/site/{siteId}/bundlesID des bundles pour le Site
Obtenir les composantsGET /v1/site/{siteId}/bundles/{bundleId}signedLink et roots par composant
Télécharger les fichiersGET sur l’URL CDN construiteFichiers d’entrée, tuiles et images

Obtenez un jeton d’accès avec 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

Résolvez votre URL API régionale :

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

La réponse contient apiUrl (par exemple https://api-ue1.prevu3d.com/realityconnect-api). Utilisez-la comme URL de base pour tous les appels API suivants.

Parcourez la hiérarchie des nœuds en partant de votre organisation :

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

Parcourez l’arborescence de manière récursive jusqu’à trouver un nœud avec type: "Site". Notez son id.

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

Retourne une liste paginée des bundles pour ce Site.

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

Exemple de réponse :

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

Chaque composant expose deux champs nécessaires au téléchargement :

  • signedLink : une URL CDN signée se terminant par /*. Le * est un caractère de remplacement. La signature accorde un accès en lecture à tous les fichiers sous ce préfixe, et pas seulement à un seul fichier.
  • roots : un tableau de chemins relatifs vers le ou les fichiers d’entrée de ce composant.

Construisez une URL téléchargeable en remplaçant le * dans signedLink par un chemin de fichier :

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

Les paramètres de requête (Policy, Signature, Key-Pair-Id) restent attachés et authentifient la requête. Téléchargez les octets du fichier directement depuis le CDN sans en-tête Authorization sur le GET.

Point d’entrée : chaque valeur dans roots est un chemin vers un fichier tileset.json (un par session de scan).

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

Parcours des tuiles : tileset.json suit la spécification OGC 3D Tiles. Il contient un arbre de nœuds de tuiles. Chaque nœud peut avoir un champ content avec un uri pointant vers un fichier .glb relatif au répertoire du tileset :

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

Résolvez les chemins des tuiles par rapport au répertoire du 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

Parcourez l’arborescence de manière récursive pour télécharger toutes les tuiles.

Point d’entrée : roots contient ["hlod_tree.json"].

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

Structure de l’arbre : hlod_tree.json contient un arbre 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" }
]
}
}

Convention de nommage des tuiles : les fichiers de géométrie et de texture utilisent le modèle {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

Parcourez le tableau children de manière récursive pour découvrir toutes les valeurs model_path à chaque niveau de détail.

Point d’entrée : roots contient ["stations.json"].

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

Fichiers supplémentaires :

  • index.tsv : un fichier d’index séparé par des tabulations à côté de stations.json
  • Les images photosphère suivent le modèle 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

Analysez stations.json pour découvrir tous les répertoires de stations et leurs fichiers image associés.

Ce script s’authentifie, trouve un Site, récupère le premier bundle, télécharge les fichiers d’entrée et suit une tuile ou une image référencée par composant.

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