接口1: 标注图人头最左/最右竖线改用耳朵分割外缘
- 最左/最右竖线由「头发轮廓」改为同一 BiSeNet 的耳朵类(7/8)外缘, 看不到耳朵(被头发/侧脸遮挡→掩膜空)则该侧不画线 - 外耳轮廓常被误标成头发: 从耳朵外缘沿紧邻前景(耳∪发)按脸宽自适应 外扩回收(上限 face_w*0.045, 遇背景间隙即停) - 先按人脸包围盒裁剪再分割: BiSeNet 在紧裁人脸上训练, 整张全身/街拍图 脸偏小会严重欠分割、丢耳朵; 裁剪后映射回原图, 耳朵稳定可分 - segment_hair_and_ears() 单次推理同出 hair_mask + ear_mask Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+63
-23
@@ -5,6 +5,7 @@
|
||||
- 四庭水平分界线:numpy 向量化渐变消失(中间亮、两侧渐隐)。
|
||||
- 纵向竖线 8 条:人头最左 + 左脸颊/左眼外/内角/右眼内/外角/右脸颊 + 人头最右,
|
||||
把头宽切 7 段(七眼),段宽数值上下交替(上 3 / 下 4),带虚线双箭头。
|
||||
人头最左/最右取自耳朵分割外缘,看不到耳朵则省略该侧(最少 6 点 5 段)。
|
||||
- 四庭:图片左侧,「名」上「数值」下两行换行(不带 cm),带竖向虚线双箭头。
|
||||
- 五条横线右侧标名:头顶/发际线/眉心/鼻翼下缘/下巴尖。
|
||||
- 单位 cm 统一标在底部「单位cm」。
|
||||
@@ -116,38 +117,77 @@ _LINE_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
def _head_edges_from_mask(hair_mask, y0, y1, left_cheek_x, right_cheek_x, min_gap):
|
||||
"""从头发分割掩膜取人头最左/最右 x(仅在脸纵向范围 [y0,y1] 内统计)。
|
||||
def _grow_outward(start_col, fg_band, direction, limit):
|
||||
"""从 start_col 沿 direction(+1 右 / -1 左) 在前景带 fg_band 内逐列外扩。
|
||||
|
||||
返回 (head_left_x, head_right_x);某侧无掩膜/向内噪声,或离脸颊线过近
|
||||
(间距 < min_gap)则该侧为 None —— 即与脸颊线太近时只保留脸颊线。
|
||||
用于回收被误标成「头发」的外耳轮廓:耳朵被头发遮挡时,外耳轮廓那一圈常被
|
||||
分割并入头发类,故耳朵掩膜外缘会偏内。这里把外缘沿紧邻的前景(耳∪发)向外
|
||||
延伸,最多 limit 列;一旦下一列无前景(背景间隙)立即停止,绝不窜到分离的
|
||||
那缕头发上。返回外扩后的列号。
|
||||
"""
|
||||
if hair_mask is None:
|
||||
w = fg_band.shape[1]
|
||||
c = int(start_col)
|
||||
for _ in range(int(limit)):
|
||||
nc = c + direction
|
||||
if not (0 <= nc < w) or not fg_band[:, nc].any():
|
||||
break
|
||||
c = nc
|
||||
return c
|
||||
|
||||
|
||||
def _ear_edges_from_mask(ear_mask, hair_mask, y0, y1, left_cheek_x, right_cheek_x,
|
||||
face_center_x):
|
||||
"""从耳朵分割掩膜取人头最左/最右 x(仅在脸纵向范围 [y0,y1] 内统计)。
|
||||
|
||||
左线 = 脸中线左侧耳朵像素的最左列;右线 = 右侧耳朵像素的最右列;再沿紧邻的
|
||||
前景(耳∪发)按脸宽自适应外扩,回收被误标成头发的外耳轮廓(见 _grow_outward)。
|
||||
某侧耳朵不可见(被头发/侧脸遮挡 → 掩膜为空),或外缘未越过对应脸颊线(非真实
|
||||
头宽边缘)时该侧返回 None —— 即「看不到耳朵就不画这条线」。
|
||||
"""
|
||||
if ear_mask is None:
|
||||
return None, None
|
||||
m = np.asarray(hair_mask)
|
||||
m = np.asarray(ear_mask)
|
||||
if m.ndim == 3:
|
||||
m = m[..., 0]
|
||||
m = m > 0
|
||||
h = m.shape[0]
|
||||
y0 = max(0, int(y0)); y1 = min(h - 1, int(y1))
|
||||
if y1 <= y0:
|
||||
return None, None
|
||||
band = m[y0:y1 + 1] > 0
|
||||
cols = np.where(band.any(axis=0))[0]
|
||||
ear_band = m[y0:y1 + 1]
|
||||
cols = np.where(ear_band.any(axis=0))[0]
|
||||
if cols.size == 0:
|
||||
return None, None
|
||||
hl, hr = float(cols.min()), float(cols.max())
|
||||
# 仅当确实在脸颊外侧、且与脸颊线间距足够大时才采用
|
||||
return (hl if (left_cheek_x - hl) >= min_gap else None,
|
||||
hr if (hr - right_cheek_x) >= min_gap else None)
|
||||
# 前景带 = 耳∪发(外耳轮廓常被误标为发),外扩上限按脸宽自适应
|
||||
fg_band = ear_band
|
||||
if hair_mask is not None:
|
||||
hm = np.asarray(hair_mask)
|
||||
if hm.ndim == 3:
|
||||
hm = hm[..., 0]
|
||||
fg_band = ear_band | (hm[y0:y1 + 1] > 0)
|
||||
grow = max(2, round(max(1.0, right_cheek_x - left_cheek_x) * 0.045))
|
||||
|
||||
left_cols = cols[cols < face_center_x]
|
||||
right_cols = cols[cols > face_center_x]
|
||||
# 左/右耳外缘(外扩后),且必须在对应脸颊线外侧(否则视为残缺/噪声,只保留脸颊线)
|
||||
head_l = head_r = None
|
||||
if right_cols.size:
|
||||
edge = _grow_outward(right_cols.max(), fg_band, +1, grow)
|
||||
head_r = float(edge) if edge >= right_cheek_x else None
|
||||
if left_cols.size:
|
||||
edge = _grow_outward(left_cols.min(), fg_band, -1, grow)
|
||||
head_l = float(edge) if edge <= left_cheek_x else None
|
||||
return head_l, head_r
|
||||
|
||||
|
||||
def create_annotated_image(image_bgr, measure_result, hair_mask=None, variant="v1"):
|
||||
def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=None,
|
||||
variant="v1"):
|
||||
"""生成标注图层 PNG(透明底 RGBA,尺寸同原图)。返回 PIL.Image。
|
||||
|
||||
布局(对齐 img1.png 版本5):
|
||||
- 纵向竖线:人头最左 + 七眼 6 点 + 人头最右,切 7 段(七眼)。人头最左/最右
|
||||
取自头发分割掩膜的轮廓(方案 B);无掩膜(方案 A 兜底)时省略头部端线,
|
||||
只画 6 点 5 段。
|
||||
取自耳朵分割掩膜的外缘(方案 B,BiSeNet 类 7/8);耳朵不可见(被头发/侧脸
|
||||
遮挡 → 掩膜空)或无掩膜时省略该侧端线,只画对应脸颊线。
|
||||
- 横向 5 条分界线:头顶/发际线/眉心/鼻翼下缘/下巴尖,右侧标名。
|
||||
- 四庭(顶/上/中/下庭)在左侧:名 + 数值两行换行(无 cm),竖向虚线双箭头。
|
||||
- 七眼段宽数值上下交替(上 3 / 下 4,无 cm),横向虚线双箭头。
|
||||
@@ -186,12 +226,11 @@ def create_annotated_image(image_bgr, measure_result, hair_mask=None, variant="v
|
||||
if variant == "v6":
|
||||
xs = sorted(base_xs) # 接口6:仅七眼 6 点,不画人头最左/最右端线
|
||||
else:
|
||||
# 人头最左/最右:取自头发分割掩膜(方案B),脸纵向范围内统计;无掩膜则省略。
|
||||
# 与脸颊线间距 < 脸宽×8% 视为太近,只保留脸颊线(不画头部端线)。
|
||||
face_w = pts["right_cheek"][0] - pts["left_cheek"][0]
|
||||
min_gap = max(1.0, face_w * 0.08)
|
||||
head_l, head_r = _head_edges_from_mask(
|
||||
hair_mask, ys[0], ys[-1], pts["left_cheek"][0], pts["right_cheek"][0], min_gap)
|
||||
# 人头最左/最右:取自耳朵分割掩膜外缘(方案B,类7/8),脸纵向范围内统计。
|
||||
# 看不到耳朵(被头发/侧脸遮挡 → 掩膜空)或外缘未越过脸颊线则省略该侧端线。
|
||||
lcx, rcx = pts["left_cheek"][0], pts["right_cheek"][0]
|
||||
head_l, head_r = _ear_edges_from_mask(
|
||||
ear_mask, hair_mask, ys[0], ys[-1], lcx, rcx, (lcx + rcx) / 2)
|
||||
head_xs = [x for x in (head_l, head_r) if x is not None]
|
||||
xs = sorted(base_xs + head_xs) # 自左向右(6 或 7/8 点)
|
||||
|
||||
@@ -304,15 +343,16 @@ if __name__ == "__main__":
|
||||
print("未检出人脸")
|
||||
sys.exit(1)
|
||||
mask = None
|
||||
ears = None
|
||||
try:
|
||||
from face_analysis.hair_segmenter import get_segmenter
|
||||
mask = get_segmenter().segment_hair(img)
|
||||
mask, ears = get_segmenter().segment_hair_and_ears(img)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[warn] 分割不可用,回退方案 A:{e}")
|
||||
|
||||
result = measure_face(lms, mask, w, h)
|
||||
t0 = time.time()
|
||||
canvas = create_annotated_image(img, result, hair_mask=mask)
|
||||
canvas = create_annotated_image(img, result, ear_mask=ears, hair_mask=mask)
|
||||
dt = time.time() - t0
|
||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||
canvas.save(out)
|
||||
|
||||
Reference in New Issue
Block a user