Optimal camera placement for markerless biomechanics motion capture labs.
Given a room layout, a set of cameras and a capture zone, this tool finds the configuration that maximises 3D body coverage (head-to-toe visibility) across all evaluation points, with optional bilateral coverage constraints.
- Any room shape — define any polygon floor plan (L-shaped, rectangular, etc.)
- Obstacles & walls — pillars, partial-height furniture, irregular wall segments
- Multiple camera sets — wall-mounted cameras + optional tripod cameras
- Per-camera height variation — each camera in the same configuration can be at a different height
- 3D visibility — checks both horizontal FOV and vertical body coverage (0 → subject height)
- Line-of-sight — occlusion by walls and floor-to-ceiling obstacles
- Bilateral constraint — ensures coverage from both sides of the capture axis (configurable weight)
- Zone sweep — automatically tests multiple corridor/polygon zone positions and finds the best layout
- Room preview — top-down visualisation of your room before running optimisation
- YAML configuration — no code editing required to adapt to your lab
Want to understand how it works?
See ALGORITHM.md for a full explanation of the scoring function, the greedy optimisation, the combo sweep and all tuning parameters.
pip install lab-camera-optimizerThen initialise a working directory with the example configs:
mkdir my-lab && cd my-lab
lab-camera-initThis copies the example YAML configs into configs/ and creates the outputs/
folder in your current directory. Three commands are then available:
lab-camera-init # copy example configs (run once)
lab-camera-preview --config configs/example_simple.yaml
lab-camera-optimizer --config configs/example_simple.yamlgit clone https://github.com/flodelaplace/lab-camera-optimizer.git
cd lab-camera-optimizer
pip install -r requirements.txt
python optimize.py --config configs/example_simple.yaml
python preview_room.py --config configs/example_simple.yamlPython ≥ 3.10 recommended.
Before running the (potentially long) optimisation, always verify your room geometry, obstacles and capture zones visually:
python preview_room.py --config configs/example_simple.yamlThis saves a top-down PNG to outputs/preview_room/ and opens an interactive
window. Use it every time you modify your config to catch geometry errors early.
Tip:
preview_room.pycan be run standalone at any time — it does not require the optimiser to have been run first.
python optimize.py --config configs/example_simple.yamlThe room preview is shown automatically at startup. Close the window to start the optimisation. To skip the preview (e.g. for batch runs):
python optimize.py --config configs/example_simple.yaml --no-previewResults are saved in outputs/:
FINAL_RESULT_*.png— 4-panel figure (top view, heatmap, side view, coverage bar chart)graphs/— intermediate graphs for each optimisation attemptgraphs_optimal/— best result per zone combination, ranked by scorelog_*.txt— full optimisation logpreview_room/— room layout previews
| File | Description |
|---|---|
configs/example_simple.yaml |
Start here — 10×6 m rectangle, one camera set, no obstacles |
configs/example_real_world.yaml |
Full real-world example — L-shaped room, obstacles, two camera sets, corridor |
configs/T_zone_direction_change.yaml |
T-shaped capture zone for direction-change analysis |
cp configs/example_simple.yaml configs/my_lab.yamlThe YAML file has six sections:
room:
corners: [[0,0],[10,0],[10,5],[0,5]] # (X,Y) vertices in metres, in order
height: 3.0 # floor-to-ceiling height (metres)obstacles:
- type: polygon
vertices: [[1.0,0.0],[1.2,0.0],[1.2,0.5],[1.0,0.5]]
height: 3.0 # = room height → fully blocks line-of-sight
label: "Pillar A"
can_mount_camera: false
- type: polygon
vertices: [[2,0],[2,1],[3,1],[3,0]]
height: 1.2 # partial height → cameras can see over it
label: "Table"
can_mount_camera: falseTip: run
python preview_room.py --config configs/my_lab.yamlafter every obstacle addition to verify the geometry looks right before optimising.
subject:
height: 1.9 # metres
foot_z: 0.0camera_sets:
- id: "cam_A"
name: "My Camera"
mounting: "wall" # wall | tripod
fov_h_landscape: 110.0
fov_v_landscape: 70.0
fov_h_portrait: 70.0
fov_v_portrait: 110.0
height_options: [2.0, 2.2] # tested heights (metres)
max_range: 10.0
min_range: 0.5
max_count: 8
min_spacing: 1.2
score_weight: 1.0
color: "#1f77b4"Corridor-based (walking path):
capture_zones:
- id: "full_corridor"
type: "corridor"
priority: 0.5
length: 10.0
width: 1.0
placement:
x_start_options: [1.0, 2.0]
y_options: [1.5, 2.0]
- id: "analysis_zone"
type: "sub_zone"
priority: 1.0
length: 6.0
contained_in: "full_corridor"
offset_options: [1.0, 2.0, 3.0]
- id: "key_point"
type: "point"
priority: 2.0
radius: 0.5
contained_in: "analysis_zone"
auto_optimize: truePolygon-based (arbitrary shape — L, T, cross…):
capture_zones:
- id: "approach"
type: "polygon"
priority: 1.0
grid_step: 0.30
vertices:
- [0.0, 0.0]
- [6.0, 0.0]
- [6.0, 1.0]
- [0.0, 1.0]
placement:
x_offsets: [0.0, 0.5]
y_offsets: [2.0, 2.5]optimization:
target_coverage: 4 # cameras per evaluation point
bilateral_weight: 0.8 # 0 = disabled, 1 = fully enforced
vertical_coverage_threshold: 0.9
restarts_per_combo: 15
algo: "greedy_1opt" # greedy | greedy_1opt
early_stop: 5 # stop after N restarts with no improvement
graph_mode: "best_per_combo" # all | records_only | best_per_combo
wall_step: 0.35
angle_steps: 24
tripod_grid_step: 0.70
distance_quality_factor: 0.001# Verify geometry
python preview_room.py --config configs/my_lab.yaml
# Run optimisation
python optimize.py --config configs/my_lab.yaml| Panel | Description |
|---|---|
| Top-left: Top view | Room plan + camera cones. Grey = blind zone. Coloured = useful coverage. |
| Top-right: XY heatmap | Number of cameras covering each floor position in 3D. |
| Bottom-left: XZ side view | Number of cameras covering each height slice along the room length. |
| Bottom-right: Coverage bar chart | Camera count per X position along the corridor. Green = target reached. |
=================================================================
OPTIMAL CONFIGURATION
=================================================================
Score: 829.08
Bilateral: SOUTH=939.9 (49%) NORTH=981.1 (51%) balance=96% OK
Wall cameras (cam_A):
A 1 [L][S] pos=(0.00m, 0.36m) h=2.2m
Pan: 10deg to the RIGHT | Tilt: 50.3deg downward
A 2 [P][S] pos=(8.78m, 0.00m) h=2.0m
Pan: 55deg to the RIGHT | Tilt: 36.9deg downward
A 3 [P][S] pos=(4.22m, 0.00m) h=2.0m
Pan: 55deg to the LEFT | Tilt: 36.9deg downward
A 4 [P][S] pos=(11.95m, 0.00m) h=2.2m
Pan: 55deg to the RIGHT | Tilt: 41.8deg downward
A 5 [P][N] pos=(8.00m, 4.70m) h=2.2m
Pan: 55deg to the RIGHT | Tilt: 20.7deg downward
A 6 [L][N] pos=(0.00m, 2.53m) h=2.0m
Pan: 10deg to the LEFT | Tilt: 42.9deg downward
A 7 [P][S] pos=(2.04m, 0.00m) h=2.2m
Pan: 55deg to the LEFT | Tilt: 41.8deg downward
A 8 [L][N] pos=(2.04m, 4.07m) h=2.2m
Pan: 35deg to the LEFT | Tilt: 25.0deg downward
A 9 [P][N] pos=(0.00m, 4.34m) h=2.2m
Pan: 25deg to the LEFT | Tilt: 23.0deg downward
A10 [L][N] pos=(13.00m, 3.00m) h=2.2m
Pan: 35deg to the RIGHT | Tilt: 38.0deg downward
Tripod cameras (cam_B):
B1 [P] pos=(1.10m, 0.40m) h=1.5m angle=20deg tilt=28.8deg
B2 [L] pos=(10.90m, 0.40m) h=1.5m angle=180deg tilt=28.8deg
=================================================================
Each camera line gives:
[L]/[P]— landscape or portrait orientation[S]/[N]— south or north side of the capture axispos— XY position on the wall (metres)h— mounting height (metres)- Pan — horizontal angle left/right from the wall normal
- Tilt — vertical angle downward toward the subject
Y
^
|
| (room interior)
|
+-----------> X
(0,0)
- X axis: room length (left → right)
- Y axis: room width (bottom → top)
- Z axis: height (floor = 0)
- Angles: 0° = East (+X), 90° = North (+Y), 180° = West (−X)
The full technical documentation is in ALGORITHM.md. Here is a short summary:
| Component | Description |
|---|---|
| Sample grid | Evaluation points distributed across capture zones, weighted by priority |
| Score per point | v² (vertical body coverage) × dist_quality × bilateral factor × angular diversity |
| Bilateral factor | Rewards cameras on both sides of the capture axis (configurable weight) |
| Greedy | Cameras added one by one to maximise score; fast but local optima |
| Greedy + 1-opt | Greedy init with diverse spatial spread, then 1-opt local search; recommended |
| Coverage ratio | Penalises candidates that see only a tiny slice of the zone, regardless of proximity |
| Combo sweep | Tests all combinations of zone positions (X offset, Y position, run-up distance) |
| walk_y | Bilateral axis auto-derived from the highest-priority zone centroid each combo |
If you use this tool in a research publication, please cite it using the
CITATION.cff file at the root of this repository (GitHub shows a
"Cite this repository" button automatically).
Delaplace F. — Lab Camera Optimizer: automated camera placement for markerless biomechanics motion capture laboratories — 2026.
This project is licensed under the MIT License — see the LICENSE file for details.
Pull requests and issues are welcome. Please open an issue before submitting large changes.


