diff --git a/README.md b/README.md index 283daad..dbe0bb7 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ In order to create your own algorithm to test: 2. Create a class which extends `TaskPerceiver` (see `perception/tasks/TaskPerceiver.py` for the template with documentation) and decorate it with `@register_perceiver(task=..., algo=...)` from `perception/tasks/registry.py`. This is what makes it discoverable by `vis.py` — see the **vis** section below. +3. Pass `default=True` to `@register_perceiver` if this should be the algo `vis.py` runs for its task when `--algo` is omitted. A task with only one registered algo defaults to it automatically; a task with several needs one of them explicitly marked (currently `segmentation_a` for `gate`, and the sole algo for `slalom`/`path_marker`). Registering a second `default=True` algo for the same task raises an error. + ## vis: Visualization tools for interactively running and debugging task algorithms. @@ -77,14 +79,16 @@ Every algorithm is a `TaskPerceiver` subclass (see `perception/tasks/TaskPerceiv Then run it with: - python -m perception.vis.vis --task gate --algo my_algo [--data ] [--profile ] [--save_video] [--resize ] + python -m perception.vis.vis --task gate [--algo my_algo] [--data ] [--profile ] [--save_video] [--resize ] [--compare ] [--hide_labels] -- `--task` / `--algo` are required and select the registered perceiver to run. +- `--task` is required and selects which task's perceivers to run. +- `--algo` is optional — omit it to use the task's default algo (see point 3 above); `vis.py` prints which algo it picked. If the task has no default set, it errors and asks you to pass `--algo` explicitly. - `--data` defaults to your webcam; point it at an image, video, or a directory of either. - `--profile` is off by default; pass a `cProfile` stats key (or omit for `'all'`) to profile the run. - `--save_video` writes the debug-frame grid to `vis_rec.mp4`. - `--resize` scales every frame before display (default `1.0`, no resize). - `--compare ` runs a second algo for the same `--task` on the same frames and stacks it below the primary algo's grid in one "Debug Frames" window, each half labeled with its algo name in the top-left corner — useful for A/B'ing two algorithms (e.g. `center` vs. `segmentation_a` for `gate`) against the same footage. Stacking below (rather than beside) keeps each pane's width unchanged, so sub-frame resolution and label/slider text stay legible regardless of how many debug frames either algo returns. Sliders for both algos appear in the same window, prefixed with their algo name (e.g. `center: canny_low`) to keep them distinguishable. `--save_video` saves the combined, labeled view. +- `--hide_labels` drops the corner labels in `--compare` mode (shown by default). While a window is focused: `q`/`Esc` quits, `p` pauses, `i`/`o` slow down/speed up frame playback. diff --git a/perception/tasks/gate/classical/GateSegmentationAlgoA.py b/perception/tasks/gate/classical/GateSegmentationAlgoA.py index e59411b..5ae8510 100644 --- a/perception/tasks/gate/classical/GateSegmentationAlgoA.py +++ b/perception/tasks/gate/classical/GateSegmentationAlgoA.py @@ -8,15 +8,14 @@ from perception.tasks.TaskPerceiver import TaskPerceiver -@register_perceiver(task="gate", algo="segmentation_a") +@register_perceiver(task="gate", algo="segmentation_a", default=True) class GateSegmentationAlgoA(TaskPerceiver): center_x_locs, center_y_locs = [], [] def __init__(self): super().__init__() self.combined_filter = init_combined_filter() - - # TODO: fix return typing + def analyze(self, frame: np.ndarray, debug: bool, slider_vals=None) -> Tuple[float, float]: """Takes in the background removed image and returns the center between the two gate posts. diff --git a/perception/tasks/registry.py b/perception/tasks/registry.py index 1306e1c..793dd30 100644 --- a/perception/tasks/registry.py +++ b/perception/tasks/registry.py @@ -22,10 +22,15 @@ from perception.tasks.TaskPerceiver import TaskPerceiver _REGISTRY: dict[tuple[str, str], type[TaskPerceiver]] = {} +_DEFAULTS: dict[str, str] = {} -def register_perceiver(task: str, algo: str): - """Class decorator: register a TaskPerceiver subclass under (task, algo).""" +def register_perceiver(task: str, algo: str, default: bool = False): + """Class decorator: register a TaskPerceiver subclass under (task, algo). + + Pass default=True to make this the algo get_default_algo() returns for + `task` when a caller (e.g. the vis CLI) doesn't specify one explicitly. + """ def decorator(cls: type[TaskPerceiver]) -> type[TaskPerceiver]: key = (task, algo) @@ -37,6 +42,14 @@ def decorator(cls: type[TaskPerceiver]) -> type[TaskPerceiver]: f"register {cls.__module__}.{cls.__qualname__}" ) _REGISTRY[key] = cls + if default: + existing_default = _DEFAULTS.get(task) + if existing_default is not None and existing_default != algo: + raise ValueError( + f"task={task!r} already has default algo {existing_default!r}, " + f"cannot also mark {algo!r} as default" + ) + _DEFAULTS[task] = algo return cls return decorator @@ -62,6 +75,22 @@ def list_algos(task: str) -> list[str]: return sorted(algo for t, algo in _REGISTRY if t == task) +def get_default_algo(task: str) -> str: + """The default algo for a task: whichever was marked default=True, or the + sole registered algo if the task only has one. Raises KeyError if neither + applies, i.e. the caller must specify an algo explicitly. + """ + explicit = _DEFAULTS.get(task) + if explicit is not None: + return explicit + algos = list_algos(task) + if len(algos) == 1: + return algos[0] + raise KeyError( + f"no default algo for task={task!r} (algos: {', '.join(algos) or 'none registered'})" + ) + + _EXCLUDED_PACKAGES = ("perception.tasks._archive",) diff --git a/perception/vis/vis.py b/perception/vis/vis.py index 251b362..daf0046 100644 --- a/perception/vis/vis.py +++ b/perception/vis/vis.py @@ -46,7 +46,7 @@ def _stack_compare(primary, compare): def run(data_sources, algorithm, save_video=False, resize=0.15, compare_algorithm=None, - algo_label=None, compare_label=None): + algo_label=None, compare_label=None, show_labels=True): out = None window_name = 'Debug Frames' compare_mode = compare_algorithm is not None @@ -66,10 +66,10 @@ def run(data_sources, algorithm, save_video=False, resize=0.15, compare_algorith _, to_show = _analyze(algorithm, window_builder, frame) if compare_mode: _, compare_to_show = _analyze(compare_algorithm, compare_window_builder, frame) - to_show = _stack_compare( - _label_frame(to_show, algo_label or 'primary'), - _label_frame(compare_to_show, compare_label or 'compare'), - ) + if show_labels: + to_show = _label_frame(to_show, algo_label or 'primary') + compare_to_show = _label_frame(compare_to_show, compare_label or 'compare') + to_show = _stack_compare(to_show, compare_to_show) cv.imshow(window_name, to_show) if save_video: @@ -127,7 +127,13 @@ def profile(*args, stats='all', **kwargs): parser.add_argument( '--task', type=str, required=True, help='e.g. slalom, gate, path_marker' ) - parser.add_argument('--algo', type=str, required=True, help='e.g. classical') + parser.add_argument( + '--algo', + default=None, + type=str, + help='e.g. classical. If omitted, uses the task\'s default algo ' + '(its sole registered algo, or whichever was marked default=True).', + ) parser.add_argument( '--compare', default=None, @@ -139,6 +145,12 @@ def profile(*args, stats='all', **kwargs): ) parser.add_argument('--profile', default=None, type=str) parser.add_argument('--save_video', action='store_true') + parser.add_argument( + '--hide_labels', + action='store_true', + help='Hide the corner labels that identify each pane in --compare mode ' + '(shown by default).', + ) parser.add_argument( "--resize", default=1.0, @@ -150,8 +162,17 @@ def profile(*args, stats='all', **kwargs): # Discover every @register_perceiver in perception.tasks, then look up the # requested one. No shared file needs hand-editing to add a new algorithm. registry.discover_all() + + algo_name = args.algo + if algo_name is None: + try: + algo_name = registry.get_default_algo(args.task) + except KeyError as exc: + raise SystemExit(f"{exc}. Pass --algo explicitly.") from None + print(f"No --algo given, using default for task {args.task!r}: {algo_name}") + try: - algorithm = registry.get_perceiver(args.task, args.algo)() + algorithm = registry.get_perceiver(args.task, algo_name)() except KeyError as exc: available = ", ".join( f"{task}/{algo}" @@ -181,11 +202,11 @@ def profile(*args, stats='all', **kwargs): if args.profile is None: run( data_sources, algorithm, args.save_video, args.resize, compare_algorithm, - algo_label=args.algo, compare_label=args.compare, + algo_label=algo_name, compare_label=args.compare, show_labels=not args.hide_labels, ) else: profile( data_sources, algorithm, args.save_video, args.resize, compare_algorithm, - algo_label=args.algo, compare_label=args.compare, + algo_label=algo_name, compare_label=args.compare, show_labels=not args.hide_labels, stats=args.profile, )