fix(jump-hop): clean magenta cutout fringes
This commit is contained in:
@@ -146,7 +146,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
|
||||
|
||||
1. 创作端只保留主题输入,作品标题、简介、标签和地块提示词由系统派生;
|
||||
2. v1 不再单独生成角色图片,运行态固定使用抠除白底后的陶泥儿 logo 透明 PNG 作为玩家角色;
|
||||
3. 地块只调用一次 image2,输出一张 `5行*5列`、`1:1`、单一纯洋红 `#FF00FF` key 背景的主题地块图集;跳一跳地块常包含草地、花、雪、白石和云朵,后端透明化必须使用跳一跳专用洋红 key,不启用近白底扣除,也不清理非边缘连通的 key 色像素,避免把绿色或白色主体误扣;地块造型提示词要求以主题物体本身外轮廓为准,允许苹果近似圆形、香蕉近似长条或长方形、西瓜近似扇形等自然差异,只统一单格规格、安全留白、正面30度视角和 2D/2.5D 手绘风格包装;所有地块素材必须保持统一正面30度视角,相机位于物体正前方略高位置、镜头向下约30度,必须看到清晰正面、侧壁、下沿、明显自身厚度和少量上表面,主体正面或侧壁可见面积必须接近或大于顶面面积,顶面只能作为辅助可见面;水果主题需要明确要求橙瓣看到橙皮正面外侧和果肉厚度、椰子看到壳的正面侧壁和切口厚度、浆果不能只是从上往下看的圆形球顶;避免生成纯俯视、正上方俯拍、鸟瞰地图块、平铺俯拍、圆形顶视图或扁平图标;主题物体本身必须是唯一可落脚体,只能用自身切面、边缘厚度、花瓣层或果皮边表现承重,禁止在主题物体下方额外垫石台、土墩、木板、圆台、托盘、岛屿底座或通用地板;前端和后端默认 `tilePrompt` 都必须使用“正面30度视角主题物体图集,物体本身作为跳跃落点”的口径,不再提交“平台素材 / 跳台 / 地块 / 地砖”等会把模型拉回通用平台造型的词,后端生成前也会清洗旧草稿遗留的这些词;
|
||||
3. 地块只调用一次 image2,输出一张 `5行*5列`、`1:1`、单一纯洋红 `#FF00FF` key 背景的主题地块图集;跳一跳地块常包含草地、花、雪、白石和云朵,后端透明化必须使用跳一跳专用洋红 key,不启用近白底扣除,也不清理非边缘连通的 key 色像素,避免把绿色或白色主体误扣;后处理必须对边缘连通 key 色做容差清理、去彩边 defringe 和底部残影清理,主体图不得自带洋红阴影、紫色底边、粉色脏边、彩色光晕或发光底边,运行态阴影统一由 DOM 绘制;地块造型提示词要求以主题物体本身外轮廓为准,允许苹果近似圆形、香蕉近似长条或长方形、西瓜近似扇形等自然差异,只统一单格规格、安全留白、正面30度视角和 2D/2.5D 手绘风格包装;所有地块素材必须保持统一正面30度视角,相机位于物体正前方略高位置、镜头向下约30度,必须看到清晰正面、侧壁、下沿、明显自身厚度和少量上表面,主体正面或侧壁可见面积必须接近或大于顶面面积,顶面只能作为辅助可见面;水果主题需要明确要求橙瓣看到橙皮正面外侧和果肉厚度、椰子看到壳的正面侧壁和切口厚度、浆果不能只是从上往下看的圆形球顶;避免生成纯俯视、正上方俯拍、鸟瞰地图块、平铺俯拍、圆形顶视图或扁平图标;主题物体本身必须是唯一可落脚体,只能用自身切面、边缘厚度、花瓣层或果皮边表现承重,禁止在主题物体下方额外垫石台、土墩、木板、圆台、托盘、岛屿底座或通用地板;前端和后端默认 `tilePrompt` 都必须使用“正面30度视角主题物体图集,物体本身作为跳跃落点”的口径,不再提交“平台素材 / 跳台 / 地块 / 地砖”等会把模型拉回通用平台造型的词,后端生成前也会清洗旧草稿遗留的这些词;
|
||||
4. 背景底图同样由 image2 生成,复用现有 `coverComposite` / `coverImageSrc` 作为运行态背景读写字段,OSS 槽位固定为 `background/image.png`,不新增 SpacetimeDB 字段;提示词必须严格以用户主题关键词为背景主题,结构以左右两侧氛围为主,中央纵轴 1/2 区域保持少元素、简洁、可读且有纵深感,两侧允许更强立体层次和行进感;背景只作为底图,禁止生成跳板、地块、落脚物、角色、UI、文字、路径箭头或海报排版;
|
||||
5. 后端按从上到下、从左到右均匀切分为 `tile-01` 到 `tile-25` 的透明 PNG,每个切片必须使用唯一 slot/path 持久化,不能按重复的 `tileType` 复用槽位;
|
||||
6. 结果页只展示陶泥儿 logo 透明角色预览、地块池预览和首屏 3 地块预览;不再提供旧角色图生成槽。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -385,7 +385,13 @@ fn remove_generated_asset_sheet_green_screen_background(
|
||||
let mut red = pixels[offset] as f32;
|
||||
let mut green = pixels[offset + 1] as f32;
|
||||
let mut blue = pixels[offset + 2] as f32;
|
||||
let blend = clamp_generated_asset_sheet_unit(contamination.max(0.22));
|
||||
let blend = if options.key_color.is_green_screen() {
|
||||
clamp_generated_asset_sheet_unit(contamination.max(0.22))
|
||||
} else {
|
||||
// 中文注释:洋红 / 青色等非绿幕 key 的残留更容易表现成彩边,
|
||||
// 需要比绿幕更强地向主体邻近色收敛,避免 PNG 边缘继续带 key 色。
|
||||
clamp_generated_asset_sheet_unit((key_score * 1.35).max(contamination).max(0.28))
|
||||
};
|
||||
|
||||
if let Some((sample_red, sample_green, sample_blue)) = sample {
|
||||
red = lerp_generated_asset_sheet_channel(red, sample_red as f32, blend);
|
||||
@@ -400,6 +406,17 @@ fn remove_generated_asset_sheet_green_screen_background(
|
||||
green = green.min(sample_green as f32 + 26.0);
|
||||
blue = blue.min(sample_blue as f32 + 26.0);
|
||||
}
|
||||
if !options.key_color.is_green_screen() && key_score > 0.04 {
|
||||
let defringed = suppress_generated_asset_sheet_key_color_fringe(
|
||||
[red, green, blue],
|
||||
[sample_red as f32, sample_green as f32, sample_blue as f32],
|
||||
key_score,
|
||||
options.key_color,
|
||||
);
|
||||
red = defringed[0];
|
||||
green = defringed[1];
|
||||
blue = defringed[2];
|
||||
}
|
||||
} else {
|
||||
if options.key_color.is_green_screen() && key_score > 0.04 {
|
||||
let toned_green = (green - (green - red.max(blue)) * 0.78)
|
||||
@@ -417,10 +434,26 @@ fn remove_generated_asset_sheet_green_screen_background(
|
||||
blue = blue.min(toned_value);
|
||||
}
|
||||
}
|
||||
if !options.key_color.is_green_screen() && key_score > 0.04 {
|
||||
let neutral = (red + green + blue) / 3.0;
|
||||
let defringed = suppress_generated_asset_sheet_key_color_fringe(
|
||||
[red, green, blue],
|
||||
[neutral, neutral, neutral],
|
||||
key_score,
|
||||
options.key_color,
|
||||
);
|
||||
red = defringed[0];
|
||||
green = defringed[1];
|
||||
blue = defringed[2];
|
||||
}
|
||||
}
|
||||
|
||||
let mut next_alpha = alpha;
|
||||
let edge_fade = (key_score * 0.35).max(white_score * 0.28);
|
||||
let edge_fade = if options.key_color.is_green_screen() {
|
||||
(key_score * 0.35).max(white_score * 0.28)
|
||||
} else {
|
||||
(key_score * 0.48).max(white_score * 0.28)
|
||||
};
|
||||
if edge_fade > 0.08 {
|
||||
next_alpha = ((alpha as f32) * (1.0 - edge_fade)).round() as u8;
|
||||
if next_alpha < 10 {
|
||||
@@ -448,6 +481,37 @@ fn remove_generated_asset_sheet_green_screen_background(
|
||||
changed
|
||||
}
|
||||
|
||||
pub(super) fn suppress_generated_asset_sheet_key_color_fringe(
|
||||
color: [f32; 3],
|
||||
target: [f32; 3],
|
||||
key_score: f32,
|
||||
key_color: GeneratedAssetSheetKeyColor,
|
||||
) -> [f32; 3] {
|
||||
let strength = clamp_generated_asset_sheet_unit(key_score * 1.18);
|
||||
let key_channels = [
|
||||
key_color.red as f32 / 255.0,
|
||||
key_color.green as f32 / 255.0,
|
||||
key_color.blue as f32 / 255.0,
|
||||
];
|
||||
let mut next = color;
|
||||
|
||||
for index in 0..3 {
|
||||
if key_channels[index] >= 0.66 {
|
||||
let cap = target[index] + 18.0 + (1.0 - strength) * 28.0;
|
||||
next[index] = next[index].min(lerp_generated_asset_sheet_channel(
|
||||
next[index],
|
||||
cap,
|
||||
strength,
|
||||
));
|
||||
} else if key_channels[index] <= 0.34 {
|
||||
next[index] =
|
||||
lerp_generated_asset_sheet_channel(next[index], target[index], strength * 0.72);
|
||||
}
|
||||
}
|
||||
|
||||
next
|
||||
}
|
||||
|
||||
fn compute_generated_asset_sheet_key_score(
|
||||
pixel: [u8; 4],
|
||||
key_color: GeneratedAssetSheetKeyColor,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use super::alpha::{
|
||||
GeneratedAssetSheetAlphaOptions, apply_generated_asset_sheet_green_screen_alpha,
|
||||
suppress_generated_asset_sheet_key_color_fringe,
|
||||
};
|
||||
use super::color::{
|
||||
compute_generated_asset_sheet_key_color_score,
|
||||
clamp_generated_asset_sheet_unit, compute_generated_asset_sheet_key_color_score,
|
||||
compute_generated_asset_sheet_white_screen_score, is_generated_asset_sheet_foreground_pixel,
|
||||
is_generated_asset_sheet_green_contaminated_edge_pixel,
|
||||
is_generated_asset_sheet_soft_edge_pixel, is_generated_asset_sheet_strong_green_contamination,
|
||||
is_generated_asset_sheet_view_background_pixel, is_generated_asset_sheet_visible_pixel,
|
||||
touches_generated_asset_sheet_background_mask,
|
||||
lerp_generated_asset_sheet_channel, touches_generated_asset_sheet_background_mask,
|
||||
};
|
||||
use super::error::GeneratedAssetSheetError;
|
||||
use image::{GenericImageView, ImageFormat};
|
||||
@@ -588,11 +589,54 @@ fn remove_generated_asset_sheet_view_edge_matte(
|
||||
pixels[offset + 1],
|
||||
pixels[offset + 2],
|
||||
));
|
||||
let next_red = replacement.0.max(pixels[offset]);
|
||||
let next_blue = replacement.2.max(pixels[offset + 2]);
|
||||
let next_green = replacement
|
||||
.1
|
||||
.min(next_red.max(next_blue).saturating_add(12));
|
||||
let (next_red, next_green, next_blue) = if options.key_color.is_green_screen() {
|
||||
let next_red = replacement.0.max(pixels[offset]);
|
||||
let next_blue = replacement.2.max(pixels[offset + 2]);
|
||||
let next_green = replacement
|
||||
.1
|
||||
.min(next_red.max(next_blue).saturating_add(12));
|
||||
(next_red, next_green, next_blue)
|
||||
} else {
|
||||
let key_score = compute_generated_asset_sheet_key_color_score(
|
||||
pixel,
|
||||
[
|
||||
options.key_color.red,
|
||||
options.key_color.green,
|
||||
options.key_color.blue,
|
||||
],
|
||||
);
|
||||
let blend = clamp_generated_asset_sheet_unit((key_score * 1.25).max(0.36));
|
||||
let red = lerp_generated_asset_sheet_channel(
|
||||
pixels[offset] as f32,
|
||||
replacement.0 as f32,
|
||||
blend,
|
||||
);
|
||||
let green = lerp_generated_asset_sheet_channel(
|
||||
pixels[offset + 1] as f32,
|
||||
replacement.1 as f32,
|
||||
blend,
|
||||
);
|
||||
let blue = lerp_generated_asset_sheet_channel(
|
||||
pixels[offset + 2] as f32,
|
||||
replacement.2 as f32,
|
||||
blend,
|
||||
);
|
||||
let defringed = suppress_generated_asset_sheet_key_color_fringe(
|
||||
[red, green, blue],
|
||||
[
|
||||
replacement.0 as f32,
|
||||
replacement.1 as f32,
|
||||
replacement.2 as f32,
|
||||
],
|
||||
key_score,
|
||||
options.key_color,
|
||||
);
|
||||
(
|
||||
defringed[0].round().clamp(0.0, 255.0) as u8,
|
||||
defringed[1].round().clamp(0.0, 255.0) as u8,
|
||||
defringed[2].round().clamp(0.0, 255.0) as u8,
|
||||
)
|
||||
};
|
||||
if next_red != pixels[offset]
|
||||
|| next_green != pixels[offset + 1]
|
||||
|| next_blue != pixels[offset + 2]
|
||||
|
||||
@@ -206,6 +206,78 @@ fn generated_asset_sheet_magenta_edge_matte_does_not_remove_white_subject() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_asset_sheet_magenta_alpha_defringes_pink_halo() {
|
||||
let mut sheet = RgbaImage::from_pixel(24, 24, Rgba([255, 0, 255, 255]));
|
||||
for y in 7..17 {
|
||||
for x in 7..17 {
|
||||
sheet.put_pixel(x, y, Rgba([198, 170, 120, 255]));
|
||||
}
|
||||
}
|
||||
for y in 6..18 {
|
||||
sheet.put_pixel(6, y, Rgba([226, 26, 218, 220]));
|
||||
sheet.put_pixel(17, y, Rgba([226, 26, 218, 220]));
|
||||
}
|
||||
for x in 6..18 {
|
||||
sheet.put_pixel(x, 6, Rgba([226, 26, 218, 220]));
|
||||
sheet.put_pixel(x, 17, Rgba([226, 26, 218, 220]));
|
||||
}
|
||||
|
||||
let cleaned = apply_generated_asset_sheet_alpha_with_options(
|
||||
DynamicImage::ImageRgba8(sheet),
|
||||
GeneratedAssetSheetAlphaOptions::jump_hop_magenta_screen(),
|
||||
)
|
||||
.to_rgba8();
|
||||
let edge = cleaned.get_pixel(6, 12).0;
|
||||
|
||||
assert_eq!(cleaned.get_pixel(0, 0).0[3], 0);
|
||||
assert_eq!(cleaned.get_pixel(12, 12).0, [198, 170, 120, 255]);
|
||||
if edge[3] > 0 {
|
||||
assert!(
|
||||
edge[0].saturating_sub(edge[1]) <= 76,
|
||||
"红色 key 通道残留过强:{edge:?}"
|
||||
);
|
||||
assert!(
|
||||
edge[2].saturating_sub(edge[1]) <= 76,
|
||||
"蓝色 key 通道残留过强:{edge:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_asset_sheet_magenta_edge_matte_defringes_bottom_shadow() {
|
||||
let mut sheet = RgbaImage::from_pixel(32, 32, Rgba([0, 0, 0, 0]));
|
||||
for y in 8..18 {
|
||||
for x in 10..22 {
|
||||
sheet.put_pixel(x, y, Rgba([202, 176, 126, 255]));
|
||||
}
|
||||
}
|
||||
for y in 18..22 {
|
||||
for x in 9..23 {
|
||||
sheet.put_pixel(x, y, Rgba([224, 30, 220, 186]));
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned = crop_generated_asset_sheet_view_edge_matte_with_options(
|
||||
DynamicImage::ImageRgba8(sheet),
|
||||
GeneratedAssetSheetAlphaOptions::jump_hop_magenta_screen(),
|
||||
)
|
||||
.to_rgba8();
|
||||
|
||||
assert!(
|
||||
cleaned
|
||||
.pixels()
|
||||
.any(|pixel| pixel.0 == [202, 176, 126, 255])
|
||||
);
|
||||
assert!(
|
||||
!cleaned.pixels().any(|pixel| {
|
||||
let [red, green, blue, alpha] = pixel.0;
|
||||
alpha > 0 && red > 200 && blue > 200 && green < 96
|
||||
}),
|
||||
"底部洋红残影应被删除或去彩边"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_asset_sheet_view_edge_matte_trims_transparent_border() {
|
||||
let mut sheet = RgbaImage::from_pixel(20, 20, Rgba([0, 0, 0, 0]));
|
||||
|
||||
Reference in New Issue
Block a user