在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
self.mp_face_mesh = mp.solutions.face_mesh
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):
img = cv2.cvtColor(cv2.flip(img, 1), cv2.COLOR_BGR2RGB)
img.flags.writeable = False
self.results = self.face_mesh.process(img)
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])
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()
self.focal_length = self.size[1]
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")
self.dist_coeefs = np.zeros((4, 1))
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)
(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))
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=[
[
33,
7,
163,
144,
145,
153,
154,
155,
133,
246,
161,
160,
159,
158,
157,
173
],
[
263,
249,
390,
373,
374,
380,
381,
382,
362,
466,
388,
387,
386,
385,
384,
398
]
]
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)
def eye_aspect_ratio(image_points, side):
p1, p2, p3, p4, p5, p6 = 0, 0, 0, 0, 0, 0
tip_of_eyebrow = 0
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[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]
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
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
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
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]
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)
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
height = 600
stop :bpy.props.BoolProperty()
detector = FaceMeshDetector()
pose_estimator = PoseEstimator((height, width))
image_points = np.zeros((pose_estimator.model_points_full.shape[0], 2))
iris_image_points = np.zeros((10, 2))
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)
img = cv2.flip(img, 1)
if faces:
print("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]
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)
mouth_height = FacialFeatures.mouth_height(self.image_points)
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]
bones["head_fk"].rotation_euler[2] = -(y - self.first_angle[1])
bones["head_fk"].rotation_euler[1] = z - self.first_angle[2]
bones["head_fk"].keyframe_insert(data_path="rotation_euler", index=-1)
else:
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.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()
bpy.ops.wm.mediapipe_operator()
|