Turn a folder of images into a 3D model from the command line, using Apple's
Object Capture (PhotogrammetrySession). A reusable Swift engine
(PhotogrammetryKit) plus a photogram CLI. A SwiftUI drop-target app may
follow — the engine is built to be shared.
photogram convert ./shoe-photos shoe.usdz --mode object --detail full- macOS 15+ on Apple silicon with enough RAM/GPU (uses RealityKit's
PhotogrammetrySession). Runphotogram doctorto confirm. - Swift 6 / Xcode 26 toolchain to build.
git clone https://github.com/saidutt46/photogram.git
cd photogram
make install # release build → ~/.local/bin/photogram (+ man page)~/.local/bin is already on PATH on this machine. Verify:
photogram doctor
man photogramOr just run from the source tree without installing:
swift run photogram doctor# Inspect a folder before committing to a 2-minute run
photogram info ./shoe-photos
# Single object, high quality
photogram convert ./shoe-photos shoe.usdz --mode object --detail full --ordering sequential
# A room / scene
photogram convert ./room-photos room.usdz --mode area --detail mediumconvert is the default subcommand, so the word is optional:
photogram ./shoe-photos shoe.usdzIf you omit the output name it defaults to <folder-name>.usdz. Use a .obj
extension to emit OBJ instead of USDZ.
| Command | What it does |
|---|---|
convert <input> [output] |
Reconstruct a model from a folder of images. (default) |
info <input> |
Image count, format breakdown, and whether the folder is usable. |
doctor |
Whether this Mac supports Object Capture, plus engine limits. |
All three accept --json for machine-readable output.
| Flag | Maps to | Notes |
|---|---|---|
--mode |
masking + bounding box combo | object (single item: masked + cropped) vs area (scene/room: no mask, full extent). Default object. |
--detail |
Request.Detail |
preview | reduced | medium | full | raw | custom. Default medium. |
--ordering |
Configuration.sampleOrdering |
unordered | sequential. sequential is faster when frames were captured in a path. |
--sensitivity |
Configuration.featureSensitivity |
normal | high. high helps smooth / low-texture objects. |
--mesh-primitive |
Configuration.meshPrimitive |
triangle | quad. |
--object-masking / --no-object-masking |
isObjectMaskingEnabled |
Override the --mode default. |
--ignore-bounding-box / --no-ignore-bounding-box |
ignoreBoundingBox |
Override the --mode default. |
--checkpoint-dir <dir> |
checkpointDirectory |
Enables resume after interruption. |
--max-polygons <n> |
custom maximumPolygonCount |
--detail custom only. |
--texture-size <size> |
custom maximumTextureDimension |
oneK…sixteenK. Custom only. |
--texture-maps <maps…> |
custom outputTextureMaps |
diffuse normal roughness displacement ambientOcclusion. Custom only. |
--texture-format <fmt> / --jpeg-quality <q> |
custom textureFormat |
png | jpeg. Custom only. |
--roi <6 floats> |
Request.Geometry.bounds |
Crop box minX minY minZ maxX maxY maxZ (meters). |
--force / -f |
— | Overwrite the output if it exists. |
--dry-run |
— | Validate inputs and print the resolved config without running. |
--json |
— | Emit JSON progress on stdout. |
--quiet / -q |
— | Print only the final path and errors. |
PhotogrammetrySession has no literal mode switch — --mode resolves to a
combination of two knobs that mirrors how iOS Object Capture treats each case:
--mode |
object masking | bounding box |
|---|---|---|
object |
on (segment from background) | kept (crop to the object) |
area |
off | ignored (reconstruct the whole scene) |
Use object for a single item shot from all sides; use area for rooms and
scenes. Override either knob individually with --[no-]object-masking /
--[no-]ignore-bounding-box.
Photos only — there is no mesh input. The input type (PhotogrammetrySample)
carries an image plus an optional depth map, gravity vector, and object
mask. Photos captured by Apple's iOS pipeline embed depth (LiDAR) + gravity in
each HEIC, which PhotogrammetrySession reads automatically to improve
real-world scale and accuracy. Plain photos work too; they just lack those
scale hints.
- 20–40 overlapping photos, going all the way around the subject.
- Keep the subject sharp and evenly lit; avoid motion blur and harsh shadows.
- For a single object, isolate it on a contrasting surface and use
--mode object. For a room, use--mode area. - Smooth or shiny / low-texture subjects: add
--sensitivity high.
With --json, convert writes one JSON object per line to stdout
(progress on stderr is suppressed), suitable for scripts or a UI:
event is one of: start, stage, progress, eta, inputComplete,
invalidSample, skippedSample, automaticDownsampling, finished, error.
Processing stages: preProcessing, imageAlignment, pointCloudGeneration,
meshGeneration, textureMapping, optimization.
import PhotogrammetryKit
let engine = PhotogrammetryEngine()
let config = ReconstructionConfig(mode: .object, detail: .full, ordering: .sequential)
for try await event in engine.reconstruct(
inputFolder: inputURL, outputFile: outputURL, config: config) {
switch event {
case .stage(let s): print("stage:", s)
case .progress(let f): print("progress:", f)
case .finished(let url): print("done:", url.path)
default: break
}
}Warning … Failed to resolve reference … baked_mesh_…png— cosmetic. It comes from Apple's USD texture-baking step on a background thread; the final.usdzis complete and self-contained. Not an error.doctorreports not supported — needs an Apple-silicon Mac on macOS 15+ with adequate RAM/GPU.- Patchy / holey result — usually coverage: more overlapping photos, and
match
--modeto the subject. Rooms are inherently harder than single objects.
0 success · 1 reconstruction or runtime failure · 64 invalid arguments /
validation error (standard ArgumentParser usage code).
swift build # debug build
swift test # unit tests (Swift Testing)
swift run photogram …
make manpage # regenerate man/photogram.1 after CLI changesSee CLAUDE.md for architecture notes and invariants.
- Core engine (
PhotogrammetryKit) with full knob coverage - CLI:
convert/info/doctor, object/area modes, JSON output - Tests, man page, install, docs
- SwiftUI Mac app: drag-drop folders, live progress, job queue/history
MIT © 2026 Sai Dutt G.V
{"event":"start","images":31,"mode":"object","detail":"full","output":"/…/shoe.usdz"} {"event":"stage","stage":"imageAlignment"} {"event":"progress","fraction":0.42,"stage":"imageAlignment"} {"event":"eta","secondsRemaining":120} {"event":"inputComplete"} {"event":"finished","output":"/…/shoe.usdz","seconds":139}