Salta ai contenuti

Caricare dati in un Data Bundle

Questa guida spiega come caricare i file di input grezzi di un’acquisizione in un data bundle tramite la RealityConnect API.

Tutti gli endpoint di caricamento richiedono lo scope write:bundle; elencare le sessioni richiede read:bundle.


Un data bundle contiene gli input grezzi di una singola acquisizione: una nuvola di punti, una traiettoria, immagini, metadati per immagine e così via. L’insieme esatto di file che un bundle accetta dipende dal dispositivo per cui il bundle è stato creato.

I file vengono caricati in sessioni di caricamento. Una sessione è un contenitore creato dal server per un batch di file di un bundle. Quando la apri dichiari quanti file conterrà il batch (expectedFileCount); la sessione si completa automaticamente una volta finalizzato quel numero di file.

Ogni singolo file viene caricato con un caricamento multipart di S3:

  1. Apri una sessione sul bundle, dichiarando il numero totale di file che invierai.
  2. Per ogni file: avvia il caricamento, esegui il PUT delle sue parti direttamente agli URL restituiti, quindi finalizzalo.
  3. Quando il numero di file finalizzati raggiunge expectedFileCount, la sessione viene contrassegnata come completed.
sequenceDiagram
  participant Client as Client
  participant RCAPI as RCAPI
  participant S3 as S3

  Client->>RCAPI: POST upload-sessions
  RCAPI-->>Client: sessionId + requirements

  loop for each file
    Client->>RCAPI: POST initiate file
    RCAPI-->>Client: fileId + presigned part URLs
    loop for each part
      Client->>S3: PUT part
      S3-->>Client: ETag
    end
    Client->>RCAPI: POST finalize
    RCAPI-->>Client: file completed + sessionStatus
  end

I requirements di caricamento (restituiti quando apri una sessione) ti indicano esattamente quali tipi di file il bundle accetta, quali sono obbligatori e come dipendono l’uno dall’altro.

Un bundle ZF SLAM è un buon esempio perché accetta quattro diversi tipi di file con una dipendenza tra tipi. Un’acquisizione ZF completa è composta da:

FileTipoNote
scan.lasnuvola di puntiesattamente una
…laser_0.txttraiettoriaesattamente una
panorama/*.jpgimmagini panoramichemolte, ciascuna abbinata ai metadati
panorama/*.jsonmetadati per immaginemolti, uno per immagine

Quindi un batch ZF completo è 1 nuvola di punti + 1 traiettoria + N immagini + N file di metadati. Li caricheremo tutti in un’unica sessione.

Tutti i percorsi sono relativi al tuo api_url regionale. Il bundle è indirizzato dal proprio id (bundleId).

MetodoPercorsoScopeScopo
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleAprire una sessione
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleElencare le sessioni (ripresa / progressi)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleAvviare il caricamento di un file
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundleRiemettere gli URL delle parti scaduti
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleFinalizzare il caricamento di un file

Per caricare dati in un data bundle, hai bisogno di:

  • un flusso OAuth completato con un access_token e il tuo api_url regionale
  • un data bundle esistente (bundleId)

Se non hai ancora un bundle, creane prima uno seguendo Creare un Data Bundle. Quel flusso recupera l’albero degli input del dispositivo, sceglie un dbuPath foglia e crea il bundle sotto un nodo padre.

Dichiara il numero totale di file che il batch conterrà. Per un’acquisizione ZF completa con 13 immagini è 1 + 1 + 13 + 13 = 28.

POST {api_url}/v1/bundles/{bundleId}/upload-sessions
Authorization: Bearer {access_token}
Content-Type: application/json
{ "expectedFileCount": 28 }

La risposta restituisce il sessionId e i requirements del bundle:

{
"sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b",
"expectedFileCount": 28,
"requirements": [
{ "type": "pointclouds", "allowedExtensions": ["las", "laz"], "optional": false, "allowMultiples": false, "dependencies": [] },
{ "type": "trajectories", "allowedExtensions": ["txt"], "optional": false, "allowMultiples": false, "dependencies": [] },
{ "type": "pictures", "allowedExtensions": ["jpg", "png", "jpeg"], "optional": true, "allowMultiples": true, "dependencies": ["picturesMetadata"] },
{ "type": "picturesMetadata", "allowedExtensions": ["json"], "optional": true, "allowMultiples": true, "dependencies": ["pictures"] }
]
}

Ogni voce in requirements descrive un tipo di file che il bundle accetta:

CampoSignificato
typeL’identificatore che passi come type quando avvii il caricamento di un file.
allowedExtensionsEstensioni di file accettate per questo tipo.
optionalSe false, il bundle non può essere elaborato senza almeno un file di questo tipo.
allowMultiplesSe false, è possibile caricare solo un file di questo tipo.
dependenciesAltri tipi che devono essere presenti ogni volta che questo tipo è presente.

Per l’esempio ZF:

  • pointclouds e trajectories sono obbligatori (optional: false) e singoli (allowMultiples: false). Ogni bundle ZF ne richiede esattamente uno di ciascuno.
  • pictures e picturesMetadata sono opzionali e accettano più file.
  • I due tipi di immagine dipendono l’uno dall’altro: pictures.dependencies = ["picturesMetadata"] e picturesMetadata.dependencies = ["pictures"].

Una dipendenza significa: se un tipo è presente, ogni tipo che elenca deve essere presente anch’esso. Poiché i tipi di immagine ZF si riferiscono l’uno all’altro, sono tutto-o-niente come coppia. Puoi caricare le immagini e i loro metadati insieme, oppure ometterli entrambi, ma non puoi caricarne uno senza l’altro.

Batch ZF validi:

  • Minimo1 pointcloud + 1 trajectory (nessuna immagine). expectedFileCount = 2.
  • Completo1 pointcloud + 1 trajectory + N pictures + N picturesMetadata. expectedFileCount = 2 + 2N.

Non valido:

  • Immagini senza i loro metadati (o viceversa) poiché la dipendenza tra tipi non è soddisfatta.
  • Un batch senza nuvola di punti o senza traiettoria poiché manca un tipo obbligatorio.

Prima di aprire una sessione, conta ogni file che intendi caricare e imposta quel totale come expectedFileCount. I tipi opzionali contano solo se li includi nel batch. La sessione si completa quando quel numero di file viene finalizzato, quindi il numero che dichiari deve corrispondere a ciò che carichi effettivamente.

Ripeti le tre chiamate seguenti per ogni file del batch.

Passa il nome del file, la sua dimensione in byte e il suo type (uno dei tipi di requirements):

POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files
Authorization: Bearer {access_token}
Content-Type: application/json
{ "fileName": "scan.las", "fileSize": 734003200, "type": "pointclouds" }

La risposta descrive il caricamento multipart: quante parts inviare e un url prefirmato per parte.

{
"fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5",
"parts": 8,
"urls": [
{ "partNumber": 1, "url": "https://s3…/part-1?…" },
{ "partNumber": 2, "url": "https://s3…/part-2?…" }
]
}

Suddividi il file in parts blocchi sequenziali di ceil(fileSize / parts) byte ed esegui il PUT di ogni blocco al suo url prefirmato. Su queste richieste non viene inviato alcun header di autenticazione; l’URL è già firmato. Conserva l’header di risposta ETag di ogni parte, ti serve per la finalizzazione.

import math, requests
def upload_parts(file_path, file_size, urls):
part_size = math.ceil(file_size / len(urls))
parts = []
with open(file_path, "rb") as handle:
for entry in sorted(urls, key=lambda u: u["partNumber"]):
res = requests.put(entry["url"], data=handle.read(part_size))
res.raise_for_status()
parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return parts

Passa l’ETag esattamente come restituito, comprese le virgolette che lo racchiudono.

Invia le parti raccolte per completare il file. L’endpoint restituisce 200 OK.

POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalize
Authorization: Bearer {access_token}
Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "\"a1b2…\"" }, { "partNumber": 2, "etag": "\"c3d4…\"" } ] }

La risposta riporta il file e comunica lo stato della sessione:

{
"fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5",
"fileName": "scan.las",
"type": "pointclouds",
"size": 734003200,
"status": "completed",
"sessionStatus": "pending"
}

sessionStatus rimane pending finché non viene finalizzato l’ultimo file previsto, momento in cui diventa completed.

Gli URL prefirmati scadono. Se un caricamento dura a lungo e un PUT inizia a restituire 403, riemetti gli URL a partire dalla parte in cui ti sei fermato e continua:

GET {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/0
Authorization: Bearer {access_token}
{ "startIndex": 0, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ] }

Il file non deve essere riavviato; vengono rinnovati solo gli URL delle parti.

Elenca le sessioni di un bundle per verificare i progressi o riprendere un batch interrotto senza mantenere il sessionId in memoria. L’elenco è paginato e ordinabile, con un filtro status opzionale.

Parametri di query: page (predefinito 1), limit (predefinito 20, massimo 20), sortBy (createdAt o status, predefinito createdAt), sortDir (ASC o DESC, predefinito ASC), status (filtro opzionale: pending o completed).

GET {api_url}/v1/bundles/{bundleId}/upload-sessions?page=1&limit=20&sortBy=createdAt&sortDir=DESC
Authorization: Bearer {access_token}
{
"items": [
{
"sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b",
"status": "pending",
"expectedFileCount": 28,
"finalizedFileCount": 14
}
],
"total": 1
}

finalizedFileCount rispetto a expectedFileCount ti indica quanti file rimangono. Per riprendere, avvia e finalizza solo i file non ancora completati.

Leggi attentamente questa sezione perché determina di cosa sei responsabile quando usi la RealityConnect API per caricare dati.

  • Gli endpoint di caricamento registrano ogni file sotto il type che fornisci e completano la sessione puramente in base al conteggio: una volta che finalizedFileCount è uguale a expectedFileCount, la sessione è completed. Al momento del caricamento, il server non verifica che il type sia uno dei requirements, che l’estensione del file sia consentita, che i tipi obbligatori siano presenti o che le dipendenze tra tipi siano soddisfatte.
  • Tutte le regole (tipi obbligatori, allowMultiples e le dependencies tra i tipi) vengono applicate dopo che il bundle è stato inviato per l’elaborazione. Un batch che ignora i requirements verrà caricato correttamente ma non potrà essere elaborato.
  • La validazione approfondita del contenuto non viene mai eseguita dall’API di caricamento. Se un .las è una vera nuvola di punti ZF, o se un .json fa correttamente riferimento alla sua immagine, viene verificato solo in fase di elaborazione.

Considera i requirements come il contratto. Rispetta i tipi, le estensioni, l’opzionalità, la molteplicità e le dipendenze nel tuo batch di caricamento affinché il bundle sia elaborabile una volta caricato. Gli errori di elaborazione causati da una struttura di caricamento errata non sono rimborsabili.

Mettendo tutto insieme per l’acquisizione ZF. files è l’elenco che costruisci dal tuo dataset locale, con ogni voce che abbina un percorso al suo type da requirements.

import math, requests
API_URL = "https://<your-regional-api-url>"
TOKEN = "<access_token>"
BUNDLE_ID = "<bundleId>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# (percorso locale, tipo di schema) per ogni file del batch
files = [
("scan.las", "pointclouds"),
("2024-07-scan.laser_laser_0.txt", "trajectories"),
("panorama/2024-07-scan-idx40.jpg", "pictures"),
("panorama/2024-07-scan-idx40.json", "picturesMetadata"),
# … coppie immagine / metadati rimanenti …
]
def upload_one(session_id, path, file_type):
size = __import__("os").path.getsize(path)
base = f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions/{session_id}/files"
initiated = requests.post(
base, headers=HEADERS,
json={"fileName": path.split("/")[-1], "fileSize": size, "type": file_type},
).json()
part_size = math.ceil(size / initiated["parts"])
parts = []
with open(path, "rb") as handle:
for entry in sorted(initiated["urls"], key=lambda u: u["partNumber"]):
res = requests.put(entry["url"], data=handle.read(part_size))
res.raise_for_status()
parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return requests.post(
f"{base}/{initiated['fileId']}/finalize", headers=HEADERS, json={"parts": parts},
).json()
# 1. Apri la sessione per il numero esatto di file che invieremo.
session = requests.post(
f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions",
headers=HEADERS, json={"expectedFileCount": len(files)},
).json()
# 2. Carica ogni file sotto il suo tipo.
result = None
for path, file_type in files:
result = upload_one(session["sessionId"], path, file_type)
# 3. L'ultima finalizzazione riporta la sessione come completata.
assert result["sessionStatus"] == "completed"

Una volta caricato ogni file e completata la sessione (completed), continua con Elaborazione e monitoraggio per trasformare gli input grezzi in output visualizzabili.