增强分离标记框的可视辨识度
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 3m4s
Project CI / Native shell tests (pull_request) Failing after 4m38s

为分离标记框增加两条绿色角到角交叉线。

新增 MARKER_LINE_WIDTH 常量统一配置标记线宽,并确保绘制结果裁剪在矩形内。

补充标记框像素级回归测试并同步更新自动分离专题文档。
This commit is contained in:
2026-09-09 16:21:16 +08:00
parent aa2a78f7e0
commit 8a0dcf8be7
3 changed files with 145 additions and 9 deletions
@@ -3,6 +3,8 @@ use base64::Engine as _;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
const MARKER_LINE_WIDTH: u32 = 2;
pub async fn build_marked_image( pub async fn build_marked_image(
source_url: String, source_url: String,
nodes: Vec<SeparationNode>, nodes: Vec<SeparationNode>,
@@ -120,12 +122,144 @@ fn draw_frame(
return; return;
}; };
let green = image::Rgba([0, 255, 0, 255]); let green = image::Rgba([0, 255, 0, 255]);
for xx in x0..=x1 { draw_line(
image.put_pixel(xx, y0, green); image,
image.put_pixel(xx, y1, green); (x0, y0),
} (x1, y0),
for yy in y0..=y1 { green,
image.put_pixel(x0, yy, green); MARKER_LINE_WIDTH,
image.put_pixel(x1, yy, green); (x0, y0, x1, y1),
);
draw_line(
image,
(x0, y1),
(x1, y1),
green,
MARKER_LINE_WIDTH,
(x0, y0, x1, y1),
);
draw_line(
image,
(x0, y0),
(x0, y1),
green,
MARKER_LINE_WIDTH,
(x0, y0, x1, y1),
);
draw_line(
image,
(x1, y0),
(x1, y1),
green,
MARKER_LINE_WIDTH,
(x0, y0, x1, y1),
);
draw_line(
image,
(x0, y0),
(x1, y1),
green,
MARKER_LINE_WIDTH,
(x0, y0, x1, y1),
);
draw_line(
image,
(x1, y0),
(x0, y1),
green,
MARKER_LINE_WIDTH,
(x0, y0, x1, y1),
);
}
fn draw_line(
image: &mut image::RgbaImage,
start: (u32, u32),
end: (u32, u32),
color: image::Rgba<u8>,
line_width: u32,
bounds: (u32, u32, u32, u32),
) {
let mut x = start.0 as i64;
let mut y = start.1 as i64;
let target_x = end.0 as i64;
let target_y = end.1 as i64;
let dx = (target_x - x).abs();
let sx = if x < target_x { 1 } else { -1 };
let dy = -(target_y - y).abs();
let sy = if y < target_y { 1 } else { -1 };
let mut error = dx + dy;
loop {
draw_brush(image, x, y, color, line_width, bounds);
if x == target_x && y == target_y {
break;
}
let twice_error = error * 2;
if twice_error >= dy {
error += dy;
x += sx;
}
if twice_error <= dx {
error += dx;
y += sy;
}
}
}
fn draw_brush(
image: &mut image::RgbaImage,
x: i64,
y: i64,
color: image::Rgba<u8>,
line_width: u32,
bounds: (u32, u32, u32, u32),
) {
let (x0, y0, x1, y1) = bounds;
let line_width = line_width.max(1) as i64;
let before = (line_width - 1) / 2;
let after = line_width / 2;
let min_x = (x - before).max(x0 as i64);
let max_x = (x + after).min(x1 as i64);
let min_y = (y - before).max(y0 as i64);
let max_y = (y + after).min(y1 as i64);
for yy in min_y..=max_y {
for xx in min_x..=max_x {
image.put_pixel(xx as u32, yy as u32, color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draw_frame_adds_green_cross_corner_lines() {
let mut image = image::RgbaImage::from_pixel(8, 6, image::Rgba([1, 2, 3, 255]));
draw_frame(&mut image, 1, 1, 5, 3, 8, 6);
let green = image::Rgba([0, 255, 0, 255]);
for &(x, y) in &[(1, 1), (5, 1), (1, 3), (5, 3), (3, 2)] {
assert_eq!(*image.get_pixel(x, y), green, "pixel ({x}, {y})");
}
assert_eq!(*image.get_pixel(3, 1), green);
assert_eq!(*image.get_pixel(3, 3), green);
assert_eq!(*image.get_pixel(2, 2), green);
assert_eq!(*image.get_pixel(4, 2), green);
assert_eq!(*image.get_pixel(0, 0), image::Rgba([1, 2, 3, 255]));
}
#[test]
fn draw_frame_keeps_cross_inside_clipped_rect() {
let mut image = image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255]));
draw_frame(&mut image, 2, 2, 4, 4, 4, 4);
let green = image::Rgba([0, 255, 0, 255]);
for y in 2..4 {
for x in 2..4 {
assert_eq!(*image.get_pixel(x, y), green);
}
}
assert_eq!(*image.get_pixel(1, 1), image::Rgba([1, 2, 3, 255]));
} }
} }
@@ -5,8 +5,10 @@ const SHARED_SEPARATION_REQ: &str = r#"
MUST hard edges; preserve no glow/blur beyond the exact visible shape. MUST hard edges; preserve no glow/blur beyond the exact visible shape.
NEVER keep its parent's background with it. NEVER keep its parent's background with it.
UI elements that needs to extract has been marked with GREEN line frames (only for mark purpose, NEVER wrap a frame in your extraction). UI elements that needs to extract has been marked with GREEN line frames box with crossline inside. (only for mark purpose, NEVER wrap a frame in your extraction).
On some UI elements, there is some PURPLE filled area, they were removed UI elements, reconstruct the background under where they were. On some UI elements, there is some PURPLE filled area, they were removed UI elements, reconstruct the background under where they were.
MUST extract exactly these marked UI elements area.
"#; "#;
pub(super) fn gen_extract_prompt(separation_notes: Vec<SeparationNote>) -> String { pub(super) fn gen_extract_prompt(separation_notes: Vec<SeparationNote>) -> String {
let extract_system_prompt = format!( let extract_system_prompt = format!(
@@ -29,7 +29,7 @@
## 图片编辑与视觉绑定 ## 图片编辑与视觉绑定
- image-edit 使用源 UI design 图片及由 Rust 生成的绿色标记/紫色重建输入。处理父节点时,紫色填充其 children 的矩形区域(包括已 problematic 的 children),再在父节点自身外围绘制绿色框;绿色覆盖在紫色之上。叶节点只绘制绿色框,不填充自身。 - image-edit 使用源 UI design 图片及由 Rust 生成的绿色标记/紫色重建输入。处理父节点时,紫色填充其 children 的矩形区域(包括已 problematic 的 children),再在父节点自身外围绘制绿色框和角到角的绿色交叉线;绿色标记覆盖在紫色之上。叶节点只绘制绿色框和角到角的绿色交叉线,不填充自身。
- 请求尺寸始终使用源 UI design 尺寸;Raw GPT Image 2 API 保证返回相同尺寸,客户端不额外做尺寸拒绝检查。 - 请求尺寸始终使用源 UI design 尺寸;Raw GPT Image 2 API 保证返回相同尺寸,客户端不额外做尺寸拒绝检查。
- 标记图构建、处理图解码/写入和 cut 裁切属于本地 CPU/文件操作,放入独立的 - 标记图构建、处理图解码/写入和 cut 裁切属于本地 CPU/文件操作,放入独立的
`spawn_blocking` 任务;image-edit 与 visual binding 网络请求仍运行在 async future 中。 `spawn_blocking` 任务;image-edit 与 visual binding 网络请求仍运行在 async future 中。