ImagePRO-Python 0.1.1__py3-none-any.whl → 0.2.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/human_analysis/body_analysis/body_pose_estimation.py +60 -56
- ImagePRO/human_analysis/body_analysis/hand_tracking.py +67 -72
- ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +72 -42
- ImagePRO/human_analysis/face_analysis/face_detection.py +53 -28
- ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +130 -77
- ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +67 -38
- ImagePRO/pre_processing/blur.py +133 -65
- ImagePRO/pre_processing/contrast.py +110 -62
- ImagePRO/pre_processing/crop.py +41 -16
- ImagePRO/pre_processing/grayscale.py +32 -10
- ImagePRO/pre_processing/resize.py +39 -18
- ImagePRO/pre_processing/rotate.py +91 -70
- ImagePRO/pre_processing/sharpen.py +52 -36
- ImagePRO/utils/io_handler.py +5 -3
- {imagepro_python-0.1.1.dist-info → imagepro_python-0.2.0.dist-info}/METADATA +1 -1
- imagepro_python-0.2.0.dist-info/RECORD +25 -0
- imagepro_python-0.1.1.dist-info/RECORD +0 -25
- {imagepro_python-0.1.1.dist-info → imagepro_python-0.2.0.dist-info}/WHEEL +0 -0
- {imagepro_python-0.1.1.dist-info → imagepro_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {imagepro_python-0.1.1.dist-info → imagepro_python-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -13,37 +13,47 @@ from utils.io_handler import IOHandler
|
|
|
13
13
|
mp_pose = mp.solutions.pose
|
|
14
14
|
|
|
15
15
|
def detect_body_pose(
|
|
16
|
-
model_accuracy=0.7,
|
|
17
|
-
landmarks_idx=None,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
model_accuracy: float = 0.7,
|
|
17
|
+
landmarks_idx: list | None = None,
|
|
18
|
+
src_image_path: str | None = None,
|
|
19
|
+
src_np_image=None,
|
|
20
|
+
output_image_path: str | None = None,
|
|
21
|
+
output_csv_path: str | None = None,
|
|
21
22
|
pose_obj=None
|
|
22
23
|
):
|
|
23
24
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
Detect body landmarks from an image using MediaPipe Pose.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
model_accuracy : float, default=0.7
|
|
30
|
+
Minimum detection confidence [0, 1].
|
|
31
|
+
landmarks_idx : list[int] | None, optional
|
|
32
|
+
Indices of landmarks to extract. Default: all 33.
|
|
33
|
+
src_image_path : str | None, optional
|
|
34
|
+
Path to image file.
|
|
35
|
+
src_np_image : np.ndarray | None, optional
|
|
36
|
+
Preloaded BGR image array.
|
|
37
|
+
output_image_path : str | None, optional
|
|
38
|
+
If provided, saves annotated image.
|
|
39
|
+
output_csv_path : str | None, optional
|
|
40
|
+
If provided, saves landmarks CSV.
|
|
41
|
+
pose_obj : mp.solutions.pose.Pose | None, optional
|
|
42
|
+
Optional pre-initialized pose model.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
tuple[np.ndarray, list]
|
|
47
|
+
Annotated image and landmarks list.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ValueError, TypeError, FileNotFoundError, IOError
|
|
36
52
|
"""
|
|
37
|
-
if landmarks_idx is not None and not isinstance(landmarks_idx, list):
|
|
38
|
-
raise TypeError("'landmarks_idx' must be a list or None.")
|
|
53
|
+
if landmarks_idx is not None and (not isinstance(landmarks_idx, list) or not all(isinstance(i, int) for i in landmarks_idx)):
|
|
54
|
+
raise TypeError("'landmarks_idx' must be a list of ints or None.")
|
|
39
55
|
|
|
40
|
-
|
|
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)
|
|
56
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
47
57
|
|
|
48
58
|
if landmarks_idx is None:
|
|
49
59
|
landmarks_idx = list(range(33))
|
|
@@ -61,47 +71,41 @@ def detect_body_pose(
|
|
|
61
71
|
all_landmarks = []
|
|
62
72
|
|
|
63
73
|
if result.pose_landmarks:
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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'):
|
|
74
|
+
if len(landmarks_idx) == 33:
|
|
75
|
+
mp.solutions.drawing_utils.draw_landmarks(
|
|
76
|
+
annotated_image,
|
|
77
|
+
result.pose_landmarks,
|
|
78
|
+
mp_pose.POSE_CONNECTIONS,
|
|
79
|
+
landmark_drawing_spec=mp.solutions.drawing_styles.get_default_pose_landmarks_style()
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
h, w, _ = annotated_image.shape
|
|
80
83
|
for idx in landmarks_idx:
|
|
81
84
|
lm = result.pose_landmarks.landmark[idx]
|
|
82
|
-
|
|
85
|
+
x, y = int(w * lm.x), int(h * lm.y)
|
|
86
|
+
cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
|
|
87
|
+
|
|
88
|
+
for idx in landmarks_idx:
|
|
89
|
+
lm = result.pose_landmarks.landmark[idx]
|
|
90
|
+
all_landmarks.append([idx, lm.x, lm.y, lm.z])
|
|
91
|
+
|
|
92
|
+
if output_image_path:
|
|
93
|
+
print(IOHandler.save_image(annotated_image, output_image_path))
|
|
94
|
+
if output_csv_path:
|
|
95
|
+
print(IOHandler.save_csv(all_landmarks, output_csv_path))
|
|
83
96
|
|
|
84
|
-
|
|
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
|
|
97
|
+
return annotated_image, all_landmarks
|
|
91
98
|
|
|
92
99
|
|
|
93
100
|
def detect_body_pose_live():
|
|
94
|
-
"""
|
|
95
|
-
Starts webcam and shows real-time body pose detection.
|
|
96
|
-
Press ESC to exit.
|
|
97
|
-
"""
|
|
101
|
+
"""Starts webcam and shows real-time body pose detection. Press ESC to exit."""
|
|
98
102
|
cap = cv2.VideoCapture(0)
|
|
99
103
|
if not cap.isOpened():
|
|
100
104
|
raise RuntimeError("Failed to open webcam.")
|
|
101
105
|
|
|
102
106
|
pose_obj = mp_pose.Pose(
|
|
103
107
|
min_detection_confidence=0.7,
|
|
104
|
-
static_image_mode=
|
|
108
|
+
static_image_mode=False
|
|
105
109
|
)
|
|
106
110
|
|
|
107
111
|
try:
|
|
@@ -112,7 +116,7 @@ def detect_body_pose_live():
|
|
|
112
116
|
continue
|
|
113
117
|
|
|
114
118
|
try:
|
|
115
|
-
annotated_img, _ = detect_body_pose(
|
|
119
|
+
annotated_img, _ = detect_body_pose(src_np_image=frame, pose_obj=pose_obj)
|
|
116
120
|
except ValueError:
|
|
117
121
|
annotated_img = frame
|
|
118
122
|
|
|
@@ -14,45 +14,54 @@ mp_hands = mp.solutions.hands
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def detect_hands(
|
|
17
|
-
max_hands=2,
|
|
18
|
-
min_confidence=0.7,
|
|
19
|
-
landmarks_idx=None,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
max_hands: int = 2,
|
|
18
|
+
min_confidence: float = 0.7,
|
|
19
|
+
landmarks_idx: list | None = None,
|
|
20
|
+
src_image_path: str | None = None,
|
|
21
|
+
src_np_image=None,
|
|
22
|
+
output_image_path: str | None = None,
|
|
23
|
+
output_csv_path: str | None = None,
|
|
23
24
|
hands_obj=None
|
|
24
25
|
):
|
|
25
26
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
Detect hand landmarks in an image using MediaPipe Hands.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
max_hands : int, default=2
|
|
32
|
+
Max number of hands to detect.
|
|
33
|
+
min_confidence : float, default=0.7
|
|
34
|
+
Minimum detection confidence [0, 1].
|
|
35
|
+
landmarks_idx : list[int] | None, optional
|
|
36
|
+
Landmark indices to extract (default: all 21).
|
|
37
|
+
src_image_path : str | None, optional
|
|
38
|
+
Path to image file.
|
|
39
|
+
src_np_image : np.ndarray | None, optional
|
|
40
|
+
Preloaded BGR image array.
|
|
41
|
+
output_image_path : str | None, optional
|
|
42
|
+
If provided, saves annotated image.
|
|
43
|
+
output_csv_path : str | None, optional
|
|
44
|
+
If provided, saves landmarks CSV.
|
|
45
|
+
hands_obj : mp.solutions.hands.Hands | None, optional
|
|
46
|
+
Optional pre-initialized model.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
tuple[np.ndarray, list]
|
|
51
|
+
Annotated image and landmarks list.
|
|
52
|
+
|
|
53
|
+
Raises
|
|
54
|
+
------
|
|
55
|
+
ValueError, TypeError, FileNotFoundError, IOError
|
|
39
56
|
"""
|
|
40
57
|
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
41
58
|
raise ValueError("'max_hands' must be a positive integer.")
|
|
42
|
-
|
|
43
59
|
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
44
60
|
raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
|
|
61
|
+
if landmarks_idx is not None and (not isinstance(landmarks_idx, list) or not all(isinstance(i, int) for i in landmarks_idx)):
|
|
62
|
+
raise TypeError("'landmarks_idx' must be a list of ints or None.")
|
|
45
63
|
|
|
46
|
-
|
|
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)
|
|
64
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
56
65
|
|
|
57
66
|
if hands_obj is None:
|
|
58
67
|
hands_obj = mp_hands.Hands(
|
|
@@ -72,49 +81,35 @@ def detect_hands(
|
|
|
72
81
|
|
|
73
82
|
if results.multi_hand_landmarks:
|
|
74
83
|
for hand_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 = []
|
|
84
|
+
if len(landmarks_idx) == 21:
|
|
85
|
+
mp.solutions.drawing_utils.draw_landmarks(
|
|
86
|
+
image=annotated_image,
|
|
87
|
+
landmark_list=hand_landmarks,
|
|
88
|
+
connections=mp_hands.HAND_CONNECTIONS,
|
|
89
|
+
landmark_drawing_spec=mp.solutions.drawing_styles.get_default_hand_landmarks_style(),
|
|
90
|
+
connection_drawing_spec=mp.solutions.drawing_styles.get_default_hand_connections_style()
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
h, w, _ = annotated_image.shape
|
|
93
94
|
for idx in landmarks_idx:
|
|
94
95
|
lm = hand_landmarks.landmark[idx]
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
96
|
+
x, y = int(w * lm.x), int(h * lm.y)
|
|
97
|
+
cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
|
|
113
98
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
for idx in landmarks_idx:
|
|
100
|
+
lm = hand_landmarks.landmark[idx]
|
|
101
|
+
all_landmarks.append([hand_id, idx, lm.x, lm.y, lm.z])
|
|
102
|
+
|
|
103
|
+
if output_image_path:
|
|
104
|
+
print(IOHandler.save_image(annotated_image, output_image_path))
|
|
105
|
+
if output_csv_path:
|
|
106
|
+
print(IOHandler.save_csv(all_landmarks, output_csv_path))
|
|
107
|
+
|
|
108
|
+
return annotated_image, all_landmarks
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def detect_hands_live(max_hands: int = 2, min_confidence: float = 0.7):
|
|
112
|
+
"""Real-time hand detection via webcam. Press ESC to exit."""
|
|
118
113
|
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
119
114
|
raise ValueError("'max_hands' must be a positive integer.")
|
|
120
115
|
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
@@ -127,7 +122,7 @@ def detect_hands_live(max_hands=2, min_confidence=0.7):
|
|
|
127
122
|
hands_obj = mp_hands.Hands(
|
|
128
123
|
min_detection_confidence=min_confidence,
|
|
129
124
|
max_num_hands=max_hands,
|
|
130
|
-
static_image_mode=
|
|
125
|
+
static_image_mode=False
|
|
131
126
|
)
|
|
132
127
|
|
|
133
128
|
try:
|
|
@@ -140,7 +135,7 @@ def detect_hands_live(max_hands=2, min_confidence=0.7):
|
|
|
140
135
|
annotated_img, _ = detect_hands(
|
|
141
136
|
max_hands=max_hands,
|
|
142
137
|
min_confidence=min_confidence,
|
|
143
|
-
|
|
138
|
+
src_np_image=frame,
|
|
144
139
|
hands_obj=hands_obj
|
|
145
140
|
)
|
|
146
141
|
|
|
@@ -15,51 +15,71 @@ mp_face_mesh = mp.solutions.face_mesh
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def analyze_eye_status(
|
|
18
|
-
min_confidence=0.7,
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
min_confidence: float = 0.7,
|
|
19
|
+
src_image_path: str | None = None,
|
|
20
|
+
src_np_image=None,
|
|
21
21
|
face_mesh_obj=None,
|
|
22
|
-
threshold=0.2
|
|
23
|
-
):
|
|
22
|
+
threshold: float = 0.2,
|
|
23
|
+
) -> bool:
|
|
24
24
|
"""
|
|
25
|
-
Analyze eye open/closed status
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
Analyze right-eye open/closed status via Eye Aspect Ratio (EAR).
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
min_confidence : float, default=0.7
|
|
30
|
+
Minimum detection confidence for FaceMesh in [0, 1].
|
|
31
|
+
src_image_path : str | None, optional
|
|
32
|
+
Path to input image file.
|
|
33
|
+
src_np_image : np.ndarray | None, optional
|
|
34
|
+
Image array in BGR (OpenCV). If both provided, ``src_np_image`` takes precedence.
|
|
35
|
+
face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
|
|
36
|
+
Reusable FaceMesh instance; if ``None``, one is created (static image mode).
|
|
37
|
+
threshold : float, default=0.2
|
|
38
|
+
EAR threshold below which the eye is considered closed.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
bool
|
|
43
|
+
``True`` if eye is open, ``False`` if closed.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
ValueError
|
|
48
|
+
* If ``min_confidence`` is out of range, or landmarks are not detected.
|
|
49
|
+
* From ``IOHandler.load_image`` when both inputs are ``None`` or image loading fails.
|
|
50
|
+
TypeError
|
|
51
|
+
From ``IOHandler.load_image`` on invalid argument types.
|
|
52
|
+
FileNotFoundError
|
|
53
|
+
From ``IOHandler.load_image`` when ``src_image_path`` does not exist.
|
|
54
|
+
|
|
55
|
+
Notes
|
|
56
|
+
-----
|
|
57
|
+
* Uses MediaPipe indices for the right eye:
|
|
58
|
+
386 (upper lid), 374 (lower lid), 263 (outer corner), 362 (inner corner).
|
|
59
|
+
* EAR is computed as ``vertical / horizontal`` on pixel coordinates.
|
|
39
60
|
"""
|
|
40
61
|
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
41
62
|
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
42
63
|
|
|
43
|
-
np_image = IOHandler.load_image(image_path=
|
|
64
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
44
65
|
h, w = np_image.shape[:2]
|
|
45
66
|
|
|
46
67
|
if face_mesh_obj is None:
|
|
47
68
|
face_mesh_obj = mp_face_mesh.FaceMesh(
|
|
48
69
|
min_detection_confidence=min_confidence,
|
|
49
70
|
refine_landmarks=True,
|
|
50
|
-
static_image_mode=True
|
|
71
|
+
static_image_mode=True,
|
|
51
72
|
)
|
|
52
73
|
|
|
53
|
-
# Landmark indices for right eye (
|
|
54
|
-
# 386 (top eyelid), 374 (bottom eyelid), 263 (outer corner), 362 (inner corner)
|
|
74
|
+
# Landmark indices for right eye (MediaPipe 468-point model)
|
|
55
75
|
indices = [386, 374, 263, 362]
|
|
56
76
|
|
|
57
77
|
_, landmarks = analyze_face_mesh(
|
|
58
78
|
max_faces=1,
|
|
59
79
|
min_confidence=min_confidence,
|
|
60
80
|
landmarks_idx=indices,
|
|
61
|
-
|
|
62
|
-
face_mesh_obj=face_mesh_obj
|
|
81
|
+
src_np_image=np_image,
|
|
82
|
+
face_mesh_obj=face_mesh_obj,
|
|
63
83
|
)
|
|
64
84
|
|
|
65
85
|
if not landmarks:
|
|
@@ -72,8 +92,8 @@ def analyze_eye_status(
|
|
|
72
92
|
bottom_y = eye_points[374][3] * h
|
|
73
93
|
left_x = eye_points[263][2] * w
|
|
74
94
|
right_x = eye_points[362][2] * w
|
|
75
|
-
except KeyError:
|
|
76
|
-
raise ValueError("Missing necessary eye landmarks.")
|
|
95
|
+
except KeyError as e:
|
|
96
|
+
raise ValueError("Missing necessary eye landmarks.") from e
|
|
77
97
|
|
|
78
98
|
vertical_dist = abs(bottom_y - top_y)
|
|
79
99
|
horizontal_dist = abs(right_x - left_x)
|
|
@@ -85,17 +105,28 @@ def analyze_eye_status(
|
|
|
85
105
|
return ear > threshold
|
|
86
106
|
|
|
87
107
|
|
|
88
|
-
def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
|
|
108
|
+
def analyze_eye_status_live(min_confidence: float = 0.7, threshold: float = 0.2) -> None:
|
|
89
109
|
"""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
Live eye open/closed detection using the default webcam.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
min_confidence : float, default=0.7
|
|
115
|
+
Minimum detection confidence in [0, 1].
|
|
116
|
+
threshold : float, default=0.2
|
|
117
|
+
EAR threshold to consider eyes open.
|
|
118
|
+
|
|
119
|
+
Raises
|
|
120
|
+
------
|
|
121
|
+
ValueError
|
|
122
|
+
If ``min_confidence`` is invalid.
|
|
123
|
+
RuntimeError
|
|
124
|
+
If the webcam cannot be accessed.
|
|
125
|
+
|
|
126
|
+
Notes
|
|
127
|
+
-----
|
|
128
|
+
* Uses ``static_image_mode=False`` for better performance on video streams.
|
|
129
|
+
* Press **ESC** to exit.
|
|
99
130
|
"""
|
|
100
131
|
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
101
132
|
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
@@ -108,7 +139,7 @@ def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
|
|
|
108
139
|
max_num_faces=1,
|
|
109
140
|
min_detection_confidence=min_confidence,
|
|
110
141
|
refine_landmarks=True,
|
|
111
|
-
static_image_mode=
|
|
142
|
+
static_image_mode=False, # better for live video
|
|
112
143
|
)
|
|
113
144
|
|
|
114
145
|
try:
|
|
@@ -121,9 +152,9 @@ def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
|
|
|
121
152
|
try:
|
|
122
153
|
is_open = analyze_eye_status(
|
|
123
154
|
min_confidence=min_confidence,
|
|
124
|
-
|
|
155
|
+
src_np_image=frame,
|
|
125
156
|
face_mesh_obj=face_mesh,
|
|
126
|
-
threshold=threshold
|
|
157
|
+
threshold=threshold,
|
|
127
158
|
)
|
|
128
159
|
status = "Open" if is_open else "Closed"
|
|
129
160
|
except ValueError:
|
|
@@ -136,13 +167,12 @@ def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
|
|
|
136
167
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
137
168
|
0.7,
|
|
138
169
|
(0, 255, 0) if status == "Open" else (0, 0, 255),
|
|
139
|
-
2
|
|
170
|
+
2,
|
|
140
171
|
)
|
|
141
172
|
|
|
142
173
|
cv2.imshow("ImagePRO - Eye Status (ESC to Exit)", frame)
|
|
143
174
|
if cv2.waitKey(5) & 0xFF == 27:
|
|
144
175
|
break
|
|
145
|
-
|
|
146
176
|
finally:
|
|
147
177
|
cap.release()
|
|
148
178
|
cv2.destroyAllWindows()
|
|
@@ -13,59 +13,80 @@ from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def detect_faces(
|
|
16
|
-
max_faces=1,
|
|
17
|
-
min_confidence=0.7,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
face_mesh_obj=None
|
|
16
|
+
max_faces: int = 1,
|
|
17
|
+
min_confidence: float = 0.7,
|
|
18
|
+
src_image_path: str | None = None,
|
|
19
|
+
src_np_image=None,
|
|
20
|
+
output_image_path: str | None = None,
|
|
21
|
+
face_mesh_obj=None,
|
|
22
22
|
):
|
|
23
23
|
"""
|
|
24
|
-
Detect and crop face regions using MediaPipe
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
Detect and crop face regions using MediaPipe facial landmarks.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
max_faces : int, default=1
|
|
29
|
+
Maximum number of faces to detect.
|
|
30
|
+
min_confidence : float, default=0.7
|
|
31
|
+
Detection confidence threshold in [0, 1].
|
|
32
|
+
src_image_path : str | None, optional
|
|
33
|
+
Path to input image file.
|
|
34
|
+
src_np_image : np.ndarray | None, optional
|
|
35
|
+
Image array in BGR format. If both provided, `src_np_image` takes precedence.
|
|
36
|
+
output_image_path : str | None, optional
|
|
37
|
+
If provided, saves cropped faces to disk (single path used as base name for multiple faces).
|
|
38
|
+
face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
|
|
39
|
+
Optional reusable FaceMesh instance.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
list[np.ndarray] | str
|
|
44
|
+
If `output_image_path` is provided, returns the save message from
|
|
45
|
+
`IOHandler.save_image`. Otherwise, returns a list of cropped face images.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
ValueError
|
|
50
|
+
* If inputs are invalid or no face landmarks are detected (this function).
|
|
51
|
+
* From IOHandler.load_image when both inputs are None or image loading fails.
|
|
52
|
+
TypeError
|
|
53
|
+
From IOHandler.load_image/save_image on invalid argument types.
|
|
54
|
+
FileNotFoundError
|
|
55
|
+
From IOHandler.load_image when `src_image_path` does not exist.
|
|
56
|
+
IOError
|
|
57
|
+
From IOHandler.save_image when saving the image fails.
|
|
39
58
|
"""
|
|
59
|
+
# --- Validate inputs -----------------------------------------------------
|
|
40
60
|
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
41
61
|
raise ValueError("'max_faces' must be a positive integer.")
|
|
42
62
|
|
|
43
63
|
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
44
64
|
raise ValueError("'min_confidence' must be a float between 0 and 1.")
|
|
45
65
|
|
|
46
|
-
|
|
66
|
+
# --- Load image -----------------------------------------------------------
|
|
67
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
47
68
|
height, width = np_image.shape[:2]
|
|
48
69
|
|
|
49
|
-
# Selected face outline landmark indices (
|
|
70
|
+
# Selected face outline landmark indices (MediaPipe 468-point model)
|
|
50
71
|
face_outline_indices = [
|
|
51
72
|
10, 338, 297, 332, 284, 251, 389, 356, 454, 323,
|
|
52
73
|
361, 288, 397, 365, 379, 378, 400, 377, 152, 148,
|
|
53
74
|
176, 149, 150, 136, 164, 163, 153, 157
|
|
54
75
|
]
|
|
55
76
|
|
|
56
|
-
# Run face mesh analysis
|
|
77
|
+
# --- Run face mesh analysis ----------------------------------------------
|
|
57
78
|
_, raw_landmarks = analyze_face_mesh(
|
|
58
79
|
max_faces=max_faces,
|
|
59
80
|
min_confidence=min_confidence,
|
|
60
81
|
landmarks_idx=face_outline_indices,
|
|
61
|
-
|
|
82
|
+
src_np_image=np_image,
|
|
62
83
|
face_mesh_obj=face_mesh_obj
|
|
63
84
|
)
|
|
64
85
|
|
|
65
86
|
if not raw_landmarks:
|
|
66
87
|
raise ValueError("No face landmarks detected in the input image.")
|
|
67
88
|
|
|
68
|
-
# Convert normalized landmark coordinates to pixel positions
|
|
89
|
+
# --- Convert normalized landmark coordinates to pixel positions ----------
|
|
69
90
|
all_polygons = []
|
|
70
91
|
for face in raw_landmarks:
|
|
71
92
|
polygon = [
|
|
@@ -74,11 +95,15 @@ def detect_faces(
|
|
|
74
95
|
]
|
|
75
96
|
all_polygons.append(np.array(polygon, dtype=np.int32))
|
|
76
97
|
|
|
77
|
-
# Crop faces using boundingRect
|
|
98
|
+
# --- Crop faces using boundingRect ---------------------------------------
|
|
78
99
|
cropped_faces = []
|
|
79
100
|
for polygon in all_polygons:
|
|
80
101
|
x, y, w, h = cv2.boundingRect(polygon)
|
|
81
102
|
cropped = np_image[y:y + h, x:x + w]
|
|
82
103
|
cropped_faces.append(cropped)
|
|
83
104
|
|
|
84
|
-
|
|
105
|
+
# --- Output handling ------------------------------------------------------
|
|
106
|
+
if output_image_path:
|
|
107
|
+
print(IOHandler.save_image(cropped_faces, output_image_path))
|
|
108
|
+
|
|
109
|
+
return cropped_faces
|