diff --git a/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs b/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs index d95b4675..ac23daf0 100644 --- a/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs +++ b/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs @@ -38,6 +38,10 @@ pub struct GeneratedAssetSheetAlphaOptions { pub key_color: GeneratedAssetSheetKeyColor, pub remove_near_white_background: bool, pub remove_disconnected_hard_key_background: bool, + // 中文注释:检测并清除被主体包围、不与画布四边连通的品红镂空区域。 + // 仅对独立连通域整体判定,通过 min_pixels 过滤微小噪点。 + pub detect_internal_holes: bool, + pub internal_hole_min_pixels: usize, } impl GeneratedAssetSheetAlphaOptions { @@ -46,6 +50,8 @@ impl GeneratedAssetSheetAlphaOptions { key_color: GeneratedAssetSheetKeyColor::GREEN_SCREEN, remove_near_white_background: true, remove_disconnected_hard_key_background: true, + detect_internal_holes: false, + internal_hole_min_pixels: 0, } } @@ -54,6 +60,8 @@ impl GeneratedAssetSheetAlphaOptions { key_color: GeneratedAssetSheetKeyColor::MAGENTA_SCREEN, remove_near_white_background: false, remove_disconnected_hard_key_background: false, + detect_internal_holes: true, + internal_hole_min_pixels: 16, } } } @@ -216,6 +224,66 @@ fn remove_generated_asset_sheet_green_screen_background( } } + // 中文注释:内部镂空洞检测——寻找与四边不连通、被主体包围的品红区域。 + // 必须在软 matte 扩展之前执行,避免软扩展跨越窄前景通道误判。 + if options.detect_internal_holes && options.internal_hole_min_pixels > 0 { + let mut hole_visited = vec![false; pixel_count]; + let mut hole_queue = Vec::::new(); + + for start_index in 0..pixel_count { + if background_mask[start_index] != 0 + || key_scores[start_index] < GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE + || hole_visited[start_index] + { + continue; + } + + // 中文注释:BFS 收集当前候选背景连通域 + hole_queue.clear(); + hole_queue.push(start_index); + hole_visited[start_index] = true; + let mut component = Vec::::new(); + let mut touches_border = false; + let mut queue_cursor = 0usize; + + while queue_cursor < hole_queue.len() { + let pixel_index = hole_queue[queue_cursor]; + queue_cursor += 1; + component.push(pixel_index); + + let x = pixel_index % width; + let y = pixel_index / width; + if x == 0 || x == width.saturating_sub(1) || y == 0 || y == height.saturating_sub(1) { + touches_border = true; + } + + let neighbors = [ + if x > 0 { Some(pixel_index - 1) } else { None }, + if x + 1 < width { Some(pixel_index + 1) } else { None }, + if y > 0 { Some(pixel_index - width) } else { None }, + if y + 1 < height { Some(pixel_index + width) } else { None }, + ]; + + for next in neighbors.into_iter().flatten() { + if background_mask[next] != 0 || hole_visited[next] { + continue; + } + if key_scores[next] < GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE { + continue; + } + hole_visited[next] = true; + hole_queue.push(next); + } + } + + if !touches_border && component.len() >= options.internal_hole_min_pixels { + for pixel_index in component { + background_mask[pixel_index] = 1; + } + } + } + } + let soft_green_cleanup_rounds = (width.min(height) / 40).clamp(4, 14); for _ in 0..soft_green_cleanup_rounds { let mut expanded_mask = background_mask.clone();