Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <path to file/directory>] [--profile <function name>] [--save_video] [--resize <scale>]
python -m perception.vis.vis --task gate [--algo my_algo] [--data <path to file/directory>] [--profile <function name>] [--save_video] [--resize <scale>] [--compare <algo>] [--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 <algo>` 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.

Expand Down
5 changes: 2 additions & 3 deletions perception/tasks/gate/classical/GateSegmentationAlgoA.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 31 additions & 2 deletions perception/tasks/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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",)


Expand Down
39 changes: 30 additions & 9 deletions perception/vis/vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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}"
Expand Down Expand Up @@ -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,
)
Loading