cmvr-biohead/biohead/algo.py

300 lines
12 KiB
Python
Raw Normal View History

2025-08-21 13:49:41 +08:00
import numpy as np
from biohead.define import HeadJoints, FaceMap
import math
from scipy.optimize import fsolve
from biohead.define import HeadJoints, FaceMap
class LowPassFilter:
def __init__(self, alpha=0.2):
self.alpha = alpha
self.last_val = None
def __call__(self, val):
if self.last_val is None:
self.last_val = val
else:
self.last_val = self.alpha * val + (1 - self.alpha) * self.last_val
return self.last_val
lip_z_lpf_left = LowPassFilter(alpha=0.5)
lip_z_lpf_right = LowPassFilter(alpha=0.5)
def calc_eyebrow(uv, result: HeadJoints):
try:
# LEFT
left_outer = np.array(uv[FaceMap.left_eyelid_outside])
left_inner = np.array(uv[FaceMap.left_eyelid_inside])
eye_width_left = np.linalg.norm(left_inner - left_outer) + 1e-6
dir_eye_left = (left_inner - left_outer) / eye_width_left
perp_left = np.cross(np.array([0, 0, 1]), dir_eye_left)
perp_left /= np.linalg.norm(perp_left) + 1e-6
mid_left = (left_outer + left_inner) / 2
delta_out_left = np.array(uv[FaceMap.left_eyebrow_outside]) - mid_left
delta_in_left = np.array(uv[FaceMap.left_eyebrow_inside]) - mid_left
result.left_eyebrow_outside_y = abs(np.dot(delta_out_left, perp_left)) / eye_width_left
result.left_eyebrow_inside_y = abs(np.dot(delta_in_left, perp_left)) / eye_width_left
# RIGHT
right_outer = np.array(uv[FaceMap.right_eyelid_outside])
right_inner = np.array(uv[FaceMap.right_eyelid_inside])
eye_width_right = np.linalg.norm(right_inner - right_outer) + 1e-6
dir_eye_right = (right_inner - right_outer) / eye_width_right
perp_right = np.cross(np.array([0, 0, 1]), dir_eye_right)
perp_right /= np.linalg.norm(perp_right) + 1e-6
mid_right = (right_outer + right_inner) / 2
delta_out_right = np.array(uv[FaceMap.right_eyebrow_outside]) - mid_right
delta_in_right = np.array(uv[FaceMap.right_eyebrow_inside]) - mid_right
result.right_eyebrow_outside_y = abs(np.dot(delta_out_right, perp_right)) / eye_width_right
result.right_eyebrow_inside_y = abs(np.dot(delta_in_right, perp_right)) / eye_width_right
return result
except KeyError as e:
print("Eyebrow landmarks missing:", e)
def calc_eyelid(uv, result: HeadJoints):
try:
# LEFT
left_outer = np.array(uv[FaceMap.left_eyelid_outside])
left_inner = np.array(uv[FaceMap.left_eyelid_inside])
eye_width_left = np.linalg.norm(left_inner - left_outer) + 1e-6
dir_eye_left = (left_inner - left_outer) / eye_width_left
perp_left = np.cross(np.array([0, 0, 1]), dir_eye_left)
perp_left /= np.linalg.norm(perp_left) + 1e-6
mid_left = (left_outer + left_inner) / 2
delta_upper_left = np.array(uv[FaceMap.left_eyelid_upper]) - mid_left
delta_lower_left = np.array(uv[FaceMap.left_eyelid_lower]) - mid_left
result.left_eye_upper_lid_y = abs(np.dot(delta_upper_left, perp_left)) / eye_width_left
result.left_eye_lower_lid_y = abs(np.dot(delta_lower_left, perp_left)) / eye_width_left
# RIGHT
right_outer = np.array(uv[FaceMap.right_eyelid_outside])
right_inner = np.array(uv[FaceMap.right_eyelid_inside])
eye_width_right = np.linalg.norm(right_inner - right_outer) + 1e-6
dir_eye_right = (right_inner - right_outer) / eye_width_right
perp_right = np.cross(np.array([0, 0, 1]), dir_eye_right)
perp_right /= np.linalg.norm(perp_right) + 1e-6
mid_right = (right_outer + right_inner) / 2
delta_upper_right = np.array(uv[FaceMap.right_eyelid_upper]) - mid_right
delta_lower_right = np.array(uv[FaceMap.right_eyelid_lower]) - mid_right
result.right_eye_upper_lid_y = abs(np.dot(delta_upper_right, perp_right)) / eye_width_right
result.right_eye_lower_lid_y = abs(np.dot(delta_lower_right, perp_right)) / eye_width_right
return result
except KeyError as e:
print("Eyelid landmarks missing:", e)
def calc_eyeball(uv, result: HeadJoints):
try:
# LEFT
left_outer = np.array(uv[FaceMap.left_eyelid_outside])
left_inner = np.array(uv[FaceMap.left_eyelid_inside])
eye_width_left = np.linalg.norm(left_inner - left_outer) + 1e-6
dir_eye_left = (left_inner - left_outer) / eye_width_left
perp_left = np.cross(np.array([0, 0, 1]), dir_eye_left)
perp_left /= np.linalg.norm(perp_left) + 1e-6
mid_left = (left_outer + left_inner) / 2
eyeball_center_left = np.array(uv[FaceMap.left_eyeball_center])
delta_left = eyeball_center_left - mid_left
result.left_eye_ball_x = np.dot(delta_left, dir_eye_left) / eye_width_left
result.left_eye_ball_y = np.dot(delta_left, perp_left) / eye_width_left
# RIGHT
right_outer = np.array(uv[FaceMap.right_eyelid_outside])
right_inner = np.array(uv[FaceMap.right_eyelid_inside])
eye_width_right = np.linalg.norm(right_inner - right_outer) + 1e-6
dir_eye_right = (right_inner - right_outer) / eye_width_right
perp_right = np.cross(np.array([0, 0, 1]), dir_eye_right)
perp_right /= np.linalg.norm(perp_right) + 1e-6
mid_right = (right_outer + right_inner) / 2
eyeball_center_right = np.array(uv[FaceMap.right_eyeball_center])
delta_right = eyeball_center_right - mid_right
result.right_eye_ball_x = np.dot(delta_right, dir_eye_right) / eye_width_right
result.right_eye_ball_y = np.dot(delta_right, perp_right) / eye_width_right
return result
except KeyError as e:
print("Eyeball landmarks missing:", e)
def _jaw_ik_eqs_3d(thetas, y_d, z_d, L1, L2):
theta1, theta2 = thetas
t1, t2 = math.radians(theta1), math.radians(theta2)
y = L1 * math.cos(t1) + L2 * math.cos(t2)
z = L1 * math.sin(t1) + L2 * math.sin(t2)
return [
y - y_d,
z - z_d
]
def _solve_jaw_servo_angles(y_d, z_d, L1, L2, init_guess=(0.0, 0.0)):
sol = fsolve(_jaw_ik_eqs_3d, init_guess, args=(y_d, z_d, L1, L2))
return sol[0], sol[1]
def _normalize_angle(angle, min_angle=-100, max_angle=100):
return max(0.0, min(1.0, (angle - min_angle) / (max_angle - min_angle)))
def calc_mouth(uv, result: HeadJoints):
try:
left_uv = uv[FaceMap.left_head]
right_uv = uv[FaceMap.right_head]
top_uv = uv[FaceMap.top_head]
bottom_uv = uv[FaceMap.bottom_head]
head_width = abs(right_uv[0] - left_uv[0]) + 1e-6
head_height = abs(top_uv[1] - bottom_uv[1]) + 1e-6
# === 新增:计算头部平均深度用于 z 归一化 ===
head_depth = (
abs(left_uv[2]) + abs(right_uv[2]) +
abs(top_uv[2]) + abs(bottom_uv[2]) +
abs(uv[FaceMap.mid_up_lip][2])
) / 5 + 1e-6
upper_center_u = (left_uv[0] + right_uv[0] + top_uv[0]) / 3
upper_center_v = (left_uv[1] + right_uv[1] + top_uv[1]) / 3
bottom_center_u = bottom_uv[0]
bottom_center_v = bottom_uv[1]
upper_mouth_points = {
"upper_right_lip": FaceMap.right_upper_lip,
"upper_left_lip": FaceMap.left_upper_lip,
"right_corner_lip": FaceMap.right_mouth_corner,
"left_corner_lip": FaceMap.left_mouth_corner,
"upper_lip": FaceMap.mid_up_lip
}
jaw_y_accum = 0.0
jaw_z_accum = 0.0
jaw_count = 0
for name, idx in upper_mouth_points.items():
u, v, z = uv[idx]
delta_u = u - upper_center_u
delta_v = v - upper_center_v
x_val = delta_u / head_width
y_val = delta_v / head_height
# === 滤波并归一化 z 值 ===
if name == "left_corner_lip":
z = lip_z_lpf_left(z)
elif name == "right_corner_lip":
z = lip_z_lpf_right(z)
z_val = z / head_depth
setattr(result, f"{name}_x", x_val)
setattr(result, f"{name}_y", y_val)
setattr(result, f"{name}_z", z_val)
if name in ("left_corner_lip", "right_corner_lip"):
jaw_y_accum += y_val * head_height
jaw_z_accum += z_val * head_height
jaw_count += 1
# === IK 解算(仅嘴角) ===
if jaw_count > 0:
jaw_displacement_y = jaw_y_accum / jaw_count
jaw_displacement_z = jaw_z_accum / jaw_count
else:
jaw_displacement_y = 0.0
jaw_displacement_z = 0.0
L1, L2 = 145, 177
theta1, theta2 = _solve_jaw_servo_angles(
y_d=jaw_displacement_y,
z_d=jaw_displacement_z,
L1=L1,
L2=L2,
init_guess=(0.0, 0.0)
)
norm_theta1 = _normalize_angle(theta1)
norm_theta2 = _normalize_angle(theta2)
print("norm_theta1", norm_theta1)
print("norm_theta2", norm_theta2)
result.upper_left_lip_y = norm_theta1
result.upper_right_lip_y = norm_theta1
result.left_corner_lip_y = norm_theta2
result.right_corner_lip_y = norm_theta2
# === 下唇位置记录(不参与 IK ===
lower_mouth_points = {
"lower_right_lip": FaceMap.right_lower_lip,
"lower_left_lip": FaceMap.left_lower_lip,
"lower_lip": FaceMap.mid_down_lip
}
for name, idx in lower_mouth_points.items():
u, v, z = uv[idx]
delta_u = u - bottom_center_u
delta_v = v - bottom_center_v
x_val = delta_u / head_width
y_val = delta_v / head_height
z_val = z / head_depth
setattr(result, f"{name}_x", x_val)
setattr(result, f"{name}_y", y_val)
setattr(result, f"{name}_z", z_val)
return result
except KeyError as e:
print("Mouth landmarks missing:", e)
return result
def calc_jaw(uv, result: HeadJoints):
try:
# === 获取头部关键点 ===
upper_indices = [
FaceMap.left_eyebrow_inside,
FaceMap.right_eyebrow_inside,
FaceMap.left_eyelid_inside,
FaceMap.right_eyelid_inside,
FaceMap.top_head
]
lower_indices = [
FaceMap.mid_down_lip,
FaceMap.left_mouth_corner,
FaceMap.right_mouth_corner,
FaceMap.bottom_head
]
upper_points = np.array([uv[idx] for idx in upper_indices])
lower_points = np.array([uv[idx] for idx in lower_indices])
upper_center = np.mean(upper_points, axis=0)
lower_center = np.mean(lower_points, axis=0)
# === 获取头高进行归一化 ===
top_uv = np.array(uv[FaceMap.top_head])
bottom_uv = np.array(uv[FaceMap.bottom_head])
head_height = abs(top_uv[1] - bottom_uv[1]) + 1e-6
head_width = abs(uv[FaceMap.left_head][0] - uv[FaceMap.right_head][0]) + 1e-6
# === 张嘴判断:中心点垂直位移归一化 ===
vertical_offset = abs(lower_center[1] - upper_center[1])
mouth_open_ratio = vertical_offset / head_height
if mouth_open_ratio > 0.035:
result.jaw_y = mouth_open_ratio
else:
result.jaw_y = 0.0
# === 左右偏移保留 ===
upper_lip = np.array(uv[FaceMap.mid_up_lip])
lower_lip = np.array(uv[FaceMap.mid_down_lip])
result.jaw_x = abs(lower_lip[0] - upper_lip[0]) / head_width
return result
except KeyError as e:
print("Jaw landmarks missing:", e)
return result