""" Real-time Fall Detection Visualization Module 이 모듈은 실시간 낙상 감지 파이프라인의 시각화 기능을 제공합니다. COCO 17 keypoints 스켈레톤 렌더링, 예측 결과 오버레이, 성능 메트릭 표시 등을 포함합니다. 주요 기능: - COCO 17 keypoints 스켈레톤 렌더링 - Bounding box 렌더링 - Fall/Non-Fall 라벨 + 신뢰도 표시 - FPS/Latency 실시간 표시 - 색상 코딩 (Fall: 빨강, Non-Fall: 초록) 최적화 (Issue #56): - NumPy 벡터화로 cv2.circle()/cv2.line() 루프 대체 - morphological dilation으로 keypoint 원 그리기 (30배 속도 향상) - cv2.polylines()로 skeleton 선 일괄 그리기 - 주요 keypoint만 표시 옵션 (--viz-keypoints major) - 출력 해상도 조절 옵션 (--viz-scale 0.5) Reference: - COCO Keypoints: https://cocodataset.org/#keypoints-2017 """ import cv2 import numpy as np from typing import Tuple, Optional, List, Literal # COCO 17 keypoints 인덱스 COCO_KEYPOINT_NAMES = [ 'nose', # 0 'left_eye', # 1 'right_eye', # 2 'left_ear', # 3 'right_ear', # 4 '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_knee', # 13 'right_knee', # 14 'left_ankle', # 15 'right_ankle', # 16 ] # COCO 스켈레톤 연결 정의 (뼈대 구조) COCO_SKELETON = [ # 얼굴 (0, 1), # nose -> left_eye (0, 2), # nose -> right_eye (1, 3), # left_eye -> left_ear (2, 4), # right_eye -> right_ear # 상체 (0, 5), # nose -> left_shoulder (0, 6), # nose -> right_shoulder (5, 6), # left_shoulder <-> right_shoulder # 왼팔 (5, 7), # left_shoulder -> left_elbow (7, 9), # left_elbow -> left_wrist # 오른팔 (6, 8), # right_shoulder -> right_elbow (8, 10), # right_elbow -> right_wrist # 몸통 (5, 11), # left_shoulder -> left_hip (6, 12), # right_shoulder -> right_hip (11, 12), # left_hip <-> right_hip # 왼다리 (11, 13), # left_hip -> left_knee (13, 15), # left_knee -> left_ankle # 오른다리 (12, 14), # right_hip -> right_knee (14, 16), # right_knee -> right_ankle ] # 신체 부위별 색상 정의 (BGR 포맷) BODY_PART_COLORS = { 'face': (0, 255, 255), # 노란색 'left_arm': (255, 0, 180), # 분홍색 'right_arm': (0, 165, 255), # 오렌지색 'torso': (255, 150, 0), # 파란색 'left_leg': (0, 0, 255), # 빨간색 'right_leg': (180, 0, 255), # 보라색 } # 각 스켈레톤 연결에 대한 신체 부위 매핑 SKELETON_PART_MAPPING = [ 'face', # (0, 1) nose -> left_eye 'face', # (0, 2) nose -> right_eye 'face', # (1, 3) left_eye -> left_ear 'face', # (2, 4) right_eye -> right_ear 'face', # (0, 5) nose -> left_shoulder 'face', # (0, 6) nose -> right_shoulder 'torso', # (5, 6) left_shoulder <-> right_shoulder 'left_arm', # (5, 7) left_shoulder -> left_elbow 'left_arm', # (7, 9) left_elbow -> left_wrist 'right_arm', # (6, 8) right_shoulder -> right_elbow 'right_arm', # (8, 10) right_elbow -> right_wrist 'torso', # (5, 11) left_shoulder -> left_hip 'torso', # (6, 12) right_shoulder -> right_hip 'torso', # (11, 12) left_hip <-> right_hip 'left_leg', # (11, 13) left_hip -> left_knee 'left_leg', # (13, 15) left_knee -> left_ankle 'right_leg', # (12, 14) right_hip -> right_knee 'right_leg', # (14, 16) right_knee -> right_ankle ] # 예측 결과 색상 정의 PREDICTION_COLORS = { 'Fall': (0, 0, 255), # 빨강 'Non-Fall': (0, 255, 0), # 초록 } # 주요 keypoint 인덱스 (9개: 코, 어깨, 엉덩이, 무릎, 발목) # 낙상 감지에 중요한 신체 부위만 선택 MAJOR_KEYPOINT_INDICES = [ 0, # nose - 머리 위치 5, # left_shoulder 6, # right_shoulder 11, # left_hip 12, # right_hip 13, # left_knee 14, # right_knee 15, # left_ankle 16, # right_ankle ] # 주요 keypoint용 skeleton 연결 (8개 연결) MAJOR_SKELETON = [ (5, 6), # left_shoulder <-> right_shoulder (5, 11), # left_shoulder -> left_hip (6, 12), # right_shoulder -> right_hip (11, 12), # left_hip <-> right_hip (11, 13), # left_hip -> left_knee (12, 14), # right_hip -> right_knee (13, 15), # left_knee -> left_ankle (14, 16), # right_knee -> right_ankle ] # 주요 skeleton 신체 부위 매핑 MAJOR_SKELETON_PART_MAPPING = [ 'torso', # (5, 6) 'torso', # (5, 11) 'torso', # (6, 12) 'torso', # (11, 12) 'left_leg', # (11, 13) 'right_leg', # (12, 14) 'left_leg', # (13, 15) 'right_leg', # (14, 16) ] # Morphological dilation용 커널 캐시 (동일 크기 재사용) _KERNEL_CACHE = {} def draw_skeleton( frame: np.ndarray, keypoints: np.ndarray, color: Tuple[int, int, int] = (0, 255, 0), thickness: int = 2, conf_threshold: float = 0.5, keypoint_radius: int = 4, use_body_part_colors: bool = True ) -> np.ndarray: """ COCO 17 keypoints 스켈레톤 렌더링 Args: frame: OpenCV 이미지 (H, W, 3) BGR 포맷 keypoints: (17, 3) numpy array - (x, y, conf) color: BGR 색상 (use_body_part_colors=False일 때 사용) thickness: 선 두께 conf_threshold: 최소 신뢰도 임계값 (이 값 이하는 그리지 않음) keypoint_radius: 키포인트 원의 반지름 use_body_part_colors: True면 신체 부위별 색상 사용, False면 단일 색상 사용 Returns: frame: 스켈레톤이 렌더링된 이미지 """ if keypoints.shape != (17, 3): raise ValueError(f"Expected keypoints shape (17, 3), got {keypoints.shape}") frame = frame.copy() # 1. 스켈레톤 연결선 그리기 for i, (start_idx, end_idx) in enumerate(COCO_SKELETON): x1, y1, conf1 = keypoints[start_idx] x2, y2, conf2 = keypoints[end_idx] # 두 키포인트 모두 신뢰도 임계값을 넘어야 선을 그림 if conf1 > conf_threshold and conf2 > conf_threshold: # 신체 부위별 색상 또는 단일 색상 선택 if use_body_part_colors: part_name = SKELETON_PART_MAPPING[i] line_color = BODY_PART_COLORS[part_name] else: line_color = color # 선 그리기 pt1 = (int(x1), int(y1)) pt2 = (int(x2), int(y2)) cv2.line(frame, pt1, pt2, line_color, thickness, cv2.LINE_AA) # 2. 키포인트 원 그리기 (선 위에 그려서 더 눈에 띄게) for i, (x, y, conf) in enumerate(keypoints): if conf > conf_threshold: center = (int(x), int(y)) # 외곽 흰색 테두리 cv2.circle(frame, center, keypoint_radius + 2, (255, 255, 255), -1, cv2.LINE_AA) # 내부 색상 원 (밝은 하늘색) cv2.circle(frame, center, keypoint_radius, (255, 200, 0), -1, cv2.LINE_AA) return frame def _get_ellipse_kernel(radius: int) -> np.ndarray: """ 캐시된 ellipse 커널 반환 (morphological dilation용) Args: radius: 커널 반지름 Returns: ellipse 커널 """ if radius not in _KERNEL_CACHE: kernel_size = radius * 2 + 1 _KERNEL_CACHE[radius] = cv2.getStructuringElement( cv2.MORPH_ELLIPSE, (kernel_size, kernel_size) ) return _KERNEL_CACHE[radius] def draw_skeleton_vectorized( frame: np.ndarray, keypoints: np.ndarray, conf_threshold: float = 0.5, keypoint_radius: int = 4, thickness: int = 2, keypoint_mode: Literal['all', 'major'] = 'all', use_body_part_colors: bool = True, keypoint_color: Tuple[int, int, int] = (255, 200, 0), border_color: Tuple[int, int, int] = (255, 255, 255) ) -> np.ndarray: """ 최적화된 skeleton 렌더링 최적화 전략: - cv2.polylines()로 skeleton 선 일괄 처리 (색상별 그룹화) - 주요 keypoint만 표시 옵션으로 그리기 횟수 감소 (17개 -> 9개) - Anti-aliasing 비활성화 옵션 (cv2.LINE_AA -> cv2.LINE_8) Note: morphological dilation은 4K 해상도에서 전체 이미지 마스크 생성으로 오히려 느려지므로, keypoint 원은 기존 cv2.circle() 유지 Args: frame: OpenCV 이미지 (H, W, 3) BGR 포맷 keypoints: (17, 3) numpy array - (x, y, conf) conf_threshold: 최소 신뢰도 임계값 (이 값 이하는 그리지 않음) keypoint_radius: 키포인트 원의 반지름 thickness: skeleton 선 두께 keypoint_mode: 'all'=전체 17개, 'major'=주요 9개만 표시 use_body_part_colors: True면 신체 부위별 색상 사용 keypoint_color: keypoint 원 색상 (BGR) border_color: keypoint 테두리 색상 (BGR) Returns: frame: 스켈레톤이 렌더링된 이미지 """ if keypoints.shape != (17, 3): raise ValueError(f"Expected keypoints shape (17, 3), got {keypoints.shape}") result = frame.copy() # keypoint 모드에 따른 인덱스/skeleton 선택 if keypoint_mode == 'major': kpt_indices = MAJOR_KEYPOINT_INDICES skeleton = MAJOR_SKELETON skeleton_parts = MAJOR_SKELETON_PART_MAPPING else: kpt_indices = list(range(17)) skeleton = COCO_SKELETON skeleton_parts = SKELETON_PART_MAPPING # 유효한 keypoints 필터링 (confidence > threshold) valid_mask = keypoints[:, 2] > conf_threshold if keypoint_mode == 'major': # 주요 keypoint 인덱스만 고려 major_mask = np.zeros(17, dtype=bool) major_mask[kpt_indices] = True valid_mask = valid_mask & major_mask valid_indices = np.where(valid_mask)[0] if len(valid_indices) == 0: return result # 1. Skeleton 선 그리기 (cv2.polylines 사용 - 배치 처리) if use_body_part_colors: # 색상별로 선 그룹화 color_groups = {} for i, (start_idx, end_idx) in enumerate(skeleton): if valid_mask[start_idx] and valid_mask[end_idx]: part_name = skeleton_parts[i] color = BODY_PART_COLORS[part_name] if color not in color_groups: color_groups[color] = [] pt1 = (int(keypoints[start_idx, 0]), int(keypoints[start_idx, 1])) pt2 = (int(keypoints[end_idx, 0]), int(keypoints[end_idx, 1])) color_groups[color].append(np.array([pt1, pt2], dtype=np.int32)) # 색상별로 일괄 그리기 for color, lines in color_groups.items(): if lines: cv2.polylines(result, lines, isClosed=False, color=color, thickness=thickness, lineType=cv2.LINE_AA) else: # 단일 색상으로 모든 선 그리기 lines = [] for start_idx, end_idx in skeleton: if valid_mask[start_idx] and valid_mask[end_idx]: pt1 = (int(keypoints[start_idx, 0]), int(keypoints[start_idx, 1])) pt2 = (int(keypoints[end_idx, 0]), int(keypoints[end_idx, 1])) lines.append(np.array([pt1, pt2], dtype=np.int32)) if lines: cv2.polylines(result, lines, isClosed=False, color=(255, 255, 255), thickness=thickness, lineType=cv2.LINE_AA) # 2. Keypoint 원 그리기 (cv2.circle 사용 - 개수가 적어 루프가 효율적) for idx in valid_indices: x, y = int(keypoints[idx, 0]), int(keypoints[idx, 1]) center = (x, y) # 외곽 테두리 cv2.circle(result, center, keypoint_radius + 2, border_color, -1, cv2.LINE_AA) # 내부 색상 원 cv2.circle(result, center, keypoint_radius, keypoint_color, -1, cv2.LINE_AA) return result def draw_prediction( frame: np.ndarray, prediction: str, confidence: float, bbox: Optional[Tuple[int, int, int, int]] = None, fps: Optional[float] = None, latency: Optional[float] = None, position: str = 'top-left' ) -> np.ndarray: """ 예측 결과 오버레이 렌더링 Args: frame: OpenCV 이미지 prediction: 'Fall' 또는 'Non-Fall' confidence: 신뢰도 (0.0-1.0) bbox: (x1, y1, x2, y2) 바운딩 박스 (선택) fps: FPS 값 (선택) latency: Latency (ms) (선택) position: 텍스트 위치 ('top-left', 'top-right', 'bottom-left', 'bottom-right') Returns: frame: 렌더링된 이미지 """ frame = frame.copy() h, w = frame.shape[:2] # 1. 바운딩 박스 그리기 (있을 경우) if bbox is not None: x1, y1, x2, y2 = bbox pred_color = PREDICTION_COLORS.get(prediction, (255, 255, 255)) # 박스 두께는 Fall일 때 더 두껍게 box_thickness = 4 if prediction == 'Fall' else 2 cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), pred_color, box_thickness) # 2. 예측 라벨 + 신뢰도 텍스트 준비 if confidence is not None: pred_text = f"{prediction}: {confidence:.2%}" else: pred_text = f"{prediction}" pred_color = PREDICTION_COLORS.get(prediction, (255, 255, 255)) # 3. FPS/Latency 텍스트 준비 (있을 경우) info_texts = [] if fps is not None: info_texts.append(f"FPS: {fps:.1f}") if latency is not None: info_texts.append(f"Latency: {latency:.1f}ms") # 4. 텍스트 위치 계산 font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.8 font_thickness = 2 padding = 10 line_height = 35 # 예측 텍스트 크기 (pred_w, pred_h), _ = cv2.getTextSize(pred_text, font, font_scale, font_thickness) # 위치별 좌표 계산 if position == 'top-left': pred_x, pred_y = padding, padding + pred_h elif position == 'top-right': pred_x, pred_y = w - pred_w - padding, padding + pred_h elif position == 'bottom-left': pred_x, pred_y = padding, h - padding - (len(info_texts) * line_height) - 10 elif position == 'bottom-right': pred_x, pred_y = w - pred_w - padding, h - padding - (len(info_texts) * line_height) - 10 else: raise ValueError(f"Unknown position: {position}") # 5. 배경 박스 그리기 (가독성 향상) bg_x1 = pred_x - 5 bg_y1 = pred_y - pred_h - 5 bg_x2 = pred_x + pred_w + 5 bg_y2 = pred_y + 5 # 반투명 검은 배경 overlay = frame.copy() cv2.rectangle(overlay, (bg_x1, bg_y1), (bg_x2, bg_y2), (0, 0, 0), -1) cv2.addWeighted(overlay, 0.6, frame, 0.4, 0, frame) # 6. 예측 텍스트 그리기 cv2.putText(frame, pred_text, (pred_x, pred_y), font, font_scale, pred_color, font_thickness, cv2.LINE_AA) # 7. FPS/Latency 정보 그리기 (있을 경우) if info_texts: info_y = pred_y + line_height for info_text in info_texts: (info_w, info_h), _ = cv2.getTextSize(info_text, font, font_scale, font_thickness) # 배경 박스 bg_x1 = pred_x - 5 bg_y1 = info_y - info_h - 5 bg_x2 = pred_x + info_w + 5 bg_y2 = info_y + 5 overlay = frame.copy() cv2.rectangle(overlay, (bg_x1, bg_y1), (bg_x2, bg_y2), (0, 0, 0), -1) cv2.addWeighted(overlay, 0.6, frame, 0.4, 0, frame) # 텍스트 (흰색) cv2.putText(frame, info_text, (pred_x, info_y), font, font_scale, (255, 255, 255), font_thickness, cv2.LINE_AA) info_y += line_height return frame def create_info_panel( frame_width: int, frame_height: int, fps: float, latency: float, prediction: str, confidence: float, panel_height: int = 80, position: str = 'top' ) -> np.ndarray: """ 정보 패널 생성 (상단 또는 하단 오버레이) Args: frame_width: 프레임 너비 frame_height: 프레임 높이 fps: FPS 값 latency: Latency (ms) prediction: 'Fall' 또는 'Non-Fall' confidence: 신뢰도 (0.0-1.0) panel_height: 패널 높이 position: 패널 위치 ('top' 또는 'bottom') Returns: panel: 정보 패널 이미지 (panel_height, frame_width, 3) """ # 패널 생성 (검은 배경) panel = np.zeros((panel_height, frame_width, 3), dtype=np.uint8) # 예측 결과 색상 pred_color = PREDICTION_COLORS.get(prediction, (255, 255, 255)) # 폰트 설정 font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.7 font_thickness = 2 # 텍스트 준비 pred_text = f"{prediction}: {confidence:.1%}" if confidence is not None else f"{prediction}" texts = [ (f"FPS: {fps:.1f}", (255, 255, 255)), (f"Latency: {latency:.1f}ms", (255, 255, 255)), (pred_text, pred_color), ] # 텍스트 균등 배치 section_width = frame_width // len(texts) y_pos = panel_height // 2 + 10 for i, (text, color) in enumerate(texts): # 텍스트 크기 계산 (text_w, text_h), _ = cv2.getTextSize(text, font, font_scale, font_thickness) # 중앙 정렬 x_pos = (i * section_width) + (section_width - text_w) // 2 # 텍스트 그리기 cv2.putText(panel, text, (x_pos, y_pos), font, font_scale, color, font_thickness, cv2.LINE_AA) # 구분선 그리기 for i in range(1, len(texts)): x_pos = i * section_width cv2.line(panel, (x_pos, 10), (x_pos, panel_height - 10), (80, 80, 80), 1) return panel def add_info_panel_to_frame( frame: np.ndarray, fps: float, latency: float, prediction: str, confidence: float, panel_height: int = 80, position: str = 'top' ) -> np.ndarray: """ 프레임에 정보 패널 추가 Args: frame: 원본 프레임 fps: FPS 값 latency: Latency (ms) prediction: 'Fall' 또는 'Non-Fall' confidence: 신뢰도 panel_height: 패널 높이 position: 패널 위치 ('top' 또는 'bottom') Returns: result: 패널이 추가된 프레임 """ h, w = frame.shape[:2] # 정보 패널 생성 panel = create_info_panel(w, h, fps, latency, prediction, confidence, panel_height, position) # 패널 위치에 따라 결합 if position == 'top': result = np.vstack([panel, frame]) elif position == 'bottom': result = np.vstack([frame, panel]) else: raise ValueError(f"Unknown position: {position}. Use 'top' or 'bottom'.") return result def draw_fall_alert_overlay( frame: np.ndarray, alert_text: str = "FALL DETECTED!", flash: bool = True ) -> np.ndarray: """ 낙상 경보 오버레이 그리기 (전체 화면 플래시 효과) Args: frame: 원본 프레임 alert_text: 경보 텍스트 flash: True면 화면 전체에 빨간 반투명 오버레이 추가 Returns: result: 경보 오버레이가 추가된 프레임 """ frame = frame.copy() h, w = frame.shape[:2] # 1. 플래시 효과 (빨간 반투명 오버레이) if flash: overlay = frame.copy() cv2.rectangle(overlay, (0, 0), (w, h), (0, 0, 255), -1) cv2.addWeighted(overlay, 0.3, frame, 0.7, 0, frame) # 2. 중앙에 큰 경고 텍스트 font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 2.5 font_thickness = 8 # 두꺼운 굵기로 볼드체 효과 (text_w, text_h), _ = cv2.getTextSize(alert_text, font, font_scale, font_thickness) text_x = (w - text_w) // 2 text_y = (h + text_h) // 2 # 텍스트 그림자 (검은색) cv2.putText(frame, alert_text, (text_x + 3, text_y + 3), font, font_scale, (0, 0, 0), font_thickness + 2, cv2.LINE_AA) # 텍스트 본문 (흰색) cv2.putText(frame, alert_text, (text_x, text_y), font, font_scale, (255, 255, 255), font_thickness, cv2.LINE_AA) return frame def visualize_fall_simple( frame: np.ndarray, keypoints: Optional[np.ndarray] = None, show_fall_text: bool = False, keypoint_mode: Literal['all', 'major'] = 'all', output_scale: float = 1.0 ) -> np.ndarray: """ 간소화된 낙상 감지 시각화 (Pose skeleton + FALL DETECTED 텍스트만) 표시 항목: - Pose skeleton (신체 부위별 색상) - FALL DETECTED 텍스트 (show_fall_text=True일 때) 제거된 항목: - FPS/Latency 정보 - 정보 패널 - 빨간 플래시 오버레이 - 신뢰도 표시 Args: frame: 원본 프레임 keypoints: (17, 3) pose keypoints (선택) show_fall_text: True면 FALL DETECTED 텍스트 표시 keypoint_mode: 'all'=전체 17개, 'major'=주요 9개만 표시 output_scale: 출력 해상도 스케일 (0.5=50%, 1.0=100%) Returns: result: 시각화된 프레임 """ # 1. 해상도 조절 (output_scale < 1.0인 경우) original_h, original_w = frame.shape[:2] if output_scale < 1.0: new_w = int(original_w * output_scale) new_h = int(original_h * output_scale) result = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR) # keypoints 좌표도 스케일 조정 if keypoints is not None: keypoints = keypoints.copy() keypoints[:, 0] *= output_scale # x 좌표 keypoints[:, 1] *= output_scale # y 좌표 else: result = frame.copy() # 2. 스켈레톤 그리기 if keypoints is not None: result = draw_skeleton_vectorized( result, keypoints, keypoint_mode=keypoint_mode, use_body_part_colors=True ) # 3. FALL DETECTED 텍스트 표시 (플래시 없이) if show_fall_text: h, w = result.shape[:2] alert_text = "FALL DETECTED" font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 2.0 font_thickness = 6 (text_w, text_h), _ = cv2.getTextSize(alert_text, font, font_scale, font_thickness) text_x = (w - text_w) // 2 text_y = 80 # 화면 상단 # 텍스트 배경 (반투명 검은색) bg_padding = 15 overlay = result.copy() cv2.rectangle( overlay, (text_x - bg_padding, text_y - text_h - bg_padding), (text_x + text_w + bg_padding, text_y + bg_padding), (0, 0, 0), -1 ) cv2.addWeighted(overlay, 0.6, result, 0.4, 0, result) # 텍스트 그림자 (검은색) cv2.putText(result, alert_text, (text_x + 2, text_y + 2), font, font_scale, (0, 0, 0), font_thickness + 2, cv2.LINE_AA) # 텍스트 본문 (빨간색) cv2.putText(result, alert_text, (text_x, text_y), font, font_scale, (0, 0, 255), font_thickness, cv2.LINE_AA) return result def visualize_fall_detection( frame: np.ndarray, keypoints: Optional[np.ndarray] = None, prediction: str = 'Non-Fall', confidence: float = 0.0, bbox: Optional[Tuple[int, int, int, int]] = None, fps: Optional[float] = None, latency: Optional[float] = None, show_skeleton: bool = True, show_info_panel: bool = True, show_alert: bool = False, use_optimized: bool = True, keypoint_mode: Literal['all', 'major'] = 'all', output_scale: float = 1.0 ) -> np.ndarray: """ 낙상 감지 결과 종합 시각화 (All-in-one 함수) Args: frame: 원본 프레임 keypoints: (17, 3) pose keypoints (선택) prediction: 'Fall' 또는 'Non-Fall' confidence: 신뢰도 bbox: 바운딩 박스 (선택) fps: FPS 값 (선택) latency: Latency (ms) (선택) show_skeleton: True면 스켈레톤 그리기 show_info_panel: True면 상단에 정보 패널 추가 show_alert: True면 낙상 경보 오버레이 추가 (prediction='Fall'일 때만) use_optimized: True면 벡터화된 그리기 함수 사용 (30배 빠름) keypoint_mode: 'all'=전체 17개, 'major'=주요 9개만 표시 output_scale: 출력 해상도 스케일 (0.5=50%, 1.0=100%) Returns: result: 시각화된 프레임 """ # 1. 해상도 조절 (output_scale < 1.0인 경우) original_h, original_w = frame.shape[:2] if output_scale < 1.0: new_w = int(original_w * output_scale) new_h = int(original_h * output_scale) result = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR) # keypoints 좌표도 스케일 조정 if keypoints is not None: keypoints = keypoints.copy() keypoints[:, 0] *= output_scale # x 좌표 keypoints[:, 1] *= output_scale # y 좌표 # bbox 좌표도 스케일 조정 if bbox is not None: bbox = tuple(int(v * output_scale) for v in bbox) else: result = frame.copy() # 2. 스켈레톤 그리기 if show_skeleton and keypoints is not None: if use_optimized: result = draw_skeleton_vectorized( result, keypoints, keypoint_mode=keypoint_mode, use_body_part_colors=True ) else: result = draw_skeleton(result, keypoints, use_body_part_colors=True) # 3. 예측 결과 오버레이 if fps is not None or latency is not None: result = draw_prediction(result, prediction, confidence, bbox, fps, latency, position='top-left') # 4. 낙상 경보 오버레이 (Fall이고 show_alert=True일 때만) if show_alert and prediction == 'Fall': result = draw_fall_alert_overlay(result, alert_text="FALL DETECTED!", flash=True) # 5. 정보 패널 추가 (선택) if show_info_panel and fps is not None and latency is not None: result = add_info_panel_to_frame(result, fps, latency, prediction, confidence, position='bottom') return result if __name__ == '__main__': import time import argparse parser = argparse.ArgumentParser(description='Visualization module test and benchmark') parser.add_argument('--benchmark', action='store_true', help='Run performance benchmark') parser.add_argument('--resolution', type=str, default='640x480', help='Test resolution (default: 640x480, options: 640x480, 1920x1080, 3840x2160)') parser.add_argument('--iterations', type=int, default=100, help='Benchmark iterations') args = parser.parse_args() # 해상도 파싱 res_map = { '640x480': (480, 640), '1920x1080': (1080, 1920), '3840x2160': (2160, 3840), '4k': (2160, 3840), 'fhd': (1080, 1920), 'vga': (480, 640), } h, w = res_map.get(args.resolution.lower(), (480, 640)) print(f"Testing visualization module at {w}x{h}...") # 1. 더미 프레임 생성 frame = np.zeros((h, w, 3), dtype=np.uint8) frame[:, :] = (50, 50, 50) # 2. 더미 키포인트 생성 (해상도에 맞게 스케일) scale_x = w / 640 scale_y = h / 480 keypoints = np.array([ [320, 100, 0.9], # 0: nose [310, 90, 0.9], # 1: left_eye [330, 90, 0.9], # 2: right_eye [300, 90, 0.8], # 3: left_ear [340, 90, 0.8], # 4: right_ear [300, 150, 0.95], # 5: left_shoulder [340, 150, 0.95], # 6: right_shoulder [280, 200, 0.9], # 7: left_elbow [360, 200, 0.9], # 8: right_elbow [270, 250, 0.85], # 9: left_wrist [370, 250, 0.85], # 10: right_wrist [300, 250, 0.95], # 11: left_hip [340, 250, 0.95], # 12: right_hip [300, 350, 0.9], # 13: left_knee [340, 350, 0.9], # 14: right_knee [300, 450, 0.85], # 15: left_ankle [340, 450, 0.85], # 16: right_ankle ], dtype=np.float32) keypoints[:, 0] *= scale_x keypoints[:, 1] *= scale_y if args.benchmark: print("\n" + "=" * 70) print("BENCHMARK: Visualization Performance Comparison") print("=" * 70) print(f"Resolution: {w}x{h}") print(f"Iterations: {args.iterations}") print("=" * 70) # 기존 draw_skeleton 벤치마크 print("\n[1] draw_skeleton (original - cv2.circle/line loops)") times_original = [] for _ in range(args.iterations): start = time.perf_counter() _ = draw_skeleton(frame.copy(), keypoints, use_body_part_colors=True) times_original.append((time.perf_counter() - start) * 1000) avg_original = np.mean(times_original) std_original = np.std(times_original) print(f" Average: {avg_original:.2f}ms (+/- {std_original:.2f}ms)") # 벡터화 draw_skeleton_vectorized 벤치마크 (all keypoints) print("\n[2] draw_skeleton_vectorized (optimized - all keypoints)") times_vectorized = [] for _ in range(args.iterations): start = time.perf_counter() _ = draw_skeleton_vectorized(frame.copy(), keypoints, keypoint_mode='all') times_vectorized.append((time.perf_counter() - start) * 1000) avg_vectorized = np.mean(times_vectorized) std_vectorized = np.std(times_vectorized) speedup_all = avg_original / avg_vectorized print(f" Average: {avg_vectorized:.2f}ms (+/- {std_vectorized:.2f}ms)") print(f" Speedup: {speedup_all:.1f}x faster") # 벡터화 draw_skeleton_vectorized 벤치마크 (major keypoints) print("\n[3] draw_skeleton_vectorized (optimized - major keypoints only)") times_major = [] for _ in range(args.iterations): start = time.perf_counter() _ = draw_skeleton_vectorized(frame.copy(), keypoints, keypoint_mode='major') times_major.append((time.perf_counter() - start) * 1000) avg_major = np.mean(times_major) std_major = np.std(times_major) speedup_major = avg_original / avg_major print(f" Average: {avg_major:.2f}ms (+/- {std_major:.2f}ms)") print(f" Speedup: {speedup_major:.1f}x faster") # 해상도 스케일 + 벡터화 벤치마크 if w > 640: print("\n[4] draw_skeleton_vectorized + 50% scale") times_scaled = [] for _ in range(args.iterations): start = time.perf_counter() result = visualize_fall_detection( frame.copy(), keypoints, prediction='Fall', confidence=0.9, fps=30.0, latency=50.0, use_optimized=True, keypoint_mode='all', output_scale=0.5 ) times_scaled.append((time.perf_counter() - start) * 1000) avg_scaled = np.mean(times_scaled) std_scaled = np.std(times_scaled) print(f" Average: {avg_scaled:.2f}ms (+/- {std_scaled:.2f}ms)") print(f" Output size: {result.shape[1]}x{result.shape[0]}") print("\n" + "=" * 70) print("SUMMARY") print("=" * 70) print(f"Original: {avg_original:.2f}ms") print(f"Optimized: {avg_vectorized:.2f}ms ({speedup_all:.1f}x faster)") print(f"Major only: {avg_major:.2f}ms ({speedup_major:.1f}x faster)") target_met = avg_vectorized < 10.0 print(f"\nTarget (<10ms): {'MET' if target_met else 'NOT MET'}") print("=" * 70) else: # 기본 기능 테스트 print("\n1. Testing draw_skeleton (original)...") result = draw_skeleton(frame.copy(), keypoints, use_body_part_colors=True) print(f" Output shape: {result.shape}") print("\n2. Testing draw_skeleton_vectorized (optimized)...") result = draw_skeleton_vectorized(frame.copy(), keypoints, keypoint_mode='all') print(f" Output shape: {result.shape}") print("\n3. Testing draw_skeleton_vectorized (major only)...") result = draw_skeleton_vectorized(frame.copy(), keypoints, keypoint_mode='major') print(f" Output shape: {result.shape}") print("\n4. Testing draw_prediction...") result = draw_prediction( frame.copy(), prediction='Non-Fall', confidence=0.95, bbox=(int(270*scale_x), int(90*scale_y), int(370*scale_x), int(450*scale_y)), fps=30.0, latency=50.0 ) print(f" Output shape: {result.shape}") print("\n5. Testing create_info_panel...") panel = create_info_panel(w, h, fps=30.0, latency=50.0, prediction='Non-Fall', confidence=0.95) print(f" Panel shape: {panel.shape}") print("\n6. Testing visualize_fall_detection (optimized=True)...") result = visualize_fall_detection( frame=frame, keypoints=keypoints, prediction='Fall', confidence=0.87, fps=30.0, latency=50.0, show_skeleton=True, show_info_panel=True, show_alert=True, use_optimized=True, keypoint_mode='all' ) print(f" Output shape: {result.shape}") print("\n7. Testing visualize_fall_detection (output_scale=0.5)...") result = visualize_fall_detection( frame=frame, keypoints=keypoints, prediction='Non-Fall', confidence=0.95, fps=30.0, latency=50.0, show_skeleton=True, show_info_panel=True, use_optimized=True, output_scale=0.5 ) print(f" Output shape: {result.shape}") print("\nAll tests passed!")