| import cv2 |
| import numpy as np |
|
|
| |
| FEATURE_TO_LANDMARK = { |
| 0: 13, |
| 1: 14, |
| 2: 25, |
| 3: 26, |
| 4: 11, |
| 5: 12, |
| 6: 23, |
| 7: 24, |
| 8: 15, |
| 9: 16 |
| } |
|
|
| FEATURE_LABELS = { |
| 0: "Tay trái", 1: "Tay phải", |
| 2: "Chân trái", 3: "Chân phải", |
| 4: "Vai trái", 5: "Vai phải", |
| 6: "Hông trái", 7: "Hông phải", |
| 8: "Bàn tay trái", 9: "Bàn tay phải" |
| } |
|
|
| def draw_error_highlight(frame, landmark, color=(0, 0, 255), radius=35, thickness=4): |
| """ |
| Draws a red circle around a faulty landmark. |
| """ |
| h, w, _ = frame.shape |
| cx, cy = int(landmark[0] * w), int(landmark[1] * h) |
| cv2.circle(frame, (cx, cy), radius, color, thickness) |
| cv2.circle(frame, (cx, cy), radius + 5, color, 1) |
|
|
| def overlay_skeleton(frame, landmarks, color=(0, 255, 0), thickness=2): |
| """ |
| Draws skeleton connections on the frame. |
| """ |
| h, w, _ = frame.shape |
| pose = landmarks.get("pose") |
| if not pose: return frame |
| |
| connections = [ |
| (11, 13), (13, 15), (12, 14), (14, 16), |
| (11, 12), (23, 24), (11, 23), (12, 24), |
| (23, 25), (25, 27), (24, 26), (26, 28) |
| ] |
| |
| for s, e in connections: |
| if s < len(pose) and e < len(pose): |
| p1 = (int(pose[s][0] * w), int(pose[s][1] * h)) |
| p2 = (int(pose[e][0] * w), int(pose[e][1] * h)) |
| cv2.line(frame, p1, p2, color, thickness) |
| return frame |
|
|
| def create_combined_overlay(user_frame, user_landmarks, frame_error=None, error_threshold=120.0): |
| """ |
| Vẽ các chỉ số sửa lỗi trực tiếp lên frame của sinh viên. |
| error_threshold=120.0: Ngưỡng cộng dồn sai lệch góc để hiển thị vòng tròn đỏ. |
| """ |
| output_frame = user_frame.copy() |
| |
| |
| output_frame = overlay_skeleton(output_frame, user_landmarks, color=(0, 255, 0), thickness=2) |
| |
| |
| if frame_error and user_landmarks.get("pose"): |
| pose = user_landmarks["pose"] |
| |
| |
| for i, diff in enumerate(frame_error.get("angles", [])): |
| if diff > error_threshold: |
| if i in FEATURE_TO_LANDMARK: |
| draw_error_highlight(output_frame, pose[FEATURE_TO_LANDMARK[i]]) |
| |
| |
| for i, diff in enumerate(frame_error.get("hands", [])): |
| if diff > 30.0: |
| if i+8 in FEATURE_TO_LANDMARK: |
| |
| draw_error_highlight(output_frame, pose[FEATURE_TO_LANDMARK[i+8]], color=(255, 128, 0)) |
| |
| return output_frame |
|
|
| def generate_result_video(user_video_path, user_landmarks_seq, path_mapping, errors, output_path): |
| """ |
| Renders the study video showing ONLY the student but with AI highlights. |
| """ |
| cap_user = cv2.VideoCapture(user_video_path) |
| fps = cap_user.get(cv2.CAP_PROP_FPS) or 30.0 |
| w = int(cap_user.get(cv2.CAP_PROP_FRAME_WIDTH)) |
| h = int(cap_user.get(cv2.CAP_PROP_FRAME_HEIGHT)) |
| |
| fourcc = cv2.VideoWriter_fourcc(*'vp80') |
| out = cv2.VideoWriter(output_path, fourcc, fps, (w, h)) |
| |
| curr_u = 0 |
| while cap_user.isOpened(): |
| ret, u_frame = cap_user.read() |
| if not ret: break |
| |
| |
| matches = [p_idx for p_idx, (u, r) in enumerate(path_mapping) if u == curr_u] |
| |
| if matches: |
| match_idx = matches[0] |
| user_lms = user_landmarks_seq[curr_u]["landmarks"] |
| frame_err = errors[match_idx] |
| |
| |
| combined = create_combined_overlay(u_frame, user_lms, frame_err) |
| out.write(combined) |
| else: |
| |
| out.write(u_frame) |
| |
| curr_u += 1 |
| |
| cap_user.release() |
| out.release() |
|
|