在Blender中使用代码控制人物模型的头部姿态 - 代码实践mediapipe版本
flyfish
MediaPipe的FaceMesh能够检测468个人脸关键点,这里使用此组件驱动头部动作。本文最后是整体代码。
可以按照该文配置环境,然后安装mediapipe,简便的安装方式就是 pip install mediapipe
检测结果类似这样
下面的图片可以下载放大看,每个数值所代表的关键点
期望代码能够实现类似下面效果的头部姿态
关于头部姿态的整体代码如下
from argparse import ArgumentParser
import cv2
import mediapipe as mp
import numpy as np
from collections import deque
from platform import system
import bpy
import mathutils
import time
from imutils import face_utils
class FaceMeshDetector:
def __init__(self,
static_image_mode=False,
max_num_faces=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5):
self.static_image_mode = static_image_mode
self.max_num_faces = max_num_faces
self.min_detection_confidence = min_detection_confidence
self.min_tracking_confidence = min_tracking_confidence
# Facemesh
self.mp_face_mesh = mp.solutions.face_mesh
# The object to do the stuffs
self.face_mesh = self.mp_face_mesh.FaceMesh(
self.static_image_mode,
self.max_num_faces,
True,
self.min_detection_confidence,
self.min_tracking_confidence
)
self.mp_drawing = mp.solutions.drawing_utils
self.drawing_spec = self.mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
def findFaceMesh(self, img, draw=True):
# convert the img from BRG to RGB
img = cv2.cvtColor(cv2.flip(img, 1), cv2.COLOR_BGR2RGB)
# To improve performance, optionally mark the image as not writeable to
# pass by reference.
img.flags.writeable = False
self.results = self.face_mesh.process(img)
# Draw the face mesh annotations on the image.
img.flags.writeable = True
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
self.imgH, self.imgW, self.imgC = img.shape
self.faces = []
if self.results.multi_face_landmarks:
for face_landmarks in self.results.multi_face_landmarks:
if draw:
self.mp_drawing.draw_landmarks(
image = img,
landmark_list = face_landmarks,
connections = self.mp_face_mesh.FACEMESH_TESSELATION,
landmark_drawing_spec = self.drawing_spec,
connection_drawing_spec = self.drawing_spec)
face = []
for id, lmk in enumerate(face_landmarks.landmark):
x, y = int(lmk.x * self.imgW), int(lmk.y * self.imgH)
face.append([x, y])
# show the id of each point on the image
# cv2.putText(img, str(id), (x-4, y-4), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1, cv2.LINE_AA)
self.faces.append(face)
return img, self.faces
"""
Estimate head pose according to the facial landmarks
"""
class PoseEstimator:
def __init__(self, img_size=(480, 640)):
self.size = img_size
self.model_points_full = self.get_full_model_points()
# Camera internals
self.focal_length = self.size[1] #height
self.camera_center = (self.size[1] / 2, self.size[0] / 2)
self.camera_matrix = np.array(
[[self.focal_length, 0, self.camera_center[0]],
[0, self.focal_length, self.camera_center[1]],
[0, 0, 1]], dtype="double")
# Assuming no lens distortion
self.dist_coeefs = np.zeros((4, 1))
# Rotation vector and translation vector
self.r_vec = None
self.t_vec = None
def get_full_model_points(self, filename='/media/ubuntu/data/tool/blender-2.82-linux64/pure/model.txt'):
"""Get all 468 3D model points from file"""
raw_value = []
with open(filename) as file:
for line in file:
raw_value.append(line)
model_points = np.array(raw_value, dtype=np.float32)
model_points = np.reshape(model_points, (-1, 3))
return model_points
def solve_pose_by_all_points(self, image_points):
"""
Solve pose from all the 468 image points
Return (rotation_vector, translation_vector) as pose.
"""
if self.r_vec is None:
(_, rotation_vector, translation_vector) = cv2.solvePnP(
self.model_points_full, image_points, self.camera_matrix, self.dist_coeefs)
self.r_vec = rotation_vector
self.t_vec = translation_vector
(_, rotation_vector, translation_vector) = cv2.solvePnP(
self.model_points_full,
image_points,
self.camera_matrix,
self.dist_coeefs,
rvec=self.r_vec,
tvec=self.t_vec,
useExtrinsicGuess=True)
return (rotation_vector, translation_vector),rotation_vector
def draw_annotation_box(self, image, rotation_vector, translation_vector, color=(255, 255, 255), line_width=2):
"""Draw a 3D box as annotation of pose"""
point_3d = []
rear_size = 75
rear_depth = 0
point_3d.append((-rear_size, -rear_size, rear_depth))
point_3d.append((-rear_size, rear_size, rear_depth))
point_3d.append((rear_size, rear_size, rear_depth))
point_3d.append((rear_size, -rear_size, rear_depth))
point_3d.append((-rear_size, -rear_size, rear_depth))
front_size = 40
front_depth = 400
point_3d.append((-front_size, -front_size, front_depth))
point_3d.append((-front_size, front_size, front_depth))
point_3d.append((front_size, front_size, front_depth))
point_3d.append((front_size, -front_size, front_depth))
point_3d.append((-front_size, -front_size, front_depth))
point_3d = np.array(point_3d, dtype=np.float).reshape(-1, 3)
# Map to 2d image points
(point_2d, _) = cv2.projectPoints(point_3d,
rotation_vector,
translation_vector,
self.camera_matrix,
self.dist_coeefs)
point_2d = np.int32(point_2d.reshape(-1, 2))
# Draw all the lines
cv2.polylines(image, [point_2d], True, color, line_width, cv2.LINE_AA)
cv2.line(image, tuple(point_2d[1]), tuple(
point_2d[6]), color, line_width, cv2.LINE_AA)
cv2.line(image, tuple(point_2d[2]), tuple(
point_2d[7]), color, line_width, cv2.LINE_AA)
cv2.line(image, tuple(point_2d[3]), tuple(
point_2d[8]), color, line_width, cv2.LINE_AA)
def draw_axis(self, img, R, t):
axis_length = 20
axis = np.float32(
[[axis_length, 0, 0], [0, axis_length, 0], [0, 0, axis_length]]).reshape(-1, 3)
axisPoints, _ = cv2.projectPoints(
axis, R, t, self.camera_matrix, self.dist_coeefs)
img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
axisPoints[0].ravel()), (255, 0, 0), 3)
img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
axisPoints[1].ravel()), (0, 255, 0), 3)
img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
axisPoints[2].ravel()), (0, 0, 255), 3)
def draw_axes(self, img, R, t):
img = cv2.drawFrameAxes(img, self.camera_matrix, self.dist_coeefs, R, t, 20)
def reset_r_vec_t_vec(self):
self.r_vec = None
self.t_vec = None
from enum import Enum
class Eyes(Enum):
LEFT = 1
RIGHT = 2
class FacialFeatures:
eye_key_indicies=[
[
# Left eye
# eye lower contour
33,
7,
163,
144,
145,
153,
154,
155,
133,
# eye upper contour (excluding corners)
246,
161,
160,
159,
158,
157,
173
],
[
# Right eye
# eye lower contour
263,
249,
390,
373,
374,
380,
381,
382,
362,
# eye upper contour (excluding corners)
466,
388,
387,
386,
385,
384,
398
]
]
# custom img resize function
def resize_img(img, scale_percent):
width = int(img.shape[1] * scale_percent / 100.0)
height = int(img.shape[0] * scale_percent / 100.0)
return cv2.resize(img, (width, height), interpolation = cv2.INTER_AREA)
# calculate eye apsect ratio to detect blinking
# and/ or control closing/ opening of eye
def eye_aspect_ratio(image_points, side):
p1, p2, p3, p4, p5, p6 = 0, 0, 0, 0, 0, 0
tip_of_eyebrow = 0
# get the contour points at img pixel first
# following the eye aspect ratio formula with little modifications
# to match the facemesh model
if side == Eyes.LEFT:
eye_key_left = FacialFeatures.eye_key_indicies[0]
p2 = np.true_divide(
np.sum([image_points[eye_key_left[10]], image_points[eye_key_left[11]]], axis=0),
2)
p3 = np.true_divide(
np.sum([image_points[eye_key_left[13]], image_points[eye_key_left[14]]], axis=0),
2)
p6 = np.true_divide(
np.sum([image_points[eye_key_left[2]], image_points[eye_key_left[3]]], axis=0),
2)
p5 = np.true_divide(
np.sum([image_points[eye_key_left[5]], image_points[eye_key_left[6]]], axis=0),
2)
p1 = image_points[eye_key_left[0]]
p4 = image_points[eye_key_left[8]]
# tip_of_eyebrow = image_points[63]
tip_of_eyebrow = image_points[105]
elif side == Eyes.RIGHT:
eye_key_right = FacialFeatures.eye_key_indicies[1]
p3 = np.true_divide(
np.sum([image_points[eye_key_right[10]], image_points[eye_key_right[11]]], axis=0),
2)
p2 = np.true_divide(
np.sum([image_points[eye_key_right[13]], image_points[eye_key_right[14]]], axis=0),
2)
p5 = np.true_divide(
np.sum([image_points[eye_key_right[2]], image_points[eye_key_right[3]]], axis=0),
2)
p6 = np.true_divide(
np.sum([image_points[eye_key_right[5]], image_points[eye_key_right[6]]], axis=0),
2)
p1 = image_points[eye_key_right[8]]
p4 = image_points[eye_key_right[0]]
tip_of_eyebrow = image_points[334]
# https://downloads.hindawi.com/journals/cmmm/2020/1038906.pdf
# Fig (3)
ear = np.linalg.norm(p2-p6) + np.linalg.norm(p3-p5)
ear /= (2 * np.linalg.norm(p1-p4) + 1e-6)
ear = ear * (np.linalg.norm(tip_of_eyebrow-image_points[2]) / np.linalg.norm(image_points[6]-image_points[2]))
return ear
# calculate mouth aspect ratio to detect mouth movement
# to control opening/ closing of mouth in avatar
# https://miro.medium.com/max/1508/0*0rVqugQAUafxXYXE.jpg
def mouth_aspect_ratio(image_points):
p1 = image_points[78]
p2 = image_points[81]
p3 = image_points[13]
p4 = image_points[311]
p5 = image_points[308]
p6 = image_points[402]
p7 = image_points[14]
p8 = image_points[178]
mar = np.linalg.norm(p2-p8) + np.linalg.norm(p3-p7) + np.linalg.norm(p4-p6)
mar /= (2 * np.linalg.norm(p1-p5) + 1e-6)
return mar
def mouth_distance(image_points):
p1 = image_points[78]
p5 = image_points[308]
return np.linalg.norm(p1-p5)
def mouth_height(image_points):
p3 = image_points[13]
p7 = image_points[14]
return np.linalg.norm(p3-p7)-0.5
# detect iris through new landmark coordinates produced by mediapipe
# replacing the old image processing method
def detect_iris(image_points, iris_image_points, side):
'''
return:
x_rate: how much the iris is toward the left. 0 means totally left and 1 is totally right.
y_rate: how much the iris is toward the top. 0 means totally top and 1 is totally bottom.
'''
iris_img_point = -1
p1, p4 = 0, 0
eye_y_high, eye_y_low = 0, 0
x_rate, y_rate = 0.5, 0.5
# get the corresponding image coordinates of the landmarks
if side == Eyes.LEFT:
iris_img_point = 468
eye_key_left = FacialFeatures.eye_key_indicies[0]
p1 = image_points[eye_key_left[0]]
p4 = image_points[eye_key_left[8]]
eye_y_high = image_points[eye_key_left[12]]
eye_y_low = image_points[eye_key_left[4]]
elif side == Eyes.RIGHT:
iris_img_point = 473
eye_key_right = FacialFeatures.eye_key_indicies[1]
p1 = image_points[eye_key_right[8]]
p4 = image_points[eye_key_right[0]]
eye_y_high = image_points[eye_key_right[12]]
eye_y_low = image_points[eye_key_right[4]]
p_iris = iris_image_points[iris_img_point - 468]
# find the projection of iris_image_point on the straight line fromed by p1 and p4
# through vector dot product
# to get x_rate
vec_p1_iris = [p_iris[0] - p1[0], p_iris[1] - p1[1]]
vec_p1_p4 = [p4[0] - p1[0], p4[1] - p1[1]]
x_rate = (np.dot(vec_p1_iris, vec_p1_p4) / (np.linalg.norm(p1-p4) + 1e-06)) / (np.linalg.norm(p1-p4) + 1e-06)
# find y-rate simiilarily
vec_eye_h_iris = [p_iris[0] - eye_y_high[0], p_iris[1] - eye_y_high[1]]
vec_eye_h_eye_l = [eye_y_low[0] - eye_y_high[0], eye_y_low[1] - eye_y_high[1]]
y_rate = (np.dot(vec_eye_h_eye_l, vec_eye_h_iris) / (np.linalg.norm(eye_y_high - eye_y_low) + 1e-06)) / (np.linalg.norm(eye_y_high - eye_y_low) + 1e-06)
return x_rate, y_rate
def print_debug_msg(args):
msg = '%.4f ' * len(args) % args
print(msg)
class MediaPipeAnimOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "wm.mediapipe_operator"
bl_label = "MediaPipe Animation Operator"
rig_name = "RIG-Vincent"
_timer = None
_cap = None
width = 800 #640
height = 600 #480
stop :bpy.props.BoolProperty()
# Facemesh
detector = FaceMeshDetector()
# Pose estimation related 480 640
pose_estimator = PoseEstimator((height, width))
image_points = np.zeros((pose_estimator.model_points_full.shape[0], 2))
# extra 10 points due to new attention model (in iris detection)
iris_image_points = np.zeros((10, 2))
# Keeps a moving average of given length
def smooth_value(self, name, length, value):
if not hasattr(self, 'smooth'):
self.smooth = {}
if not name in self.smooth:
self.smooth[name] = np.array([value])
else:
self.smooth[name] = np.insert(arr=self.smooth[name], obj=0, values=value)
if self.smooth[name].size > length:
self.smooth[name] = np.delete(self.smooth[name], self.smooth[name].size-1, 0)
sum = 0
for val in self.smooth[name]:
sum += val
return sum / self.smooth[name].size
def modal(self, context, event):
if (event.type in {'RIGHTMOUSE', 'ESC'}) or self.stop == True:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
self.init_camera()
success, img = self._cap.read()
if not success:
print("Ignoring empty camera frame.")
return {'PASS_THROUGH'}
img_facemesh, faces = self.detector.findFaceMesh(img)
# flip the input image so that it matches the facemesh stuff
img = cv2.flip(img, 1)
# if there is any face detected
if faces:
print("face")
# only get the first face
for i in range(len(self.image_points)):
self.image_points[i, 0] = faces[0][i][0]
self.image_points[i, 1] = faces[0][i][1]
for j in range(len(self.iris_image_points)):
self.iris_image_points[j, 0] = faces[0][j + 468][0]
self.iris_image_points[j, 1] = faces[0][j + 468][1]
# The third step: pose estimation
# pose: [[rvec], [tvec]]
pose,rotation_vector = self.pose_estimator.solve_pose_by_all_points(self.image_points)
x_ratio_left, y_ratio_left = FacialFeatures.detect_iris(self.image_points, self.iris_image_points, Eyes.LEFT)
x_ratio_right, y_ratio_right = FacialFeatures.detect_iris(self.image_points, self.iris_image_points, Eyes.RIGHT)
ear_left = FacialFeatures.eye_aspect_ratio(self.image_points, Eyes.LEFT)
ear_right = FacialFeatures.eye_aspect_ratio(self.image_points, Eyes.RIGHT)
pose_eye = [ear_left, ear_right, x_ratio_left, y_ratio_left, x_ratio_right, y_ratio_right]
mar = FacialFeatures.mouth_aspect_ratio(self.image_points)
mouth_distance = FacialFeatures.mouth_distance(self.image_points) #width
mouth_height = FacialFeatures.mouth_height(self.image_points)
# print("left eye: %.2f, %.2f" % (x_ratio_left, y_ratio_left))
# print("right eye: %.2f, %.2f" % (x_ratio_right, y_ratio_right))
bones = bpy.data.objects['RIG-Vincent'].pose.bones
if not hasattr(self, 'first_angle'):
self.first_angle = np.copy(rotation_vector)
x=rotation_vector[0]
y=rotation_vector[1]
z=rotation_vector[2]
bones["head_fk"].rotation_euler[0] = x - self.first_angle[0] # Up/Down
bones["head_fk"].rotation_euler[2] = -(y - self.first_angle[1]) # Rotate
bones["head_fk"].rotation_euler[1] = z - self.first_angle[2] # Left/Right
bones["head_fk"].keyframe_insert(data_path="rotation_euler", index=-1)
else:
# reset our pose estimator
pass
cv2.imshow("Output",img_facemesh)
cv2.waitKey(1)
return {'PASS_THROUGH'}
def init_camera(self):
if self._cap == None:
self._cap = cv2.VideoCapture(0)
#self._cap = cv2.VideoCapture("/media/ubuntu/data/sign_videos/HabenSieSchmerzen0.mp4")
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
time.sleep(0.5)
def stop_playback(self, scene):
print(format(scene.frame_current) + " / " + format(scene.frame_end))
if scene.frame_current == scene.frame_end:
bpy.ops.screen.animation_cancel(restore_frame=False)
def execute(self, context):
bpy.app.handlers.frame_change_pre.append(self.stop_playback)
wm = context.window_manager
self._timer = wm.event_timer_add(0.02, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
cv2.destroyAllWindows()
self._cap.release()
self._cap = None
def register():
bpy.utils.register_class(MediaPipeAnimOperator)
def unregister():
bpy.utils.unregister_class(MediaPipeAnimOperator)
if __name__ == "__main__":
register()
# test call
bpy.ops.wm.mediapipe_operator()