chirality
sleap.qc.features.chirality
¶
Chirality (left/right mirror flip) features.
A whole-instance left/right mirror flip is invariant to every distance- and unsigned-angle-based feature (edge lengths, joint angles, pairwise distances, convex hull, ...), so it is invisible to those detectors. Detecting it requires a signed statistic that encodes which side of the body axis each landmark falls on, i.e. the chirality of the pose.
This module learns, per symmetric landmark pair, the canonical (majority) side of the body axis on which the "left" member of the pair sits, then scores a new instance by the fraction of co-visible symmetric pairs whose observed side disagrees with that learned canonical side:
- a clean pose scores
~0, - a whole-instance mirror flip scores
~1(every pair disagrees), - a partial / subset swap scores an intermediate value.
The signed side of a point p relative to the body axis is the sign of the
2D cross product of the axis vector with (p - axis_origin). This is
invariant to translation, uniform scaling, and rotation of the whole instance
(it only flips under reflection), which is exactly the property needed to
isolate mirror flips from ordinary pose variation.
Local (spine-relative) axis. A single straight body axis (e.g. the chord
from nose to tail-base) misjudges the side of a pair whenever the animal curls:
a curled-but-correctly-labeled instance then trips the detector. To stay robust
to body curvature, each symmetric pair is measured against the local tangent
of the body midline near that pair. The midline is supplied as an ordered
list of non-symmetric node indices (nose -> tail), forming a polyline; for each
pair the pair midpoint is projected onto the nearest midline segment and that
segment's tangent is used as the local axis in the cross product. With a
single-segment (two-node) midline this reduces exactly to the straight-axis
sign, so the two-node axis_node_indices form remains a special case.
The midline polyline is resolved per instance in this order:
- the visible nodes of
midline_node_indices(the ordered midline), if at least two are visible; - otherwise the two
axis_node_indicesanchors, if both are visible (a degenerate single-segment midline); - otherwise the first principal component of the visible non-symmetric points (PCA fallback), as a single-segment midline through their centroid.
Functions:
| Name | Description |
|---|---|
compute_chirality |
Score a single instance for a left/right mirror flip. |
fit_chirality |
Learn the canonical signed side per symmetric pair from training poses. |
infer_symmetry_pairs_by_name |
Infer left/right symmetric pairs from node-name suffixes/prefixes. |
order_midline_by_pca |
Order midline node indices nose -> tail by their mean PCA projection. |
compute_chirality(points, symmetry_pairs, midline_node_indices, model, axis_node_indices=None, min_pairs=2)
¶
Score a single instance for a left/right mirror flip.
For each co-visible symmetric pair with a learned canonical side, the signed
side of the left member relative to the local midline tangent is
compared to the learned canonical side. The returned
chirality_wrong_fraction is the fraction of such pairs whose observed
side disagrees with the canonical one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
ndarray
|
|
required |
symmetry_pairs
|
list[tuple[int, int]]
|
List of |
required |
midline_node_indices
|
Optional[list[int]]
|
Ordered (nose -> tail) non-symmetric midline node
indices for the body midline polyline. If |
required |
model
|
dict
|
Model dict returned by :func: |
required |
axis_node_indices
|
Optional[tuple[int, int]]
|
Optional |
None
|
min_pairs
|
int
|
Minimum number of scorable co-visible pairs required for a
meaningful score. Below this, |
2
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Dictionary with:
|
Source code in sleap/qc/features/chirality.py
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 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 | |
fit_chirality(instances, symmetry_pairs, midline_node_indices=None, axis_node_indices=None)
¶
Learn the canonical signed side per symmetric pair from training poses.
For each symmetric pair (left, right) and each training instance where
the pair is co-visible and the body midline is resolvable, the signed side
of the left member relative to the local midline tangent is computed.
The canonical side is the majority sign across instances (sign of the mean
of the per-instance signs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instances
|
list[ndarray]
|
List of |
required |
symmetry_pairs
|
list[tuple[int, int]]
|
List of |
required |
midline_node_indices
|
Optional[list[int]]
|
Ordered (nose -> tail) list of non-symmetric
midline node indices defining the body midline polyline. When at
least two of them are visible for an instance, the local tangent of
the nearest midline segment is used as that pair's axis. If |
None
|
axis_node_indices
|
Optional[tuple[int, int]]
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
A model dict with:
|
Source code in sleap/qc/features/chirality.py
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 385 386 387 388 389 390 391 392 393 394 | |
infer_symmetry_pairs_by_name(node_names)
¶
Infer left/right symmetric pairs from node-name suffixes/prefixes.
Used when a skeleton has no symmetries defined (e.g. CVAT-style imports), so
that mirror-flip detection still works. Pairs nodes whose names share a stem
but differ by a left/right token, e.g. Ear_L/Ear_R,
Shoulder_left/Shoulder_right, Haunch_left/Haunch_right,
L_Eye/R_Eye.
The single-letter _L/_R form is only honored when a matching stem
exists on the other side, which avoids spuriously treating e.g. a lone
tail ending in no token as symmetric.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_names
|
list[str]
|
Ordered list of node names (index = node index). |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of |
Source code in sleap/qc/features/chirality.py
order_midline_by_pca(instances, midline_node_indices, min_points=2)
¶
Order midline node indices nose -> tail by their mean PCA projection.
The body midline polyline needs its nodes in anatomical order, but a skeleton's graph topology does not always provide it (a star-topology skeleton, for example, attaches several midline nodes to a single hub with no path between them). This orders the supplied midline nodes by the average of their projections onto the first principal component of the non-symmetric points, which recovers the nose -> tail ordering robustly across topologies.
The PCA axis has an arbitrary sign; the returned order is therefore unique only up to reversal. Reversing the midline does not change the local-tangent line (only its orientation), and the signed-side cross product flips sign consistently for every pair, so the learned canonical sides absorb the choice. The orientation is fixed deterministically (first node gets the smaller mean projection) purely for reproducibility.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instances
|
list[ndarray]
|
List of |
required |
midline_node_indices
|
list[int]
|
Unordered midline (non-symmetric) node indices. |
required |
min_points
|
int
|
Minimum non-symmetric points needed in an instance for it to contribute to the projection estimate. |
2
|
Returns:
| Type | Description |
|---|---|
list[int]
|
The midline node indices ordered by mean PCA projection. If no instance yields a usable axis, the input order is returned unchanged. |