@@ -31,12 +31,13 @@ class Sam3VideoFrameResult:
3131 object_score_logits : mx .array # [num_obj]
3232
3333
34- def _box_to_corner_points (box : tuple [float , float , float , float ]) -> tuple [ mx .array , mx . array ] :
35- """xyxy box (prompt-encoder input-image coords) -> SAM corner points (TL= 2, BR=3) ."""
34+ def _box_corner_points (box : tuple [float , float , float , float ]) -> mx .array :
35+ """xyxy box (prompt-encoder input-image coords) -> 2 SAM corner points ``[ 2, 2]`` ."""
3636 x0 , y0 , x1 , y1 = (float (v ) for v in box )
37- coords = mx .array ([[[x0 , y0 ], [x1 , y1 ]]]) # [1, 2, 2]
38- labels = mx .array ([[2.0 , 3.0 ]]) # [1, 2]
39- return coords , labels
37+ return mx .array ([[x0 , y0 ], [x1 , y1 ]])
38+
39+
40+ _BOX_CORNER_LABELS = (2.0 , 3.0 ) # SAM box corners: top-left=2, bottom-right=3
4041
4142
4243@dataclass
@@ -53,7 +54,7 @@ def __init__(self, model: Sam3VideoModel, *, num_maskmem: int | None = None):
5354 self .model = model
5455 self .tracker = model .tracker_model
5556 self .num_maskmem = num_maskmem if num_maskmem is not None else self .tracker .num_maskmem
56- self ._prompt : _Prompt | None = None
57+ self ._prompts : list [ _Prompt ] = []
5758
5859 @classmethod
5960 def from_tracker (cls , tracker : Sam3TrackerVideoModel , * , num_maskmem : int | None = None ) -> "Sam3VideoSession" :
@@ -62,11 +63,12 @@ def from_tracker(cls, tracker: Sam3TrackerVideoModel, *, num_maskmem: int | None
6263 session .model = None
6364 session .tracker = tracker
6465 session .num_maskmem = num_maskmem if num_maskmem is not None else tracker .num_maskmem
65- session ._prompt = None
66+ session ._prompts = []
6667 return session
6768
6869 def add_box_prompt (self , frame_index : int , box , object_id : int ) -> None :
69- self ._prompt = _Prompt (frame_index = int (frame_index ), box = tuple (box ), object_id = int (object_id ))
70+ """Seed an object with a box prompt. Multiple objects may be added (multiplex batch)."""
71+ self ._prompts .append (_Prompt (frame_index = int (frame_index ), box = tuple (box ), object_id = int (object_id )))
7072
7173 def propagate (self , pixel_values_per_frame ) -> list [Sam3VideoFrameResult ]:
7274 """Extract per-frame tracker features via the detector, then run the streaming loop."""
@@ -75,34 +77,60 @@ def propagate(self, pixel_values_per_frame) -> list[Sam3VideoFrameResult]:
7577 features = [self .model .extract_tracker_features (pv ) for pv in pixel_values_per_frame ]
7678 return self .run_from_features (features )
7779
80+ @staticmethod
81+ def _to_object_batch (x : mx .array , num_objects : int ) -> mx .array :
82+ """Broadcast a single shared-frame tensor ``[1, ...]`` to the object batch ``[B, ...]``."""
83+ if x .shape [0 ] == num_objects :
84+ return x
85+ if x .shape [0 ] != 1 :
86+ raise ValueError (f"expected frame tensor with leading dim 1 or { num_objects } , got { x .shape [0 ]} " )
87+ return mx .repeat (x , num_objects , axis = 0 )
88+
89+ def _seed_point_inputs (self , prompts : list [_Prompt ]) -> tuple [mx .array , mx .array ]:
90+ """Batched box-corner prompts for the seed frame -> ``(coords [B,2,2], labels [B,2])``."""
91+ coords = mx .stack ([_box_corner_points (p .box ) for p in prompts ], axis = 0 )
92+ labels = mx .broadcast_to (mx .array ([_BOX_CORNER_LABELS ]), (len (prompts ), len (_BOX_CORNER_LABELS )))
93+ return coords , labels
94+
7895 def run_from_features (self , per_frame_features ) -> list [Sam3VideoFrameResult ]:
7996 """Run the memory-propagation loop over pre-extracted per-frame features.
8097
81- Each entry is ``(image_embeddings, image_positional_embeddings, high_res_features)``;
82- ``high_res_features`` is ``[4g-res, 2g-res]`` raw FPN levels (NHWC).
98+ Each entry is ``(image_embeddings, image_positional_embeddings, high_res_features)`` for a
99+ single frame (shared across objects); ``high_res_features`` is ``[4g-res, 2g-res]`` raw FPN
100+ levels (NHWC). All seeded objects are tracked together as the batch dimension ``B`` (Object
101+ Multiplex): each object carries its own memory + box prompt; per frame they run through one
102+ ``track_step`` over the shared frame features.
83103 """
84- if self ._prompt is None :
104+ if not self ._prompts :
85105 raise ValueError ("Sam3VideoSession requires a prompt; call add_box_prompt first" )
86- prompt = self ._prompt
106+ seed_frames = {p .frame_index for p in self ._prompts }
107+ if len (seed_frames ) != 1 :
108+ raise ValueError (
109+ "Sam3VideoSession batches objects that share one seed frame; per-object seed frames / "
110+ "mid-clip spawning is handled by the association layer (later slice)"
111+ )
112+ prompts = self ._prompts
113+ seed_frame = prompts [0 ].frame_index
114+ object_ids = [p .object_id for p in prompts ]
115+ num_objects = len (prompts )
87116 bank : list [Sam3TrackerStageOutput ] = []
88117 results : list [Sam3VideoFrameResult ] = []
89118 for frame_index , (vision_features , vision_pos , high_res_features ) in enumerate (per_frame_features ):
90- is_init = frame_index == prompt .frame_index
91- point_inputs = _box_to_corner_points (prompt .box ) if is_init else None
119+ is_init = frame_index == seed_frame
92120 out = self .tracker .track_step (
93- vision_features = vision_features ,
94- vision_pos = vision_pos ,
95- high_res_features = high_res_features ,
121+ vision_features = self . _to_object_batch ( vision_features , num_objects ) ,
122+ vision_pos = self . _to_object_batch ( vision_pos , num_objects ) ,
123+ high_res_features = [ self . _to_object_batch ( h , num_objects ) for h in high_res_features ] ,
96124 is_init_cond_frame = is_init ,
97- point_inputs = point_inputs ,
125+ point_inputs = self . _seed_point_inputs ( prompts ) if is_init else None ,
98126 previous_frames = bank if bank else None ,
99127 )
100128 bank .append (out )
101129 results .append (
102130 Sam3VideoFrameResult (
103131 frame_index = frame_index ,
104- object_ids = [ prompt . object_id ] ,
105- masks = out .low_res_masks [:, :, :, 0 ] > 0 ,
132+ object_ids = list ( object_ids ) ,
133+ masks = out .low_res_masks [:, :, :, 0 ] > 0 , # [B, h, w] (one row per object)
106134 object_score_logits = out .object_score_logits .reshape (- 1 ),
107135 )
108136 )
0 commit comments