ImagePRO-Python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ImagePRO/__init__.py +0 -0
- ImagePRO/human_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +129 -0
- ImagePRO/human_analysis/body_analysis/hand_tracking.py +157 -0
- ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
- ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +152 -0
- ImagePRO/human_analysis/face_analysis/face_detection.py +84 -0
- ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +172 -0
- ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +148 -0
- ImagePRO/pre_processing/__init__.py +0 -0
- ImagePRO/pre_processing/blur.py +130 -0
- ImagePRO/pre_processing/contrast.py +105 -0
- ImagePRO/pre_processing/crop.py +51 -0
- ImagePRO/pre_processing/grayscale.py +27 -0
- ImagePRO/pre_processing/resize.py +43 -0
- ImagePRO/pre_processing/rotate.py +94 -0
- ImagePRO/pre_processing/sharpen.py +74 -0
- imagepro_python-0.1.0.dist-info/METADATA +17 -0
- imagepro_python-0.1.0.dist-info/RECORD +23 -0
- imagepro_python-0.1.0.dist-info/WHEEL +5 -0
- imagepro_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- imagepro_python-0.1.0.dist-info/top_level.txt +1 -0
ImagePRO/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import mediapipe as mp
|
|
6
|
+
|
|
7
|
+
# Add parent directory to import custom modules
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
mp_pose = mp.solutions.pose
|
|
14
|
+
|
|
15
|
+
def detect_body_pose(
|
|
16
|
+
model_accuracy=0.7,
|
|
17
|
+
landmarks_idx=None,
|
|
18
|
+
image_path=None,
|
|
19
|
+
np_image=None,
|
|
20
|
+
result_path=None,
|
|
21
|
+
pose_obj=None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Detects body landmarks from an image using MediaPipe Pose.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
model_accuracy (float): Confidence threshold (0.0 to 1.0).
|
|
28
|
+
landmarks_idx (list[int] | None): Indices to extract (default: all 33).
|
|
29
|
+
image_path (str | None): Path to image file.
|
|
30
|
+
np_image (np.ndarray | None): Already loaded image (used if image_path is None).
|
|
31
|
+
result_path (str | None): Path to save image (.jpg) or coordinates (.csv).
|
|
32
|
+
pose_obj (mp.solutions.pose.Pose | None): Optional pre-initialized pose model.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str | tuple[np.ndarray, list]: Output message or annotated image and landmark list.
|
|
36
|
+
"""
|
|
37
|
+
if landmarks_idx is not None and not isinstance(landmarks_idx, list):
|
|
38
|
+
raise TypeError("'landmarks_idx' must be a list or None.")
|
|
39
|
+
|
|
40
|
+
if result_path:
|
|
41
|
+
if not isinstance(result_path, str):
|
|
42
|
+
raise TypeError("'result_path' must be a string.")
|
|
43
|
+
if not (result_path.endswith('.jpg') or result_path.endswith('.csv')):
|
|
44
|
+
raise ValueError("Supported extensions: '.jpg', '.csv'.")
|
|
45
|
+
|
|
46
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
47
|
+
|
|
48
|
+
if landmarks_idx is None:
|
|
49
|
+
landmarks_idx = list(range(33))
|
|
50
|
+
|
|
51
|
+
if pose_obj is None:
|
|
52
|
+
pose_obj = mp_pose.Pose(
|
|
53
|
+
min_detection_confidence=model_accuracy,
|
|
54
|
+
static_image_mode=True
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
image_rgb = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
58
|
+
result = pose_obj.process(image_rgb)
|
|
59
|
+
|
|
60
|
+
annotated_image = np_image.copy()
|
|
61
|
+
all_landmarks = []
|
|
62
|
+
|
|
63
|
+
if result.pose_landmarks:
|
|
64
|
+
if result_path is None or result_path.endswith('.jpg'):
|
|
65
|
+
if len(landmarks_idx) == 33:
|
|
66
|
+
mp.solutions.drawing_utils.draw_landmarks(
|
|
67
|
+
annotated_image,
|
|
68
|
+
result.pose_landmarks,
|
|
69
|
+
mp_pose.POSE_CONNECTIONS,
|
|
70
|
+
landmark_drawing_spec=mp.solutions.drawing_styles.get_default_pose_landmarks_style()
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
h, w, _ = annotated_image.shape
|
|
74
|
+
for idx in landmarks_idx:
|
|
75
|
+
lm = result.pose_landmarks.landmark[idx]
|
|
76
|
+
x, y = int(w * lm.x), int(h * lm.y)
|
|
77
|
+
cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
|
|
78
|
+
|
|
79
|
+
if result_path is None or result_path.endswith('.csv'):
|
|
80
|
+
for idx in landmarks_idx:
|
|
81
|
+
lm = result.pose_landmarks.landmark[idx]
|
|
82
|
+
all_landmarks.append([idx, lm.x, lm.y, lm.z])
|
|
83
|
+
|
|
84
|
+
if result_path:
|
|
85
|
+
if result_path.endswith('.jpg'):
|
|
86
|
+
return IOHandler.save_image(annotated_image, result_path)
|
|
87
|
+
elif result_path.endswith('.csv'):
|
|
88
|
+
return IOHandler.save_csv(all_landmarks, result_path)
|
|
89
|
+
else:
|
|
90
|
+
return annotated_image, all_landmarks
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def detect_body_pose_live():
|
|
94
|
+
"""
|
|
95
|
+
Starts webcam and shows real-time body pose detection.
|
|
96
|
+
Press ESC to exit.
|
|
97
|
+
"""
|
|
98
|
+
cap = cv2.VideoCapture(0)
|
|
99
|
+
if not cap.isOpened():
|
|
100
|
+
raise RuntimeError("Failed to open webcam.")
|
|
101
|
+
|
|
102
|
+
pose_obj = mp_pose.Pose(
|
|
103
|
+
min_detection_confidence=0.7,
|
|
104
|
+
static_image_mode=True
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
while True:
|
|
109
|
+
success, frame = cap.read()
|
|
110
|
+
if not success:
|
|
111
|
+
print("Ignoring empty camera frame.")
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
annotated_img, _ = detect_body_pose(np_image=frame, pose_obj=pose_obj)
|
|
116
|
+
except ValueError:
|
|
117
|
+
annotated_img = frame
|
|
118
|
+
|
|
119
|
+
cv2.imshow('ImagePRO - Live Body Pose Detection', annotated_img)
|
|
120
|
+
|
|
121
|
+
if cv2.waitKey(5) & 0xFF == 27:
|
|
122
|
+
break
|
|
123
|
+
finally:
|
|
124
|
+
cap.release()
|
|
125
|
+
cv2.destroyAllWindows()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
detect_body_pose_live()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import mediapipe as mp
|
|
6
|
+
|
|
7
|
+
# Add parent directory to import custom modules
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
mp_hands = mp.solutions.hands
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_hands(
|
|
17
|
+
max_hands=2,
|
|
18
|
+
min_confidence=0.7,
|
|
19
|
+
landmarks_idx=None,
|
|
20
|
+
image_path=None,
|
|
21
|
+
np_image=None,
|
|
22
|
+
result_path=None,
|
|
23
|
+
hands_obj=None
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Detects hand landmarks in an image using MediaPipe.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
max_hands (int): Max number of hands to detect.
|
|
30
|
+
min_confidence (float): Minimum confidence for detection.
|
|
31
|
+
landmarks_idx (list[int] | None): Landmark indices to extract (default: all 21).
|
|
32
|
+
image_path (str | None): Image file path.
|
|
33
|
+
np_image (np.ndarray | None): Preloaded image array.
|
|
34
|
+
result_path (str | None): Path to save output image (.jpg) or landmarks (.csv).
|
|
35
|
+
hands_obj (mp.solutions.hands.Hands | None): Optional pre-initialized model.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
np.ndarray | list: Annotated image and landmarks list, or saved file confirmation.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
41
|
+
raise ValueError("'max_hands' must be a positive integer.")
|
|
42
|
+
|
|
43
|
+
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
44
|
+
raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
|
|
45
|
+
|
|
46
|
+
if landmarks_idx is not None and not isinstance(landmarks_idx, list):
|
|
47
|
+
raise TypeError("'landmarks_idx' must be a list or None.")
|
|
48
|
+
|
|
49
|
+
if result_path:
|
|
50
|
+
if not isinstance(result_path, str):
|
|
51
|
+
raise TypeError("'result_path' must be a string.")
|
|
52
|
+
if not (result_path.endswith('.jpg') or result_path.endswith('.csv')):
|
|
53
|
+
raise ValueError("Only '.jpg' and '.csv' extensions are supported.")
|
|
54
|
+
|
|
55
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
56
|
+
|
|
57
|
+
if hands_obj is None:
|
|
58
|
+
hands_obj = mp_hands.Hands(
|
|
59
|
+
min_detection_confidence=min_confidence,
|
|
60
|
+
max_num_hands=max_hands,
|
|
61
|
+
static_image_mode=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if landmarks_idx is None:
|
|
65
|
+
landmarks_idx = list(range(21))
|
|
66
|
+
|
|
67
|
+
rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
68
|
+
results = hands_obj.process(rgb_image)
|
|
69
|
+
|
|
70
|
+
annotated_image = np_image.copy()
|
|
71
|
+
all_landmarks = []
|
|
72
|
+
|
|
73
|
+
if results.multi_hand_landmarks:
|
|
74
|
+
for hand_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
|
|
75
|
+
if result_path is None or result_path.endswith('.jpg'):
|
|
76
|
+
if len(landmarks_idx) == 21:
|
|
77
|
+
mp.solutions.drawing_utils.draw_landmarks(
|
|
78
|
+
image=annotated_image,
|
|
79
|
+
landmark_list=hand_landmarks,
|
|
80
|
+
connections=mp_hands.HAND_CONNECTIONS,
|
|
81
|
+
landmark_drawing_spec=mp.solutions.drawing_styles.get_default_hand_landmarks_style(),
|
|
82
|
+
connection_drawing_spec=mp.solutions.drawing_styles.get_default_hand_connections_style()
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
h, w, _ = annotated_image.shape
|
|
86
|
+
for idx in landmarks_idx:
|
|
87
|
+
lm = hand_landmarks.landmark[idx]
|
|
88
|
+
x, y = int(w * lm.x), int(h * lm.y)
|
|
89
|
+
cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
|
|
90
|
+
|
|
91
|
+
if result_path is None or result_path.endswith('.csv'):
|
|
92
|
+
hand_data = []
|
|
93
|
+
for idx in landmarks_idx:
|
|
94
|
+
lm = hand_landmarks.landmark[idx]
|
|
95
|
+
hand_data.append([hand_id, idx, lm.x, lm.y, lm.z])
|
|
96
|
+
all_landmarks.append(hand_data)
|
|
97
|
+
else:
|
|
98
|
+
return np_image, []
|
|
99
|
+
|
|
100
|
+
if result_path:
|
|
101
|
+
if result_path.endswith('.jpg'):
|
|
102
|
+
return IOHandler.save_image(annotated_image, result_path)
|
|
103
|
+
elif result_path.endswith('.csv'):
|
|
104
|
+
flat_landmarks = [item for sublist in all_landmarks for item in sublist]
|
|
105
|
+
return IOHandler.save_csv(flat_landmarks, result_path)
|
|
106
|
+
else:
|
|
107
|
+
return annotated_image, all_landmarks
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def detect_hands_live(max_hands=2, min_confidence=0.7):
|
|
111
|
+
"""
|
|
112
|
+
Real-time hand detection via webcam.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
max_hands (int): Maximum hands to detect.
|
|
116
|
+
min_confidence (float): Detection confidence threshold.
|
|
117
|
+
"""
|
|
118
|
+
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
119
|
+
raise ValueError("'max_hands' must be a positive integer.")
|
|
120
|
+
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
121
|
+
raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
|
|
122
|
+
|
|
123
|
+
cap = cv2.VideoCapture(0)
|
|
124
|
+
if not cap.isOpened():
|
|
125
|
+
raise RuntimeError("Failed to open webcam.")
|
|
126
|
+
|
|
127
|
+
hands_obj = mp_hands.Hands(
|
|
128
|
+
min_detection_confidence=min_confidence,
|
|
129
|
+
max_num_hands=max_hands,
|
|
130
|
+
static_image_mode=True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
while True:
|
|
135
|
+
success, frame = cap.read()
|
|
136
|
+
if not success:
|
|
137
|
+
print("Ignoring empty camera frame.")
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
annotated_img, _ = detect_hands(
|
|
141
|
+
max_hands=max_hands,
|
|
142
|
+
min_confidence=min_confidence,
|
|
143
|
+
np_image=frame,
|
|
144
|
+
hands_obj=hands_obj
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
cv2.imshow('Live hand detector - ImagePRO', annotated_img)
|
|
148
|
+
|
|
149
|
+
if cv2.waitKey(5) & 0xFF == 27:
|
|
150
|
+
break
|
|
151
|
+
finally:
|
|
152
|
+
cap.release()
|
|
153
|
+
cv2.destroyAllWindows()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
detect_hands_live(max_hands=2)
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import mediapipe as mp
|
|
6
|
+
|
|
7
|
+
# Add parent directory to sys.path
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
|
|
13
|
+
|
|
14
|
+
mp_face_mesh = mp.solutions.face_mesh
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def analyze_eye_status(
|
|
18
|
+
min_confidence=0.7,
|
|
19
|
+
image_path=None,
|
|
20
|
+
np_image=None,
|
|
21
|
+
face_mesh_obj=None,
|
|
22
|
+
threshold=0.2
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Analyze eye open/closed status using Eye Aspect Ratio (EAR).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
min_confidence (float): Minimum confidence for FaceMesh detection.
|
|
29
|
+
image_path (str, optional): Path to input image.
|
|
30
|
+
np_image (np.ndarray, optional): Image array.
|
|
31
|
+
face_mesh_obj (FaceMesh, optional): Reusable instance of FaceMesh.
|
|
32
|
+
threshold (float): EAR threshold below which eye is considered closed.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
bool: True if eye is open, False if closed.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: If landmarks are not detected or inputs are invalid.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
41
|
+
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
42
|
+
|
|
43
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
44
|
+
h, w = np_image.shape[:2]
|
|
45
|
+
|
|
46
|
+
if face_mesh_obj is None:
|
|
47
|
+
face_mesh_obj = mp_face_mesh.FaceMesh(
|
|
48
|
+
min_detection_confidence=min_confidence,
|
|
49
|
+
refine_landmarks=True,
|
|
50
|
+
static_image_mode=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Landmark indices for right eye (from MediaPipe's model)
|
|
54
|
+
# 386 (top eyelid), 374 (bottom eyelid), 263 (outer corner), 362 (inner corner)
|
|
55
|
+
indices = [386, 374, 263, 362]
|
|
56
|
+
|
|
57
|
+
_, landmarks = analyze_face_mesh(
|
|
58
|
+
max_faces=1,
|
|
59
|
+
min_confidence=min_confidence,
|
|
60
|
+
landmarks_idx=indices,
|
|
61
|
+
np_image=np_image,
|
|
62
|
+
face_mesh_obj=face_mesh_obj
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not landmarks:
|
|
66
|
+
raise ValueError("No face landmarks detected.")
|
|
67
|
+
|
|
68
|
+
eye_points = {lm[1]: lm for lm in landmarks[0]}
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
top_y = eye_points[386][3] * h
|
|
72
|
+
bottom_y = eye_points[374][3] * h
|
|
73
|
+
left_x = eye_points[263][2] * w
|
|
74
|
+
right_x = eye_points[362][2] * w
|
|
75
|
+
except KeyError:
|
|
76
|
+
raise ValueError("Missing necessary eye landmarks.")
|
|
77
|
+
|
|
78
|
+
vertical_dist = abs(bottom_y - top_y)
|
|
79
|
+
horizontal_dist = abs(right_x - left_x)
|
|
80
|
+
|
|
81
|
+
if horizontal_dist == 0:
|
|
82
|
+
return False # avoid division by zero
|
|
83
|
+
|
|
84
|
+
ear = vertical_dist / horizontal_dist
|
|
85
|
+
return ear > threshold
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
|
|
89
|
+
"""
|
|
90
|
+
Perform live eye open/closed detection using webcam input.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
min_confidence (float): Minimum confidence for detection.
|
|
94
|
+
threshold (float): EAR threshold to consider eyes open.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If confidence value is invalid.
|
|
98
|
+
RuntimeError: If webcam cannot be accessed.
|
|
99
|
+
"""
|
|
100
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
101
|
+
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
102
|
+
|
|
103
|
+
cap = cv2.VideoCapture(0)
|
|
104
|
+
if not cap.isOpened():
|
|
105
|
+
raise RuntimeError("Cannot access webcam.")
|
|
106
|
+
|
|
107
|
+
face_mesh = mp_face_mesh.FaceMesh(
|
|
108
|
+
max_num_faces=1,
|
|
109
|
+
min_detection_confidence=min_confidence,
|
|
110
|
+
refine_landmarks=True,
|
|
111
|
+
static_image_mode=True
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
while True:
|
|
116
|
+
success, frame = cap.read()
|
|
117
|
+
if not success:
|
|
118
|
+
print("Skipping empty frame.")
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
is_open = analyze_eye_status(
|
|
123
|
+
min_confidence=min_confidence,
|
|
124
|
+
np_image=frame,
|
|
125
|
+
face_mesh_obj=face_mesh,
|
|
126
|
+
threshold=threshold
|
|
127
|
+
)
|
|
128
|
+
status = "Open" if is_open else "Closed"
|
|
129
|
+
except ValueError:
|
|
130
|
+
status = "No face"
|
|
131
|
+
|
|
132
|
+
cv2.putText(
|
|
133
|
+
frame,
|
|
134
|
+
f"Eye: {status}",
|
|
135
|
+
(10, 40),
|
|
136
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
137
|
+
0.7,
|
|
138
|
+
(0, 255, 0) if status == "Open" else (0, 0, 255),
|
|
139
|
+
2
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
cv2.imshow("ImagePRO - Eye Status (ESC to Exit)", frame)
|
|
143
|
+
if cv2.waitKey(5) & 0xFF == 27:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
finally:
|
|
147
|
+
cap.release()
|
|
148
|
+
cv2.destroyAllWindows()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
analyze_eye_status_live()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# Add parent directory to sys.path for custom module imports
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_faces(
|
|
16
|
+
max_faces=1,
|
|
17
|
+
min_confidence=0.7,
|
|
18
|
+
image_path=None,
|
|
19
|
+
np_image=None,
|
|
20
|
+
result_path=None,
|
|
21
|
+
face_mesh_obj=None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Detect and crop face regions using MediaPipe face landmarks.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
max_faces (int): Maximum number of faces to detect.
|
|
28
|
+
min_confidence (float): Confidence threshold (0–1).
|
|
29
|
+
image_path (str, optional): Path to input image.
|
|
30
|
+
np_image (np.ndarray, optional): Image array.
|
|
31
|
+
result_path (str, optional): Save path for cropped face(s).
|
|
32
|
+
face_mesh_obj (FaceMesh, optional): Reusable MediaPipe instance.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str or list[np.ndarray]: Confirmation message if saved, else list of cropped face images.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError, ValueError: On invalid inputs or if no faces detected.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
41
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
42
|
+
|
|
43
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
44
|
+
raise ValueError("'min_confidence' must be a float between 0 and 1.")
|
|
45
|
+
|
|
46
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
47
|
+
height, width = np_image.shape[:2]
|
|
48
|
+
|
|
49
|
+
# Selected face outline landmark indices (based on MediaPipe's 468-point model)
|
|
50
|
+
face_outline_indices = [
|
|
51
|
+
10, 338, 297, 332, 284, 251, 389, 356, 454, 323,
|
|
52
|
+
361, 288, 397, 365, 379, 378, 400, 377, 152, 148,
|
|
53
|
+
176, 149, 150, 136, 164, 163, 153, 157
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Run face mesh analysis
|
|
57
|
+
_, raw_landmarks = analyze_face_mesh(
|
|
58
|
+
max_faces=max_faces,
|
|
59
|
+
min_confidence=min_confidence,
|
|
60
|
+
landmarks_idx=face_outline_indices,
|
|
61
|
+
np_image=np_image,
|
|
62
|
+
face_mesh_obj=face_mesh_obj
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not raw_landmarks:
|
|
66
|
+
raise ValueError("No face landmarks detected in the input image.")
|
|
67
|
+
|
|
68
|
+
# Convert normalized landmark coordinates to pixel positions
|
|
69
|
+
all_polygons = []
|
|
70
|
+
for face in raw_landmarks:
|
|
71
|
+
polygon = [
|
|
72
|
+
(int(x * width), int(y * height))
|
|
73
|
+
for _, _, x, y, _ in face
|
|
74
|
+
]
|
|
75
|
+
all_polygons.append(np.array(polygon, dtype=np.int32))
|
|
76
|
+
|
|
77
|
+
# Crop faces using boundingRect
|
|
78
|
+
cropped_faces = []
|
|
79
|
+
for polygon in all_polygons:
|
|
80
|
+
x, y, w, h = cv2.boundingRect(polygon)
|
|
81
|
+
cropped = np_image[y:y + h, x:x + w]
|
|
82
|
+
cropped_faces.append(cropped)
|
|
83
|
+
|
|
84
|
+
return IOHandler.save_image(cropped_faces, result_path)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import mediapipe as mp
|
|
6
|
+
|
|
7
|
+
# Add parent directory to path for custom module imports
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
mp_face_mesh = mp.solutions.face_mesh
|
|
14
|
+
mp_drawing_utils = mp.solutions.drawing_utils
|
|
15
|
+
mp_drawing_styles = mp.solutions.drawing_styles
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def analyze_face_mesh(
|
|
19
|
+
max_faces=1,
|
|
20
|
+
min_confidence=0.7,
|
|
21
|
+
landmarks_idx=None,
|
|
22
|
+
image_path=None,
|
|
23
|
+
np_image=None,
|
|
24
|
+
result_path=None,
|
|
25
|
+
face_mesh_obj=None
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Detect facial landmarks using MediaPipe FaceMesh.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
max_faces (int): Maximum number of faces to detect.
|
|
32
|
+
min_confidence (float): Detection confidence threshold.
|
|
33
|
+
landmarks_idx (list, optional): Indices of landmarks to extract or draw.
|
|
34
|
+
image_path (str, optional): Path to input image file.
|
|
35
|
+
np_image (np.ndarray, optional): Image array.
|
|
36
|
+
result_path (str, optional): Save path (supports .jpg or .csv).
|
|
37
|
+
face_mesh_obj (FaceMesh, optional): External instance to reuse.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str or tuple or np.ndarray or list:
|
|
41
|
+
- If `result_path` ends with `.jpg`: save/return annotated image.
|
|
42
|
+
- If `result_path` ends with `.csv`: save/return coordinates.
|
|
43
|
+
- If no result_path: returns (image, landmarks list).
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
46
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
47
|
+
|
|
48
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
49
|
+
raise ValueError("'min_confidence' must be a float between 0 and 1.")
|
|
50
|
+
|
|
51
|
+
if landmarks_idx is not None and not isinstance(landmarks_idx, list):
|
|
52
|
+
raise TypeError("'landmarks_idx' must be a list of integers or None.")
|
|
53
|
+
|
|
54
|
+
if result_path and not isinstance(result_path, str):
|
|
55
|
+
raise TypeError("'result_path' must be a string or None.")
|
|
56
|
+
|
|
57
|
+
if result_path and not result_path.endswith(('.jpg', '.csv')):
|
|
58
|
+
raise ValueError("Only '.jpg' or '.csv' formats are supported.")
|
|
59
|
+
|
|
60
|
+
if face_mesh_obj is None:
|
|
61
|
+
face_Mesh = mp_face_mesh.FaceMesh(
|
|
62
|
+
max_num_faces=max_faces,
|
|
63
|
+
min_detection_confidence=min_confidence,
|
|
64
|
+
refine_landmarks=True,
|
|
65
|
+
static_image_mode=True
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
face_Mesh = face_mesh_obj
|
|
69
|
+
|
|
70
|
+
np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
|
|
71
|
+
rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
72
|
+
results = face_Mesh.process(rgb_image)
|
|
73
|
+
|
|
74
|
+
if not results.multi_face_landmarks:
|
|
75
|
+
raise ValueError("No face landmarks detected.")
|
|
76
|
+
|
|
77
|
+
landmarks_idx = landmarks_idx or list(range(468))
|
|
78
|
+
annotated = np_image.copy()
|
|
79
|
+
all_landmarks = []
|
|
80
|
+
|
|
81
|
+
for face_id, face_landmarks in enumerate(results.multi_face_landmarks):
|
|
82
|
+
if result_path is None or result_path.endswith('.jpg'):
|
|
83
|
+
if len(landmarks_idx) == 468:
|
|
84
|
+
mp_drawing_utils.draw_landmarks(
|
|
85
|
+
image=annotated,
|
|
86
|
+
landmark_list=face_landmarks,
|
|
87
|
+
connections=mp_face_mesh.FACEMESH_TESSELATION,
|
|
88
|
+
landmark_drawing_spec=None,
|
|
89
|
+
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
h, w = annotated.shape[:2]
|
|
93
|
+
for idx in landmarks_idx:
|
|
94
|
+
lm = face_landmarks.landmark[idx]
|
|
95
|
+
cx, cy = int(w * lm.x), int(h * lm.y)
|
|
96
|
+
cv2.circle(annotated, (cx, cy), 3, (0, 0, 255), -1)
|
|
97
|
+
|
|
98
|
+
if result_path is None or result_path.endswith('.csv'):
|
|
99
|
+
face_data = [
|
|
100
|
+
[face_id, idx, lm.x, lm.y, lm.z]
|
|
101
|
+
for idx in landmarks_idx
|
|
102
|
+
for lm in [face_landmarks.landmark[idx]]
|
|
103
|
+
]
|
|
104
|
+
all_landmarks.append(face_data)
|
|
105
|
+
|
|
106
|
+
# Output handling
|
|
107
|
+
if result_path:
|
|
108
|
+
if result_path.endswith('.jpg'):
|
|
109
|
+
return IOHandler.save_image(annotated, result_path)
|
|
110
|
+
elif result_path.endswith('.csv'):
|
|
111
|
+
flat_data = [row for face in all_landmarks for row in face]
|
|
112
|
+
return IOHandler.save_csv(flat_data, result_path)
|
|
113
|
+
|
|
114
|
+
return annotated, all_landmarks
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def analyze_face_mesh_live(max_faces=1, min_confidence=0.7):
|
|
118
|
+
"""
|
|
119
|
+
Launch webcam with real-time face landmark overlay.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
max_faces (int): Max faces to detect.
|
|
123
|
+
min_confidence (float): Detection confidence threshold.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
RuntimeError: If webcam can't be opened.
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
129
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
130
|
+
|
|
131
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
132
|
+
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
133
|
+
|
|
134
|
+
cap = cv2.VideoCapture(0)
|
|
135
|
+
if not cap.isOpened():
|
|
136
|
+
raise RuntimeError("Unable to open webcam.")
|
|
137
|
+
|
|
138
|
+
face_Mesh = mp_face_mesh.FaceMesh(
|
|
139
|
+
max_num_faces=max_faces,
|
|
140
|
+
min_detection_confidence=min_confidence,
|
|
141
|
+
refine_landmarks=True,
|
|
142
|
+
static_image_mode=True
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
while True:
|
|
147
|
+
success, frame = cap.read()
|
|
148
|
+
if not success:
|
|
149
|
+
print("Skipping empty frame.")
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
result_image, _ = analyze_face_mesh(
|
|
154
|
+
max_faces=max_faces,
|
|
155
|
+
min_confidence=min_confidence,
|
|
156
|
+
np_image=frame,
|
|
157
|
+
face_mesh_obj=face_Mesh
|
|
158
|
+
)
|
|
159
|
+
except ValueError:
|
|
160
|
+
result_image = frame
|
|
161
|
+
|
|
162
|
+
cv2.imshow("ImagePRO - Face Mesh", result_image)
|
|
163
|
+
|
|
164
|
+
if cv2.waitKey(1) & 0xFF == 27: # ESC key
|
|
165
|
+
break
|
|
166
|
+
finally:
|
|
167
|
+
cap.release()
|
|
168
|
+
cv2.destroyAllWindows()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
analyze_face_mesh_live(max_faces=1)
|