import cv2
import numpy as np
import math
from collections import deque
from ultralytics import YOLO
import time
import os
try:
from PIL import ImageFont, ImageDraw, Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
# 关键点索引定义
KEYPOINT_INDICES = {
"left_shoulder": 5,
"right_shoulder": 6,
"left_elbow": 7,
"right_elbow": 8,
"left_wrist": 9,
"right_wrist": 10,
"left_hip": 11,
"right_hip": 12,
"left_ear": 3,
"right_ear": 4
}
def draw_stability_bar(panel, x, stability, color):
"""绘制稳定性进度条"""
bar_width = 60
fill_width = int(bar_width * stability / 100)
cv2.rectangle(panel, (x, 20 - 10), (x + bar_width, 20 + 5), (100, 100, 100), -1)
cv2.rectangle(panel, (x, 20 - 10), (x + fill_width, 20 + 5), color, -1)
stability_text = f"{stability:.0f}%"
cv2.putText(panel, stability_text, (x + bar_width + 5, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
class DumbbellCurlAnalyzer:
def __init__(self, model_path='yolov8s-pose.pt', display_width=1280, display_height=720):
"""初始化哑铃弯举分析器"""
self.display_width = display_width
self.display_height = display_height
# 尝试加载模型
try:
print(f"正在加载模型: {model_path}")
self.model = YOLO(model_path)
test_img = np.zeros((640, 640, 3), dtype=np.uint8)
self.model.predict(test_img, verbose=False)
print("模型加载成功")
except Exception as e:
raise RuntimeError(f"模型加载失败: {str(e)}")
# 性能优化参数
self.skip_counter = 0
self.skip_interval = 2 # 每3帧处理1帧
self.last_results = None # 结果缓存
# 多人状态跟踪
self.max_persons = 3
self.person_states = [] # 动态创建状态
# 颜色映射
self.person_colors = [
(0, 255, 0), # 绿色
(0, 165, 255), # 橙色
(0, 0, 255) # 红色
]
# 角度阈值
self.min_angle = 0
self.max_angle = 150
# 代偿参数
self.compensation_threshold = 0.05 # 身高的5%
self.compensation_sensitivity = 0.6
# 帧率跟踪
self.prev_frame_time = 0
self.current_fps = 30
self.fps_smoother = deque(maxlen=5)
# 中文支持
self.PIL_AVAILABLE = PIL_AVAILABLE
self.DEFAULT_FONT_PATH = self._find_font_path()
# 初始化历史计数
self.counter_history = {}
def _find_font_path(self):
"""查找系统中可用的中文字体"""
possible_fonts = [
"C:/Windows/Fonts/simsun.ttc", # Windows 宋体
"C:/Windows/Fonts/simhei.ttf", # Windows 黑体
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # Linux 文泉驿微米黑
"/System/Library/Fonts/PingFang.ttc", # macOS 苹方
"/Library/Fonts/SimHei.ttf" # macOS 黑体
]
for font_path in possible_fonts:
if os.path.exists(font_path):
print(f"找到中文字体: {font_path}")
return font_path
print("警告: 未找到中文字体,中文可能无法正常显示")
return None
def calculate_angle(self, a, b, c):
"""计算三点夹角(B为顶点)"""
ba = [a[0] - b[0], a[1] - b[1]]
bc = [c[0] - b[0], c[1] - b[1]]
dot_product = ba[0] * bc[0] + ba[1] * bc[1]
magnitude_ba = math.sqrt(ba[0] ** 2 + ba[1] ** 2)
magnitude_bc = math.sqrt(bc[0] ** 2 + bc[1] ** 2)
if magnitude_ba * magnitude_bc == 0:
return 0
cosine = dot_product / (magnitude_ba * magnitude_bc)
cosine = max(-1.0, min(1.0, cosine))
return math.degrees(math.acos(cosine))
def detect_compensation(self, keypoints_dict, side, person_state):
"""检测肩部代偿动作"""
shoulder = f"{side}_shoulder"
ear = f"{side}_ear"
compensation_types = []
confidence = 0
# 身高估计
ref_distance = self.get_reference_distance(keypoints_dict)
if ref_distance < 10:
return compensation_types, confidence
# 肩部位移检测
if shoulder in keypoints_dict:
tracker = person_state["shoulder_trackers"][side]
current_pos = keypoints_dict[shoulder]
# 添加速度计算
avg_speed = 0
if len(tracker["path"]) > 0:
# 计算最近5帧的平均移动速度
speeds = []
for i in range(1, min(5, len(tracker["path"]))):
dx = tracker["path"][-i][0] - tracker["path"][-i - 1][0]
dy = tracker["path"][-i][1] - tracker["path"][-i - 1][1]
speed = math.sqrt(dx ** 2 + dy ** 2) / ref_distance
speeds.append(speed)
if speeds:
avg_speed = sum(speeds) / len(speeds)
# 速度阈值-小于此值视为静止
SPEED_THRESHOLD = 0.005 # 身高的0.5%
if tracker["previous"]:
dx = current_pos[0] - tracker["previous"][0]
dy = current_pos[1] - tracker["previous"][1]
# 相对位移计算
relative_dx = dx / ref_distance
relative_dy = dy / ref_distance
# 只有当速度超过阈值时才进行代偿检测
if avg_speed > SPEED_THRESHOLD:
if abs(relative_dx) > self.compensation_threshold or abs(relative_dy) > self.compensation_threshold:
compensation_types.append(f"shoulder_displacement_{side}")
confidence += 0.4
# 耸肩检测(相对位移dy为负表示向上)
if relative_dy < -self.compensation_threshold:
compensation_types.append(f"shoulder_elevation_{side}")
confidence += 0.3
# 更新代偿计数
if relative_dx > self.compensation_threshold or relative_dy < -self.compensation_threshold:
tracker["compensation_count"] = min(10, tracker["compensation_count"] + 1)
else:
tracker["compensation_count"] = max(0, tracker["compensation_count"] - 2)
else:
# 静止状态下减少代偿计数
tracker["compensation_count"] = max(0, tracker["compensation_count"] - 3)
# 更新历史位置
tracker["previous"] = current_pos
tracker["path"].append(current_pos)
else:
# 第一次检测到,初始化previous
tracker["previous"] = current_pos
tracker["path"].append(current_pos)
# 连续代偿增强置信度
if "shoulder_trackers" in person_state and side in person_state["shoulder_trackers"]:
tracker = person_state["shoulder_trackers"][side]
if tracker["compensation_count"] > 3:
confidence += min(0.3, tracker["compensation_count"] * 0.1)
# 肩耳相对位置检测-仅当有移动时才检测
if avg_speed > SPEED_THRESHOLD and shoulder in keypoints_dict and ear in keypoints_dict:
shoulder_y = keypoints_dict[shoulder][1]
ear_y = keypoints_dict[ear][1]
elevation_ratio = (ear_y - shoulder_y) / ref_distance
if elevation_ratio < 0.25:
compensation_types.append(f"shoulder_elevation_{side}")
confidence += max(0.3, (0.25 - elevation_ratio) * 2)
return compensation_types, min(1.0, confidence)
def get_reference_distance(self, keypoints_dict):
"""估计身高作为参考"""
if "left_shoulder" in keypoints_dict and "right_shoulder" in keypoints_dict:
left = keypoints_dict["left_shoulder"]
right = keypoints_dict["right_shoulder"]
shoulder_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2)
return shoulder_width * 4 # 肩宽×4估计身高
elif "left_hip" in keypoints_dict and "right_hip" in keypoints_dict:
left = keypoints_dict["left_hip"]
right = keypoints_dict["right_hip"]
hip_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2)
return hip_width * 3 # 髋宽×3估计身高
return 0
def analyze_motion_phase(self, side, person_state):
"""判断动作阶段(上举/下落/保持)"""
angles = list(person_state["history_angles"][side])
if len(angles) < 5:
return "UNKNOWN"
# 计算速度
velocity = np.mean(np.diff(angles[-5:])) if len(angles) >= 5 else 0
if velocity > 7:
return "LIFTING"
elif velocity < -7:
return "LOWERING"
else:
return "HOLDING"
def interpolate_point(self, previous_point, current_pos, max_distance=100):
"""关键点缺失时插值"""
if previous_point is None:
return current_pos
dx = current_pos[0] - previous_point[0]
dy = current_pos[1] - previous_point[1]
distance = math.sqrt(dx ** 2 + dy ** 2)
if distance > max_distance:
return current_pos
return previous_point
def get_or_create_person_state(self, center):
"""获取或创建人员状态"""
# 如果状态列表为空,直接创建第一个状态
if not self.person_states:
return self.create_new_person_state(center)
# 寻找最近的现有状态
min_dist = float('inf')
closest_idx = None
for i, state in enumerate(self.person_states):
if state["last_position"]:
dist = math.sqrt(
(center[0] - state["last_position"][0]) ** 2 +
(center[1] - state["last_position"][1]) ** 2
)
if dist < min_dist:
min_dist = dist
closest_idx = i
# 如果没有足够近的现有状态,创建新状态
if min_dist > 100 or closest_idx is None:
if len(self.person_states) < self.max_persons:
return self.create_new_person_state(center)
else:
# 已满,返回最旧的状态
return self.person_states[0], 0
# 更新最近状态的位置
self.person_states[closest_idx]["last_position"] = center
return self.person_states[closest_idx], closest_idx
def create_new_person_state(self, center):
"""创建新的人员状态"""
new_state = {
"history_angles": {
"left": deque(maxlen=15),
"right": deque(maxlen=15)
},
"shoulder_trackers": {
"left": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0},
"right": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0}
},
"prev_keypoints": {
"left": {"shoulder": None, "elbow": None, "wrist": None},
"right": {"shoulder": None, "elbow": None, "wrist": None}
},
"missing_frames": {
"left": {"shoulder": 0, "elbow": 0, "wrist": 0},
"right": {"shoulder": 0, "elbow": 0, "wrist": 0}
},
"counter": {"left": 0, "right": 0},
"counter_state": {"left": "down", "right": "down"},
"last_position": center
}
self.person_states.append(new_state)
return new_state, len(self.person_states) - 1
def analyze_frame(self, frame):
"""分析单帧图像"""
# 帧率计算
current_time = time.time()
if self.prev_frame_time > 0:
self.current_fps = 1 / (current_time - self.prev_frame_time)
self.prev_frame_time = current_time
# 平滑帧率
self.fps_smoother.append(self.current_fps)
if len(self.fps_smoother) > 0:
smoothed_fps = sum(self.fps_smoother) / len(self.fps_smoother)
else:
smoothed_fps = self.current_fps
# 跳帧处理
self.skip_counter = (self.skip_counter + 1) % (self.skip_interval + 1)
if self.skip_counter != 0 and self.last_results is not None:
return self.last_results
# 调整帧大小以匹配显示尺寸
frame = cv2.resize(frame, (self.display_width, self.display_height))
# 姿态估计
results = self.model(frame, verbose=False)
# 结果初始化
analysis_results = {
"fps": smoothed_fps,
"persons": [] # 存储每个人的结果
}
try:
# 动态置信度阈值
conf_threshold = max(0.2, min(0.7, 0.5 * (smoothed_fps / 30)))
if len(results) == 0 or results[0].keypoints is None:
return analysis_results, frame
boxes = results[0].boxes.xyxy.cpu().numpy()
kpts = results[0].keypoints.data.cpu().numpy()
if len(boxes) == 0:
return analysis_results, frame
# 根据面积排序
areas = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
sorted_idxs = np.argsort(areas)[::-1][:self.max_persons] # 取面积最大的三个
# 处理每个人
for idx in sorted_idxs:
kpts_data = kpts[idx]
box = boxes[idx]
center = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2)
# 获取或创建对应的状态
person_state, person_id = self.get_or_create_person_state(center)
# 提取关键点
keypoints_dict = {}
for part, idx_kpt in KEYPOINT_INDICES.items():
if idx_kpt < len(kpts_data):
x, y, conf = kpts_data[idx_kpt]
if conf > conf_threshold:
keypoints_dict[part] = (int(x), int(y))
side, point = part.split('_', 1)
if point in person_state["missing_frames"][side]:
if conf > conf_threshold:
person_state["missing_frames"][side][point] = 0
else:
person_state["missing_frames"][side][point] += 1
# 关键点插值
for side in ["left", "right"]:
for point in ["shoulder", "elbow", "wrist"]:
key_name = f"{side}_{point}"
if key_name not in keypoints_dict and person_state["prev_keypoints"][side][point] is not None:
if person_state["missing_frames"][side][point] < 15: # 最大插值帧数
keypoints_dict[key_name] = self.interpolate_point(
person_state["prev_keypoints"][side][point],
person_state["prev_keypoints"][side][point]
)
# 更新历史关键点
for side in ["left", "right"]:
for point in ["shoulder", "elbow", "wrist"]:
key_name = f"{side}_{point}"
prev_key = person_state["prev_keypoints"][side][point]
if key_name in keypoints_dict:
person_state["prev_keypoints"][side][point] = keypoints_dict[key_name]
else:
person_state["prev_keypoints"][side][point] = prev_key
# 初始化个人结果
person_result = {
"id": person_id,
"color": self.person_colors[person_id % len(self.person_colors)],
"left_angle": None,
"right_angle": None,
"left_feedback": "",
"right_feedback": "",
"left_compensation": [],
"right_compensation": [],
"left_compensation_confidence": 0,
"right_compensation_confidence": 0,
"left_phase": "UNKNOWN",
"right_phase": "UNKNOWN",
"left_count": person_state["counter"]["left"],
"right_count": person_state["counter"]["right"],
"box": box,
"keypoints": keypoints_dict
}
# 更新历史计数
person_key = f"person_{person_id}"
if person_key not in self.counter_history:
self.counter_history[person_key] = []
self.counter_history[person_key].append(
person_state["counter"]["left"] + person_state["counter"]["right"])
# 分析左右手臂
for side in ["left", "right"]:
shoulder = f"{side}_shoulder"
elbow = f"{side}_elbow"
wrist = f"{side}_wrist"
if shoulder in keypoints_dict and elbow in keypoints_dict and wrist in keypoints_dict:
# 计算角度
angle = self.calculate_angle(
keypoints_dict[shoulder],
keypoints_dict[elbow],
keypoints_dict[wrist]
)
# 添加到历史
person_state["history_angles"][side].append(angle)
person_result[f"{side}_angle"] = angle
# 动作阶段
phase = self.analyze_motion_phase(side, person_state)
person_result[f"{side}_phase"] = phase
# 反馈信息
feedback = ""
if angle < self.min_angle:
feedback = "手臂过度伸展!"
elif angle > self.max_angle:
feedback = "弯曲角度过大!"
else:
feedback = "动作规范"
feedback += f"|{phase}"
# 代偿检测
compensations, confidence = self.detect_compensation(keypoints_dict, side, person_state)
person_result[f"{side}_compensation"] = compensations
person_result[f"{side}_compensation_confidence"] = confidence
# 代偿反馈
if confidence > self.compensation_sensitivity:
if f"shoulder_displacement_{side}" in compensations:
feedback += "|肩部不稳定!"
if f"shoulder_elevation_{side}" in compensations:
feedback += "|避免耸肩!"
person_result[f"{side}_feedback"] = feedback
# 动作计数
if angle < 40 and person_state["counter_state"][side] == "down":
person_state["counter_state"][side] = "up"
elif angle > 120 and person_state["counter_state"][side] == "up":
person_state["counter"][side] += 1
person_state["counter_state"][side] = "down"
person_result[f"{side}_count"] = person_state["counter"][side]
else:
person_result[f"{side}_feedback"] = f"{side}关键点未检测到"
analysis_results["persons"].append(person_result)
# 可视化
viz_frame = self.visualize_feedback(frame, analysis_results)
except Exception as e:
print(f"分析错误: {str(e)}")
viz_frame = frame
analysis_results["persons"] = []
self.last_results = (analysis_results, viz_frame)
return analysis_results, viz_frame
def visualize_feedback(self, frame, analysis_results):
"""可视化分析结果"""
viz_frame = frame.copy()
height, width = frame.shape[:2]
# 绘制人物信息(骨架、关键点等)
for person in analysis_results["persons"]:
color = person["color"]
# 绘制边界框
box = person["box"]
cv2.rectangle(viz_frame, (int(box[0]), int(box[1])),
(int(box[2]), int(box[3])), color, 2)
# 绘制ID
cv2.putText(viz_frame, f"ID:{person['id']}",
(int(box[0]), int(box[1]) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
# 显示计数信息
count_text = f"L:{person['left_count']} R:{person['right_count']}"
cv2.putText(viz_frame, count_text,
(int(box[0]), int(box[1]) + 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
# 关键点可视化
for person in analysis_results["persons"]:
color = person["color"]
keypoints_dict = person.get("keypoints", {})
# 获取对应的person_state
if person["id"] < len(self.person_states):
person_state = self.person_states[person["id"]]
else:
continue # 跳过无法找到状态的人
# 绘制身体骨架 (简化版)
skeleton_pairs = [
("left_shoulder", "left_elbow"), ("left_elbow", "left_wrist"),
("right_shoulder", "right_elbow"), ("right_elbow", "right_wrist"),
("left_shoulder", "right_shoulder"),
("left_shoulder", "left_hip"), ("right_shoulder", "right_hip"),
("left_hip", "right_hip")
]
for start, end in skeleton_pairs:
if start in keypoints_dict and end in keypoints_dict:
cv2.line(viz_frame,
keypoints_dict[start],
keypoints_dict[end],
color, 2)
# 绘制关节点
for point in keypoints_dict.values():
cv2.circle(viz_frame, point, 5, color, -1)
# 绘制手臂角度
for side in ["left", "right"]:
elbow_key = f"{side}_elbow"
if person[f"{side}_angle"] and elbow_key in keypoints_dict:
angle = person[f"{side}_angle"]
position = (keypoints_dict[elbow_key][0] + 10,
keypoints_dict[elbow_key][1] - 10)
cv2.putText(viz_frame, f"{angle:.0f}°", position,
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
# 实时反馈信息板
feedback_height = 120
feedback_panel = np.zeros((feedback_height, width, 3), dtype=np.uint8)
feedback_panel[:] = (40, 40, 60)
y_offset = 20
for person in analysis_results["persons"]:
color = person["color"]
id_text = f"ID {person['id']}:"
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
feedback_panel = self.put_chinese_text(feedback_panel, id_text, (20, y_offset), color, 22)
else:
cv2.putText(feedback_panel, id_text, (20, y_offset),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
# 左右手臂反馈信息
for side in ["left", "right"]:
feedback_text = f"{side}臂: {person[f'{side}_feedback']}"
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
feedback_panel = self.put_chinese_text(feedback_panel, feedback_text, (100, y_offset), color, 18)
else:
cv2.putText(feedback_panel, feedback_text, (100, y_offset),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
y_offset += 25
# 代偿信息
compensations = person["left_compensation"] + person["right_compensation"]
if compensations:
compensation_text = "代偿: " + ", ".join(compensations)
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
feedback_panel = self.put_chinese_text(feedback_panel, compensation_text, (100, y_offset),
(0, 0, 255), 18)
else:
cv2.putText(feedback_panel, compensation_text, (100, y_offset),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
y_offset += 25
y_offset += 10 # 人员间间隔
# 叠加反馈面板到右侧
feedback_width = min(400, width // 3)
viz_frame[0:feedback_height, -feedback_width:] = cv2.addWeighted(
viz_frame[0:feedback_height, -feedback_width:], 0.2,
feedback_panel[:, -feedback_width:], 0.8, 0
)
# 绘制固定的数据框(顶部面板)
self.draw_fixed_data_panel(viz_frame, analysis_results)
return viz_frame
def draw_fixed_data_panel(self, frame, analysis_results):
"""绘制固定在顶部的数据面板"""
panel_height = 120 # 增加面板高度以容纳更多内容
panel_width = frame.shape[1]
panel = np.zeros((panel_height, panel_width, 3), dtype=np.uint8)
panel[:] = (40, 40, 60) # 深蓝灰色背景
# 标题
title = "多人哑铃弯举分析"
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, title, (20, 30), (0, 200, 255), 28)
else:
cv2.putText(panel, title, (20, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 200, 255), 2)
# 列标题和数据
headers = ["ID", "左计数", "右计数", "左肩稳定", "右肩稳定"]
col_positions = [100, 200, 300, 420, 540]
header_y = 60 # 标题行位置
data_y = 90 # 数据行位置
# 绘制列标题
for i, header in enumerate(headers):
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, header, (col_positions[i], header_y), (0, 255, 255), 20)
else:
cv2.putText(panel, header, (col_positions[i], header_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1)
# 绘制每个人的数据
for i, person in enumerate(analysis_results["persons"]):
if i >= 3: # 只显示前3个人
break
color = person["color"]
x_offset = i * 200 # 为多个人物设置水平偏移
# ID
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, str(person["id"]), (col_positions[0] + x_offset, data_y), color,
20)
else:
cv2.putText(panel, str(person["id"]), (col_positions[0] + x_offset, data_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1)
# 左计数
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y),
color, 20)
else:
cv2.putText(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1)
# 右计数
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y),
color, 20)
else:
cv2.putText(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1)
# 左肩稳定
left_stability = max(0, min(100, 100 - person["left_compensation_confidence"] * 100))
draw_stability_bar(panel, col_positions[3] + x_offset, left_stability, color)
# 右肩稳定
right_stability = max(0, min(100, 100 - person["right_compensation_confidence"] * 100))
draw_stability_bar(panel, col_positions[4] + x_offset, right_stability, color)
# 添加帧率信息
fps_text = f"FPS: {analysis_results['fps']:.1f}"
cv2.putText(panel, fps_text,
(panel_width - 150, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1)
# 添加操作提示
hint_text = "ESC退出 | F全屏 | S截图 | Q切换质量"
if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH:
panel = self.put_chinese_text(panel, hint_text, (panel_width - 350, panel_height - 15), (200, 200, 200), 18)
else:
cv2.putText(panel, hint_text, (panel_width - 350, panel_height - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
# 叠加面板到顶部
frame[0:panel_height, 0:panel_width] = cv2.addWeighted(
frame[0:panel_height, 0:panel_width], 0.3,
panel, 0.7, 0
)
def put_chinese_text(self, img, text, pos, color, font_size):
"""在图像上绘制中文文本"""
if not self.PIL_AVAILABLE or self.DEFAULT_FONT_PATH is None:
# 如果无法使用PIL或未找到字体,尝试使用默认字体
cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2)
return img
try:
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
font = ImageFont.truetype(self.DEFAULT_FONT_PATH, font_size)
draw.text(pos, text, font=font, fill=tuple(reversed(color)))
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
except Exception as e:
print(f"绘制中文失败: {e}")
# 回退到默认字体
cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2)
return img
def main():
"""主函数"""
try:
# 模型选择
model_options = {
's': 'yolov8s-pose.pt', # 小模型,性能优先
'm': 'yolov8m-pose.pt', # 中等模型,平衡速度和精度
'l': 'yolov8l-pose.pt' # 大模型,精度优先
}
print("请选择模型:")
print("1. 小模型 (yolov8s-pose, 快速但精度较低)")
print("2. 中模型 (yolov8m-pose, 平衡速度和精度)")
print("3. 大模型 (yolov8l-pose, 高精度但较慢)")
model_choice = input("请输入数字 (1-3, 默认2): ")
model_key = {
'1': 's',
'2': 'm',
'3': 'l'
}.get(model_choice, 'm')
model_path = model_options[model_key]
print(f"使用模型: {model_path}")
# 创建分析器实例,指定显示尺寸
display_width = 1280
display_height = 720
analyzer = DumbbellCurlAnalyzer(model_path, display_width, display_height)
# 打开摄像头
print("正在打开摄像头...")
cap = cv2.VideoCapture(0)
# 设置摄像头分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, display_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, display_height)
# 检查摄像头是否成功打开
if not cap.isOpened():
print("无法打开摄像头")
return
# 设置窗口属性
cv2.namedWindow("哑铃弯举分析", cv2.WINDOW_NORMAL)
cv2.resizeWindow("哑铃弯举分析", display_width, display_height)
# 全屏标志
is_fullscreen = False
# 模型质量级别 (影响处理速度)
quality_level = 1 # 1=高(完整处理), 2=中(跳帧处理), 3=低(低分辨率)
# 主循环
print("开始分析,请进行哑铃弯举动作...")
print("操作提示:")
print(" ESC: 退出程序")
print(" F: 切换全屏显示")
print(" S: 保存当前画面截图")
print(" Q: 切换处理质量级别")
while True:
# 读取一帧
ret, frame = cap.read()
# 检查是否成功读取帧
if not ret:
print("无法获取帧")
break
# 根据质量级别调整处理方式
if quality_level == 3:
# 低质量: 降低分辨率
frame = cv2.resize(frame, (640, 360))
elif quality_level == 2:
# 中等质量: 增加跳帧间隔
analyzer.skip_interval = 3
else:
# 高质量: 正常处理
analyzer.skip_interval = 2
# 分析帧
analysis_results, viz_frame = analyzer.analyze_frame(frame)
# 显示结果
cv2.imshow("哑铃弯举分析", viz_frame)
# 处理按键事件
key = cv2.waitKey(1)
# ESC键退出
if key == 27:
break
# F键切换全屏
if key == ord('f') or key == ord('F'):
is_fullscreen = not is_fullscreen
if is_fullscreen:
cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
else:
cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL)
# S键保存当前帧
if key == ord('s') or key == ord('S'):
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = f"dumbbell_curl_{timestamp}.png"
cv2.imwrite(filename, viz_frame)
print(f"已保存截图: {filename}")
# Q键切换质量级别
if key == ord('q') or key == ord('Q'):
quality_level = (quality_level % 3) + 1
quality_names = ["高", "中", "低"]
print(f"已切换处理质量: {quality_names[quality_level - 1]}")
# 释放资源
cap.release()
cv2.destroyAllWindows()
print("程序已退出")
except Exception as e:
print(f"程序运行出错: {str(e)}")
if __name__ == "__main__":
main()