Multi-shot¶
Many moments of one recording in a single picture: a chronophotograph.
The room is recovered as a plate—what was there before anybody came in — bodies are cut out of frames spread through the recording, and all of them are laid back onto it. One image then carries where somebody was, how they were shaped, and how far apart the moments were. It answers nothing a motiongram answers and shows something no gram does.

The bundled dancer.avi, six bodies. This dancer stays on one spot, so the selection
spreads over the gesture instead of the floor and the picture becomes a Marey-style
chronophotograph of the arms. A dancer who travels reads as separate figures across
the room.
The build-up is available as a looping GIF with animate=True: the bare room first,
then one more body per frame in time order, with the finished composite held at the end.
The same moments, chosen the same way—the animation is the composite gaining its bodies,
not a different selection.

As a method, which is how the rest of the object API reads:
import musicalgestures as mg
video = mg.MgVideo("session.mp4")
video.multishot(n_bodies=8, start=1932, end=4478) # -> MgImage
video.multishot(n_bodies=8, animate=True) # -> looping GIF of the build-up
video.plate(width=1920) # the room on its own -> MgImage
Or as a function, when the arrays are wanted rather than a file:
picture, plate = mg.multishot("session.mp4", n_bodies=8, start=1932, end=4478)
The method raises ValueError when no frame held a body of a plausible size, rather than
handing back a picture of an empty room—which would look like the recording was empty
instead of like the size bounds matching nothing.
It absorbed stroboscope()¶
MGT had chronophotography before this, as stroboscope(): silhouettes at evenly sampled
times on a mean average frame, tinted by time. Having two functions make the same picture
two ways meant a reader had to know which—so they are one, and stroboscope() is a
deprecated wrapper that delegates here until 2.0.
Both ways survive:
what stroboscope() did |
how to ask for it now |
|---|---|
| even sampling | select='even' |
| mean-average background | background='average' |
| time tint | colorize=True |
| flat or first-frame ground | background='black' / 'white' / 'first' |
video.multishot(select='even', background='average', colorize=True) # the old picture
video.multishot() # the new default
The defaults are an opinion, and worth stating. Even sampling is what makes two moments land in the same place, and a mean average keeps a faint ghost of everyone who crossed — which is exactly what a median plate exists to remove. Even spacing still answers a real question, though, and it is the one a time tint belongs to: how a body looked at regular instants, the Muybridge reading. Spatial separation answers where it went.
Frames are chosen for separation, not at intervals¶
Evenly spaced frames put bodies on top of each other as often as not, and two overlapping silhouettes read as one smear rather than as two moments. Candidates are scored by how far each sits from every body already placed, and the greediest spacing wins.
More bodies is not simply better. A room is finite, so each body after the first takes the emptiest remaining spot and the spots run out. Past a dozen or so they overlap whatever the selection does. Eight suits a studio; a long section with a lot of travel takes more.
Give it a section, not a whole recording¶
A whole session is usually mostly setup, and the picture then fills with people standing
about rather than with the work. start and end take seconds.
It assumes a performer who moves through space¶
Separation is spatial, so a performer who stays put gives it nothing to separate. On a seated pianist it returns heads and hands stacked in one place: the static torso is part of the room by then, and only the moving parts survive the mask. The picture is reflecting what the recording contains, which is the intended behaviour—but it is an overlay of moving limbs rather than a chronophotograph, and worth knowing which of the two you are looking at.
What it cannot do¶
- It composites whoever is in shot. Somebody sitting at a laptop differs from the room like anybody else. Where that matters, mask the region or restrict the span.
- It inherits the plate's faults exactly. A prop that stood in the room through a break
appears here as a hole or a ghost.
room_platespreads its sampling and warns when it cannot, but heed the warning. - Figure-ground separation is the limit. A dark costume against a dark background barely differs from the plate, and the silhouette goes semi-transparent. Rooms differ enormously in how well this works, and it is the room and not the setting that decides.
Many moments of one recording in a single picture: a chronophotograph.
The room is recovered as a plate --- what was there before anybody came in --- bodies are cut out of frames spread through the recording, and all of them are laid back onto it. One image then carries where somebody was, how they were shaped, and how far apart the moments were. It answers nothing a motiongram answers and shows something no gram shows.
This absorbed stroboscope(), which made the same picture a different way and is now
a deprecated wrapper. Both ways are here: select='even' samples at regular intervals,
background='average' composites onto the mean of every frame, colorize=True tints each
body by time. The defaults are the opinionated half --- moments chosen for spatial
separation, on a median plate --- because even sampling is what makes two bodies land in
the same place, and a mean average keeps a faint ghost of everyone who crossed.
Frames are chosen for separation, not at regular intervals. Evenly spaced frames put bodies on top of each other as often as not, and two overlapping silhouettes read as one smear rather than as two moments. Candidates are therefore scored by how far each sits from every body already placed, and the greediest spacing wins. That is a picture-making decision rather than a measurement, and it is made explicitly here rather than by luck.
More bodies is not simply better. A room is finite, so each body after the first takes the emptiest remaining spot and the spots run out. Past a dozen or so they overlap whatever the selection does. Eight suits a studio; a long section with a lot of travel takes more.
It assumes a performer who MOVES THROUGH SPACE, and it should. Separation is spatial, so a performer who stays put gives it nothing to separate: run on a seated pianist it returns heads and hands stacked in one place, because a static torso is in the plate and only the moving parts survive the mask. That is not a fault to tune away --- the picture is reflecting what the recording contains. For travelling performers it is a chronophotograph; for a seated one it becomes an overlay of moving limbs, which is a different object and worth knowing you are looking at.
The mask is the weak point and is treated as such. A body is whatever differs from the plate by more than the tolerance --- which is also true of their shadow, of a screen showing a video call, and of anything the plate got wrong. Shadows are rejected on the RATIO of brightness rather than its difference, since a shadow keeps the colour of the floor it falls on and only darkens it; fragments too small to be a person are dropped; and the edge is feathered so a cut-out does not read as a sticker.
It inherits the plate's faults exactly. Build the plate with stratify=True, which is
the default, and heed the warning it raises when the frames it used cluster in time: a prop
that stood in the room through a break will otherwise appear here as a hole or a ghost.
And it composites whoever is in shot. Somebody sitting at a laptop differs from the room like anybody else. Where that matters, mask the region or restrict the span.
body_mask ¶
body_mask(frame, plate, tolerance=TOLERANCE, shadow_ratio=SHADOW_RATIO, min_area=MIN_AREA)
Where somebody is, with shadow and speckle taken out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
One frame, colour, as read. |
required | |
plate
|
The room, same shape, from |
required | |
tolerance
|
float
|
Difference counting as somebody, on an 8-bit scale. |
TOLERANCE
|
shadow_ratio
|
float
|
Brightness ratio below which a difference is read as shadow. |
SHADOW_RATIO
|
min_area
|
float
|
Sets the size below which a connected piece is not a person and
is dropped. Scaled from the same human-body assumption as |
MIN_AREA
|
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
|
|
|
covers, so a caller can reject a frame without paying for the rest of the work. |
Source code in musicalgestures/_multishot.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
touches_edge ¶
touches_edge(mask, margin=2)
Does this body actually reach the frame edge?
A share of the mask is the wrong test and it let real faults through. A silhouette can be 95 per cent inside the picture and still be somebody with their hand cut off: the share lying against the edge is small precisely because the rest of the body is large. What matters is whether the mask reaches the boundary at all.
margin is small on purpose --- a body near the wall is ordinary in a studio and must
not be rejected for it.
Source code in musicalgestures/_multishot.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
choose_spaced ¶
choose_spaced(candidates, n_bodies)
Which moments to composite: each as far as possible from those already placed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
Dicts carrying at least |
required | |
n_bodies
|
int
|
How many to place. Fewer are returned if there are fewer. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
list |
The chosen candidates, in time order, since a composite lays later |
|
|
bodies over earlier ones and that order is not cosmetic. |
Source code in musicalgestures/_multishot.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
choose_even ¶
choose_even(candidates, n_bodies)
Evenly spaced in TIME, which is what stroboscope() did before the merge.
Kept because the two answer different questions. Even spacing says how a body looked at regular instants, which is the Muybridge reading and the one a time tint belongs to; spatial separation says where a body went, and refuses to stack two moments in one place. Even spacing is what makes them stack.
Returns:
| Name | Type | Description |
|---|---|---|
list |
The chosen candidates, in time order. |
Source code in musicalgestures/_multishot.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
multishot ¶
multishot(video, n_bodies=8, n_candidates=120, start=None, end=None, plate=None, width=960, tolerance=TOLERANCE, feather=FEATHER, min_area=MIN_AREA, max_area=MAX_AREA, max_border=MAX_BORDER, shadow_ratio=SHADOW_RATIO, region=None, segmenter='plate', select='spaced', background='plate', colorize=False, animate=False)
A chronophotograph of one recording, or of one span of it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Path to the video. |
required | |
n_bodies
|
int
|
Moments to place. Defaults to 8. Past a dozen or so they overlap whatever the selection does, because a room is finite. |
8
|
n_candidates
|
int
|
Frames examined before choosing. Defaults to 120. |
120
|
start, end
|
float
|
Seconds. Restrict to one section --- worth doing, since a whole recording is usually mostly setup and the picture fills with people standing about rather than with the work. |
required |
plate
|
optional
|
The room, from |
None
|
width
|
int
|
Working width. Defaults to 960. |
960
|
tolerance
|
float
|
Difference from the plate counting as somebody. |
TOLERANCE
|
feather
|
int
|
Edge softening in pixels, so a cut-out does not read as a sticker. |
FEATHER
|
min_area, max_area
|
float
|
How much of the frame a body may cover, as a
fraction. The defaults --- 0.4 to 6 per cent --- describe a whole person at
studio distance, which is what this is tuned for. Move them when the
framing differs: a closer camera or a larger performer needs |
required |
max_border
|
float
|
Retained for callers that set it; the rejection now asks whether the mask REACHES the edge at all, since a body can be almost entirely inside the picture and still have a hand cut off. |
MAX_BORDER
|
shadow_ratio
|
float
|
Brightness ratio below which a difference reads as shadow. |
SHADOW_RATIO
|
region
|
tuple
|
|
None
|
segmenter
|
str
|
|
'plate'
|
select
|
str
|
|
'spaced'
|
background
|
str
|
|
'plate'
|
colorize
|
bool
|
Tint each body by time, early to late, so the order reads. Off by default because it changes the pixels: a tinted composite is a diagram rather than a photograph of the room. |
False
|
animate
|
bool
|
Return the build-up rather than only its end: the bare room, then one more body per frame, in time order. The same moments, chosen the same way --- the animation is the composite gaining its bodies, not a different selection. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
|
|
|
a list of BGR frames, the bare plate first and the full composite last. |
||
|
|
||
|
empty room is not a result and is not returned as one. |
Source code in musicalgestures/_multishot.py
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 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 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 323 324 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 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
mg_multishot ¶
mg_multishot(self, n_bodies=8, n_candidates=120, start=None, end=None, width=960, tolerance=TOLERANCE, feather=FEATHER, min_area=MIN_AREA, max_area=MAX_AREA, max_border=MAX_BORDER, shadow_ratio=SHADOW_RATIO, region=None, segmenter='plate', select='spaced', background='plate', colorize=False, animate=False, frame_ms=700, target_name=None, overwrite=True)
Many moments of this recording in one picture, as an MgImage.
The method form of multishot, so it composes with the rest of the object API. See
that function for what the arguments mean and for how this differs from
stroboscope().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
animate
|
bool
|
Write a looping GIF of the build-up instead of the still: the bare room first, one more body per frame in time order, and the finished composite held for three frames' worth at the end. |
False
|
frame_ms
|
int
|
Milliseconds per animation frame. Defaults to 700. |
700
|
target_name
|
str
|
Output name. Defaults to "_multishot.png", or
"_multishot.gif" when |
None
|
overwrite
|
bool
|
Overwrite or auto-increment. Defaults to True. |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
MgImage |
'MgImage'
|
the composite, or the GIF of it building up. |
Raises:
| Type | Description |
|---|---|
ValueError
|
when no frame held a body of a plausible size. An empty room is not a result, and returning one silently would look like the recording was empty rather than like the size bounds matched nothing. |
Source code in musicalgestures/_multishot.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | |
mg_plate ¶
mg_plate(self, width=960, n_samples=300, stratify=True, target_name=None, overwrite=True)
The empty room this recording was made in, as an MgImage.
The method form of room_plate: a per-pixel median over sampled frames, twice, with
the second pass spread over the recording so that anything standing there through a
break does not become furniture.
In colour, from the frames the grey pass chose --- not a fresh draw, which would be a different room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
width
|
int
|
Working width. Defaults to 960; pass the video's own width for a full-resolution room. |
960
|
n_samples
|
int
|
Frames to sample. Defaults to 300. |
300
|
stratify
|
bool
|
Spread the second pass over the recording. Defaults to True. |
True
|
target_name
|
str
|
Output name. Defaults to "_plate.png". |
None
|
overwrite
|
bool
|
Overwrite or auto-increment. Defaults to True. |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
MgImage |
'MgImage'
|
the room. |
Source code in musicalgestures/_multishot.py
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 | |