diff --git a/server-rs/crates/api-server/src/character_animation_assets.rs b/server-rs/crates/api-server/src/character_animation_assets.rs index 62d308a7b..43aa0111b 100644 --- a/server-rs/crates/api-server/src/character_animation_assets.rs +++ b/server-rs/crates/api-server/src/character_animation_assets.rs @@ -4334,8 +4334,6 @@ fn extract_video_frames_to_png( format!("[f{frame_index}]"), "-frames:v".to_string(), "1".to_string(), - "-pix_fmt".to_string(), - "rgb24".to_string(), "-f".to_string(), "image2".to_string(), output_path.to_string_lossy().into_owned(), @@ -4471,6 +4469,25 @@ fn run_process_with_timeout( } } +/// BgFilter 只接受不透明 RGB 输入;直接丢弃 alpha 会让全透明像素下未定义的 +/// RGB 值以杂色进入抠图,这里先按白底合成再转 RGB。 +fn flatten_alpha_onto_white_rgb(image: image::DynamicImage) -> image::RgbImage { + if !image.color().has_alpha() { + return image.to_rgb8(); + } + let rgba = image.to_rgba8(); + let mut flattened = image::RgbImage::new(rgba.width(), rgba.height()); + for (source, target) in rgba.pixels().zip(flattened.pixels_mut()) { + let [red, green, blue, alpha] = source.0; + let opacity = f32::from(alpha) / 255.0; + let blend = |channel: u8| -> u8 { + (f32::from(channel) * opacity + 255.0 * (1.0 - opacity)).round() as u8 + }; + target.0 = [blend(red), blend(green), blend(blue)]; + } + flattened +} + fn prepare_editor_character_animation_bgfilter_input( source: &[u8], mime_type: &str, @@ -4486,8 +4503,8 @@ fn prepare_editor_character_animation_bgfilter_input( "provider": "character-animation", "message": format!("解析 BgFilter 输入动作帧图片失败:{error}"), })) - })? - .to_rgb8(); + })?; + let image = flatten_alpha_onto_white_rgb(image); let (draw_width, draw_height) = compute_contain_dimensions(image.width(), image.height(), target_width, target_height); @@ -6478,6 +6495,38 @@ mod tests { ); } + #[test] + fn editor_character_animation_bgfilter_input_flattens_alpha_onto_white() { + let mut source = RgbaImage::from_pixel(8, 8, Rgba([17, 99, 201, 255])); + source.put_pixel(0, 0, Rgba([255, 0, 0, 0])); + source.put_pixel(1, 0, Rgba([0, 0, 0, 127])); + let mut source_png = Vec::new(); + PngEncoder::new(&mut source_png) + .write_image( + source.as_raw(), + source.width(), + source.height(), + ColorType::Rgba8.into(), + ) + .expect("source RGBA frame should encode"); + + let prepared = prepare_editor_character_animation_bgfilter_input( + source_png.as_slice(), + "image/png", + 8, + 8, + ) + .expect("BgFilter input should be prepared"); + let decoded = + image::load_from_memory_with_format(prepared.bytes.as_slice(), ImageFormat::Png) + .expect("prepared BgFilter input should decode") + .to_rgb8(); + + assert_eq!(decoded.get_pixel(0, 0).0, [255, 255, 255]); + assert_eq!(decoded.get_pixel(1, 0).0, [128, 128, 128]); + assert_eq!(decoded.get_pixel(2, 0).0, [17, 99, 201]); + } + #[test] fn editor_character_animation_final_frame_adds_transparent_vertical_padding() { let source = RgbaImage::from_pixel(323, 434, Rgba([31, 127, 223, 191]));