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,42 +8,52 @@ 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_pose = mp.solutions.pose
14
14
 
15
15
  def detect_body_pose(
16
- model_accuracy=0.7,
17
- landmarks_idx=None,
18
- image_path=None,
19
- np_image=None,
20
- result_path=None,
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
- Detects body landmarks from an image using MediaPipe Pose.
25
-
26
- Args:
27
- model_accuracy (float): Confidence threshold (0.0 to 1.0).
28
- landmarks_idx (list[int] | None): Indices to extract (default: all 33).
29
- image_path (str | None): Path to image file.
30
- np_image (np.ndarray | None): Already loaded image (used if image_path is None).
31
- result_path (str | None): Path to save image (.jpg) or coordinates (.csv).
32
- pose_obj (mp.solutions.pose.Pose | None): Optional pre-initialized pose model.
33
-
34
- Returns:
35
- str | tuple[np.ndarray, list]: Output message or annotated image and landmark list.
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
- if result_path:
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 result_path is None or result_path.endswith('.jpg'):
65
- if len(landmarks_idx) == 33:
66
- mp.solutions.drawing_utils.draw_landmarks(
67
- annotated_image,
68
- result.pose_landmarks,
69
- mp_pose.POSE_CONNECTIONS,
70
- landmark_drawing_spec=mp.solutions.drawing_styles.get_default_pose_landmarks_style()
71
- )
72
- else:
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
- all_landmarks.append([idx, lm.x, lm.y, lm.z])
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
- if result_path:
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=True
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(np_image=frame, pose_obj=pose_obj)
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
 
@@ -8,51 +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
 
13
13
  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
- image_path=None,
21
- np_image=None,
22
- result_path=None,
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
- Detects hand landmarks in an image using MediaPipe.
27
-
28
- Args:
29
- max_hands (int): Max number of hands to detect.
30
- min_confidence (float): Minimum confidence for detection.
31
- landmarks_idx (list[int] | None): Landmark indices to extract (default: all 21).
32
- image_path (str | None): Image file path.
33
- np_image (np.ndarray | None): Preloaded image array.
34
- result_path (str | None): Path to save output image (.jpg) or landmarks (.csv).
35
- hands_obj (mp.solutions.hands.Hands | None): Optional pre-initialized model.
36
-
37
- Returns:
38
- np.ndarray | list: Annotated image and landmarks list, or saved file confirmation.
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
- if landmarks_idx is not None and not isinstance(landmarks_idx, list):
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 result_path is None or result_path.endswith('.jpg'):
76
- if len(landmarks_idx) == 21:
77
- mp.solutions.drawing_utils.draw_landmarks(
78
- image=annotated_image,
79
- landmark_list=hand_landmarks,
80
- connections=mp_hands.HAND_CONNECTIONS,
81
- landmark_drawing_spec=mp.solutions.drawing_styles.get_default_hand_landmarks_style(),
82
- connection_drawing_spec=mp.solutions.drawing_styles.get_default_hand_connections_style()
83
- )
84
- else:
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
- hand_data.append([hand_id, idx, lm.x, lm.y, lm.z])
96
- all_landmarks.append(hand_data)
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
- Args:
115
- max_hands (int): Maximum hands to detect.
116
- min_confidence (float): Detection confidence threshold.
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=True
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
- np_image=frame,
138
+ src_np_image=frame,
144
139
  hands_obj=hands_obj
145
140
  )
146
141
 
@@ -8,58 +8,78 @@ 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 analyze_eye_status(
18
- min_confidence=0.7,
19
- image_path=None,
20
- np_image=None,
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 using Eye Aspect Ratio (EAR).
26
-
27
- Args:
28
- min_confidence (float): Minimum confidence for FaceMesh detection.
29
- image_path (str, optional): Path to input image.
30
- np_image (np.ndarray, optional): Image array.
31
- face_mesh_obj (FaceMesh, optional): Reusable instance of FaceMesh.
32
- threshold (float): EAR threshold below which eye is considered closed.
33
-
34
- Returns:
35
- bool: True if eye is open, False if closed.
36
-
37
- Raises:
38
- ValueError: If landmarks are not detected or inputs are invalid.
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=image_path, np_image=np_image)
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 (from MediaPipe's model)
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
- np_image=np_image,
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
- Perform live eye open/closed detection using webcam input.
91
-
92
- Args:
93
- min_confidence (float): Minimum confidence for detection.
94
- threshold (float): EAR threshold to consider eyes open.
95
-
96
- Raises:
97
- ValueError: If confidence value is invalid.
98
- RuntimeError: If webcam cannot be accessed.
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=True
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
- np_image=frame,
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()
@@ -8,64 +8,85 @@ import numpy as np
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
 
15
15
  def detect_faces(
16
- max_faces=1,
17
- min_confidence=0.7,
18
- image_path=None,
19
- np_image=None,
20
- result_path=None,
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 face landmarks.
25
-
26
- Args:
27
- max_faces (int): Maximum number of faces to detect.
28
- min_confidence (float): Confidence threshold (0–1).
29
- image_path (str, optional): Path to input image.
30
- np_image (np.ndarray, optional): Image array.
31
- result_path (str, optional): Save path for cropped face(s).
32
- face_mesh_obj (FaceMesh, optional): Reusable MediaPipe instance.
33
-
34
- Returns:
35
- str or list[np.ndarray]: Confirmation message if saved, else list of cropped face images.
36
-
37
- Raises:
38
- TypeError, ValueError: On invalid inputs or if no faces detected.
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
- np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
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 (based on MediaPipe's 468-point model)
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
- np_image=np_image,
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
- return IOHandler.save_image(cropped_faces, result_path)
105
+ # --- Output handling ------------------------------------------------------
106
+ if output_image_path:
107
+ print(IOHandler.save_image(cropped_faces, output_image_path))
108
+
109
+ return cropped_faces