SmaugC137 commited on
Commit
b6d00f2
·
1 Parent(s): ec5c688

fix(core): labelling after an apairo resume silently did nothing

Browse files

AnnotationController cached cloud.labels at construction, but resume_apairo
replaces the array wholesale — every later assign wrote into the orphaned
pre-resume array while the UI (and saves) read the new one. The controller
now reads cloud.labels live, and resume clears the undo history so edits
recorded against the old array can't be spliced into the resumed labels.

tests/test_annotation_history.py CHANGED
@@ -50,3 +50,14 @@ def test_new_edit_clears_redo():
50
  ann.undo()
51
  ann.assign(Selection.from_indices([1], 6), 2)
52
  assert ann.redo() is None # redo stack was cleared by the new edit
 
 
 
 
 
 
 
 
 
 
 
 
50
  ann.undo()
51
  ann.assign(Selection.from_indices([1], 6), 2)
52
  assert ann.redo() is None # redo stack was cleared by the new edit
53
+
54
+
55
+ def test_assign_survives_a_wholesale_labels_swap():
56
+ # Resuming a previous session replaces cloud.labels with a fresh array;
57
+ # the controller must write into the new one, not a cached reference to
58
+ # the old (the "labelling silently does nothing after resume" bug).
59
+ cloud = _cloud()
60
+ ann = AnnotationController(cloud)
61
+ cloud.labels = np.array([7, 7, 0, 0, 0, 0], np.int32)
62
+ ann.assign(Selection.from_indices([2], 6), class_id=5)
63
+ assert cloud.labels.tolist() == [7, 7, 5, 0, 0, 0]
tests/test_apairo_channel.py CHANGED
@@ -286,3 +286,26 @@ def test_apairo_info_false_for_loose_cloud(tmp_path):
286
  svc = AnnotationService()
287
  svc.open_cloud(str(path))
288
  assert svc.apairo_info()["is_apairo"] is False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  svc = AnnotationService()
287
  svc.open_cloud(str(path))
288
  assert svc.apairo_info()["is_apairo"] is False
289
+
290
+
291
+ def test_assign_after_resume_reaches_the_resumed_labels(tmp_path):
292
+ # Resume replaces cloud.labels wholesale; a later assign must land in the
293
+ # resumed array (regression: the controller kept writing into the
294
+ # pre-resume array, so labelling after a resume silently did nothing).
295
+ from toaster.api.service import AnnotationService
296
+
297
+ seq = _apairo_seq(tmp_path)
298
+ frame = seq / "ouster_points" / "000000.npy"
299
+ np.save(frame, np.zeros((4, 4), np.float32))
300
+ gt = seq / "ground_truth"
301
+ gt.mkdir()
302
+ np.save(gt / "000000.npy", np.array([1, 1, 0, 0], np.int32))
303
+
304
+ svc = AnnotationService()
305
+ svc.open_cloud(str(frame))
306
+ assert svc.apairo_resume_info()["available"] is True
307
+ svc.resume_apairo()
308
+ assert not svc._ctl().session.history.can_undo() # pre-resume edits dropped
309
+ svc.box([2])
310
+ svc.assign(class_id=5)
311
+ assert svc._ctl().label_array().tolist() == [1, 1, 5, 0]
toaster/api/service.py CHANGED
@@ -368,6 +368,10 @@ class AnnotationService:
368
  f"'{channel}' frame {target.stem} has {full.shape[0]} labels, expected {expected}"
369
  )
370
  cloud.labels = cloud.from_source_frame(full)
 
 
 
 
371
  return self.state()
372
 
373
  def apairo_nav(self) -> dict[str, Any]:
 
368
  f"'{channel}' frame {target.stem} has {full.shape[0]} labels, expected {expected}"
369
  )
370
  cloud.labels = cloud.from_source_frame(full)
371
+ # The resume replaced the labels wholesale: edits recorded before it
372
+ # refer to values from the pre-resume array — undoing across the swap
373
+ # would splice stale values into the resumed labels.
374
+ session.history.clear()
375
  return self.state()
376
 
377
  def apairo_nav(self) -> dict[str, Any]:
toaster/core/annotation.py CHANGED
@@ -94,7 +94,14 @@ class AnnotationController:
94
  ) -> None:
95
  self.cloud = cloud
96
  self.history = history if history is not None else EditHistory()
97
- self._labels = cloud.ensure_labels(unlabeled_id)
 
 
 
 
 
 
 
98
 
99
  def assign(self, selection: Selection, class_id: int) -> np.ndarray:
100
  """Write ``class_id`` to every selected point; return the touched indices.
 
94
  ) -> None:
95
  self.cloud = cloud
96
  self.history = history if history is not None else EditHistory()
97
+ cloud.ensure_labels(unlabeled_id)
98
+
99
+ @property
100
+ def _labels(self) -> np.ndarray:
101
+ # Always read through the cloud: anything that swaps the array wholesale
102
+ # (e.g. resuming a previous session's labels) must not leave this
103
+ # controller writing into the orphaned old array.
104
+ return self.cloud.labels
105
 
106
  def assign(self, selection: Selection, class_id: int) -> np.ndarray:
107
  """Write ``class_id`` to every selected point; return the touched indices.