| """Controller tests with a fake viewer — exercises the workflow without Qt/VTK.""" |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
|
|
| from toaster.core import Grouping, Session |
| from toaster.interaction import InteractionController |
|
|
|
|
| class FakeViewer: |
| """Records what the controller asks the renderer to do.""" |
|
|
| def __init__(self): |
| self.last_highlight = None |
| self.recolored = [] |
| self.point_cb = None |
| self.box_cb = None |
| self.cloud_colors = None |
| self.visible_mask = None |
|
|
| def set_cloud(self, xyz, colors): |
| self.cloud_colors = colors |
|
|
| def update_colors(self, indices, colors): |
| self.recolored.append((np.asarray(indices), np.asarray(colors))) |
|
|
| def highlight(self, indices): |
| self.last_highlight = np.asarray(indices) |
|
|
| def clear_highlight(self): |
| self.last_highlight = np.empty(0, dtype=np.int64) |
|
|
| def set_point_pick_callback(self, cb): |
| self.point_cb = cb |
|
|
| def set_box_pick_callback(self, cb): |
| self.box_cb = cb |
|
|
| def set_pick_mode(self, mode): |
| self.pick_mode = mode |
|
|
| def set_visible_mask(self, mask): |
| self.visible_mask = None if mask is None else np.asarray(mask) |
|
|
| def reset_camera(self): |
| pass |
|
|
| def render(self): |
| pass |
|
|
|
|
| def _session(two_clusters, schema): |
| two_clusters.ensure_labels(schema.unlabeled_id) |
| return Session(two_clusters, schema) |
|
|
|
|
| def test_pick_without_grouping_selects_single_point(two_clusters, schema): |
| session = _session(two_clusters, schema) |
| viewer = FakeViewer() |
| ctl = InteractionController(session, viewer) |
| ctl.on_pick(7) |
| assert session.selection.indices.tolist() == [7] |
| assert viewer.last_highlight.tolist() == [7] |
|
|
|
|
| def test_pick_with_active_grouping_selects_whole_group(two_clusters, schema): |
| session = _session(two_clusters, schema) |
| |
| gid = np.where(np.arange(two_clusters.n) < 50, 0, 1).astype(np.int32) |
| session.add_grouping(Grouping(gid)) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.on_pick(3) |
| assert session.selection.count == 50 |
| assert session.selection.indices.max() < 50 |
|
|
|
|
| def test_shift_adds_ctrl_subtracts(two_clusters, schema): |
| session = _session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.on_pick(1) |
| ctl.on_pick(2, frozenset({"shift"})) |
| assert session.selection.indices.tolist() == [1, 2] |
| ctl.on_pick(1, frozenset({"ctrl"})) |
| assert session.selection.indices.tolist() == [2] |
|
|
|
|
| def test_assign_writes_labels_and_recolors(two_clusters, schema): |
| session = _session(two_clusters, schema) |
| viewer = FakeViewer() |
| ctl = InteractionController(session, viewer) |
| ctl.on_pick(5) |
| ctl.assign(2) |
| assert session.cloud.labels[5] == 2 |
| assert viewer.recolored |
| |
| assert session.selection.is_empty() |
|
|
|
|
| def _grouped_session(two_clusters, schema): |
| """Session with a 2-group grouping active (group 0 = first 50, group 1 = rest).""" |
| two_clusters.ensure_labels(schema.unlabeled_id) |
| session = Session(two_clusters, schema) |
| gid = np.where(np.arange(two_clusters.n) < 50, 0, 1).astype(np.int32) |
| session.add_grouping(Grouping(gid, suggested_labels={1: 2})) |
| return session |
|
|
|
|
| def test_select_all_selects_every_point(two_clusters, schema): |
| session = _session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.select_all() |
| assert session.selection.count == two_clusters.n |
| assert session.selection.indices.tolist() == list(range(two_clusters.n)) |
|
|
|
|
| def test_select_group_selects_whole_segment(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.select_group(0) |
| assert session.selection.count == 50 |
| assert session.selection.indices.max() < 50 |
|
|
|
|
| def test_assign_group_labels_the_segment(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| viewer = FakeViewer() |
| ctl = InteractionController(session, viewer) |
| ctl.set_active_class(1) |
| touched = ctl.assign_group(0) |
| assert touched.size == 50 |
| assert (session.cloud.labels[:50] == 1).all() |
| assert (session.cloud.labels[50:] == 0).all() |
| assert viewer.recolored |
|
|
|
|
| def test_apply_suggested_single_and_all(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| |
| touched = ctl.apply_suggested(1) |
| assert touched.size == 50 |
| assert (session.cloud.labels[50:] == 2).all() |
| |
| assert ctl.apply_suggested(0).size == 0 |
| |
| session2 = _grouped_session(two_clusters, schema) |
| ctl2 = InteractionController(session2, FakeViewer()) |
| assert ctl2.apply_suggested(None).size == 50 |
|
|
|
|
| def test_group_ops_noop_without_active_grouping(two_clusters, schema): |
| two_clusters.ensure_labels(schema.unlabeled_id) |
| session = Session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| assert ctl.assign_group(0).size == 0 |
| assert ctl.apply_suggested().size == 0 |
|
|
|
|
| def test_group_visibility_commands(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| viewer = FakeViewer() |
| ctl = InteractionController(session, viewer) |
|
|
| |
| ctl.set_group_visibility(0, False) |
| assert viewer.visible_mask is not None |
| assert viewer.visible_mask[:50].sum() == 0 |
| assert viewer.visible_mask[50:].all() |
| assert next(s for s in ctl.snapshot().segments if s.id == 0).visible is False |
|
|
| |
| ctl.hide_all_groups() |
| assert viewer.visible_mask is not None and viewer.visible_mask.sum() == 0 |
| assert all(s.visible is False for s in ctl.snapshot().segments) |
|
|
| |
| ctl.show_all_groups() |
| assert viewer.visible_mask is None |
| assert all(s.visible for s in ctl.snapshot().segments) |
|
|
|
|
| def test_assign_visible_groups_labels_only_checked(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
|
|
| |
| ctl.set_group_visibility(0, False) |
| ctl.set_active_class(2) |
| touched = ctl.assign_visible_groups() |
| labels = session.cloud.labels |
| assert touched.size == 50 |
| assert (labels[:50] == schema.unlabeled_id).all() |
| assert (labels[50:] == 2).all() |
|
|
| |
| ctl.undo() |
| assert (session.cloud.labels == schema.unlabeled_id).all() |
|
|
|
|
| def test_clear_grouping_discards_segmentation_keeps_labels(two_clusters, schema): |
| session = _grouped_session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.set_display_mode("grouping") |
| |
| labelled = ctl.assign_group(0, 1) |
| assert labelled.size == 50 |
|
|
| ctl.clear_grouping() |
| assert session.active_grouping is None |
| assert session.groupings == [] |
| assert ctl.display_mode == "labels" |
| assert (session.cloud.labels[:50] == 1).all() |
| assert ctl.snapshot().segments == [] |
| |
| ctl.clear_grouping() |
| assert session.active_grouping is None |
|
|
|
|
| def test_class_editing(two_clusters, schema): |
| from toaster.core import Selection |
|
|
| two_clusters.ensure_labels(schema.unlabeled_id) |
| session = Session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
|
|
| |
| new_id = ctl.add_class("tree", (1, 2, 3)) |
| assert session.active_class == new_id |
| assert session.schema.get(new_id).color == (1, 2, 3) |
|
|
| |
| ctl.rename_class(1, "floor") |
| assert session.schema.get(1).name == "floor" |
| ctl.set_class_color(1, (9, 9, 9)) |
| assert session.schema.get(1).color == (9, 9, 9) |
|
|
| |
| session.annotation.assign(Selection.from_indices([0, 1, 2], two_clusters.n), 1) |
| ctl.remove_class(1) |
| assert 1 not in [c.id for c in session.schema.classes] |
| assert (session.cloud.labels[:3] == schema.unlabeled_id).all() |
|
|
|
|
| def test_snapshot_is_a_flat_serializable_read_model(two_clusters, schema): |
| from dataclasses import asdict |
|
|
| session = _grouped_session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.set_active_class(1) |
| snap = ctl.snapshot() |
|
|
| assert [c.id for c in snap.classes] == [0, 1, 2] |
| assert snap.active_class == 1 |
| assert snap.class_name(1) == "a" |
| assert snap.active_grouping is not None and snap.active_grouping.n_groups == 2 |
| assert len(snap.segments) == 2 |
| seg1 = next(s for s in snap.segments if s.id == 1) |
| assert seg1.count == 50 |
| assert seg1.suggested == 2 |
| assert snap.has_suggestions is True |
| |
| asdict(snap) |
|
|
|
|
| def test_run_segmenter_sets_active_grouping(two_clusters, schema): |
| from toaster.segment import get_segmenter |
|
|
| session = _session(two_clusters, schema) |
| ctl = InteractionController(session, FakeViewer()) |
| ctl.run_segmenter(get_segmenter("dbscan", eps=0.5, min_samples=5)) |
| assert session.active_grouping is not None |
| assert session.active_grouping.n_groups == 2 |
| assert ctl.display_mode == "grouping" |
|
|