@click.command(context_settings={"help_option_names": ["-h", "--help"]})
@click.argument("data_path", required=True)
@click.option(
"-m",
"--model",
"models",
multiple=True,
help=(
"Path to trained model directory (with training_config json/yaml). "
"Multiple models can be specified, each preceded by --model."
),
)
@click.option(
"--frames",
"frames",
help=(
"List of frames to predict when running on a video. Can be specified as a "
"comma separated list (e.g. 1,2,3) or a range separated by hyphen (e.g., 1-3, "
"for 1,2,3). If not provided, defaults to predicting on the entire video."
),
)
@click.option(
"--only-labeled-frames",
"only_labeled_frames",
is_flag=True,
help=(
"Only run inference on user labeled frames when running on labels dataset. "
"This is useful for generating predictions to compare against ground truth."
),
)
@click.option(
"--only-suggested-frames",
"only_suggested_frames",
is_flag=True,
help=(
"Only run inference on unlabeled suggested frames when running on labels "
"dataset. This is useful for generating predictions for initialization during "
"labeling."
),
)
@click.option(
"-o",
"--output",
"output",
help=(
"The output filename or directory path to use for the predicted data. If not "
"provided, defaults to '[data_path].predictions.slp'."
),
)
@click.option("--video.dataset", "video_dataset", help="The dataset for HDF5 videos.")
@click.option(
"--video.input_format",
"video_input_format",
help="The input_format for HDF5 videos.",
)
@click.option(
"--video.index",
"video_index",
type=int,
help=(
"Integer index of video in .slp file to predict on. To be used with an .slp "
"path as an alternative to specifying the video path."
),
)
@click.option(
"--cpu",
"cpu",
is_flag=True,
help="Run inference only on CPU. If not specified, will use available GPU.",
)
@click.option(
"--first-gpu",
"first_gpu",
is_flag=True,
help="Run inference on the first GPU, if available.",
)
@click.option(
"--last-gpu",
"last_gpu",
is_flag=True,
help="Run inference on the last GPU, if available.",
)
@click.option(
"--gpu",
"gpu",
help=(
"Run training on the i-th GPU on the system. If 'auto', run on the GPU with "
"the highest percentage of available memory."
),
)
@click.option(
"--max_edge_length_ratio",
"max_edge_length_ratio",
type=float,
help=(
"The maximum expected length of a connected pair of points as a fraction of "
"the "
"image size. Candidate connections longer than this length will be penalized "
"during matching. Only applies to bottom-up (PAF) models."
),
)
@click.option(
"--dist_penalty_weight",
"dist_penalty_weight",
type=float,
help=(
"A coefficient to scale weight of the distance penalty. Set to values greater "
"than 1.0 to enforce the distance penalty more strictly. Only applies to "
"bottom-up (PAF) models."
),
)
@click.option(
"--batch_size",
"batch_size",
type=int,
help=(
"Number of frames to predict at a time. Larger values result in faster "
"inference speeds, but require more memory."
),
)
@click.option(
"--open-in-gui",
"open_in_gui",
is_flag=True,
help="Open the resulting predictions in the GUI when finished.",
)
@click.option(
"--peak_threshold",
"peak_threshold",
type=float,
help="Minimum confidence map value to consider a peak as valid.",
)
@click.option(
"-n",
"--max_instances",
"max_instances",
type=int,
help=(
"Limit maximum number of instances in multi-instance models. Not available for "
"ID models. Defaults to None."
),
)
@click.option(
"--tracking.tracker",
"tracking_tracker",
help="Options: simple, flow, simplemaxtracks, flowmaxtracks, None (default: None)",
)
@click.option(
"--tracking.max_tracking",
"tracking_max_tracking",
type=int,
help="If 1 (True) then the tracker will cap the max number of tracks. 0 (False)",
)
@click.option(
"--tracking.max_tracks",
"tracking_max_tracks",
type=int,
help="Maximum number of tracks to be tracked by the tracker. (default: None)",
)
@click.option(
"--tracking.post_connect_single_breaks",
"tracking_post_connect_single_breaks",
type=int,
help=(
"If non-zero and target_instance_count is also non-zero, then connect track "
"breaks when exactly one track is lost and exactly one track is spawned in "
"frame. (default: 0)"
),
)
@click.option(
"--tracking.similarity",
"tracking_similarity",
help=(
"Options: instance, normalized_instance, object_keypoint, centroid, iou "
"(default: instance)"
),
)
@click.option(
"--tracking.match",
"tracking_match",
help="Options: hungarian, greedy (default: greedy)",
)
@click.option(
"--tracking.robust",
"tracking_robust",
type=float,
help=(
"Robust quantile of similarity score for instance matching. If equal to 1, "
"keep the max similarity score (non-robust). (default: 1)"
),
)
@click.option(
"--tracking.track_window",
"tracking_track_window",
type=int,
help="How many frames back to look for matches (default: 5)",
)
@click.option(
"--tracking.min_new_track_points",
"tracking_min_new_track_points",
type=int,
help="Minimum number of instance points for spawning new track (default: 0)",
)
@click.option(
"--tracking.min_match_points",
"tracking_min_match_points",
type=int,
help="Minimum points for match candidates (default: 0)",
)
@click.option(
"--tracking.img_scale",
"tracking_img_scale",
type=float,
help="For optical-flow: Image scale (default: 1.0)",
)
@click.option(
"--tracking.of_window_size",
"tracking_of_window_size",
type=int,
help=(
"For optical-flow: Optical flow window size to consider at each pyramid "
"(default: 21)"
),
)
@click.option(
"--tracking.of_max_levels",
"tracking_of_max_levels",
type=int,
help="For optical-flow: Number of pyramid scale levels to consider (default: 3)",
)
def track_command(
data_path,
models,
frames,
only_labeled_frames,
only_suggested_frames,
output,
video_dataset,
video_input_format,
video_index,
cpu,
first_gpu,
last_gpu,
gpu,
max_edge_length_ratio,
dist_penalty_weight,
batch_size,
open_in_gui,
peak_threshold,
max_instances,
tracking_tracker,
tracking_max_tracking,
tracking_max_tracks,
tracking_post_connect_single_breaks,
tracking_similarity,
tracking_match,
tracking_robust,
tracking_track_window,
tracking_min_new_track_points,
tracking_min_match_points,
tracking_img_scale,
tracking_of_window_size,
tracking_of_max_levels,
):
"""Track instances in video data using trained SLEAP models."""
try:
import torch
from sleap_nn.predict import run_inference as predict
# Build kwargs for the tracking function
kwargs = {}
if models is not None:
kwargs["model_paths"] = models
if frames is not None:
kwargs["frames"] = frames
if only_labeled_frames is not None and only_labeled_frames:
kwargs["only_labeled_frames"] = True
if only_suggested_frames is not None and only_suggested_frames:
kwargs["only_suggested_frames"] = True
kwargs["output_path"] = output
if output is None:
kwargs["output_path"] = f"{data_path}.predictions.slp"
if video_dataset is not None:
kwargs["video_dataset"] = video_dataset
if video_input_format is not None:
kwargs["video_input_format"] = video_input_format
if video_index is not None:
kwargs["video_index"] = video_index
if cpu is not None and cpu:
kwargs["device"] = "cpu"
if torch.cuda.is_available():
if first_gpu:
kwargs["device"] = "cuda:0"
if last_gpu:
n_gpus = torch.cuda.device_count()
kwargs["device"] = f"cuda:{n_gpus - 1}"
if gpu:
kwargs["device"] = f"cuda:{gpu}" if gpu != "auto" else "cuda"
if max_edge_length_ratio is not None:
kwargs["max_edge_length_ratio"] = max_edge_length_ratio
if dist_penalty_weight is not None:
kwargs["dist_penalty_weight"] = dist_penalty_weight
if batch_size is not None:
kwargs["batch_size"] = batch_size
# if open_in_gui:
# kwargs['open_in_gui'] = True #TODO
if peak_threshold is not None:
kwargs["peak_threshold"] = peak_threshold
if max_instances is not None:
kwargs["max_instances"] = max_instances
if tracking_tracker:
if "flow" in tracking_tracker:
kwargs["use_flow"] = True
# Check max_tracking flag int value for True/False
if tracking_max_tracking is not None and tracking_max_tracking == 1:
kwargs["candidates_method"] = "local_queues"
kwargs["max_tracks"] = tracking_max_tracks
if tracking_similarity is not None:
if tracking_similarity == "oks":
kwargs["features"] = "keypoints"
kwargs["scoring_method"] = "oks"
elif tracking_similarity == "centroids":
kwargs["features"] = "centroids"
kwargs["scoring_method"] = "euclidean_dist"
elif tracking_similarity == "iou":
kwargs["features"] = "bboxes"
kwargs["scoring_method"] = "iou"
if (
tracking_post_connect_single_breaks is not None
and tracking_post_connect_single_breaks
):
kwargs["post_connect_single_breaks"] = tracking_post_connect_single_breaks
if tracking_match is not None:
kwargs["track_matching_method"] = tracking_match
if tracking_robust is not None:
kwargs["robust_best_instance"] = tracking_robust
if tracking_track_window is not None:
kwargs["tracking_window_size"] = tracking_track_window
if tracking_min_new_track_points is not None:
kwargs["min_new_track_points"] = tracking_min_new_track_points
if tracking_min_match_points is not None:
kwargs["min_match_points"] = tracking_min_match_points
if tracking_img_scale is not None:
kwargs["of_img_scale"] = tracking_img_scale
if tracking_of_window_size is not None:
kwargs["of_window_size"] = tracking_of_window_size
if tracking_of_max_levels is not None:
kwargs["of_max_levels"] = tracking_of_max_levels
# # Call the original tracking function with kwargs
predict(data_path=data_path, **kwargs)
if open_in_gui:
import subprocess
# Launch SLEAP GUI with the output file
subprocess.run(["sleap-label", str(kwargs.get("output_path", ""))])
except ImportError:
logger.error(
"sleap-nn is not installed. This appears to be a GUI-only installation. "
"To enable training, please install SLEAP with the 'nn' dependency. "
"See the installation guide: https://docs.sleap.ai/latest/installation/"
)