Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
d533c0d
add MultiClassClassifer implementation
gbeane Apr 15, 2026
5a290bf
relax criteria for multi-class leave one group out validation (not al…
gbeane Apr 16, 2026
4c3ed46
add some validation for behavior_name parameter in MultiClassClassifier
gbeane Apr 16, 2026
267255f
address a PR comment
gbeane Apr 16, 2026
e782265
in MultiClassClassifier, ensure there are no conflicting labels when …
gbeane Apr 16, 2026
4cc735f
in leave_one_group_out ensure both behavior/not behavior are represen…
gbeane Apr 16, 2026
aa151db
fix Classifier.get_leave_one_group_out_max to also check training spl…
gbeane Apr 16, 2026
ffbc854
fix catboost loss function for multi-class
gbeane Apr 16, 2026
21958e0
add some tests for MultiClassClassifier to hit paths other than rando…
gbeane Apr 16, 2026
5fbc00d
if feature_names are supplied, select those columns
gbeane Apr 16, 2026
3075ecf
avoid duplicate warnings in Classifier and MultiClassClassifier if xg…
gbeane Apr 16, 2026
27a18f8
update leave_one_group_out docstring
gbeane Apr 16, 2026
9a20b3d
fix test in get_leave_one_group_out_max
gbeane Apr 16, 2026
5b4a31b
add parameter validation in min_test_classes
gbeane Apr 16, 2026
b2d670a
validate at least 2 labeled classes in multi-class classifier
gbeane Apr 16, 2026
e9cea40
add some validation in MultiClassClassifer.merge_labels
gbeane Apr 16, 2026
3874d9a
add a classifier mode (binary or multiclass) to project settings dialog
gbeane Apr 17, 2026
28a1202
use more user-friendly names in the classifier mode drop down selection
gbeane Apr 17, 2026
dd92fe0
when initializing the classifier mode drop down on the settings menu,…
gbeane Apr 17, 2026
bb77968
Merge pull request #349 from KumarLabJax/add-classifier-mode-project-…
gbeane Apr 17, 2026
efd52e0
Merge pull request #347 from KumarLabJax/implement-MultiClassClassifier
gbeane Apr 17, 2026
eb4c2c2
change blue not-behavior button label to 'None' when in multi-class mode
gbeane Apr 21, 2026
fcb86ac
in multi-class, make sure applying a None label clears other beahviors
gbeane Apr 21, 2026
6097c52
add VideoLabels.iter_behavior_labels() and use it to enforce mutual e…
gbeane Apr 21, 2026
0a4d399
log session tracker label_created for all manual labeling actions in …
gbeane Apr 21, 2026
75b58aa
Merge pull request #352 from KumarLabJax/klaus-409
gbeane Apr 21, 2026
1e98ae4
lay groundwork for multi-class label and prediction visualization in …
gbeane Apr 22, 2026
b066018
update .gitignore
gbeane Apr 22, 2026
3b48414
replace winner-take-all bin downsampling with proportional color blen…
gbeane Apr 22, 2026
0a591d2
simplify set_labels docstring in LabelOverviewWidget
gbeane Apr 22, 2026
eb84946
update docstring
gbeane Apr 22, 2026
a960790
fix stale docstrings and comments in label_overview_widget
gbeane Apr 22, 2026
9afe432
guard _downsample_to_size against ZeroDivisionError when size == 0
gbeane Apr 22, 2026
8680ee6
remove stale 'add 1 shift' comment in predicted_label_widget
gbeane Apr 22, 2026
1b87325
validate behavior_names in build_multiclass_label_array
gbeane Apr 22, 2026
3c0afaa
validate behavior_names in make_behavior_color_map and build_multicla…
gbeane Apr 22, 2026
056dd0e
widen probability type annotations from float32 to np.floating
gbeane Apr 22, 2026
5cc9059
improve resize handling of the stacked timeline widget -- bigly
gbeane Apr 22, 2026
6ccec9f
fix stale bin size, zero-size pixmap, and subpixel search hit markers
gbeane Apr 22, 2026
863ec9f
fix some rendering issues with the TimelineLabelWidget
gbeane Apr 22, 2026
5edc9ce
refactor(ui): add defensive validation and probability clamping in ti…
gbeane Apr 22, 2026
5607474
simplify num_frames guard in set_labels validation
gbeane Apr 23, 2026
63e1191
Merge pull request #355 from KumarLabJax/improve-timeline-resizing
gbeane Apr 23, 2026
5927b59
validate LUT shape in set_color_lut
gbeane Apr 23, 2026
956e43b
improve timeline color blending: exclude background from labeled bins…
gbeane Apr 23, 2026
02681c0
Merge pull request #354 from KumarLabJax/feature/timeline-widget-mult…
gbeane Apr 23, 2026
e7a2b51
extend StackedTimelineWidget for multi-class visualization (T5)
gbeane Apr 24, 2026
6e6d3e6
merge main: compact mode, cache format settings, other fixes
gbeane Apr 24, 2026
31a8d2d
per-class prediction bars: probability heatmap, separator, compact, h…
gbeane Apr 25, 2026
e1a027c
fix multiclass mode: refresh labels after labeling, block binary pred…
gbeane Apr 25, 2026
8b28392
multiclass layout menu actions; show overview bar before predictions …
gbeane Apr 25, 2026
e0ed6e9
edit docstring
gbeane Apr 25, 2026
c392c9e
address PR review: validate inner list lengths in set_predictions, re…
gbeane Apr 25, 2026
d268326
log warning on invalid classifier mode or CV grouping in project file
gbeane Apr 27, 2026
5f552dd
clear stale prediction overlay when switching to multiclass mode
gbeane Apr 28, 2026
648aff0
Merge pull request #360 from KumarLabJax/feature/multiclass-timeline-…
gbeane Apr 28, 2026
6aca27f
fix exception when switching a project from multiclass to binary clas…
gbeane Apr 28, 2026
4dde792
fix some type warnings in timeline_label_widget.py
gbeane Apr 28, 2026
14d591e
use np.float32 divisor to avoid float64 promotion in lut normalization
gbeane Apr 28, 2026
735bbbf
check for conflicting labels when migrating a project from binary to …
gbeane Apr 28, 2026
48fd965
Merge pull request #361 from KumarLabJax/bug/fix-exception-when-switc…
gbeane Apr 28, 2026
b7460c6
Merge pull request #362 from KumarLabJax/fix/type-warnings-in-timelin…
gbeane Apr 28, 2026
834e367
include None track in overlap conflict check, consistent with merge_l…
gbeane Apr 28, 2026
0a0a9c4
accumulate per-frame behavior count instead of stacking arrays in ove…
gbeane Apr 28, 2026
591b90a
Handle overlap validation errors
gbeane Apr 28, 2026
f1f584d
skip pose load for videos with no annotations
gbeane Apr 29, 2026
786e8e1
Merge pull request #365 from KumarLabJax/multiclass-check-for-label-c…
gbeane Apr 29, 2026
a347be6
rename stacked_timeline_widget package and all classes to behavior_ti…
gbeane Apr 30, 2026
8b8661e
fix load_video_labels mock to match optional pose parameter
gbeane Apr 30, 2026
f687c33
address some PR comments
gbeane Apr 30, 2026
a5ebf6e
update docstring
gbeane Apr 30, 2026
bfb77ea
change bar height in LabelOverviewBar
gbeane Apr 30, 2026
b801f60
Merge pull request #367 from KumarLabJax/refactor/behavior-timeline-r…
gbeane Apr 30, 2026
95f6505
end-to-end integration of multi-class training and classification to GUI
gbeane Apr 30, 2026
fb468e3
address some pull requests comments
gbeane Apr 30, 2026
a98c213
address some pull requests comments
gbeane Apr 30, 2026
8462e8e
fix a few GUI bugs training/classifying with multi-class
gbeane May 1, 2026
cb968c0
cleanup a type warning
gbeane May 1, 2026
c66e1a7
address some PR comments
gbeane May 1, 2026
4aed1f8
Potential fix for pull request finding
gbeane May 1, 2026
b47e200
address PR comment and clean up some type warnings
gbeane May 1, 2026
1739dfd
Merge branch 'feature/multiclass-end-to-end-gui-integration' of https…
gbeane May 1, 2026
dd992be
some refactoring of parallel_workers.py
gbeane May 1, 2026
1af7390
fix bug in training thread for multi-class path
gbeane May 1, 2026
df2b1b6
Update src/jabs/project/parallel_workers.py
gbeane May 4, 2026
ab0d05d
Merge pull request #368 from KumarLabJax/feature/multiclass-end-to-en…
gbeane May 4, 2026
ce8eef3
Merge remote-tracking branch 'origin/main' into feature/multiclass
gbeane May 5, 2026
a78956f
Merge branch 'main' of https://github.com/KumarLabJax/JABS-behavior-c…
gbeane May 7, 2026
882bf30
add type annotation for _export_progress_dialog
gbeane May 11, 2026
e74c6fb
T13: add multi-class training data export (GUI, CLI, and thread)
gbeane May 11, 2026
ab7593c
T13: add multiclass reader, from_training_file, fix class names, add …
gbeane May 12, 2026
7da5cf3
replace "background" terminology with MULTICLASS_NONE_BEHAVIOR in doc…
gbeane May 12, 2026
0306ced
WIP: add multi-class support to jabs-classify classify command (T7)
gbeane May 11, 2026
743b5b9
T7: add multi-class support to jabs-classify train command
gbeane May 12, 2026
65b3c04
add from_pickle() classmethods; use them in _load_classifier_from_pickle
gbeane May 12, 2026
efb31b4
wire classifier-type override flags through to from_training_file()
gbeane May 12, 2026
fa8d129
document all exceptions from _load_classifier_from_pickle; catch at c…
gbeane May 12, 2026
d622382
use an assert to resolve a type warning
gbeane May 12, 2026
a35f5d1
add Path as a type hint for source_file in IdentityFeatures init
gbeane May 12, 2026
c11aece
update the label overlay to work with multi-class projects
gbeane May 12, 2026
40e843c
address PR review comments: repaint on LUT change, extract helper, gu…
gbeane May 12, 2026
647f8ac
Merge pull request #376 from KumarLabJax/feature/t13-multiclass-export
gbeane May 15, 2026
caa1309
Merge pull request #378 from KumarLabJax/feature/multiclass-label-ove…
gbeane May 19, 2026
b7a288e
Merge pull request #377 from KumarLabJax/feature/multiclass-cli-classify
gbeane May 19, 2026
a054e49
extract BaseClassifier to remove binary/multi-class duplication
gbeane May 20, 2026
7ce4156
align _FakeProject.get_multiclass_labeled_features with real signature
gbeane May 20, 2026
ed8e0ca
split CrossValidationResult into binary/multi-class subclasses
gbeane May 20, 2026
a75e48e
decompose run_leave_one_group_out_cv; unify LOGO acceptance rule
gbeane May 21, 2026
8fa724c
dedup project label-collection methods; split worker into binary/mult…
gbeane May 21, 2026
311c5cf
split TrainingThread by mode: extract BinaryTrainingStrategy and Mult…
gbeane May 21, 2026
92c3e32
split ClassifyThread by mode: extract BinaryClassifyStrategy and Mult…
gbeane May 21, 2026
c4b590f
drop dead PredictionManager attrs; widen write_predictions classifier…
gbeane May 21, 2026
5d43bf0
move multiclass label-threshold logic onto MultiClassClassifier
gbeane May 22, 2026
ace3c74
extract central_widget_mode helpers to consolidate label/prediction d…
gbeane May 22, 2026
64f958c
extract UI thread test fakes into tests/ui/_fakes module
gbeane May 22, 2026
4d8d8a3
consolidate FakeIdentityFeatures via factory; replace T9 tracker comment
gbeane May 23, 2026
0e709ce
merge origin/feature/multiclass into refactor/multiclass-cleanup
gbeane May 26, 2026
2ab81fd
address review: persist Classifier feature_names; clarify binary feat…
gbeane May 27, 2026
1130295
clarify fake stand-in docstrings in tests/ui/_fakes
gbeane May 27, 2026
cf9ac39
Merge pull request #383 from KumarLabJax/refactor/multiclass-cleanup
gbeane May 28, 2026
0e48056
Merge branch 'main' of https://github.com/KumarLabJax/JABS-behavior-c…
gbeane May 28, 2026
a6af82b
Fix rename_behavior desync in multi-class projects
gbeane May 30, 2026
4f37003
Remove behavior_name ordering coupling in set_project_settings
gbeane May 30, 2026
0e94962
Reject cross-mode prediction loads in prediction manager
gbeane May 30, 2026
b2edda9
Address review: refresh classifier hash on rename and reject reserved…
gbeane May 30, 2026
d3feac6
Repoint per-video prediction metadata at renamed multi-class classifier
gbeane Jun 1, 2026
0f4c213
Derive prediction probability shape from class_names in save_predictions
gbeane Jun 1, 2026
d767ee4
Validate per-identity probability shape in save_predictions
gbeane Jun 1, 2026
3465b5e
Label multi-class classifier mode as a preview feature
gbeane Jun 1, 2026
8ef9274
Merge pull request #385 from KumarLabJax/fix/multiclass-rename-behavi…
gbeane Jun 2, 2026
dcebcf3
Merge pull request #388 from KumarLabJax/feature/multiclass-preview-l…
gbeane Jun 2, 2026
e7b6daf
Make label summary multi-class aware (behavior name / None)
gbeane Jun 2, 2026
d4b458c
Add multi-class preview user-guide page with known limitations
gbeane Jun 2, 2026
49094f4
Cache None-track counts across behavior changes, invalidate on projec…
gbeane Jun 2, 2026
36960cc
Merge branch 'feature/multiclass' of https://github.com/KumarLabJax/J…
gbeane Jun 2, 2026
45581f1
Merge pull request #386 from KumarLabJax/fix/multiclass-prediction-ty…
gbeane Jun 2, 2026
ba4ab2a
Merge remote-tracking branch 'origin/feature/multiclass' into fix/mul…
gbeane Jun 2, 2026
058787b
Merge pull request #387 from KumarLabJax/fix/multiclass-save-predicti…
gbeane Jun 2, 2026
1ab7027
Merge pull request #389 from KumarLabJax/feature/multiclass-beta-polish
gbeane Jun 2, 2026
82c0b0c
Harden prediction load and multiclass CV against invalid/empty input
gbeane Jun 3, 2026
6d68929
Merge pull request #391 from KumarLabJax/fix/multiclass-prediction-cv…
gbeane Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,11 @@ cache invalidation logic (version mismatch, pose hash, distance scale).
- `__init__.py` files: unused imports (F401) are allowed
- **Naming**: `PascalCase` classes, `snake_case` functions/methods/files,
`UPPER_SNAKE_CASE` constants, `_` prefix for private members
- Prefer American English spelling (e.g., "behavior" not "behaviour").
- Use American English spelling (e.g., "behavior" not "behaviour").
- Prefer importing at the top of the module. It is acceptable to import within
functions/methods if an expensive import is only needed in specific code paths.
- Do not use ambiguous `–` (EN DASH) in docstrings. Use `-` (HYPHEN-MINUS) to avoid
Ruff RUF001 error.

## Coding Standards

Expand Down
381 changes: 381 additions & 0 deletions dev/preview_multiclass_timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,381 @@
"""Preview script: StackedTimelineWidget with fake multi-class data.

Run with:
uv run python preview_multiclass_timeline.py

Displays 3 identities, 3 behaviors, ~2 000 frames. A slider scrubs through
frames; radio buttons toggle binary / multi-class mode and single / all-identity
view so you can compare layouts.
"""

import sys

import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
QApplication,
QButtonGroup,
QCheckBox,
QGroupBox,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QRadioButton,
QScrollArea,
QSlider,
QVBoxLayout,
QWidget,
)

from jabs.core.enums import ClassifierMode
from jabs.ui.stacked_timeline_widget import StackedTimelineWidget

# ---------------------------------------------------------------------------
# Fake pose shim (duck-types the attributes/method that _reset_layout needs)
# ---------------------------------------------------------------------------


class _FakePose:
def __init__(self, num_identities: int, num_frames: int) -> None:
self.num_identities = num_identities
self.num_frames = num_frames

def identity_index_to_display(self, index: int) -> str:
return f"Mouse {index + 1}"


# ---------------------------------------------------------------------------
# Fake data generation
# ---------------------------------------------------------------------------

NUM_IDENTITIES = 3
NUM_FRAMES = 2000
FRAMERATE = 30
BEHAVIORS = ["grooming", "rearing", "locomotion"]

rng = np.random.default_rng(seed=42)


def _make_bouts(n_frames: int, avg_gap: int = 120, avg_dur: int = 60) -> np.ndarray:
"""Return a boolean mask with random on/off bouts."""
mask = np.zeros(n_frames, dtype=bool)
frame = 0
while frame < n_frames:
frame += max(10, int(rng.exponential(avg_gap)))
dur = max(5, int(rng.exponential(avg_dur)))
mask[frame : frame + dur] = True
frame += dur
return mask


def _make_multiclass_labels(n_frames: int) -> np.ndarray:
"""Build a combined class-index label array for one identity.

Index layout (matches build_multiclass_color_lut):
0 = unlabeled, 1 = "None", 2 = grooming, 3 = rearing, 4 = locomotion
"""
labels = np.zeros(n_frames, dtype=np.int16)
for behavior_idx in range(len(BEHAVIORS)):
bouts = _make_bouts(n_frames, avg_gap=200, avg_dur=50)
free = labels == 0
labels[free & bouts] = behavior_idx + 2
none_bouts = _make_bouts(n_frames, avg_gap=400, avg_dur=20)
labels[(labels == 0) & none_bouts] = 1
return labels


def _make_binary_labels(n_frames: int) -> np.ndarray:
"""Binary LUT-index labels: 1 = not-behavior, 2 = behavior."""
arr = np.ones(n_frames, dtype=np.int16)
arr[_make_bouts(n_frames, avg_gap=200, avg_dur=60)] = 2
return arr


def _make_per_class_predictions(
n_frames: int,
) -> tuple[list[np.ndarray], list[np.ndarray]]:
"""Per-class binary predictions + probabilities for one identity.

Returns one array per class: [None/background, behA, behB, behC, ...].
Each prediction array uses the 3-entry per-class LUT:
0 = no pose, 1 = not this class, 2 = this class predicted.
"""
behavior_bouts = [_make_bouts(n_frames, avg_gap=200, avg_dur=55) for _ in BEHAVIORS]
any_behavior = np.zeros(n_frames, dtype=bool)
for b in behavior_bouts:
any_behavior |= b

preds, probs = [], []

# None/background class: predicted wherever no behavior is active
none_pred = np.ones(n_frames, dtype=np.int16)
none_pred[~any_behavior] = 2
none_prob = np.where(
~any_behavior,
rng.uniform(0.6, 1.0, n_frames),
rng.uniform(0.0, 0.3, n_frames),
).astype(np.float32)
preds.append(none_pred)
probs.append(none_prob)

for bouts in behavior_bouts:
pred = np.ones(n_frames, dtype=np.int16)
pred[bouts] = 2
prob = np.where(
bouts,
rng.uniform(0.6, 1.0, n_frames),
rng.uniform(0.0, 0.3, n_frames),
).astype(np.float32)
preds.append(pred)
probs.append(prob)

return preds, probs


# ---------------------------------------------------------------------------
# Pre-generate data for all identities
# ---------------------------------------------------------------------------

MULTICLASS_LABELS = [_make_multiclass_labels(NUM_FRAMES) for _ in range(NUM_IDENTITIES)]
BINARY_LABELS = [_make_binary_labels(NUM_FRAMES) for _ in range(NUM_IDENTITIES)]
MASKS = [np.ones(NUM_FRAMES, dtype=np.int8) for _ in range(NUM_IDENTITIES)]

_mc_data = [_make_per_class_predictions(NUM_FRAMES) for _ in range(NUM_IDENTITIES)]
MULTICLASS_PREDS_LIST = [preds for preds, _ in _mc_data]
MULTICLASS_PROBS_LIST = [probs for _, probs in _mc_data]

BINARY_PREDS_LIST = [[_make_binary_labels(NUM_FRAMES)] for _ in range(NUM_IDENTITIES)]
BINARY_PROBS_LIST = [
[rng.uniform(0.0, 1.0, NUM_FRAMES).astype(np.float32)] for _ in range(NUM_IDENTITIES)
]

FAKE_POSE = _FakePose(NUM_IDENTITIES, NUM_FRAMES)


# ---------------------------------------------------------------------------
# Main window
# ---------------------------------------------------------------------------


class PreviewWindow(QMainWindow):
"""Simple preview window for StackedTimelineWidget multi-class layout."""

def __init__(self) -> None:
super().__init__()
self.setWindowTitle("StackedTimelineWidget — multi-class preview")
self.resize(1200, 500)

self._mode = ClassifierMode.MULTICLASS

central = QWidget()
self.setCentralWidget(central)
root_layout = QVBoxLayout(central)

# ---- Controls row --------------------------------------------------
controls = QWidget()
controls_layout = QHBoxLayout(controls)
controls_layout.setContentsMargins(4, 4, 4, 4)

# Classifier mode toggle
mode_box = QGroupBox("Classifier mode")
mode_layout = QHBoxLayout(mode_box)
self._rb_multiclass = QRadioButton("Multi-class")
self._rb_binary = QRadioButton("Binary")
self._rb_multiclass.setChecked(True)
mode_group = QButtonGroup(self)
mode_group.addButton(self._rb_multiclass)
mode_group.addButton(self._rb_binary)
mode_layout.addWidget(self._rb_multiclass)
mode_layout.addWidget(self._rb_binary)
self._rb_multiclass.toggled.connect(self._on_mode_toggled)

# Identity view toggle
identity_box = QGroupBox("Identity view")
identity_layout = QHBoxLayout(identity_box)
self._rb_active = QRadioButton("Active only")
self._rb_all = QRadioButton("All animals")
self._rb_active.setChecked(True)
identity_group = QButtonGroup(self)
identity_group.addButton(self._rb_active)
identity_group.addButton(self._rb_all)
identity_layout.addWidget(self._rb_active)
identity_layout.addWidget(self._rb_all)
self._rb_all.toggled.connect(self._on_identity_mode_toggled)

# Collapse inactive checkboxes (only relevant in all-animals mode)
collapse_box = QGroupBox("Collapse inactive")
collapse_layout = QHBoxLayout(collapse_box)
self._chk_collapse_label = QCheckBox("Label bar")
self._chk_collapse_label.setChecked(False)
self._chk_collapse_label.setEnabled(False)
self._chk_collapse_combined = QCheckBox("Combined bar")
self._chk_collapse_combined.setChecked(False)
self._chk_collapse_combined.setEnabled(False)
self._chk_collapse_per_class = QCheckBox("Per-class bars")
self._chk_collapse_per_class.setChecked(True)
self._chk_collapse_per_class.setEnabled(False)
collapse_layout.addWidget(self._chk_collapse_label)
collapse_layout.addWidget(self._chk_collapse_combined)
collapse_layout.addWidget(self._chk_collapse_per_class)
self._chk_collapse_label.toggled.connect(
lambda v: setattr(self._timeline, "collapse_inactive_label_bar", v)
)
self._chk_collapse_combined.toggled.connect(
lambda v: setattr(self._timeline, "collapse_inactive_combined_bar", v)
)
self._chk_collapse_per_class.toggled.connect(
lambda v: setattr(self._timeline, "collapse_inactive_per_class_bars", v)
)

# Hide per-class rows entirely for inactive animals
hide_box = QGroupBox("Hide inactive")
hide_layout = QHBoxLayout(hide_box)
self._chk_hide_per_class = QCheckBox("Per-class bars")
self._chk_hide_per_class.setChecked(False)
self._chk_hide_per_class.setEnabled(False)
hide_layout.addWidget(self._chk_hide_per_class)
self._chk_hide_per_class.toggled.connect(
lambda v: setattr(self._timeline, "hide_inactive_per_class_widgets", v)
)

# Frame slider
slider_box = QGroupBox(f"Frame (0 - {NUM_FRAMES - 1})")
slider_layout = QHBoxLayout(slider_box)
self._slider = QSlider(Qt.Orientation.Horizontal)
self._slider.setRange(0, NUM_FRAMES - 1)
self._slider.setValue(0)
self._frame_label = QLabel("0")
self._frame_label.setFixedWidth(50)
slider_layout.addWidget(self._slider)
slider_layout.addWidget(self._frame_label)
self._slider.valueChanged.connect(self._on_frame_changed)

# Active identity selector
identity_sel_box = QGroupBox("Active identity ([ / ] or Tab)")
identity_sel_layout = QHBoxLayout(identity_sel_box)
self._btn_prev_id = QPushButton("<")
self._btn_prev_id.setFixedWidth(28)
self._btn_next_id = QPushButton(">")
self._btn_next_id.setFixedWidth(28)
self._active_id_label = QLabel(self._identity_display(0))
self._active_id_label.setFixedWidth(70)
identity_sel_layout.addWidget(self._btn_prev_id)
identity_sel_layout.addWidget(self._active_id_label)
identity_sel_layout.addWidget(self._btn_next_id)
self._btn_prev_id.clicked.connect(self._prev_identity)
self._btn_next_id.clicked.connect(self._next_identity)

controls_layout.addWidget(mode_box)
controls_layout.addWidget(identity_box)
controls_layout.addWidget(collapse_box)
controls_layout.addWidget(hide_box)
controls_layout.addWidget(identity_sel_box)
controls_layout.addWidget(slider_box, stretch=1)

self._controls = controls
root_layout.addWidget(controls)

# ---- Keyboard shortcuts -------------------------------------------
QShortcut(QKeySequence(Qt.Key.Key_Tab), self).activated.connect(self._next_identity)
QShortcut(QKeySequence("["), self).activated.connect(self._prev_identity)
QShortcut(QKeySequence("]"), self).activated.connect(self._next_identity)

# ---- Scrollable timeline area -------------------------------------
self._scroll = QScrollArea()
self._scroll.setWidgetResizable(True)
self._scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

self._timeline = StackedTimelineWidget()
self._scroll.setWidget(self._timeline)
root_layout.addWidget(self._scroll, stretch=1)

# ---- Load initial data -------------------------------------------
self._timeline.pose = FAKE_POSE
self._timeline.framerate = FRAMERATE
self._apply_mode()

# ------------------------------------------------------------------

@staticmethod
def _identity_display(index: int) -> str:
return FAKE_POSE.identity_index_to_display(index)

def _set_active_identity(self, index: int) -> None:
self._timeline.active_identity_index = index
self._active_id_label.setText(
f"{self._identity_display(index)} ({index + 1}/{NUM_IDENTITIES})"
)

def _prev_identity(self) -> None:
current = self._timeline.active_identity_index or 0
self._set_active_identity((current - 1) % NUM_IDENTITIES)

def _next_identity(self) -> None:
current = self._timeline.active_identity_index or 0
self._set_active_identity((current + 1) % NUM_IDENTITIES)

def _apply_mode(self) -> None:
"""Push the current mode + fake data into the timeline widget."""
if self._mode == ClassifierMode.MULTICLASS:
self._timeline.set_classifier_mode(ClassifierMode.MULTICLASS, BEHAVIORS)
self._timeline.set_labels(MULTICLASS_LABELS, MASKS)
self._timeline.set_predictions(MULTICLASS_PREDS_LIST, MULTICLASS_PROBS_LIST)
else:
self._timeline.set_classifier_mode(ClassifierMode.BINARY, [])
self._timeline.set_labels(BINARY_LABELS, MASKS)
self._timeline.set_predictions(BINARY_PREDS_LIST, BINARY_PROBS_LIST)

def _on_mode_toggled(self, checked: bool) -> None:
if checked:
self._mode = ClassifierMode.MULTICLASS
else:
self._mode = ClassifierMode.BINARY
self._apply_mode()

def _on_identity_mode_toggled(self, _checked: bool) -> None:
is_all = self._rb_all.isChecked()
self._timeline.identity_mode = (
StackedTimelineWidget.IdentityMode.ALL
if is_all
else StackedTimelineWidget.IdentityMode.ACTIVE
)
self._chk_collapse_label.setEnabled(is_all)
self._chk_collapse_combined.setEnabled(is_all)
self._chk_collapse_per_class.setEnabled(is_all)
self._chk_hide_per_class.setEnabled(is_all)
self._resize_to_content()

def _resize_to_content(self) -> None:
"""Resize the window height to fit the current timeline content."""
margins = self.centralWidget().layout().contentsMargins()
controls_h = self._controls.sizeHint().height()
timeline_h = self._timeline.sizeHint().height()
chrome = self.frameGeometry().height() - self.geometry().height()
needed = (
controls_h
+ timeline_h
+ margins.top()
+ margins.bottom()
+ chrome
+ 8 # a little breathing room
)
screen_h = self.screen().availableGeometry().height()
self.resize(self.width(), min(needed, screen_h - 40))

def _on_frame_changed(self, frame: int) -> None:
self._frame_label.setText(str(frame))
self._timeline.set_current_frame(frame)


def main() -> None:
"""Entry point."""
app = QApplication(sys.argv)
win = PreviewWindow()
win.show()
sys.exit(app.exec())


if __name__ == "__main__":
main()
Loading
Loading