Skip to main content

Supported Inputs

A scene is built from one uploaded file: a video, a zip of photos, or a zip holding a COLMAP workspace. A video or photo zip can also come with ARKit camera poses. The content_type you send to POST /v1/scenes decides how the file is read.

Which input should I use?

Rules for every input

  • content_type: video/*, application/zip or application/x-zip-compressed. Anything else is 400 invalid_input, except application/x-r3d: create accepts it, but the pipeline has no Record3D reader and hands the file to its video decoder (_stage_extract_frames). Don’t use it. Leave content_type out and the scene is read as a video (createScene in api/src/lib/scenes.ts).
  • The PUT sends the same Content-Type. The upload URL signs it, so storage refuses a PUT with any other type (generatePresignedUploadUrl).
  • The upload URL lasts 24 hours (UPLOAD_URL_TTL_SECONDS, api/src/lib/r2-presign.ts). A second PUT to it before /process replaces the file.
  • At most 5 GB (5,363,466,240 bytes), the same on every plan (MAX_UPLOAD_BYTES, packages/core/src/uploads.ts). See Upload limits.
  • At most 800 frames or 400 photos are kept (MAX_FRAMES, MAX_PHOTOS, pipeline/training/frame_extract.py). Bigger sets are thinned evenly. A COLMAP workspace is the exception: over 800 registered images is refused.

Where each check runs

/process refuses what it can see before it charges anything. The rest fails the job, and a failed scene’s credits are refunded. After a 400 source_missing or 422 source_unusable the scene stays uploading: PUT a usable file to the same upload URL and call /process again.

Video

  • Containers and codecs. Create accepts any video/* type, e.g. video/quicktime for a MOV. The pipeline decodes with ffmpeg, through sharp-frame-extractor (_extract_sharp_frames); a file it can’t decode fails the job.
  • Frames. The sharpest frame in each 0.67 s window is kept and turned upright from the video’s rotation metadata (_extract_sharp_frames). Frames under half the median sharpness are then dropped, at most 40% of them and never below 24 frames (_gate_frames_by_sharpness).
  • Length: at least 30 seconds, or take at least 20 photos instead (Photo zip). /process and /retrain refuse a shorter video with 422 source_unusable before charging, details {"field": "video_seconds", "value": 12.4, "allowed": {"min": 30}} (MIN_VIDEO_SECONDS). The length is read from the MP4 or MOV header; a video whose length can’t be read that way (WebM, a fragmented MP4 that records no total length, a damaged header) launches, and fewer than 5 frames fails the job (_extract_video_frames). The web app also checks the length in the browser, before the upload (app/upload/page.tsx). A SplatKing video export’s wide.mov is not checked.
  • No 360° video, yet. /process and /retrain refuse a 360° video at any length with 422 source_unusable before charging: COLMAP has no equirectangular camera model, and the trainer can’t render one. That is a video whose frame, as shown (rotation applied), is 2:1 within 1%, details {"field": "video_aspect", "value": 2, "allowed": {"not": 2}} (isEquirectAspect), or one of another shape whose sample entry carries Spherical Video V2 metadata (an sv3d box), {"field": "video_projection", "value": "spherical", "allowed": ["flat"]}. A flat video at exactly 2:1 is refused too. A 360° video under 30 seconds gets this refusal, not the length one. The web app checks the frame in the browser too.
  • 800-frame cap. More than 800 frames are thinned evenly to 800 (_stage_extract_frames). At one frame per 0.67 s, that starts at about 9 minutes of video.

Photo zip

Then POST /v1/scenes/{id}/process with no body.
  • JPEG and PNG only, by extension: .jpg, .jpeg, .png, any case (IMAGE_SUFFIXES). HEIC, HEIF, WebP and every other file are skipped: export them as JPEG first.
  • At least 20 photos, taken all around the subject, or record at least 30 seconds of video. /process and /retrain refuse fewer with 422 source_unusable before charging, details {"field": "photo_count", "value": 12, "allowed": {"min": 20}} (MIN_LAUNCH_PHOTOS), whenever they can read the zip’s file list (screenZipTail). A COLMAP model needs 5, and a SplatKing video export isn’t counted. A zip whose file list can’t be read (ZIP64) launches; under 5 photos the job then fails at extraction and is refunded (_extract_photos_zip).
  • At most 400 kept. A bigger set keeps 400, evenly spaced in path order, the first and last included (_pick_photos).
  • Folders. Photos are read at any depth, so a wrapper folder is fine. Folders, empty files, __MACOSX, dotfiles, Thumbs.db, desktop.ini and symlinks are skipped (_screen_archive_entries).
  • Mixed cameras. Photos may come from different cameras and sizes: photo sets aren’t forced onto one shared camera’s intrinsics (ColmapSiftGlomapBackend).
  • Orientation. Photos are turned upright from their EXIF orientation (_apply_exif_orientation), unless ARKit poses come with them, the zip holds a COLMAP model or loose COLMAP .bin files, or the photos are split across two or more top-level folders.
  • The zip itself. Use your computer’s built-in zip, zip, or 7-Zip’s default: members compressed with bzip2 or LZMA fail the job, as does a member that expands more than 100x or a zip that expands more than 10x (_check_expansion). More than 50,000 files fails too (_screen_archive_entries).

ARKit capture

Send ARKit camera poses in the /process body, with a video or a photo zip uploaded as above.
One pose:
  • Poses. 5 to 1,000 per scene (processSceneBodySchema, api/src/routes/schemas.ts); the SfM backends guide has the field table. Two poses with the same file_path are 400 invalid_input (processScene).
  • What makes it an ARKit job. With 5 or more poses, the API stores them and sets the pipeline’s has_arkit_poses flag (processScene). The pipeline converts them to a pose sidecar, arkit_colmap_poses.json (_download_and_convert_arkit_poses). It treats a job as ARKit only when it has both.
  • What the poses do. GLOMAP still solves the camera poses from the images (_stage_sfm in pipeline/training/impl.py). The ARKit poses set which way is up, and for a video, which frames are cut: up to 150, at the pose timestamps, in the sensor’s native orientation (_extract_arkit_timestamp_frames).
  • Gravity up. Poses must be in a gravity-aligned world with +Y up: ARKit worldAlignment .gravity (the default) or .gravityAndHeading, never .camera. Fewer than 30 frames or photos paired with a pose, or a median tilt residual over 3°, falls back to the camera-based estimate, which lays portrait captures on their side (MIN_PAIRS, MAX_MEDIAN_TILT_DEG, pipeline/training/arkit_gravity.py).
  • Pairing. Video frames pair with poses by video time, allowing a gap of −0.5 to 2.5 s between the video’s first frame and the first pose (up_from_frame_times). Photos pair by file_path, the photo’s path in the zip; the file name alone works when it is unique (up_from_file_names). Photos are used as stored, with no EXIF rotation, so their pixels must be in the camera axes their transform describes.
  • A sparse/ model in the zip is ignored when ARKit poses are sent (_extract_photos_zip).
What the Swift SDK sends. Splat3D records an MP4 under ARKit world tracking with the default gravity alignment, creates the scene with content_type: "video/mp4" and uploads it as video/mp4. Its /process body carries poses sampled at about 10 per second while tracking is normal, thinned evenly to 1,000, plus LiDAR points (at most 50,000) on devices that have LiDAR (SplatScanner, ProcessSceneBody in splat3d/splat-swift-sdk).

COLMAP workspace zip

A zip with a COLMAP sparse model skips structure-from-motion: the model is validated, then training starts. Detection is automatic; there is no flag to set (find_prebuilt_model, pipeline/colmap/prebuilt.py).
Create and upload it as a photo zip (application/zip), then call /process with no body.
  • Layout. images/ plus sparse/<model>/ holding all three binaries, or all three text files (cameras.txt, images.txt, points3D.txt), which are converted to binaries first (convert_text_model, pipeline/colmap/text_model.py); binaries win when a folder has both. A zip whose sparse/ models all lack a binary fails the job; an incomplete set of text files is ignored, and the photos go through SfM. A single wrapper folder is fine. Among several models, sparse/0 wins, then the one with images/ beside its sparse/; a remaining tie fails, naming each (_pick_model). Photos resolve to the nearest images/ at or above the model’s workspace (_resolve_images_prefix).
  • Tool outputs that work as zipped: Inria 3DGS convert.py (its undistorted sparse/0 and images/) and nerfstudio ns-process-data (colmap/sparse/0 with the top-level images/).
  • Validation (validate_prebuilt_reconstruction) fails the job before training, with a message saying what to fix, on:
    • truncated or corrupt binaries;
    • a binary over its size cap, checked at extraction: 8 MB cameras.bin, 512 MB images.bin, 2 GB points3D.bin (MAX_BIN_BYTES), and the same caps on the matching text files (MAX_TEXT_BYTES);
    • a text file line that doesn’t parse, longer than 16 MB, or not UTF-8;
    • a camera model other than SIMPLE_PINHOLE, PINHOLE, SIMPLE_RADIAL, RADIAL or OPENCV;
    • a camera with zero width or height, a focal length that isn’t a positive number, or a parameter that isn’t finite;
    • a pose that isn’t finite, or an all-zero rotation quaternion;
    • a sparse point whose position isn’t finite;
    • an image whose camera id isn’t in cameras.bin;
    • fewer than 5 or more than 800 registered images;
    • fewer than 1,000 or more than 5,000,000 sparse points;
    • a name in images.bin with no matching photo, or listed twice;
    • a photo that isn’t its camera’s width × height, or a uniform resize of it.
  • 800 registered images at most, refused rather than thinned (MAX_REGISTERED_IMAGES): dropping an image would orphan its pose. A text model whose points have no tracks (SplatKing) is thinned instead; see below. Photos the model never registered are ignored (prune_unreferenced_images).
  • Price. The full price of the preset, like any scene: /process prices by preset only (processScene).
  • 5 photos, not 20. SfM is skipped, so the photo zip minimum doesn’t apply: /process refuses fewer than 5 photos, and validation needs 5 registered images.
  • Loose .bin files outside a sparse/ folder don’t count. That zip runs as a photo zip, and GLOMAP solves the poses (_extract_plain_photo_zip).
SfM backends has the full requirements, with the COLMAP commands that fix each failure.

SplatKing export

Zips from the SplatKing iOS app upload as they are: create the scene with content_type: "application/zip", PUT the zip, and call /process with no body.
  • LiDAR capture (LidarSeries_<date>_<time>/). Its COLMAP_Text_Model/ is a COLMAP text model of the app’s ARKit poses, with the photos in COLMAP_Text_Model/images/. It takes the COLMAP workspace path: SfM is skipped and the poses are trained as they are, with the same validation. Photos outside COLMAP_Text_Model/images/ (a rejected frame the app can leave at the capture root) are ignored. The photos are stored sideways for a portrait capture, so up is not estimated from the cameras: the model’s header declares a gravity-aligned world with +Y up, and training uses that (declared_world_up). Training starts from the app’s LiDAR point cloud in points3D.txt (134,977 to 258,496 points in the captures we tested), held to the same 1,000 to 5,000,000 range; the depth maps in sensor_data/ are not used. Those points carry no tracks, so a capture over 800 frames (about 14 minutes at the app’s rate) keeps 800, evenly spaced by name (_cap_text_model).
  • Video capture (Video_<date>_<time>/). It holds no photos and no camera poses, so wide.mov (the 1x lens) is processed as a video and ultra.mov is ignored (_pick_zip_video). This applies to any zip with fewer than 5 photos and one file named wide.mov; /process refuses two such files with 422 source_unusable. With 5 to 19 photos beside a wide.mov, the photos are processed, so /process refuses the zip as too few photos. ultra.mov doubles the zip’s size, so a capture longer than about 3 minutes passes the 5 GB upload cap: upload its wide.mov alone as a video instead.
  • Upload limits: the 5 GB cap and how it is checked.
  • SfM backends: pose estimation, ARKit poses and COLMAP models.
  • Quickstart: the full create, upload, process and download loop.