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,11 @@
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 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
6
+ from ImagePRO.human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
7
+ from ImagePRO.utils.image import Image
8
+ from ImagePRO.utils.result import Result
13
9
 
14
10
  # Constants
15
11
  mp_face_mesh = mp.solutions.face_mesh
@@ -19,125 +15,157 @@ HEAD_POSE_INDICES = [1, 152, 33, 263, 168] # nose_tip, chin, left_eye, right_ey
19
15
 
20
16
 
21
17
  def estimate_head_pose(
18
+ image: Image,
19
+ *,
22
20
  max_faces: int = DEFAULT_MAX_FACES,
23
21
  min_confidence: float = DEFAULT_MIN_CONFIDENCE,
24
- src_image_path: str | None = None,
25
- src_np_image=None,
26
- output_csv_path: str | None = None,
27
- face_mesh_obj=None,
28
- ):
29
- """
30
- Estimate head pose (yaw, pitch) from a single image using MediaPipe facial landmarks.
31
-
32
- Parameters
33
- ----------
34
- max_faces : int, default=1
35
- Number of faces to detect.
36
- min_confidence : float, default=0.7
37
- Detection confidence threshold in [0, 1].
38
- src_image_path : str | None, optional
39
- Path to the input image file.
40
- src_np_image : np.ndarray | None, optional
41
- Input image array in BGR format. If both are provided, `src_np_image` takes precedence.
42
- output_csv_path : str | None, optional
43
- If provided, saves the results as CSV rows: [face_id, yaw, pitch].
44
- face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
45
- Optional reusable FaceMesh instance.
46
-
47
- Returns
48
- -------
49
- list[list[float]] | str
50
- Save message from IOHandler.save_csv if `output_csv_path` is provided,
51
- otherwise list of [face_id, yaw, pitch] for each detected face.
52
-
53
- Raises
54
- ------
55
- ValueError
56
- * If inputs are invalid or no face landmarks are detected (this function).
57
- * From IOHandler.load_image when both inputs are None or image loading fails.
58
- TypeError
59
- From IOHandler.load_image/save_csv on invalid argument types.
60
- FileNotFoundError
61
- From IOHandler.load_image when `src_image_path` does not exist.
22
+ face_mesh_obj: mp.solutions.face_mesh.FaceMesh | None = None
23
+ ) -> Result:
24
+ """Estimate head pose angles using facial landmarks.
25
+
26
+ Calculates approximate yaw and pitch angles based on relative
27
+ positions of key facial landmarks (nose, eyes, chin).
28
+
29
+ Args:
30
+ image: Input image to process.
31
+ max_faces: Maximum number of faces to analyze.
32
+ Must be positive.
33
+ Default: 1
34
+ min_confidence: Detection confidence threshold.
35
+ Must be between 0 and 1.
36
+ Default: 0.7
37
+ face_mesh_obj: Pre-initialized face mesh detector.
38
+ If None, creates new instance.
39
+ Default: None
40
+
41
+ Returns:
42
+ Result object with pose estimates:
43
+ - data: List of [face_id, yaw, pitch] per face
44
+ None if no faces detected
45
+ - meta: Operation info and parameters
46
+ Includes error info if detection fails
47
+
48
+ Raises:
49
+ TypeError: If image is not an Image instance
50
+ ValueError: If max_faces is not positive
51
+ ValueError: If min_confidence not in [0,1]
62
52
  """
63
- if not isinstance(max_faces, int) or max_faces <= 0:
64
- raise ValueError("'max_faces' must be a positive integer.")
65
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
66
- raise ValueError("'min_confidence' must be between 0 and 1.")
53
+ if not isinstance(image, Image):
54
+ raise TypeError("'image' must be an Image instance")
67
55
 
68
- np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
56
+ if not isinstance(max_faces, int) or max_faces <= 0:
57
+ raise ValueError("'max_faces' must be positive")
69
58
 
70
- # Important landmark indices (MediaPipe 468 model)
71
- indices = HEAD_POSE_INDICES
59
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
60
+ raise ValueError("'min_confidence' must be between 0 and 1")
72
61
 
73
- _, landmarks = analyze_face_mesh(
62
+ # Get face landmarks
63
+ mesh_result = analyze_face_mesh(
64
+ image=image,
74
65
  max_faces=max_faces,
75
66
  min_confidence=min_confidence,
76
- landmarks_idx=indices,
77
- src_np_image=np_image,
67
+ landmarks_idx=HEAD_POSE_INDICES,
78
68
  face_mesh_obj=face_mesh_obj
79
69
  )
70
+ landmarks = mesh_result.data
80
71
 
72
+ # Handle no detections
81
73
  if not landmarks:
82
- raise ValueError("No face landmarks detected.")
83
-
84
- results = []
74
+ return Result(
75
+ image=None,
76
+ data=None,
77
+ meta={
78
+ "source": image,
79
+ "operation": "estimate_head_pose",
80
+ "max_faces": max_faces,
81
+ "min_confidence": min_confidence,
82
+ "error": "No face landmarks detected"
83
+ }
84
+ )
85
+
86
+ # Calculate angles for each face
87
+ pose_data = []
85
88
  for face in landmarks:
89
+ # Map landmarks by index
86
90
  points = {lm[1]: lm for lm in face}
87
-
91
+
88
92
  try:
89
- nose_x, nose_y = points[1][2:4]
90
- chin_y = points[152][3]
91
- left_x = points[33][2]
92
- right_x = points[263][2]
93
- nasion_x, nasion_y = points[168][2:4]
93
+ # Extract key points
94
+ nose_x, nose_y = points[1][2:4] # Nose tip
95
+ chin_y = points[152][3] # Chin
96
+ left_x = points[33][2] # Left eye
97
+ right_x = points[263][2] # Right eye
98
+ nasion_x, nasion_y = points[168][2:4] # Nose bridge
94
99
  except KeyError:
95
- continue # skip this face if any point is missing
96
-
97
- # Simplified proportional estimation on normalized coords
98
- yaw = 100 * ((right_x - nasion_x) - (nasion_x - left_x))
99
- pitch = 100 * ((chin_y - nose_y) - (nose_y - nasion_y))
100
-
101
- results.append([face[0][0], yaw, pitch])
102
-
103
- if output_csv_path:
104
- print(IOHandler.save_csv(results, output_csv_path))
105
-
106
- return results
100
+ return Result(
101
+ image=None,
102
+ data=None,
103
+ meta={
104
+ "source": image,
105
+ "operation": "estimate_head_pose",
106
+ "max_faces": max_faces,
107
+ "min_confidence": min_confidence,
108
+ "error": "Missing required landmarks"
109
+ }
110
+ )
111
+
112
+ # Calculate angles
113
+ yaw = 100 * ((right_x - nasion_x) - (nasion_x - left_x)) # Horizontal rotation
114
+ pitch = 100 * ((chin_y - nose_y) - (nose_y - nasion_y)) # Vertical rotation
115
+ pose_data.append([face[0][0], yaw, pitch])
116
+
117
+ return Result(
118
+ image=None,
119
+ data=pose_data,
120
+ meta={
121
+ "source": image,
122
+ "operation": "estimate_head_pose",
123
+ "max_faces": max_faces,
124
+ "min_confidence": min_confidence
125
+ }
126
+ )
107
127
 
108
128
 
109
- def estimate_head_pose_live(max_faces: int = 1, min_confidence: float = 0.7):
110
- """
111
- Live head pose estimation using the default webcam.
112
-
113
- Parameters
114
- ----------
115
- max_faces : int, default=1
116
- Number of faces to detect per frame.
117
- min_confidence : float, default=0.7
118
- Detection confidence threshold in [0, 1].
119
-
120
- Raises
121
- ------
122
- ValueError
123
- On invalid inputs.
124
- RuntimeError
125
- If the camera cannot be opened.
126
-
127
- Notes
128
- -----
129
- * Uses static_image_mode=False for better performance on video streams.
130
- * Press ESC to exit the preview window.
129
+ def estimate_head_pose_live(
130
+ *,
131
+ max_faces: int = DEFAULT_MAX_FACES,
132
+ min_confidence: float = DEFAULT_MIN_CONFIDENCE
133
+ ) -> None:
134
+ """Run live head pose estimation using webcam feed.
135
+
136
+ Opens a window displaying webcam feed with overlaid head pose angles.
137
+ Press ESC to exit.
138
+
139
+ Args:
140
+ max_faces: Maximum number of faces to analyze per frame.
141
+ Must be positive.
142
+ Default: 1
143
+ min_confidence: Detection confidence threshold.
144
+ Must be between 0 and 1.
145
+ Default: 0.7
146
+
147
+ Raises:
148
+ TypeError: If max_faces is not an integer
149
+ ValueError: If max_faces is not positive
150
+ ValueError: If min_confidence not in [0,1]
151
+ RuntimeError: If webcam cannot be accessed
131
152
  """
132
- if not isinstance(max_faces, int) or max_faces <= 0:
133
- raise ValueError("'max_faces' must be a positive integer.")
134
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
135
- raise ValueError("'min_confidence' must be between 0 and 1.")
136
-
153
+ # Validate inputs
154
+ if not isinstance(max_faces, int):
155
+ raise TypeError("'max_faces' must be an integer")
156
+ if max_faces <= 0:
157
+ raise ValueError("'max_faces' must be positive")
158
+ if not isinstance(min_confidence, (int, float)):
159
+ raise TypeError("'min_confidence' must be a number")
160
+ if not 0 <= min_confidence <= 1:
161
+ raise ValueError("'min_confidence' must be between 0 and 1")
162
+
163
+ # Initialize webcam
137
164
  cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
138
165
  if not cap.isOpened():
139
- raise RuntimeError("Failed to access webcam.")
166
+ raise RuntimeError("Failed to access webcam")
140
167
 
168
+ # Initialize face mesh detector for video
141
169
  face_mesh = mp_face_mesh.FaceMesh(
142
170
  max_num_faces=max_faces,
143
171
  min_detection_confidence=min_confidence,
@@ -147,31 +175,43 @@ def estimate_head_pose_live(max_faces: int = 1, min_confidence: float = 0.7):
147
175
 
148
176
  try:
149
177
  while True:
178
+ # Read frame
150
179
  ret, frame = cap.read()
151
180
  if not ret:
152
- print("Skipping empty frame.")
181
+ print("Warning: Failed to read frame, skipping...")
153
182
  continue
154
183
 
184
+ # Process frame
155
185
  try:
156
- face_angles = estimate_head_pose(
186
+ img = Image.from_array(frame)
187
+ result = estimate_head_pose(
188
+ image=img,
157
189
  max_faces=max_faces,
158
190
  min_confidence=min_confidence,
159
- src_np_image=frame,
160
191
  face_mesh_obj=face_mesh
161
192
  )
162
- except ValueError:
193
+ face_angles = result.data or []
194
+ except (TypeError, ValueError):
163
195
  face_angles = []
164
196
 
197
+ # Draw results
165
198
  for i, face in enumerate(face_angles):
166
199
  face_id, yaw, pitch = face
167
- text = f"Face {int(face_id)+1}: Yaw={yaw:.2f}, Pitch={pitch:.2f}"
168
- cv2.putText(frame, text, (10, 30 + i * 25),
169
- cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
200
+ text = f"Face {int(face_id)+1}: Yaw={yaw:.1f}, Pitch={pitch:.1f}"
201
+ cv2.putText(
202
+ frame, text,
203
+ (10, 30 + i * 25),
204
+ cv2.FONT_HERSHEY_SIMPLEX,
205
+ 0.6, (0, 255, 0), 2
206
+ )
170
207
 
208
+ # Display frame
171
209
  cv2.imshow("ImagePRO - Head Pose Estimation", frame)
172
210
 
173
- if cv2.waitKey(5) & 0xFF == 27: # ESC
211
+ # Check for ESC key
212
+ if cv2.waitKey(5) & 0xFF == 27:
174
213
  break
214
+
175
215
  finally:
176
216
  cap.release()
177
217
  cv2.destroyAllWindows()
@@ -1,12 +1,9 @@
1
- from ultralytics import YOLO
2
- import sys
3
- from pathlib import Path
1
+ from __future__ import annotations
4
2
 
5
- # Add parent directory to path for custom module imports
6
- parent_dir = Path(__file__).resolve().parent.parent
7
- sys.path.append(str(parent_dir))
3
+ from ultralytics import YOLO
8
4
 
9
- from utils.io_handler import IOHandler
5
+ from ImagePRO.utils.image import Image
6
+ from ImagePRO.utils.result import Result
10
7
 
11
8
  # Constants
12
9
  DEFAULT_ACCURACY_LEVEL = 1
@@ -21,80 +18,78 @@ MODEL_MAPPING = {
21
18
 
22
19
 
23
20
  def detect_objects(
24
- model=None,
21
+ image: Image,
22
+ *,
23
+ model: YOLO | None = None,
25
24
  accuracy_level: int = DEFAULT_ACCURACY_LEVEL,
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
25
  show_result: bool = False
31
- ):
32
- """
33
- Run object detection on a single image using Ultralytics YOLO.
26
+ ) -> Result:
27
+ """Detect objects in an image using YOLO models.
28
+
29
+ Uses Ultralytics YOLO for object detection with customizable
30
+ model size and accuracy tradeoffs.
34
31
 
35
- Parameters
36
- ----------
37
- model : ultralytics.engine.model.YOLO | None, optional
38
- A pre-loaded YOLO model instance. If provided, `accuracy_level` is ignored.
39
- If None, a model is created based on `accuracy_level`.
40
- accuracy_level : int, default=1
41
- Model size preset in {1..5} mapping to:
42
- 1 -> "yolo11n.pt", 2 -> "yolo11s.pt", 3 -> "yolo11m.pt",
43
- 4 -> "yolo11l.pt", 5 -> "yolo11x.pt".
44
- src_image_path : str | None, optional
45
- Path to the input image on disk. Used by IOHandler if `src_np_image` is None.
46
- src_np_image : np.ndarray | None, optional
47
- Image array (BGR). If provided, it is preferred over `src_image_path`.
48
- output_image_path : str | None, optional
49
- If provided, saves the annotated image (result.plot()) to this path.
50
- output_csv_path : str | None, optional
51
- If provided, saves detections as rows of `[class_id, [x1n, y1n, x2n, y2n], confidence]`.
52
- Note: coordinates are normalized in [0, 1].
53
- show_result : bool, default=False
54
- If True, shows the result window via `result.show()` (may require a GUI environment).
32
+ Args:
33
+ image: Input image to process.
34
+ model: Pre-loaded YOLO model. If None, loads based on accuracy_level.
35
+ Default: None
36
+ accuracy_level: Model size/accuracy preset (1-5):
37
+ 1: yolo11n (fastest)
38
+ 2: yolo11s
39
+ 3: yolo11m
40
+ 4: yolo11l
41
+ 5: yolo11x (most accurate)
42
+ Default: 1
43
+ show_result: Show detection visualization window.
44
+ Default: False
55
45
 
56
- Returns
57
- -------
58
- tuple
59
- (annotated_image: np.ndarray, lines: list[list])
60
- where `annotated_image` is the result of `result.plot()` and
61
- `lines` is a list of detection rows saved to CSV when requested.
46
+ Returns:
47
+ Result object with detections and metadata:
48
+ - image: Original with bounding boxes drawn
49
+ - data: List of [class_id, [x1,y1,x2,y2], confidence]
50
+ - meta: Operation info and model used
62
51
 
63
- Raises
64
- ------
65
- ValueError
66
- If `accuracy_level` is not in {1..5}, or if image loading fails inside IOHandler.
52
+ Raises:
53
+ TypeError: If image is not an Image instance
54
+ ValueError: If accuracy_level not in range 1-5
67
55
  """
68
- # Create a model from preset if not provided by caller
56
+ if not isinstance(image, Image):
57
+ raise TypeError("'image' must be an Image instance")
58
+
59
+ # Initialize model
60
+ model_name = None
69
61
  if model is None:
70
62
  if accuracy_level not in MODEL_MAPPING:
71
- raise ValueError(f"'accuracy_level' must be in {list(MODEL_MAPPING.keys())}, got {accuracy_level}")
63
+ raise ValueError(
64
+ f"'accuracy_level' must be in {list(MODEL_MAPPING.keys())}, "
65
+ f"got {accuracy_level}"
66
+ )
72
67
 
73
68
  model_name = MODEL_MAPPING[accuracy_level]
74
69
  model = YOLO(model=model_name)
75
70
 
76
- # Load image from path or ndarray using the shared IO helper
77
- frame = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
78
-
79
- # Run inference (first result only)
80
- result = model(frame)[0]
71
+ # Run inference
72
+ result = model(image._data.copy())[0]
81
73
  boxes = result.boxes
82
74
 
83
- # Collect rows as: [class_id, [x1n, y1n, x2n, y2n], confidence]
84
- lines = []
75
+ # Process detections
76
+ detections = []
85
77
  for box in boxes:
86
78
  box_class = int(box.cls)
87
- conf = float(box.conf)
79
+ confidence = float(box.conf)
88
80
  x1, y1, x2, y2 = [float(c) for c in box.xyxyn.squeeze().tolist()]
89
- lines.append([box_class, [x1, y1, x2, y2], conf])
81
+ detections.append([box_class, [x1, y1, x2, y2], confidence])
90
82
 
91
- # Optional visualization and saving
83
+ # Show visualization if requested
92
84
  if show_result:
93
85
  result.show()
94
- if output_image_path:
95
- print(IOHandler.save_image(np_image=result.plot(), result_path=output_image_path))
96
- if output_csv_path:
97
- print(IOHandler.save_csv(data=lines, result_path=output_csv_path))
98
-
99
- # Return the plotted image and the CSV-friendly rows
100
- return result.plot(), lines
86
+
87
+ return Result(
88
+ image=result.plot(),
89
+ data=detections,
90
+ meta={
91
+ "source": image,
92
+ "operation": "detect_objects",
93
+ "model": model_name or "custom"
94
+ }
95
+ )