ImagePRO-Python 0.5.2__py3-none-any.whl → 1.0.1__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.
Files changed (27) hide show
  1. ImagePRO/__init__.py +1 -1
  2. ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +104 -72
  3. ImagePRO/human_analysis/body_analysis/hand_tracking.py +118 -74
  4. ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +149 -109
  5. ImagePRO/human_analysis/face_analysis/face_comparison.py +100 -37
  6. ImagePRO/human_analysis/face_analysis/face_detection.py +83 -76
  7. ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +153 -145
  8. ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +153 -113
  9. ImagePRO/object_analysis/object_detection.py +59 -64
  10. ImagePRO/pre_processing/blur.py +173 -155
  11. ImagePRO/pre_processing/contrast.py +129 -127
  12. ImagePRO/pre_processing/crop.py +45 -53
  13. ImagePRO/pre_processing/dataset_generator.py +97 -85
  14. ImagePRO/pre_processing/grayscale.py +42 -42
  15. ImagePRO/pre_processing/resize.py +41 -49
  16. ImagePRO/pre_processing/rotate.py +131 -102
  17. ImagePRO/pre_processing/sharpen.py +94 -73
  18. ImagePRO/utils/__init__.py +3 -2
  19. ImagePRO/utils/image.py +118 -0
  20. ImagePRO/utils/result.py +146 -0
  21. {imagepro_python-0.5.2.dist-info → imagepro_python-1.0.1.dist-info}/METADATA +2 -1
  22. imagepro_python-1.0.1.dist-info/RECORD +30 -0
  23. ImagePRO/utils/io_handler.py +0 -180
  24. imagepro_python-0.5.2.dist-info/RECORD +0 -29
  25. {imagepro_python-0.5.2.dist-info → imagepro_python-1.0.1.dist-info}/WHEEL +0 -0
  26. {imagepro_python-0.5.2.dist-info → imagepro_python-1.0.1.dist-info}/licenses/LICENSE +0 -0
  27. {imagepro_python-0.5.2.dist-info → imagepro_python-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,182 +1,222 @@
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
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
16
12
  DEFAULT_MIN_CONFIDENCE = 0.7
17
13
  DEFAULT_THRESHOLD = 0.2
18
- RIGHT_EYE_INDICES = [386, 374, 263, 362] # MediaPipe 468-point model
14
+ RIGHT_EYE_INDICES = [386, 374, 263, 362] # MediaPipe right eye landmarks
19
15
 
20
16
 
21
17
  def analyze_eye_status(
18
+ image: Image,
19
+ *,
22
20
  min_confidence: float = DEFAULT_MIN_CONFIDENCE,
23
- src_image_path: str | None = None,
24
- src_np_image=None,
25
- face_mesh_obj=None,
26
21
  threshold: float = DEFAULT_THRESHOLD,
27
- ) -> bool:
28
- """
29
- Analyze right-eye open/closed status via Eye Aspect Ratio (EAR).
30
-
31
- Parameters
32
- ----------
33
- min_confidence : float, default=0.7
34
- Minimum detection confidence for FaceMesh in [0, 1].
35
- src_image_path : str | None, optional
36
- Path to input image file.
37
- src_np_image : np.ndarray | None, optional
38
- Image array in BGR (OpenCV). If both provided, ``src_np_image`` takes precedence.
39
- face_mesh_obj : mediapipe.python.solutions.face_mesh.FaceMesh | None, optional
40
- Reusable FaceMesh instance; if ``None``, one is created (static image mode).
41
- threshold : float, default=0.2
42
- EAR threshold below which the eye is considered closed.
43
-
44
- Returns
45
- -------
46
- bool
47
- ``True`` if eye is open, ``False`` if closed.
48
-
49
- Raises
50
- ------
51
- ValueError
52
- * If ``min_confidence`` is out of range, or landmarks are not detected.
53
- * From ``IOHandler.load_image`` when both inputs are ``None`` or image loading fails.
54
- TypeError
55
- From ``IOHandler.load_image`` on invalid argument types.
56
- FileNotFoundError
57
- From ``IOHandler.load_image`` when ``src_image_path`` does not exist.
58
-
59
- Notes
60
- -----
61
- * Uses MediaPipe indices for the right eye:
62
- 386 (upper lid), 374 (lower lid), 263 (outer corner), 362 (inner corner).
63
- * EAR is computed as ``vertical / horizontal`` on pixel coordinates.
22
+ face_mesh_obj: mp.solutions.face_mesh.FaceMesh | None = None,
23
+ ) -> Result:
24
+ """Analyze if the right eye is open using Eye Aspect Ratio (EAR).
25
+
26
+ Calculates eye openness by comparing vertical to horizontal distance
27
+ between key eye landmarks. Uses FaceMesh landmark detection.
28
+
29
+ Args:
30
+ image: Input image to process.
31
+ min_confidence: Detection confidence threshold.
32
+ Must be between 0 and 1.
33
+ Default: 0.7
34
+ threshold: EAR threshold for open vs closed.
35
+ Eye considered open if EAR > threshold.
36
+ Default: 0.2
37
+ face_mesh_obj: Pre-initialized face mesh detector.
38
+ If None, creates new instance in static mode.
39
+ Default: None
40
+
41
+ Returns:
42
+ Result object with eye status:
43
+ - data: True if eye is open, False if closed
44
+ None if detection fails
45
+ - meta: Operation info and error details if failed
46
+
47
+ Raises:
48
+ TypeError: If image is not an Image instance
49
+ ValueError: If min_confidence not in [0,1]
64
50
  """
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.")
51
+ if not isinstance(image, Image):
52
+ raise TypeError("'image' must be an Image instance")
53
+
54
+ if not isinstance(min_confidence, (int, float)):
55
+ raise TypeError("'min_confidence' must be a number")
56
+ if not 0 <= min_confidence <= 1:
57
+ raise ValueError("'min_confidence' must be between 0 and 1")
67
58
 
68
- np_image = IOHandler.load_image(image_path=src_image_path, np_image=src_np_image)
69
- h, w = np_image.shape[:2]
59
+ # Get image dimensions
60
+ h, w = image.shape[:2]
70
61
 
62
+ # Initialize detector if needed
71
63
  if face_mesh_obj is None:
72
64
  face_mesh_obj = mp_face_mesh.FaceMesh(
73
65
  min_detection_confidence=min_confidence,
74
66
  refine_landmarks=True,
75
- static_image_mode=True,
67
+ static_image_mode=True
76
68
  )
77
69
 
78
- # Landmark indices for right eye (MediaPipe 468-point model)
79
- indices = RIGHT_EYE_INDICES
80
-
81
- _, landmarks = analyze_face_mesh(
70
+ # Get face landmarks
71
+ mesh_result = analyze_face_mesh(
72
+ image=image,
82
73
  max_faces=1,
83
74
  min_confidence=min_confidence,
84
- landmarks_idx=indices,
85
- src_np_image=np_image,
86
- face_mesh_obj=face_mesh_obj,
75
+ landmarks_idx=RIGHT_EYE_INDICES,
76
+ face_mesh_obj=face_mesh_obj
87
77
  )
78
+ landmarks = mesh_result.data
88
79
 
80
+ # Handle no detections
89
81
  if not landmarks:
90
- raise ValueError("No face landmarks detected.")
82
+ return Result(
83
+ image=None,
84
+ data=None,
85
+ meta={
86
+ "source": image,
87
+ "operation": "analyze_eye_status",
88
+ "min_confidence": min_confidence,
89
+ "threshold": threshold,
90
+ "error": "No face landmarks detected"
91
+ }
92
+ )
91
93
 
94
+ # Map landmarks by index
92
95
  eye_points = {lm[1]: lm for lm in landmarks[0]}
93
96
 
94
97
  try:
95
- top_y = eye_points[386][3] * h
96
- bottom_y = eye_points[374][3] * h
97
- left_x = eye_points[263][2] * w
98
- right_x = eye_points[362][2] * w
98
+ # Extract key points (scale to image dimensions)
99
+ top_y = eye_points[386][3] * h # Top of eye
100
+ bottom_y = eye_points[374][3] * h # Bottom of eye
101
+ left_x = eye_points[263][2] * w # Left corner
102
+ right_x = eye_points[362][2] * w # Right corner
99
103
  except KeyError as e:
100
- raise ValueError("Missing necessary eye landmarks.") from e
104
+ return Result(
105
+ image=None,
106
+ data=None,
107
+ meta={
108
+ "source": image,
109
+ "operation": "analyze_eye_status",
110
+ "min_confidence": min_confidence,
111
+ "threshold": threshold,
112
+ "error": f"Missing landmark: {e}"
113
+ }
114
+ )
101
115
 
116
+ # Calculate Eye Aspect Ratio (EAR)
102
117
  vertical_dist = abs(bottom_y - top_y)
103
118
  horizontal_dist = abs(right_x - left_x)
104
119
 
105
- if horizontal_dist == 0:
106
- return False # avoid division by zero
107
-
108
- ear = vertical_dist / horizontal_dist
109
- return ear > threshold
120
+ # Determine eye state
121
+ is_open = False
122
+ if horizontal_dist > 0: # Avoid division by zero
123
+ ear = vertical_dist / horizontal_dist
124
+ is_open = ear > threshold
125
+
126
+ return Result(
127
+ image=None,
128
+ data=is_open,
129
+ meta={
130
+ "source": image,
131
+ "operation": "analyze_eye_status",
132
+ "min_confidence": min_confidence,
133
+ "threshold": threshold
134
+ }
135
+ )
110
136
 
111
137
 
112
- def analyze_eye_status_live(min_confidence: float = 0.7, threshold: float = 0.2) -> None:
113
- """
114
- Live eye open/closed detection using the default webcam.
115
-
116
- Parameters
117
- ----------
118
- min_confidence : float, default=0.7
119
- Minimum detection confidence in [0, 1].
120
- threshold : float, default=0.2
121
- EAR threshold to consider eyes open.
122
-
123
- Raises
124
- ------
125
- ValueError
126
- If ``min_confidence`` is invalid.
127
- RuntimeError
128
- If the webcam cannot be accessed.
129
-
130
- Notes
131
- -----
132
- * Uses ``static_image_mode=False`` for better performance on video streams.
133
- * Press **ESC** to exit.
138
+ def analyze_eye_status_live(
139
+ *,
140
+ min_confidence: float = DEFAULT_MIN_CONFIDENCE,
141
+ threshold: float = DEFAULT_THRESHOLD
142
+ ) -> None:
143
+ """Run live eye status detection using webcam feed.
144
+
145
+ Opens a window displaying webcam feed with overlaid eye status.
146
+ Press ESC to exit.
147
+
148
+ Args:
149
+ min_confidence: Detection confidence threshold.
150
+ Must be between 0 and 1.
151
+ Default: 0.7
152
+ threshold: EAR threshold for open vs closed.
153
+ Eye considered open if EAR > threshold.
154
+ Default: 0.2
155
+
156
+ Raises:
157
+ TypeError: If min_confidence is not a number
158
+ ValueError: If min_confidence not in [0,1]
159
+ RuntimeError: If webcam cannot be accessed
134
160
  """
135
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
136
- raise ValueError("'min_confidence' must be between 0 and 1.")
161
+ # Validate inputs
162
+ if not isinstance(min_confidence, (int, float)):
163
+ raise TypeError("'min_confidence' must be a number")
164
+ if not 0 <= min_confidence <= 1:
165
+ raise ValueError("'min_confidence' must be between 0 and 1")
137
166
 
167
+ # Initialize webcam
138
168
  cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
139
169
  if not cap.isOpened():
140
- raise RuntimeError("Cannot access webcam.")
170
+ raise RuntimeError("Failed to access webcam")
141
171
 
172
+ # Initialize face mesh detector for video
142
173
  face_mesh = mp_face_mesh.FaceMesh(
143
174
  max_num_faces=1,
144
175
  min_detection_confidence=min_confidence,
145
176
  refine_landmarks=True,
146
- static_image_mode=False, # better for live video
177
+ static_image_mode=False # Optimize for video
147
178
  )
148
179
 
149
180
  try:
150
181
  while True:
151
- success, frame = cap.read()
152
- if not success:
153
- print("Skipping empty frame.")
182
+ # Read frame
183
+ ret, frame = cap.read()
184
+ if not ret:
185
+ print("Warning: Failed to read frame, skipping...")
154
186
  continue
155
187
 
188
+ # Process frame
156
189
  try:
157
- is_open = analyze_eye_status(
190
+ img = Image.from_array(frame)
191
+ result = analyze_eye_status(
192
+ image=img,
158
193
  min_confidence=min_confidence,
159
- src_np_image=frame,
160
194
  face_mesh_obj=face_mesh,
161
- threshold=threshold,
195
+ threshold=threshold
162
196
  )
163
- status = "Open" if is_open else "Closed"
164
- except ValueError:
197
+ status = "Open" if result.data else "Closed"
198
+ except (TypeError, ValueError):
165
199
  status = "No face"
166
200
 
201
+ # Draw results
202
+ color = (0, 255, 0) if status == "Open" else (0, 0, 255)
167
203
  cv2.putText(
168
204
  frame,
169
205
  f"Eye: {status}",
170
206
  (10, 40),
171
207
  cv2.FONT_HERSHEY_SIMPLEX,
172
208
  0.7,
173
- (0, 255, 0) if status == "Open" else (0, 0, 255),
174
- 2,
209
+ color,
210
+ 2
175
211
  )
176
212
 
177
- cv2.imshow("ImagePRO - Eye Status (ESC to Exit)", frame)
213
+ # Display frame
214
+ cv2.imshow("ImagePRO - Eye Status", frame)
215
+
216
+ # Check for ESC key
178
217
  if cv2.waitKey(5) & 0xFF == 27:
179
218
  break
219
+
180
220
  finally:
181
221
  cap.release()
182
222
  cv2.destroyAllWindows()
@@ -1,6 +1,13 @@
1
- from insightface.app import FaceAnalysis
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
2
5
  import cv2
3
6
  import numpy as np
7
+ from insightface.app import FaceAnalysis
8
+
9
+ from ImagePRO.utils.image import Image
10
+ from ImagePRO.utils.result import Result
4
11
 
5
12
  # Constants
6
13
  DEFAULT_SIMILARITY_THRESHOLD = 0.5
@@ -8,59 +15,115 @@ DEFAULT_MODEL_NAME = "buffalo_l"
8
15
  DEFAULT_PROVIDER = "CPUExecutionProvider"
9
16
 
10
17
 
11
- def compare_faces(src_image_path_1, src_image_path_2, app=None):
12
- """
13
- Compare two face images using FaceAnalysis embeddings.
18
+ def compare_faces(
19
+ image_1: Image,
20
+ image_2: Image,
21
+ *,
22
+ app: FaceAnalysis | None = None
23
+ ) -> Result:
24
+ """Compare two face images to determine if they are the same person.
14
25
 
15
- This function calculates the cosine similarity between the embeddings
16
- of two detected faces to check if they belong to the same person.
26
+ Uses InsightFace's FaceAnalysis model to extract facial embeddings
27
+ and compares them using cosine similarity.
17
28
 
18
29
  Args:
19
- src_image_path_1 (str | Path): Path to the first image.
20
- src_image_path_2 (str | Path): Path to the second image.
21
- app (FaceAnalysis, optional): Pre-loaded FaceAnalysis model instance.
22
- If not provided, it will be initialized inside the function.
30
+ image_1: First image to compare.
31
+ Must contain a clearly visible face.
32
+ image_2: Second image to compare.
33
+ Must contain a clearly visible face.
34
+ app: Pre-initialized FaceAnalysis model.
35
+ If None, creates new instance.
36
+ Default: None
23
37
 
24
38
  Returns:
25
- bool | str:
26
- - True : Faces match (cosine similarity > 0.5).
27
- - False : Faces do not match.
28
- - 'No Face Detected' : If no face is found in either image.
39
+ Result object with comparison results:
40
+ - image: None
41
+ - data: True if same person, False if different
42
+ None if face detection fails
43
+ - meta: Operation info and similarity score
44
+ Error details if detection fails
45
+
46
+ Raises:
47
+ TypeError: If either image is not an Image instance
48
+ FileNotFoundError: If image data cannot be loaded
29
49
  """
30
- # Prepare FaceAnalysis model if not provided
50
+ # Validate inputs
51
+ if not isinstance(image_1, Image):
52
+ raise TypeError("'image_1' must be an Image instance")
53
+ if not isinstance(image_2, Image):
54
+ raise TypeError("'image_2' must be an Image instance")
55
+
56
+ # Initialize model if needed
31
57
  if app is None:
32
58
  app = FaceAnalysis(
33
59
  name=DEFAULT_MODEL_NAME,
34
- providers=[DEFAULT_PROVIDER],
60
+ providers=[DEFAULT_PROVIDER]
35
61
  )
36
- app.prepare(ctx_id=0) # Run on CPU
62
+ app.prepare(ctx_id=0) # Use CPU
63
+
64
+ def save_temp_image(image: Image, temp_name: str) -> str:
65
+ """Save image data to temporary file if needed."""
66
+ if image.source_type == 'path':
67
+ return image.path
68
+ temp_path = os.path.join(os.getcwd(), temp_name)
69
+ cv2.imwrite(temp_path, image._data)
70
+ return temp_path
37
71
 
38
- # Helper function to read an image and convert to RGB
39
- def load_rgb(path):
72
+ def load_rgb_image(path: str) -> np.ndarray | None:
73
+ """Load image file and convert to RGB."""
40
74
  img = cv2.imread(str(path))
41
75
  return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if img is not None else None
42
76
 
43
- # Load and preprocess both images
44
- img1 = load_rgb(src_image_path_1)
45
- img2 = load_rgb(src_image_path_2)
77
+ try:
78
+ # Save images to disk if needed
79
+ path_1 = save_temp_image(image_1._data, 'tmp1.jpg')
80
+ path_2 = save_temp_image(image_2._data, 'tmp2.jpg')
46
81
 
47
- if img1 is None or img2 is None:
48
- return 'Invalid Image Path'
82
+ # Load and preprocess images
83
+ img1 = load_rgb_image(path_1)
84
+ img2 = load_rgb_image(path_2)
49
85
 
50
- # Detect faces in both images
51
- faces1 = app.get(img1)
52
- faces2 = app.get(img2)
86
+ if img1 is None or img2 is None:
87
+ raise FileNotFoundError("Failed to load one or both images")
53
88
 
54
- # Ensure a face was detected in both images
55
- if not faces1 or not faces2:
56
- return 'No Face Detected'
89
+ # Detect faces
90
+ faces1 = app.get(img1)
91
+ faces2 = app.get(img2)
57
92
 
58
- # Get the embeddings of the first detected face in each image
59
- emb1 = faces1[0].embedding
60
- emb2 = faces2[0].embedding
93
+ # Validate detections
94
+ if not faces1 or not faces2:
95
+ return Result(
96
+ image=None,
97
+ data=None,
98
+ meta={
99
+ "source": (image_1, image_2),
100
+ "operation": "compare_faces",
101
+ "error": "No face detected in one or both images"
102
+ }
103
+ )
61
104
 
62
- # Compute cosine similarity between the embeddings
63
- sim = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
105
+ # Extract embeddings
106
+ emb1 = faces1[0].embedding
107
+ emb2 = faces2[0].embedding
108
+
109
+ # Calculate similarity
110
+ similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
111
+ is_match = similarity > DEFAULT_SIMILARITY_THRESHOLD
112
+
113
+ return Result(
114
+ image=None,
115
+ data=is_match,
116
+ meta={
117
+ "source": (image_1, image_2),
118
+ "operation": "compare_faces",
119
+ "similarity": float(similarity),
120
+ "threshold": DEFAULT_SIMILARITY_THRESHOLD
121
+ }
122
+ )
64
123
 
65
- # Return match result based on threshold
66
- return True if sim > DEFAULT_SIMILARITY_THRESHOLD else False
124
+ finally:
125
+ # Cleanup temporary files
126
+ if image_1.source_type != 'path' and os.path.exists('tmp1.jpg'):
127
+ os.remove('tmp1.jpg')
128
+ if image_2.source_type != 'path' and os.path.exists('tmp2.jpg'):
129
+ os.remove('tmp2.jpg')