Actions¶
Cutting a motion envelope into spans, and keeping segmentation apart from recognition.
Actions: the layer between motion and meaning.
Three words, and this module owns the middle one.
Motion is continuous displacement in space over time. The toolbox already measures it: quantity of motion, optical flow, pose landmarks.
An action is a segment of that motion, usually with a beginning and an end. Reaching for a cup is an action; so is a drum stroke; so is a phrase of dance that is going nowhere in particular. Actions need not be goal-directed, and in music they are often sound-producing.
A gesture is an action carrying meaning, and meaning is not a property of the signal. A recogniser can say a segment looks like someone waving; whether that wave means hello, stop or nothing at all is not visible in the pixels. So this module derives actions from motion, and then lets labels be attached to actions --- to some of them, not all --- rather than pretending that recognition and meaning are one step.
That layering is the design. :class:Action is a span with provenance and a place to hang
labels. :func:segment_actions produces spans from a motion envelope. :func:action_type
describes a span in terms of how the motion is distributed within it. Recognisers, of
which there can be several and which may disagree, add named labels to spans they did not
have to produce.
The point of separating them is that the segmenter and the recogniser fail differently. A segmenter that misses a boundary loses an action entirely; a recogniser that guesses wrong leaves the action there to be relabelled. Keeping the record of what happened when apart from what it was means the second can be revised without redoing the first.
.. note::
Everything here works from a motion envelope --- a one-dimensional series saying how much motion there was at each moment --- and not from pixels directly. That is deliberate: issue #373 asks that recognition follow human activity rather than camera motion or scene change, and an envelope computed from pose landmarks carries only the body. Pass a pixel-derived envelope and the module will work, and a pan of the camera will read as an action.
Action
dataclass
¶
Action(start, end, source='unknown', labels=dict(), features=dict())
One segment of motion, with somewhere to record what it was.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
float
|
Start time in seconds. |
end |
float
|
End time in seconds. |
source |
str
|
What produced this span, so that spans from different segmenters can be told apart when they are pooled. |
labels |
dict
|
Names given to this action by recognisers, keyed by recogniser. Empty is the normal state: most actions are never named, and an action with no label is still an action. This is where a gesture would be recorded, if anything could establish one. |
features |
dict
|
Numbers describing the span, from :func: |
overlaps ¶
overlaps(other)
Whether this action shares any time with other.
Source code in musicalgestures/_actions.py
75 76 77 | |
segment_actions ¶
segment_actions(envelope, fs, threshold=0.15, min_duration=0.1, min_gap=0.1, source='envelope', range_mode='minmax', range_percentiles=(1.0, 99.0))
Cut a motion envelope into actions, where motion rises above rest.
An action begins where the envelope crosses threshold and ends where it falls back.
Short gaps are closed before short spans are dropped, in that order, because a single
action that dips momentarily below the threshold would otherwise be discarded as two
fragments rather than kept as one.
The threshold is a fraction of the envelope's range, not an absolute value, so the same setting transfers between recordings of different scale. Motion never rising above it yields no actions, which is the correct answer for a still recording rather than an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
envelope
|
Motion per frame, one dimension. Non-finite values are interpolated. |
required | |
fs
|
float
|
Sampling rate of the envelope, in frames per second. |
required |
threshold
|
float
|
Level counting as motion, as a fraction of the envelope's range. Defaults to 0.15. |
0.15
|
min_duration
|
float
|
Spans shorter than this, in seconds, are discarded as noise. Defaults to 0.1. |
0.1
|
min_gap
|
float
|
Gaps shorter than this, in seconds, are closed. Defaults to 0.1. |
0.1
|
source
|
str
|
Recorded on each Action, to identify what produced it. |
'envelope'
|
range_mode
|
str
|
How the envelope's range is measured before |
'minmax'
|
range_percentiles
|
tuple
|
The percentiles bounding the range when
|
(1.0, 99.0)
|
Returns:
| Name | Type | Description |
|---|---|---|
list |
list[Action]
|
The actions found, in time order. |
Source code in musicalgestures/_actions.py
93 94 95 96 97 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 142 143 144 145 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 | |
action_type ¶
action_type(envelope, fs, iterative_min_peaks=3, impulsive_centroid=0.42)
Describe how motion is distributed inside one action.
Three shapes, following the typology of Sound Actions:
- impulsive --- energy arrives at once and decays. A hit, a tap, a clap.
- sustained --- energy is held across the span. A bowed note, a slow reach.
- iterative --- energy repeats within the span. A tremolo, a shake, a scrub.
Decided on two measures rather than a classifier, so the call can be read and argued
with. peaks counts internal maxima above half the span's peak: several of them mean
the motion repeated. centroid is where the span's energy sits, 0 at its start and 1
at its end: an impulse is front-loaded, a held action is centred.
Iterative is tested first, because a repeated action is also a centred one, and the repetition is the more specific description.
The discriminator is the centroid rather than time spent above half height, and that is a correction rather than a preference. Segmentation cuts an action at a threshold, so a decaying impulse arrives here already truncated to its loud third, which raises the fraction of it spent above half height until it is indistinguishable from a held action. Measured on the canonical shapes after segmentation: time-above-half reads 0.385 for a decay against 0.510 for a tremolo and 1.000 for a plateau, while the centroid reads 0.332, 0.481 and 0.500. Only the second separates the impulse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
envelope
|
The motion envelope of ONE action, not of the whole recording. |
required | |
fs
|
float
|
Sampling rate of the envelope, in frames per second. |
required |
iterative_min_peaks
|
int
|
Internal peaks needed to call a span iterative. Defaults to 3. |
3
|
impulsive_centroid
|
float
|
Energy centroid below which a span is called impulsive. Defaults to 0.42, midway between a decay and a plateau as measured above. |
0.42
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict
|
|
Source code in musicalgestures/_actions.py
174 175 176 177 178 179 180 181 182 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 | |
describe_actions ¶
describe_actions(actions, envelope, fs)
Attach :func:action_type to each action, in place, and return them.
Measuring is kept apart from naming on purpose: this fills features, never labels.
A shape is something the signal shows; a name is something a recogniser claims.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actions
|
list
|
Actions to describe, as returned by :func: |
required |
envelope
|
The motion envelope the actions were cut from. |
required | |
fs
|
float
|
Sampling rate of the envelope. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
list |
list[Action]
|
The same actions, with |
Source code in musicalgestures/_actions.py
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | |
mg_actions ¶
mg_actions(self, envelope=None, fs=None, threshold=0.15, min_duration=0.1, min_gap=0.1)
Segment this video into actions and describe the shape of each.
With no envelope given, one is built from the body rather than from the picture: pose landmarks are extracted if they are not cached, and their quantity of motion becomes the envelope. That is what makes the result follow the person and not the camera --- a pan moves every pixel and moves no landmark relative to the others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
envelope
|
A motion envelope to segment. Defaults to None, meaning build one from pose. Pass your own to segment something else, and note that a pixel-derived envelope will read camera motion as action. |
None
|
|
fs
|
float
|
Sampling rate of |
None
|
threshold
|
float
|
Level counting as motion, as a fraction of the envelope's range. Defaults to 0.15. |
0.15
|
min_duration
|
float
|
Shortest span kept, in seconds. Defaults to 0.1. |
0.1
|
min_gap
|
float
|
Longest gap closed, in seconds. Defaults to 0.1. |
0.1
|
Returns:
| Name | Type | Description |
|---|---|---|
list |
list[Action]
|
The actions found, each carrying its shape in |
Source code in musicalgestures/_actions.py
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 | |