Pular para o conteúdo

Baixando Arquivos de Scan

Este guia explica como baixar saídas de scan processadas da RealityConnect API: nuvens de pontos, malhas e imagens de fotosfera. O fluxo de trabalho usa o fluxo Client Credentials e o escopo download:scan.


  • Um aplicativo OAuth configurado para Client Credentials
  • Escopos: read:basic, read:hierarchy, download:scan
  • Seu client_id e client_secret
  • Acesso ao conteúdo do site que contém os bundles que você quer baixar

read:twin e read:asset não são necessários para o download de arquivos de scan.

Os dados de scan processados ficam em bundles anexados a um site. Cada bundle contém um ou mais componentes, em que cada componente é um formato de saída diferente:

Tipo de componenteFormatoDescrição
OgcPointCloudHlodOGC 3D Tiles (padrão aberto)Dados de nuvem de pontos. Ponto de entrada tileset.json com arquivos de tile .glb
RealityMeshHlodHLOD da Prevu3DDados de malha. Ponto de entrada hlod_tree.json com arquivos de tile .pvt
RealityPhotosphereImagens JPEGDados de fotosfera. Manifesto stations.json com imagens .jpeg
flowchart LR
  A["1. Autenticar"] --> B["2. Encontrar um site"]
  B --> C["3. Listar bundles"]
  C --> D["4. Obter detalhes do bundle"]
  D --> E["5. Baixar arquivos de entrada"]
  E --> F["6. Seguir referências de arquivo"]
EtapaEndpointO que você obtém
AutenticarPOST /oauth/tokenToken de acesso
Resolver a URL da APIGET /oauth/api-infoapiUrl regional e ID da organização
Encontrar siteGET /v1/nodes/{id}/browseID do site na hierarquia de conteúdo
Listar bundlesGET /v1/site/{siteId}/bundlesIDs de bundle do site
Obter componentesGET /v1/site/{siteId}/bundles/{bundleId}signedLink e roots por componente
Baixar arquivosGET na URL de CDN construídaArquivos de entrada, tiles e imagens

Obtenha um token de acesso com 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

Resolva sua URL de API regional:

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

A resposta contém apiUrl (por exemplo https://api-ue1.prevu3d.com/realityconnect-api). Use isso como a URL base para todas as chamadas de API seguintes.

Navegue pela hierarquia de nós começando pela sua organização:

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

Percorra a árvore recursivamente até encontrar um nó com type: "Site". Anote seu id.

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

Retorna uma lista paginada de bundles para esse site.

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

Exemplo de resposta:

{
"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 expõe dois campos necessários para o download:

  • signedLink: uma URL de CDN assinada terminando em /*. O * é um placeholder. A assinatura concede acesso de leitura a todos os arquivos sob esse prefixo, não apenas a um único arquivo.
  • roots: um array de caminhos relativos para o(s) arquivo(s) de ponto de entrada deste componente.

Construa uma URL para download substituindo o * em signedLink por um caminho de arquivo:

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

Os parâmetros de consulta (Policy, Signature, Key-Pair-Id) permanecem anexados e autenticam a requisição. Baixe os bytes do arquivo diretamente do CDN sem cabeçalho Authorization no GET.

Ponto de entrada: cada valor em roots é um caminho para um arquivo tileset.json (um por sessão de scan).

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

Percurso dos tiles: o tileset.json segue a especificação OGC 3D Tiles. Ele contém uma árvore de nós de tile. Cada nó pode ter um campo content com um uri apontando para um arquivo .glb relativo ao diretório do tileset:

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

Resolva os caminhos dos tiles relativos ao diretório do 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

Percorra a árvore recursivamente para baixar todos os tiles.

Ponto de entrada: roots contém ["hlod_tree.json"].

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

Estrutura da árvore: o hlod_tree.json contém uma árvore 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 dos arquivos de tile: os arquivos de geometria e de textura usam o padrão {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

Percorra o array children recursivamente para descobrir todos os valores de model_path em cada nível de detalhe.

Ponto de entrada: roots contém ["stations.json"].

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

Arquivos adicionais:

  • index.tsv: um arquivo de índice separado por tabulações, ao lado de stations.json
  • As imagens de fotosfera seguem o padrão 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

Analise o stations.json para descobrir todos os diretórios de estação e seus arquivos de imagem associados.

Este script autentica, encontra um site, obtém o primeiro bundle, baixa os arquivos de ponto de entrada e segue um tile ou imagem referenciado por 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