ImagePRO-Python 0.1.0__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.
@@ -8,7 +8,7 @@ import mediapipe as mp
8
8
  parent_dir = Path(__file__).resolve().parent.parent.parent
9
9
  sys.path.append(str(parent_dir))
10
10
 
11
- from io_handler import IOHandler
11
+ from utils.io_handler import IOHandler
12
12
 
13
13
  mp_face_mesh = mp.solutions.face_mesh
14
14
  mp_drawing_utils = mp.solutions.drawing_utils
@@ -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
- image_path=None,
23
- np_image=None,
24
- result_path=None,
25
- face_mesh_obj=None
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
- 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).
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 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.")
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
- np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
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
- 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)
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 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.
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=True
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
- np_image=frame,
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
- if cv2.waitKey(1) & 0xFF == 27: # ESC key
216
+ # Exit on ESC key
217
+ if cv2.waitKey(1) & 0xFF == 27:
165
218
  break
166
219
  finally:
167
220
  cap.release()
@@ -8,43 +8,60 @@ import mediapipe as mp
8
8
  parent_dir = Path(__file__).resolve().parent.parent.parent
9
9
  sys.path.append(str(parent_dir))
10
10
 
11
- from io_handler import IOHandler
11
+ from utils.io_handler import IOHandler
12
12
  from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
13
13
 
14
14
  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
- image_path=None,
21
- np_image=None,
22
- result_path=None,
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
- Args:
29
- max_faces (int): Number of faces to detect.
30
- min_confidence (float): Confidence threshold (0–1).
31
- image_path (str, optional): Path to input image.
32
- np_image (np.ndarray, optional): Input image as array.
33
- result_path (str, optional): If given, save results as CSV.
34
- face_mesh_obj (FaceMesh, optional): Reusable MediaPipe instance.
35
-
36
- Returns:
37
- str or list[list]: CSV confirmation if saved, else list of [face_id, yaw, pitch].
38
-
39
- Raises:
40
- ValueError: On invalid input or no detected face.
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=image_path, np_image=np_image)
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
- np_image=np_image,
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]) # face_id, yaw, pitch
97
+ results.append([face[0][0], yaw, pitch])
81
98
 
82
- if result_path:
83
- return IOHandler.save_csv(results, result_path)
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
- Args:
93
- max_faces (int): Number of faces to detect.
94
- min_confidence (float): Confidence threshold (0–1).
95
-
96
- Raises:
97
- ValueError, RuntimeError: On invalid inputs or camera failure.
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=True
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
- np_image=frame,
155
+ src_np_image=frame,
127
156
  face_mesh_obj=face_mesh
128
157
  )
129
158
  except ValueError: