API Reference
Entry point
The recommended way to start a DUSTrack session. Hand it a path and it
figures out whether you’re starting fresh on a bare video or resuming
inside a DLC project, dispatching to :class:~dustrack.gui.DUSTrack
or :meth:~dustrack.dlcinterface.DLCProject.annotate accordingly. Direct
construction of :class:~dustrack.gui.DUSTrack still works for
advanced use, but dustrack.open(...) is the documented surface.
- dustrack._open.open(path=None, layer_name=None, **dustrack_kwargs)
Open a DUSTrack annotation session; auto-resolves single- vs multi-video.
The unified entry point for the DUSTrack workflow. Users hand it a path (or a list of paths) and DUSTrack figures out whether they’re starting fresh on a standalone video (Phase 1), resuming inside a DLC project on one video (Phase 2 single), or queueing every video in a DLC project for in-session swap navigation (Phase 2 multi, 1.2.0a3).
- Phase 1 – bare video, no DLC project context.
Equivalent to
DUSTrack(path, layer_name, **kwargs). Works withoutdeeplabcutinstalled – the GUI plus the LK-RSTC post-processing run standalone, which is the “Option 1” install path from the paper. Single-video only; multi-video requires a DLC project (see Phase 2 multi).- Phase 2 single – one video inside a DLC project.
Accepts a video inside a project’s
videos/folder. Resolves theDLCProjectand dispatches toDLCProject.annotate()so a fresh DUSTrack opens with all existing annotation layers, DLC trace overlays, and a new iteration layer wired up.- Phase 2 multi – the whole project (or a subset). (1.2.0a3)
Three entry shapes:
Project folder:
dustrack.open('S:/path/to/project/')queues every video inproject.config['video_sets'](in project order). Behavior change vs <=1.2.0a2 (which opened only the first video).DLC config.yaml:
dustrack.open('.../config.yaml')queues every video inproject.config['video_sets'], same as the folder form. Behavior change vs <=1.2.0a3-pre (which opened video 0 only).DLCProject.__init__runsrebase_to_config()on each video_sets key, so stale paths after a project-folder rename self-heal here.List of videos:
dustrack.open([v0, v1, ...])queues exactly those videos, in the given order. Every entry must resolve to the same DLC project; mismatches and bare-video entries raise. The list-form is the way to override the YAML-stored order (e.g. Ctrl+click in the file dialog).
The active session opens on the first queued video; the rest are background-hydrated and reachable via the sidebar’s arrow nav row (or
Alt+Left/Alt+Right). Per-video state (active layer, overlay, frame, frozen axes, image-pane zoom, unsaved edits) persists across swaps – swap-back returns to the exact visual state the user left.
The two-phase split mirrors DUSTrack’s deliberate copy-on-project- creation design: once a DLC project exists, the project folder is the workspace and the original video becomes a frozen “rewind point” (delete the folder to start over).
open()honors that boundary – pointing at the original video gives you Phase 1, pointing at the in-project copy gives you Phase 2.- Parameters:
path – Video file,
config.yaml, DLC project folder, a sequence of videos inside one DLC project, orNone–Nonepops a Qt file picker and lets the user pick one or more videos.layer_name – Annotation layer name. Optional in both phases: Phase 1 defaults to
'iteration-0'(the canonical seed name for the rest of the DLC pipeline – the next DLC training iteration lands asiteration-1); Phase 2 defaults toiteration-{N+1}(the next-iteration suffix derived from the project’s training history). Callers can still pass an explicit name to override. Ignored for the project-folder multi-video form.**dustrack_kwargs – Forwarded to the underlying
DUSTrackconstructor (dark_mode,fast_render,clahe_clip,gamma,brightness, etc.).
- Returns:
Live annotation UI, ready to use.
Noneif the no-arg form’s file picker was cancelled.- Return type:
- Raises:
FileNotFoundError – If
pathdoesn’t exist (or any entry in a list form is missing).ValueError – Path is a directory that isn’t a DLC project, an empty sequence was supplied, a multi-video list mixes DLC projects, or a multi-video list includes any Phase 1 (bare-video) entry.
ImportError – Phase 2 entry on a system without
deeplabcutinstalled.
Examples
Zero-argument launch (pops a video picker):
import dustrack tracker = dustrack.open()
Fresh annotation (default layer name
'iteration-0'):tracker = dustrack.open('video.mp4')
Multi-video launch from a DLC project folder (queues every video in the project):
tracker = dustrack.open('S:/path/to/project/')
Multi-video launch from a subset of project videos:
tracker = dustrack.open(['S:/proj/videos/v0.mp4', 'S:/proj/videos/v3.mp4'])
Resume after closing the UI mid-workflow (any of these work as single-video entries):
tracker = dustrack.open('S:/path/to/project/videos/video.mp4') tracker = dustrack.open('S:/path/to/project/config.yaml')
With UI options:
tracker = dustrack.open('video.mp4', 'manual', dark_mode=True)
DUSTrack GUI
- class dustrack.gui.DUSTrack(vid_name, annotation_names='iteration-0', *args, clahe_clip=1.0, clahe_grid=8, gamma=1.0, brightness=0, dark_mode=False, n_labels: int = 1, titlefunc: Callable | None = None, height_ratios: tuple = (10, 1, 1), fast_render: bool = True, **kwargs)
Bases:
VideoBrowserInteractive video point annotator with DeepLabCut integration.
DUSTrack provides a GUI for manual annotation of points in videos, with integrated support for creating DeepLabCut projects, training models, and post-processing results using optical flow algorithms.
- Features:
Manual point annotation with event marking
Real-time trajectory visualization
DeepLabCut project creation and training
Optical flow-based jitter reduction
Multiple annotation layer management
- Variables:
_dlcproject (DLCProject) – Associated DeepLabCut project instance.
_ax_lims (dict) – Stores axis limits when plot axes are frozen.
Example
>>> # Most users should call :func:`dustrack.open` instead of >>> # constructing DUSTrack directly -- it dispatches to either >>> # this class (bare video) or :meth:`DLCProject.annotate` >>> # (resume in a DLC project) based on the path you pass. >>> # Direct construction below is for advanced / scripted use. >>> >>> # Basic usage -- annotation_names defaults to "iteration-0", >>> # the canonical seed name for the DLC pipeline (the next >>> # DLC training round lands as iteration-1). >>> tracker = DUSTrack('video.mp4') >>> >>> # Explicit layer name -- saved as {video_name}_annotations_pn.json >>> tracker = DUSTrack('video.mp4', "pn") >>> >>> # With multiple annotation layers >>> tracker = DUSTrack('video.mp4', { ... 'manual': 'manual_labels.json', ... 'dlc_iter1': 'dlc_predictions.h5' ... })
- CORRECTIONS_LAYER_NAME = 'dlccorr'
- __init__(vid_name, annotation_names='iteration-0', *args, clahe_clip=1.0, clahe_grid=8, gamma=1.0, brightness=0, dark_mode=False, n_labels: int = 1, titlefunc: Callable | None = None, height_ratios: tuple = (10, 1, 1), fast_render: bool = True, **kwargs)
- Parameters:
vid_name (str) – Path to the video file.
titlefunc (Optional[Callable]) – Function to generate the title for the plot.
figure_or_ax_handle (Optional[Union[plt.Axes, plt.Figure]]) – Handle to the figure or axis.
image_process_func (Callable) – Function to process the image.
fast_render (bool) – If True, render the video frame and any annotation scatter overlays Qt-native (QGraphicsView + QPixmapItem) instead of through matplotlib’s imshow. Bypasses the canvas pixmap-upload cost that dominates per-frame budget on QtAgg (see BENCHMARKING.md probe 13 / 14 for the architecture rationale). Requires a Qt matplotlib backend; silently falls back to Tier 1 if no Qt window is found. In fast_render mode, user additions to
_ax_imagevia mpl plotting calls are NOT supported (the image region is no longer an mpl Axes);_axand_imare set toNone.
- discard_unsaved_annotations(event=None)
Drop in-memory edits on the active layer and reload from disk.
Inverse of
save: confirms viaConfirmOverlay, then callsVideoAnnotation.reload()onself.annand triggersself.update()so the trace pane + scatter re-render. The confirm body branches on whether the layer’s backing file exists:File exists -> “Reload from disk” semantic.
File missing -> “Reset to empty” semantic.
Refuses on the
dlccorrsplice and any layer matching_is_dense_layer_name()– those are regenerated from the active + overlay layers (viaapply_manual_correctionsorprocess_with_lk), not authored by hand, so “discard and reload” has no meaningful semantic. Points the user atRemove layerinstead.
- freeze_plot_axes(event=None)
Lock the axis limits of trajectory plots to current view.
Useful for comparing tracking quality across different frames without automatic axis rescaling distracting from the comparison.
- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
- unfreeze_plot_axes(event=None)
Restore automatic axis scaling for trajectory plots.
- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
- create_dlc_project(event=None, name=None, path=None, experimenter='x', seed_bundle_path=None) DLCProject
Create a new DeepLabCut project using current annotations as training labels.
rc2 (1.1.0rc2): on a Qt backend, project creation runs off the GUI thread under a modal overlay (no progress bar – it’s a fast op, but the overlay surfaces DLC’s stdout and a Done button so the user can confirm the project location and any warnings before continuing). On non-Qt backends the call runs synchronously and returns the new
DLCProject.1.2.0a2: if the active manual layer is empty when the user clicks Create DLC Project on the Qt path, a multi-step Seed-from-bundle modal opens (
_prompt_seed_bundle()). Picking a valid bundle routes through a seeding flow that: (a) creates the project as usual, (b) callsimport_seed_bundle_into_project()to install the bundle’s snapshot as iteration-0’s trained model (overwriting the project’s bodyparts with the bundle’s), and (c) runsanalyze_videos(iteration_num=0)to produce a dense reference layer the user can refine into iteration-1.seed_bundle_pathmay also be passed programmatically to bypass the modal.- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
name (str, optional) – Project name. Defaults to “{video_name}_{annotation_layer}”.
path (str, optional) – Directory for project. Defaults to video’s parent directory.
experimenter (str, optional) – Experimenter name. Defaults to config value.
seed_bundle_path (str, optional) – Path to a seed bundle folder (as produced by
extract_snapshot_for_seeding()). When supplied, the seeding flow runs unconditionally (Qt or non-Qt), bypassing the empty-layer-triggered modal.
- Returns:
The newly created project instance on the sync path.
Noneon the Qt async path – readself._dlcprojectafter the Done button is clicked.- Return type:
Note
Project names must contain an underscore for proper DLC configuration handling.
- process_dlc_project(event=None, *args, **kwargs)
Train the DeepLabCut model without leaving the annotation UI.
rc2 (1.1.0rc2): training runs off the GUI thread, the DUSTrack window stays open under a “Training in progress” overlay (progress bar + scrolling stdout tail), and on success the new DLC prediction layers are added to the live session via
add_annotation_layers()– no relaunch. The overlay transitions to a “Complete” / “Failed” state with a Done button so the user can review the final stdout before the predictions swap in (or read the error before retrying); failure paths fold into the overlay rather than popping a separateQMessageBox.1.2.0a2: a Training options modal runs before everything else (Qt path only). It surfaces
DLCProject.train_iteration()’s arg surface in the UI: refine_mode (scratch / in-project / external), source iteration/snapshot picker for in-project, Browse… for external.pt, training epochs (DLC3) / iterations (DLC2), and a create-labeled-video toggle. Cancel returns to the UI without kicking off training. On accept, the underlying call routes throughDLCProject.train_iteration()(the explicit-args sibling ofDLCProject.process()) – so the DLC2 silent-drop bug onrefine=<str>is gone, and external snapshots work on both DLC versions (DLC3 viatrain_network(snapshot_path=...), DLC2 via a pose_cfginit_weightsedit).A unified pre-flight runs before the overlay starts, scanning every manual annotation layer in the session (file-pattern detection –
.jsonalongside the video,<video_stem>_annotations*.json, minusdlccorr/buffer/dlc*; agnostic to which layer is active, the overlay, or a placeholder, and to whether the user renamediteration-Nto something else). For each manual layer we report (a) in-memory diffs vs the on-disk JSON and (b) frames missing one or more bodyparts. If any layer has either kind of issue, a single modal lists the per-layer breakdown and offers two actions: Save and clean (write per-layer recovery sidecars for the dropped frames, drop the incomplete frames from every affected layer, then save every affected layer, then train) and Cancel (return to the UI). Layers without issues are not touched; DLC trace layers anddlccorrare not in scope. Recovery sidecars are written as<fstem>.dustrack-dropped-incomplete-<YYYYMMDDTHHMMSS>; the composite extension avoids.jsonso the annotation-discovery glob does not re-ingest them on subsequent training runs.DLC’s stdout/stderr are also teed to the launching terminal (
sys.__stdout__) so callers who launched from a shell can watch progress there as before.On non-Qt backends (or if the QMainWindow can’t be located), the method falls back to the pre-rc2 behavior: close the figure, run training synchronously on the calling thread, and return a fresh DUSTrack via
DLCProject.annotate(). The pre-flight modal is skipped on this path (no GUI to host it).- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
*args – Forwarded to
DLCProject.process()on the non-Qt fallback path; ignored on the Qt path (the Training options modal owns the kwarg surface there).**kwargs – Same – non-Qt fallback only.
create_videodefaults toFalseon the non-Qt path (vs.Truefor directDLCProject.process()calls) so the annotate -> train -> review -> annotate loop doesn’t write a labeled mp4 each pass; the Qt path’s create-labeled-video checkbox owns this on its own.
- Returns:
selfon the Qt path (training is asynchronous; the same DUSTrack will refresh in place when the user clicks Done). Alsoselfif the user cancels either the Training options modal or the pre-flight modal – the UI is left intact. On the fallback path, the freshly-launched DUSTrack fromDLCProject.annotate().- Return type:
- Raises:
ImportError – If
deeplabcutisn’t installed.ValueError – If no DLCProject has been created yet.
- process_with_lk(event=None, *args, **kwargs)
Apply Lucas-Kanade optical flow post-processing to reduce tracking jitter.
rc2 (1.1.0rc2): on a Qt backend, the LK-RSTC pass runs off the GUI thread under a progress overlay that mirrors the Train DLC flow (tqdm bars drive the progress widget; phase label reflects “Submitting” vs “Processing”; Done button lets the user confirm before the smoothed layer swaps in). On non-Qt backends the call runs synchronously and returns the new
VideoAnnotation.Uses the Lucas-Kanade RSTC (Reverse Sigmoid Tracking Correction) algorithm to smooth trajectories. The processed annotation is saved and added as a new layer, with the original set as overlay for comparison.
rc2 layer-naming harmonisation: the new layer is adopted via
_adopt_layer(), which derives its name from the output filepath viaVideoFileManager.canonical_layer_name()– identical to what a close + reopen would show. Replaces the earlier behaviour where the in-session layer briefly carried the"noname"fallback until reload. When the source layer is a DLC trace, the smoothed output is also plot-type-normalised to"line"so it looks identical to otherdlc_*layers.The source layer is save()-ed to disk right before LK kicks off (mirroring the pre-train save in
process_dlc_project()), so the on-disk state matches what LK sees. In the typical workflow the source is thedlccorrlayer (active afterapply_manual_corrections()) and the save persists any in-memory manual edits. Sources without a.jsonfilename (e.g. raw DLC traces loaded from.h5) are read-only inputs; the save is skipped for them with a one-line note.- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
*args – Additional arguments passed to lk_moving_average_filter.
**kwargs – Additional keyword arguments (e.g., window_size) passed to filter.
- Returns:
The smoothed annotation layer on the sync path.
Noneon the Qt async path – the new layer is added toself.annotationswhen the Done button is clicked.- Return type:
See also
dustrack.lk_filter.lk_moving_average_filter: The filtering algorithm.
- remove_current_layer(event=None)
Remove the active annotation layer from the DUSTrack session.
Session-only: the underlying JSON / HDF5 file on disk is not touched – so on next launch the layer reappears if its file is still next to the video. Pair with a manual file delete (or
Save annotation as...to a different name + delete the original) when the intent is “undo manual corrections”.Refuses with a notice if only one removable layer remains in the session (excluding the implicit
"buffer"layer, which is never user-visible but always present). Otherwise confirms viaConfirmOverlay; the confirm body is severity- aware via_is_dense_layer_name():Dense / derived (
dlc_*,dlccorr,*lkmovavg*): regenerable, default button = Remove.Sparse / authored (manual labels): irreversible, default button = Cancel.
- copy_existing_annotations_from_overlay(event=None)
Copy overlay annotation points to the current annotation layer for selected frames.
Useful when DLC predictions are more accurate than manual labels. Typically used with manual annotations in the primary annotation layer, and the model predictions in the overlay layer. Data is copied only for frames that exist in the primary annotation layer. Perform this action within a specified frame range by selecting an interval.
- Parameters:
event – Mouse/keyboard event (unused, for button compatibility).
- Raises:
ValueError – If no annotation overlay is currently selected.
Note
Only affects the current label. Other labels remain unchanged.
- MANUAL_CORRECTIONS_SUFFIX = '_manual_corrections'
- apply_manual_corrections(event=None)
Splice the active layer’s manual entries into the overlay to produce/refresh the
dlccorrlayer.Workflow context (step 4 of the DUSTrack pipeline): after iterating DLC training (steps 2-3), you flip into a manual-correction mode. The active annotation layer holds sparse hand-edits (a few frames where DLC was wrong); the annotation overlay points at the DLC trace you want to correct. Clicking this button produces a layer named
CORRECTIONS_LAYER_NAME("dlccorr") that is the overlay’s data with your manual entries spliced in wherever they exist. The file lives next to the video as<video>_annotations_dlccorr.jsonand is automatically excluded from DLC training input (the_dlccorrfilter in_extract_frames()) – this is a terminal output.Preflight save. If the active (patch) layer has any unsaved in-memory edits, they are written to disk before the splice runs. The user has explicitly clicked Apply, so “intent to commit” is signalled; saving the source as a side effect keeps the on-disk state coherent with
dlccorr(which is always saved by this method).Source-layer rename. On a successful splice the patch layer is renamed in place from
<old_name>to<old_name>_manual_corrections(the file is moved on disk too, old file deleted). The rename marks “this is the layer whose manual entries the on-diskdlccorrwas spliced from” so the relationship survives reload. No-op if the patch is already named*_manual_corrections(idempotent re-apply).Post-apply state. The corrections layer becomes the active annotation layer and the (now-renamed) manual layer becomes the overlay so you can see where your hand was. To iterate, switch the active layer back to your manual layer, set the overlay back to the DLC trace, add more points, click again. Each click regenerates the corrections layer from the current
(overlay, active)pair, so adding annotations directly to the corrections layer is not recommended – they’ll be discarded on the next apply.Idempotency. If a
dlccorrlayer is already present in the session, its in-memory data is replaced wholesale (with a_revisionbump so the trace cache invalidates) and the file overwritten. Otherwise a freshVideoAnnotationis built, saved, and adopted under the canonical machinery.- Parameters:
event – Mouse/keyboard event (unused, for button compat).
- Raises:
ValueError – If no annotation overlay is currently set, or if the active annotation layer is already the corrections layer (would create a circular splice).
- save_annotation_as(event=None)
Save the active annotation layer to a user-chosen path.
Opens a Qt save-file dialog seeded with the video’s folder and a suggested filename of
<video_stem>_annotations_<layer>.json. Falls back toself.ann.save()(which writes to the layer’s existing.fname) when no Qt window is available – e.g. headless / Agg backend.
- swap_active_and_overlay(event=None)
Swap the active annotation layer with the overlay layer.
No-op if no overlay is currently selected.
- update()
Update the display with current frame and maintain frozen axis limits if set.
- Returns:
Result from parent class update() method.
- swap_to(index: int) bool
Switch the active video to
self._bundles[index]. See_bundle_swap.swap_to().
- swap_prev(event=None) bool
Move to the previous bundle (no-op at index 0).
Connected to the sidebar’s
◀button and theAlt+Leftkeybinding. Returns the underlyingswap_to()result so keybinding-handler callers can short-circuit if desired.
- swap_next(event=None) bool
Move to the next bundle (no-op at last index).
Connected to the sidebar’s
▶button and theAlt+Rightkeybinding.
- add_video(path_or_paths, *, layer_name=None, set_active=False, **dustrack_kwargs) list[int]
Append one or more videos to this tracker’s bundle list. See
_bundle_swap.add_video().
- remove_video(index: int) bool
Drop a bundle from the tracker’s bundle list. See
_bundle_swap.remove_video().
- replace_active_with(path_or_paths, *, layer_name=None, **dustrack_kwargs) list[int]
Swap the active bundle for newly-picked video(s). See
_bundle_swap.replace_active_with().
- classmethod from_annotations(annotations: list[dustrack.annotations.VideoAnnotation], *args, **kwargs) DUSTrack
- add_annotation_layers(annotation_names: list[str] | dict[str, pathlib.Path] | list[dustrack.annotations.VideoAnnotation], n_labels: int = 1) None
Load data from annotation files if they exist, otherwise initialize annotation layers.
- remove_annotation_layer(name: str) None
Remove an annotation layer from the active session.
Tears down the layer’s plot artists (scatter + per-label trace lines on x/y trace axes) via
VideoAnnotation.clear_display(), drops the layer fromannotationsviaAssetContainer.remove(), then resyncs theannotation_layer/annotation_overlaystatevariables through_refresh_annotation_state_lists()so the dropdown rotations + current selections stay valid.Pre-flight: -
namemust be an existing layer name. - Refuses if it would leave the container empty (callers needinga “reset to single empty layer” semantic should use
VideoAnnotation.reload()on the surviving layer instead).Active-layer handoff: if
nameis currently the primary layer (annotation_layer.current_state), the previous layer in the rotation becomes the new primary (or the first one if the removed layer was at index 0). Ifnameis currently the overlay, the overlay clears toNone.Note:
"buffer"is not excluded here – the dnav layer treats every named layer the same. Consumers (DUSTrack) enforce buffer-exclusion at the UI layer.
- set_key_bindings() None
Set the keyboard actions.
Groups mirror the 5-step workflow in
docs/source/resources/ keyboard_shortcuts.png: layer selection -> label selection -> frame navigation -> edit -> refine.self._section_orderpins the cheatsheet’s section order to the workflow order (section 3 first, then 1, 2, 4, 5a, 5b, 5c) regardless of when each binding was originally registered. Bindings not on the PNG (save, refresh, reset view, pan, keep-overlapping, toggle-num-keys mode) fall through to the “Other” section.
- refresh(event: Any | None = None) None
Force-refresh the UI from the current annotation
.data.Drops the trace-display cache on every annotation layer and the frame-marker cache, then calls
update()so the next draw re-reads every value from the backing dicts.Recovery path for the rare case where
.datawas mutated directly (e.g. from an IPython prompt:v.ann.data["0"][42] = [x, y]) bypassing the publicadd()/remove()/add_at_frame()API and therefore skipping the_revisionbump thatVideoAnnotation.update_display_trace()andupdate_frame_marker()cache on. In normal in-code mutation paths the public API bumps_revisionand the caches invalidate automatically; callingrefresh()is always safe but rarely necessary.
- add_events() None
Add an event to specify time intervals for interpolating with lucas-kanade.
- property ann: VideoAnnotation
Return current annotation layer.
- set_image_background_color(color: Any) None
Set the background color of the image region.
Tier 1 routes to
self._ax_image.set_facecolor(matplotlib); Tier 2 routes to the Qt image pane’sset_background_color. The single-method surface lets consumers (DUSTrack’s_apply_dark_theme) issue one call regardless of which tier is active.
- update_annotation_visibility(draw: bool = False) None
Update the visibility of all annotation layers, for example, when the layer is changed.
- update_frame_marker(draw: bool = False) None
Update the current frame location in the trace plots.
The trace ylim / FOI tick positions are a pure function of the active label + per-annotation contents + frames_of_interest, none of which change when only
_current_idxmoves. Cache them keyed on a cheap tuple of (label, annotation revisions, frames_of_interest) so per-frame navigation only does the frame-marker set_data calls.
- copy_annotations_from_overlay() None
Copy annotations from the overlay layer into the current layer in the current frame.
- copy_current_annotation_from_overlay() None
Copy annotations from the overlay layer into the current layer.
- copy_frames_of_interest_from_overlay() None
copy annotations at frames of interest from buffer into the current layer. If there is no buffer, then copy from the overlay layer.
- copy_frames_in_interval_from_overlay() None
For the current label only.
- add_annotation(event: Any) None
Add annotation at frame. If it exists, it’ll get overwritten.
- remove_annotation(event: Any | None = None) None
remove annotation at the current frame if it exists
- previous_frame_with_current_label(event: Any | None = None) None
Go to the previous frame with the current label in the current annotation layer.
- next_frame_with_current_label(event: Any | None = None) None
Go to the next frame with the current label in the current annotation layer.
- previous_frame_with_any_label(event: Any | None = None) None
Go to the previous frame with any label in the current annotation layer.
- next_frame_with_any_label(event: Any | None = None) None
Go to the next frame with any label in the current annotation layer.
- previous_frame_of_interest(event: Any | None = None) None
Go to the previous frame of interest.
- next_frame_of_interest(event: Any | None = None) None
- previous_annotation_layer() None
Go to the previous annotation layer.
- next_annotation_layer() None
Go to the next annotation layer
- previous_annotation_overlay() None
Go to the previous annotation overlay layer.
- next_annotation_overlay() None
Go to the next annotation overlay layer.
- previous_annotation_label() None
Set current annotation label to the previous one.
- next_annotation_label() None
Set current annotation label to the next one.
- update_annotation_label_states() None
Update the states of the annotation label state variable.
- cycle_number_keys_behavior() None
Number keys can be used to either select labels, or place a specific label. Toggle between these two behaviors.
- increment_label_range() None
Increment the label range by 10.
- decrement_label_range() None
Increment the label range by 10.
- increment_if_unannotated(event: Any | None = None) None
Advance the frame if the current frame doesn’t have any annotations. Useful to pause at annotated frames when adding a new label.
- decrement_if_unannotated(event: Any | None = None) None
Go to the previous frame if the current frame doesn’t have any annotations. Useful to pause at annotated frames when adding a new label.
- save() None
Save current annotation layer json file.
- select_label_with_mouse(event: Any) None
Select a label by clicking on it with the left mousebutton.
- place_label_with_mouse(event: Any) None
Place the selected label with the right mousebutton.
- go_to_frame(event: Any) None
- toggle_frame_of_interest(event: Any) None
Mark/unmark the current frame as a frame of interest
- keep_overlapping_continuous_frames() None
- keep_overlapping_frames() None
- predict_labels_with_lucas_kanade(labels: str | list[str] = 'all', start_frame: int | None = None, mode: str = 'full') Any
Compute the location of labels at the current frame using Lucas-Kanade algorithm.
- get_selected_interval() tuple[int, int]
- remove_labels_in_interval(all_labels: bool = False) None
- decimate_annotations_in_interval() None
Prune incomplete frames, then drop every other complete frame in the selected interval.
Prep step for training: ensures the surviving in-interval frames are (a) fully annotated across the required-label set and (b) half as dense (even-stride sampling). Frame-level – every label is removed at each dropped frame.
Required-label set follows the same project-aware / project-unaware split the Train preflight uses (
_preflight.scan_incomplete_frames()):DLC project loaded – required = project
bodyparts(mapped through_dlc_bodyparts_to_layer_labels()). Stray non-bodypart labels on the layer are ignored from the required check, but any frame they sit on is still part ofall_frames– if it’s missing a required label it still counts as incomplete, and pruning cleans the stray with it.No DLC project – required = labels with at least one annotation. Empty labels are treated as UI placeholders (user created a slot and abandoned it). This is the best inference available without external truth and matches the project-unaware Train preflight rule.
Starter form of the “general-model workflow” decimation feature; the DINOv3-feature farthest-point-sampling variant is deferred. Incomplete frames in the interval are always pruned. The every-other halving is a no-op when fewer than 2 complete frames remain in the interval after pruning.
- interpolate_with_lk(all_labels: bool = False) None
Interpolate with lk-rstc between selected interval. Use data in the overlay layer as start and end points. Add interpolated points to the current layer.
- interpolate_with_lk_norstc(all_labels: bool = False) None
Infer the motion of the current label using lucas-kanade algorithm in the selected interval.
- check_labels_with_lk(mode: str = 'minimal') None
Interpolate between all labeled frames. This only makes sense for sparse-labeled annotations. Use this when doing first-time annotations (as opposed to refinement).
I am testing if refining the start labels using this strategy, and augmenting the training data will improve deeplabcut tracking!
- Parameters:
mode (str, optional) – “all” - LK-interpolation for all labels across all labeled frames “current” - LK-interpolation for current label across all labeled frames “minimal” - LK-interpolation for current label between labeled frames near the current frame (two previous to two next) Defaults to “minimal”.
- render(start_frame: int, end_frame: int, out_vid_name: str | None = None) None
Render the video with annotations.
Deep learning
Interface class for using ResNets via DeepLabCut.
- class dustrack.dlcinterface.DLCProject(path, videos=[], name='test_01', experimenter='x', annotation_suffix='', internal_to_dlc_labels: dict = None)
Bases:
objectInterface to deeplabcut training and inference Current workflow:
- Create a project with some videos. Videos will be copied.
d = DLCProject(r’C:/data_opr02/004_02/ml_models/dlc’, name=’opr02_s004_muscles’, experimenter=’praneeth’, videos=[<video_list>])
- Launch the initial annnotator for video 0, repeat if there are more videos
d.annotate(0)
- Extract frames, train network, evaluate network, analyze videos, and create labeled video
d.process()
- Refine the labels
d.annotate(0, ‘praneeth_2’) # the second argument determines the suffix for the annotations file. CAUTION: Make sure that the files are read by extract_frames in the correct order! Pay attention to the output of this method.
- Re-train network with refined labels
d.process()
Repeat steps 4 and 5 until satisfied with the results.
- __init__(path, videos=[], name='test_01', experimenter='x', annotation_suffix='', internal_to_dlc_labels: dict = None)
Initialize or load a DeepLabCut project.
If a config.yaml exists at the path, loads the existing project. Otherwise, creates a new project with the provided videos.
- Parameters:
path (str) – Directory containing or for the project.
videos (list) – List of video file paths to include.
name (str) – Project name (must contain underscore for proper config handling).
experimenter (str) – Experimenter identifier.
annotation_suffix (str) – Suffix for annotation files (e.g., ‘manual’, ‘refined’).
internal_to_dlc_labels (dict, optional) – Custom label name mapping.
Note
Videos are copied into the project folder by default. Project names without underscores may cause config issues with network paths.
- property paths: Mapping[str, Path]
Full paths to project folder and standard DLC subfolders.
- Returns:
- Mapping of folder names to Path objects with keys:
’project’: Main project directory
’models’: Trained model weights (dlc-models or dlc-models-pytorch)
’results’: Evaluation results
’labels’: Labeled frame data
’training_data’: Training datasets
’videos’: Video files
- Return type:
dict
- property config: dict
Current project configuration dictionary.
- Returns:
Parsed contents of config.yaml.
- Return type:
dict
- property name: str
Project name from configuration.
- property trackers: list
Names of tracked body parts as used internally by DLC.
- Returns:
Body part names (e.g., [‘point0’, ‘point1’]).
- Return type:
list
- property label_names: list
Human-readable names for tracked points.
Returns meaningful names from dlc_trackermap.txt if available, otherwise returns the internal tracker names.
- Returns:
Display names for body parts.
- Return type:
list
- property trackermap
Load meaningful label names from dlc_trackermap.txt.
This file maps internal names (point0, point1) to biological names (nose, left_ear, etc.) for better interpretability.
- Returns:
Mapping from internal names to display names.
- Return type:
dict
- Example dlc_trackermap.txt content:
point0 - muscle_boundary point1 - fascia point2 - bone
- edit_config(config_file=None, **kwargs)
Modify project configuration parameters.
- Parameters:
config_file (str, optional) – Path to config file. Defaults to main config.
**kwargs – Configuration parameters to update (e.g., iteration=2, snapshotindex=5).
- Returns:
Result of deeplabcut.auxiliaryfunctions.edit_config().
- property video_list: list[pathlib.Path]
Full paths to videos in the project.
- property video_names: list[str]
Video filenames without extensions.
- property current_iteration: int
Model iteration number currently set in config.yaml.
- property latest_iteration: int
Highest iteration number in dlc-models folder.
- property latest_trained_iteration: int
Most recent iteration that has saved model snapshots.
- property all_iterations: list
All iteration numbers found in dlc-models, sorted ascending.
- property all_snapshots: Mapping[int, list[int]]
Training snapshots for each model iteration.
- Returns:
- Maps iteration number to list of training iteration numbers.
For DLC3, identifies .pt files; for DLC2, identifies .index files.
- Return type:
dict
- current_iteration_is_trained() bool
Check if current iteration has any saved snapshots.
- latest_iteration_is_trained() bool
Check if latest iteration has any saved snapshots.
- iteration_is_trained(iteration_num: int) bool
Check if a specific iteration has been trained.
- Parameters:
iteration_num (int) – Model iteration to check.
- Returns:
True if snapshots exist for this iteration.
- Return type:
bool
- increment_iteration()
Advance to next iteration if current one is trained.
- Returns:
For method chaining.
- Return type:
self
- add_videos(videos: list[pathlib.Path])
Add new videos to existing project and copy their annotations.
- Parameters:
videos – List of video file paths to add.
- Returns:
For method chaining.
- Return type:
self
- copy_annotations(video_name: Path | list)
Copy DUSTrack JSON files into project’s video folder.
- Parameters:
video_name – Single video path or list of video paths.
- Returns:
Path(s) to copied annotation file(s), or None if not found.
- Return type:
str or list
Note
Looks for files matching {video_stem}_annotations_{suffix}.json
- extract_frames(annotation_file_names=None, suffix_merged='merged', save_merged_json=False, check=False)
Extract labeled frames from videos and convert annotations to DLC format.
This method: 1. Finds all annotation JSON files for each video 2. Merges multiple annotation files if present 3. Extracts the annotated frames from videos 4. Converts annotations to DLC’s CSV/HDF5 format in labeled-data folder
- Parameters:
annotation_file_names (list, optional) – Specific annotation files to use. If None, automatically finds all matching files.
suffix_merged (str) – Suffix for merged annotation file. Defaults to ‘merged’.
save_merged_json (bool) – Whether to save the merged JSON. Defaults to False.
check (bool) – Whether to run deeplabcut.check_labels(). Defaults to False.
- Returns:
For method chaining.
- Return type:
self
Note
Automatically excludes files with ‘_dlccorr’ suffix (correction files).
- get_pose_cfg_file(iteration_num: int = None, type_: str = 'train') Path
Get path to pose configuration file for an iteration.
- Parameters:
iteration_num (int, optional) – Iteration number. Defaults to current.
type (str) – ‘train’ or ‘test’ subfolder. Defaults to ‘train’.
- Returns:
Full path to pose_cfg.yaml (DLC2) or pytorch_config (DLC3).
- Return type:
Path
- get_best_snapshot(iteration_num: int = None) int
Find training iteration with lowest test error.
For DLC3, uses the snapshot marked as ‘best’ unless DLC3_USE_LAST_SNAPSHOT is True in config, in which case returns the last snapshot. For DLC2, parses CombinedEvaluation-results.csv.
- Parameters:
iteration_num (int, optional) – Model iteration. Defaults to current.
- Returns:
Training iteration number of best snapshot.
- Return type:
int
- get_best_snapshot_test_error(iteration_num: int = None) float
Get test error (RMSE in pixels) at best snapshot.
- Parameters:
iteration_num (int, optional) – Model iteration. Defaults to latest trained.
- Returns:
Test error in pixels, or -1.0 if evaluation file doesn’t exist.
- Return type:
float
- get_best_snapshot_idx(iteration_num: int = None) int
Get snapshot index (not training iteration number) of best snapshot.
- Parameters:
iteration_num (int, optional) – Model iteration. Defaults to current.
- Returns:
Index in the all_snapshots list for this iteration.
- Return type:
int
- initialize_weights(source_iteration: int = None, source_snapshot: int = None, dest_iteration: int = None)
Initialize model weights from a previous iteration (transfer learning).
Used when refining a model with additional labels. Edits the pose_cfg file to set init_weights parameter.
- Parameters:
source_iteration (int, optional) – Iteration to copy from. Defaults to second-to-last iteration.
source_snapshot (int, optional) – Training iteration within source_iteration. Defaults to best snapshot.
dest_iteration (int, optional) – Iteration to initialize. Defaults to latest iteration.
- Returns:
For method chaining.
- Return type:
self
Note
Does nothing if there’s only one iteration (no source to copy from).
- create_training_dataset(**kwargs)
Call deeplabcut.create_training_dataset.
- train(**kwargs)
Train the neural network model.
Sets custom learning rate schedule and trains with more iterations than DLC defaults for better convergence.
- Parameters:
**kwargs – Passed to deeplabcut.train_network(). - maxiters (int): Total training iterations. Default: 500000 (DLC2) or 1000 (DLC3 epochs). - max_snapshots_to_keep (int): Max saved checkpoints. Default: 20.
- Returns:
For method chaining.
- Return type:
self
Note
Custom learning rate schedule: [0.005@10k, 0.02@350k, 0.002@425k, 0.001@1M]
- evaluate(**kwargs)
Evaluate all training snapshots on test set.
Temporarily sets snapshotindex to ‘all’ to evaluate every checkpoint, then restores original value.
- Parameters:
**kwargs – Passed to deeplabcut.evaluate_network().
- Returns:
For method chaining.
- Return type:
self
- analyze_videos(iteration_num=None, snapshotindex=None, create_video=True, **kwargs)
Run inference on videos and optionally create labeled output videos.
- Parameters:
iteration_num (int, optional) – Model iteration to use. Defaults to current.
snapshotindex (int, optional) – Snapshot index to use. Defaults to best snapshot. Negative indices supported.
create_video (bool) – Whether to create labeled video. Defaults to True.
**kwargs – Additional arguments for deeplabcut.analyze_videos(). - videos: List of video paths or indices. If not provided, analyzes all videos.
- Returns:
For method chaining.
- Return type:
self
Note
Results saved to videos/iteration-{N}/ subfolder. If videos kwarg contains integers, they’re treated as indices into self.video_list.
- process(iteration_num=None, maxiters=None, refine: bool | str = True, create_video=True, source_snapshot=None, **kwargs)
Automated workflow: extract frames, train, evaluate, and analyze.
This is the main method for handling the full DLC pipeline. It intelligently decides what steps to run based on the current project state: - If iteration already evaluated: just analyze videos - If frames need extraction: extract them - If not trained: train the model - If refining: initialize weights from previous iteration
- Parameters:
iteration_num (int or str, optional) – Iteration to process. Can be integer or ‘latest’. Defaults to ‘latest’.
maxiters (int, optional) – Training iterations. Defaults: 500000 (DLC2) or 1000 epochs (DLC3).
refine (bool) – Use transfer learning from previous iteration. Defaults to True.
create_video (bool) – Create labeled output video. Defaults to True.
source_snapshot (int, optional) – Specific snapshot for weight initialization.
**kwargs – Additional arguments. - videos: List of videos to analyze (can be indices or paths).
- Returns:
For method chaining.
- Return type:
self
Example
>>> proj = DLCProject('path/to/project') >>> proj.process() # Full automated workflow
- train_iteration(*, refine_mode: Literal['scratch', 'in_project', 'external'] = 'scratch', source_iteration: int = None, source_snapshot: int = None, external_snapshot_path: str = None, maxiters: int = None, create_video: bool = False, videos: list = None, analyze_batchsize: int = None)
Explicit-args training driver for UI-triggered flows.
Distinct from
process()(auto-infer for CLI ergonomics). Caller decides everything: refine source, training duration, output options. Strict validation perrefine_mode; no inference and no silent fallbacks.The mechanics of advancing iterations (extract_frames → increment_iteration if latest is trained → create_training_dataset if needed) mirror
process(); the only difference is how weights are initialised once the destination iteration is in place.- Parameters:
refine_mode – How to initialise weights for the next training round.
"scratch"starts from random init (no pose_cfg edit);"in_project"copies weights from a snapshot in this project (requiressource_iteration, optionalsource_snapshot);"external"initialises from an arbitrary snapshot file (requiresexternal_snapshot_path; supported on both DLC2 and DLC3 – DLC3 passes the path throughtrain_network(snapshot_path=...), DLC2 edits pose_cfg’sinit_weightsvia_initialize_weights_from_external_path()).source_iteration – in-project iteration to copy weights from. Only valid with
refine_mode="in_project"; must point at a trained iteration.source_snapshot – specific snapshot within
source_iteration. Only valid withrefine_mode="in_project"; defaults to the best snapshot whenNone.external_snapshot_path – path to an external snapshot file (
.pton DLC3,.indexon DLC2). Only valid withrefine_mode="external"; the file must exist at call time.maxiters – training epochs (DLC3) or iterations (DLC2). Defaults to the same values
process()uses (50 / 500000) so the two methods stay consistent until the UI exposes the field.create_video – write a labeled output video after analyze. Defaults to
False(the UI ergonomics default;process()defaults toTruefor CLI parity).videos – list of videos (indices or paths) to analyze. Forwarded to
analyze_videos.Noneanalyses every video in the project.analyze_batchsize – batchsize for
analyze_videos. Forwarded on if set;Noneletsanalyze_videospick its own default (post-2026-05-20: rc14 knee at 4).
- Returns:
For method chaining.
- Return type:
self
- Raises:
ValueError – on refine_mode / argument mismatch (see
_validate_train_iteration_args()).TypeError – if
source_iteration/source_snapshotaren’tintwhen given.FileNotFoundError – if
external_snapshot_pathis set but the file doesn’t exist.
- annotate(video_index: int = 0, new_annotation_suffix=None, **dustrack_kwargs)
Launch interactive annotation GUI for a video.
Opens DUSTrack interface with existing annotation layers loaded, including any DLC predictions as line plot overlays.
- Parameters:
video_index (int) – Index of video in video_list. Defaults to 0. Negative indices supported.
new_annotation_suffix (str, optional) – Suffix for new annotation layer. Defaults to ‘iteration-{N}’ where N is the next iteration number.
**dustrack_kwargs – Forwarded to the DUSTrack constructor. Notable pass-through options:
fast_render=True(datanavigator 1.5.0+ Tier 2 Qt-native video pane, ~3x speedup on the interosseous_pn24-x benchmark),dark_mode=True,clahe_clip,clahe_grid,gamma,brightness.
- Returns:
Interactive annotation interface.
- Return type:
Note
Creates a ‘buffer’ layer for temporary annotations. Latest DLC predictions are automatically set as overlay.
- get_trajectories(videos=None, iteration=None)
Load tracking results as DLCData objects.
- Parameters:
videos (list or str, optional) – Videos to load. Defaults to all videos.
iteration (int, optional) – Model iteration. Defaults to current.
- Returns:
Maps video stem to DLCData object.
- Return type:
dict
- Raises:
ValueError – If a requested video is not in the project.
- open()
Open project folder in Windows Explorer.
Optical flow
Module for optical flow based postprocessing using the LK-RSTC algorithm.
Post-processing module for video annotations using optical flow algorithms, typically applied on the output of deep learing models when the results have frame to frame jitter.
The primary algorithm is Lucas-Kanade optical flow with Reverse Sigmoid Tracking Correction (RSTC) applied in a “transposed” moving average window.
- Key Features:
Lucas-Kanade optical flow tracking for point trajectories
Reverse Sigmoid Tracking Correction (RSTC) guarantees the locations of the start and end points
Moving average filter over time windows
Parallel processing support for faster computation
Video frame buffering to minimize disk I/O
- Typical Usage:
>>> from dustrack.lk_filter import lk_moving_average_filter >>> from dustrack import VideoAnnotation >>> >>> # Load annotations >>> ann = VideoAnnotation('video_annotations.json', 'video.mp4') >>> >>> # Apply post-processing with 0.5 second window >>> ann_smooth = lk_moving_average_filter(ann, window_size=0.5)
- In practice, access the lk_moving_average_filter method via
VideoAnnotation: >>> from dustrack import VideoAnnotation >>> ann = VideoAnnotation('video_dlc_predictions.h5', 'video.mp4') >>> ann_rstc = ann.postprocess()
- Algorithm Overview:
The RSTC algorithm tracks points both forward and backward in time, then combines the two trajectories using sigmoid weights that emphasize the forward path at the start and the backward path at the end. This reduces drift accumulation compared to pure forward tracking.
The moving average applies RSTC over overlapping time windows and averages the results, further reducing noise while maintaining temporal coherence.
- Performance:
Processing speed depends on: - Video resolution and frame rate - Number of tracked points - Window size (larger = more computation) - Parallel processing (use_parallel=True recommended)
Typical performance: ~5-10 fps for 1080p video with 10 points and 0.5s window.
- Reference for the LK-RSTC Algorithm:
Magana-Salgado, U., Namburi, P., Feigin-Almon, M., Pallares-Lopez, R., & Anthony, B. (2023). A comparison of point-tracking algorithms in ultrasound videos from the upper limb. BioMedical Engineering OnLine, 22(1), 52.
- dustrack.lk_filter.gray(video_frame: ndarray) ndarray
Convert a color video frame to grayscale.
The Lucas-Kanade algorithm operates on grayscale images. Input frames come from dnav
VideoReader(PyAV+TOC). Since 1.5.0a2, dnav auto-detects monochrome-encoded sources (h265pix_fmt=gray) and decodes them directly as (H, W) gray ndarrays, skipping the YUV->RGB color matrix. On those inputs this helper is a no-op short-circuit. On the legacy color-source path the frame arrives as RGB and is converted via BT.601COLOR_RGB2GRAY(Y = 0.299 R + 0.587 G + 0.114 B).- Parameters:
video_frame (np.ndarray) – Input frame – (H, W, 3) RGB or (H, W) gray.
- Returns:
Grayscale frame (H, W).
- Return type:
np.ndarray
Pre-2026-05-21 this used
COLOR_BGR2GRAYon the same RGB input – functionally still grayscale, but it swapped the R and B coefficients, computingY = 0.114 R + 0.587 G + 0.299 B. Self-consistent withinlk_moving_average_filter(both template and search frames went through the same wrong conversion, so LK gradient + iteration math worked fine), but it diverged fromdustrack.lk_opticalflowwhich always usedCOLOR_RGB2GRAY. Unified to RGB2GRAY in the 1.2.0a2 perf pass.
- dustrack.lk_filter.lucas_kanade_2(frame_list: list, init_points: ndarray, **lk_config) ndarray
Track points through a sequence of frames using Lucas-Kanade optical flow.
This implementation uses OpenCV’s pyramidal Lucas-Kanade method to track points from the initial position through all subsequent frames. Tracking is performed in a single forward pass.
- Parameters:
frame_list (list) – List of grayscale video frames (each frame is np.ndarray).
init_points (np.ndarray) – Initial point positions, shape (n_points, 2) where each row is [x, y] in pixel coordinates.
**lk_config – Configuration parameters for cv.calcOpticalFlowPyrLK(): - winSize (tuple): Window size for search. Default: (45, 45) - maxLevel (int): Pyramid levels. Default: 2 - criteria (tuple): Termination criteria. Default: (EPS|COUNT, 10, 0.03)
- Returns:
- Tracked point trajectories, shape (n_frames, n_points, 2).
Missing/lost points are filled with NaN.
- Return type:
np.ndarray
Note
Larger winSize provides more robustness but slower computation. Higher maxLevel helps track larger motions but may lose precision.
Example
>>> frames = [gray(f) for f in video_frames] >>> initial_pts = np.array([[100, 200], [150, 250]]) >>> trajectories = lucas_kanade_2(frames, initial_pts) >>> # trajectories.shape = (len(frames), 2, 2)
- dustrack.lk_filter.compute_sigmoid_weights(n_frames: int, epsilon: float = 0.01) tuple[numpy.ndarray, numpy.ndarray]
Compute forward and reverse sigmoid weights for RSTC blending.
The weights control how forward and backward tracking paths are combined: - At the start: forward weight ≈ 1, reverse weight ≈ 0 - At the end: forward weight ≈ 0, reverse weight ≈ 1 - In the middle: both weights ≈ 0.5
- Parameters:
n_frames (int) – Number of frames.
epsilon (float, optional) – Small value to control sigmoid scaling. Defaults to 0.01.
- Returns:
- Two arrays of shape (n_frames,):
sigmoid_forward: Decreasing weights from ~1 to ~0
sigmoid_reverse: Increasing weights from ~0 to ~1
Forward and reverse weights sum to 1.0 at each frame.
- Return type:
tuple[np.ndarray, np.ndarray]
- Mathematical Form:
sigmoid(x) = 1 / (1 + exp(b * (x - c))) where b controls steepness and c is the center point.
- dustrack.lk_filter.lucas_kanade_rstc_2(frame_list: list, start_points: ndarray, end_points: ndarray, sigmoid_forward: ndarray = None, sigmoid_reverse: ndarray = None, **lk_config) ndarray
Apply Reverse Sigmoid Tracking Correction (RSTC) for robust point tracking.
RSTC combines forward tracking (from start_points) and backward tracking (from end_points) using sigmoid weights. This reduces error accumulation compared to pure forward tracking, especially over long sequences.
- Algorithm:
Track forward from start_points through all frames
Track backward from end_points through all frames (in reverse)
Blend the two paths using sigmoid weights: result = forward_path * sigmoid_forward + reverse_path * sigmoid_reverse
- Parameters:
frame_list (list) – List of grayscale video frames.
start_points (np.ndarray) – Initial positions at first frame, shape (n_points, 2).
end_points (np.ndarray) – Final positions at last frame, shape (n_points, 2). Should match start_points for consistency.
sigmoid_forward (np.ndarray, optional) – Forward weights. If None, computed automatically.
sigmoid_reverse (np.ndarray, optional) – Reverse weights. If None, computed automatically.
**lk_config – Lucas-Kanade configuration (passed to lucas_kanade_2).
- Returns:
Blended tracking paths, shape (n_frames, n_points, 2).
- Return type:
np.ndarray
Note
Precomputing sigmoid weights and reusing them across windows significantly improves performance when processing multiple sequences of the same length.
Example
>>> frames = [gray(f) for f in video_frames[10:20]] >>> start = np.array([[100, 200]]) >>> end = np.array([[105, 205]]) # Approximate end position >>> trajectory = lucas_kanade_rstc_2(frames, start, end)
- dustrack.lk_filter.process_window(video_frame_buffer, start_points, end_points, sigmoid_forward, sigmoid_reverse)
Process a single time window using RSTC.
This is a helper function designed for parallel execution by ThreadPoolExecutor. It wraps lucas_kanade_rstc_2 with precomputed sigmoid weights.
- Parameters:
video_frame_buffer – Deque or list of grayscale frames for this window.
start_points (np.ndarray) – Point positions at window start.
end_points (np.ndarray) – Point positions at window end.
sigmoid_forward (np.ndarray) – Precomputed forward weights.
sigmoid_reverse (np.ndarray) – Precomputed reverse weights.
- Returns:
Tracked trajectories for this window, shape (window_frames, n_points, 2).
- Return type:
np.ndarray
- dustrack.lk_filter.lk_moving_average_filter(tracked_points: str | VideoAnnotation, video_name: str = None, window_size: float = 0.5, use_parallel: bool = True, save_raw: bool = True) VideoAnnotation
Apply Lucas-Kanade moving average filter to smooth tracking annotations.
This is the main post-processing function. It applies RSTC over overlapping time windows and averages the results to produce smooth, jitter-free trajectories.
- Workflow:
Load video and annotations
Slide a time window through the video
For each window: - Apply RSTC to get smoothed trajectory - Store results indexed by window position
Average all trajectories that cover each frame
Save smoothed annotations
- Parameters:
tracked_points (Union[str, VideoAnnotation]) – Either: - Path to annotation JSON file - VideoAnnotation object with tracking data
video_name (str, optional) – Video path. Defaults to None; required when
tracked_pointsis a file path (asserted at call time). Ignored whentracked_pointsis already aVideoAnnotation.window_size (float, optional) – Time window in seconds for moving average. Larger windows provide more smoothing but increase computation. Typical range: 0.1 to 1.0 seconds. Defaults to 0.5.
use_parallel (bool, optional) – Use ThreadPoolExecutor for parallel processing. Significantly faster for longer videos. Defaults to True.
save_raw (bool, optional) – If True (default), allocate the full
(n_window_frames, n_total_frames, n_labels, 2)per-window RSTC array and dill-pickle it to{stem}_lkmovavg_{window_size}.pklalongside the averaged JSON. Per-window data is consumed downstream bypn-projects/wobbleandgaitmusic(their.rawlkproperty →lk_gradientsvelocity estimation), so keep True for those flows. If False, stream a running(n_total_frames, n_labels, 2)sum and per-cell count instead, divide at the end, and skip the .pkl write. Cuts peak memory fromW*N*L*16bytes toN*L*12bytes (~10x on typical W=15, L=4 cases). On the example-video bench wall time is essentially unchanged (post-processing is <0.3% of total) but on real-world long videos (N=36715+) the .pkl round-trip alone is a couple of seconds. FP summation order changes between modes sosave_raw=Falseoutput is numerically close (np.allclosewithatol~1e-10) rather than bit-exact vssave_raw=True.
- Returns:
- New annotation object with smoothed trajectories.
Saved to {original_stem}_lkmovavg_{window_size}.json
- Return type:
- Output Files:
{stem}_lkmovavg_{window_size}.pkl: Per-window RSTC results (only when
save_raw=True; consumed by.rawlkdownstream).{stem}_lkmovavg_{window_size}.json: Averaged smoothed annotations (always written).
- Performance Tips:
Use use_parallel=True for videos longer than a few seconds
Smaller window_size is faster but provides less smoothing
Results are cached: the .json is the primary cache, and when
save_raw=Truethe .pkl is reused on subsequent calls regardless of thesave_rawflag (a pre-existing .pkl is always read if the .json is missing). Delete either to recompute the corresponding stage.
Example
>>> # Post-process manual annotations >>> ann = VideoAnnotation('video_annotations.json', 'video.mp4') >>> ann_smooth = lk_moving_average_filter(ann, window_size=0.5) >>> >>> # Post-process DLC predictions with larger window >>> ann_dlc = VideoAnnotation('video_dlc_predictions.h5', 'video.mp4') >>> ann_smooth = lk_moving_average_filter(ann_dlc, window_size=1.0) >>> >>> # Non-parallel processing for short clips >>> ann_smooth = lk_moving_average_filter(ann, window_size=0.3, use_parallel=False) >>> >>> # GUI-style: skip the .pkl sidecar (memory + I/O savings) >>> ann_smooth = lk_moving_average_filter(ann, save_raw=False)
In practice, the
postprocessshortcut on aVideoAnnotationis equivalent: >>> from dustrack import VideoAnnotation >>> ann = VideoAnnotation(‘video_dlc_predictions.h5’, ‘video.mp4’) >>> ann_rstc = ann.postprocess()- Raises:
AssertionError – If video_name is not provided when tracked_points is a file path.
AssertionError – If tracked_points is not a VideoAnnotation or valid file path.
Note
The algorithm uses a deque-based frame buffer to minimize memory usage and avoid repeated video decoding. Frames are read once and discarded after the window moves past them.
Helpers
- class dustrack.annotations.VideoAnnotation(fname: str | None = None, vname: str | None = None, name: str | None = None, n_labels: int = 1, **kwargs)
Bases:
objectManage one point annotation layer in a video.
Each annotation layer can contain up to 10 labels, which are string representations of digits 0-9. Each label is a dictionary mapping a frame number to a 2D location on the video frame.
- Parameters:
fname (str, optional) – File name of the annotations (.json) file. If it doesn’t exist, it will be created when the save method is used. If this is a video file, fname will default to <video_name>_annotations.json. This can also be a DeepLabCut .h5 file (either labeled data OR predicted trace). Defaults to None.
vname (str, optional) – Name of the video being annotated. Defaults to None.
name (str, optional) – Name of the annotation (something meaningful, e.g., <muscle_name>_<scorer> such as brachialis_praneeth). Defaults to None.
n_labels (int, optional) – Number of labels for annotation. Defaults to 10.
**kwargs – Additional optional parameters: - palette_name (str, default=’Set2’): Color scheme to use. Defaults to ‘Set2’ from seaborn. - ax_list (list, default=[]): If specified, the annotation display will be initialized on these axes. Alternatively, use
VideoAnnotation.setup_display()to specify the axis list and colors. - preloaded_json (dict, optional): The result of VideoAnnotation._load_json (in case you prefer to pickle the JSON files).
- to_dlc()
Convert from JSON file format into a DeepLabCut DataFrame format, and optionally save the file.
- __init__(fname: str | None = None, vname: str | None = None, name: str | None = None, n_labels: int = 1, **kwargs) None
- property data: dict[str, dustrack.annotations._TrackedFrameDict]
Per-label frame→location dictionary.
Reads behave like a normal
dict[str, dict[int, list[float]]]. Writes throughann.data[label][frame] = ...automatically bump_revision, keeping consumers’ caches consistent. See_TrackedFrameDict.
- classmethod from_multiple_files(fname_list: list[str], vname: str, name: str, fname_merged: str, **kwargs) VideoAnnotation
Merge annotations from multiple files. If multiple files contain an annotation label for the same frame, values from the last file will be kept.
- load(n_annotations: int = 1, **h5_kwargs) dict
Load annotations from a json file, dlc h5 file, or initialize an annotation dictionary if a file doesn’t exist.
DUSTrack-shaped: the DeepLabCut
.h5branch exists in the generic class because datanavigator and DUSTrack co-developed. The DLC paths will migrate todustrack.VideoAnnotationin 1.3.0 alongside thepointtracking.pysplit; the JSON path stays here.
- property n_frames: int
Number of frames in the video being annotated
- property n_annotations: int
Number of points being annotated in the video.
- property labels: list[str]
Labels of the annotations.
- property palette: list[tuple]
Color palette for the annotations.
- property frames: list[int]
Frame numbers in the video that have annotations.
- property frames_overlapping: list[int]
list of frames in the video where all the labels are annotated.
- property plot_type: str
Type of plot to use for the annotations.
- get_frames(label: str) list[int]
Return a list of frames that are annotated with the current label.
- reload() None
Drop in-memory state and reload from disk (or start fresh).
Inverse of
save(). IffnameisNoneor the file doesn’t exist, restores the empty{str(i): {} for i in range(n)}shape via the existingload()fallback (see the file-missing branch). Wholesale- replacesself.dataso the property setter rewraps every per-label inner dict as a_TrackedFrameDict, and bumps_revisionexplicitly so per-frame caches keyed on(label_list, _revision)invalidate – the outer setter rewraps but does not itself bump (mirrorssort_data()andremove_empty_labels()).
- save(fname: str | None = None) None
Save the annotations json file. self.fname should be a valid file path.
Empty-but-declared labels are preserved (written as
"label": {}). 1.4.0rc2 promoted labels from a derived property of “which keys have data” to first-class schema; the previous implicitself.remove_empty_labels()call here is gone. Callers that want a lean export (e.g. DUSTrack pre-flight before DLC training) still driveremove_empty_labels()explicitly.
- to_json() None
Alias for save method.
- sort_labels() None
Sort labels in the data dictionary.
- sort_data() None
Sort annotations by the frame numbers.
- clip_trailing_empty_labels() None
Remove trailing empty labels from the annotation dictionary.
- remove_empty_labels() None
Remove empty labels from the annotation dictionary.
- get_values_cv(frame_num: int) ndarray
Return annotations at frame_num in a format for openCV’s optical flow algorithms
- add_at_frame(frame_num: int, values: ndarray) None
Add annotations at a frame, given the annotation values.
- get_at_frame(frame_num: int) list[list[float]]
Retrieve annotations at a given frame number. If an annotation is not present, nan values will be used.
- to_dlc(scorer: str = 'praneeth', output_path: str | None = None, file_prefix: str | None = None, img_prefix: str = 'img', img_suffix: str = '.png', label_prefix: str = 'point', save: bool = True, internal_to_dlc_labels: dict[str, str] | None = None) DataFrame
Save annotations in deeplabcut format.
DUSTrack-shaped: emits a DeepLabCut-shaped DataFrame (and writes an
.h5ifsave=True); slated to migrate todustrack.VideoAnnotationin 1.3.0 alongside thepointtracking.pysplit.
- to_trace(label: str) ndarray
Return a 2d numpy array of n_frames x 2.
- Parameters:
label (str) – Annotation label.
- Returns:
- 2d numpy array of number of frames x 2.
xy position values of uannotated frames will be filled with np.nan.
- Return type:
np.ndarray
Schema-tolerant: a label absent from
datais treated as all-frames-unannotated and returns the full NaN array. Lets cross-layer trace consumers (e.g._DUSTrackBase.update_frame_marker(), which iterates every annotation layer with one shared label) survive a layer that legitimately doesn’t carry the active label – either a freshly created layer or a layer where the user hasn’t placed any points for that label yet. 1.4.0rc2: prior to the schema-tolerant relaxation this asserted, which crashed the corrections-layer flow when the patch had been saved with one of its labels empty (save then pruned that label, so it disappeared fromlabelsand the assert fired).
- to_traces() Mapping[str, ndarray]
Return annotations as traces (numpy arrays of size n_frames x 2).
- Returns:
Dictionary mapping labels to 2d numpy arrays.
- Return type:
Mapping[str, np.ndarray]
- to_signal(label: str) Data
Return an annotation as pysampled.Data at the frame rate of the video.
- Parameters:
label (str) – Annotation label.
- Returns:
Signal sampled at the frame rate of the video being annotated.
- Return type:
pysampled.Data
- to_signals() Mapping[str, Data]
Return annotations as a dictionary of sampled Data signals sampled at the frame rate of the video being annotated.
- Returns:
Dictionary mapping labels to pysampled.Data.
- Return type:
Mapping[str, pysampled.Data]
- to_pysampled() Data
Return annotations as a pysampled.Data object
- Returns:
pysampled.Data
- add_label(label: str | None = None, color: tuple[float, float, float] | None = None) None
- add(location: list[float], label: str, frame_number: int) None
Add a point annotation (location) of a given label at a frame number.
- remove(label: str, frame_number: int) None
Remove a point annotation of a given label at a frame number.
- setup_display_scatter(ax_list_scatter=None) None
Setup scatter plot display.
Each entry in
ax_list_scatteris either: - a matplotlibAxes(Tier 1) – aPathCollectioniscreated via
ax.scatterand stashed inplot_handles;a Qt
QGraphicsItemGroup(Tier 2) – adatanavigator._qt._QtScatterArtistis built on the group and stashed instead. Both expose the same mpl-PathCollection-shaped API consumed downstream.
- setup_display_trace(ax_list_trace_x: None | Axes | list[matplotlib.axes._axes.Axes] = None, ax_list_trace_y: None | Axes | list[matplotlib.axes._axes.Axes] = None) None
Setup trace plot display.
- setup_display(ax_list_scatter: None | Axes | list[matplotlib.axes._axes.Axes] = None, ax_list_trace_x: None | Axes | list[matplotlib.axes._axes.Axes] = None, ax_list_trace_y: None | Axes | list[matplotlib.axes._axes.Axes] = None) None
Setup display for scatter and trace plots.
- clear_display() None
Clear the display.
- re_setup_display() None
re-establish display elements when adding a label
- invalidate_caches() None
Drop per-annotation render caches so the next
update_display_tracere-runs the full ydata sweep.Recovery hatch for direct
.datamutations from the command line (which skip the_revisionbump the trace cache keys on). Normal in-code mutations should go throughadd()/remove()/add_at_frame()instead.
- update_display_scatter(frame_number: int, draw: bool = False) None
Update scatter plot display.
- update_display_trace(label: str | None = None, draw: bool = False) None
Update trace plot display.
Trace contents are a pure function of (label_list, self.data); none of that changes when only the parent’s
_current_idxmoves. Cache on (label_list, self._revision) so per-frame navigation skips the to_trace / set_ydata sweep entirely.
- update_display(frame_number: int, label: str | None = None, draw: bool = False) None
Update scatter and trace plot display.
- hide(draw: bool = True) None
Hide all elements (scatter, traces) in this annotation.
- show(draw: bool = True) None
Show all elements (scatter, traces) in this annotation.
- show_trace(label: str, draw: bool = True) None
Show trace for a specific label.
- hide_trace(label: str, draw: bool = True) None
Hide trace for a specific label.
- show_one_trace(label: str, draw: bool = True) None
Show only one trace for a specific label.
- set_alpha(alpha: float = 0.4, label: str | None = None, draw: bool = True) None
Set the transparency level of all (or one) the traces and labels in this annotation.
- Parameters:
alpha (float, optional) – alpha value between 0 and 1. Defaults to 0.4.
label (_type_, optional) – Defaults to all labels.
draw (bool, optional) – Update display if True. Defaults to True.
- set_plot_type(type_: str = 'line', draw: bool = True) None
Set the plot type for traces.
Records the choice on
_plot_typeand applies the visual style, so subsequentre_setup_display()/setup_display()calls (which readself.plot_type) don’t revert to the stale value. Symmetric with theplot_typeproperty setter.- Parameters:
type (str, optional) – “line” or “dot”. Defaults to “line”.
draw (bool, optional) – Update display if True. Defaults to True.
- clip_labels(start_frame: int, end_frame: int) None
Remove annotations outside the clip range. Clip range includes start and end frame.
- keep_overlapping_continuous_frames() None
Keep data from consecutive frames that have all labels.
- keep_overlapping_frames() None
Keep data only from frames where every label is annotated.
Sibling of
keep_overlapping_continuous_frames()without the consecutive-runs constraint: fully-labeled but isolated frames are preserved. Motivating use case is DLC training pre-flight, where partial frames degrade the trained model even though DLC tolerates per-bodypart NaN in its CSV.
- get_area(labels: list[str] | str, lowpass: float | None = None) Data | ndarray
Get the area in pixel squared.
- export_video(out_file_name=None, start_frame=None, end_frame=None)
- postprocess
Post-processes the annotation using LK-RSTC filter. See
lk_moving_average_filter()for details.
- class dustrack._file_management.VideoFileManager(d: DLCProject, video_index: int)
Bases:
FileManagerFile manager for organizing annotation and result files for one video.
Provides convenient access to all files associated with a video in a DLC project: - Manual annotation JSON files - DLC prediction HDF5 files - Labeled data files for training
- Variables:
project_name (str) – Name of the DLC project.
video_stem (str) – Video filename without extension.
video_fname (str) – Full path to video file.
- __init__(d: DLCProject, video_index: int)
Initialize file manager for a specific video.
- Parameters:
d (DLCProject) – Parent DLC project.
video_index (int) – Index of video in project’s video list.
- property annotations: dict
Map annotation names to file paths.
- Returns:
{annotation_name: file_path} for all JSON annotation files.
- Return type:
dict
- property annotation_files: list
List of full paths to annotation JSON files.
- property annotation_names: list
List of annotation layer names (without paths or extensions).
- static canonical_layer_name(fname) str
Single source of truth for DUSTrack layer names derived from a filepath.
The DUSTrack workflow produces three categories of layer file:
Manual / hand-edited annotations:
<video>_annotations[_<name>].json. Returns the suffix after_annotations(or empty string if absent), which is what users picked when they saved.DLC prediction traces: live under
videos/iteration-{N}/and haveDLCin the stem. Returns'dlc_iteration-{N}_<last underscore-token of stem>'. This pattern also catches LK-RSTC post-processed outputs, which inherit the DLC source stem – so a jitter-reduced layer gets a deterministicdlc_iteration-{N}_<window>name rather than the"noname"fallback thatVideoAnnotation.__init__produces for paths without_annotations.Anything else: the file stem.
Called by
annotations/dlc_tracesat fresh-load time AND byDUSTrack._adopt_layer()for in-session adds, so the name a user sees in the layer panel is identical regardless of whether the layer was discovered on disk or produced live.
- property dlc_traces: dict
Map DLC trace names to HDF5 file paths.
- Returns:
- {trace_name: file_path} for all DLC prediction files.
Trace names format: ‘dlc_iteration-{N}_{training_iter}’
- Return type:
dict
- property dlc_trace_files
List of full paths to DLC prediction HDF5 files.
- property dlc_trace_names
List of DLC trace identifiers.
- property labeled_data
Path to HDF5 file containing training labels in DLC format.
- Returns:
Full path to CollectedData HDF5 file.
- Return type:
str
- Raises:
AssertionError – If file doesn’t exist or multiple files found.
- get_new_json(new_suffix) Path
Create path for a new annotation file with given suffix. Used to generate the the filename for the next refinement iteration.
- Parameters:
new_suffix (str) – Suffix for new annotation layer.
- Returns:
Full path to new JSON file.
- Return type:
Path
- Raises:
ValueError – If file with this suffix already exists.
- get_all_annotation_layers(new_annotation_suffix: str = None)
Collect all annotation sources for loading into DUSTrack.
- Parameters:
new_annotation_suffix (str, optional) – Suffix for a new layer to create.
- Returns:
- Maps layer names to file paths, including:
Existing JSON annotation files
New empty layer (if suffix provided)
Labeled training data
DLC prediction HDF5 files
- Return type:
dict
- dustrack._file_management._extract_frames(video_file_name: str, frame_idx: list, output_path: str, coords: list)
Legacy frame extraction using DLC’s VideoWriter (OpenCV-based).
Note
This function is kept for backwards compatibility but _extract_frames_decord is now used by default for better performance, and because of discrepancy in extracted frames (seeking issues) when using OpenCV vs decord.
- Parameters:
video_file_name (str) – Path to video file.
frame_idx (list) – Frame numbers to extract (0-indexed).
output_path (str) – Directory to save extracted frames.
coords (list) – Crop coordinates [x, y, width, height].
- Returns:
Paths to saved image files.
- Return type:
list
- dustrack._file_management._extract_frames_decord(video_file_name: str, frame_idx: list, output_path: str, coords: list)
Extract video frames using Decord library for better performance.
This is the default frame extraction method. It uses batch reading for better I/O efficiency compared to OpenCV sequential reading.
- Parameters:
video_file_name (str) – Path to video file.
frame_idx (list) – Frame numbers to extract (0-indexed).
output_path (str) – Directory to save extracted frames.
coords (list) – Crop coordinates. Interpreted as: - [x1, y1, x2, y2] if values look like absolute corners - [x, y, width, height] otherwise
- Returns:
Paths to saved image files.
- Return type:
list
Note
Skips extraction if image file already exists. Handles invalid frame indices gracefully.
- dustrack._file_management.get_annotation_file_name(video_file_name: Path, annotation_suffix: str = '') str | None
Get full path to annotation file if it exists.
- Parameters:
video_file_name (Path) – Video file path.
annotation_suffix (str) – Annotation suffix (e.g., ‘manual’, ‘refined’).
- Returns:
Full path if file exists, None otherwise.
- Return type:
str or None
- dustrack._file_management.make_annotation_file_name(video_file_name: Path, annotation_suffix: str = '') str
Construct annotation filename from video filename and suffix.
- Parameters:
video_file_name (Path) – Video file path.
annotation_suffix (str) – Annotation suffix. Empty string means no suffix.
- Returns:
Full path to annotation file (may not exist yet).
- Return type:
str
Example
>>> make_annotation_file_name('video.mp4', 'manual') 'video_annotations_manual.json' >>> make_annotation_file_name('video.mp4', '') 'video_annotations.json'
- dustrack._file_management.merge_annotations_in_folder(path, annotation_suffix='merged')
Merge multiple annotation files for each video in a folder.
Useful for combining annotations from multiple annotators or sessions. Creates a single merged JSON file for each video.
- Parameters:
path (str) – Directory containing videos and annotation JSON files.
annotation_suffix (str) – Suffix for merged output files. Defaults to ‘merged’.