Skip to main content

Automated camera placement optimiser for markerless biomechanics motion capture labs

Project description

Lab Camera Optimizer

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.

Project overview


Features

  • 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.


Preview

Room layout (preview_room.py)

Room preview

Optimisation result

Optimisation result


Installation

Option A — pip install (recommended)

pip install lab-camera-optimizer

Then initialise a working directory with the example configs:

mkdir my-lab && cd my-lab
lab-camera-init

This 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.yaml

Option B — Clone and run locally

git 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.yaml

Python ≥ 3.10 recommended.


Quick start

1. Preview your room layout

Before running the (potentially long) optimisation, always verify your room geometry, obstacles and capture zones visually:

python preview_room.py --config configs/example_simple.yaml

This 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.py can be run standalone at any time — it does not require the optimiser to have been run first.

2. Run the optimiser

python optimize.py --config configs/example_simple.yaml

The 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-preview

Results are saved in outputs/:

  • FINAL_RESULT_*.png — 4-panel figure (top view, heatmap, side view, coverage bar chart)
  • graphs/ — intermediate graphs for each optimisation attempt
  • graphs_optimal/ — best result per zone combination, ranked by score
  • log_*.txt — full optimisation log
  • preview_room/ — room layout previews

Adapting to your lab

Step 1 — Choose a starting config

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.yaml

Step 2 — Edit my_lab.yaml

The YAML file has six sections:

room — room geometry

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 — walls, pillars, furniture

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: false

Tip: run python preview_room.py --config configs/my_lab.yaml after every obstacle addition to verify the geometry looks right before optimising.

subject — person being recorded

subject:
  height: 1.9      # metres
  foot_z: 0.0

camera_sets — define your cameras

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

capture_zones — where coverage matters

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: true

Polygon-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 — tuning parameters

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

Step 3 — Preview, then run

# Verify geometry
python preview_room.py --config configs/my_lab.yaml

# Run optimisation
python optimize.py --config configs/my_lab.yaml

Output explained

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.

Example terminal output

=================================================================
  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 axis
  • pos — 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

Room coordinate system

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)

How it works

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 (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

Citing this tool

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.


License

This project is licensed under the MIT License — see the LICENSE file for details.


Contributing

Pull requests and issues are welcome. Please open an issue before submitting large changes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lab_camera_optimizer-1.0.2.tar.gz (54.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lab_camera_optimizer-1.0.2-py3-none-any.whl (49.5 kB view details)

Uploaded Python 3

File details

Details for the file lab_camera_optimizer-1.0.2.tar.gz.

File metadata

  • Download URL: lab_camera_optimizer-1.0.2.tar.gz
  • Upload date:
  • Size: 54.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for lab_camera_optimizer-1.0.2.tar.gz
Algorithm Hash digest
SHA256 b93ca48f8083f89d2ad5dd9d97bd0240c5f997eb18f1f4b7d202d9275147a67f
MD5 0dc4cd5a7112421d8057d68e57a8f2b0
BLAKE2b-256 0b02450e716f15344fd5333030dc25e386f4d8cff17bf40d4c1c9d87569070c5

See more details on using hashes here.

File details

Details for the file lab_camera_optimizer-1.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for lab_camera_optimizer-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cdd21997176499dff8ffd43f7075d984de02ec09b87f033cfd80a1e4fb1d5a5d
MD5 49dc94268697344738a6da42839e8a53
BLAKE2b-256 f13a79d59fe7353a80b88c9ab0ffcb3ba446f2cf21ff11037c16c3f0ff8700af

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page