コンテンツにスキップ

Data Bundleへのデータアップロード

このガイドでは、RealityConnect APIを通じてキャプチャの生入力ファイルをData Bundleにアップロードする方法を説明します。

すべてのアップロードエンドポイントにはwrite:bundleスコープが必要です。セッションの一覧にはread:bundleが必要です。


Data Bundleは1回のキャプチャの生入力を保持します。点群、軌跡、画像、画像ごとのメタデータなどです。Bundleが受け入れるファイルの正確なセットは、Bundleが作成されたデバイスによって異なります。

ファイルはアップロードセッションでアップロードされます。セッションは、Bundleのファイル1バッチ用にサーバーが作成するコンテナです。開始時にバッチに含まれるファイル数(expectedFileCount)を宣言します。その数のファイルが確定されると、セッションは自動的に完了します。

各ファイルはS3のマルチパートアップロードでアップロードされます:

  1. Bundleでセッションを開始し、送信するファイルの合計数を宣言する。
  2. 各ファイルについて:アップロードを開始し、返されたURLにパーツを直接PUTし、確定する。
  3. 確定されたファイル数がexpectedFileCountに達すると、セッションは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

アップロードrequirements(セッション開始時に返される)は、Bundleが受け入れるファイルタイプ、必須タイプ、およびそれらの依存関係を正確に示します。

ZF SLAM Bundleは、タイプ間依存関係を持つ4つの異なるファイルタイプを受け入れるため、良い例です。完全なZFキャプチャは以下で構成されます:

ファイルタイプ備考
scan.las点群正確に1つ
…laser_0.txt軌跡正確に1つ
panorama/*.jpgパノラマ画像多数、各画像にメタデータがペア
panorama/*.json画像ごとのメタデータ多数、画像1つにつき1つ

したがって、完全なZFバッチは1点群 + 1軌跡 + N画像 + Nメタデータファイルです。これらすべてを1つのセッションでアップロードします。

すべてのパスはリージョンのapi_urlを基準とします。Bundleは独自のid(bundleId)でアドレス指定されます。

メソッドパススコープ用途
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleセッションの開始
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleセッションの一覧(再開 / 進捗)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleファイルアップロードの開始
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundle期限切れパーツURLの再発行
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleファイルアップロードの確定

Data Bundleにデータをアップロードするには、以下が必要です:

  • access_tokenとリージョンのapi_urlを含む完了したOAuthフロー
  • 既存のData Bundle(bundleId

まだBundleがない場合は、先にData Bundleの作成に従って作成してください。そのフローではデバイス入力ツリーを取得し、リーフのdbuPathを選択し、親ノードの下にBundleを作成します。

ステップ1:アップロードセッションの開始

Section titled “ステップ1:アップロードセッションの開始”

バッチに含まれるファイルの合計数を宣言します。13枚の画像を含む完全なZFキャプチャの場合、1 + 1 + 13 + 13 = 28です。

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

レスポンスはsessionIdとBundleのrequirementsを返します:

{
"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とタイプ間依存関係の理解

Section titled “requirementsとタイプ間依存関係の理解”

requirementsの各エントリは、Bundleが受け入れる1つのファイルタイプを記述します:

フィールド意味
typeファイルアップロード開始時にtypeとして渡す識別子。
allowedExtensionsこのタイプで受け入れられるファイル拡張子。
optionalfalseの場合、このタイプのファイルが少なくとも1つないとBundleを処理できない。
allowMultiplesfalseの場合、このタイプのファイルは1つのみアップロード可能。
dependenciesこのタイプが存在する場合に常に存在する必要がある他のタイプ。

ZFの例では:

  • **pointcloudstrajectories**は必須(optional: false)で単一(allowMultiples: false)です。すべてのZF Bundleにはそれぞれ正確に1つずつ必要です。
  • **picturespicturesMetadata**は任意で、複数ファイルを受け入れます。
  • 2つの画像タイプは互いに依存します:pictures.dependencies = ["picturesMetadata"]およびpicturesMetadata.dependencies = ["pictures"]

依存関係とは、タイプが存在する場合、リストされたすべてのタイプも存在する必要があるという意味です。ZFの画像タイプは互いを参照するため、ペアとしてオールオアナッシングです。画像とメタデータを一緒にアップロードするか、両方をスキップできますが、一方だけをアップロードすることはできません。

有効なZFバッチ:

  • 最小: 1点群 + 1軌跡(画像なし)。expectedFileCount = 2
  • 完全: 1点群 + 1軌跡 + N pictures + N picturesMetadataexpectedFileCount = 2 + 2N

無効:

  • メタデータなしの画像(またはその逆)。タイプ間依存関係が満たされないため。
  • 点群または軌跡がないバッチ。必須タイプが欠落しているため。

セッションを開始する前に、アップロード予定のすべてのファイルを数え、その合計をexpectedFileCountとして設定します。任意タイプはバッチに含める場合のみカウントされます。セッションはその数のファイルが確定されると完了するため、宣言する数は実際にアップロードする数と一致する必要があります。

ステップ2:各ファイルのアップロード

Section titled “ステップ2:各ファイルのアップロード”

バッチ内のすべてのファイルについて、以下の3つの呼び出しを繰り返します。

ファイル名、バイト単位のサイズ、typerequirementsタイプの1つ)を渡します:

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

レスポンスはマルチパートアップロードを記述します。送信するpartsの数と、パーツごとに1つの署名付きurlです。

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

ファイルをparts個の連続チャンク(各ceil(fileSize / parts)バイト)に分割し、各チャンクを署名付きurlPUTします。これらのリクエストには認証ヘッダーを送信しません。URLはすでに署名されています。すべてのパーツのETagレスポンスヘッダーを保持してください。確定時に必要です。

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

ETagは返されたとおり、囲む引用符を含めて渡してください。

収集したパーツを送信してファイルを完了します。エンドポイントは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…\"" } ] }

レスポンスはファイルをエコーし、セッションステータスを報告します:

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

sessionStatusは、最後の期待ファイルが確定されるまでpendingのままです。その時点でcompletedになります。

署名付きURLは期限切れになります。アップロードが長時間かかり、PUT403を返し始めた場合、停止したパーツからURLを再発行して続行します:

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

ファイルを再開始する必要はありません。パーツURLのみが更新されます。

Bundleのセッションを一覧表示して進捗を確認するか、sessionIdをメモリに保持せずに中断したバッチを再開します。リストはページネーションとソートが可能で、任意のstatusフィルタがあります。

クエリパラメータ:page(デフォルト1)、limit(デフォルト20、最大20)、sortBycreatedAtまたはstatus、デフォルトcreatedAt)、sortDirASCまたはDESC、デフォルトASC)、status(任意フィルタ:pendingまたは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
}

finalizedFileCountexpectedFileCountの比較で、残りのファイル数がわかります。再開するには、まだ完了していないファイルのみを開始して確定します。

RealityConnect APIでデータをアップロードする際の責任範囲を決めるため、注意深くお読みください。

  • アップロードエンドポイントは、指定したtypeの下に各ファイルを記録し、件数のみでセッションを完了します。finalizedFileCountexpectedFileCountに等しくなると、セッションはcompletedになります。アップロード時、サーバーはtyperequirementsの1つであること、ファイル拡張子が許可されていること、必須タイプが存在すること、タイプ間依存関係が満たされていることを検証しません
  • すべてのルール(必須タイプ、allowMultiples、タイプ間のdependencies)は、Bundleが処理に送られた後に適用されます。requirementsを無視するバッチはアップロードは成功しますが、処理に失敗します。
  • 深いコンテンツ検証はアップロードAPIでは行われません。 .lasが本物のZF点群であるか、.jsonが画像を正しく参照しているかは、処理時にのみチェックされます。

requirementsを契約として扱ってください。アップロードバッチでタイプ、拡張子、任意性、多重度、依存関係を遵守し、アップロード後にBundleを処理可能にしてください。不正なアップロード構造による処理失敗は返金されません。

ZFキャプチャをまとめた例です。filesはローカルデータセットから構築するリストで、各エントリはパスとrequirementsからのtypeをペアにします。

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"

すべてのファイルがアップロードされ、セッションがcompletedになったら、処理とモニタリングに進み、生入力を表示可能な出力に変換してください。