Spaces:
Running
on
Zero
Running
on
Zero
import os | |
from typing import Any, Dict, List, Optional, Tuple, Union | |
import cv2 | |
import numpy as np | |
NEUTRAL_COLOR = (52, 235, 107) | |
LEFT_ARM_COLOR = (216, 235, 52) | |
LEFT_LEG_COLOR = (235, 107, 52) | |
LEFT_SIDE_COLOR = (245, 188, 113) | |
LEFT_FACE_COLOR = (235, 52, 107) | |
RIGHT_ARM_COLOR = (52, 235, 216) | |
RIGHT_LEG_COLOR = (52, 107, 235) | |
RIGHT_SIDE_COLOR = (52, 171, 235) | |
RIGHT_FACE_COLOR = (107, 52, 235) | |
COCO_MARKERS = [ | |
["nose", cv2.MARKER_CROSS, NEUTRAL_COLOR], | |
["left_eye", cv2.MARKER_SQUARE, LEFT_FACE_COLOR], | |
["right_eye", cv2.MARKER_SQUARE, RIGHT_FACE_COLOR], | |
["left_ear", cv2.MARKER_CROSS, LEFT_FACE_COLOR], | |
["right_ear", cv2.MARKER_CROSS, RIGHT_FACE_COLOR], | |
["left_shoulder", cv2.MARKER_TRIANGLE_UP, LEFT_ARM_COLOR], | |
["right_shoulder", cv2.MARKER_TRIANGLE_UP, RIGHT_ARM_COLOR], | |
["left_elbow", cv2.MARKER_SQUARE, LEFT_ARM_COLOR], | |
["right_elbow", cv2.MARKER_SQUARE, RIGHT_ARM_COLOR], | |
["left_wrist", cv2.MARKER_CROSS, LEFT_ARM_COLOR], | |
["right_wrist", cv2.MARKER_CROSS, RIGHT_ARM_COLOR], | |
["left_hip", cv2.MARKER_TRIANGLE_UP, LEFT_LEG_COLOR], | |
["right_hip", cv2.MARKER_TRIANGLE_UP, RIGHT_LEG_COLOR], | |
["left_knee", cv2.MARKER_SQUARE, LEFT_LEG_COLOR], | |
["right_knee", cv2.MARKER_SQUARE, RIGHT_LEG_COLOR], | |
["left_ankle", cv2.MARKER_TILTED_CROSS, LEFT_LEG_COLOR], | |
["right_ankle", cv2.MARKER_TILTED_CROSS, RIGHT_LEG_COLOR], | |
] | |
COCO_SKELETON = [ | |
[[16, 14], LEFT_LEG_COLOR], # Left ankle - Left knee | |
[[14, 12], LEFT_LEG_COLOR], # Left knee - Left hip | |
[[17, 15], RIGHT_LEG_COLOR], # Right ankle - Right knee | |
[[15, 13], RIGHT_LEG_COLOR], # Right knee - Right hip | |
[[12, 13], NEUTRAL_COLOR], # Left hip - Right hip | |
[[6, 12], LEFT_SIDE_COLOR], # Left hip - Left shoulder | |
[[7, 13], RIGHT_SIDE_COLOR], # Right hip - Right shoulder | |
[[6, 7], NEUTRAL_COLOR], # Left shoulder - Right shoulder | |
[[6, 8], LEFT_ARM_COLOR], # Left shoulder - Left elbow | |
[[7, 9], RIGHT_ARM_COLOR], # Right shoulder - Right elbow | |
[[8, 10], LEFT_ARM_COLOR], # Left elbow - Left wrist | |
[[9, 11], RIGHT_ARM_COLOR], # Right elbow - Right wrist | |
[[2, 3], NEUTRAL_COLOR], # Left eye - Right eye | |
[[1, 2], LEFT_FACE_COLOR], # Nose - Left eye | |
[[1, 3], RIGHT_FACE_COLOR], # Nose - Right eye | |
[[2, 4], LEFT_FACE_COLOR], # Left eye - Left ear | |
[[3, 5], RIGHT_FACE_COLOR], # Right eye - Right ear | |
[[4, 6], LEFT_FACE_COLOR], # Left ear - Left shoulder | |
[[5, 7], RIGHT_FACE_COLOR], # Right ear - Right shoulder | |
] | |
def _draw_line( | |
img: np.ndarray, | |
start: Tuple[float, float], | |
stop: Tuple[float, float], | |
color: Tuple[int, int, int], | |
line_type: str, | |
thickness: int = 1, | |
) -> np.ndarray: | |
""" | |
Draw a line segment on an image, supporting solid, dashed, or dotted styles. | |
Args: | |
img (np.ndarray): BGR image of shape (H, W, 3). | |
start (tuple of float): (x, y) start coordinates. | |
stop (tuple of float): (x, y) end coordinates. | |
color (tuple of int): BGR color values. | |
line_type (str): One of 'solid', 'dashed', or 'doted'. | |
thickness (int): Line thickness in pixels. | |
Returns: | |
np.ndarray: Image with the line drawn. | |
""" | |
start = np.array(start)[:2] | |
stop = np.array(stop)[:2] | |
if line_type.lower() == "solid": | |
img = cv2.line( | |
img, | |
(int(start[0]), int(start[1])), | |
(int(stop[0]), int(stop[1])), | |
color=(0, 0, 0), | |
thickness=thickness+1, | |
lineType=cv2.LINE_AA, | |
) | |
img = cv2.line( | |
img, | |
(int(start[0]), int(start[1])), | |
(int(stop[0]), int(stop[1])), | |
color=color, | |
thickness=thickness, | |
lineType=cv2.LINE_AA, | |
) | |
elif line_type.lower() == "dashed": | |
delta = stop - start | |
length = np.linalg.norm(delta) | |
frac = np.linspace(0, 1, num=int(length / 5), endpoint=True) | |
for i in range(0, len(frac) - 1, 2): | |
s = start + frac[i] * delta | |
e = start + frac[i + 1] * delta | |
img = cv2.line( | |
img, | |
(int(s[0]), int(s[1])), | |
(int(e[0]), int(e[1])), | |
color=color, | |
thickness=thickness, | |
lineType=cv2.LINE_AA, | |
) | |
elif line_type.lower() == "doted": | |
delta = stop - start | |
length = np.linalg.norm(delta) | |
frac = np.linspace(0, 1, num=int(length / 5), endpoint=True) | |
for i in range(0, len(frac)): | |
s = start + frac[i] * delta | |
img = cv2.circle( | |
img, | |
(int(s[0]), int(s[1])), | |
radius=max(thickness // 2, 1), | |
color=color, | |
thickness=-1, | |
lineType=cv2.LINE_AA, | |
) | |
return img | |
def pose_visualization( | |
img: Union[str, np.ndarray], | |
keypoints: Union[Dict[str, Any], np.ndarray], | |
format: str = "COCO", | |
greyness: float = 1.0, | |
show_markers: bool = True, | |
show_bones: bool = True, | |
line_type: str = "solid", | |
width_multiplier: float = 1.0, | |
bbox_width_multiplier: float = 1.0, | |
show_bbox: bool = False, | |
differ_individuals: bool = False, | |
confidence_thr: float = 0.3, | |
errors: Optional[np.ndarray] = None, | |
color: Optional[Tuple[int, int, int]] = None, | |
keep_image_size: bool = False, | |
return_padding: bool = False, | |
) -> Union[np.ndarray, Tuple[np.ndarray, List[int]]]: | |
""" | |
Overlay pose keypoints and skeleton on an image. | |
Args: | |
img (str or np.ndarray): Path to image file or BGR image array. | |
keypoints (dict or np.ndarray): Either a dict with 'bbox' and 'keypoints' or | |
an array of shape (17, 2 or 3) or multiple poses stacked. | |
format (str): Keypoint format, currently only 'COCO'. | |
greyness (float): Factor for bone/marker color intensity (0.0-1.0). | |
show_markers (bool): Whether to draw keypoint markers. | |
show_bones (bool): Whether to draw skeleton bones. | |
line_type (str): One of 'solid', 'dashed', 'doted' for bone style. | |
width_multiplier (float): Line width scaling factor for bones. | |
bbox_width_multiplier (float): Line width scaling factor for bounding box. | |
show_bbox (bool): Whether to draw bounding box around keypoints. | |
differ_individuals (bool): Use distinct color per individual pose. | |
confidence_thr (float): Confidence threshold for keypoint visibility. | |
errors (np.ndarray or None): Optional array of per-kpt errors (17,1). | |
color (tuple or None): Override color for markers and bones. | |
keep_image_size (bool): Prevent image padding for out-of-bounds keypoints. | |
return_padding (bool): If True, also return padding offsets [top,bottom,left,right]. | |
Returns: | |
np.ndarray or (np.ndarray, list of int): Annotated image, and optional | |
padding offsets if `return_padding` is True. | |
""" | |
bbox = None | |
if isinstance(keypoints, dict): | |
try: | |
bbox = np.array(keypoints["bbox"]).flatten() | |
except KeyError: | |
pass | |
keypoints = np.array(keypoints["keypoints"]) | |
# If keypoints is a list of poses, draw them all | |
if len(keypoints) % 17 != 0 or keypoints.ndim == 3: | |
if color is not None: | |
if not isinstance(color, (list, tuple)): | |
color = [color for keypoint in keypoints] | |
else: | |
color = [None for keypoint in keypoints] | |
max_padding = [0, 0, 0, 0] | |
for keypoint, clr in zip(keypoints, color): | |
img = pose_visualization( | |
img, | |
keypoint, | |
format=format, | |
greyness=greyness, | |
show_markers=show_markers, | |
show_bones=show_bones, | |
line_type=line_type, | |
width_multiplier=width_multiplier, | |
bbox_width_multiplier=bbox_width_multiplier, | |
show_bbox=show_bbox, | |
differ_individuals=differ_individuals, | |
color=clr, | |
confidence_thr=confidence_thr, | |
keep_image_size=keep_image_size, | |
return_padding=return_padding, | |
) | |
if return_padding: | |
img, padding = img | |
max_padding = [max(max_padding[i], int(padding[i])) for i in range(4)] | |
if return_padding: | |
return img, max_padding | |
else: | |
return img | |
keypoints = np.array(keypoints).reshape(17, -1) | |
# If keypoint visibility is not provided, assume all keypoints are visible | |
if keypoints.shape[1] == 2: | |
keypoints = np.hstack([keypoints, np.ones((17, 1)) * 2]) | |
assert keypoints.shape[1] == 3, "Keypoints should be in the format (x, y, visibility)" | |
assert keypoints.shape[0] == 17, "Keypoints should be in the format (x, y, visibility)" | |
if errors is not None: | |
errors = np.array(errors).reshape(17, -1) | |
assert errors.shape[1] == 1, "Errors should be in the format (K, r)" | |
assert errors.shape[0] == 17, "Errors should be in the format (K, r)" | |
else: | |
errors = np.ones((17, 1)) * np.nan | |
# If keypoint visibility is float between 0 and 1, it is detection | |
# If conf < confidence_thr: conf = 1 | |
# If conf >= confidence_thr: conf = 2 | |
vis_is_float = np.any(np.logical_and(keypoints[:, -1] > 0, keypoints[:, -1] < 1)) | |
if keypoints.shape[1] == 3 and vis_is_float: | |
# print("before", keypoints[:, -1]) | |
lower_idx = keypoints[:, -1] < confidence_thr | |
keypoints[lower_idx, -1] = 1 | |
keypoints[~lower_idx, -1] = 2 | |
# print("after", keypoints[:, -1]) | |
# print("-"*20) | |
# All visibility values should be ints | |
keypoints[:, -1] = keypoints[:, -1].astype(int) | |
if isinstance(img, str): | |
img = cv2.imread(img) | |
if img is None: | |
if return_padding: | |
return None, [0, 0, 0, 0] | |
else: | |
return None | |
if not (keypoints[:, 2] > 0).any(): | |
if return_padding: | |
return img, [0, 0, 0, 0] | |
else: | |
return img | |
valid_kpts = (keypoints[:, 0] > 0) & (keypoints[:, 1] > 0) | |
num_valid_kpts = np.sum(valid_kpts) | |
if num_valid_kpts == 0: | |
if return_padding: | |
return img, [0, 0, 0, 0] | |
else: | |
return img | |
min_x_kpts = np.min(keypoints[keypoints[:, 2] > 0, 0]) | |
min_y_kpts = np.min(keypoints[keypoints[:, 2] > 0, 1]) | |
max_x_kpts = np.max(keypoints[keypoints[:, 2] > 0, 0]) | |
max_y_kpts = np.max(keypoints[keypoints[:, 2] > 0, 1]) | |
if bbox is None: | |
min_x = min_x_kpts | |
min_y = min_y_kpts | |
max_x = max_x_kpts | |
max_y = max_y_kpts | |
else: | |
min_x = bbox[0] | |
min_y = bbox[1] | |
max_x = bbox[2] | |
max_y = bbox[3] | |
max_area = (max_x - min_x) * (max_y - min_y) | |
diagonal = np.sqrt((max_x - min_x) ** 2 + (max_y - min_y) ** 2) | |
line_width = max(int(np.sqrt(max_area) / 500 * width_multiplier), 1) | |
bbox_line_width = max(int(np.sqrt(max_area) / 500 * bbox_width_multiplier), 1) | |
marker_size = max(int(np.sqrt(max_area) / 80), 1) | |
invisible_marker_size = max(int(np.sqrt(max_area) / 100), 1) | |
marker_thickness = max(int(np.sqrt(max_area) / 100), 1) | |
if differ_individuals: | |
if color is not None: | |
instance_color = color | |
else: | |
instance_color = np.random.randint(0, 255, size=(3,)).tolist() | |
instance_color = tuple(instance_color) | |
# Pad image with dark gray if keypoints are outside the image | |
if not keep_image_size: | |
padding = [ | |
max(0, -min_y_kpts), | |
max(0, max_y_kpts - img.shape[0]), | |
max(0, -min_x_kpts), | |
max(0, max_x_kpts - img.shape[1]), | |
] | |
padding = [int(p) for p in padding] | |
img = cv2.copyMakeBorder( | |
img, | |
padding[0], | |
padding[1], | |
padding[2], | |
padding[3], | |
cv2.BORDER_CONSTANT, | |
value=(80, 80, 80), | |
) | |
# Add padding to bbox and kpts | |
value_x_to_add = max(0, -min_x_kpts) | |
value_y_to_add = max(0, -min_y_kpts) | |
keypoints[keypoints[:, 2] > 0, 0] += value_x_to_add | |
keypoints[keypoints[:, 2] > 0, 1] += value_y_to_add | |
if bbox is not None: | |
bbox[0] += value_x_to_add | |
bbox[1] += value_y_to_add | |
bbox[2] += value_x_to_add | |
bbox[3] += value_y_to_add | |
if show_bbox and not (bbox is None): | |
pts = [ | |
(bbox[0], bbox[1]), | |
(bbox[0], bbox[3]), | |
(bbox[2], bbox[3]), | |
(bbox[2], bbox[1]), | |
(bbox[0], bbox[1]), | |
] | |
for i in range(len(pts) - 1): | |
if differ_individuals: | |
img = _draw_line(img, pts[i], pts[i + 1], instance_color, "doted", thickness=bbox_line_width) | |
else: | |
img = _draw_line(img, pts[i], pts[i + 1], (0, 255, 0), line_type, thickness=bbox_line_width) | |
if show_markers: | |
for kpt, marker_info, err in zip(keypoints, COCO_MARKERS, errors): | |
if kpt[0] == 0 and kpt[1] == 0: | |
continue | |
if kpt[2] != 2: | |
color = (140, 140, 140) | |
elif differ_individuals: | |
color = instance_color | |
else: | |
color = marker_info[2] | |
if kpt[2] == 1: | |
img_overlay = img.copy() | |
img_overlay = cv2.drawMarker( | |
img_overlay, | |
(int(kpt[0]), int(kpt[1])), | |
color=color, | |
markerType=marker_info[1], | |
markerSize=marker_size, | |
thickness=marker_thickness, | |
) | |
img = cv2.addWeighted(img_overlay, 0.4, img, 0.6, 0) | |
else: | |
img = cv2.drawMarker( | |
img, | |
(int(kpt[0]), int(kpt[1])), | |
color=color, | |
markerType=marker_info[1], | |
markerSize=invisible_marker_size if kpt[2] == 1 else marker_size, | |
thickness=marker_thickness, | |
) | |
if not np.isnan(err).any(): | |
radius = err * diagonal | |
clr = (0, 0, 255) if "solid" in line_type else (0, 255, 0) | |
plus = 1 if "solid" in line_type else -1 | |
img = cv2.circle( | |
img, | |
(int(kpt[0]), int(kpt[1])), | |
radius=int(radius), | |
color=clr, | |
thickness=1, | |
lineType=cv2.LINE_AA, | |
) | |
dx = np.sqrt(radius**2 / 2) | |
img = cv2.line( | |
img, | |
(int(kpt[0]), int(kpt[1])), | |
(int(kpt[0] + plus * dx), int(kpt[1] - dx)), | |
color=clr, | |
thickness=1, | |
lineType=cv2.LINE_AA, | |
) | |
if show_bones: | |
for bone_info in COCO_SKELETON: | |
kp1 = keypoints[bone_info[0][0] - 1, :] | |
kp2 = keypoints[bone_info[0][1] - 1, :] | |
if (kp1[0] == 0 and kp1[1] == 0) or (kp2[0] == 0 and kp2[1] == 0): | |
continue | |
dashed = kp1[2] == 1 or kp2[2] == 1 | |
if differ_individuals: | |
color = np.array(instance_color) | |
else: | |
color = np.array(bone_info[1]) | |
color = (color * greyness).astype(int).tolist() | |
if dashed: | |
img_overlay = img.copy() | |
img_overlay = _draw_line(img_overlay, kp1, kp2, color, line_type, thickness=line_width) | |
img = cv2.addWeighted(img_overlay, 0.4, img, 0.6, 0) | |
else: | |
img = _draw_line(img, kp1, kp2, color, line_type, thickness=line_width) | |
if return_padding: | |
return img, padding | |
else: | |
return img | |
if __name__ == "__main__": | |
kpts = np.array( | |
[ | |
344, | |
222, | |
2, | |
356, | |
211, | |
2, | |
330, | |
211, | |
2, | |
372, | |
220, | |
2, | |
309, | |
224, | |
2, | |
413, | |
279, | |
2, | |
274, | |
300, | |
2, | |
444, | |
372, | |
2, | |
261, | |
396, | |
2, | |
398, | |
359, | |
2, | |
316, | |
372, | |
2, | |
407, | |
489, | |
2, | |
185, | |
580, | |
2, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
0, | |
] | |
) | |
kpts = kpts.reshape(-1, 3) | |
kpts[:, -1] = np.random.randint(1, 3, size=(17,)) | |
img = pose_visualization("demo/posevis_test.jpg", kpts, show_markers=True, line_type="solid") | |
kpts2 = kpts.copy() | |
kpts2[kpts2[:, 1] > 0, :2] += 10 | |
img = pose_visualization(img, kpts2, show_markers=False, line_type="doted") | |
os.makedirs("demo/outputs", exist_ok=True) | |
cv2.imwrite("demo/outputs/posevis_test_out.jpg", img) | |