307 lines
11 KiB
Python
307 lines
11 KiB
Python
import sys
|
||
sys.path.insert(0, "./generated")
|
||
|
||
import cv2
|
||
import yaml
|
||
import numpy as np
|
||
import mediapipe as mp
|
||
import pyrealsense2 as rs
|
||
from collections import deque
|
||
import grpc
|
||
import threading
|
||
import queue
|
||
import time
|
||
import traceback
|
||
|
||
from biohead.algo import *
|
||
from biohead.utils import calc_feature
|
||
from biohead.utils import norm
|
||
|
||
from datetime import datetime
|
||
from google.protobuf.timestamp_pb2 import Timestamp
|
||
from generated.cmvr.api import common_pb2
|
||
from generated.cmvr.api import biohead_service_pb2
|
||
from generated.cmvr.api import biohead_command_pb2
|
||
from generated.cmvr.api import biohead_service_pb2_grpc
|
||
|
||
|
||
def build_command_header(device_id):
|
||
"""构建命令头部proto消息 - 使用common.proto中的定义"""
|
||
# 使用common.proto中的CommandHeader.Request
|
||
header = common_pb2.CommandHeader.Request()
|
||
|
||
# 设置device_id
|
||
header.device_id = device_id
|
||
|
||
# 设置timestamp
|
||
now = datetime.utcnow()
|
||
timestamp = Timestamp()
|
||
timestamp.FromDatetime(now)
|
||
header.timestamp.CopyFrom(timestamp)
|
||
|
||
return header
|
||
|
||
|
||
def build_facial_expression(result):
|
||
"""将算法结果转换为proto面部表情消息 - 完整映射所有字段"""
|
||
expr = biohead_command_pb2.FacialExpression()
|
||
|
||
|
||
try:
|
||
# 眉毛部分
|
||
if hasattr(expr, 'eyebrow'):
|
||
expr.eyebrow.left_outside_y = result.left_eyebrow_outside_y
|
||
expr.eyebrow.left_inside_y = result.left_eyebrow_inside_y
|
||
expr.eyebrow.right_outside_y = result.right_eyebrow_outside_y
|
||
expr.eyebrow.right_inside_y = result.right_eyebrow_inside_y
|
||
|
||
# 眼睑部分s
|
||
if hasattr(expr, 'eyelid'):
|
||
expr.eyelid.left_upper_y = result.left_eye_upper_lid_y
|
||
expr.eyelid.left_lower_y = result.left_eye_lower_lid_y
|
||
expr.eyelid.right_upper_y = result.right_eye_upper_lid_y
|
||
expr.eyelid.right_lower_y = result.right_eye_lower_lid_y
|
||
print(expr.eyelid.left_upper_y, expr.eyelid.left_lower_y,expr.eyelid.right_upper_y, expr.eyelid.right_lower_y)
|
||
|
||
# 眼球部分
|
||
if hasattr(expr, 'eyeball'):
|
||
expr.eyeball.left_y = result.left_eye_ball_y
|
||
expr.eyeball.right_y = result.right_eye_ball_y
|
||
|
||
|
||
# 鼻子部分
|
||
# 注意:算法结果中没有鼻子数据,但proto定义了nose字段
|
||
# 如果需要,可以从算法中提取并设置
|
||
|
||
# 嘴巴部分
|
||
if hasattr(expr, 'mouth'):
|
||
expr.mouth.upper_lip_y = result.upper_lip_y
|
||
expr.mouth.upper_lip_z = result.upper_lip_z
|
||
expr.mouth.lower_lip_y = result.lower_lip_y
|
||
expr.mouth.lower_lip_z = result.lower_lip_z
|
||
|
||
# 左嘴唇细节
|
||
if hasattr(expr.mouth, 'left_lip'):
|
||
expr.mouth.left_lip.upper_x = result.upper_left_lip_x
|
||
expr.mouth.left_lip.upper_y = result.upper_left_lip_y
|
||
expr.mouth.left_lip.corner_x = result.left_corner_lip_x
|
||
expr.mouth.left_lip.corner_y = result.left_corner_lip_y
|
||
expr.mouth.left_lip.lower_x = result.lower_left_lip_x
|
||
expr.mouth.left_lip.lower_y = result.lower_left_lip_y
|
||
|
||
# 右嘴唇细节
|
||
if hasattr(expr.mouth, 'right_lip'):
|
||
expr.mouth.right_lip.upper_x = result.upper_right_lip_x
|
||
expr.mouth.right_lip.upper_y = result.upper_right_lip_y
|
||
expr.mouth.right_lip.corner_x = result.right_corner_lip_x
|
||
expr.mouth.right_lip.corner_y = result.right_corner_lip_y
|
||
expr.mouth.right_lip.lower_x = result.lower_right_lip_x
|
||
expr.mouth.right_lip.lower_y = result.lower_right_lip_y
|
||
|
||
# 下巴部分
|
||
if hasattr(expr, 'jaw'):
|
||
expr.jaw.x = result.jaw_y
|
||
expr.jaw.y = result.jaw_y
|
||
|
||
except AttributeError as e:
|
||
print(f"构建面部表情错误: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
return expr
|
||
|
||
def main(calib_file):
|
||
# 加载配置文件
|
||
with open("./config/config.yaml", "r") as f:
|
||
config = yaml.safe_load(f)
|
||
|
||
with open(calib_file, "r") as f:
|
||
calib = yaml.safe_load(f)
|
||
|
||
# 初始化RealSense相机
|
||
w, h, fps = config['Camera']['image_width'], config['Camera']['image_height'], config['Camera'].get('fps', 50)
|
||
pipe = rs.pipeline()
|
||
cfg = rs.config()
|
||
cfg.enable_stream(rs.stream.color, w, h, rs.format.bgr8, fps)
|
||
pipe.start(cfg)
|
||
align = rs.align(rs.stream.color)
|
||
|
||
# 初始化MediaPipe面部网格
|
||
mp_mesh = mp.solutions.face_mesh
|
||
mesh = mp_mesh.FaceMesh(
|
||
max_num_faces=1,
|
||
refine_landmarks=True,
|
||
min_detection_confidence=config['MediaPipe']['min_detection_confidence'],
|
||
min_tracking_confidence=config['MediaPipe']['min_tracking_confidence']
|
||
)
|
||
|
||
# 初始化平滑处理队列
|
||
ray_origins = deque(maxlen=config.get('Smooth', 5))
|
||
ray_directions = deque(maxlen=config.get('Smooth', 5))
|
||
result = HeadJoints()
|
||
|
||
# 初始化gRPC客户端
|
||
device_id = "bio_head"
|
||
frame_queue = queue.Queue(maxsize=30) # 增大队列容量
|
||
|
||
# 创建gRPC通道和存根
|
||
# channel = grpc.insecure_channel('localhost:50051')
|
||
channel = grpc.insecure_channel('10.148.108.162:50052')
|
||
|
||
stub = biohead_service_pb2_grpc.BioHeadServiceStub(channel)
|
||
|
||
# 请求生成器函数 - 使用正确的请求类型
|
||
def request_generator():
|
||
try:
|
||
while True:
|
||
item = frame_queue.get()
|
||
if item is None:
|
||
# 发送结束请求
|
||
request = biohead_command_pb2.StreamFacialExpression.Request(
|
||
header=build_command_header(device_id),
|
||
eof=True
|
||
)
|
||
yield request
|
||
break
|
||
|
||
header, expr = item
|
||
request = biohead_command_pb2.StreamFacialExpression.Request(
|
||
header=header,
|
||
expr=expr,
|
||
eof=False
|
||
)
|
||
yield request
|
||
frame_queue.task_done()
|
||
except Exception as e:
|
||
print(f"请求生成器出错: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
# 启动gRPC流式调用
|
||
response_stream = stub.StreamExpression(request_generator())
|
||
|
||
# 响应处理函数
|
||
def process_responses():
|
||
try:
|
||
for response in response_stream:
|
||
# 根据proto定义,响应应该是StreamFacialExpression.Feedback类型
|
||
if response.HasField("header"):
|
||
print(f"服务器响应 - 成功: {response.header.success}, 消息: {response.header.error_message}")
|
||
|
||
if response.HasField("expr_diff"):
|
||
print(f"收到表情差异 - 左眼位置差异: {response.expr_diff.eyeball.left_y:.2f}")
|
||
|
||
except grpc.RpcError as e:
|
||
print(f"gRPC错误 - 代码: {e.code()}, 详情: {e.details()}")
|
||
print(f"调试信息: {e.debug_error_string()}")
|
||
except Exception as e:
|
||
print(f"响应处理异常: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
|
||
|
||
|
||
print("[INFO] 启动RealSense + gRPC流式客户端")
|
||
|
||
try:
|
||
frame_count = 0
|
||
start_time = time.time()
|
||
frame_skip = 2 # 每2帧处理1次,降低数据生成速度
|
||
frame_counter = 0
|
||
last_called_time = time.time() # 用于跟踪上次调用时间
|
||
|
||
while True:
|
||
current_time = time.time()
|
||
|
||
# 判断是否已经2秒过去
|
||
# time.sleep(0.1)
|
||
# if current_time - last_called_time >= 0.15: # 2秒间隔
|
||
frame_counter += 1
|
||
last_called_time = current_time # 更新上次调用时间
|
||
|
||
# 获取相机帧
|
||
frames = pipe.wait_for_frames()
|
||
aligned_frames = align.process(frames)
|
||
color_frame = aligned_frames.get_color_frame()
|
||
if not color_frame:
|
||
continue
|
||
|
||
# 处理图像与面部特征
|
||
color_image = np.asanyarray(color_frame.get_data())
|
||
rgb_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2RGB)
|
||
results = mesh.process(rgb_image)
|
||
if not results.multi_face_landmarks:
|
||
cv2.putText(color_image, "未检测到面部", (20, 40),
|
||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
||
cv2.imshow("RGB", color_image)
|
||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||
break
|
||
continue
|
||
|
||
# 计算特征与归一化
|
||
uv = calc_feature(color_image, results, ray_origins, ray_directions)
|
||
result = calc_eyebrow(uv, result)
|
||
result = calc_eyelid(uv, result)
|
||
result = calc_eyeball(uv, result)
|
||
result = calc_mouth(uv, result)
|
||
result = calc_jaw(uv, result)
|
||
result = norm(result, calib)
|
||
|
||
# 计算并显示最新的FPS
|
||
elapsed_time = time.time() - start_time
|
||
fps = frame_count / elapsed_time if elapsed_time > 0 else 0
|
||
|
||
# 构建并发送数据
|
||
try:
|
||
header = build_command_header(device_id)
|
||
expr = build_facial_expression(result)
|
||
except Exception as e:
|
||
print(f"构建gRPC消息错误: {str(e)}")
|
||
traceback.print_exc()
|
||
continue
|
||
|
||
# 入队
|
||
try:
|
||
frame_queue.put((header, expr), timeout=0.01)
|
||
print(f"帧入队成功,队列大小: {frame_queue.qsize()}")
|
||
except queue.Full:
|
||
print(f"[WARNING] 队列已满({frame_queue.qsize()}/{frame_queue.maxsize}),跳过当前帧")
|
||
|
||
# 显示状态
|
||
frame_count += 1
|
||
cv2.putText(color_image, f"FPS: {fps:.1f} | 队列: {frame_queue.qsize()}/30", (20, 40),
|
||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
||
cv2.imshow("RGB", color_image)
|
||
|
||
key = cv2.waitKey(1)
|
||
if key & 0xFF == ord('q'):
|
||
break
|
||
elif key & 0xFF == ord(' '):
|
||
cv2.waitKey(0)
|
||
|
||
except KeyboardInterrupt:
|
||
print("用户中断")
|
||
except Exception as e:
|
||
print(f"主循环错误: {str(e)}")
|
||
traceback.print_exc()
|
||
finally:
|
||
print("[INFO] 关闭客户端...")
|
||
frame_queue.put(None)
|
||
pipe.stop()
|
||
cv2.destroyAllWindows()
|
||
response_thread.join(timeout=2.0)
|
||
channel.close()
|
||
print("[INFO] 客户端已关闭")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
try:
|
||
main("./config/calibrated_interval.yaml")
|
||
except Exception as e:
|
||
print(f"程序异常: {str(e)}")
|
||
traceback.print_exc()
|
||
time.sleep(5)
|
||
|
||
|
||
|
||
|
||
|