Descargar archivos de escaneo
Esta guía explica cómo descargar salidas de escaneo procesadas desde la API de RealityConnect: nubes de puntos, mallas e imágenes de fotosferas. El flujo de trabajo usa el flujo de Client Credentials y el ámbito download:scan.
Requisitos previos
Sección titulada «Requisitos previos»- Una aplicación OAuth configurada para Client Credentials
- Ámbitos:
read:basic,read:hierarchy,download:scan - Tu
client_idyclient_secret - Acceso al contenido del sitio que contiene los bundles que quieres descargar
read:twin y read:asset no son necesarios para la descarga de archivos de escaneo.
Cómo se organizan los datos de escaneo
Sección titulada «Cómo se organizan los datos de escaneo»Los datos de escaneo procesados residen en bundles asociados a un sitio. Cada bundle contiene uno o más componentes, donde cada componente es un formato de salida diferente:
| Tipo de componente | Formato | Descripción |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles (estándar abierto) | Datos de nube de puntos. Punto de entrada tileset.json con archivos de tesela .glb |
RealityMeshHlod | HLOD de Prevu3D | Datos de malla. Punto de entrada hlod_tree.json con archivos de tesela .pvt |
RealityPhotosphere | Imágenes JPEG | Datos de fotosferas. Manifiesto stations.json con imágenes .jpeg |
Flujo integral
Sección titulada «Flujo integral»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"]
| Paso | Endpoint | Qué obtienes |
|---|---|---|
| Autenticar | POST /oauth/token | Token de acceso |
| Resolver la URL de la API | GET /oauth/api-info | apiUrl regional e ID de la organización |
| Encontrar el sitio | GET /v1/nodes/{id}/browse | ID del sitio desde la jerarquía de contenido |
| Listar bundles | GET /v1/site/{siteId}/bundles | IDs de bundle del sitio |
| Obtener componentes | GET /v1/site/{siteId}/bundles/{bundleId} | signedLink y roots por componente |
| Descargar archivos | GET sobre la URL de CDN construida | Archivos de entrada, teselas e imágenes |
Paso 1 — Autenticar
Sección titulada «Paso 1 — Autenticar»Obtén un token de acceso 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_credentialsResuelve la URL regional de tu API:
GET https://cloud-api.prevu3d.com/oauth/api-infoAuthorization: Bearer {access_token}La respuesta contiene apiUrl (por ejemplo https://api-ue1.prevu3d.com/realityconnect-api). Úsala como URL base para todas las llamadas posteriores a la API.
Paso 2 — Encontrar un sitio
Sección titulada «Paso 2 — Encontrar un sitio»Explora la jerarquía de nodos partiendo de tu organización:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}Recorre el árbol de forma recursiva hasta que encuentres un nodo con type: "Site". Anota su id.
Paso 3 — Listar bundles
Sección titulada «Paso 3 — Listar bundles»GET {api_url}/v1/site/{siteId}/bundlesAuthorization: Bearer {access_token}Devuelve una lista paginada de los bundles de ese sitio.
Paso 4 — Obtener los detalles del bundle
Sección titulada «Paso 4 — Obtener los detalles del bundle»GET {api_url}/v1/site/{siteId}/bundles/{bundleId}Authorization: Bearer {access_token}Ejemplo de respuesta:
{ "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"] } ]}Cada componente expone dos campos que necesitas para la descarga:
signedLink: una URL firmada de CDN que termina en/*. El*es un marcador de posición. La firma concede acceso de lectura a todos los archivos bajo este prefijo, no solo a un único archivo.roots: un array de rutas relativas al archivo o archivos de punto de entrada de este componente.
Paso 5 — Descargar archivos
Sección titulada «Paso 5 — Descargar archivos»Construye una URL descargable reemplazando el * de signedLink por una ruta de archivo:
download_url = component["signedLink"].replace("*", root)response = requests.get(download_url)Los parámetros de consulta (Policy, Signature, Key-Pair-Id) permanecen adjuntos y autentican la solicitud. Descarga los bytes del archivo directamente desde el CDN sin cabecera Authorization en el GET.
Detalles específicos por componente
Sección titulada «Detalles específicos por componente»OgcPointCloudHlod (nubes de puntos)
Sección titulada «OgcPointCloudHlod (nubes de puntos)»Punto de entrada: cada valor de roots es una ruta a un archivo tileset.json (uno por sesión de escaneo).
for root in component["roots"]: url = component["signedLink"].replace("*", root) tileset = requests.get(url).json()Recorrido de teselas: tileset.json sigue la especificación OGC 3D Tiles. Contiene un árbol de nodos de tesela. Cada nodo puede tener un campo content con un uri que apunta a un archivo .glb relativo al directorio del tileset:
{ "asset": { "version": "1.0" }, "root": { "boundingVolume": { }, "content": { "uri": "cell.glb" }, "children": [ { "content": { "uri": "cell0.glb" }, "children": [ ] } ] }}Resuelve las rutas de las teselas de forma relativa al directorio 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).contentRecorre el árbol de forma recursiva para descargar todas las teselas.
RealityMeshHlod (mallas)
Sección titulada «RealityMeshHlod (mallas)»Punto de entrada: roots contiene ["hlod_tree.json"].
url = component["signedLink"].replace("*", "hlod_tree.json")hlod_tree = requests.get(url).json()Estructura del árbol: hlod_tree.json contiene un árbol 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" } ] }}Nomenclatura de los archivos de tesela: los archivos de geometría y textura usan el patrón {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).contentRecorre el array children de forma recursiva para descubrir todos los valores de model_path en cada nivel de detalle.
RealityPhotosphere (fotosferas)
Sección titulada «RealityPhotosphere (fotosferas)»Punto de entrada: roots contiene ["stations.json"].
url = component["signedLink"].replace("*", "stations.json")stations = requests.get(url).json()Archivos adicionales:
index.tsv: un archivo de índice separado por tabulaciones junto astations.json- Las imágenes de fotosferas siguen el patrón
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).contentAnaliza stations.json para descubrir todos los directorios de estaciones y sus archivos de imagen asociados.
Ejemplo completo
Sección titulada «Ejemplo completo»Este script se autentica, encuentra un sitio, obtiene el primer bundle, descarga los archivos de punto de entrada y sigue una tesela o imagen referenciada por 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")
break¿Qué sigue?
Sección titulada «¿Qué sigue?»- Configura la autenticación de principio a fin en la guía del flujo de Client Credentials.
- Sube y procesa nuevas capturas con los flujos de trabajo de Data Bundle.
- Explora todas las operaciones en la referencia de la API.