BBoxMaskPose-demo / demo /posevis_lite.py
Miroslav Purkrabek
add code
a249588
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)