SIGN IN SIGN UP

Miscellaneous imports and code fixes (#3411)

* Fix bone likelihood in analyzebone to use both bodyparts

analyzebone() computed each skeleton bone's likelihood as
min(bp2.likelihood, bp2.likelihood), stacking the second bodypart
twice and discarding bp1. The adjacent comment states the intent is
'keep the smallest of the two likelihoods', so the result should be
min(bp1.likelihood, bp2.likelihood).

As written, a bone whose first bodypart is occluded (low confidence)
but whose second bodypart is confident was reported as fully
confident, silently corrupting the likelihood column of every
skeleton analysis output.

* Fix SORTSkeleton removing the wrong tracker on cleanup

In SORTSkeleton.track, the reverse-iteration cleanup loop maintains i
as the current tracker's index but removed dead trackers with
self.trackers.pop() (removes the last element) instead of
self.trackers.pop(i). The sibling classes SORTBox and SORTEllipse
correctly use pop(i).

With a stale tracker in the middle of the list, pop() deleted a
different (often still-live) tracker; the dead one persisted and
animalindex[i] alignment broke, corrupting identity/tracklet
assignment for the skeleton track method.

* Fix CTD tracklet-age reset writing to a non-existent attribute

When a frame lost all individuals (NMS mask all-False), the reset
branch assigned self._idx_ages = None, but the tracking-age attribute
is _ctd_track_ages (initialised in __init__ and used by
_ctd_tracking_postprocess). _idx_ages is never read anywhere, so the
intended reset silently did nothing and a dead attribute was created.

As a result _ctd_track_ages kept its stale values; on the next
BU-seeded frame the OKS-NMS ordering was biased toward stale-old
indices, retaining the wrong pose/identity, and the bias compounded
over subsequent frames. Reset the correct attribute.

* Fix destfolder leaking across videos in filter/skeleton loops

filterpredictions() and analyzeskeleton() reassigned the destfolder
parameter inside their 'for video in Videos' loop when it was None.
After the first iteration destfolder stayed pinned to the first
video's folder, so videos located in a different folder were looked
up in the wrong place: load_analyzed_data raised FileNotFoundError
and the video was silently reported as not analyzed (and any output
would go to the wrong folder).

Use a per-iteration local 'videofolder' instead of mutating the
parameter, matching plot_trajectories and extract_outlier_frames.

* Fix except clause catching a ValueError instance instead of the type

In extract_outlier_frames (outlieralgorithm='list'), the handler was
written 'except ValueError():', which evaluates ValueError() to an
instance. When the try body raised a ValueError, Python's exception
matching hit a non-class handler and raised
'TypeError: catching classes that do not inherit from BaseException
is not allowed', masking the real error and the intended message.
Catch the ValueError type instead.

* Import scipy.ndimage functions directly (drop removed measurements namespace)

predict_multianimal.py imported 'from scipy.ndimage import
measurements'. The scipy.ndimage.measurements submodule was deprecated
in scipy 1.8 and removed in scipy 1.14; since the project pins
scipy>=1.9 with no upper bound, a fresh install resolves a scipy where
this import raises ImportError at module load. Because the module is
imported at the top of the TensorFlow multi-animal inference path,
analyze_videos on a TF maDLC project failed immediately on import.

Import label and center_of_mass directly from scipy.ndimage (their
canonical public location) and call them without the measurements
prefix.

* Replace removed np.object alias with builtin object

test.py used np.zeros(..., dtype=np.object). np.object was a deprecated
alias for the builtin object and was removed in numpy 1.24, so it
raises AttributeError under the pinned numpy (>=1.18.5,<2 resolves to
1.26). Use dtype=object.

* Fix operator precedence in condition NaN/non-positive mask

The mask 'conds <= 0 | np.isnan(conds)' parses as
'conds <= (0 | np.isnan(conds))' because bitwise | binds tighter than
the <= comparison, so the NaN check was not OR-ed with the
non-positive check as intended. Parenthesize to '(conds <= 0) |
np.isnan(conds)'.

Verified: for conds=[0.5, -0.1, nan] the buggy expression yields
[False, True, False] (misses the NaN), while the fixed expression
yields [False, True, True].

* Fix scipy.stats.mode usage for keepdims=False default (scipy>=1.11)

SORTEllipse.track indexed mode(identities[i])[0][0]. Since scipy 1.11
mode defaults to keepdims=False, so mode(1d)[0] is already a scalar and
the second [0] raises IndexError. Pass keepdims=False explicitly and
take a single [0], matching the pattern already used in
refine_training_dataset/stitch.py.

These sites are in the identity-aware branch of SORTEllipse.track,
which is currently only reached when 'identities' is passed to track()
(no in-tree caller does), so this is a latent fix rather than a live
regression.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sungjoo(Dennis) Hwang <48212469+Denny-Hwang@users.noreply.github.com>
C
Cyril Achard committed
040db9fb38a489befd0ffdf5d5a4a9cfa73ede6a
Parent: 030f458
Committed by GitHub <noreply@github.com> on 7/20/2026, 11:39:47 AM