From 0f0a8d52df19037ea23428d759d5f19bd6479278 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 21 Jul 2026 03:12:06 +0000 Subject: [PATCH] =?UTF-8?q?=E8=BF=98=E5=8E=9F=E5=85=B1=E4=BA=AB=E6=8A=BD?= =?UTF-8?q?=E5=B8=A7=E5=83=8F=E7=B4=A0=E6=A0=BC=E5=BC=8F=E5=B9=B6=E5=9C=A8?= =?UTF-8?q?=20BgFilter=20=E6=B6=88=E8=B4=B9=E7=82=B9=E5=90=88=E6=88=90=20a?= =?UTF-8?q?lpha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 撤掉 extract_video_frames_to_png 里无条件的 -pix_fmt rgb24,共享抽帧不再携带 BgFilter 专属策略 prepare_editor_character_animation_bgfilter_input 解码后先按白底合成 alpha 再转 RGB,补上对应回归测试 Co-Authored-By: Claude Fable 5 --- .../src/character_animation_assets.rs | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) 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]));