cmvr-biohead/biohead/algo.py
2025-08-21 13:49:41 +08:00

300 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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