Zhen Ye Claude Opus 4.6 (1M context) commited on
Commit
a67c231
·
1 Parent(s): 9f46868

feat(inspection): persist RLE masks during segmentation inference

Browse files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. inference.py +18 -2
  2. jobs/storage.py +36 -0
inference.py CHANGED
@@ -22,6 +22,7 @@ from models.segmenters.model_loader import load_segmenter, load_segmenter_on_dev
22
  from models.depth_estimators.model_loader import load_depth_estimator, load_depth_estimator_on_device
23
  from utils.video import StreamingVideoWriter
24
  from jobs.storage import set_track_data, store_latest_frame
 
25
  import tempfile
26
  import json as json_module
27
 
@@ -1253,6 +1254,7 @@ def run_grounded_sam2_tracking(
1253
  # --- ObjectInfo → detection dict adapter ---
1254
  def _objectinfo_to_dets(frame_objects_dict):
1255
  dets = []
 
1256
  for obj_id, info in frame_objects_dict.items():
1257
  dets.append({
1258
  "label": info.class_name,
@@ -1261,7 +1263,17 @@ def run_grounded_sam2_tracking(
1261
  "track_id": f"T{obj_id:02d}",
1262
  "instance_id": obj_id,
1263
  })
1264
- return dets
 
 
 
 
 
 
 
 
 
 
1265
 
1266
  # Shared streaming state (publisher ↔ writer)
1267
  _stream_deque: collections.deque = collections.deque() # unbounded — publisher drains at its own pace
@@ -1300,7 +1312,7 @@ def run_grounded_sam2_tracking(
1300
 
1301
  # Build detection records and track history
1302
  if fobjs:
1303
- dets = _objectinfo_to_dets(fobjs)
1304
 
1305
  # Maintain per-track bbox history (30-frame window)
1306
  for det in dets:
@@ -1316,6 +1328,10 @@ def run_grounded_sam2_tracking(
1316
  if job_id:
1317
  set_track_data(job_id, next_idx, copy.deepcopy(dets))
1318
  store_latest_frame(job_id, frm)
 
 
 
 
1319
  else:
1320
  if job_id:
1321
  set_track_data(job_id, next_idx, [])
 
22
  from models.depth_estimators.model_loader import load_depth_estimator, load_depth_estimator_on_device
23
  from utils.video import StreamingVideoWriter
24
  from jobs.storage import set_track_data, store_latest_frame
25
+ from inspection.masks import rle_encode
26
  import tempfile
27
  import json as json_module
28
 
 
1254
  # --- ObjectInfo → detection dict adapter ---
1255
  def _objectinfo_to_dets(frame_objects_dict):
1256
  dets = []
1257
+ masks_rle = {} # {instance_id: rle_dict}
1258
  for obj_id, info in frame_objects_dict.items():
1259
  dets.append({
1260
  "label": info.class_name,
 
1263
  "track_id": f"T{obj_id:02d}",
1264
  "instance_id": obj_id,
1265
  })
1266
+ # RLE-encode the mask if present
1267
+ if info.mask is not None:
1268
+ try:
1269
+ if isinstance(info.mask, torch.Tensor):
1270
+ mask_np = info.mask.cpu().numpy().astype(bool)
1271
+ else:
1272
+ mask_np = np.asarray(info.mask).astype(bool)
1273
+ masks_rle[obj_id] = rle_encode(mask_np)
1274
+ except Exception:
1275
+ logging.debug("Failed to RLE-encode mask for obj %d", obj_id)
1276
+ return dets, masks_rle
1277
 
1278
  # Shared streaming state (publisher ↔ writer)
1279
  _stream_deque: collections.deque = collections.deque() # unbounded — publisher drains at its own pace
 
1312
 
1313
  # Build detection records and track history
1314
  if fobjs:
1315
+ dets, masks_rle = _objectinfo_to_dets(fobjs)
1316
 
1317
  # Maintain per-track bbox history (30-frame window)
1318
  for det in dets:
 
1328
  if job_id:
1329
  set_track_data(job_id, next_idx, copy.deepcopy(dets))
1330
  store_latest_frame(job_id, frm)
1331
+ # Store masks
1332
+ from jobs.storage import set_mask_data as _set_mask
1333
+ for iid, rle in masks_rle.items():
1334
+ _set_mask(job_id, next_idx, iid, rle)
1335
  else:
1336
  if job_id:
1337
  set_track_data(job_id, next_idx, [])
jobs/storage.py CHANGED
@@ -40,6 +40,7 @@ class JobStorage:
40
  self._jobs: Dict[str, JobInfo] = {}
41
  self._tracks: Dict[str, Dict[int, list]] = {} # job_id -> {frame_idx -> tracks}
42
  self._latest_frames: Dict[str, any] = {} # job_id -> np.ndarray
 
43
  self._lock = RLock()
44
 
45
  def create(self, job: JobInfo) -> None:
@@ -78,6 +79,31 @@ class JobStorage:
78
  """Get the most recent frame for ISR cropping."""
79
  return self._latest_frames.get(job_id)
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def get(self, job_id: str) -> Optional[JobInfo]:
82
  with self._lock:
83
  return self._jobs.get(job_id)
@@ -95,6 +121,7 @@ class JobStorage:
95
  self._jobs.pop(job_id, None)
96
  self._tracks.pop(job_id, None)
97
  self._latest_frames.pop(job_id, None)
 
98
  shutil.rmtree(get_job_directory(job_id), ignore_errors=True)
99
 
100
  def cleanup_expired(self, max_age: timedelta) -> None:
@@ -131,3 +158,12 @@ def store_latest_frame(job_id: str, frame) -> None:
131
 
132
  def get_latest_frame(job_id: str):
133
  return get_job_storage().get_latest_frame(job_id)
 
 
 
 
 
 
 
 
 
 
40
  self._jobs: Dict[str, JobInfo] = {}
41
  self._tracks: Dict[str, Dict[int, list]] = {} # job_id -> {frame_idx -> tracks}
42
  self._latest_frames: Dict[str, any] = {} # job_id -> np.ndarray
43
+ self._mask_data: Dict[str, Dict[str, any]] = {} # job_id -> {f"{frame_idx}:{track_id}" -> rle_dict}
44
  self._lock = RLock()
45
 
46
  def create(self, job: JobInfo) -> None:
 
79
  """Get the most recent frame for ISR cropping."""
80
  return self._latest_frames.get(job_id)
81
 
82
+ def set_mask_data(self, job_id: str, frame_idx: int, track_id: int, rle: dict) -> None:
83
+ """Store RLE mask for a specific object at a specific frame."""
84
+ with self._lock:
85
+ if job_id not in self._mask_data:
86
+ self._mask_data[job_id] = {}
87
+ key = f"{frame_idx}:{track_id}"
88
+ self._mask_data[job_id][key] = rle
89
+
90
+ def get_mask_data(self, job_id: str, frame_idx: int, track_id: int) -> dict | None:
91
+ """Retrieve RLE mask for a specific object at a specific frame."""
92
+ with self._lock:
93
+ key = f"{frame_idx}:{track_id}"
94
+ return self._mask_data.get(job_id, {}).get(key)
95
+
96
+ def get_all_masks_for_frame(self, job_id: str, frame_idx: int) -> dict:
97
+ """Return {track_id: rle_dict} for all objects in a frame."""
98
+ with self._lock:
99
+ prefix = f"{frame_idx}:"
100
+ result = {}
101
+ for key, rle in self._mask_data.get(job_id, {}).items():
102
+ if key.startswith(prefix):
103
+ tid = int(key.split(":")[1])
104
+ result[tid] = rle
105
+ return result
106
+
107
  def get(self, job_id: str) -> Optional[JobInfo]:
108
  with self._lock:
109
  return self._jobs.get(job_id)
 
121
  self._jobs.pop(job_id, None)
122
  self._tracks.pop(job_id, None)
123
  self._latest_frames.pop(job_id, None)
124
+ self._mask_data.pop(job_id, None)
125
  shutil.rmtree(get_job_directory(job_id), ignore_errors=True)
126
 
127
  def cleanup_expired(self, max_age: timedelta) -> None:
 
158
 
159
  def get_latest_frame(job_id: str):
160
  return get_job_storage().get_latest_frame(job_id)
161
+
162
+ def set_mask_data(job_id: str, frame_idx: int, track_id: int, rle: dict) -> None:
163
+ get_job_storage().set_mask_data(job_id, frame_idx, track_id, rle)
164
+
165
+ def get_mask_data(job_id: str, frame_idx: int, track_id: int) -> dict | None:
166
+ return get_job_storage().get_mask_data(job_id, frame_idx, track_id)
167
+
168
+ def get_all_masks_for_frame(job_id: str, frame_idx: int) -> dict:
169
+ return get_job_storage().get_all_masks_for_frame(job_id, frame_idx)