diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 49892bd75..34c59460c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4515,7 +4515,7 @@ - 契约:普通图片 / 角色共用的图片生成请求和图标图集生成请求增加可选字符串 `style`,当前公开合法值为 `none / pixelArt`。省略、`null`、空字符串和 `none` 归一为内部 `None` 且不告警;未知字符串、或在 `spec / quick-edit / ui-design / publication-material` 等不支持的图片 `kind` 上请求 `pixelArt` 时,按 `None` 继续原管线并返回 `unsupported-image-style` 通用告警;非字符串 JSON 返回 `400`。旧队列 payload 缺少字段时兼容为 `None`。 - UI 边界:只有普通 `生成图片`、`生成角色形象` 和 `生成图标素材` 显示 `像素艺术` 勾选项;当前选择可进入已有生成器快照和请求 / 队列 payload,但不写入 `generationInputs`、素材元数据或新表。画布 Agent 和其它生成 / 编辑入口不开放该选项。 -- 处理边界:`PixelArt` 由 `platform-image` 的纯同步、纯内存 Rust 模块执行,不运行 Python、不访问 OSS / 数据库 / 画布。普通图片直接使用 provider 图;角色和图标必须等 BgFilter 成功并把 Alpha 回贴到 provider 原尺寸后,以 provider 平底原图分析网格、以透明 RGBA 图采样。固定参数为分析色数 16、Alpha 覆盖阈值 0.375、像素尺寸自动、无固定色板、K-means 最大采样 262144;单格 RGB 按 Alpha 加权,输出 Alpha 只为 0 / 255,逻辑低分辨率结果用 nearest 恢复交付尺寸并跳过 Lanczos。 +- 处理边界:`PixelArt` 由 `platform-image` 的纯同步、纯内存 Rust 模块执行,不运行 Python、不访问 OSS / 数据库 / 画布。普通图片直接使用 provider 图;角色和图标必须等 BgFilter 成功并把 Alpha 回贴到 provider 原尺寸后,以 provider 平底原图分析网格、以透明 RGBA 图采样。固定参数为分析色数 16、Alpha 覆盖阈值 0.375、像素尺寸自动、相邻边缘峰间距使用线性插值 P30 估算步长、无固定色板、K-means 最大采样 262144;单格 RGB 按 Alpha 加权,输出 Alpha 只为 0 / 255,逻辑低分辨率结果用 nearest 恢复交付尺寸并跳过 Lanczos。 - 执行边界:像素规整 CPU 工作使用进程级最大并发 2;取得并发许可的排队时间与实际处理时间共享最多 30 秒预算,同时不得晚于当前请求 deadline,最终取更早者。输入图片任一边上限为 10000 像素、总像素上限为 8294400;超限、排队超时或处理超时均按 best-effort 非致命降级,不持久化部分结果。 - 去背边界:不修改 BgFilter `flat` 参数、`cross_check`、fallback、Alpha 回贴和默认关闭 despill 的现有行为。BgFilter 最终失败时不运行像素规整;像素规整失败按 best-effort 非致命降级,保留进入该步骤前的图片并通过既有通用 `warning` 完成任务,不退款。 - 持久化边界:逻辑低分辨率图、像素化前后对比图、预览、诊断和报告一律不持久化;像素模式只替换原本即将上传的最终图片字节。普通图片、角色、图标的 OSS PUT、asset / project resource 和画布 item 数量必须与 `None` 模式完全一致;角色 / 图标最多因复用失败增加一次对已有 provider 对象的 OSS GET,不得增加 PUT、资源类型、画布项、队列类型或 schema 字段。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 7e9dde17b..0ca474eaa 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -36,7 +36,7 @@ - 普通 `生成图片`、`生成角色形象` 和 `生成图标素材` 三个面板增加紧凑的 `像素艺术` 勾选项;移动端可独占一行,但不增加功能说明文案。当前生成对象以 `style: "none" | "pixelArt"` 保存选择并随现有请求 / 队列 payload 传递;该字段不写入用户可见 `generationInputs`,也不新增素材元数据字段。其它生成、编辑、UI 素材提取、角色动画及画布 Agent 入口不展示或设置该选项。 - `style` 是可选字符串兼容边界。省略、`null`、空字符串和 `"none"` 统一归一为内部 `None`,不返回告警;`"pixelArt"` 仅允许普通图片(`kind` 省略)与 `kind="character"`,图标图集请求单独允许该值。未知字符串或在 `spec / quick-edit / ui-design / publication-material` 等不支持的图片 `kind` 上请求 `"pixelArt"` 时,按 `None` 完成原管线并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍是畸形请求并返回 `400`。旧 payload 缺少字段时等价于 `None`。 - `None` 必须保持现有生成、尺寸处理、BgFilter、上传、资源和画布链路不变。`PixelArt` 只增加父流程内的纯内存 Rust 后处理,不启动 Python 或独立服务,也不改变 BgFilter 的 `flat` 参数、Alpha 回贴、`cross_check`、fallback 或默认关闭 despill 的现有行为。 -- 普通图片在 provider 回图后,以同一张图同时作为网格分析源和 RGBA 采样源;角色与图标在 BgFilter 正常成功、现有 Alpha 蒙版回贴到 provider 原尺寸后执行双输入规整,其中网格分析源为带纯色背景的 provider 原图,RGBA 采样源为 Alpha 已回贴的透明图。固定首版参数为:分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。 +- 普通图片在 provider 回图后,以同一张图同时作为网格分析源和 RGBA 采样源;角色与图标在 BgFilter 正常成功、现有 Alpha 蒙版回贴到 provider 原尺寸后执行双输入规整,其中网格分析源为带纯色背景的 provider 原图,RGBA 采样源为 Alpha 已回贴的透明图。固定首版参数为:分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、相邻边缘峰间距使用线性插值 `P30` 估算步长、固定色板关闭、K-means 最大采样 `262144`。 - 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均按像素后处理失败的 best-effort 规则保留进入该步骤前的图片。 - 单格颜色按 `Σ(A × RGB) / ΣA` 进行 Alpha 加权;单格覆盖率按 `Σ(A / 255) / N` 计算。覆盖率大于等于 `0.375` 且 `ΣA > 0` 时输出硬 Alpha `255`,否则输出严格的 `[0,0,0,0]`;最终 Alpha 只允许 `0 / 255`。分析用 16 色只负责网格识别,不限制最终输出色数。 - 逻辑低分辨率图只存在于内存,随后使用 nearest 恢复到该任务原有交付尺寸,并直接替换原本即将持久化的最终图片字节。像素模式不得再经过 Lanczos 或其它会重新引入软边的插值。角色和图标应复用 Alpha 回贴阶段已经读取的 provider 原图;确需重新读取时,最多增加一次对已有 provider 对象的 OSS GET,不得新增 OSS PUT。 diff --git a/server-rs/crates/platform-image/src/pixel_art_snapper.rs b/server-rs/crates/platform-image/src/pixel_art_snapper.rs index 0ff50cd64..8ca7635b8 100644 --- a/server-rs/crates/platform-image/src/pixel_art_snapper.rs +++ b/server-rs/crates/platform-image/src/pixel_art_snapper.rs @@ -31,6 +31,7 @@ const KMEANS_SEED: u64 = 42; const MAX_KMEANS_ITERATIONS: usize = 15; const PEAK_THRESHOLD_MULTIPLIER: f64 = 0.2; const PEAK_DISTANCE_FILTER: usize = 4; +const STEP_SIZE_PERCENTILE: f64 = 0.3; const WALKER_SEARCH_WINDOW_RATIO: f64 = 0.35; const WALKER_MIN_SEARCH_WINDOW: f64 = 2.0; const WALKER_STRENGTH_THRESHOLD: f64 = 0.5; @@ -117,6 +118,7 @@ struct SnapConfig { max_kmeans_iterations: usize, peak_threshold_multiplier: f64, peak_distance_filter: usize, + step_size_percentile: f64, walker_search_window_ratio: f64, walker_min_search_window: f64, walker_strength_threshold: f64, @@ -134,6 +136,7 @@ impl SnapConfig { max_kmeans_iterations: MAX_KMEANS_ITERATIONS, peak_threshold_multiplier: PEAK_THRESHOLD_MULTIPLIER, peak_distance_filter: PEAK_DISTANCE_FILTER, + step_size_percentile: STEP_SIZE_PERCENTILE, walker_search_window_ratio: WALKER_SEARCH_WINDOW_RATIO, walker_min_search_window: WALKER_MIN_SEARCH_WINDOW, walker_strength_threshold: WALKER_STRENGTH_THRESHOLD, @@ -596,7 +599,15 @@ fn estimate_step_size(profile: &[f64], config: SnapConfig) -> Option { .map(|pair| (pair[1] - pair[0]) as f64) .collect::>(); differences.sort_by(f64::total_cmp); - Some(differences[differences.len() / 2]) + let percentile_position = + (differences.len() - 1) as f64 * config.step_size_percentile.clamp(0.0, 1.0); + let lower_index = percentile_position.floor() as usize; + let upper_index = percentile_position.ceil() as usize; + let interpolation = percentile_position - lower_index as f64; + Some( + differences[lower_index] + + (differences[upper_index] - differences[lower_index]) * interpolation, + ) } fn resolve_step_sizes(