feat(annotation): 线名移到右侧 + 线条只覆盖人脸范围

- 5 条横线的线名(头顶/发际线/眉心/鼻翼下缘/下巴尖)从左侧移到右侧
- 四庭 cm(顶庭/上庭/中庭/下庭)留在左侧,右对齐贴脸盒左缘
- 横线只覆盖脸宽(左右脸颊)、竖线只覆盖脸高(头顶→下巴),渐变消失
- 七眼段宽标注移到脸盒上端/下端(配合新线长),相邻段上下错行防重叠

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-06-14 19:58:12 +08:00
co-authored by Claude Opus 4.8
parent 10488171eb
commit 891bc0da8b
+39 -28
View File
@@ -110,10 +110,11 @@ def create_annotated_image(image_bgr, measure_result):
"""生成标注图层 PNG(透明底 RGBA,尺寸同原图)。返回 PIL.Image。
布局:
- 横向 5 条分界线(渐变消失):头顶/发际线/眉心/鼻翼下缘/下巴尖,左侧标线名 +
四庭 cm 数值(顶庭/上庭/中庭/下庭,各段中点)。
- 纵向 6 条线(渐变消失):左脸颊/左眼外角/左眼内角/右眼内角/右眼外角/右脸颊,
把脸宽切 5 段;每段宽度在图的**顶部和底部**各标一次(只标 `X.XXcm`,不写名)。
- 线条只覆盖人脸范围(横线=脸宽,竖线=脸高),渐变消失。
- 横向 5 条分界线:头顶/发际线/眉心/鼻翼下缘/下巴尖,**线名在右侧**;
四庭 cm 数值(顶庭/上庭/中庭/下庭)在左侧各段中点。
- 纵向 6 条线:左脸颊/左眼外角/左眼内角/右眼内角/右眼外角/右脸颊,把脸宽切 5 段;
每段宽度在脸的**上端和下端**各标一次(只标 `X.XXcm`,不写名)。
"""
h, w = image_bgr.shape[:2]
v = measure_result.vertical
@@ -121,54 +122,64 @@ def create_annotated_image(image_bgr, measure_result):
buf = np.zeros((h, w, 4), dtype=np.uint8)
# --- 1. 横向 5 条分界线(渐变) ---
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
ys = [v[name][1] for name in order]
cx_line = v["brow_center"][0]
for cy in ys:
draw_gradient_horizontal_line(buf, cx_line, cy)
# --- 2. 纵向 6 条线(渐变),切出 5 段七眼宽度 ---
pts = measure_result.eyes["points"]
seven_keys = ["left_cheek", "left_outer", "left_inner",
"right_inner", "right_outer", "right_cheek"]
xs = sorted(pts[k][0] for k in seven_keys) # 自左向右
# 顶/底各两行错开放置,避免窄段(眼宽~2cm)数字横向重叠
row_h = FONT_SIZE + 2
y_top0, y_top1 = 6, 6 + row_h
y_bot0 = h - FONT_SIZE - 6
y_bot1 = y_bot0 - row_h
yv0 = y_top1 + FONT_SIZE + 4 # 竖线避开顶部两行标签
yv1 = y_bot1 - 4 # 避开底部两行标签
# 人脸包围盒:x 为脸宽(左右脸颊),y 为脸高(头顶→下巴)
fx0, fx1 = xs[0], xs[-1]
fy0, fy1 = ys[0], ys[-1]
face_cx = (fx0 + fx1) / 2
face_half = (fx1 - fx0) / 2 * 1.08 # 略放大确保横线覆盖到脸颊
# --- 1. 横向 5 条分界线(渐变,覆盖脸宽) ---
for cy in ys:
draw_gradient_horizontal_line(buf, face_cx, cy, half_length=face_half)
# --- 2. 纵向 6 条线(渐变,覆盖脸高 头顶→下巴) ---
for vx in xs:
draw_gradient_vertical_line(buf, vx, yv0, yv1)
draw_gradient_vertical_line(buf, vx, fy0, fy1)
canvas = Image.fromarray(buf, mode="RGBA")
draw = ImageDraw.Draw(canvas)
font = _load_font()
# --- 3. 横线侧:线名 + 四庭 cm 数值 ---
left_margin = 8
court_indent = 30
# --- 3a. 横线侧:线名(头顶/发际线/眉心/鼻翼下缘/下巴尖) ---
name_x = fx1 + 8
for i, name in enumerate(order):
text = _LINE_NAMES[name]
tw, _ = _text_size(draw, text, font)
x = min(name_x, w - 2 - tw) # 右侧越界时回收
draw.text((x, ys[i] - FONT_SIZE / 2), text, fill=LINE_COLOR, font=font)
# --- 3b. 横线左侧:四庭 cm 数值(各段中点,右对齐到脸盒左缘) ---
court_cm = [measure_result.top_cm, measure_result.upper_cm,
measure_result.middle_cm, measure_result.lower_cm]
court_name = ["顶庭", "上庭", "中庭", "下庭"]
for i, name in enumerate(order):
draw.text((left_margin, ys[i] - FONT_SIZE / 2), _LINE_NAMES[name],
fill=LINE_COLOR, font=font)
for i in range(4):
text = f"{court_name[i]} {court_cm[i]:.2f}cm"
tw, _ = _text_size(draw, text, font)
x = max(2, fx0 - 8 - tw) # 贴脸盒左缘,右对齐
y_mid = (ys[i] + ys[i + 1]) / 2 - FONT_SIZE / 2
draw.text((left_margin + court_indent, y_mid),
f"{court_name[i]} {court_cm[i]:.2f}cm", fill=LINE_COLOR, font=font)
draw.text((x, y_mid), text, fill=LINE_COLOR, font=font)
# --- 4. 七眼每段宽度:顶部 + 底部各标一次(只标数字 cm相邻段上下错行) ---
# --- 4. 七眼每段宽度:脸的上端 + 下端各标一次(相邻段上下错行防重叠 ---
row_h = FONT_SIZE + 2
y_top_a = max(1, fy0 - row_h - 2) # 上端:头顶线上方两行
y_top_b = max(1, fy0 - 2 * row_h - 2)
y_bot_a = min(h - FONT_SIZE - 1, fy1 + 2) # 下端:下巴线下方两行
y_bot_b = min(h - FONT_SIZE - 1, fy1 + row_h + 2)
for i in range(len(xs) - 1):
seg_cm = (xs[i + 1] - xs[i]) / pc
cx_seg = (xs[i] + xs[i + 1]) / 2
text = f"{seg_cm:.2f}cm"
tw, _ = _text_size(draw, text, font)
ty_top = y_top0 if i % 2 == 0 else y_top1
ty_bot = y_bot0 if i % 2 == 0 else y_bot1
ty_top = y_top_a if i % 2 == 0 else y_top_b
ty_bot = y_bot_a if i % 2 == 0 else y_bot_b
draw.text((cx_seg - tw / 2, ty_top), text, fill=LINE_COLOR, font=font)
draw.text((cx_seg - tw / 2, ty_bot), text, fill=LINE_COLOR, font=font)