77from __future__ import annotations
88
99import logging
10+ import math
1011import os
11- from typing import Optional
12+ import time
13+ from typing import List , Optional , Tuple
1214
1315import numpy as np
1416import torch
15- import torch .nn .functional as F
1617
1718from ..utils import (
1819 backend_first_root ,
2829
2930logger = logging .getLogger ("MEC.MaskMatting.ViTMatte" )
3031
32+ _TILE = 512
33+ _MIN_OV = 64
34+ _STRIDE = _TILE - _MIN_OV # 448
35+
3136
3237def _have_transformers () -> bool :
3338 try :
@@ -42,7 +47,6 @@ class ViTMatteMatter(BaseMatter):
4247 KEY = "vitmatte"
4348 DISPLAY = "ViTMatte"
4449 MODELS_KEY = "vitmatte"
45- NEEDS_TRIMAP = True
4650 STATUS = "ready" if _have_transformers () else "missing-deps"
4751
4852 def load (self ) -> None :
@@ -111,29 +115,313 @@ def _is_hf_model_dir(p: str) -> bool:
111115 self ._model = VitMatteForImageMatting .from_pretrained (src , torch_dtype = dtype ).to (self .device ).eval ()
112116 self ._dtype = dtype
113117
118+ # ── single-object path ──────────────────────────────────────────────
119+
114120 def matte (self , image_bhwc , coarse_mask , * , trimap = None , edge_radius = 4 , memory_size = 8 ):
121+ """Single-object matte via per-object optimised tiling.
122+
123+ ``trimap`` is accepted for API compatibility but ignored — the trimap
124+ is derived internally from ``coarse_mask`` and ``edge_radius``.
125+ """
115126 try :
127+ t0 = time .perf_counter ()
116128 self .load ()
117- img = to_bhwc (image_bhwc )
129+ t1 = time .perf_counter ()
130+ img = to_bhwc (image_bhwc )
118131 B , H , W , _ = img .shape
119- if trimap is None :
120- trimap = mask_to_trimap (coarse_mask , dilate = int (edge_radius ) * 2 , erode = int (edge_radius ))
121- trimap = to_mask (trimap )
122-
132+ coarse = to_mask (coarse_mask )
123133 alphas = []
124134 for i in interruptible_range (B , label = "vitmatte" ):
125135 frame = (img [i ].cpu ().numpy () * 255 ).astype (np .uint8 )
126- tri_np = (trimap [i ].cpu ().numpy () * 255 ).astype (np .uint8 )
127- inputs = self ._processor (images = frame , trimaps = tri_np , return_tensors = "pt" )
128- inputs = {k : v .to (self .device , dtype = self ._dtype if v .dtype .is_floating_point else v .dtype ) for k , v in inputs .items ()}
129- with torch .inference_mode (), torch .autocast (self .device , dtype = self ._dtype , enabled = (self .device == "cuda" )):
130- out = self ._model (** inputs )
131- alpha = out .alphas
132- # Resize to original
133- alpha = F .interpolate (alpha , size = (H , W ), mode = "bilinear" , align_corners = False )
134- alphas .append (alpha [0 , 0 ].float ().cpu ())
135- alpha_t = torch .stack (alphas , 0 ).clamp (0 , 1 )
136- return {"alpha" : alpha_t , "info" : {"backend" : self .KEY }}
136+ m_np = coarse [i ].cpu ().numpy ().astype (np .float32 )
137+ ys , xs = np .where (m_np > 0.5 )
138+ if not len (xs ):
139+ alphas .append (torch .zeros (H , W , dtype = torch .float32 ))
140+ continue
141+ bbox = (int (xs .min ()), int (ys .min ()),
142+ int (xs .max ()) + 1 , int (ys .max ()) + 1 )
143+ result = self ._matte_multi_frame (
144+ frame , [m_np ], [bbox ], H , W , int (edge_radius ))
145+ alphas .append (result [0 ])
146+ t2 = time .perf_counter ()
147+ logger .warning ("[ViTMatte] matte (optimised) B=%d load=%.2fs infer=%.2fs total=%.2fs" ,
148+ B , t1 - t0 , t2 - t1 , t2 - t0 )
149+ return {"alpha" : torch .stack (alphas , 0 ).clamp (0 , 1 ),
150+ "info" : {"backend" : self .KEY }}
137151 except Exception :
138152 free_vram ()
139153 raise
154+
155+ # ── multi-object path ────────────────────────────────────────────────
156+
157+ def matte_multi (
158+ self ,
159+ image_bhwc : torch .Tensor ,
160+ object_masks_list : List [torch .Tensor ], # N × [B,H,W] or [H,W]
161+ object_bboxes_list : List [Tuple ], # N × (x0,y0,x1,y1)
162+ * ,
163+ edge_radius : int = 4 ,
164+ memory_size : int = 8 ,
165+ ) -> dict :
166+ """Run VitMatte for N objects with per-object optimised tile placement.
167+
168+ Tiles shared between non-intersecting objects are processed once
169+ (merged trimap); per-object alphas are separated by trimap-presence.
170+
171+ Returns {"alpha", "object_alphas", "object_boxes", "info"}.
172+ """
173+ try :
174+ t0 = time .perf_counter ()
175+ self .load ()
176+ t1 = time .perf_counter ()
177+ img = to_bhwc (image_bhwc )
178+ B , H , W , _ = img .shape
179+ N = len (object_masks_list )
180+
181+ if N == 0 :
182+ zero = torch .zeros (B , H , W )
183+ return {"alpha" : zero , "object_alphas" : [], "object_boxes" : [],
184+ "info" : {"backend" : self .KEY , "n_objects" : 0 }}
185+
186+ per_frame : List [List [torch .Tensor ]] = []
187+ for b in interruptible_range (B , label = "vitmatte-multi" ):
188+ frame_hwc = (img [b ].cpu ().numpy () * 255 ).astype (np .uint8 )
189+ frame_masks = []
190+ for mt in object_masks_list :
191+ m = mt [b ] if mt .ndim == 3 else mt
192+ frame_masks .append (m .cpu ().numpy ().astype (np .float32 ))
193+ per_frame .append (
194+ self ._matte_multi_frame (
195+ frame_hwc , frame_masks , object_bboxes_list , H , W , edge_radius )
196+ )
197+
198+ object_alphas : List [torch .Tensor ] = []
199+ for i in range (N ):
200+ frames = [per_frame [b ][i ] for b in range (B )]
201+ object_alphas .append (torch .stack (frames , 0 ).clamp (0 , 1 ))
202+
203+ merged = torch .stack (object_alphas , 0 ).max (dim = 0 ).values .clamp (0 , 1 )
204+ t2 = time .perf_counter ()
205+ logger .warning (
206+ "[ViTMatte] matte_multi B=%d N=%d load=%.2fs infer=%.2fs total=%.2fs" ,
207+ B , N , t1 - t0 , t2 - t1 , t2 - t0 )
208+ return {
209+ "alpha" : merged ,
210+ "object_alphas" : object_alphas ,
211+ "object_boxes" : object_bboxes_list ,
212+ "info" : {"backend" : self .KEY , "n_objects" : N },
213+ }
214+ except Exception :
215+ free_vram ()
216+ raise
217+
218+ # ── per-frame multi-object implementation ───────────────────────────
219+
220+ def _matte_multi_frame (
221+ self ,
222+ frame_hwc : np .ndarray ,
223+ object_masks : List [np .ndarray ],
224+ object_bboxes : List [Tuple ],
225+ H : int , W : int ,
226+ edge_radius : int ,
227+ ) -> List [torch .Tensor ]:
228+ _t0_frame = time .perf_counter ()
229+ N = len (object_masks )
230+ if N == 0 :
231+ return []
232+
233+ dilate = edge_radius * 2
234+ erode = edge_radius
235+ pad = dilate
236+
237+ # 1. Per-object trimaps (uint8 0/127/255)
238+ trimaps : List [np .ndarray ] = []
239+ for m_np in object_masks :
240+ m_t = torch .from_numpy (m_np ).unsqueeze (0 )
241+ tri_t = mask_to_trimap (m_t , dilate = dilate , erode = erode )
242+ trimaps .append ((tri_t [0 ].cpu ().numpy () * 255 ).astype (np .uint8 ))
243+
244+ # 2. Per-object tile positions
245+ tile_sets : List [List [Tuple [int , int ]]] = []
246+ for i , bbox in enumerate (object_bboxes ):
247+ positions = self ._object_tile_positions (bbox , H , W , pad )
248+ tile_sets .append (positions )
249+ logger .debug ("[ViTMatte] obj %d bbox=%s → %d tile(s)" , i , bbox , len (positions ))
250+
251+ # 3. Tile → object-index mapping
252+ tile_map : dict = {}
253+ for i , tset in enumerate (tile_sets ):
254+ for pos in tset :
255+ tile_map .setdefault (pos , []).append (i )
256+
257+ global_full = (len (self ._global_tile_starts_1d (W , _TILE , _STRIDE )) *
258+ len (self ._global_tile_starts_1d (H , _TILE , _STRIDE )))
259+ logger .warning (
260+ "[ViTMatte] multi-frame N=%d unique_tiles=%d full-image_tiles=%d" ,
261+ N , len (tile_map ), global_full ,
262+ )
263+
264+ # 4. Per-object accumulators
265+ alpha_acc = np .zeros ((N , H , W ), dtype = np .float64 )
266+ weight_acc = np .zeros ((N , H , W ), dtype = np .float64 )
267+
268+ for (tx , ty ), obj_idx in tile_map .items ():
269+ ty1 , tx1 = min (ty + _TILE , H ), min (tx + _TILE , W )
270+ th , tw = ty1 - ty , tx1 - tx
271+
272+ patch_img = frame_hwc [ty :ty1 , tx :tx1 ]
273+ merged_tri = np .zeros ((th , tw ), dtype = np .uint8 )
274+ for i in obj_idx :
275+ merged_tri = np .maximum (merged_tri , trimaps [i ][ty :ty1 , tx :tx1 ])
276+
277+ if th < _TILE or tw < _TILE :
278+ patch_img = np .pad (patch_img ,
279+ ((0 , _TILE - th ), (0 , _TILE - tw ), (0 , 0 )), mode = "reflect" )
280+ merged_tri = np .pad (merged_tri ,
281+ ((0 , _TILE - th ), (0 , _TILE - tw )), mode = "edge" )
282+
283+ inputs = self ._processor (images = patch_img , trimaps = merged_tri , return_tensors = "pt" )
284+ inputs = {
285+ k : v .to (self .device ,
286+ dtype = self ._dtype if v .dtype .is_floating_point else v .dtype )
287+ for k , v in inputs .items ()
288+ }
289+ with torch .inference_mode (), \
290+ torch .autocast (self .device , dtype = self ._dtype ,
291+ enabled = (self .device == "cuda" )):
292+ out = self ._model (** inputs )
293+
294+ patch_alpha = out .alphas [0 , 0 ].float ().cpu ().numpy ()[:th , :tw ]
295+ w = self ._blend_weight (th , tw , _MIN_OV )
296+
297+ for i in obj_idx :
298+ alpha_acc [i , ty :ty1 , tx :tx1 ] += patch_alpha * w
299+ weight_acc [i , ty :ty1 , tx :tx1 ] += w
300+
301+ # 5. Normalise and apply trimap-presence mask
302+ result : List [torch .Tensor ] = []
303+ for i in range (N ):
304+ wgt = weight_acc [i ]
305+ with np .errstate (invalid = "ignore" , divide = "ignore" ):
306+ alpha = np .where (wgt > 1e-8 , alpha_acc [i ] / wgt , 0.0 ).astype (np .float32 )
307+ alpha *= (trimaps [i ] > 0 ).astype (np .float32 )
308+ result .append (torch .from_numpy (alpha ))
309+ elapsed_frame = time .perf_counter () - _t0_frame
310+ logger .warning (
311+ "[ViTMatte] _matte_multi_frame N=%d unique_tiles=%d %.2fs" ,
312+ N , len (tile_map ), elapsed_frame ,
313+ )
314+ return result
315+
316+ # ── tile-placement helpers ───────────────────────────────────────────
317+
318+ @staticmethod
319+ def _global_tile_starts_1d (length : int , tile : int , stride : int ) -> List [int ]:
320+ if length <= tile :
321+ return [0 ]
322+ starts = list (range (0 , length - tile , stride ))
323+ last = length - tile
324+ if not starts or starts [- 1 ] != last :
325+ starts .append (last )
326+ return starts
327+
328+ @staticmethod
329+ def _optimal_tile_starts_1d (
330+ region_start : int , region_end : int ,
331+ img_length : int , tile : int , min_overlap : int ,
332+ ) -> List [int ]:
333+ """Minimum evenly-spaced tiles covering [region_start, region_end]."""
334+ length = region_end - region_start
335+ if length <= 0 :
336+ return []
337+ max_stride = tile - min_overlap
338+ if length <= tile :
339+ centre = region_start + length // 2
340+ s = max (0 , min (img_length - tile , centre - tile // 2 ))
341+ return [s ]
342+ n = 1 + math .ceil ((length - tile ) / max_stride )
343+ first = max (0 , region_start )
344+ last = min (img_length - tile , region_end - tile )
345+ if last < first :
346+ last = first
347+ if n == 1 :
348+ return [first ]
349+ return [round (first + i * (last - first ) / (n - 1 )) for i in range (n )]
350+
351+ @classmethod
352+ def _best_tile_starts_1d (
353+ cls ,
354+ px0 : int , px1 : int ,
355+ img_length : int , tile : int , stride : int , min_overlap : int ,
356+ ) -> List [int ]:
357+ """Per-object optimal when fewer tiles than global grid; else global."""
358+ global_all = cls ._global_tile_starts_1d (img_length , tile , stride )
359+ global_bbox = [t for t in global_all if t + tile > px0 and t < px1 ]
360+ optimal = cls ._optimal_tile_starts_1d (px0 , px1 , img_length , tile , min_overlap )
361+ return optimal if len (optimal ) < len (global_bbox ) else global_bbox
362+
363+ @classmethod
364+ def _object_tile_positions (
365+ cls , bbox : Tuple , H : int , W : int , pad : int = _MIN_OV ,
366+ ) -> List [Tuple [int , int ]]:
367+ x0 , y0 , x1 , y1 = (int (v ) for v in bbox )
368+ px0 , py0 = max (0 , x0 - pad ), max (0 , y0 - pad )
369+ px1 , py1 = min (W , x1 + pad ), min (H , y1 + pad )
370+ xs = cls ._best_tile_starts_1d (px0 , px1 , W , _TILE , _STRIDE , _MIN_OV )
371+ ys = cls ._best_tile_starts_1d (py0 , py1 , H , _TILE , _STRIDE , _MIN_OV )
372+ return [(x , y ) for y in ys for x in xs ]
373+
374+ # ── blend weight ─────────────────────────────────────────────────────
375+
376+ @staticmethod
377+ def _blend_weight (h : int , w : int , overlap : int ) -> np .ndarray :
378+ wy = np .ones (h , dtype = np .float32 )
379+ wx = np .ones (w , dtype = np .float32 )
380+ for i in range (min (overlap , h // 2 )):
381+ v = 0.5 * (1.0 - np .cos (np .pi * (i + 1 ) / (overlap + 1 )))
382+ wy [i ] = min (wy [i ], v ); wy [h - 1 - i ] = min (wy [h - 1 - i ], v )
383+ for i in range (min (overlap , w // 2 )):
384+ v = 0.5 * (1.0 - np .cos (np .pi * (i + 1 ) / (overlap + 1 )))
385+ wx [i ] = min (wx [i ], v ); wx [w - 1 - i ] = min (wx [w - 1 - i ], v )
386+ return np .outer (wy , wx )
387+
388+ # ── self-verification ────────────────────────────────────────────────
389+
390+ @classmethod
391+ def _verify_tile_logic (cls ) -> None :
392+ """Smoke-test tile placement. Raises AssertionError on failure."""
393+ tile , min_ov , stride = _TILE , _MIN_OV , _STRIDE
394+ cases = [
395+ # (rs, re, img_L, exp_n, min_ov_check)
396+ (0 , 512 , 4080 , 1 , None ),
397+ (0 , 511 , 4080 , 1 , None ),
398+ (0 , 960 , 4080 , 2 , 64 ),
399+ (0 , 961 , 4080 , 3 , 64 ),
400+ (200 , 1113 , 4080 , 2 , 64 ), # 913 px — key case
401+ (0 , 1000 , 4080 , 3 , 64 ),
402+ ]
403+ for rs , re , L , n_exp , ov_exp in cases :
404+ pos = cls ._optimal_tile_starts_1d (rs , re , L , tile , min_ov )
405+ assert len (pos ) == n_exp , \
406+ f"_optimal({ rs } ,{ re } ,{ L } ): expected { n_exp } got { len (pos )} : { pos } "
407+ assert pos [0 ] >= 0 and pos [- 1 ] + tile <= L , \
408+ f"out of image: { pos } "
409+ assert pos [0 ] <= max (0 , rs ), \
410+ f"first tile misses region start: { pos [0 ]} > { rs } "
411+ assert pos [- 1 ] + tile >= re , \
412+ f"last tile misses region end: { pos [- 1 ]+ tile } < { re } "
413+ if ov_exp :
414+ for a , b in zip (pos , pos [1 :]):
415+ ov = (a + tile ) - b
416+ assert ov >= ov_exp , \
417+ f"overlap { ov } < { ov_exp } between tiles { a } and { b } "
418+
419+ # _best_tile_starts_1d: 913-px region → 2 optimal vs 3 global
420+ best = cls ._best_tile_starts_1d (200 , 1113 , 4080 , tile , stride , min_ov )
421+ assert len (best ) == 2 , f"expected 2 best tiles for [200,1113], got { len (best )} : { best } "
422+
423+ # _object_tile_positions: 913×700 bbox with pad=8
424+ tiles = cls ._object_tile_positions ((200 , 100 , 1113 , 800 ), 3072 , 4080 , pad = 8 )
425+ # x: [192,1121] = 929 px → 2 tiles; y: [92,808] = 716 px → 2 tiles → 4
426+ assert len (tiles ) == 4 , f"expected 4 tiles for 913×700, got { len (tiles )} : { tiles } "
427+ logger .warning ("[ViTMatte] _verify_tile_logic: all checks passed" )
0 commit comments