Hand Module
Hand Detection Module
Author: Nathan Forsyth
Hand
Class for displaying and retrieving data for hands recognized by the HandDetector module.
Attributes:
| Name | Type | Description |
|---|---|---|
landmarks |
list[tuple[int, int, int]]
|
List of landmarks using 3D pixel coordinates. |
normalized_landmarks |
list[tuple[float, float, float]]
|
List of the original landmark objects provided by the HandDetector. |
side |
str
|
String value representing the side of the hand ("Left" or "Right"). |
thumb |
tuple[int, int, int]
|
3D pixel coordinates of the thumb tip position. |
index |
tuple[int, int, int]
|
3D pixel coordinates of the index fingertip. |
middle |
tuple[int, int, int]
|
3D pixel coordinates of the middle fingertip. |
ring |
tuple[int, int, int]
|
3D pixel coordinates of the ring fingertip. |
pinky |
tuple[int, int, int]
|
3D pixel coordinates of the pinky fingertip. |
wrist |
tuple[int, int, int]
|
3D pixel coordinates of the wrist landmarker. |
connection_style |
ConnectionStyle
|
Style settings for the color and thickness of the line connections of the hand when drawn. |
landmark_style |
LandmarkStyle
|
Style settings for the color, stroke color, stroke thickness, and radius of the landmarks of the hand when drawn. |
flags |
list[int]
|
List of binary values representing which fingers are up and which fingers are down. |
box |
BoundingBox
|
Bounding box of the hand. |
center |
tuple[int, int]
|
2D pixel coordinates center of the bounding box. |
image |
ndarray
|
Image used by the HandDetector. Used as the default image to display the drawn hand. |
Source code in src/pumpkinpipe/hand.py
class Hand:
"""
Class for displaying and retrieving data for hands recognized by the HandDetector module.
:ivar landmarks: List of landmarks using 3D pixel coordinates.
:ivar normalized_landmarks: List of the original landmark objects provided by the HandDetector.
:ivar side: String value representing the side of the hand ("Left" or "Right").
:ivar thumb: 3D pixel coordinates of the thumb tip position.
:ivar index: 3D pixel coordinates of the index fingertip.
:ivar middle: 3D pixel coordinates of the middle fingertip.
:ivar ring: 3D pixel coordinates of the ring fingertip.
:ivar pinky: 3D pixel coordinates of the pinky fingertip.
:ivar wrist: 3D pixel coordinates of the wrist landmarker.
:ivar connection_style: Style settings for the color and thickness of the line connections of the hand when drawn.
:ivar landmark_style: Style settings for the color, stroke color, stroke thickness, and radius of the landmarks of the hand when drawn.
:ivar flags: List of binary values representing which fingers are up and which fingers are down.
:ivar box: Bounding box of the hand.
:ivar center: 2D pixel coordinates center of the bounding box.
:ivar image: Image used by the HandDetector. Used as the default image to display the drawn hand.
"""
_DEFAULT_CONNECTION_STYLE : ConnectionStyle = ConnectionStyle()
_DEFAULT_LANDMARK_STYLE : LandmarkStyle = LandmarkStyle()
_REGION_COLORS : Sequence[tuple[int, int, int]]= (
(245, 135, 66),
(245, 66, 167),
(105, 66, 245),
(66, 152, 245),
(66, 245, 176),
(127, 127, 127)
)
_CONNECTIONS = HandLandmarksConnections.HAND_CONNECTIONS
_PALM_CONNECTIONS = HandLandmarksConnections.HAND_PALM_CONNECTIONS
_THUMB_CONNECTIONS = HandLandmarksConnections.HAND_THUMB_CONNECTIONS
_INDEX_CONNECTIONS = HandLandmarksConnections.HAND_INDEX_FINGER_CONNECTIONS
_MIDDLE_CONNECTIONS = HandLandmarksConnections.HAND_MIDDLE_FINGER_CONNECTIONS
_RING_CONNECTIONS = HandLandmarksConnections.HAND_RING_FINGER_CONNECTIONS
_PINKY_CONNECTIONS = HandLandmarksConnections.HAND_PINKY_FINGER_CONNECTIONS
_THUMB_LANDMARKS = (2, 3, 4)
_INDEX_LANDMARKS = (6, 7, 8)
_MIDDLE_LANDMARKS = (10, 11, 12)
_RING_LANDMARKS = (14, 15, 16)
_PINKY_LANDMARKS = (18, 19, 20)
_PALM_LANDMARKS = (0, 1, 5, 9, 13, 17)
_THUMB_TIP_ID = 4
_INDEX_TIP_ID = 8
_MIDDLE_TIP_ID = 12
_RING_TIP_ID = 16
_PINKY_TIP_ID = 20
_WRIST_ID = 0
def __init__(self, landmarks : list[tuple[int, int, int]], normalized_landmarks: list[tuple[float, float, float]], side : str, box : BoundingBox, image : np.ndarray):
"""
Initialize the hand.
:param landmarks: The [x,y,z] pixel coordinates of landmarks.
:param normalized_landmarks: The actual landmarks of the hand as provided by mediapipe.
:param side: The side of the hand ("Left" or "Right").
:param box: The bounding box of the hand.
:param image: The image that was used to detect the hand.
"""
self.landmarks : list[tuple[int, int, int]] = landmarks
self.normalized_landmarks : list[tuple[float, float, float]] = normalized_landmarks
self.side : str = side
self.thumb : tuple[int, int, int] = self.landmarks[Hand._THUMB_TIP_ID]
self.index : tuple[int, int, int] = self.landmarks[Hand._INDEX_TIP_ID]
self.middle : tuple[int, int, int] = self.landmarks[Hand._MIDDLE_TIP_ID]
self.ring : tuple[int, int, int] = self.landmarks[Hand._RING_TIP_ID]
self.pinky : tuple[int, int, int] = self.landmarks[Hand._PINKY_TIP_ID]
self.wrist : tuple[int, int, int] = self.landmarks[Hand._WRIST_ID]
self.connection_style : ConnectionStyle = ConnectionStyle()
self.landmark_style : LandmarkStyle = LandmarkStyle()
self.flags : list[int] = self.finger_flags()
self.box : BoundingBox = box
self.center : tuple[int, int] = self.box.center
self.image : np.ndarray = image
def landmark_distance(self, landmark_index_1 : int, landmark_index_2 : int, image : None | np.ndarray=None, draw : bool=False) -> float:
"""
Finds the distance in pixels between 2 specified landmarks.
:param landmark_index_1: The landmark index for the first point
:param landmark_index_2: The landmark index for the second point
:param image: If not None, draws a line between the 2 points on the specified image
:param draw: If True, currently does nothing. Future implementations will draw the line.
:return: The distance between 2 points
"""
landmark_1 = x_1, y_1, z_1 = self.landmarks[landmark_index_1]
landmark_2 = x_2, y_2, z_2 = self.landmarks[landmark_index_2]
point_1 = x_1, y_1
point_2 = x_2, y_2
distance : float = math.dist(landmark_1, landmark_2)
if draw:
if image is None:
image = self.image
cv2.line(
image,
point_1,
point_2,
(255,0,0),
4
)
cv2.circle(
image,
point_1,
8,
(255, 0, 0),
-1
)
cv2.circle(
image,
point_2,
8,
(255, 0, 0),
-1
)
cv2.circle(
image,
point_1,
8,
(127, 0, 0),
1
)
cv2.circle(
image,
point_2,
8,
(127, 0, 0),
1
)
return distance
def finger_flags(self) -> list[int]:
"""
Finds which fingers are up and returns them as a binary list in the order of thumb, index, middle, ring, pinky.
:return: A list of binary values representing whether a finger is up or down
"""
# Initialize empty list for finger flags
fingers : list[int] = []
# Distance for thumb to be considered open
distance_threshold = 0.3
# Vector math to determine whether landmarks 1→2 are closely aligned with 2→3
angle_a = angle_3d(self.landmarks[1], self.landmarks[2])
angle_b = angle_3d(self.landmarks[2], self.landmarks[3])
thumb_angle_distance = math.dist(angle_a, angle_b)
# Append thumb value to fingers
if thumb_angle_distance < distance_threshold:
fingers.append(1)
else:
fingers.append(0)
# Landmark indices for index_tip, middle_tip, ring_tip, and pinky_tip
finger_indices = [Hand._INDEX_TIP_ID, Hand._MIDDLE_TIP_ID, Hand._RING_TIP_ID, Hand._PINKY_TIP_ID]
# Compare the distance between the tip and the wrist to the distance between the knuckle and the wrist
for index in finger_indices:
tip_distance = math.dist(self.landmarks[index], self.landmarks[Hand._WRIST_ID])
knuckle_distance = math.dist(self.landmarks[index - 2], self.landmarks[Hand._WRIST_ID])
# Append each finger value to fingers
if tip_distance > knuckle_distance:
fingers.append(1)
else:
fingers.append(0)
# Return list of flags
return fingers
def fingers_up(self) -> list[str]:
"""
Provides a list of the fingers that are up in English.
:return: A list of finger names.
"""
# Initialize empty list for finger flags
fingers = []
finger_names = ["Thumb", "Index", "Middle", "Ring", "Pinky"]
for flag, name in zip(self.flags, finger_names):
# Append fingers
if flag > 0:
fingers.append(name)
# Return list of fingers that are up
return fingers
def fingers_down(self) -> list[str]:
"""
Provides a list of the fingers that are down in English.
:return: A list of finger names.
"""
# Initialize empty list for finger flags
fingers = []
finger_names = ["Thumb", "Index", "Middle", "Ring", "Pinky"]
for flag, name in zip(self.flags, finger_names):
# Append fingers
if flag < 1:
fingers.append(name)
# Return list of fingers that are up
return fingers
def draw(self, image : None | np.ndarray=None):
"""
Draws the hand skeleton on the specified image. Currently, this library uses a custom way to draw while drawing_utils.py remains unimplemented in the official mediapipe release.
:param image: The target image for the drawing. If none, it will draw on the hands self.image
"""
if image is None:
image = self.image
for connection in Hand._CONNECTIONS:
start_x, start_y, _ = self.landmarks[connection.start]
end_x, end_y, _ = self.landmarks[connection.end]
cv2.line(
image,
(start_x, start_y),
(end_x, end_y),
self.connection_style.stroke,
self.connection_style.thickness
)
for landmark in self.landmarks:
x, y, _ = landmark
# Filled circle
cv2.circle(
image,
(x,y),
self.landmark_style.radius,
self.landmark_style.fill,
-1
)
# Outline
cv2.circle(
image,
(x, y),
self.landmark_style.radius,
self.landmark_style.stroke,
self.landmark_style.thickness
)
def debug(self, image : None | np.ndarray=None, skeleton:bool=True, bounding_box:bool=True, center:bool=True, side:bool=True, fingers:bool=True, flags:bool=True, tip_points:bool=True):
"""
Draws the requested debug information. Defaults to all debug information.
:param image: The image to draw the debug information on. If image is none then the hand will draw on its self.image
:param skeleton: When True, draw the landmarks and connections of the hand. Hand regions are separated by color.
:param bounding_box: When True, draw the outer bounding box of the hand.
:param center: When True, draw a green circle at the center of the bounding box. Also display the value for hand.center.
:param side: When True, write the value for hand.side underneath the hand.
:param flags: When True, display the value for hand.flags (binary list) representing which fingers are up or down.
:param fingers: When True, display the value returned by hand.fingers_up() (a list of strings for each finger that is registered as being up).
:param tip_points: When True, display hand.thumb, hand.index, hand.middle, hand.ring, hand.pinky, and hand.wrist values near their corresponding fingertips.
"""
if image is None:
image = self.image
# Set default values
height, width, _ = image.shape
debug_text_size = 1
debug_font = cv2.FONT_HERSHEY_PLAIN
debug_thickness = 1
# Display the hand connections and landmarks with the different regions as different colors.
if skeleton:
connection_lines = []
for connections, color in zip(
[
Hand._THUMB_CONNECTIONS,
Hand._INDEX_CONNECTIONS,
Hand._MIDDLE_CONNECTIONS,
Hand._RING_CONNECTIONS,
Hand._PINKY_CONNECTIONS,
Hand._PALM_CONNECTIONS
],
Hand._REGION_COLORS
):
for connection in connections:
start_x, start_y, start_z = self.landmarks[connection.start]
end_x, end_y, end_z = self.landmarks[connection.end]
z_average = -(start_z + end_z) / 2
new_line : Connection = Connection((start_x, start_y), (end_x, end_y), z_average, color)
connection_lines.append(new_line)
points = []
for landmarks, color in zip(
[
Hand._THUMB_LANDMARKS,
Hand._INDEX_LANDMARKS,
Hand._MIDDLE_LANDMARKS,
Hand._RING_LANDMARKS,
Hand._PINKY_LANDMARKS,
Hand._PALM_LANDMARKS
],
Hand._REGION_COLORS
):
for landmark in landmarks:
x, y, z = self.landmarks[landmark]
points.append(Landmark((x, y,), -z, color))
connection_lines.sort(key=lambda obj: obj.z)
for connection_line in connection_lines:
cv2.line(
image,
connection_line.start,
connection_line.end,
connection_line.color,
Hand._DEFAULT_CONNECTION_STYLE.thickness
)
points.sort(key=lambda obj: obj.z)
for point in points:
cv2.circle(
image,
point.pos,
Hand._DEFAULT_LANDMARK_STYLE.radius,
point.color,
-1
)
cv2.circle(
image,
point.pos,
Hand._DEFAULT_LANDMARK_STYLE.radius,
Hand._DEFAULT_LANDMARK_STYLE.stroke,
Hand._DEFAULT_LANDMARK_STYLE.thickness
)
# Display the bounding box
if bounding_box:
self.box.draw_corners(image, length=20, thickness=5, stroke=(0,0,0))
self.box.draw_corners(image, length=19, thickness=4, stroke=(255,255,255))
# Display the hand center
# Display the hand side ("Left" or "Right")
if side:
stack_text(
image,
[self.side],
(self.center[0], self.center[1] + 5),
debug_font,
debug_text_size * 2,
debug_thickness * 2,
(255,255,255),
HAlign.CENTER,
VAlign.TOP
)
# Display the hand flags and fingers up
text_lines = []
if flags:
flag_text = f"Flags: {self.flags}"
text_lines.append(flag_text)
if fingers:
text_lines.append("Fingers:")
for finger in self.fingers_up():
text_lines.append(finger)
if self.side == "Left":
stack_text(
image,
text_lines,
(0,0),
debug_font,
debug_text_size,
debug_thickness,
(255,255,255)
)
else:
stack_text(
image,
text_lines,
(width, 0),
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
h_align=HAlign.RIGHT
)
# Display the position values for each fingertip and the wrist
if tip_points:
for tip, color in zip(
[
Hand._THUMB_TIP_ID,
Hand._INDEX_TIP_ID,
Hand._MIDDLE_TIP_ID,
Hand._RING_TIP_ID,
Hand._PINKY_TIP_ID,
Hand._WRIST_ID
],
Hand._REGION_COLORS
):
b, g, r = color
# b = b // 1.5
# g = g // 1.5
# r = r // 1.5
x, y, _ = self.landmarks[tip]
stack_text(
image,
[f"{self.landmarks[tip]}"],
(x, y),
debug_font,
debug_text_size,
debug_thickness,
(b, g, r),
HAlign.CENTER,
VAlign.BOTTOM
)
if center:
center_text = f"Center: ({self.center[0]}, {self.center[1]})"
cv2.circle(
image,
self.center,
7,
(0,255,0),
-1
)
cv2.circle(
image,
self.center,
7,
(0,0,0),
2
)
stack_text(
image,
[center_text],
self.center,
debug_font,
debug_text_size * 1.5,
debug_thickness * 2,
(0,255,0),
HAlign.CENTER,
VAlign.BOTTOM,
0
)
def set_connection_style(self, stroke: None | tuple[int, int, int] | list[int, int, int]=None, thickness : None | float=None):
"""
Modifies the style of the hand connections when it is drawn.
:param stroke: The BGR color of the connections.
:param thickness: The thickness of the connector lines in pixels.
"""
if stroke is not None:
self.connection_style.stroke = stroke
if thickness is not None:
self.connection_style.thickness = int(thickness)
def set_landmarks_style(self, fill: None | tuple[int, int, int] | list[int, int, int]=None, stroke: None | tuple[int, int, int] | list[int, int, int]=None, radius: float | None=None, thickness: float | None=None):
"""
Modifies the style of the hand landmarks when drawn.
:param fill: The BGR color of the landmarks
:param stroke: The BGR color of the outline of the landmarks
:param thickness: Thickness of outline on circle
:param radius: The radius of the landmarks
"""
if fill is not None:
self.landmark_style.fill = fill
if stroke is not None:
self.landmark_style.stroke = stroke
if radius is not None:
self.landmark_style.radius = int(radius)
if thickness is not None:
self.landmark_style.thickness = int(thickness)
__init__
__init__(landmarks: list[tuple[int, int, int]], normalized_landmarks: list[tuple[float, float, float]], side: str, box: BoundingBox, image: ndarray)
Initialize the hand.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
landmarks
|
list[tuple[int, int, int]]
|
The [x,y,z] pixel coordinates of landmarks. |
required |
normalized_landmarks
|
list[tuple[float, float, float]]
|
The actual landmarks of the hand as provided by mediapipe. |
required |
side
|
str
|
The side of the hand ("Left" or "Right"). |
required |
box
|
BoundingBox
|
The bounding box of the hand. |
required |
image
|
ndarray
|
The image that was used to detect the hand. |
required |
Source code in src/pumpkinpipe/hand.py
def __init__(self, landmarks : list[tuple[int, int, int]], normalized_landmarks: list[tuple[float, float, float]], side : str, box : BoundingBox, image : np.ndarray):
"""
Initialize the hand.
:param landmarks: The [x,y,z] pixel coordinates of landmarks.
:param normalized_landmarks: The actual landmarks of the hand as provided by mediapipe.
:param side: The side of the hand ("Left" or "Right").
:param box: The bounding box of the hand.
:param image: The image that was used to detect the hand.
"""
self.landmarks : list[tuple[int, int, int]] = landmarks
self.normalized_landmarks : list[tuple[float, float, float]] = normalized_landmarks
self.side : str = side
self.thumb : tuple[int, int, int] = self.landmarks[Hand._THUMB_TIP_ID]
self.index : tuple[int, int, int] = self.landmarks[Hand._INDEX_TIP_ID]
self.middle : tuple[int, int, int] = self.landmarks[Hand._MIDDLE_TIP_ID]
self.ring : tuple[int, int, int] = self.landmarks[Hand._RING_TIP_ID]
self.pinky : tuple[int, int, int] = self.landmarks[Hand._PINKY_TIP_ID]
self.wrist : tuple[int, int, int] = self.landmarks[Hand._WRIST_ID]
self.connection_style : ConnectionStyle = ConnectionStyle()
self.landmark_style : LandmarkStyle = LandmarkStyle()
self.flags : list[int] = self.finger_flags()
self.box : BoundingBox = box
self.center : tuple[int, int] = self.box.center
self.image : np.ndarray = image
debug
debug(image: None | ndarray = None, skeleton: bool = True, bounding_box: bool = True, center: bool = True, side: bool = True, fingers: bool = True, flags: bool = True, tip_points: bool = True)
Draws the requested debug information. Defaults to all debug information.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
None | ndarray
|
The image to draw the debug information on. If image is none then the hand will draw on its self.image |
None
|
skeleton
|
bool
|
When True, draw the landmarks and connections of the hand. Hand regions are separated by color. |
True
|
bounding_box
|
bool
|
When True, draw the outer bounding box of the hand. |
True
|
center
|
bool
|
When True, draw a green circle at the center of the bounding box. Also display the value for hand.center. |
True
|
side
|
bool
|
When True, write the value for hand.side underneath the hand. |
True
|
flags
|
bool
|
When True, display the value for hand.flags (binary list) representing which fingers are up or down. |
True
|
fingers
|
bool
|
When True, display the value returned by hand.fingers_up() (a list of strings for each finger that is registered as being up). |
True
|
tip_points
|
bool
|
When True, display hand.thumb, hand.index, hand.middle, hand.ring, hand.pinky, and hand.wrist values near their corresponding fingertips. |
True
|
Source code in src/pumpkinpipe/hand.py
def debug(self, image : None | np.ndarray=None, skeleton:bool=True, bounding_box:bool=True, center:bool=True, side:bool=True, fingers:bool=True, flags:bool=True, tip_points:bool=True):
"""
Draws the requested debug information. Defaults to all debug information.
:param image: The image to draw the debug information on. If image is none then the hand will draw on its self.image
:param skeleton: When True, draw the landmarks and connections of the hand. Hand regions are separated by color.
:param bounding_box: When True, draw the outer bounding box of the hand.
:param center: When True, draw a green circle at the center of the bounding box. Also display the value for hand.center.
:param side: When True, write the value for hand.side underneath the hand.
:param flags: When True, display the value for hand.flags (binary list) representing which fingers are up or down.
:param fingers: When True, display the value returned by hand.fingers_up() (a list of strings for each finger that is registered as being up).
:param tip_points: When True, display hand.thumb, hand.index, hand.middle, hand.ring, hand.pinky, and hand.wrist values near their corresponding fingertips.
"""
if image is None:
image = self.image
# Set default values
height, width, _ = image.shape
debug_text_size = 1
debug_font = cv2.FONT_HERSHEY_PLAIN
debug_thickness = 1
# Display the hand connections and landmarks with the different regions as different colors.
if skeleton:
connection_lines = []
for connections, color in zip(
[
Hand._THUMB_CONNECTIONS,
Hand._INDEX_CONNECTIONS,
Hand._MIDDLE_CONNECTIONS,
Hand._RING_CONNECTIONS,
Hand._PINKY_CONNECTIONS,
Hand._PALM_CONNECTIONS
],
Hand._REGION_COLORS
):
for connection in connections:
start_x, start_y, start_z = self.landmarks[connection.start]
end_x, end_y, end_z = self.landmarks[connection.end]
z_average = -(start_z + end_z) / 2
new_line : Connection = Connection((start_x, start_y), (end_x, end_y), z_average, color)
connection_lines.append(new_line)
points = []
for landmarks, color in zip(
[
Hand._THUMB_LANDMARKS,
Hand._INDEX_LANDMARKS,
Hand._MIDDLE_LANDMARKS,
Hand._RING_LANDMARKS,
Hand._PINKY_LANDMARKS,
Hand._PALM_LANDMARKS
],
Hand._REGION_COLORS
):
for landmark in landmarks:
x, y, z = self.landmarks[landmark]
points.append(Landmark((x, y,), -z, color))
connection_lines.sort(key=lambda obj: obj.z)
for connection_line in connection_lines:
cv2.line(
image,
connection_line.start,
connection_line.end,
connection_line.color,
Hand._DEFAULT_CONNECTION_STYLE.thickness
)
points.sort(key=lambda obj: obj.z)
for point in points:
cv2.circle(
image,
point.pos,
Hand._DEFAULT_LANDMARK_STYLE.radius,
point.color,
-1
)
cv2.circle(
image,
point.pos,
Hand._DEFAULT_LANDMARK_STYLE.radius,
Hand._DEFAULT_LANDMARK_STYLE.stroke,
Hand._DEFAULT_LANDMARK_STYLE.thickness
)
# Display the bounding box
if bounding_box:
self.box.draw_corners(image, length=20, thickness=5, stroke=(0,0,0))
self.box.draw_corners(image, length=19, thickness=4, stroke=(255,255,255))
# Display the hand center
# Display the hand side ("Left" or "Right")
if side:
stack_text(
image,
[self.side],
(self.center[0], self.center[1] + 5),
debug_font,
debug_text_size * 2,
debug_thickness * 2,
(255,255,255),
HAlign.CENTER,
VAlign.TOP
)
# Display the hand flags and fingers up
text_lines = []
if flags:
flag_text = f"Flags: {self.flags}"
text_lines.append(flag_text)
if fingers:
text_lines.append("Fingers:")
for finger in self.fingers_up():
text_lines.append(finger)
if self.side == "Left":
stack_text(
image,
text_lines,
(0,0),
debug_font,
debug_text_size,
debug_thickness,
(255,255,255)
)
else:
stack_text(
image,
text_lines,
(width, 0),
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
h_align=HAlign.RIGHT
)
# Display the position values for each fingertip and the wrist
if tip_points:
for tip, color in zip(
[
Hand._THUMB_TIP_ID,
Hand._INDEX_TIP_ID,
Hand._MIDDLE_TIP_ID,
Hand._RING_TIP_ID,
Hand._PINKY_TIP_ID,
Hand._WRIST_ID
],
Hand._REGION_COLORS
):
b, g, r = color
# b = b // 1.5
# g = g // 1.5
# r = r // 1.5
x, y, _ = self.landmarks[tip]
stack_text(
image,
[f"{self.landmarks[tip]}"],
(x, y),
debug_font,
debug_text_size,
debug_thickness,
(b, g, r),
HAlign.CENTER,
VAlign.BOTTOM
)
if center:
center_text = f"Center: ({self.center[0]}, {self.center[1]})"
cv2.circle(
image,
self.center,
7,
(0,255,0),
-1
)
cv2.circle(
image,
self.center,
7,
(0,0,0),
2
)
stack_text(
image,
[center_text],
self.center,
debug_font,
debug_text_size * 1.5,
debug_thickness * 2,
(0,255,0),
HAlign.CENTER,
VAlign.BOTTOM,
0
)
draw
draw(image: None | ndarray = None)
Draws the hand skeleton on the specified image. Currently, this library uses a custom way to draw while drawing_utils.py remains unimplemented in the official mediapipe release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
None | ndarray
|
The target image for the drawing. If none, it will draw on the hands self.image |
None
|
Source code in src/pumpkinpipe/hand.py
def draw(self, image : None | np.ndarray=None):
"""
Draws the hand skeleton on the specified image. Currently, this library uses a custom way to draw while drawing_utils.py remains unimplemented in the official mediapipe release.
:param image: The target image for the drawing. If none, it will draw on the hands self.image
"""
if image is None:
image = self.image
for connection in Hand._CONNECTIONS:
start_x, start_y, _ = self.landmarks[connection.start]
end_x, end_y, _ = self.landmarks[connection.end]
cv2.line(
image,
(start_x, start_y),
(end_x, end_y),
self.connection_style.stroke,
self.connection_style.thickness
)
for landmark in self.landmarks:
x, y, _ = landmark
# Filled circle
cv2.circle(
image,
(x,y),
self.landmark_style.radius,
self.landmark_style.fill,
-1
)
# Outline
cv2.circle(
image,
(x, y),
self.landmark_style.radius,
self.landmark_style.stroke,
self.landmark_style.thickness
)
finger_flags
finger_flags() -> list[int]
Finds which fingers are up and returns them as a binary list in the order of thumb, index, middle, ring, pinky.
Returns:
| Type | Description |
|---|---|
list[int]
|
A list of binary values representing whether a finger is up or down |
Source code in src/pumpkinpipe/hand.py
def finger_flags(self) -> list[int]:
"""
Finds which fingers are up and returns them as a binary list in the order of thumb, index, middle, ring, pinky.
:return: A list of binary values representing whether a finger is up or down
"""
# Initialize empty list for finger flags
fingers : list[int] = []
# Distance for thumb to be considered open
distance_threshold = 0.3
# Vector math to determine whether landmarks 1→2 are closely aligned with 2→3
angle_a = angle_3d(self.landmarks[1], self.landmarks[2])
angle_b = angle_3d(self.landmarks[2], self.landmarks[3])
thumb_angle_distance = math.dist(angle_a, angle_b)
# Append thumb value to fingers
if thumb_angle_distance < distance_threshold:
fingers.append(1)
else:
fingers.append(0)
# Landmark indices for index_tip, middle_tip, ring_tip, and pinky_tip
finger_indices = [Hand._INDEX_TIP_ID, Hand._MIDDLE_TIP_ID, Hand._RING_TIP_ID, Hand._PINKY_TIP_ID]
# Compare the distance between the tip and the wrist to the distance between the knuckle and the wrist
for index in finger_indices:
tip_distance = math.dist(self.landmarks[index], self.landmarks[Hand._WRIST_ID])
knuckle_distance = math.dist(self.landmarks[index - 2], self.landmarks[Hand._WRIST_ID])
# Append each finger value to fingers
if tip_distance > knuckle_distance:
fingers.append(1)
else:
fingers.append(0)
# Return list of flags
return fingers
fingers_down
fingers_down() -> list[str]
Provides a list of the fingers that are down in English.
Returns:
| Type | Description |
|---|---|
list[str]
|
A list of finger names. |
Source code in src/pumpkinpipe/hand.py
def fingers_down(self) -> list[str]:
"""
Provides a list of the fingers that are down in English.
:return: A list of finger names.
"""
# Initialize empty list for finger flags
fingers = []
finger_names = ["Thumb", "Index", "Middle", "Ring", "Pinky"]
for flag, name in zip(self.flags, finger_names):
# Append fingers
if flag < 1:
fingers.append(name)
# Return list of fingers that are up
return fingers
fingers_up
fingers_up() -> list[str]
Provides a list of the fingers that are up in English.
Returns:
| Type | Description |
|---|---|
list[str]
|
A list of finger names. |
Source code in src/pumpkinpipe/hand.py
def fingers_up(self) -> list[str]:
"""
Provides a list of the fingers that are up in English.
:return: A list of finger names.
"""
# Initialize empty list for finger flags
fingers = []
finger_names = ["Thumb", "Index", "Middle", "Ring", "Pinky"]
for flag, name in zip(self.flags, finger_names):
# Append fingers
if flag > 0:
fingers.append(name)
# Return list of fingers that are up
return fingers
landmark_distance
landmark_distance(landmark_index_1: int, landmark_index_2: int, image: None | ndarray = None, draw: bool = False) -> float
Finds the distance in pixels between 2 specified landmarks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
landmark_index_1
|
int
|
The landmark index for the first point |
required |
landmark_index_2
|
int
|
The landmark index for the second point |
required |
image
|
None | ndarray
|
If not None, draws a line between the 2 points on the specified image |
None
|
draw
|
bool
|
If True, currently does nothing. Future implementations will draw the line. |
False
|
Returns:
| Type | Description |
|---|---|
float
|
The distance between 2 points |
Source code in src/pumpkinpipe/hand.py
def landmark_distance(self, landmark_index_1 : int, landmark_index_2 : int, image : None | np.ndarray=None, draw : bool=False) -> float:
"""
Finds the distance in pixels between 2 specified landmarks.
:param landmark_index_1: The landmark index for the first point
:param landmark_index_2: The landmark index for the second point
:param image: If not None, draws a line between the 2 points on the specified image
:param draw: If True, currently does nothing. Future implementations will draw the line.
:return: The distance between 2 points
"""
landmark_1 = x_1, y_1, z_1 = self.landmarks[landmark_index_1]
landmark_2 = x_2, y_2, z_2 = self.landmarks[landmark_index_2]
point_1 = x_1, y_1
point_2 = x_2, y_2
distance : float = math.dist(landmark_1, landmark_2)
if draw:
if image is None:
image = self.image
cv2.line(
image,
point_1,
point_2,
(255,0,0),
4
)
cv2.circle(
image,
point_1,
8,
(255, 0, 0),
-1
)
cv2.circle(
image,
point_2,
8,
(255, 0, 0),
-1
)
cv2.circle(
image,
point_1,
8,
(127, 0, 0),
1
)
cv2.circle(
image,
point_2,
8,
(127, 0, 0),
1
)
return distance
set_connection_style
set_connection_style(stroke: None | tuple[int, int, int] | list[int, int, int] = None, thickness: None | float = None)
Modifies the style of the hand connections when it is drawn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stroke
|
None | tuple[int, int, int] | list[int, int, int]
|
The BGR color of the connections. |
None
|
thickness
|
None | float
|
The thickness of the connector lines in pixels. |
None
|
Source code in src/pumpkinpipe/hand.py
def set_connection_style(self, stroke: None | tuple[int, int, int] | list[int, int, int]=None, thickness : None | float=None):
"""
Modifies the style of the hand connections when it is drawn.
:param stroke: The BGR color of the connections.
:param thickness: The thickness of the connector lines in pixels.
"""
if stroke is not None:
self.connection_style.stroke = stroke
if thickness is not None:
self.connection_style.thickness = int(thickness)
set_landmarks_style
set_landmarks_style(fill: None | tuple[int, int, int] | list[int, int, int] = None, stroke: None | tuple[int, int, int] | list[int, int, int] = None, radius: float | None = None, thickness: float | None = None)
Modifies the style of the hand landmarks when drawn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fill
|
None | tuple[int, int, int] | list[int, int, int]
|
The BGR color of the landmarks |
None
|
stroke
|
None | tuple[int, int, int] | list[int, int, int]
|
The BGR color of the outline of the landmarks |
None
|
thickness
|
float | None
|
Thickness of outline on circle |
None
|
radius
|
float | None
|
The radius of the landmarks |
None
|
Source code in src/pumpkinpipe/hand.py
def set_landmarks_style(self, fill: None | tuple[int, int, int] | list[int, int, int]=None, stroke: None | tuple[int, int, int] | list[int, int, int]=None, radius: float | None=None, thickness: float | None=None):
"""
Modifies the style of the hand landmarks when drawn.
:param fill: The BGR color of the landmarks
:param stroke: The BGR color of the outline of the landmarks
:param thickness: Thickness of outline on circle
:param radius: The radius of the landmarks
"""
if fill is not None:
self.landmark_style.fill = fill
if stroke is not None:
self.landmark_style.stroke = stroke
if radius is not None:
self.landmark_style.radius = int(radius)
if thickness is not None:
self.landmark_style.thickness = int(thickness)
HandDetector
Class for setting up mediapipe and finding hand data.
Attributes:
| Name | Type | Description |
|---|---|---|
timestamp_ms |
Variable for tracking time between frames. |
|
frame_rate |
Desired frame rate. Set to 30. |
|
landmarker |
Pipeline to detect hand landmarks. |
Source code in src/pumpkinpipe/hand.py
class HandDetector:
"""
Class for setting up mediapipe and finding hand data.
:ivar timestamp_ms: Variable for tracking time between frames.
:ivar frame_rate: Desired frame rate. Set to 30.
:ivar landmarker: Pipeline to detect hand landmarks.
"""
def __init__(self, max_hands:int=2):
"""
Initialize the hand detector.
:param max_hands: The maximum number of hands for the detector to try and process.
"""
with get_model_path("hand_landmarker.task") as model_path:
options = vision.HandLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=model_path
),
num_hands=max_hands,
running_mode=vision.RunningMode.VIDEO
)
self.landmarker = vision.HandLandmarker.create_from_options(
options
)
self.timestamp_ms = 0
self.frame_rate = 30
def find_hands(self, image : np.ndarray, flip : bool=False) -> list[Hand]:
"""
Detect hands and add them to a list.
:param image: The image to detect hands in.
:param flip: When flip is True, handedness is reversed. "Left" becomes "Right" and vice versa.
:return: A list of detected hands.
"""
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
height, width, channels = image.shape
mp_image = mp.Image(
image_format=mp.ImageFormat.SRGB,
data=image_rgb
)
result = self.landmarker.detect_for_video(
mp_image,
self.timestamp_ms
)
self.timestamp_ms += int(1000/self.frame_rate)
if not result.hand_landmarks:
return []
hands : list[Hand]= []
for landmarks, handedness in zip(
result.hand_landmarks,
result.handedness
):
pixel_landmarks : list[tuple[int, int, int]] = []
normalized_landmarks : list[tuple[float, float, float]] = []
x_list = []
y_list = []
for lm in landmarks:
normalized_landmarks.append((lm.x, lm.y, lm.z))
px_lm = (int(lm.x * width), int(lm.y * height), int(lm.z * width))
pixel_landmarks.append(px_lm)
x_list.append(int(lm.x * width))
y_list.append(int(lm.y * height))
bounding_box = BoundingBox(
(min(x_list) - 10, min(y_list) - 10),
(max(x_list) + 10, max(y_list) + 10)
)
category = handedness[0] # usually one entry
if flip:
side = category.category_name # "Left" or "Right"
else:
if category.category_name == "Left":
side = "Right"
else:
side = "Left"
hand = Hand(pixel_landmarks, normalized_landmarks, side, bounding_box, image)
hands.append(hand)
return hands
__init__
__init__(max_hands: int = 2)
Initialize the hand detector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_hands
|
int
|
The maximum number of hands for the detector to try and process. |
2
|
Source code in src/pumpkinpipe/hand.py
def __init__(self, max_hands:int=2):
"""
Initialize the hand detector.
:param max_hands: The maximum number of hands for the detector to try and process.
"""
with get_model_path("hand_landmarker.task") as model_path:
options = vision.HandLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=model_path
),
num_hands=max_hands,
running_mode=vision.RunningMode.VIDEO
)
self.landmarker = vision.HandLandmarker.create_from_options(
options
)
self.timestamp_ms = 0
self.frame_rate = 30
find_hands
find_hands(image: ndarray, flip: bool = False) -> list[Hand]
Detect hands and add them to a list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
ndarray
|
The image to detect hands in. |
required |
flip
|
bool
|
When flip is True, handedness is reversed. "Left" becomes "Right" and vice versa. |
False
|
Returns:
| Type | Description |
|---|---|
list[Hand]
|
A list of detected hands. |
Source code in src/pumpkinpipe/hand.py
def find_hands(self, image : np.ndarray, flip : bool=False) -> list[Hand]:
"""
Detect hands and add them to a list.
:param image: The image to detect hands in.
:param flip: When flip is True, handedness is reversed. "Left" becomes "Right" and vice versa.
:return: A list of detected hands.
"""
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
height, width, channels = image.shape
mp_image = mp.Image(
image_format=mp.ImageFormat.SRGB,
data=image_rgb
)
result = self.landmarker.detect_for_video(
mp_image,
self.timestamp_ms
)
self.timestamp_ms += int(1000/self.frame_rate)
if not result.hand_landmarks:
return []
hands : list[Hand]= []
for landmarks, handedness in zip(
result.hand_landmarks,
result.handedness
):
pixel_landmarks : list[tuple[int, int, int]] = []
normalized_landmarks : list[tuple[float, float, float]] = []
x_list = []
y_list = []
for lm in landmarks:
normalized_landmarks.append((lm.x, lm.y, lm.z))
px_lm = (int(lm.x * width), int(lm.y * height), int(lm.z * width))
pixel_landmarks.append(px_lm)
x_list.append(int(lm.x * width))
y_list.append(int(lm.y * height))
bounding_box = BoundingBox(
(min(x_list) - 10, min(y_list) - 10),
(max(x_list) + 10, max(y_list) + 10)
)
category = handedness[0] # usually one entry
if flip:
side = category.category_name # "Left" or "Right"
else:
if category.category_name == "Left":
side = "Right"
else:
side = "Left"
hand = Hand(pixel_landmarks, normalized_landmarks, side, bounding_box, image)
hands.append(hand)
return hands
main
main()
Test script for the Hand Detection module.
Source code in src/pumpkinpipe/hand.py
def main():
"""
Test script for the Hand Detection module.
"""
# Initialize the webcam to capture video
cap = cv2.VideoCapture(0)
# Initialize the HandDetector class with the given parameters
hand_detector = HandDetector(2)
# Continuously get frames from the webcam
while True:
# Capture each frame from the webcam
# 'success' will be True if the frame is successfully captured, 'img' will contain the frame
success, img = cap.read()
img = cv2.flip(img, 1)
# Find hands in the current frame
hands = hand_detector.find_hands(img)
# Methods for each hand
for hand in hands:
hand.debug()
# Display the image in a window
cv2.imshow("Image", img)
# Close the window if user presses 'q' or the X button
if cv2.waitKey(1) & 0xFF == ord('q'):
break
if cv2.getWindowProperty("Image", cv2.WND_PROP_VISIBLE) < 1:
break
cap.release()
cv2.destroyAllWindows()