ImagePRO-Python 0.1.1__tar.gz → 0.2.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.
Files changed (40) hide show
  1. {imagepro_python-0.1.1/src/ImagePRO_Python.egg-info → imagepro_python-0.2.0}/PKG-INFO +1 -1
  2. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/pyproject.toml +1 -1
  3. imagepro_python-0.2.0/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +133 -0
  4. imagepro_python-0.2.0/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +152 -0
  5. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +72 -42
  6. imagepro_python-0.2.0/src/ImagePRO/human_analysis/face_analysis/face_detection.py +109 -0
  7. imagepro_python-0.2.0/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +225 -0
  8. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +67 -38
  9. imagepro_python-0.2.0/src/ImagePRO/pre_processing/blur.py +198 -0
  10. imagepro_python-0.2.0/src/ImagePRO/pre_processing/contrast.py +153 -0
  11. imagepro_python-0.2.0/src/ImagePRO/pre_processing/crop.py +76 -0
  12. imagepro_python-0.2.0/src/ImagePRO/pre_processing/grayscale.py +49 -0
  13. imagepro_python-0.2.0/src/ImagePRO/pre_processing/resize.py +64 -0
  14. imagepro_python-0.2.0/src/ImagePRO/pre_processing/rotate.py +115 -0
  15. imagepro_python-0.2.0/src/ImagePRO/pre_processing/sharpen.py +90 -0
  16. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/utils/io_handler.py +5 -3
  17. {imagepro_python-0.1.1 → imagepro_python-0.2.0/src/ImagePRO_Python.egg-info}/PKG-INFO +1 -1
  18. imagepro_python-0.1.1/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +0 -129
  19. imagepro_python-0.1.1/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +0 -157
  20. imagepro_python-0.1.1/src/ImagePRO/human_analysis/face_analysis/face_detection.py +0 -84
  21. imagepro_python-0.1.1/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +0 -172
  22. imagepro_python-0.1.1/src/ImagePRO/pre_processing/blur.py +0 -130
  23. imagepro_python-0.1.1/src/ImagePRO/pre_processing/contrast.py +0 -105
  24. imagepro_python-0.1.1/src/ImagePRO/pre_processing/crop.py +0 -51
  25. imagepro_python-0.1.1/src/ImagePRO/pre_processing/grayscale.py +0 -27
  26. imagepro_python-0.1.1/src/ImagePRO/pre_processing/resize.py +0 -43
  27. imagepro_python-0.1.1/src/ImagePRO/pre_processing/rotate.py +0 -94
  28. imagepro_python-0.1.1/src/ImagePRO/pre_processing/sharpen.py +0 -74
  29. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/LICENSE +0 -0
  30. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/setup.cfg +0 -0
  31. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/__init__.py +0 -0
  32. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/human_analysis/__init__.py +0 -0
  33. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
  34. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
  35. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/pre_processing/__init__.py +0 -0
  36. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO/utils/__init__.py +0 -0
  37. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO_Python.egg-info/SOURCES.txt +0 -0
  38. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO_Python.egg-info/dependency_links.txt +0 -0
  39. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO_Python.egg-info/requires.txt +0 -0
  40. {imagepro_python-0.1.1 → imagepro_python-0.2.0}/src/ImagePRO_Python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ImagePRO-Python
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: clean, modular, and easy-to-use Python library for image processing tasks.
5
5
  Author-email: Parsa Safaie <parsasafaie.2568@proton.me>
6
6
  License: MIT
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "ImagePRO-Python"
10
- version = "0.1.1"
10
+ version = "0.2.0"
11
11
  description = "clean, modular, and easy-to-use Python library for image processing tasks."
12
12
  readme = "README.md"
13
13
  requires-python = "==3.10.*"
@@ -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
- 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()
@@ -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