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
|
@@ -16,58 +16,95 @@ mp_drawing_styles = mp.solutions.drawing_styles
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def analyze_face_mesh(
|
|
19
|
-
max_faces=1,
|
|
20
|
-
min_confidence=0.7,
|
|
21
|
-
landmarks_idx=None,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
max_faces: int = 1,
|
|
20
|
+
min_confidence: float = 0.7,
|
|
21
|
+
landmarks_idx: list | None = None,
|
|
22
|
+
src_image_path: str | None = None,
|
|
23
|
+
src_np_image=None,
|
|
24
|
+
output_image_path: str | None = None,
|
|
25
|
+
output_csv_path: str | None = None,
|
|
26
|
+
face_mesh_obj=None,
|
|
26
27
|
):
|
|
27
28
|
"""
|
|
28
|
-
Detect facial landmarks using MediaPipe FaceMesh.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
Detect facial landmarks using MediaPipe FaceMesh on a single image (path or ndarray).
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
max_faces : int, default=1
|
|
34
|
+
Maximum number of faces to detect.
|
|
35
|
+
min_confidence : float, default=0.7
|
|
36
|
+
Minimum detection confidence in [0, 1].
|
|
37
|
+
landmarks_idx : list[int] | None, optional
|
|
38
|
+
Specific landmark indices to extract/draw. If ``None``, uses the full
|
|
39
|
+
468-point mesh.
|
|
40
|
+
src_image_path : str | None, optional
|
|
41
|
+
Path to the input image (BGR/RGB supported by ``IOHandler.load_image``).
|
|
42
|
+
src_np_image : np.ndarray | None, optional
|
|
43
|
+
Image array in BGR (as returned by OpenCV). If both ``src_image_path`` and
|
|
44
|
+
``src_np_image`` are provided, the ndarray takes precedence.
|
|
45
|
+
output_image_path : str | None, optional
|
|
46
|
+
If provided (e.g. "out.jpg"/"out.png"), the annotated image is saved there.
|
|
47
|
+
output_csv_path : str | None, optional
|
|
48
|
+
If provided (e.g. "landmarks.csv"), landmark coordinates are saved there as
|
|
49
|
+
rows: ``[face_id, index, x, y, z]`` with normalized x/y/z from MediaPipe.
|
|
50
|
+
face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
|
|
51
|
+
Reusable external FaceMesh instance. If ``None``, a new one is created
|
|
52
|
+
with ``static_image_mode=True`` and ``refine_landmarks=True``.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
tuple
|
|
57
|
+
``(annotated_image: np.ndarray, all_landmarks: list[list[list[float]]])``
|
|
58
|
+
where ``all_landmarks`` is a list per face, each containing rows of
|
|
59
|
+
``[face_id, index, x, y, z]``.
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
ValueError
|
|
64
|
+
* If inputs are invalid or no face landmarks are detected (this function).
|
|
65
|
+
* From ``IOHandler.load_image`` when both inputs are ``None`` or image loading fails.
|
|
66
|
+
TypeError
|
|
67
|
+
* If ``landmarks_idx`` is not a list of integers (this function).
|
|
68
|
+
* From ``IOHandler.load_image``/``save_image``/``save_csv`` on invalid argument types.
|
|
69
|
+
FileNotFoundError
|
|
70
|
+
From ``IOHandler.load_image`` when ``image_path`` does not exist.
|
|
71
|
+
IOError
|
|
72
|
+
From ``IOHandler.save_image`` when saving the image fails.
|
|
73
|
+
|
|
74
|
+
Notes
|
|
75
|
+
-----
|
|
76
|
+
* The x/y/z coordinates from MediaPipe are **normalized** to [0, 1] in image
|
|
77
|
+
space; multiply by width/height to get pixel coordinates.
|
|
78
|
+
* When ``landmarks_idx`` covers all 468 points, the full tessellation is drawn.
|
|
79
|
+
Otherwise, only the requested points are highlighted.
|
|
44
80
|
"""
|
|
81
|
+
|
|
82
|
+
# --- Validate inputs -----------------------------------------------------
|
|
45
83
|
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
46
84
|
raise ValueError("'max_faces' must be a positive integer.")
|
|
47
85
|
|
|
48
86
|
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
49
87
|
raise ValueError("'min_confidence' must be a float between 0 and 1.")
|
|
50
88
|
|
|
51
|
-
if landmarks_idx is not None
|
|
52
|
-
|
|
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.")
|
|
89
|
+
if landmarks_idx is not None:
|
|
90
|
+
if not isinstance(landmarks_idx, list) or not all(isinstance(i, int) for i in landmarks_idx):
|
|
91
|
+
raise TypeError("'landmarks_idx' must be a list of integers or None.")
|
|
59
92
|
|
|
93
|
+
# --- Create or reuse FaceMesh instance ----------------------------------
|
|
60
94
|
if face_mesh_obj is None:
|
|
61
95
|
face_Mesh = mp_face_mesh.FaceMesh(
|
|
62
96
|
max_num_faces=max_faces,
|
|
63
97
|
min_detection_confidence=min_confidence,
|
|
64
98
|
refine_landmarks=True,
|
|
65
|
-
static_image_mode=True
|
|
99
|
+
static_image_mode=True,
|
|
66
100
|
)
|
|
67
101
|
else:
|
|
68
102
|
face_Mesh = face_mesh_obj
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
# --- Load image (path or ndarray) ---------------------------------------
|
|
105
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
106
|
+
|
|
107
|
+
# Convert BGR -> RGB for MediaPipe
|
|
71
108
|
rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
72
109
|
results = face_Mesh.process(rgb_image)
|
|
73
110
|
|
|
@@ -75,55 +112,68 @@ def analyze_face_mesh(
|
|
|
75
112
|
raise ValueError("No face landmarks detected.")
|
|
76
113
|
|
|
77
114
|
landmarks_idx = landmarks_idx or list(range(468))
|
|
115
|
+
|
|
78
116
|
annotated = np_image.copy()
|
|
79
117
|
all_landmarks = []
|
|
80
118
|
|
|
81
119
|
for face_id, face_landmarks in enumerate(results.multi_face_landmarks):
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
# Draw either the full tessellation or specific landmark dots
|
|
121
|
+
if len(landmarks_idx) == 468:
|
|
122
|
+
mp_drawing_utils.draw_landmarks(
|
|
123
|
+
image=annotated,
|
|
124
|
+
landmark_list=face_landmarks,
|
|
125
|
+
connections=mp_face_mesh.FACEMESH_TESSELATION,
|
|
126
|
+
landmark_drawing_spec=None,
|
|
127
|
+
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style(),
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
h, w = annotated.shape[:2]
|
|
131
|
+
for idx in landmarks_idx:
|
|
132
|
+
lm = face_landmarks.landmark[idx]
|
|
133
|
+
cx, cy = int(w * lm.x), int(h * lm.y)
|
|
134
|
+
cv2.circle(annotated, (cx, cy), 3, (0, 0, 255), -1)
|
|
135
|
+
|
|
136
|
+
# Collect normalized coordinates for requested indices
|
|
137
|
+
face_data = [
|
|
138
|
+
[face_id, idx, face_landmarks.landmark[idx].x, face_landmarks.landmark[idx].y, face_landmarks.landmark[idx].z]
|
|
139
|
+
for idx in landmarks_idx
|
|
140
|
+
]
|
|
141
|
+
all_landmarks.append(face_data)
|
|
142
|
+
|
|
143
|
+
# --- Output handling -----------------------------------------------------
|
|
144
|
+
if output_image_path is not None:
|
|
145
|
+
# IOHandler.save_image returns a message/path; echo to stdout for logs
|
|
146
|
+
print(IOHandler.save_image(annotated, output_image_path))
|
|
147
|
+
|
|
148
|
+
if output_csv_path is not None:
|
|
149
|
+
flat_data = [row for face in all_landmarks for row in face]
|
|
150
|
+
print(IOHandler.save_csv(flat_data, output_csv_path))
|
|
113
151
|
|
|
114
152
|
return annotated, all_landmarks
|
|
115
153
|
|
|
116
154
|
|
|
117
|
-
def analyze_face_mesh_live(max_faces=1, min_confidence=0.7):
|
|
155
|
+
def analyze_face_mesh_live(max_faces: int = 1, min_confidence: float = 0.7):
|
|
118
156
|
"""
|
|
119
|
-
Launch webcam with real-time
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
Launch webcam preview with real-time FaceMesh overlay.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
max_faces : int, default=1
|
|
162
|
+
Max number of faces to detect per frame.
|
|
163
|
+
min_confidence : float, default=0.7
|
|
164
|
+
Minimum detection confidence in [0, 1].
|
|
165
|
+
|
|
166
|
+
Raises
|
|
167
|
+
------
|
|
168
|
+
ValueError
|
|
169
|
+
If arguments are invalid.
|
|
170
|
+
RuntimeError
|
|
171
|
+
If the webcam cannot be opened.
|
|
172
|
+
|
|
173
|
+
Notes
|
|
174
|
+
-----
|
|
175
|
+
* Uses ``static_image_mode=False`` for better performance on video streams.
|
|
176
|
+
* Press **ESC** to exit the preview window.
|
|
127
177
|
"""
|
|
128
178
|
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
129
179
|
raise ValueError("'max_faces' must be a positive integer.")
|
|
@@ -135,11 +185,12 @@ def analyze_face_mesh_live(max_faces=1, min_confidence=0.7):
|
|
|
135
185
|
if not cap.isOpened():
|
|
136
186
|
raise RuntimeError("Unable to open webcam.")
|
|
137
187
|
|
|
188
|
+
# For live video, static_image_mode=False is preferred (tracking across frames)
|
|
138
189
|
face_Mesh = mp_face_mesh.FaceMesh(
|
|
139
190
|
max_num_faces=max_faces,
|
|
140
191
|
min_detection_confidence=min_confidence,
|
|
141
192
|
refine_landmarks=True,
|
|
142
|
-
static_image_mode=
|
|
193
|
+
static_image_mode=False,
|
|
143
194
|
)
|
|
144
195
|
|
|
145
196
|
try:
|
|
@@ -153,15 +204,17 @@ def analyze_face_mesh_live(max_faces=1, min_confidence=0.7):
|
|
|
153
204
|
result_image, _ = analyze_face_mesh(
|
|
154
205
|
max_faces=max_faces,
|
|
155
206
|
min_confidence=min_confidence,
|
|
156
|
-
|
|
157
|
-
face_mesh_obj=face_Mesh
|
|
207
|
+
src_np_image=frame,
|
|
208
|
+
face_mesh_obj=face_Mesh,
|
|
158
209
|
)
|
|
159
210
|
except ValueError:
|
|
211
|
+
# No face detected in this frame; show raw frame
|
|
160
212
|
result_image = frame
|
|
161
213
|
|
|
162
214
|
cv2.imshow("ImagePRO - Face Mesh", result_image)
|
|
163
215
|
|
|
164
|
-
|
|
216
|
+
# Exit on ESC key
|
|
217
|
+
if cv2.waitKey(1) & 0xFF == 27:
|
|
165
218
|
break
|
|
166
219
|
finally:
|
|
167
220
|
cap.release()
|
|
@@ -15,36 +15,53 @@ mp_face_mesh = mp.solutions.face_mesh
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def estimate_head_pose(
|
|
18
|
-
max_faces=1,
|
|
19
|
-
min_confidence=0.7,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
face_mesh_obj=None
|
|
18
|
+
max_faces: int = 1,
|
|
19
|
+
min_confidence: float = 0.7,
|
|
20
|
+
src_image_path: str | None = None,
|
|
21
|
+
src_np_image=None,
|
|
22
|
+
output_csv_path: str | None = None,
|
|
23
|
+
face_mesh_obj=None,
|
|
24
24
|
):
|
|
25
25
|
"""
|
|
26
|
-
Estimate head pose (yaw, pitch) from facial landmarks.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
Estimate head pose (yaw, pitch) from a single image using MediaPipe facial landmarks.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
max_faces : int, default=1
|
|
31
|
+
Number of faces to detect.
|
|
32
|
+
min_confidence : float, default=0.7
|
|
33
|
+
Detection confidence threshold in [0, 1].
|
|
34
|
+
src_image_path : str | None, optional
|
|
35
|
+
Path to the input image file.
|
|
36
|
+
src_np_image : np.ndarray | None, optional
|
|
37
|
+
Input image array in BGR format. If both are provided, `src_np_image` takes precedence.
|
|
38
|
+
output_csv_path : str | None, optional
|
|
39
|
+
If provided, saves the results as CSV rows: [face_id, yaw, pitch].
|
|
40
|
+
face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
|
|
41
|
+
Optional reusable FaceMesh instance.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
list[list[float]] | str
|
|
46
|
+
Save message from IOHandler.save_csv if `output_csv_path` is provided,
|
|
47
|
+
otherwise list of [face_id, yaw, pitch] for each detected face.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ValueError
|
|
52
|
+
* If inputs are invalid or no face landmarks are detected (this function).
|
|
53
|
+
* From IOHandler.load_image when both inputs are None or image loading fails.
|
|
54
|
+
TypeError
|
|
55
|
+
From IOHandler.load_image/save_csv on invalid argument types.
|
|
56
|
+
FileNotFoundError
|
|
57
|
+
From IOHandler.load_image when `src_image_path` does not exist.
|
|
41
58
|
"""
|
|
42
59
|
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
43
60
|
raise ValueError("'max_faces' must be a positive integer.")
|
|
44
61
|
if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
|
|
45
62
|
raise ValueError("'min_confidence' must be between 0 and 1.")
|
|
46
63
|
|
|
47
|
-
np_image = IOHandler.load_image(image_path=
|
|
64
|
+
np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
|
|
48
65
|
|
|
49
66
|
# Important landmark indices (MediaPipe 468 model)
|
|
50
67
|
indices = [1, 152, 33, 263, 168] # nose_tip, chin, left_eye, right_eye, nasion
|
|
@@ -53,7 +70,7 @@ def estimate_head_pose(
|
|
|
53
70
|
max_faces=max_faces,
|
|
54
71
|
min_confidence=min_confidence,
|
|
55
72
|
landmarks_idx=indices,
|
|
56
|
-
|
|
73
|
+
src_np_image=np_image,
|
|
57
74
|
face_mesh_obj=face_mesh_obj
|
|
58
75
|
)
|
|
59
76
|
|
|
@@ -73,28 +90,40 @@ def estimate_head_pose(
|
|
|
73
90
|
except KeyError:
|
|
74
91
|
continue # skip this face if any point is missing
|
|
75
92
|
|
|
76
|
-
# Simplified proportional estimation
|
|
93
|
+
# Simplified proportional estimation on normalized coords
|
|
77
94
|
yaw = 100 * ((right_x - nasion_x) - (nasion_x - left_x))
|
|
78
95
|
pitch = 100 * ((chin_y - nose_y) - (nose_y - nasion_y))
|
|
79
96
|
|
|
80
|
-
results.append([face[0][0], yaw, pitch])
|
|
97
|
+
results.append([face[0][0], yaw, pitch])
|
|
81
98
|
|
|
82
|
-
if
|
|
83
|
-
|
|
99
|
+
if output_csv_path:
|
|
100
|
+
print(IOHandler.save_csv(results, output_csv_path))
|
|
84
101
|
|
|
85
102
|
return results
|
|
86
103
|
|
|
87
104
|
|
|
88
|
-
def estimate_head_pose_live(max_faces=1, min_confidence=0.7):
|
|
105
|
+
def estimate_head_pose_live(max_faces: int = 1, min_confidence: float = 0.7):
|
|
89
106
|
"""
|
|
90
|
-
Live head pose estimation using webcam.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
Live head pose estimation using the default webcam.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
max_faces : int, default=1
|
|
112
|
+
Number of faces to detect per frame.
|
|
113
|
+
min_confidence : float, default=0.7
|
|
114
|
+
Detection confidence threshold in [0, 1].
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
ValueError
|
|
119
|
+
On invalid inputs.
|
|
120
|
+
RuntimeError
|
|
121
|
+
If the camera cannot be opened.
|
|
122
|
+
|
|
123
|
+
Notes
|
|
124
|
+
-----
|
|
125
|
+
* Uses static_image_mode=False for better performance on video streams.
|
|
126
|
+
* Press ESC to exit the preview window.
|
|
98
127
|
"""
|
|
99
128
|
if not isinstance(max_faces, int) or max_faces <= 0:
|
|
100
129
|
raise ValueError("'max_faces' must be a positive integer.")
|
|
@@ -109,7 +138,7 @@ def estimate_head_pose_live(max_faces=1, min_confidence=0.7):
|
|
|
109
138
|
max_num_faces=max_faces,
|
|
110
139
|
min_detection_confidence=min_confidence,
|
|
111
140
|
refine_landmarks=True,
|
|
112
|
-
static_image_mode=
|
|
141
|
+
static_image_mode=False
|
|
113
142
|
)
|
|
114
143
|
|
|
115
144
|
try:
|
|
@@ -123,7 +152,7 @@ def estimate_head_pose_live(max_faces=1, min_confidence=0.7):
|
|
|
123
152
|
face_angles = estimate_head_pose(
|
|
124
153
|
max_faces=max_faces,
|
|
125
154
|
min_confidence=min_confidence,
|
|
126
|
-
|
|
155
|
+
src_np_image=frame,
|
|
127
156
|
face_mesh_obj=face_mesh
|
|
128
157
|
)
|
|
129
158
|
except ValueError:
|