接口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:
@@ -1,7 +1,8 @@
|
||||
"""方案 B:BiSeNet 头发分割 + 发际线/头顶定位。
|
||||
"""方案 B:BiSeNet 头发/耳朵分割 + 发际线/头顶定位。
|
||||
|
||||
加载 face-parsing BiSeNet(19 类,hair=17),对整图做像素级语义分割得到头发
|
||||
mask,再沿面部中轴线扫描得到真实发际线与头顶。GPU 可用时走 CUDA,否则 CPU。
|
||||
加载 face-parsing BiSeNet(CelebAMask-HQ 19 类,hair=17、l_ear=7、r_ear=8),对整图
|
||||
做像素级语义分割:得到头发 mask 用于沿面部中轴线扫描真实发际线与头顶,并得到耳朵
|
||||
mask 供标注图取人头最左/最右竖线(耳朵外缘)。GPU 可用时走 CUDA,否则 CPU。
|
||||
单例加载权重,避免每请求重载。详见技术方案 §1.4 / §4.0。
|
||||
"""
|
||||
import os
|
||||
@@ -15,6 +16,7 @@ import numpy as np
|
||||
|
||||
_WEIGHTS = os.path.join(os.path.dirname(__file__), "weights", "79999_iter.pth")
|
||||
HAIR_CLASS = 17 # CelebAMask-HQ 19 类中 hair 的索引
|
||||
EAR_CLASSES = (7, 8) # 7=l_ear / 8=r_ear(类名以人为参照,图像左右另行判定,不依赖类名)
|
||||
N_CLASSES = 19
|
||||
_INPUT_SIZE = 512 # BiSeNet 推理输入边长
|
||||
|
||||
@@ -59,8 +61,8 @@ class HairSegmenter:
|
||||
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
|
||||
])
|
||||
|
||||
def segment_hair(self, image_bgr):
|
||||
"""返回 hair_mask(H×W bool,True=头发),尺寸同输入原图。"""
|
||||
def _parse(self, image_bgr):
|
||||
"""整图语义分割,返回原图尺寸的类别图(H×W int,值为 0–18 类别号)。"""
|
||||
torch = self._torch
|
||||
h, w = image_bgr.shape[:2]
|
||||
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
||||
@@ -70,10 +72,49 @@ class HairSegmenter:
|
||||
with torch.no_grad():
|
||||
out = self.net(inp)[0] # 主输出 (1, C, 512, 512)
|
||||
parsing = out.squeeze(0).argmax(0).cpu().numpy() # (512, 512) 类别图
|
||||
hair_small = (parsing == HAIR_CLASS).astype(np.uint8)
|
||||
# 还原到原图尺寸(最近邻保持类别边界)
|
||||
hair_mask = cv2.resize(hair_small, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||
return hair_mask.astype(bool)
|
||||
return cv2.resize(parsing.astype(np.int32), (w, h),
|
||||
interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
def segment_hair(self, image_bgr):
|
||||
"""返回 hair_mask(H×W bool,True=头发),尺寸同输入原图。"""
|
||||
return self._parse(image_bgr) == HAIR_CLASS
|
||||
|
||||
def _parse_face_cropped(self, image_bgr, face_box):
|
||||
"""按人脸框裁剪后再分割,结果映射回原图尺寸(裁剪外填背景 0)。
|
||||
|
||||
BiSeNet 在 CelebAMask-HQ「紧裁对齐人脸」上训练,整张大场景图(全身/街拍,
|
||||
脸只占一小块、背景复杂)会严重欠分割、丢耳朵。先按人脸放大裁剪,让脸接近
|
||||
训练分布,耳朵/头发分割明显更稳。裁剪含足够上/侧边距以纳入发顶与双耳。
|
||||
"""
|
||||
h, w = image_bgr.shape[:2]
|
||||
x0, y0, x1, y1 = face_box
|
||||
fw, fh = max(1.0, x1 - x0), max(1.0, y1 - y0)
|
||||
cx0 = int(max(0, x0 - fw * 0.8)); cx1 = int(min(w, x1 + fw * 0.8))
|
||||
cy0 = int(max(0, y0 - fh * 1.0)); cy1 = int(min(h, y1 + fh * 0.5))
|
||||
if cx1 - cx0 < 2 or cy1 - cy0 < 2:
|
||||
return self._parse(image_bgr)
|
||||
full = np.zeros((h, w), dtype=np.int32)
|
||||
full[cy0:cy1, cx0:cx1] = self._parse(image_bgr[cy0:cy1, cx0:cx1])
|
||||
return full
|
||||
|
||||
def segment_hair_and_ears(self, image_bgr, face_box=None):
|
||||
"""单次推理返回 (hair_mask, ear_mask),均为 H×W bool,尺寸同原图。
|
||||
|
||||
ear_mask = 左耳(7) ∪ 右耳(8);耳朵被头发/侧脸遮挡时对应区域天然为空,
|
||||
正好用于「看不到耳朵就不画线」的判定。两类合并、左右按图像位置另判,
|
||||
不依赖以人为参照的类名(详见 EAR_CLASSES 注释)。
|
||||
|
||||
face_box=(x0,y0,x1,y1)(人脸关键点包围盒像素坐标)给定时先按人脸裁剪再
|
||||
分割(见 _parse_face_cropped),整张大场景图也能稳定分出耳朵;不给则整图分割。
|
||||
"""
|
||||
if face_box is None:
|
||||
parsing = self._parse(image_bgr)
|
||||
else:
|
||||
parsing = self._parse_face_cropped(image_bgr, face_box)
|
||||
hair_mask = parsing == HAIR_CLASS
|
||||
ear_mask = np.isin(parsing, EAR_CLASSES)
|
||||
return hair_mask, ear_mask
|
||||
|
||||
|
||||
_segmenter = None
|
||||
|
||||
Reference in New Issue
Block a user