Data Bundleへのデータアップロード
このガイドでは、RealityConnect APIを通じてキャプチャの生入力ファイルをData Bundleにアップロードする方法を説明します。
すべてのアップロードエンドポイントにはwrite:bundleスコープが必要です。セッションの一覧にはread:bundleが必要です。
アップロードフロー
Section titled “アップロードフロー”Data Bundleは1回のキャプチャの生入力を保持します。点群、軌跡、画像、画像ごとのメタデータなどです。Bundleが受け入れるファイルの正確なセットは、Bundleが作成されたデバイスによって異なります。
ファイルはアップロードセッションでアップロードされます。セッションは、Bundleのファイル1バッチ用にサーバーが作成するコンテナです。開始時にバッチに含まれるファイル数(expectedFileCount)を宣言します。その数のファイルが確定されると、セッションは自動的に完了します。
各ファイルはS3のマルチパートアップロードでアップロードされます:
- Bundleでセッションを開始し、送信するファイルの合計数を宣言する。
- 各ファイルについて:アップロードを開始し、返されたURLにパーツを直接PUTし、確定する。
- 確定されたファイル数が
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キャプチャ
Section titled “実践例:ZF SLAMキャプチャ”ZF SLAM Bundleは、タイプ間依存関係を持つ4つの異なるファイルタイプを受け入れるため、良い例です。完全なZFキャプチャは以下で構成されます:
| ファイル | タイプ | 備考 |
|---|---|---|
scan.las | 点群 | 正確に1つ |
…laser_0.txt | 軌跡 | 正確に1つ |
panorama/*.jpg | パノラマ画像 | 多数、各画像にメタデータがペア |
panorama/*.json | 画像ごとのメタデータ | 多数、画像1つにつき1つ |
したがって、完全なZFバッチは1点群 + 1軌跡 + N画像 + Nメタデータファイルです。これらすべてを1つのセッションでアップロードします。
エンドポイント
Section titled “エンドポイント”すべてのパスはリージョンのapi_urlを基準とします。Bundleは独自のid(bundleId)でアドレス指定されます。
| メソッド | パス | スコープ | 用途 |
|---|---|---|---|
POST | /v1/bundles/{bundleId}/upload-sessions | write:bundle | セッションの開始 |
GET | /v1/bundles/{bundleId}/upload-sessions | read:bundle | セッションの一覧(再開 / 進捗) |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files | write:bundle | ファイルアップロードの開始 |
GET | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex} | write:bundle | 期限切れパーツURLの再発行 |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalize | write: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-sessionsAuthorization: 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 | このタイプで受け入れられるファイル拡張子。 |
optional | falseの場合、このタイプのファイルが少なくとも1つないとBundleを処理できない。 |
allowMultiples | falseの場合、このタイプのファイルは1つのみアップロード可能。 |
dependencies | このタイプが存在する場合に常に存在する必要がある他のタイプ。 |
ZFの例では:
- **
pointcloudsとtrajectories**は必須(optional: false)で単一(allowMultiples: false)です。すべてのZF Bundleにはそれぞれ正確に1つずつ必要です。 - **
picturesとpicturesMetadata**は任意で、複数ファイルを受け入れます。 - 2つの画像タイプは互いに依存します:
pictures.dependencies = ["picturesMetadata"]およびpicturesMetadata.dependencies = ["pictures"]。
依存関係とは、タイプが存在する場合、リストされたすべてのタイプも存在する必要があるという意味です。ZFの画像タイプは互いを参照するため、ペアとしてオールオアナッシングです。画像とメタデータを一緒にアップロードするか、両方をスキップできますが、一方だけをアップロードすることはできません。
有効なZFバッチ:
- 最小:
1点群 + 1軌跡(画像なし)。expectedFileCount = 2。 - 完全:
1点群 + 1軌跡 + N pictures + N picturesMetadata。expectedFileCount = 2 + 2N。
無効:
- メタデータなしの画像(またはその逆)。タイプ間依存関係が満たされないため。
- 点群または軌跡がないバッチ。必須タイプが欠落しているため。
セッションを開始する前に、アップロード予定のすべてのファイルを数え、その合計をexpectedFileCountとして設定します。任意タイプはバッチに含める場合のみカウントされます。セッションはその数のファイルが確定されると完了するため、宣言する数は実際にアップロードする数と一致する必要があります。
ステップ2:各ファイルのアップロード
Section titled “ステップ2:各ファイルのアップロード”バッチ内のすべてのファイルについて、以下の3つの呼び出しを繰り返します。
2a. 開始
Section titled “2a. 開始”ファイル名、バイト単位のサイズ、type(requirementsタイプの1つ)を渡します:
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/filesAuthorization: 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?…" } ]}2b. パーツのアップロード
Section titled “2b. パーツのアップロード”ファイルをparts個の連続チャンク(各ceil(fileSize / parts)バイト)に分割し、各チャンクを署名付きurlにPUTします。これらのリクエストには認証ヘッダーを送信しません。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 partsETagは返されたとおり、囲む引用符を含めて渡してください。
2c. 確定
Section titled “2c. 確定”収集したパーツを送信してファイルを完了します。エンドポイントは200 OKを返します。
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizeAuthorization: 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の更新
Section titled “期限切れURLの更新”署名付きURLは期限切れになります。アップロードが長時間かかり、PUTが403を返し始めた場合、停止したパーツからURLを再発行して続行します:
GET {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/0Authorization: Bearer {access_token}{ "startIndex": 0, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ] }ファイルを再開始する必要はありません。パーツURLのみが更新されます。
ステップ3:進捗の追跡と再開
Section titled “ステップ3:進捗の追跡と再開”Bundleのセッションを一覧表示して進捗を確認するか、sessionIdをメモリに保持せずに中断したバッチを再開します。リストはページネーションとソートが可能で、任意のstatusフィルタがあります。
クエリパラメータ:page(デフォルト1)、limit(デフォルト20、最大20)、sortBy(createdAtまたはstatus、デフォルトcreatedAt)、sortDir(ASCまたはDESC、デフォルトASC)、status(任意フィルタ:pendingまたはcompleted)。
GET {api_url}/v1/bundles/{bundleId}/upload-sessions?page=1&limit=20&sortBy=createdAt&sortDir=DESCAuthorization: Bearer {access_token}{ "items": [ { "sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b", "status": "pending", "expectedFileCount": 28, "finalizedFileCount": 14 } ], "total": 1}finalizedFileCountとexpectedFileCountの比較で、残りのファイル数がわかります。再開するには、まだ完了していないファイルのみを開始して確定します。
RealityConnect APIでデータをアップロードする際の責任範囲を決めるため、注意深くお読みください。
- アップロードエンドポイントは、指定した
typeの下に各ファイルを記録し、件数のみでセッションを完了します。finalizedFileCountがexpectedFileCountに等しくなると、セッションはcompletedになります。アップロード時、サーバーはtypeがrequirementsの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 batchfiles = [ ("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 = Nonefor 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"次のステップ
Section titled “次のステップ”すべてのファイルがアップロードされ、セッションがcompletedになったら、処理とモニタリングに進み、生入力を表示可能な出力に変換してください。