@@ -240,7 +240,10 @@ def _build_laplacian_pyramid_torch(img: torch.Tensor, levels: int) -> List[torch
240240 """
241241 pyramid : List [torch .Tensor ] = []
242242 current = img
243- for i in _PB .track (range (levels ), levels , "InpaintSuite" ):
243+ # Inner level loop (5 iters): plain `for` — never tracked. Tracking
244+ # here was creating B*levels ProgressBar resets when called inside an
245+ # outer per-frame paste loop, which froze the frontend (Phase 9b).
246+ for i in range (levels ):
244247 h , w = current .shape [2 ], current .shape [3 ]
245248 # Downsample by 2x with Gaussian pre-filter
246249 down = _gaussian_blur_2d (current , sigma = 1.0 )
@@ -283,7 +286,8 @@ def _laplacian_pyramid_blend(img_a: torch.Tensor, img_b: torch.Tensor,
283286 # Build Gaussian pyramid for the mask
284287 mask_pyr : List [torch .Tensor ] = []
285288 current_mask = blend_mask
286- for i in _PB .track (range (levels ), levels , "InpaintSuite" ):
289+ # Inner level loop: plain `for` (see _build_laplacian_pyramid_torch).
290+ for i in range (levels ):
287291 mask_pyr .append (current_mask )
288292 h , w = current_mask .shape [2 ], current_mask .shape [3 ]
289293 current_mask = F .interpolate (current_mask , size = (max (1 , h // 2 ), max (1 , w // 2 )),
@@ -482,7 +486,8 @@ def _remove_small_regions_torch(mask: torch.Tensor, min_area: int) -> torch.Tens
482486 binary = (mask [b ].cpu ().numpy () > 0.5 ).astype (np .uint8 )
483487 n_labels , labels , stats , _ = cv2 .connectedComponentsWithStats (binary , 8 )
484488 filtered = np .zeros_like (binary )
485- for i in _PB .track (range (1 , n_labels ), None , "InpaintSuite" ):
489+ # Inner labels loop: plain `for` to avoid nested ProgressBar spam.
490+ for i in range (1 , n_labels ):
486491 if stats [i , cv2 .CC_STAT_AREA ] >= min_area :
487492 filtered [labels == i ] = 1
488493 results .append (torch .from_numpy (filtered .astype (np .float32 )))
@@ -689,11 +694,35 @@ def _resize_mask(mask: torch.Tensor, target_h: int, target_w: int,
689694
690695
691696def _resize_lanczos (image : torch .Tensor , target_h : int , target_w : int ) -> torch .Tensor :
692- """Resize (B,H,W,C) via PIL Lanczos (high quality, slightly slower)."""
693- from PIL import Image as PILImage
697+ """Resize (B,H,W,C) high-quality.
698+
699+ For video batches (B > 4) we use torch's GPU bicubic with antialiasing
700+ which is ~50-200x faster than CPU PIL Lanczos and visually identical for
701+ the small downscales used in inpaint stitching. Single-image / small
702+ batches keep the original PIL Lanczos path for max fidelity.
703+ """
694704 B , H , W , C = image .shape
705+ if image .shape [1 ] == target_h and image .shape [2 ] == target_w :
706+ return image
707+ if B > 4 :
708+ # Vectorized GPU path — antialiased bicubic.
709+ img_bchw = image .permute (0 , 3 , 1 , 2 )
710+ try :
711+ resized = F .interpolate (
712+ img_bchw , size = (target_h , target_w ),
713+ mode = "bicubic" , align_corners = False , antialias = True ,
714+ )
715+ except TypeError :
716+ # Older torch without antialias kwarg.
717+ resized = F .interpolate (
718+ img_bchw , size = (target_h , target_w ),
719+ mode = "bicubic" , align_corners = False ,
720+ )
721+ return resized .permute (0 , 2 , 3 , 1 ).clamp (0.0 , 1.0 )
722+
723+ from PIL import Image as PILImage
695724 out = []
696- for i in _PB .track (range (B ), B , "InpaintSuite" ):
725+ for i in _PB .track (range (B ), B , "InpaintSuite: lanczos " ):
697726 _IC .check ()
698727 arr = (image [i ].cpu ().numpy () * 255.0 ).clip (0 , 255 ).astype ("uint8" )
699728 if C == 1 :
@@ -1459,8 +1488,41 @@ def _stitch_v2(self, stitch_data: Dict[str, Any], inpainted_image: torch.Tensor,
14591488 # Build the per-frame blend mask 4D tensor once (shared across frames).
14601489 m3 = blend_mask_crop .unsqueeze (- 1 ) # (B,h,w,1)
14611490
1491+ # Fast path (Phase 9b): when every frame pastes at the same (fx, fy)
1492+ # and the blend is gaussian, do ONE vectorized batched composite
1493+ # instead of B per-frame slices. This is by far the common case
1494+ # (all non-Ronin video and all single-image inpaints) and turns
1495+ # an O(B) Python loop into a single torch op.
1496+ uniform_offsets = (
1497+ len (offsets_iter ) > 1
1498+ and all (o == offsets_iter [0 ] for o in offsets_iter )
1499+ )
1500+ if uniform_offsets and blend_mode == "gaussian" and not color_match :
1501+ fx0 , fy0 = offsets_iter [0 ]
1502+ canvas_crop = canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w , :].clone ()
1503+ mask_b = m3 if m3 .shape [0 ] == B else m3 .expand (B , - 1 , - 1 , - 1 )
1504+ blended = canvas_crop * (1.0 - mask_b ) + inp_resized * mask_b
1505+ canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w , :] = blended
1506+ paste_iter : List [Tuple [int , int ]] = [] # skip slow per-frame loop
1507+ elif uniform_offsets and blend_mode in ("laplacian_pyramid" , "frequency_blend" ) and not color_match :
1508+ # Batched pyramid / FFT blend across the whole batch in one call.
1509+ fx0 , fy0 = offsets_iter [0 ]
1510+ canvas_crop = canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w , :].clone ()
1511+ mask_bb = m3 if m3 .shape [0 ] == B else m3 .expand (B , - 1 , - 1 , - 1 )
1512+ a_bchw = canvas_crop .permute (0 , 3 , 1 , 2 )
1513+ b_bchw = inp_resized .permute (0 , 3 , 1 , 2 )
1514+ m_b1hw = mask_bb .permute (0 , 3 , 1 , 2 )
1515+ if blend_mode == "laplacian_pyramid" :
1516+ blended = _laplacian_pyramid_blend (a_bchw , b_bchw , m_b1hw , levels = 5 )
1517+ else :
1518+ blended = _frequency_blend (a_bchw , b_bchw , m_b1hw )
1519+ canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w , :] = blended .permute (0 , 2 , 3 , 1 ).clamp (0.0 , 1.0 )
1520+ paste_iter = []
1521+ else :
1522+ paste_iter = list (offsets_iter )
1523+
14621524 for b , (fx , fy ) in _PB .track (
1463- list (enumerate (offsets_iter )), len (offsets_iter ),
1525+ list (enumerate (paste_iter )), len (paste_iter ),
14641526 "InpaintStitchProMEC: paste frames" ,
14651527 ):
14661528 canvas_crop = canvas [b :b + 1 , fy :fy + ctc_h , fx :fx + ctc_w , :].clone ()
@@ -1495,13 +1557,22 @@ def _stitch_v2(self, stitch_data: Dict[str, Any], inpainted_image: torch.Tensor,
14951557 H , W = cto_h , cto_w
14961558 blend_canvas = torch .zeros (B , canvas .shape [1 ], canvas .shape [2 ],
14971559 device = device , dtype = canvas .dtype )
1498- for b , (fx , fy ) in _PB .track (
1499- list (enumerate (offsets_iter )), len (offsets_iter ),
1500- "InpaintStitchProMEC: build blend mask" ,
1501- ):
1502- blend_canvas [b , fy :fy + ctc_h , fx :fx + ctc_w ] = (
1503- blend_mask_crop [b ] if blend_mask_crop .shape [0 ] == B else blend_mask_crop [0 ]
1504- )
1560+ # Fast path: when offsets are uniform, paint the blend mask in one
1561+ # broadcast write instead of B per-frame slices.
1562+ if uniform_offsets :
1563+ fx0 , fy0 = offsets_iter [0 ]
1564+ if blend_mask_crop .shape [0 ] == B :
1565+ blend_canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w ] = blend_mask_crop
1566+ else :
1567+ blend_canvas [:, fy0 :fy0 + ctc_h , fx0 :fx0 + ctc_w ] = blend_mask_crop [0 :1 ].expand (B , - 1 , - 1 )
1568+ else :
1569+ for b , (fx , fy ) in _PB .track (
1570+ list (enumerate (offsets_iter )), len (offsets_iter ),
1571+ "InpaintStitchProMEC: build blend mask" ,
1572+ ):
1573+ blend_canvas [b , fy :fy + ctc_h , fx :fx + ctc_w ] = (
1574+ blend_mask_crop [b ] if blend_mask_crop .shape [0 ] == B else blend_mask_crop [0 ]
1575+ )
15051576 blend_mask_full = blend_canvas [:, cto_y :cto_y + cto_h , cto_x :cto_x + cto_w ]
15061577
15071578 color_info = "\n color_match: applied" if color_match else ""
0 commit comments