lab-camera-optimizer 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {lab_camera_optimizer-1.0.2/lab_camera_optimizer.egg-info → lab_camera_optimizer-1.0.3}/PKG-INFO +1 -1
  2. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/init_project.py +23 -12
  3. lab_camera_optimizer-1.0.3/lab_camera_optimizer/__init__.py +3 -0
  4. lab_camera_optimizer-1.0.3/lab_camera_optimizer/configs/T_zone_direction_change.yaml +200 -0
  5. lab_camera_optimizer-1.0.3/lab_camera_optimizer/configs/example_real_world.yaml +270 -0
  6. lab_camera_optimizer-1.0.3/lab_camera_optimizer/configs/example_simple.yaml +180 -0
  7. lab_camera_optimizer-1.0.3/lab_camera_optimizer/configs/labo_CHU.yaml +268 -0
  8. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3/lab_camera_optimizer.egg-info}/PKG-INFO +1 -1
  9. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/lab_camera_optimizer.egg-info/SOURCES.txt +6 -1
  10. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/lab_camera_optimizer.egg-info/top_level.txt +1 -0
  11. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/pyproject.toml +3 -3
  12. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/LICENSE +0 -0
  13. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/MANIFEST.in +0 -0
  14. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/README.md +0 -0
  15. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/configs/T_zone_direction_change.yaml +0 -0
  16. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/configs/example_real_world.yaml +0 -0
  17. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/configs/example_simple.yaml +0 -0
  18. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/configs/labo_CHU.yaml +0 -0
  19. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/__init__.py +0 -0
  20. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/candidates.py +0 -0
  21. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/config_loader.py +0 -0
  22. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/greedy.py +0 -0
  23. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/room.py +0 -0
  24. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/scoring.py +0 -0
  25. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/core/visualize.py +0 -0
  26. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/lab_camera_optimizer.egg-info/dependency_links.txt +0 -0
  27. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/lab_camera_optimizer.egg-info/entry_points.txt +0 -0
  28. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/lab_camera_optimizer.egg-info/requires.txt +0 -0
  29. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/optimize.py +0 -0
  30. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/preview_room.py +0 -0
  31. {lab_camera_optimizer-1.0.2 → lab_camera_optimizer-1.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lab-camera-optimizer
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Automated camera placement optimiser for markerless biomechanics motion capture labs
5
5
  Author: Florian Delaplace
6
6
  License-Expression: MIT
@@ -13,20 +13,31 @@ import sys
13
13
 
14
14
  def _find_bundled_configs():
15
15
  """
16
- Locate the bundled configs/ directory whether the package is:
17
- - installed via pip (configs/ sits next to this file in site-packages)
18
- - run locally from the repo root
16
+ Locate the bundled configs/ directory.
17
+ After pip install, configs live inside the lab_camera_optimizer package:
18
+ site-packages/lab_camera_optimizer/configs/
19
+ When run locally from the repo, they are at:
20
+ ./configs/ or ./lab_camera_optimizer/configs/
19
21
  """
20
- # Same directory as this file
22
+ import importlib.util
23
+
24
+ # 1. Try via the installed lab_camera_optimizer package (pip install path)
25
+ spec = importlib.util.find_spec("lab_camera_optimizer")
26
+ if spec and spec.origin:
27
+ pkg_dir = os.path.dirname(spec.origin)
28
+ candidate = os.path.join(pkg_dir, "configs")
29
+ if os.path.isdir(candidate):
30
+ return candidate
31
+
32
+ # 2. Local repo: configs/ next to this file
21
33
  here = os.path.dirname(os.path.abspath(__file__))
22
- candidate = os.path.join(here, "configs")
23
- if os.path.isdir(candidate):
24
- return candidate
25
-
26
- # One level up (editable install or local run from a sub-dir)
27
- candidate = os.path.join(os.path.dirname(here), "configs")
28
- if os.path.isdir(candidate):
29
- return candidate
34
+ for candidate in [
35
+ os.path.join(here, "configs"),
36
+ os.path.join(here, "lab_camera_optimizer", "configs"),
37
+ os.path.join(os.path.dirname(here), "configs"),
38
+ ]:
39
+ if os.path.isdir(candidate):
40
+ return candidate
30
41
 
31
42
  return None
32
43
 
@@ -0,0 +1,3 @@
1
+ # lab_camera_optimizer package
2
+ # This package contains the core modules, entry points and bundled configs.
3
+
@@ -0,0 +1,200 @@
1
+ # =============================================================
2
+ # Lab Camera Optimizer — T-shaped capture zone
3
+ # Use case: approach run + 90° direction change at the END
4
+ #
5
+ # Room : 12m × 7m rectangle with a small structural pillar
6
+ # Cameras: 10 wall-mounted cameras at 2.0m or 2.2m height
7
+ #
8
+ # T-shape (RELATIVE coordinates, junction at the end of the run):
9
+ #
10
+ # [8,-1.5]──[10,-1.5]
11
+ # │ left │ ← turn left priority 1.8
12
+ # │ turn │
13
+ # [0,0]──────────────────[8, 0]──[10, 0]
14
+ # │ approach (8m×1m) │ inter │ priority 1.0 / 2.5
15
+ # [0,1]──────────────────[8, 1]──[10, 1]
16
+ # │ right │ ← turn right priority 1.8
17
+ # │ turn │
18
+ # [8, 2.5]──[10,2.5]
19
+ #
20
+ # Approach arm : X=0→8, Y=0→1 (8m long × 1m wide)
21
+ # Turn arm : X=8→10, Y=-1.5→2.5 (2m wide × 4m total)
22
+ # Intersection : X=8→10, Y=0→1 (2m × 1m — end of run)
23
+ #
24
+ # The whole T translates via x_offsets / y_offsets.
25
+ # =============================================================
26
+
27
+ # ─────────────────────────────────────────────────────────────
28
+ # 1. ROOM
29
+ # ─────────────────────────────────────────────────────────────
30
+ room:
31
+ corners:
32
+ - [0, 0]
33
+ - [12, 0]
34
+ - [12, 7]
35
+ - [0, 7]
36
+ height: 3.0
37
+
38
+ # ─────────────────────────────────────────────────────────────
39
+ # 2. OBSTACLES
40
+ # ─────────────────────────────────────────────────────────────
41
+ obstacles:
42
+ - type: rect
43
+ bounds: [5.8, 0.0, 6.1, 0.3]
44
+ height: 3.0
45
+ label: "Pillar"
46
+ can_mount_camera: false
47
+
48
+ # ─────────────────────────────────────────────────────────────
49
+ # 3. SUBJECT
50
+ # ─────────────────────────────────────────────────────────────
51
+ subject:
52
+ height: 1.9
53
+ foot_z: 0.0
54
+
55
+ # ─────────────────────────────────────────────────────────────
56
+ # 4. CAMERA SETS
57
+ # ─────────────────────────────────────────────────────────────
58
+ camera_sets:
59
+
60
+ - id: "cam_A"
61
+ name: "Wall camera"
62
+ mounting: "wall"
63
+ optional: false
64
+ fov_h_landscape: 110.0
65
+ fov_v_landscape: 70.0
66
+ fov_h_portrait: 70.0
67
+ fov_v_portrait: 110.0
68
+ max_range: 12.0
69
+ min_range: 0.5
70
+ height_options: [2.0, 2.2]
71
+ max_count: 10
72
+ min_spacing: 1.2
73
+ score_weight: 1.0
74
+ color: "#1f77b4"
75
+
76
+ - id: "cam_B"
77
+ name: "Tripod camera"
78
+ mounting: "tripod"
79
+ optional: true
80
+ fov_h_landscape: 80.0
81
+ fov_v_landscape: 58.0
82
+ fov_h_portrait: 58.0
83
+ fov_v_portrait: 80.0
84
+ max_range: 8.0
85
+ min_range: 0.5
86
+ height_options: [1.5]
87
+ max_count: 0 # disabled — set > 0 to re-enable
88
+ min_spacing: 1.5
89
+ walk_axis_margin: 0.7
90
+ score_weight: 0.6
91
+ color: "#d62728"
92
+
93
+ # ─────────────────────────────────────────────────────────────
94
+ # 5. CAPTURE ZONES
95
+ # ─────────────────────────────────────────────────────────────
96
+ #
97
+ # All coordinates are RELATIVE.
98
+ # The entire T moves together via the shared x_offsets / y_offsets.
99
+ #
100
+ # Visual (relative coords):
101
+ #
102
+ # [8,-1.5]──[10,-1.5]
103
+ # │ LEFT │
104
+ # │ TURN │ priority 1.8
105
+ # [0,0]──────────────────[8, 0 ]──[10, 0]
106
+ # │ ║ INTER ║
107
+ # │ APPROACH (8m×1m) ║ 2.5 ║ priority 1.0
108
+ # │ ║ ║
109
+ # [0,1]──────────────────[8, 1 ]──[10, 1]
110
+ # │ RIGHT │
111
+ # │ TURN │ priority 1.8
112
+ # [8, 2.5]──[10,2.5]
113
+ #
114
+ capture_zones:
115
+
116
+ # ── Level 1 : approach corridor (8m × 1m) ────────────────────────────
117
+ # The full run-up. Covered but not the priority focus.
118
+ - id: "approach"
119
+ type: "polygon"
120
+ priority: 1.0
121
+ grid_step: 0.30
122
+ vertices:
123
+ - [1.0, 0.0] # start bottom-left
124
+ - [7.0, 0.0] # end bottom-right (junction with turn arm)
125
+ - [7.0, 1.0] # end top-right
126
+ - [1.0, 1.0] # start top-left
127
+ placement:
128
+ # 3 X positions × 2 Y positions = 6 combos
129
+ # x_offsets : left end of approach at X = 0, 0.5, 1.0
130
+ # y_offsets : bottom of approach at Y = 2.5, 3.0
131
+ x_offsets: [0.0, 0.5, 1.0]
132
+ y_offsets: [2.5, 3.0]
133
+
134
+ # ── Level 2 : turn arm — left side (toward +Y, 2m × 1.5m) ───────────
135
+ # The portion of the perpendicular corridor ABOVE the approach axis.
136
+ # Subject turns left here.
137
+ - id: "turn_left"
138
+ type: "polygon"
139
+ priority: 1.8
140
+ grid_step: 0.25
141
+ vertices:
142
+ - [8.0, -1.5] # far end top-left
143
+ - [10.0, -1.5] # far end top-right
144
+ - [10.0, 0.0] # meets approach top (intersection boundary)
145
+ - [8.0, 0.0] # meets approach top (intersection boundary)
146
+ placement:
147
+ x_offsets: [0.0, 0.5, 1.0]
148
+ y_offsets: [2.5, 3.0]
149
+
150
+ # ── Level 2 : turn arm — right side (toward -Y, 2m × 1.5m) ──────────
151
+ # The portion of the perpendicular corridor BELOW the approach axis.
152
+ # Subject turns right here.
153
+ - id: "turn_right"
154
+ type: "polygon"
155
+ priority: 1.8
156
+ grid_step: 0.25
157
+ vertices:
158
+ - [8.0, 1.0] # meets approach bottom (intersection boundary)
159
+ - [10.0, 1.0] # meets approach bottom (intersection boundary)
160
+ - [10.0, 2.5] # far end bottom-right
161
+ - [8.0, 2.5] # far end bottom-left
162
+ placement:
163
+ x_offsets: [0.0, 0.5, 1.0]
164
+ y_offsets: [2.5, 3.0]
165
+
166
+ # ── Level 3 : intersection (2m × 1m) — maximum priority ──────────────
167
+ # Where the approach meets the turn arm. The direction change happens
168
+ # here — maximise camera coverage at all costs.
169
+ - id: "intersection"
170
+ type: "polygon"
171
+ priority: 2.5
172
+ grid_step: 0.15
173
+ vertices:
174
+ - [7.0, 0.0] # bottom-left (end of approach / top of right turn)
175
+ - [10.0, 0.0] # bottom-right
176
+ - [10.0, 1.0] # top-right
177
+ - [7.0, 1.0] # top-left
178
+ placement:
179
+ x_offsets: [0.0, 0.5, 1.0]
180
+ y_offsets: [2.5, 3.0]
181
+
182
+ # ─────────────────────────────────────────────────────────────
183
+ # 6. OPTIMISATION PARAMETERS
184
+ # ─────────────────────────────────────────────────────────────
185
+ optimization:
186
+ target_coverage: 5
187
+ bilateral_weight: 0.9
188
+ vertical_coverage_threshold: 0.9
189
+ restarts_per_combo: 15
190
+ wall_step: 0.35
191
+ angle_steps: 24
192
+ tripod_grid_step: 0.80
193
+ distance_quality_factor: 0.005
194
+ # algo options:
195
+ # greedy → pure greedy (fast, original behaviour)
196
+ # greedy_1opt → greedy init + 1-opt local search (better quality, recommended)
197
+ algo: "greedy_1opt"
198
+ early_stop: 5 # stop if no improvement after 5 consecutive restarts (0 = auto)
199
+ graph_mode: "all"
200
+
@@ -0,0 +1,270 @@
1
+ # =============================================================
2
+ # Lab Camera Optimizer — Real-world example configuration
3
+ # L-shaped room with obstacles, two camera sets, corridor zones.
4
+ # Based on an actual biomechanics lab (13m × 4.7m, L-shaped).
5
+ # Use this as a starting point for complex lab geometries.
6
+ # =============================================================
7
+ # Units : metres for all distances, degrees for all angles.
8
+ # Angles follow the standard mathematical convention:
9
+ # 0° = East (+X), 90° = North (+Y), 180°/-180° = West (-X)
10
+ # =============================================================
11
+
12
+ # ─────────────────────────────────────────────────────────────
13
+ # 1. ROOM GEOMETRY
14
+ # ─────────────────────────────────────────────────────────────
15
+ room:
16
+ # List of (X, Y) vertices of the room polygon, in order (CW or CCW).
17
+ # The room is assumed to be a flat floor plan.
18
+ corners:
19
+ - [0, 0 ]
20
+ - [13, 0 ]
21
+ - [13, 3.0]
22
+ - [8, 3.0]
23
+ - [8, 4.7]
24
+ - [0, 4.7]
25
+
26
+ # Floor-to-ceiling height (metres)
27
+ height: 3.0
28
+
29
+
30
+ # ─────────────────────────────────────────────────────────────
31
+ # 2. OBSTACLES
32
+ # ─────────────────────────────────────────────────────────────
33
+ # Two types supported:
34
+ #
35
+ # type: rect → axis-aligned rectangle
36
+ # bounds: [x_min, y_min, x_max, y_max]
37
+ #
38
+ # type: polygon → arbitrary polygon
39
+ # vertices: [[x0,y0],[x1,y1],...] (in order)
40
+ #
41
+ # height: floor-to-ceiling height of the obstacle (metres).
42
+ # = room.height → fully blocks line-of-sight (wall/pillar)
43
+ # < room.height → partial height (object, furniture)
44
+ # does NOT block line-of-sight above it
45
+ #
46
+ # can_mount_camera: true → wall cameras can be placed on this surface
47
+ # false (default) → no camera on this obstacle
48
+
49
+ obstacles:
50
+ - type: rect
51
+ bounds: [1.70, 0.00, 2.04, 0.34]
52
+ height: 3.0
53
+ label: "Stub 1"
54
+ can_mount_camera: true
55
+
56
+ - type: rect
57
+ bounds: [5.39, 0.00, 5.59, 0.42]
58
+ height: 3.0
59
+ label: "Stub 2"
60
+ can_mount_camera: true
61
+
62
+ - type: rect
63
+ bounds: [8.94, 0.00, 9.14, 0.42]
64
+ height: 3.0
65
+ label: "Stub 3"
66
+ can_mount_camera: true
67
+
68
+ - type: rect
69
+ bounds: [5.65, 3.85, 5.85, 4.25]
70
+ height: 3.0
71
+ label: "Stub 4"
72
+ can_mount_camera: true
73
+
74
+ - type: rect
75
+ bounds: [5.65, 2.85, 6.35, 3.55]
76
+ height: 1.4
77
+ label: "Object 1"
78
+ can_mount_camera: false # partial-height object, no camera mount
79
+
80
+ - type: polygon
81
+ vertices:
82
+ - [1.47, 4.70]
83
+ - [1.47, 4.38]
84
+ - [1.71, 4.38]
85
+ - [1.71, 3.88]
86
+ - [2.04, 3.88]
87
+ - [2.04, 4.27]
88
+ - [3.02, 4.27]
89
+ - [3.02, 4.70]
90
+ height: 3.0
91
+ label: "Wall N"
92
+ can_mount_camera: true
93
+
94
+
95
+ # ─────────────────────────────────────────────────────────────
96
+ # 3. SUBJECT (person being recorded)
97
+ # ─────────────────────────────────────────────────────────────
98
+ subject:
99
+ height: 1.9 # total height feet-to-head (metres)
100
+ foot_z: 0.0 # height of feet above floor (metres, usually 0)
101
+
102
+
103
+ # ─────────────────────────────────────────────────────────────
104
+ # 4. CAMERA SETS
105
+ # ─────────────────────────────────────────────────────────────
106
+ # Define as many camera sets as needed.
107
+ # Each set is an independent group of cameras with its own FOV,
108
+ # mounting type, placement constraints and score weight.
109
+ #
110
+ # mounting:
111
+ # wall → cameras placed on room walls and floor-to-ceiling obstacles
112
+ # tripod → cameras placed freely inside the room on a tripod stand
113
+ # ceiling → (future) cameras hung from the ceiling
114
+ #
115
+ # optional: true → this entire set is skipped if max_count is 0
116
+
117
+ camera_sets:
118
+
119
+ - id: "cam_A"
120
+ name: "ZED2i"
121
+ mounting: "wall"
122
+ optional: false
123
+
124
+ # Field of view (degrees) — landscape and portrait orientations
125
+ fov_h_landscape: 110.0
126
+ fov_v_landscape: 70.0
127
+ fov_h_portrait: 70.0
128
+ fov_v_portrait: 110.0
129
+
130
+ # Detection range (metres)
131
+ max_range: 15.0
132
+ min_range: 0.5 # blind zone closer than this
133
+
134
+ # Camera heights to evaluate (metres).
135
+ # Each candidate position is tested at ALL heights listed here.
136
+ # A single optimised configuration can mix different heights.
137
+ height_options: [2.0, 2.2]
138
+
139
+ # Placement constraints
140
+ max_count: 10 # maximum number of cameras to place
141
+ min_spacing: 1.5 # minimum distance between two cameras of this set (m)
142
+
143
+ # Score contribution
144
+ score_weight: 1.0 # relative weight vs other camera sets
145
+
146
+ # Visualisation
147
+ color: "#1f77b4" # hex colour for graphs
148
+
149
+
150
+ - id: "cam_B"
151
+ name: "iPad"
152
+ mounting: "tripod"
153
+ optional: true # set max_count: 0 to fully disable
154
+
155
+ fov_h_landscape: 80.0
156
+ fov_v_landscape: 58.0
157
+ fov_h_portrait: 58.0
158
+ fov_v_portrait: 80.0
159
+
160
+ max_range: 10.0
161
+ min_range: 0.5
162
+
163
+ height_options: [1.5]
164
+
165
+ max_count: 2
166
+ min_spacing: 1.5
167
+
168
+ # Tripod-specific: minimum distance from the capture axis (metres).
169
+ # Prevents the tripod from blocking the walking path.
170
+ walk_axis_margin: 0.7
171
+
172
+ score_weight: 0.6
173
+
174
+ color: "#d62728"
175
+
176
+
177
+ # ─────────────────────────────────────────────────────────────
178
+ # 5. CAPTURE ZONES
179
+ # ─────────────────────────────────────────────────────────────
180
+ # Zones define WHERE the cameras must provide coverage and HOW MUCH
181
+ # each zone matters (priority weight in the global score).
182
+ #
183
+ # Zone types:
184
+ # corridor → rectangular strip, explored at several X/Y positions
185
+ # sub_zone → rectangular strip contained within a parent corridor
186
+ # point → circular zone (e.g. a chair test)
187
+ #
188
+ # The optimiser tests all combinations of (x_start, y_position, sub_offset)
189
+ # and keeps the globally best placement.
190
+
191
+ capture_zones:
192
+
193
+ # ── Primary corridor (total walking path) ─────────────────
194
+ - id: "full_corridor"
195
+ type: "corridor"
196
+ priority: 0.5 # lower weight: coverage outside analysis is a bonus
197
+
198
+ length: 10.0 # length of the corridor (metres, along main axis)
199
+ width: 1.0 # transverse width of the evaluation strip (metres)
200
+
201
+ # Positions to explore during optimisation
202
+ placement:
203
+ # X start positions of the corridor to test (metres from room origin)
204
+ x_start_options: [1.0, 2.0]
205
+ # Transverse (Y) axis positions of the corridor centre to test
206
+ y_options: [1.0, 1.4, 1.8]
207
+
208
+ # ── Analysis zone (priority sub-zone inside the corridor) ──
209
+ - id: "analysis_zone"
210
+ type: "sub_zone"
211
+ priority: 1.0 # main zone: highest weight on coverage score
212
+
213
+ length: 6.0 # length of the priority sub-zone (metres)
214
+ contained_in: "full_corridor"
215
+
216
+ # Offsets (metres) from the corridor start where the sub-zone can begin.
217
+ # e.g. offset 1.0 → sub-zone starts 1m after corridor start (1m run-up)
218
+ # offset 3.0 → sub-zone starts 3m in (3m run-up, 1m deceleration)
219
+ offset_options: [1.0, 2.0, 3.0]
220
+
221
+ # ── STS point (sit-to-stand chair test — secondary priority zone) ──
222
+ - id: "sts_point"
223
+ type: "point"
224
+ priority: 2.0 # highest weight: this spot must be very well covered
225
+
226
+ radius: 0.6 # coverage circle radius (metres)
227
+ contained_in: "analysis_zone"
228
+
229
+ # If true, the optimiser searches automatically for the best position
230
+ # of this point inside the parent zone (maximises camera coverage score).
231
+ auto_optimize: true
232
+
233
+
234
+ # ─────────────────────────────────────────────────────────────
235
+ # 6. OPTIMISATION PARAMETERS
236
+ # ─────────────────────────────────────────────────────────────
237
+ optimization:
238
+
239
+ # Number of cameras required per evaluation point to consider it "well covered"
240
+ target_coverage: 6
241
+
242
+ # Bilateral constraint weight [0.0 – 1.0]
243
+ # 0.0 = disabled (pure coverage maximisation)
244
+ # 1.0 = fully enforced (score 0 if only one side covered)
245
+ # Recommended: 0.6 – 0.8 for biomechanics labs
246
+ bilateral_weight: 0.9
247
+
248
+ # Fraction of the subject body (feet→head) that must be visible
249
+ # for a camera to "count" at an evaluation point.
250
+ vertical_coverage_threshold: 0.9
251
+
252
+ # Greedy optimisation restarts per combination of zone positions
253
+ restarts_per_combo: 10
254
+
255
+ # Candidate generation resolution
256
+ wall_step: 0.35 # spacing between camera positions along walls (m)
257
+ angle_steps: 24 # number of yaw angles tested per wall position
258
+ tripod_grid_step: 0.70 # spacing of the interior tripod grid (m)
259
+ distance_quality_factor: 0.001
260
+
261
+ # Distance quality decay: score multiplier = 1 / (1 + k * d²)
262
+ # Higher k → strong penalty for distant cameras
263
+ # k=0.01: 0.96 @ 2m | 0.80 @ 5m | 0.50 @ 10m
264
+ # Graph generation mode:
265
+ # all → save a graph for every optimisation attempt (verbose)
266
+ # records_only → save only when a new global record is set
267
+ # best_per_combo → save only the best result of each zone combination
268
+ algo: "greedy_1opt" # greedy | greedy_1opt
269
+ graph_mode: "all"
270
+
@@ -0,0 +1,180 @@
1
+ # =============================================================
2
+ # Lab Camera Optimizer — Minimal example configuration
3
+ # Simple rectangular room, one wall-mounted camera set, no obstacles.
4
+ # A good starting point for any new lab.
5
+ # =============================================================
6
+ # Units : metres for distances, degrees for angles.
7
+ # =============================================================
8
+
9
+ # ─────────────────────────────────────────────────────────────
10
+ # 1. ROOM
11
+ # ─────────────────────────────────────────────────────────────
12
+ room:
13
+ # Simple 10m × 6m rectangle
14
+ corners:
15
+ - [0, 0]
16
+ - [10, 0]
17
+ - [10, 6]
18
+ - [0, 6]
19
+ height: 3.0 # floor-to-ceiling (metres)
20
+
21
+
22
+ # ─────────────────────────────────────────────────────────────
23
+ # 2. OBSTACLES
24
+ # ─────────────────────────────────────────────────────────────
25
+ # No obstacles in this minimal example.
26
+ # To add one, uncomment and edit the block below:
27
+ #
28
+ # obstacles:
29
+ # - type: rect
30
+ # bounds: [2.0, 0.0, 2.2, 0.4] # [x_min, y_min, x_max, y_max]
31
+ # height: 3.0
32
+ # label: "Pillar"
33
+ # can_mount_camera: false
34
+ obstacles: []
35
+
36
+
37
+ # ─────────────────────────────────────────────────────────────
38
+ # 3. SUBJECT
39
+ # ─────────────────────────────────────────────────────────────
40
+ subject:
41
+ height: 1.9 # subject height feet-to-head (metres)
42
+ foot_z: 0.0 # height of feet above floor (almost always 0)
43
+
44
+
45
+ # ─────────────────────────────────────────────────────────────
46
+ # 4. CAMERA SETS
47
+ # ─────────────────────────────────────────────────────────────
48
+ camera_sets:
49
+
50
+ - id: "cam_A"
51
+ name: "Wall camera"
52
+ mounting: "wall"
53
+ optional: false
54
+
55
+ # Field of view — landscape and portrait orientations (degrees)
56
+ fov_h_landscape: 110.0
57
+ fov_v_landscape: 70.0
58
+ fov_h_portrait: 70.0
59
+ fov_v_portrait: 110.0
60
+
61
+ # Detection range
62
+ max_range: 12.0 # metres
63
+ min_range: 0.5 # blind zone closer than this
64
+
65
+ # Heights to test (metres). Mix different heights in one run.
66
+ height_options: [2.0]
67
+
68
+ max_count: 8 # maximum cameras to place
69
+ min_spacing: 1.5 # minimum distance between two cameras of this set
70
+
71
+ score_weight: 1.0
72
+ color: "#1f77b4"
73
+
74
+ # Optional tripod camera set — set max_count: 0 to disable entirely
75
+ - id: "cam_B"
76
+ name: "Tripod camera"
77
+ mounting: "tripod"
78
+ optional: true
79
+
80
+ fov_h_landscape: 80.0
81
+ fov_v_landscape: 58.0
82
+ fov_h_portrait: 58.0
83
+ fov_v_portrait: 80.0
84
+
85
+ max_range: 8.0
86
+ min_range: 0.5
87
+
88
+ height_options: [1.5]
89
+
90
+ max_count: 0 # set to 0 = disabled, set to 2 = enable 2 tripod cameras
91
+ min_spacing: 1.5
92
+ walk_axis_margin: 0.7 # tripod must stay at least this far from the capture axis
93
+
94
+ score_weight: 0.6
95
+ color: "#d62728"
96
+
97
+
98
+ # ─────────────────────────────────────────────────────────────
99
+ # 5. CAPTURE ZONES
100
+ # ─────────────────────────────────────────────────────────────
101
+ # Zone types:
102
+ #
103
+ # corridor → rectangular strip swept at multiple X/Y positions
104
+ # sub_zone → rectangle contained inside a corridor
105
+ # point → circle (e.g. chair test)
106
+ # polygon → arbitrary shape (L, T, U, cross…) — fixed position in the room
107
+ # vertices: list of (X,Y) corners in order
108
+ # grid_step: evaluation point density inside (metres)
109
+ #
110
+ capture_zones:
111
+
112
+ # Full walking corridor (low priority — bonus coverage)
113
+ - id: "full_corridor"
114
+ type: "corridor"
115
+ priority: 0.5
116
+ length: 8.0 # total corridor length (metres)
117
+ width: 1.0 # transverse evaluation strip width
118
+ placement:
119
+ x_start_options: [1.0] # where the corridor can start along X
120
+ y_options: [3.0] # Y axis position (centre of room = 3.0m)
121
+
122
+ # Priority analysis zone inside the corridor
123
+ - id: "analysis_zone"
124
+ type: "sub_zone"
125
+ priority: 1.0
126
+ length: 4.0 # length of the priority sub-zone
127
+ contained_in: "full_corridor"
128
+ offset_options: [2.0] # starts 2m after corridor start → 2m run-up
129
+
130
+ # Key point (e.g. chair test, STS) — highest priority
131
+ - id: "key_point"
132
+ type: "point"
133
+ priority: 2.0
134
+ radius: 0.5
135
+ contained_in: "analysis_zone"
136
+ auto_optimize: true # optimiser finds the best position automatically
137
+
138
+ # ── Example polygon zone (commented out — uncomment to use) ──────────
139
+ # Vertices are in RELATIVE coordinates (origin = 0,0 = bottom-left corner
140
+ # of the shape). The placement.x_offsets and y_offsets define how to
141
+ # translate the whole shape across the room — like a corridor sweep but
142
+ # for any arbitrary shape (L, T, U, cross…).
143
+ #
144
+ # - id: "L_shaped_zone"
145
+ # type: "polygon"
146
+ # priority: 1.5
147
+ # grid_step: 0.20 # evaluation point spacing inside the polygon (metres)
148
+ # vertices: # relative (X, Y) corners — origin is bottom-left of shape
149
+ # - [0.0, 0.0] # bottom-left
150
+ # - [6.0, 0.0] # bottom-right of horizontal arm
151
+ # - [6.0, 1.5] # top-right of horizontal arm
152
+ # - [2.0, 1.5] # inner corner of the L
153
+ # - [2.0, 4.0] # top of vertical arm
154
+ # - [0.0, 4.0] # top-left
155
+ # placement:
156
+ # x_offsets: [0.5, 1.0, 1.5] # translate shape by these X values
157
+ # y_offsets: [1.0, 1.5] # translate shape by these Y values
158
+ # # → 3 × 2 = 6 polygon placements tested in addition to corridor sweeps
159
+
160
+
161
+ # ─────────────────────────────────────────────────────────────
162
+ # 6. OPTIMISATION PARAMETERS
163
+ # ─────────────────────────────────────────────────────────────
164
+ optimization:
165
+ target_coverage: 3 # target number of cameras per evaluation point
166
+ bilateral_weight: 0.9 # 0.0 = disabled, 1.0 = fully enforced
167
+ vertical_coverage_threshold: 0.9
168
+ restarts_per_combo: 10 # lower = faster (increase for better results)
169
+ wall_step: 0.40 # camera spacing along walls (m) — increase for speed
170
+ angle_steps: 20 # yaw angles tested per position
171
+ tripod_grid_step: 0.80
172
+ distance_quality_factor: 0.01
173
+
174
+ # Graph generation mode:
175
+ # all → save a graph for every optimisation attempt (verbose)
176
+ # records_only → save only when a new global record is set
177
+ # best_per_combo → save only the best result of each zone combination
178
+ algo: "greedy_1opt" # greedy | greedy_1opt
179
+ graph_mode: "all"
180
+
@@ -0,0 +1,268 @@
1
+ # =============================================================
2
+ # Lab Camera Optimizer — Configuration file
3
+ # Project : CHU Biomechanics Lab — L-shaped room
4
+ # =============================================================
5
+ # Units : metres for all distances, degrees for all angles.
6
+ # Angles follow the standard mathematical convention:
7
+ # 0° = East (+X), 90° = North (+Y), 180°/-180° = West (-X)
8
+ # =============================================================
9
+
10
+ # ─────────────────────────────────────────────────────────────
11
+ # 1. ROOM GEOMETRY
12
+ # ─────────────────────────────────────────────────────────────
13
+ room:
14
+ # List of (X, Y) vertices of the room polygon, in order (CW or CCW).
15
+ # The room is assumed to be a flat floor plan.
16
+ corners:
17
+ - [0, 0 ]
18
+ - [13, 0 ]
19
+ - [13, 3.0]
20
+ - [8, 3.0]
21
+ - [8, 4.7]
22
+ - [0, 4.7]
23
+
24
+ # Floor-to-ceiling height (metres)
25
+ height: 3.0
26
+
27
+
28
+ # ─────────────────────────────────────────────────────────────
29
+ # 2. OBSTACLES
30
+ # ─────────────────────────────────────────────────────────────
31
+ # Two types supported:
32
+ #
33
+ # type: rect → axis-aligned rectangle
34
+ # bounds: [x_min, y_min, x_max, y_max]
35
+ #
36
+ # type: polygon → arbitrary polygon
37
+ # vertices: [[x0,y0],[x1,y1],...] (in order)
38
+ #
39
+ # height: floor-to-ceiling height of the obstacle (metres).
40
+ # = room.height → fully blocks line-of-sight (wall/pillar)
41
+ # < room.height → partial height (object, furniture)
42
+ # does NOT block line-of-sight above it
43
+ #
44
+ # can_mount_camera: true → wall cameras can be placed on this surface
45
+ # false (default) → no camera on this obstacle
46
+
47
+ obstacles:
48
+ - type: rect
49
+ bounds: [1.70, 0.00, 2.04, 0.34]
50
+ height: 3.0
51
+ label: "Stub 1"
52
+ can_mount_camera: true
53
+
54
+ - type: rect
55
+ bounds: [5.39, 0.00, 5.59, 0.42]
56
+ height: 3.0
57
+ label: "Stub 2"
58
+ can_mount_camera: true
59
+
60
+ - type: rect
61
+ bounds: [8.94, 0.00, 9.14, 0.42]
62
+ height: 3.0
63
+ label: "Stub 3"
64
+ can_mount_camera: true
65
+
66
+ - type: rect
67
+ bounds: [5.65, 3.85, 5.85, 4.25]
68
+ height: 3.0
69
+ label: "Stub 4"
70
+ can_mount_camera: true
71
+
72
+ - type: rect
73
+ bounds: [5.65, 2.85, 6.35, 3.55]
74
+ height: 1.4
75
+ label: "Object 1"
76
+ can_mount_camera: false # partial-height object, no camera mount
77
+
78
+ - type: polygon
79
+ vertices:
80
+ - [1.47, 4.70]
81
+ - [1.47, 4.38]
82
+ - [1.71, 4.38]
83
+ - [1.71, 3.88]
84
+ - [2.04, 3.88]
85
+ - [2.04, 4.27]
86
+ - [3.02, 4.27]
87
+ - [3.02, 4.70]
88
+ height: 3.0
89
+ label: "Wall N"
90
+ can_mount_camera: true
91
+
92
+
93
+ # ─────────────────────────────────────────────────────────────
94
+ # 3. SUBJECT (person being recorded)
95
+ # ─────────────────────────────────────────────────────────────
96
+ subject:
97
+ height: 1.9 # total height feet-to-head (metres)
98
+ foot_z: 0.0 # height of feet above floor (metres, usually 0)
99
+
100
+
101
+ # ─────────────────────────────────────────────────────────────
102
+ # 4. CAMERA SETS
103
+ # ─────────────────────────────────────────────────────────────
104
+ # Define as many camera sets as needed.
105
+ # Each set is an independent group of cameras with its own FOV,
106
+ # mounting type, placement constraints and score weight.
107
+ #
108
+ # mounting:
109
+ # wall → cameras placed on room walls and floor-to-ceiling obstacles
110
+ # tripod → cameras placed freely inside the room on a tripod stand
111
+ # ceiling → (future) cameras hung from the ceiling
112
+ #
113
+ # optional: true → this entire set is skipped if max_count is 0
114
+
115
+ camera_sets:
116
+
117
+ - id: "cam_A"
118
+ name: "ZED2i"
119
+ mounting: "wall"
120
+ optional: false
121
+
122
+ # Field of view (degrees) — landscape and portrait orientations
123
+ fov_h_landscape: 110.0
124
+ fov_v_landscape: 70.0
125
+ fov_h_portrait: 70.0
126
+ fov_v_portrait: 110.0
127
+
128
+ # Detection range (metres)
129
+ max_range: 15.0
130
+ min_range: 0.5 # blind zone closer than this
131
+
132
+ # Camera heights to evaluate (metres).
133
+ # Each candidate position is tested at ALL heights listed here.
134
+ # A single optimised configuration can mix different heights.
135
+ height_options: [2.0, 2.2]
136
+
137
+ # Placement constraints
138
+ max_count: 10 # maximum number of cameras to place
139
+ min_spacing: 1.5 # minimum distance between two cameras of this set (m)
140
+
141
+ # Score contribution
142
+ score_weight: 1.0 # relative weight vs other camera sets
143
+
144
+ # Visualisation
145
+ color: "#1f77b4" # hex colour for graphs
146
+
147
+
148
+ - id: "cam_B"
149
+ name: "iPad"
150
+ mounting: "tripod"
151
+ optional: true # set max_count: 0 to fully disable
152
+
153
+ fov_h_landscape: 80.0
154
+ fov_v_landscape: 58.0
155
+ fov_h_portrait: 58.0
156
+ fov_v_portrait: 80.0
157
+
158
+ max_range: 10.0
159
+ min_range: 0.5
160
+
161
+ height_options: [1.5]
162
+
163
+ max_count: 2
164
+ min_spacing: 1.5
165
+
166
+ # Tripod-specific: minimum distance from the capture axis (metres).
167
+ # Prevents the tripod from blocking the walking path.
168
+ walk_axis_margin: 0.7
169
+
170
+ score_weight: 0.6
171
+
172
+ color: "#d62728"
173
+
174
+
175
+ # ─────────────────────────────────────────────────────────────
176
+ # 5. CAPTURE ZONES
177
+ # ─────────────────────────────────────────────────────────────
178
+ # Zones define WHERE the cameras must provide coverage and HOW MUCH
179
+ # each zone matters (priority weight in the global score).
180
+ #
181
+ # Zone types:
182
+ # corridor → rectangular strip, explored at several X/Y positions
183
+ # sub_zone → rectangular strip contained within a parent corridor
184
+ # point → circular zone (e.g. a chair test)
185
+ #
186
+ # The optimiser tests all combinations of (x_start, y_position, sub_offset)
187
+ # and keeps the globally best placement.
188
+
189
+ capture_zones:
190
+
191
+ # ── Primary corridor (total walking path) ─────────────────
192
+ - id: "full_corridor"
193
+ type: "corridor"
194
+ priority: 0.5 # lower weight: coverage outside analysis is a bonus
195
+
196
+ length: 10.0 # length of the corridor (metres, along main axis)
197
+ width: 1.0 # transverse width of the evaluation strip (metres)
198
+
199
+ # Positions to explore during optimisation
200
+ placement:
201
+ # X start positions of the corridor to test (metres from room origin)
202
+ x_start_options: [1.0, 2.0]
203
+ # Transverse (Y) axis positions of the corridor centre to test
204
+ y_options: [1.0, 1.4, 1.8]
205
+
206
+ # ── Analysis zone (priority sub-zone inside the corridor) ──
207
+ - id: "analysis_zone"
208
+ type: "sub_zone"
209
+ priority: 1.0 # main zone: highest weight on coverage score
210
+
211
+ length: 6.0 # length of the priority sub-zone (metres)
212
+ contained_in: "full_corridor"
213
+
214
+ # Offsets (metres) from the corridor start where the sub-zone can begin.
215
+ # e.g. offset 1.0 → sub-zone starts 1m after corridor start (1m run-up)
216
+ # offset 3.0 → sub-zone starts 3m in (3m run-up, 1m deceleration)
217
+ offset_options: [1.0, 2.0, 3.0]
218
+
219
+ # ── STS point (sit-to-stand chair test — secondary priority zone) ──
220
+ - id: "sts_point"
221
+ type: "point"
222
+ priority: 2.0 # highest weight: this spot must be very well covered
223
+
224
+ radius: 0.6 # coverage circle radius (metres)
225
+ contained_in: "analysis_zone"
226
+
227
+ # If true, the optimiser searches automatically for the best position
228
+ # of this point inside the parent zone (maximises camera coverage score).
229
+ auto_optimize: true
230
+
231
+
232
+ # ─────────────────────────────────────────────────────────────
233
+ # 6. OPTIMISATION PARAMETERS
234
+ # ─────────────────────────────────────────────────────────────
235
+ optimization:
236
+
237
+ # Number of cameras required per evaluation point to consider it "well covered"
238
+ target_coverage: 6
239
+
240
+ # Bilateral constraint weight [0.0 – 1.0]
241
+ # 0.0 = disabled (pure coverage maximisation)
242
+ # 1.0 = fully enforced (score 0 if only one side covered)
243
+ # Recommended: 0.6 – 0.8 for biomechanics labs
244
+ bilateral_weight: 0.9
245
+
246
+ # Fraction of the subject body (feet→head) that must be visible
247
+ # for a camera to "count" at an evaluation point.
248
+ vertical_coverage_threshold: 0.9
249
+
250
+ # Greedy optimisation restarts per combination of zone positions
251
+ restarts_per_combo: 10
252
+
253
+ # Candidate generation resolution
254
+ wall_step: 0.35 # spacing between camera positions along walls (m)
255
+ angle_steps: 24 # number of yaw angles tested per wall position
256
+ tripod_grid_step: 0.70 # spacing of the interior tripod grid (m)
257
+ distance_quality_factor: 0.001
258
+
259
+ # Distance quality decay: score multiplier = 1 / (1 + k * d²)
260
+ # Higher k → strong penalty for distant cameras
261
+ # k=0.01: 0.96 @ 2m | 0.80 @ 5m | 0.50 @ 10m
262
+ # Graph generation mode:
263
+ # all → save a graph for every optimisation attempt (verbose)
264
+ # records_only → save only when a new global record is set
265
+ # best_per_combo → save only the best result of each zone combination
266
+ algo: "greedy_1opt" # greedy | greedy_1opt
267
+ graph_mode: "all"
268
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lab-camera-optimizer
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Automated camera placement optimiser for markerless biomechanics motion capture labs
5
5
  Author: Florian Delaplace
6
6
  License-Expression: MIT
@@ -16,9 +16,14 @@ core/greedy.py
16
16
  core/room.py
17
17
  core/scoring.py
18
18
  core/visualize.py
19
+ lab_camera_optimizer/__init__.py
19
20
  lab_camera_optimizer.egg-info/PKG-INFO
20
21
  lab_camera_optimizer.egg-info/SOURCES.txt
21
22
  lab_camera_optimizer.egg-info/dependency_links.txt
22
23
  lab_camera_optimizer.egg-info/entry_points.txt
23
24
  lab_camera_optimizer.egg-info/requires.txt
24
- lab_camera_optimizer.egg-info/top_level.txt
25
+ lab_camera_optimizer.egg-info/top_level.txt
26
+ lab_camera_optimizer/configs/T_zone_direction_change.yaml
27
+ lab_camera_optimizer/configs/example_real_world.yaml
28
+ lab_camera_optimizer/configs/example_simple.yaml
29
+ lab_camera_optimizer/configs/labo_CHU.yaml
@@ -1,4 +1,5 @@
1
1
  core
2
2
  init_project
3
+ lab_camera_optimizer
3
4
  optimize
4
5
  preview_room
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lab-camera-optimizer"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "Automated camera placement optimiser for markerless biomechanics motion capture labs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -46,10 +46,10 @@ lab-camera-init = "init_project:main"
46
46
 
47
47
  [tool.setuptools.packages.find]
48
48
  where = ["."]
49
- include = ["core*"]
49
+ include = ["core*", "lab_camera_optimizer*"]
50
50
 
51
51
  [tool.setuptools.package-data]
52
- "*" = ["configs/*.yaml"]
52
+ "lab_camera_optimizer" = ["configs/*.yaml"]
53
53
 
54
54
  # Include top-level scripts and configs in the source distribution
55
55
  [tool.setuptools]