Room and occupancy¶
The empty room, how much of the frame anybody fills, and which pixels change whatever is in front of them.

Left: the room recovered as a per-pixel median over sampled frames. Median and not mean, because a mean keeps a faint ghost of everyone who crossed, and subtracting a ghost leaves holes shaped like people.
Right: how restless each pixel is. The bright region here is not the video-call screen on the left of the room but a table on the right, with a laptop and a seated researcher—in shot for the whole recording, and counted by quantity of motion like anybody else. On this corpus that non-dancer motion was 2.8 to 7.1 per cent of the total.
Occupancy answers what motion cannot: somebody standing still has no motion and plenty of occupancy.
The frames the plate is built from must be spread over the recording¶
The median is taken twice: once over a blind sample, then again over the emptiest of those frames, which tightens the plate where passing traffic left residue.
The catch is that the emptiest frames are not spread through a recording. They cluster in whatever stretch nobody was working—a break, a setup, a pack-down—and anything standing in the room during that stretch enters the plate as though it were furniture.
On one recording here a stepladder stood in the middle of the floor for about ten minutes of a two-hour session. Those frames were the emptiest by a wide margin, all of the refinement's frames came from them, and the ladder became part of "the room". Every occupancy figure afterwards then read that region as occupied 18.6 per cent of the time --- as though a body were standing there—because the plate expected a ladder that was usually absent.
So the second pass takes the emptiest frame from each of k equal stretches rather than the
emptiest k overall. The choice is still made on emptiness; it simply cannot all come from
one place. stratify=False restores the older behaviour, and plate_spread reports how
much of the recording the chosen frames span—room_plate warns below half.
plate, used = mg.room_plate("session.mp4")
mg.plate_spread(used, n_frames) # near 1 is spread, near 0 is one stretch
A refinement that changes the room is concentrating, and backs off¶
What the second pass cannot do is remove somebody who stood in one place for most of the recording: no selection of frames recovers a room that no frame shows. Worse, on material where the performer rarely leaves—standstill recordings—the frames most like the first plate are exactly the ones with the performer in place, and re-taking the median over them makes the performer solid where the full sample had washed them out. On a 2012 standstill performance recording the refined plate acquired a performer standing solidly in an otherwise empty room.
The failure is detectable at the output. Under a median first pass the kept frames are the
ones that agree with the plate, so a refinement that changes the room materially is
concentrating, not cleaning. room_plate measures that change and returns the unrefined
plate with a warning when it exceeds max_refine_change (default 0.02 of the frame—above
a body's residue and below a body). On material where the performer never leaves, pass
refine=False and skip the second pass altogether.
The empty room, and where the people are relative to it.
A long recording of a room contains the room. Recovering it gives two things: a background to subtract, and an occupancy signal saying how much of the frame anybody filled --- a different question from quantity of motion, and one motion cannot answer. A dancer standing still has no motion and plenty of occupancy.
Median over frames, never a mean. A mean keeps a faint ghost of the dancers everywhere they went, and subtracting a ghost leaves holes shaped like people. The median discards whatever is present in fewer than half the samples, which is exactly what somebody crossing a room is, and keeps whatever is usually there, which is exactly what a chair is.
Then refine once, and check the refinement did not make things worse. The second pass
re-takes the median over the emptiest tenth of the samples --- the frames most like the
first plate --- which tightens the plate where passing traffic left residue. What it cannot
do is remove somebody who stood in one place for most of the recording: no selection of
frames recovers a room that no frame shows. Worse, on material where the performer rarely
leaves --- standstill recordings --- the look-alike frames are exactly the ones with the
performer in place, and re-taking the median over them makes the performer solid where the
full sample had washed them out. That failure is detectable at the output: under a median
first pass the kept frames are the ones that AGREE with the plate, so a refinement that
changes the room materially is concentrating, not cleaning. room_plate measures that
change and hands back the unrefined plate with a warning when it is material.
Occupancy tolerates downsampling that segmentation does not. Nothing here needs full resolution, and a few hundred pixels wide is plenty.
sample_frame_indices ¶
sample_frame_indices(n_frames, n_samples)
Evenly spread sample positions across a recording.
Evenly rather than randomly: a random sample of a session where the dancers work in one half leaves the other half under-represented by luck, and the plate is then a picture of the busy half.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_frames
|
int
|
How many frames the recording has. |
required |
n_samples
|
int
|
How many to sample. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: Ascending indices. Every frame once when |
ndarray
|
|
Source code in musicalgestures/_plate.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
plate_from_stack ¶
plate_from_stack(stack)
The room, as the per-pixel median over a stack of frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stack
|
Frames, shape (n, h, w) or (n, h, w, c). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: One frame, the median. |
Source code in musicalgestures/_plate.py
62 63 64 65 66 67 68 69 70 71 72 73 | |
occupancy_from_plate ¶
occupancy_from_plate(frame, plate, threshold=12.0)
What fraction of the frame differs from the room by more than threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
One frame. |
required | |
plate
|
The room, same shape. |
required | |
threshold
|
float
|
Difference counting as "something is there", in the units of the image. Defaults to 12.0 on an 8-bit scale. Without a threshold every frame is fully occupied, because sensor noise puts a small difference everywhere. |
12.0
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
A fraction in [0, 1]. |
Source code in musicalgestures/_plate.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
refine_indices ¶
refine_indices(diffs, keep_fraction=0.1, stratify=True)
Which sampled frames to rebuild the plate from: the emptiest, spread over time.
The smallest differences are the emptiest frames, the ones with least in front of
the room --- a reading that holds only while the first plate is mostly empty. Where
the performer stood in place through most of the recording, the frames most like the
plate are the ones with the performer in them, and this selection inverts; that is
caught downstream, where room_plate checks what the refinement did to the plate
rather than trusting the selection.
But the emptiest frames cluster. They fall in whatever stretch nobody was working --- a break, a setup, a pack-down --- and anything standing in the room then goes into the plate as though it were furniture. On one recording a stepladder stood still for ten minutes of a two-hour session, those frames were the emptiest by a wide margin, and the ladder became part of "the room" for every occupancy figure afterwards, reading as a body 18.6 per cent of the time where it was not.
So the default takes the emptiest frame from each of k equal stretches of the
recording rather than the emptiest k overall. The choice is still made on
emptiness; it simply cannot all come from one place. stratify=False restores the
older behaviour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
diffs
|
One number per sampled frame, how much it differs from the first plate. |
required | |
keep_fraction
|
float
|
Fraction to keep. Defaults to 0.10. |
0.1
|
stratify
|
bool
|
Spread the choice over the recording. Defaults to True. |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: Indices into |
ndarray
|
over one frame is that frame and not a plate. |
Source code in musicalgestures/_plate.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
plate_spread ¶
plate_spread(indices, n_frames)
How much of the recording the plate's frames were drawn from, 0 to 1.
A plate built from one stretch describes the room during that stretch. This is what
room_plate checks itself against before handing one back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
The frame indices the plate was built from. |
required | |
n_frames
|
int
|
Frames in the recording. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The span the chosen frames cover, as a fraction of the recording. 0 when |
float
|
fewer than two frames were used. |
Source code in musicalgestures/_plate.py
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
room_plate ¶
room_plate(video, n_samples=400, width=320, keep_fraction=0.1, refine=True, stratify=True, min_spread=0.5, max_refine_change=0.02)
The empty room, from a two-pass median over sampled frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Path to the video. |
required | |
n_samples
|
int
|
Frames to sample. Defaults to 400. |
400
|
width
|
int
|
Working width in pixels. Defaults to 320; occupancy tolerates downsampling that segmentation does not. |
320
|
keep_fraction
|
float
|
Fraction of samples the second pass keeps. |
0.1
|
refine
|
bool
|
Take the second pass. Defaults to True. |
True
|
stratify
|
bool
|
Spread the second pass over the recording rather than taking the globally emptiest frames, which cluster in breaks and setups and carry whatever was standing in the room then into the plate. Defaults to True. |
True
|
min_spread
|
float
|
Warn when the frames used span less than this fraction of the recording, since such a plate describes one stretch rather than the room. Defaults to 0.5. |
0.5
|
max_refine_change
|
float
|
Fall back to the unrefined plate, with a warning, when refinement changed more than this fraction of it. The kept frames are the ones that agree with the first plate, so a large change means they agree on something the full sample rejected --- on standstill material, the performer, made solid. Defaults to 0.02, above a body's residue and below a body. |
0.02
|
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
(plate, indices_used). Check |
|
|
the plate matters: a low value means the room it describes is one moment's. |
Source code in musicalgestures/_plate.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
occupancy_track ¶
occupancy_track(video, plate, every_n=25, width=320, threshold=12.0)
How much of the frame is occupied, sampled through the recording.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Path to the video. |
required | |
plate
|
The room, from |
required | |
every_n
|
int
|
Sample one frame in this many. Defaults to 25. |
25
|
width
|
int
|
Working width, which must match the plate's. |
320
|
threshold
|
float
|
Difference counting as occupied. |
12.0
|
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
(frame_indices, occupancy) — a fraction per sampled frame. |
Source code in musicalgestures/_plate.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
restless_map ¶
restless_map(stack)
How much each pixel changes across the sampled frames, robustly.
Median absolute deviation, not range. A screen showing a video call changes in nearly every frame; a dancer occupies a given pixel occasionally. Both have a large range, so a range-based measure marks them alike --- and masking the dancer along with the screen is worse than masking nothing. The median deviation separates them: it is large only where change is the pixel's normal state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stack
|
Frames, shape (n, h, w). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: One value per pixel, in the image's own units. |
Source code in musicalgestures/_plate.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
restless_regions ¶
restless_regions(stack, quantile=0.98, min_value=2.0)
A mask of the pixels that change constantly, whatever is or is not in front of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stack
|
Frames, shape (n, h, w). |
required | |
quantile
|
float
|
How far up the deviation distribution the cut sits. Defaults to 0.98, roughly the brightest two per cent of the frame. |
0.98
|
min_value
|
float
|
An absolute floor, in the image's units. Without it a quantile of a flat map marks the top slice of nothing, so a perfectly still recording would come back with two per cent of itself masked. |
2.0
|
Returns:
| Name | Type | Description |
|---|---|---|
ndarray
|
np.ndarray: A boolean mask the shape of one frame. It cannot cover the whole |
|
frame |
ndarray
|
the cut is at least the map's own minimum, so the quietest pixel is always |
ndarray
|
outside it. That is a property of the arithmetic rather than a guard --- an |
|
ndarray
|
explicit check for it was written, found unreachable by mutation, and removed, |
|
ndarray
|
because defensive code no test can reach hides the fault it pretends to catch. |
Source code in musicalgestures/_plate.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | |
texture_mask ¶
texture_mask(image, grid=16, percentile=40.0)
Which cells of a picture carry enough texture to trust motion vectors on.
An encoder's motion search is unconstrained where nothing textures the block: every candidate vector predicts a flat region equally well, so the vectors there are rate decisions that propagate from wherever real motion is, not measurements. Measured on a corpus whose one room hangs a dark curtain near the lens, the plate's local texture predicted the accumulated vector motion at Spearman -0.6, and masking the low-texture cells removed a negative motion-to-dwell correlation that had made the maps unreadable, while leaving well-textured rooms untouched.
The threshold is a percentile of this picture's own cell textures, never an absolute number, so the mask adapts to any room and any exposure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
A greyscale picture, typically the room plate the maps are built against, as (rows, cols). |
required | |
grid
|
int
|
Cell size in pixels; 16 matches the macroblock lattice the vector maps accumulate on. Defaults to 16. |
16
|
percentile
|
float
|
Cells whose standard deviation falls below this percentile of all cells' are masked. Defaults to 40. |
40.0
|
Returns:
| Type | Description |
|---|---|
|
np.ndarray: Boolean (rows, cols) cell grid, True where vectors are evidence. |
Source code in musicalgestures/_plate.py
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |