Ga naar inhoud

Gegevens uploaden naar een data bundle

Deze gids legt uit hoe u de ruwe invoerbestanden van een opname uploadt naar een data bundle via de RealityConnect API.

Alle upload-endpoints vereisen de write:bundle-scope; het opsommen van sessies vereist read:bundle.


Een data bundle bevat de ruwe invoer voor één opname: een puntenwolk, een traject, afbeeldingen, metadata per afbeelding, enzovoort. De exacte set bestanden die een bundle accepteert, hangt af van het apparaat waarvoor de bundle is gemaakt.

Bestanden worden geüpload in uploadsessies. Een sessie is een door de server gemaakte container voor één batch van de bestanden van een bundle. Wanneer u deze opent, geeft u aan hoeveel bestanden de batch zal bevatten (expectedFileCount); de sessie wordt automatisch voltooid zodra dat aantal bestanden is voltooid.

Elk afzonderlijk bestand wordt geüpload met een S3-multipart-upload:

  1. Open een sessie op de bundle en geef het totale aantal bestanden aan dat u zult verzenden.
  2. Voor elk bestand: initieer de upload, PUT de delen ervan rechtstreeks naar de geretourneerde URL’s en voltooi het vervolgens.
  3. Wanneer het aantal voltooide bestanden expectedFileCount bereikt, wordt de sessie gemarkeerd als 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

De upload-requirements (geretourneerd wanneer u een sessie opent) vertellen u precies welke bestandstypen de bundle accepteert, welke verplicht zijn en hoe ze van elkaar afhankelijk zijn.

Het uitgewerkte voorbeeld: een ZF SLAM-opname

Section titled “Het uitgewerkte voorbeeld: een ZF SLAM-opname”

Een ZF SLAM-bundle is een goed voorbeeld omdat deze vier verschillende bestandstypen accepteert met een cross-type-afhankelijkheid. Een volledige ZF-opname bestaat uit:

BestandTypeOpmerkingen
scan.laspuntenwolkprecies één
…laser_0.txttrajectprecies één
panorama/*.jpgpanorama-afbeeldingenveel, elk gekoppeld aan metadata
panorama/*.jsonmetadata per afbeeldingveel, één per afbeelding

Een volledige ZF-batch is dus 1 puntenwolk + 1 traject + N afbeeldingen + N metadatabestanden. We uploaden ze allemaal in één sessie.

Alle paden zijn relatief ten opzichte van uw regionale api_url. De bundle wordt geadresseerd via zijn eigen id (bundleId).

MethodePadScopeDoel
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleEen sessie openen
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleSessies opsommen (hervatten / voortgang)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleEen bestandsupload initiëren
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundleVerlopen deel-URL’s opnieuw uitgeven
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleEen bestandsupload voltooien

Om gegevens naar een data bundle te uploaden, hebt u nodig:

  • een voltooide OAuth-flow met een access_token en uw regionale api_url
  • een bestaande data bundle (bundleId)

Als u nog geen bundle hebt, maakt u er eerst een door Een data bundle maken te volgen. Die flow haalt de apparaatinvoerboom op, kiest een leaf-dbuPath en maakt de bundle onder een bovenliggende node.

Geef het totale aantal bestanden aan dat de batch zal bevatten. Voor een volledige ZF-opname met 13 afbeeldingen is dat 1 + 1 + 13 + 13 = 28.

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

De response retourneert de sessionId en de requirements van de 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"] }
]
}

requirements en cross-afhankelijkheden begrijpen

Section titled “requirements en cross-afhankelijkheden begrijpen”

Elke vermelding in requirements beschrijft één bestandstype dat de bundle accepteert:

VeldBetekenis
typeDe identifier die u meegeeft als type bij het initiëren van een bestandsupload.
allowedExtensionsBestandsextensies die voor dit type worden geaccepteerd.
optionalAls false, kan de bundle niet worden verwerkt zonder ten minste één bestand van dit type.
allowMultiplesAls false, mag er slechts één bestand van dit type worden geüpload.
dependenciesAndere typen die ook aanwezig moeten zijn wanneer dit type aanwezig is.

Voor het ZF-voorbeeld:

  • pointclouds en trajectories zijn verplicht (optional: false) en enkelvoudig (allowMultiples: false). Elke ZF-bundle heeft precies één van elk nodig.
  • pictures en picturesMetadata zijn optioneel en accepteren meerdere bestanden.
  • De twee afbeeldingstypen zijn van elkaar afhankelijk: pictures.dependencies = ["picturesMetadata"] en picturesMetadata.dependencies = ["pictures"].

Een afhankelijkheid betekent: als een type aanwezig is, moet elk type dat het vermeldt ook aanwezig zijn. Omdat de ZF-afbeeldingstypen naar elkaar verwijzen, zijn ze alles-of-niets als paar. U kunt de afbeeldingen en hun metadata samen uploaden, of beide overslaan, maar u kunt niet het een zonder het ander uploaden.

Geldige ZF-batches:

  • Minimaal1 pointcloud + 1 trajectory (geen afbeeldingen). expectedFileCount = 2.
  • Volledig1 pointcloud + 1 trajectory + N pictures + N picturesMetadata. expectedFileCount = 2 + 2N.

Ongeldig:

  • Afbeeldingen zonder hun metadata (of omgekeerd), aangezien de cross-afhankelijkheid niet is voldaan.
  • Een batch zonder puntenwolk of zonder traject, aangezien een verplicht type ontbreekt.

Voordat u een sessie opent, telt u elk bestand dat u van plan bent te uploaden en stelt u dat totaal in als expectedFileCount. Optionele typen tellen alleen mee als u ze in de batch opneemt. De sessie wordt voltooid wanneer dat aantal bestanden is voltooid, dus het aantal dat u opgeeft, moet overeenkomen met wat u daadwerkelijk uploadt.

Herhaal de volgende drie aanroepen voor elk bestand in de batch.

Geef de bestandsnaam, de grootte ervan in bytes en het type (een van de requirements-typen) mee:

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

De response beschrijft de multipart-upload: hoeveel parts er moeten worden verzonden en één presigned url per deel.

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

Splits het bestand in parts opeenvolgende brokken van ceil(fileSize / parts) bytes en PUT elke brok naar de bijbehorende presigned url. Er wordt geen auth-header verzonden bij deze verzoeken; de URL is al ondertekend. Bewaar de ETag-responseheader van elk deel; u hebt deze nodig om te voltooien.

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

Geef de ETag exact terug zoals geretourneerd, inclusief de omringende aanhalingstekens.

Verzend de verzamelde delen om het bestand te voltooien. Het endpoint retourneert 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…\"" } ] }

De response echoot het bestand en rapporteert de sessiestatus:

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

sessionStatus blijft pending totdat het laatste verwachte bestand is voltooid, waarna het completed wordt.

Presigned URL’s verlopen. Als een upload lang duurt en een PUT 403 begint te retourneren, geeft u de URL’s opnieuw uit vanaf het deel waar u bent gestopt en gaat u verder:

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?…" } ] }

Het bestand hoeft niet opnieuw te worden geïnitieerd; alleen de deel-URL’s worden vernieuwd.

Som de sessies van een bundle op om de voortgang te controleren of een onderbroken batch te hervatten zonder de sessionId in het geheugen te houden. De lijst is gepagineerd en sorteerbaar, met een optioneel status-filter.

Queryparameters: page (standaard 1), limit (standaard 20, maximum 20), sortBy (createdAt of status, standaard createdAt), sortDir (ASC of DESC, standaard ASC), status (optioneel filter: pending of 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 versus expectedFileCount vertelt u hoeveel bestanden er nog over zijn. Om te hervatten, initieert en voltooit u alleen de bestanden die nog niet zijn voltooid.

Lees dit zorgvuldig, want het bepaalt waarvoor u verantwoordelijk bent bij het gebruik van de RealityConnect API om gegevens te uploaden.

  • De upload-endpoints registreren elk bestand onder het type dat u opgeeft en voltooien de sessie puur op basis van het aantal: zodra finalizedFileCount gelijk is aan expectedFileCount, is de sessie completed. Op het moment van uploaden verifieert de server niet dat het type een van de requirements is, dat de bestandsextensie is toegestaan, dat verplichte typen aanwezig zijn of dat aan cross-afhankelijkheden is voldaan.
  • Alle regels (verplichte typen, allowMultiples en de dependencies tussen typen) worden afgedwongen nadat de bundle voor verwerking is verzonden. Een batch die de requirements negeert, wordt succesvol geüpload maar mislukt bij de verwerking.
  • Diepgaande inhoudsvalidatie wordt nooit uitgevoerd door de upload-API. Of een .las een echte ZF-puntenwolk is, of een .json correct verwijst naar zijn afbeelding, wordt alleen bij de verwerking gecontroleerd.

Behandel requirements als het contract. Respecteer de typen, extensies, optionaliteit, multipliciteit en afhankelijkheden in uw uploadbatch, zodat de bundle verwerkbaar is zodra deze is geüpload. Verwerkingsfouten die worden veroorzaakt door een onjuiste uploadstructuur worden niet terugbetaald.

Alles samengevoegd voor de ZF-opname. files is de lijst die u opbouwt uit uw lokale dataset, waarbij elke vermelding een pad koppelt aan het type uit requirements.

import math, requests
API_URL = "https://<your-regional-api-url>"
TOKEN = "<access_token>"
BUNDLE_ID = "<bundleId>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# (local path, schema type) for every file in the 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"),
# … remaining image / metadata pairs …
]
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. Open the session for the exact number of files we will send.
session = requests.post(
f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions",
headers=HEADERS, json={"expectedFileCount": len(files)},
).json()
# 2. Upload every file under its type.
result = None
for path, file_type in files:
result = upload_one(session["sessionId"], path, file_type)
# 3. The last finalize reports the session as completed.
assert result["sessionStatus"] == "completed"

Zodra elk bestand is geüpload en de sessie completed is, gaat u verder naar Verwerking en bewaking om de ruwe invoer om te zetten in weergeefbare uitvoer.