ImagePRO-Python 0.5.2__py3-none-any.whl → 1.0.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.
@@ -1,15 +1,13 @@
1
- import sys
2
- from pathlib import Path
1
+ from __future__ import annotations
3
2
 
4
3
  import cv2
4
+ import mediapipe as mp
5
5
  import numpy as np
6
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))
7
+ from ImagePRO.human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
8
+ from ImagePRO.utils.image import Image
9
+ from ImagePRO.utils.result import Result
10
10
 
11
- from utils.io_handler import IOHandler
12
- from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
13
11
 
14
12
  # Constants
15
13
  DEFAULT_MAX_FACES = 1
@@ -22,93 +20,102 @@ FACE_OUTLINE_INDICES = [
22
20
 
23
21
 
24
22
  def detect_faces(
23
+ image: Image,
24
+ *,
25
25
  max_faces: int = DEFAULT_MAX_FACES,
26
26
  min_confidence: float = DEFAULT_MIN_CONFIDENCE,
27
- src_image_path: str | None = None,
28
- src_np_image=None,
29
- output_image_path: str | None = None,
30
- face_mesh_obj=None,
31
- ):
27
+ face_mesh_obj: mp.solutions.face_mesh.FaceMesh | None = None
28
+ ) -> Result:
29
+ """Detect and crop face regions using facial landmarks.
30
+
31
+ Uses face mesh to locate face outline points,
32
+ then extracts rectangular regions containing each face.
33
+
34
+ Args:
35
+ image: Input image to process.
36
+ max_faces: Maximum number of faces to detect.
37
+ Must be positive.
38
+ Default: 1
39
+ min_confidence: Detection confidence threshold.
40
+ Must be between 0 and 1.
41
+ Default: 0.7
42
+ face_mesh_obj: Pre-initialized face mesh detector.
43
+ If None, creates new instance.
44
+ Default: None
45
+
46
+ Returns:
47
+ Result object with detections:
48
+ - image: List of cropped face images
49
+ None if no faces detected
50
+ - data: List of face outline polygons
51
+ None if no faces detected
52
+ - meta: Operation info and parameters
53
+ Includes error info if detection fails
54
+
55
+ Raises:
56
+ TypeError: If image is not an Image instance
57
+ ValueError: If max_faces is not positive
58
+ ValueError: If min_confidence not in [0,1]
32
59
  """
33
- Detect and crop face regions using MediaPipe facial landmarks.
34
-
35
- Parameters
36
- ----------
37
- max_faces : int, default=1
38
- Maximum number of faces to detect.
39
- min_confidence : float, default=0.7
40
- Detection confidence threshold in [0, 1].
41
- src_image_path : str | None, optional
42
- Path to input image file.
43
- src_np_image : np.ndarray | None, optional
44
- Image array in BGR format. If both provided, `src_np_image` takes precedence.
45
- output_image_path : str | None, optional
46
- If provided, saves cropped faces to disk (single path used as base name for multiple faces).
47
- face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
48
- Optional reusable FaceMesh instance.
49
-
50
- Returns
51
- -------
52
- list[np.ndarray] | str
53
- If `output_image_path` is provided, returns the save message from
54
- `IOHandler.save_image`. Otherwise, returns a list of cropped face images.
55
-
56
- Raises
57
- ------
58
- ValueError
59
- * If inputs are invalid or no face landmarks are detected (this function).
60
- * From IOHandler.load_image when both inputs are None or image loading fails.
61
- TypeError
62
- From IOHandler.load_image/save_image on invalid argument types.
63
- FileNotFoundError
64
- From IOHandler.load_image when `src_image_path` does not exist.
65
- IOError
66
- From IOHandler.save_image when saving the image fails.
67
- """
68
- # --- Validate inputs -----------------------------------------------------
60
+ if not isinstance(image, Image):
61
+ raise TypeError("'image' must be an Image instance")
62
+
69
63
  if not isinstance(max_faces, int) or max_faces <= 0:
70
- raise ValueError("'max_faces' must be a positive integer.")
64
+ raise ValueError("'max_faces' must be positive")
71
65
 
72
66
  if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
73
- raise ValueError("'min_confidence' must be a float between 0 and 1.")
67
+ raise ValueError("'min_confidence' must be between 0 and 1")
74
68
 
75
- # --- Load image -----------------------------------------------------------
76
- np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
77
- height, width = np_image.shape[:2]
69
+ # Get image dimensions
70
+ height, width = image.shape[:2]
78
71
 
79
- # Selected face outline landmark indices (MediaPipe 468-point model)
80
- face_outline_indices = FACE_OUTLINE_INDICES
81
-
82
- # --- Run face mesh analysis ----------------------------------------------
83
- _, raw_landmarks = analyze_face_mesh(
72
+ # Get face landmarks
73
+ result_mesh = analyze_face_mesh(
74
+ image=image,
84
75
  max_faces=max_faces,
85
76
  min_confidence=min_confidence,
86
- landmarks_idx=face_outline_indices,
87
- src_np_image=np_image,
77
+ landmarks_idx=FACE_OUTLINE_INDICES,
88
78
  face_mesh_obj=face_mesh_obj
89
79
  )
80
+ raw_landmarks = result_mesh.data
90
81
 
82
+ # Handle no detections
91
83
  if not raw_landmarks:
92
- raise ValueError("No face landmarks detected in the input image.")
93
-
94
- # --- Convert normalized landmark coordinates to pixel positions ----------
95
- all_polygons = []
84
+ return Result(
85
+ image=None,
86
+ data=None,
87
+ meta={
88
+ "source": image,
89
+ "operation": "detect_faces",
90
+ "max_faces": max_faces,
91
+ "min_confidence": min_confidence,
92
+ "error": "No face landmarks detected"
93
+ }
94
+ )
95
+
96
+ # Convert landmarks to pixel coordinates
97
+ face_polygons = []
96
98
  for face in raw_landmarks:
97
99
  polygon = [
98
100
  (int(x * width), int(y * height))
99
101
  for _, _, x, y, _ in face
100
102
  ]
101
- all_polygons.append(np.array(polygon, dtype=np.int32))
103
+ face_polygons.append(np.array(polygon, dtype=np.int32))
102
104
 
103
- # --- Crop faces using boundingRect ---------------------------------------
104
- cropped_faces = []
105
- for polygon in all_polygons:
105
+ # Extract face regions
106
+ face_regions = []
107
+ for polygon in face_polygons:
106
108
  x, y, w, h = cv2.boundingRect(polygon)
107
- cropped = np_image[y:y + h, x:x + w]
108
- cropped_faces.append(cropped)
109
-
110
- # --- Output handling ------------------------------------------------------
111
- if output_image_path:
112
- print(IOHandler.save_image(cropped_faces, output_image_path))
113
-
114
- return cropped_faces
109
+ face_region = image._data[y:y + h, x:x + w]
110
+ face_regions.append(face_region)
111
+
112
+ return Result(
113
+ image=face_regions,
114
+ data=face_polygons,
115
+ meta={
116
+ "source": image,
117
+ "operation": "detect_faces",
118
+ "max_faces": max_faces,
119
+ "min_confidence": min_confidence
120
+ }
121
+ )
@@ -1,14 +1,10 @@
1
- import sys
2
- from pathlib import Path
1
+ from __future__ import annotations
3
2
 
4
3
  import cv2
5
4
  import mediapipe as mp
6
5
 
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 utils.io_handler import IOHandler
6
+ from ImagePRO.utils.image import Image
7
+ from ImagePRO.utils.result import Result
12
8
 
13
9
  # Constants
14
10
  mp_face_mesh = mp.solutions.face_mesh
@@ -20,210 +16,222 @@ TOTAL_FACE_LANDMARKS = 468
20
16
 
21
17
 
22
18
  def analyze_face_mesh(
19
+ image: Image,
20
+ *,
23
21
  max_faces: int = DEFAULT_MAX_FACES,
24
22
  min_confidence: float = DEFAULT_MIN_CONFIDENCE,
25
- landmarks_idx: list | None = None,
26
- src_image_path: str | None = None,
27
- src_np_image=None,
28
- output_image_path: str | None = None,
29
- output_csv_path: str | None = None,
30
- face_mesh_obj=None,
31
- ):
32
- """
33
- Detect facial landmarks using MediaPipe FaceMesh on a single image (path or ndarray).
34
-
35
- Parameters
36
- ----------
37
- max_faces : int, default=1
38
- Maximum number of faces to detect.
39
- min_confidence : float, default=0.7
40
- Minimum detection confidence in [0, 1].
41
- landmarks_idx : list[int] | None, optional
42
- Specific landmark indices to extract/draw. If ``None``, uses the full
43
- 468-point mesh.
44
- src_image_path : str | None, optional
45
- Path to the input image (BGR/RGB supported by ``IOHandler.load_image``).
46
- src_np_image : np.ndarray | None, optional
47
- Image array in BGR (as returned by OpenCV). If both ``src_image_path`` and
48
- ``src_np_image`` are provided, the ndarray takes precedence.
49
- output_image_path : str | None, optional
50
- If provided (e.g. "out.jpg"/"out.png"), the annotated image is saved there.
51
- output_csv_path : str | None, optional
52
- If provided (e.g. "landmarks.csv"), landmark coordinates are saved there as
53
- rows: ``[face_id, index, x, y, z]`` with normalized x/y/z from MediaPipe.
54
- face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
55
- Reusable external FaceMesh instance. If ``None``, a new one is created
56
- with ``static_image_mode=True`` and ``refine_landmarks=True``.
57
-
58
- Returns
59
- -------
60
- tuple
61
- ``(annotated_image: np.ndarray, all_landmarks: list[list[list[float]]])``
62
- where ``all_landmarks`` is a list per face, each containing rows of
63
- ``[face_id, index, x, y, z]``.
64
-
65
- Raises
66
- ------
67
- ValueError
68
- * If inputs are invalid or no face landmarks are detected (this function).
69
- * From ``IOHandler.load_image`` when both inputs are ``None`` or image loading fails.
70
- TypeError
71
- * If ``landmarks_idx`` is not a list of integers (this function).
72
- * From ``IOHandler.load_image``/``save_image``/``save_csv`` on invalid argument types.
73
- FileNotFoundError
74
- From ``IOHandler.load_image`` when ``image_path`` does not exist.
75
- IOError
76
- From ``IOHandler.save_image`` when saving the image fails.
77
-
78
- Notes
79
- -----
80
- * The x/y/z coordinates from MediaPipe are **normalized** to [0, 1] in image
81
- space; multiply by width/height to get pixel coordinates.
82
- * When ``landmarks_idx`` covers all 468 points, the full tessellation is drawn.
83
- Otherwise, only the requested points are highlighted.
23
+ landmarks_idx: list[int] | None = None,
24
+ face_mesh_obj: mp.solutions.face_mesh.FaceMesh | None = None
25
+ ) -> Result:
26
+ """Detect facial landmarks using MediaPipe FaceMesh.
27
+
28
+ Extracts 3D facial mesh coordinates for precise face analysis.
29
+ Can focus on specific landmarks or detect full 468-point mesh.
30
+
31
+ Args:
32
+ image: Input image to process.
33
+ max_faces: Maximum number of faces to detect.
34
+ Must be positive.
35
+ Default: 1
36
+ min_confidence: Detection confidence threshold.
37
+ Must be between 0 and 1.
38
+ Default: 0.7
39
+ landmarks_idx: Specific landmark indices to detect.
40
+ If None, uses all 468 points.
41
+ Default: None
42
+ face_mesh_obj: Pre-initialized face mesh detector.
43
+ If None, creates new instance.
44
+ Default: None
45
+
46
+ Returns:
47
+ Result object with detections and visualization:
48
+ - image: Input image with landmarks drawn
49
+ None if no faces detected
50
+ - data: List of [face_id, point_id, x, y, z]
51
+ None if no faces detected
52
+ - meta: Operation info and parameters
53
+ Includes error info if detection fails
54
+
55
+ Raises:
56
+ TypeError: If image is not an Image instance
57
+ ValueError: If max_faces is not positive
58
+ ValueError: If min_confidence not in [0,1]
59
+ TypeError: If landmarks_idx is not list[int]
60
+
61
+ Notes:
62
+ - Coordinates are normalized [0,1]. Multiply by width/height for pixels
63
+ - Full mesh (468 points) shows tessellation, specific points show dots
84
64
  """
85
65
 
86
- # --- Validate inputs -----------------------------------------------------
66
+ # Validate inputs
67
+ if not isinstance(image, Image):
68
+ raise TypeError("'image' must be an Image instance")
69
+
87
70
  if not isinstance(max_faces, int) or max_faces <= 0:
88
- raise ValueError("'max_faces' must be a positive integer.")
71
+ raise ValueError("'max_faces' must be positive")
89
72
 
90
73
  if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
91
- raise ValueError("'min_confidence' must be a float between 0 and 1.")
74
+ raise ValueError("'min_confidence' must be between 0 and 1")
92
75
 
93
- if landmarks_idx is not None:
94
- if not isinstance(landmarks_idx, list) or not all(isinstance(i, int) for i in landmarks_idx):
95
- raise TypeError("'landmarks_idx' must be a list of integers or None.")
76
+ if landmarks_idx is not None and (
77
+ not isinstance(landmarks_idx, list)
78
+ or not all(isinstance(i, int) for i in landmarks_idx)
79
+ ):
80
+ raise TypeError("'landmarks_idx' must be a list of integers")
96
81
 
97
- # --- Create or reuse FaceMesh instance ----------------------------------
82
+ # Initialize detector if needed
98
83
  if face_mesh_obj is None:
99
- face_Mesh = mp_face_mesh.FaceMesh(
84
+ face_mesh = mp_face_mesh.FaceMesh(
100
85
  max_num_faces=max_faces,
101
86
  min_detection_confidence=min_confidence,
102
87
  refine_landmarks=True,
103
- static_image_mode=True,
88
+ static_image_mode=True
104
89
  )
105
90
  else:
106
- face_Mesh = face_mesh_obj
107
-
108
- # --- Load image (path or ndarray) ---------------------------------------
109
- np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
91
+ face_mesh = face_mesh_obj
110
92
 
111
- # Convert BGR -> RGB for MediaPipe
112
- rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
113
- results = face_Mesh.process(rgb_image)
93
+ # Prepare image
94
+ img_copy = image._data.copy()
95
+ img_rgb = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)
96
+
97
+ # Detect facial landmarks
98
+ results = face_mesh.process(img_rgb)
114
99
 
100
+ # Handle no detections
115
101
  if not results.multi_face_landmarks:
116
- raise ValueError("No face landmarks detected.")
117
-
118
- landmarks_idx = landmarks_idx or list(range(468))
119
-
120
- annotated = np_image.copy()
121
- all_landmarks = []
102
+ return Result(
103
+ image=None,
104
+ data=None,
105
+ meta={
106
+ "source": image,
107
+ "operation": "analyze_face_mesh",
108
+ "landmarks_idx": landmarks_idx,
109
+ "max_faces": max_faces,
110
+ "min_confidence": min_confidence,
111
+ "error": "No face landmarks detected"
112
+ }
113
+ )
122
114
 
115
+ # Use all landmarks if none specified
116
+ landmarks_idx = landmarks_idx or list(range(TOTAL_FACE_LANDMARKS))
117
+ # Process each detected face
118
+ landmarks = []
119
+
123
120
  for face_id, face_landmarks in enumerate(results.multi_face_landmarks):
124
- # Draw either the full tessellation or specific landmark dots
121
+ # Draw landmarks
125
122
  if len(landmarks_idx) == TOTAL_FACE_LANDMARKS:
123
+ # Draw full face mesh
126
124
  mp_drawing_utils.draw_landmarks(
127
- image=annotated,
125
+ image=img_copy,
128
126
  landmark_list=face_landmarks,
129
127
  connections=mp_face_mesh.FACEMESH_TESSELATION,
130
128
  landmark_drawing_spec=None,
131
- connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style(),
129
+ connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()
132
130
  )
133
131
  else:
134
- h, w = annotated.shape[:2]
132
+ # Draw specific points
133
+ h, w = img_copy.shape[:2]
135
134
  for idx in landmarks_idx:
136
135
  lm = face_landmarks.landmark[idx]
137
136
  cx, cy = int(w * lm.x), int(h * lm.y)
138
- cv2.circle(annotated, (cx, cy), 3, (0, 0, 255), -1)
137
+ cv2.circle(img_copy, (cx, cy), 3, (0, 0, 255), -1)
139
138
 
140
- # Collect normalized coordinates for requested indices
139
+ # Extract coordinates
141
140
  face_data = [
142
- [face_id, idx, face_landmarks.landmark[idx].x, face_landmarks.landmark[idx].y, face_landmarks.landmark[idx].z]
141
+ [face_id, idx, lm.x, lm.y, lm.z]
143
142
  for idx in landmarks_idx
143
+ for lm in [face_landmarks.landmark[idx]]
144
144
  ]
145
- all_landmarks.append(face_data)
146
-
147
- # --- Output handling -----------------------------------------------------
148
- if output_image_path is not None:
149
- # IOHandler.save_image returns a message/path; echo to stdout for logs
150
- print(IOHandler.save_image(annotated, output_image_path))
151
-
152
- if output_csv_path is not None:
153
- flat_data = [row for face in all_landmarks for row in face]
154
- print(IOHandler.save_csv(flat_data, output_csv_path))
155
-
156
- return annotated, all_landmarks
145
+ landmarks.append(face_data)
146
+
147
+ return Result(
148
+ image=img_copy,
149
+ data=landmarks,
150
+ meta={
151
+ "source": image,
152
+ "operation": "analyze_face_mesh",
153
+ "landmarks_idx": landmarks_idx,
154
+ "max_faces": max_faces,
155
+ "min_confidence": min_confidence
156
+ }
157
+ )
157
158
 
158
159
 
159
- def analyze_face_mesh_live(max_faces: int = DEFAULT_MAX_FACES, min_confidence: float = DEFAULT_MIN_CONFIDENCE):
160
- """
161
- Launch webcam preview with real-time FaceMesh overlay.
162
-
163
- Parameters
164
- ----------
165
- max_faces : int, default=1
166
- Max number of faces to detect per frame.
167
- min_confidence : float, default=0.7
168
- Minimum detection confidence in [0, 1].
169
-
170
- Raises
171
- ------
172
- ValueError
173
- If arguments are invalid.
174
- RuntimeError
175
- If the webcam cannot be opened.
176
-
177
- Notes
178
- -----
179
- * Uses ``static_image_mode=False`` for better performance on video streams.
180
- * Press **ESC** to exit the preview window.
160
+ def analyze_face_mesh_live(
161
+ *,
162
+ max_faces: int = DEFAULT_MAX_FACES,
163
+ min_confidence: float = DEFAULT_MIN_CONFIDENCE
164
+ ) -> None:
165
+ """Start live webcam feed with real-time face mesh visualization.
166
+
167
+ Opens default camera and shows face landmarks in real-time.
168
+ Uses tracking mode for better performance on video.
169
+ Press ESC to exit.
170
+
171
+ Args:
172
+ max_faces: Maximum number of faces to track.
173
+ Must be positive.
174
+ Default: 1
175
+ min_confidence: Detection confidence threshold.
176
+ Must be between 0 and 1.
177
+ Default: 0.7
178
+
179
+ Raises:
180
+ ValueError: If parameters are invalid
181
+ RuntimeError: If camera cannot be accessed
181
182
  """
183
+ # Validate parameters
182
184
  if not isinstance(max_faces, int) or max_faces <= 0:
183
- raise ValueError("'max_faces' must be a positive integer.")
185
+ raise ValueError("'max_faces' must be positive")
184
186
 
185
187
  if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
186
- raise ValueError("'min_confidence' must be between 0 and 1.")
188
+ raise ValueError("'min_confidence' must be between 0 and 1")
187
189
 
190
+ # Initialize camera
188
191
  cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
189
192
  if not cap.isOpened():
190
- raise RuntimeError("Unable to open webcam.")
193
+ raise RuntimeError("Cannot access webcam")
191
194
 
192
- # For live video, static_image_mode=False is preferred (tracking across frames)
193
- face_Mesh = mp_face_mesh.FaceMesh(
195
+ # Initialize detector in tracking mode for better performance
196
+ face_mesh = mp_face_mesh.FaceMesh(
194
197
  max_num_faces=max_faces,
195
198
  min_detection_confidence=min_confidence,
196
199
  refine_landmarks=True,
197
- static_image_mode=False,
200
+ static_image_mode=False
198
201
  )
199
202
 
200
203
  try:
201
204
  while True:
205
+ # Get frame
202
206
  success, frame = cap.read()
203
207
  if not success:
204
- print("Skipping empty frame.")
208
+ print("Failed to capture frame, skipping...")
205
209
  continue
206
210
 
211
+ # Process frame
207
212
  try:
208
- result_image, _ = analyze_face_mesh(
213
+ result = analyze_face_mesh(
214
+ image=Image.from_array(frame),
209
215
  max_faces=max_faces,
210
216
  min_confidence=min_confidence,
211
- src_np_image=frame,
212
- face_mesh_obj=face_Mesh,
217
+ face_mesh_obj=face_mesh
213
218
  )
214
- except ValueError:
215
- # No face detected in this frame; show raw frame
216
- result_image = frame
219
+ display_frame = result.image if result.image is not None else frame
220
+ except (TypeError, ValueError):
221
+ # Fall back to raw frame if detection fails
222
+ display_frame = frame
217
223
 
218
- cv2.imshow("ImagePRO - Face Mesh", result_image)
224
+ # Show result
225
+ cv2.imshow("ImagePRO - Face Mesh", display_frame)
219
226
 
220
- # Exit on ESC key
227
+ # Check for ESC key
221
228
  if cv2.waitKey(1) & 0xFF == 27:
222
229
  break
223
230
  finally:
231
+ # Clean up resources
224
232
  cap.release()
225
233
  cv2.destroyAllWindows()
226
234
 
227
235
 
228
236
  if __name__ == "__main__":
229
- analyze_face_mesh_live(max_faces=1)
237
+ analyze_face_mesh_live()