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.
Pré-requisitos
Seção intitulada “Pré-requisitos”- Um aplicativo OAuth configurado para Client Credentials
- Escopos:
read:basic,read:hierarchy,download:scan - Seu
client_ideclient_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.
Como os dados de scan são organizados
Seção intitulada “Como os dados de scan são organizados”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 componente | Formato | Descrição |
|---|---|---|
OgcPointCloudHlod | OGC 3D Tiles (padrão aberto) | Dados de nuvem de pontos. Ponto de entrada tileset.json com arquivos de tile .glb |
RealityMeshHlod | HLOD da Prevu3D | Dados de malha. Ponto de entrada hlod_tree.json com arquivos de tile .pvt |
RealityPhotosphere | Imagens JPEG | Dados de fotosfera. Manifesto stations.json com imagens .jpeg |
Fluxo de ponta a ponta
Seção intitulada “Fluxo de ponta a ponta”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"]
| Etapa | Endpoint | O que você obtém |
|---|---|---|
| Autenticar | POST /oauth/token | Token de acesso |
| Resolver a URL da API | GET /oauth/api-info | apiUrl regional e ID da organização |
| Encontrar site | GET /v1/nodes/{id}/browse | ID do site na hierarquia de conteúdo |
| Listar bundles | GET /v1/site/{siteId}/bundles | IDs de bundle do site |
| Obter componentes | GET /v1/site/{siteId}/bundles/{bundleId} | signedLink e roots por componente |
| Baixar arquivos | GET na URL de CDN construída | Arquivos de entrada, tiles e imagens |
Etapa 1 — Autenticar
Seção intitulada “Etapa 1 — Autenticar”Obtenha um token de acesso com 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_credentialsResolva sua URL de API regional:
GET https://cloud-api.prevu3d.com/oauth/api-infoAuthorization: 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.
Etapa 2 — Encontrar um site
Seção intitulada “Etapa 2 — Encontrar um site”Navegue pela hierarquia de nós começando pela sua organização:
GET {api_url}/v1/nodes/{organizationId}/browseAuthorization: Bearer {access_token}Percorra a árvore recursivamente até encontrar um nó com type: "Site". Anote seu id.
Etapa 3 — Listar bundles
Seção intitulada “Etapa 3 — Listar bundles”GET {api_url}/v1/site/{siteId}/bundlesAuthorization: Bearer {access_token}Retorna uma lista paginada de bundles para esse site.
Etapa 4 — Obter detalhes do bundle
Seção intitulada “Etapa 4 — Obter detalhes do bundle”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.
Etapa 5 — Baixar arquivos
Seção intitulada “Etapa 5 — Baixar arquivos”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.
Detalhes específicos por componente
Seção intitulada “Detalhes específicos por componente”OgcPointCloudHlod (nuvens de pontos)
Seção intitulada “OgcPointCloudHlod (nuvens de pontos)”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).contentPercorra a árvore recursivamente para baixar todos os tiles.
RealityMeshHlod (malhas)
Seção intitulada “RealityMeshHlod (malhas)”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).contentPercorra o array children recursivamente para descobrir todos os valores de model_path em cada nível de detalhe.
RealityPhotosphere (fotosferas)
Seção intitulada “RealityPhotosphere (fotosferas)”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 destations.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).contentAnalise o stations.json para descobrir todos os diretórios de estação e seus arquivos de imagem associados.
Exemplo completo
Seção intitulada “Exemplo completo”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 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")
breakPróximos passos
Seção intitulada “Próximos passos”- Configure a autenticação de ponta a ponta no guia Fluxo Client Credentials.
- Envie e processe novas capturas com os Fluxos de trabalho de Data Bundle.
- Explore todas as operações na referência da API.