终于修改对了,还是opus

This commit is contained in:
xsl
2026-07-11 23:14:07 +08:00
parent 41bb164a52
commit 1b9f3fdb6f
7 changed files with 205 additions and 180 deletions
+183 -156
View File
@@ -105,172 +105,194 @@ def _gray_b64(gray_float):
# 步骤1:接口9 头发遮罩(复用 head_mask 构件)
# ---------------------------------------------------------------------------
def _extract_hairline(hair_mask, upper=None, mode="column", rid="",
baseline_pts=None, band_expand_px=0):
"""提取头发/皮肤交界线(头发区域内轮廓朝脸一侧),用 baseline 水平 y 线截断。
def _close_cyclic(mask, gap):
"""对 1D 布尔环形序列做闭运算,填掉短于 gap 的 False 缺口(消除内侧判定的小抖动)。"""
if gap <= 0 or mask.all() or not mask.any():
return mask
n = len(mask)
tripled = np.concatenate([mask, mask, mask]).astype(np.uint8).reshape(1, -1)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2 * gap + 1, 1))
closed = cv2.morphologyEx(tripled, cv2.MORPH_CLOSE, kernel).reshape(-1)
return closed[n:2 * n].astype(bool)
直接取头发区域的最大轮廓(头发/皮肤交界),逐列取最靠下的边界点作为该列发际线 y。
然后用 baseline 的 y 值做水平截断:只保留每列发际线 y < baseline_y 的部分
baseline 线以上=额头+发际线区域;baseline 以下=脸下半部,丢弃)。无任何竖线。
upper:兼容旧签名保留,不参与计算。
baseline_ptsbaseline 关键点,其折线 y 值用于水平截断。
band_expand_px:兼容签名,不再使用(已不竖向截断)。
modecontour(轮廓,推荐)| column(逐列下沿,兜底)。
返回 (hairline_y, lo, hi) —— hairline_y: 长度=w 的 y 数组(baseline 以下或无轮廓处置 NaN)。
lo/hi: 发际线有效范围的首末列(用于 _pushed_mask 限定填充范围)。
def _longest_true_run_cyclic(mask):
"""返回环形布尔序列中最长连续 True 段的索引(按顺序)。全 True 返回全体索引。"""
n = len(mask)
if mask.all():
return np.arange(n)
if not mask.any():
return np.empty(0, dtype=np.int64)
# 从一个 False 处断开成线性序列,避免最长段跨越首尾
start = int(np.where(~mask)[0][0])
order = np.roll(np.arange(n), -start)
rmask = mask[order]
best_len = best_s = cur = cur_s = 0
for i, v in enumerate(rmask):
if v:
if cur == 0:
cur_s = i
cur += 1
if cur > best_len:
best_len, best_s = cur, cur_s
else:
cur = 0
return order[best_s:best_s + best_len]
def _draw_polyline(image, pts, color, thickness=3):
"""把有序点列 (Nx2) 画成折线(用于内轮廓/外推线可视化)。点<2 原样返回。"""
out = image.copy()
if pts is not None and len(pts) >= 2:
poly = np.ascontiguousarray(np.asarray(pts).reshape(-1, 1, 2), dtype=np.int32)
cv2.polylines(out, [poly], isClosed=False, color=color,
thickness=thickness, lineType=cv2.LINE_AA)
return out
def _extract_hairline(hair_mask, center, chin_y=None, rid="", sample_px=12):
"""提取头发区域朝脸一侧的内轮廓线(发际线)。
结果是一条有序折线:额头弧线 + 左右两侧鬓角/脸颊边界,一直向下到下颌 chin_y。
做法(轮廓 + 内侧判定,见 docs/发际线生发遮罩算法_pushed模式.md ①-f):
1. 取头发 mask 最大连通域的外轮廓(CHAIN_APPROX_NONE,逐像素稠密点,保序)。
2. 逐点判定「内侧」:从该轮廓点朝脸中心 center 采样 sample_px 像素,若落点是
非头发像素 → 该点朝向脸(头发/皮肤交界的内轮廓);否则是朝背景的外侧剪影,丢弃。
3. 内轮廓点在闭合轮廓上本是一段连续弧,先环形闭运算填小缝,再取最长连续段并保序。
4. 下颌截断:丢掉 y > chin_y 的点(把两侧末端截到下颌一带),得到红真值那样的环脸弧。
center: 脸中心 (cx, cy),取 151 点(眉心)。内侧判定与 ①-g 径向外推共用此圆心。
chin_y: 下颌截断 y(一般取下巴关键点 152 的 y);None 则不截断。
sample_px: 内侧判定的采样距离(像素),一般 ≈ 0.4cm。
返回按顺序排列的内轮廓点 np.ndarray(Nx2, int32);不足 2 点返回空数组 (0,2)。
"""
lg = lambda msg: logger.info("[%s] %s", rid, msg) if rid else None
h, w = hair_mask.shape
col_down = np.full(w, -1, dtype=np.int32)
empty = np.empty((0, 2), dtype=np.int32)
if mode == "contour":
mask_u8 = hair_mask.astype(np.uint8)
if mask_u8.sum() > 0:
cnts, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
lg(f"_extract_hairline contour: 轮廓数={len(cnts) if cnts else 0}")
if cnts:
big = max(cnts, key=cv2.contourArea)
lg(f" 最大轮廓面积={cv2.contourArea(big):.0f}")
for pt in big[:, 0]:
x, y = int(pt[0]), int(pt[1])
if y > col_down[x]:
col_down[x] = y
else: # column(兜底:逐列下沿,与 contour 结果接近)
cols = np.where(hair_mask.any(axis=0))[0]
lg(f"_extract_hairline column: 有头发的列数={len(cols)}/{w}")
for x in cols:
col_down[x] = int(np.where(hair_mask[:, x])[0].max())
m = _largest_cc(hair_mask).astype(np.uint8)
if m.sum() == 0:
lg("_extract_hairline: 头发 mask 为空")
return empty
cnts, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
if not cnts:
lg("_extract_hairline: 无轮廓")
return empty
big = max(cnts, key=cv2.contourArea)
pts = big[:, 0].astype(np.float64) # N×2 (x, y),沿边界有序
n = len(pts)
lg(f"_extract_hairline: 最大轮廓点数={n} 面积={cv2.contourArea(big):.0f} center={center} chin_y={chin_y}")
if n < 3 or center is None:
return empty
valid = col_down >= 0
if valid.sum() < 2:
lg(f" 警告: 有效列<2,退化为空发际线")
nan = np.full(w, np.nan)
return nan, 0, w - 1
xs = np.where(valid)[0]
ys = col_down[valid].astype(np.float64)
raw = np.interp(np.arange(w), xs, ys)
raw = np.clip(raw, 0, h - 1)
cx, cy = float(center[0]), float(center[1])
# 内侧判定:每个轮廓点朝脸中心方向采样 sample_px,落在非头发上 → 内轮廓点
d = np.stack([cx - pts[:, 0], cy - pts[:, 1]], axis=1)
dist = np.hypot(d[:, 0], d[:, 1])
dist[dist < 1e-3] = 1.0
step = d / dist[:, None] * float(sample_px)
sx = np.clip(np.round(pts[:, 0] + step[:, 0]).astype(int), 0, w - 1)
sy = np.clip(np.round(pts[:, 1] + step[:, 1]).astype(int), 0, h - 1)
inner = m[sy, sx] == 0
lg(f" 内侧点数={int(inner.sum())}/{n}")
if inner.sum() < 3:
lg(" 内侧点<3,退化为空内轮廓")
return empty
# baseline 每列的 y(水平截断线:发际线 y 必须 < baseline_y 才保留
if baseline_pts is not None and len(baseline_pts) >= 2:
x0, y0 = baseline_pts[0]
x1, y1 = baseline_pts[-1]
chain_x = np.array([0] + [p[0] for p in baseline_pts] + [w - 1])
chain_y = np.array([y0] + [p[1] for p in baseline_pts] + [y1])
baseline_y = np.interp(np.arange(w), chain_x, chain_y)
# 水平截断:只保留发际线在 baseline 以上(y 更小)的列
keep = raw < baseline_y
out = np.where(keep, raw, np.nan)
valid_cols = np.where(keep)[0]
lo = int(valid_cols.min()) if len(valid_cols) else 0
hi = int(valid_cols.max()) if len(valid_cols) else w - 1
lg(f" baseline 水平截断: baseline_y范围[{int(baseline_y.min())},{int(baseline_y.max())}] "
f"保留{keep.sum()}列 → 发际线范围[{lo},{hi}]")
else:
out = raw
lo, hi = 0, w - 1
lg(f" 无 baseline,取全宽")
# 环形闭运算填掉短缝隙,再取最长连续内侧段(= 朝脸的整段内轮廓
inner = _close_cyclic(inner, gap=max(3, n // 100))
idx = _longest_true_run_cyclic(inner)
arc = pts[idx].astype(np.int32)
lg(f" 最长内侧段点数={len(arc)}")
return out, lo, hi
# 下颌截断:只保留 y <= chin_y 的点(把两侧末端截到下颌)
if chin_y is not None and len(arc):
keep = arc[:, 1] <= int(chin_y)
arc = arc[keep]
lg(f" 下颌截断(chin_y={int(chin_y)})后点数={len(arc)}")
if len(arc) < 2:
return empty
# 稠密点降采样,减小后续多边形/外推开销(保序)
if len(arc) > 600:
arc = arc[:: len(arc) // 600 + 1]
return arc
def _pushed_mask(hair_mask, upper, baseline_pts, push_px, mode, rid="",
center=None, band_expand_px=0):
"""发际线外推遮罩:发际线(仅额带凹处)以眉心为圆心逐点径向外推 push_px,
与 baseline 组闭合区域。
def _pushed_mask(hair_mask, upper, baseline_pts, push_px, rid="",
center=None, chin_y=None, sample_px=12):
"""发际线外推遮罩:先提取头发内轮廓线(①-f),以眉心为圆心把内轮廓逐点径向
「向外」(远离脸中心、推进现有头发)外推 push_px 得到外推线(①-g 黄线);
再取外推线与 baseline(①-a 分割线)组成的闭合区域作为最终遮罩。
center: 圆心 (cx, cy),一般取 151 点(眉心)。
band_expand_px: 额带边界(黄竖线)两侧再外扩的像素数
返回 (mask_bool, hairline_y, pushed_y, lo, hi)
hairline_y/pushed_y —— 长度=w 的 y 数组(额带外 NaN),每列发际线下沿/外推后下沿
注:径向外推后,同一列可能出现多个外推点,这里按列取最靠上的作为遮罩顶界。
lo/hi —— 扩展后的额带左右边界列
即逐列从外推线 pushed_y[x] 填充到 baseline_y[x](仅 pushed_y 在 baseline 以上的列),
与旧逻辑一致:遮罩顶界=外推发际线(覆盖现有头发 push_cm),底界=baseline
区别只是现在 pushed_y 来自修正后的整条内轮廓(额头弧已延伸到两侧鬓角),
额头遮罩宽度不再被截短
center: 圆心 (cx, cy),取 151 点(眉心);内侧判定与径向外推共用
chin_y: 下颌截断 y,透传给 _extract_hairline。
sample_px: 内侧判定采样距离,透传给 _extract_hairline。
返回 (mask_bool, inner_pts, outer_pts)
inner_pts —— 头发内轮廓有序点列 (Nx2)。
outer_pts —— 内轮廓径向外推 push_px 后的有序点列 (Nx2)。
"""
lg = lambda msg: logger.info("[%s] %s", rid, msg) if rid else None
h, w = hair_mask.shape
# 截断完全交给 _extract_hairline(用 baseline 范围 + 两侧外扩)
hairline_y, lo, hi = _extract_hairline(hair_mask, upper, mode, rid=rid,
baseline_pts=baseline_pts, band_expand_px=band_expand_px)
valid = ~np.isnan(hairline_y)
hairline_y = np.where(valid, np.clip(hairline_y, 0, h - 1), np.nan).astype(np.float64)
valid = ~np.isnan(hairline_y)
empty = np.empty((0, 2), dtype=np.int32)
lg(f"_pushed_mask: push_px={push_px} 额带(baseline截断)[{lo},{hi}] 圆心={center}")
inner_pts = _extract_hairline(hair_mask, center=center, chin_y=chin_y,
rid=rid, sample_px=sample_px)
lg(f"_pushed_mask: push_px={push_px} 内轮廓点数={len(inner_pts)} 圆心={center}")
if len(inner_pts) < 2 or center is None:
return np.zeros((h, w), dtype=bool), inner_pts, empty
# 逐点径向外推:每个发际线点沿「从圆心指向它」的方向往外推 push_px。
# 外推后新位置可能不在原列,按列收集所有外推点取最靠上的 y 作为该列遮罩顶界。
pushed_y = np.full(w, np.nan)
if center is not None and valid.any():
cx, cy = float(center[0]), float(center[1])
xs = np.where(valid)[0]
ys = hairline_y[valid]
# 每个点的径向方向(从圆心指向该点),单位向量
dx = xs - cx
dy = ys - cy
dist = np.sqrt(dx * dx + dy * dy)
dist = np.where(dist < 1e-3, 1.0, dist) # 圆心点本身防除零
ux, uy = dx / dist, dy / dist
# 外推后的新坐标
nx = xs + ux * push_px
ny = ys + uy * push_px
ny = np.clip(ny, 0, h - 1)
# 按新 x 做最近邻归并到整数列,取每列最小 ny(最靠上=遮罩顶界)
nx_int = np.clip(np.round(nx).astype(int), 0, w - 1)
for xi, yi in zip(nx_int, ny):
if np.isnan(pushed_y[xi]) or yi < pushed_y[xi]:
pushed_y[xi] = yi
lg(f" 径向外推: 推前y范围[{int(np.nanmin(ys))},{int(np.nanmax(ys))}] "
f"推后y范围[{int(np.nanmin(ny))},{int(np.nanmax(ny))}]")
# 径向归并会产生空列(锯齿),在额带 [lo,hi] 内插值填补,保证遮罩顶界连续
v3 = ~np.isnan(pushed_y)
if v3.any():
xv = np.where(v3)[0]
yv = pushed_y[v3]
pushed_y[lo:hi + 1] = np.interp(np.arange(lo, hi + 1), xv, yv)
else:
# 无圆心:退化为统一往上推
pushed_y = hairline_y - int(push_px)
v2 = ~np.isnan(pushed_y)
pushed_y = np.where(v2, np.clip(pushed_y, 0, h - 1), np.nan)
cx, cy = float(center[0]), float(center[1])
# 逐点径向外推:沿「从圆心指向该点」方向(远离脸中心 = 推进现有头发)外推 push_px
d = inner_pts.astype(np.float64) - np.array([cx, cy])
dist = np.hypot(d[:, 0], d[:, 1])
dist[dist < 1e-3] = 1.0
u = d / dist[:, None]
outer = inner_pts.astype(np.float64) + u * float(push_px)
outer[:, 0] = np.clip(outer[:, 0], 0, w - 1)
outer[:, 1] = np.clip(outer[:, 1], 0, h - 1)
outer_pts = np.round(outer).astype(np.int32)
# baseline 每列的 y
# baseline 每列的 y(①-a 分割线,含左右水平延长)
x0, y0 = baseline_pts[0]
x1, y1 = baseline_pts[-1]
chain_x = np.array([0] + [p[0] for p in baseline_pts] + [w - 1])
chain_y = np.array([y0] + [p[1] for p in baseline_pts] + [y1])
baseline_y = np.interp(np.arange(w), chain_x, chain_y).astype(np.int32)
baseline_y = np.interp(np.arange(w), chain_x, chain_y)
# 逐列填充:仅额带内、且 pushed_y < baseline_y 的列
# 外推线逐列归并:只取 baseline 以上的外推点,逐列取最靠上 y 作为遮罩顶界 pushed_y。
# 两侧鬓角向下的段落到 baseline 以下,自然被排除(与旧逻辑一致,遮罩=额头闭合区域)。
ox = np.clip(outer_pts[:, 0], 0, w - 1)
oy = outer_pts[:, 1].astype(np.float64)
above = oy < baseline_y[ox]
pushed_y = np.full(w, np.nan)
for xi, yi in zip(ox[above], oy[above]):
if np.isnan(pushed_y[xi]) or yi < pushed_y[xi]:
pushed_y[xi] = yi
cols_valid = np.where(~np.isnan(pushed_y))[0]
if len(cols_valid) >= 2:
lo, hi = int(cols_valid.min()), int(cols_valid.max())
pushed_y[lo:hi + 1] = np.interp(np.arange(lo, hi + 1), cols_valid,
pushed_y[cols_valid])
lg(f" 外推线额带[{lo},{hi}] 推后y范围[{int(np.nanmin(pushed_y))},{int(np.nanmax(pushed_y))}]")
# 逐列从 pushed_y 填充到 baseline_y(闭合区域),仅 pushed_y<baseline_y 的列
baseline_yi = baseline_y.astype(np.int32)
mask = np.zeros((h, w), dtype=bool)
fill_valid = ~np.isnan(pushed_y)
cols = np.where(fill_valid & (pushed_y.astype(int) < baseline_y))[0]
lg(f" 有效填充列数={len(cols)} (额带内且 pushed_y<baseline_y)")
pushed_int = np.nan_to_num(pushed_y, nan=float(h)).astype(np.int32)
cols = np.where(fill_valid & (pushed_int < baseline_yi))[0]
lg(f" 有效填充列数={len(cols)} (pushed_y<baseline_y)")
for x in cols:
mask[int(pushed_y[x]):baseline_y[x] + 1, x] = True
lg(f" 填充后 mask 像素={int(mask.sum())}")
mask[int(pushed_int[x]):baseline_yi[x] + 1, x] = True
mask = _largest_cc(mask & upper)
return mask, hairline_y, pushed_y, lo, hi
def _draw_curve(image, y_per_col, color, thickness=3):
""""每列一个 y"的曲线画到图上(用于发际线/外推线可视化)。NaN 列跳过。"""
out = image.copy()
pts = []
segs = []
for x in range(len(y_per_col)):
v = y_per_col[x]
if np.isnan(v):
if len(pts) >= 2:
segs.append(np.array(pts, dtype=np.int32))
pts = []
else:
pts.append([x, int(v)])
if len(pts) >= 2:
segs.append(np.array(pts, dtype=np.int32))
for seg in segs:
cv2.polylines(out, [seg], isClosed=False, color=color, thickness=thickness, lineType=cv2.LINE_AA)
return out
lg(f" 闭合区域 mask 像素={int(mask.sum())}")
return mask, inner_pts, outer_pts
@@ -313,17 +335,22 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
pushed_info = None
if mask_type == "pushed":
push_px = int(round(max(0.0, hairline_push_cm) * px_per_cm))
# 圆心 = 151 点(眉心)完整坐标,用于额带搜索起点 + 径向外推圆心
# 圆心 = 151 点(眉心)完整坐标,内侧判定与径向外推共用
center = baseline_pts[5] if len(baseline_pts) > 5 else None
# 额带边界两侧各外扩 1cm(像素
band_expand_px = int(round(1.0 * px_per_cm))
lg(f"进入 PUSHED 分支: push_px={push_px} 圆心(151)={center} band_expand={band_expand_px}px "
f"edge={hairline_edge}")
mask_bool, hairline_y, pushed_y, lo, hi = _pushed_mask(
hair_mask, upper, baseline_pts, push_px, hairline_edge, rid=rid,
center=center, band_expand_px=band_expand_px)
pushed_info = (hairline_y, pushed_y, push_px, lo, hi)
lg(f"PUSHED 结果: 额带[{lo},{hi}] mask_pixels={int(mask_bool.sum())}")
# 下颌截断线:下巴关键点 152 的 y(内轮廓两侧向下画到这里为止
try:
chin_y = int(round(landmarks.landmark[152].y * h))
except Exception: # noqa: BLE001
chin_y = None
# 内侧判定采样距离 ≈ 0.4cm
sample_px = max(6, int(round(0.4 * px_per_cm)))
lg(f"进入 PUSHED 分支: push_px={push_px} 圆心(151)={center} chin_y={chin_y} "
f"sample_px={sample_px} edge={hairline_edge}")
mask_bool, inner_pts, outer_pts = _pushed_mask(
hair_mask, upper, baseline_pts, push_px, rid=rid,
center=center, chin_y=chin_y, sample_px=sample_px)
pushed_info = (inner_pts, outer_pts, push_px)
lg(f"PUSHED 结果: 内轮廓点数={len(inner_pts)} mask_pixels={int(mask_bool.sum())}")
elif mask_type == "eroded":
mask_bool = eroded
lg(f"进入 ERODED 分支: 用 eroded 遮罩 pixels={int(eroded.sum())}")
@@ -359,16 +386,16 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
"mask_overlay_base64": _jpg_b64(_overlay(image_bgr, mask_bool, (0, 0, 255))),
"mask_base64": _png_b64((mask_bool.astype(np.uint8)) * 255),
}
# pushed 模式:补充发际线提取 + 外推线可视化
# pushed 模式:补充内轮廓提取 + 外推线可视化
if pushed_info is not None:
hairline_y, pushed_y, push_px, lo, hi = pushed_info
# ①-f 提取发际线:绿=头发下沿发际线(已用 baseline 范围截断),黄=baseline 折线(截断依据)
inner_pts, outer_pts, push_px = pushed_info
# ①-f 提取内轮廓:绿=头发内轮廓线(额头弧+两侧到下颌),黄=baseline 折线
hl_img = _draw_baseline(image_bgr, baseline_pts, w) # 画 baseline(黄线+关键点)
hl_img = _draw_curve(hl_img, hairline_y, (0, 255, 0), 3)
hl_img = _draw_polyline(hl_img, inner_pts, (0, 255, 0), 3)
viz["hairline_overlay_base64"] = _jpg_b64(hl_img)
# ①-g 外推发际线:圆心红点(151) + 原发际线(绿)+ 外推线(青)+ 遮罩(红半透明)
ps_img = _draw_curve(image_bgr.copy(), hairline_y, (0, 255, 0), 2)
ps_img = _draw_curve(ps_img, pushed_y, (0, 255, 255), 3)
# ①-g 外推:圆心红点(151) + 内轮廓(绿)+ 外推线(青)+ 遮罩(红半透明)
ps_img = _draw_polyline(image_bgr.copy(), inner_pts, (0, 255, 0), 2)
ps_img = _draw_polyline(ps_img, outer_pts, (0, 255, 255), 3)
# 画圆心(151 点)红点,标示径向外推的中心
if baseline_pts is not None and len(baseline_pts) > 5:
cx151, cy151 = baseline_pts[5]