ImagePRO-Python 0.1.1__tar.gz → 0.3.0__tar.gz
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_python-0.1.1/src/ImagePRO_Python.egg-info → imagepro_python-0.3.0}/PKG-INFO +1 -1
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/pyproject.toml +1 -1
- imagepro_python-0.3.0/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +133 -0
- imagepro_python-0.3.0/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +152 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +72 -42
- imagepro_python-0.3.0/src/ImagePRO/human_analysis/face_analysis/face_detection.py +109 -0
- imagepro_python-0.3.0/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +225 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +67 -38
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/blur.py +198 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/contrast.py +153 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/crop.py +76 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/dataset_generator.py +178 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/grayscale.py +49 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/resize.py +64 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/rotate.py +115 -0
- imagepro_python-0.3.0/src/ImagePRO/pre_processing/sharpen.py +90 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/utils/io_handler.py +5 -3
- {imagepro_python-0.1.1 → imagepro_python-0.3.0/src/ImagePRO_Python.egg-info}/PKG-INFO +1 -1
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO_Python.egg-info/SOURCES.txt +1 -0
- imagepro_python-0.1.1/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +0 -129
- imagepro_python-0.1.1/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +0 -157
- imagepro_python-0.1.1/src/ImagePRO/human_analysis/face_analysis/face_detection.py +0 -84
- imagepro_python-0.1.1/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +0 -172
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/blur.py +0 -130
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/contrast.py +0 -105
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/crop.py +0 -51
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/grayscale.py +0 -27
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/resize.py +0 -43
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/rotate.py +0 -94
- imagepro_python-0.1.1/src/ImagePRO/pre_processing/sharpen.py +0 -74
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/LICENSE +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/setup.cfg +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/human_analysis/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/pre_processing/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO/utils/__init__.py +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO_Python.egg-info/dependency_links.txt +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO_Python.egg-info/requires.txt +0 -0
- {imagepro_python-0.1.1 → imagepro_python-0.3.0}/src/ImagePRO_Python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
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 utils.io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
mp_pose = mp.solutions.pose
|
|
14
|
+
|
|
15
|
+
def detect_body_pose(
|
|
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,
|
|
22
|
+
pose_obj=None
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
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
|
|
52
|
+
"""
|
|
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.")
|
|
55
|
+
|
|
56
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
57
|
+
|
|
58
|
+
if landmarks_idx is None:
|
|
59
|
+
landmarks_idx = list(range(33))
|
|
60
|
+
|
|
61
|
+
if pose_obj is None:
|
|
62
|
+
pose_obj = mp_pose.Pose(
|
|
63
|
+
min_detection_confidence=model_accuracy,
|
|
64
|
+
static_image_mode=True
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
image_rgb = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
68
|
+
result = pose_obj.process(image_rgb)
|
|
69
|
+
|
|
70
|
+
annotated_image = np_image.copy()
|
|
71
|
+
all_landmarks = []
|
|
72
|
+
|
|
73
|
+
if result.pose_landmarks:
|
|
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
|
|
83
|
+
for idx in landmarks_idx:
|
|
84
|
+
lm = result.pose_landmarks.landmark[idx]
|
|
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))
|
|
96
|
+
|
|
97
|
+
return annotated_image, all_landmarks
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_body_pose_live():
|
|
101
|
+
"""Starts webcam and shows real-time body pose detection. Press ESC to exit."""
|
|
102
|
+
cap = cv2.VideoCapture(0)
|
|
103
|
+
if not cap.isOpened():
|
|
104
|
+
raise RuntimeError("Failed to open webcam.")
|
|
105
|
+
|
|
106
|
+
pose_obj = mp_pose.Pose(
|
|
107
|
+
min_detection_confidence=0.7,
|
|
108
|
+
static_image_mode=False
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
while True:
|
|
113
|
+
success, frame = cap.read()
|
|
114
|
+
if not success:
|
|
115
|
+
print("Ignoring empty camera frame.")
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
annotated_img, _ = detect_body_pose(src_np_image=frame, pose_obj=pose_obj)
|
|
120
|
+
except ValueError:
|
|
121
|
+
annotated_img = frame
|
|
122
|
+
|
|
123
|
+
cv2.imshow('ImagePRO - Live Body Pose Detection', annotated_img)
|
|
124
|
+
|
|
125
|
+
if cv2.waitKey(5) & 0xFF == 27:
|
|
126
|
+
break
|
|
127
|
+
finally:
|
|
128
|
+
cap.release()
|
|
129
|
+
cv2.destroyAllWindows()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
detect_body_pose_live()
|
|
@@ -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 import custom modules
|
|
8
|
+
parent_dir = Path(__file__).resolve().parent.parent.parent
|
|
9
|
+
sys.path.append(str(parent_dir))
|
|
10
|
+
|
|
11
|
+
from utils.io_handler import IOHandler
|
|
12
|
+
|
|
13
|
+
mp_hands = mp.solutions.hands
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_hands(
|
|
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,
|
|
24
|
+
hands_obj=None
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
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
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
58
|
+
raise ValueError("'max_hands' must be a positive integer.")
|
|
59
|
+
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
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.")
|
|
63
|
+
|
|
64
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
65
|
+
|
|
66
|
+
if hands_obj is None:
|
|
67
|
+
hands_obj = mp_hands.Hands(
|
|
68
|
+
min_detection_confidence=min_confidence,
|
|
69
|
+
max_num_hands=max_hands,
|
|
70
|
+
static_image_mode=True
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if landmarks_idx is None:
|
|
74
|
+
landmarks_idx = list(range(21))
|
|
75
|
+
|
|
76
|
+
rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
77
|
+
results = hands_obj.process(rgb_image)
|
|
78
|
+
|
|
79
|
+
annotated_image = np_image.copy()
|
|
80
|
+
all_landmarks = []
|
|
81
|
+
|
|
82
|
+
if results.multi_hand_landmarks:
|
|
83
|
+
for hand_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
|
|
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
|
|
94
|
+
for idx in landmarks_idx:
|
|
95
|
+
lm = hand_landmarks.landmark[idx]
|
|
96
|
+
x, y = int(w * lm.x), int(h * lm.y)
|
|
97
|
+
cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
|
|
98
|
+
|
|
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."""
|
|
113
|
+
if not isinstance(max_hands, int) or max_hands <= 0:
|
|
114
|
+
raise ValueError("'max_hands' must be a positive integer.")
|
|
115
|
+
if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
|
|
116
|
+
raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
|
|
117
|
+
|
|
118
|
+
cap = cv2.VideoCapture(0)
|
|
119
|
+
if not cap.isOpened():
|
|
120
|
+
raise RuntimeError("Failed to open webcam.")
|
|
121
|
+
|
|
122
|
+
hands_obj = mp_hands.Hands(
|
|
123
|
+
min_detection_confidence=min_confidence,
|
|
124
|
+
max_num_hands=max_hands,
|
|
125
|
+
static_image_mode=False
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
while True:
|
|
130
|
+
success, frame = cap.read()
|
|
131
|
+
if not success:
|
|
132
|
+
print("Ignoring empty camera frame.")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
annotated_img, _ = detect_hands(
|
|
136
|
+
max_hands=max_hands,
|
|
137
|
+
min_confidence=min_confidence,
|
|
138
|
+
src_np_image=frame,
|
|
139
|
+
hands_obj=hands_obj
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
cv2.imshow('Live hand detector - ImagePRO', annotated_img)
|
|
143
|
+
|
|
144
|
+
if cv2.waitKey(5) & 0xFF == 27:
|
|
145
|
+
break
|
|
146
|
+
finally:
|
|
147
|
+
cap.release()
|
|
148
|
+
cv2.destroyAllWindows()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
detect_hands_live(max_hands=2)
|
|
@@ -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()
|
|
@@ -0,0 +1,109 @@
|
|
|
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 utils.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: 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
|
+
):
|
|
23
|
+
"""
|
|
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.
|
|
58
|
+
"""
|
|
59
|
+
# --- Validate inputs -----------------------------------------------------
|
|
60
|
+
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
61
|
+
raise ValueError("'max_faces' must be a positive integer.")
|
|
62
|
+
|
|
63
|
+
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
64
|
+
raise ValueError("'min_confidence' must be a float between 0 and 1.")
|
|
65
|
+
|
|
66
|
+
# --- Load image -----------------------------------------------------------
|
|
67
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
68
|
+
height, width = np_image.shape[:2]
|
|
69
|
+
|
|
70
|
+
# Selected face outline landmark indices (MediaPipe 468-point model)
|
|
71
|
+
face_outline_indices = [
|
|
72
|
+
10, 338, 297, 332, 284, 251, 389, 356, 454, 323,
|
|
73
|
+
361, 288, 397, 365, 379, 378, 400, 377, 152, 148,
|
|
74
|
+
176, 149, 150, 136, 164, 163, 153, 157
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# --- Run face mesh analysis ----------------------------------------------
|
|
78
|
+
_, raw_landmarks = analyze_face_mesh(
|
|
79
|
+
max_faces=max_faces,
|
|
80
|
+
min_confidence=min_confidence,
|
|
81
|
+
landmarks_idx=face_outline_indices,
|
|
82
|
+
src_np_image=np_image,
|
|
83
|
+
face_mesh_obj=face_mesh_obj
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not raw_landmarks:
|
|
87
|
+
raise ValueError("No face landmarks detected in the input image.")
|
|
88
|
+
|
|
89
|
+
# --- Convert normalized landmark coordinates to pixel positions ----------
|
|
90
|
+
all_polygons = []
|
|
91
|
+
for face in raw_landmarks:
|
|
92
|
+
polygon = [
|
|
93
|
+
(int(x * width), int(y * height))
|
|
94
|
+
for _, _, x, y, _ in face
|
|
95
|
+
]
|
|
96
|
+
all_polygons.append(np.array(polygon, dtype=np.int32))
|
|
97
|
+
|
|
98
|
+
# --- Crop faces using boundingRect ---------------------------------------
|
|
99
|
+
cropped_faces = []
|
|
100
|
+
for polygon in all_polygons:
|
|
101
|
+
x, y, w, h = cv2.boundingRect(polygon)
|
|
102
|
+
cropped = np_image[y:y + h, x:x + w]
|
|
103
|
+
cropped_faces.append(cropped)
|
|
104
|
+
|
|
105
|
+
# --- Output handling ------------------------------------------------------
|
|
106
|
+
if output_image_path:
|
|
107
|
+
print(IOHandler.save_image(cropped_faces, output_image_path))
|
|
108
|
+
|
|
109
|
+
return cropped_faces
|