还原共享抽帧像素格式并在 BgFilter 消费点合成 alpha

撤掉 extract_video_frames_to_png 里无条件的 -pix_fmt rgb24,共享抽帧不再携带 BgFilter 专属策略
prepare_editor_character_animation_bgfilter_input 解码后先按白底合成 alpha 再转 RGB,补上对应回归测试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 03:12:06 +00:00
parent 0150837d69
commit 0f0a8d52df
@@ -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]));