ImagePRO-Python 1.4.0__tar.gz → 1.5.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 (60) hide show
  1. {imagepro_python-1.4.0/src/ImagePRO_Python.egg-info → imagepro_python-1.5.0}/PKG-INFO +2 -2
  2. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/pyproject.toml +1 -1
  3. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/readme.md +1 -1
  4. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/__init__.py +1 -1
  5. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +10 -4
  6. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/body_analysis/hand_tracking.py +13 -7
  7. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +10 -5
  8. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/face_comparison.py +54 -61
  9. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/face_detection.py +3 -2
  10. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +20 -9
  11. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +20 -7
  12. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/object_analysis/object_detection.py +12 -3
  13. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pipeline/pipeline.py +3 -1
  14. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/blur.py +13 -6
  15. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/contrast.py +31 -13
  16. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/crop.py +6 -3
  17. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/dataset_generator.py +93 -26
  18. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/histogram.py +16 -11
  19. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/resize.py +4 -1
  20. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/rotate.py +2 -2
  21. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/sharpen.py +20 -21
  22. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/utils/image.py +11 -4
  23. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/utils/result.py +20 -13
  24. {imagepro_python-1.4.0 → imagepro_python-1.5.0/src/ImagePRO_Python.egg-info}/PKG-INFO +2 -2
  25. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_body_pose_estimation.py +9 -3
  26. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_contrast.py +32 -0
  27. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_crop.py +13 -0
  28. imagepro_python-1.5.0/tests/test_dataset_generator.py +190 -0
  29. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_eye_status_analysis.py +20 -13
  30. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_face_comparison.py +20 -5
  31. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_face_mesh_analysis.py +29 -0
  32. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_hand_tracking.py +7 -1
  33. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_head_pose_estimation.py +3 -1
  34. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_histogram.py +34 -0
  35. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_image.py +32 -0
  36. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_object_detection.py +4 -2
  37. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_result.py +32 -0
  38. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_sharpen.py +52 -0
  39. imagepro_python-1.4.0/tests/test_dataset_generator.py +0 -72
  40. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/LICENSE +0 -0
  41. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/setup.cfg +0 -0
  42. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/__init__.py +0 -0
  43. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/body_analysis/__init__.py +0 -0
  44. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/human_analysis/face_analysis/__init__.py +0 -0
  45. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/object_analysis/__init__.py +0 -0
  46. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pipeline/__init__.py +0 -0
  47. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/__init__.py +0 -0
  48. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/pre_processing/grayscale.py +0 -0
  49. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO/utils/__init__.py +0 -0
  50. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO_Python.egg-info/SOURCES.txt +0 -0
  51. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO_Python.egg-info/dependency_links.txt +0 -0
  52. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO_Python.egg-info/requires.txt +0 -0
  53. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/src/ImagePRO_Python.egg-info/top_level.txt +0 -0
  54. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_blur.py +0 -0
  55. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_face_detection.py +0 -0
  56. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_grayscale.py +0 -0
  57. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_integration.py +0 -0
  58. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_pipeline.py +0 -0
  59. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_resize.py +0 -0
  60. {imagepro_python-1.4.0 → imagepro_python-1.5.0}/tests/test_rotate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ImagePRO-Python
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Professional & modular image processing library with OpenCV, MediaPipe, and YOLO integration.
5
5
  Author-email: Parsa Safaie <parsasafaie.2568@proton.me>
6
6
  License: MIT
@@ -207,7 +207,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
207
207
  - **Documentation**: Google-style docstrings for all functions
208
208
 
209
209
  ### Key Design Principles
210
- - **Non-Destructive Operations**: Operations never write into the input image; they return results in new `Result` objects (note: `crop_image` returns a view into the source array)
210
+ - **Non-Destructive Operations**: Operations never write into the input image; they return results in new `Result` objects
211
211
  - **Functional Style**: Stateless functions that can be easily composed
212
212
  - **Result Objects**: Unified return type containing image, data, and metadata
213
213
  - **Keyword Arguments**: All optional parameters use keyword-only syntax
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "ImagePRO-Python"
10
- version = "1.4.0"
10
+ version = "1.5.0"
11
11
  description = "Professional & modular image processing library with OpenCV, MediaPipe, and YOLO integration."
12
12
  readme = "readme.md"
13
13
  requires-python = ">=3.9,<3.13"
@@ -158,7 +158,7 @@ ImagePRO is built with a modular architecture designed for extensibility and mai
158
158
  - **Documentation**: Google-style docstrings for all functions
159
159
 
160
160
  ### Key Design Principles
161
- - **Non-Destructive Operations**: Operations never write into the input image; they return results in new `Result` objects (note: `crop_image` returns a view into the source array)
161
+ - **Non-Destructive Operations**: Operations never write into the input image; they return results in new `Result` objects
162
162
  - **Functional Style**: Stateless functions that can be easily composed
163
163
  - **Result Objects**: Unified return type containing image, data, and metadata
164
164
  - **Keyword Arguments**: All optional parameters use keyword-only syntax
@@ -16,7 +16,7 @@ Features:
16
16
  - Real-time processing capabilities
17
17
  """
18
18
 
19
- __version__ = "1.4.0"
19
+ __version__ = "1.5.0"
20
20
  __author__ = "Parsa Safaie"
21
21
  __email__ = "parsasafaie.2568@proton.me"
22
22
 
@@ -23,6 +23,11 @@ TOTAL_LANDMARKS = 33
23
23
  DEFAULT_CONFIDENCE = 0.7
24
24
  LANDMARK_RADIUS = 3
25
25
  LANDMARK_COLOR = (0, 0, 255) # Red color for landmarks
26
+ # Backend-agnostic capture: the DSHOW backend only exists on Windows, so
27
+ # the default leaves the choice to OpenCV. Tests inject a fake through
28
+ # this hook.
29
+ DEFAULT_CAMERA_BACKEND = None
30
+
26
31
 
27
32
  def detect_body_pose(
28
33
  image: Image,
@@ -62,12 +67,13 @@ def detect_body_pose(
62
67
  if not isinstance(image, Image):
63
68
  raise TypeError("'image' must be an Image instance")
64
69
 
65
- if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
70
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
71
+ or not (0.0 <= min_confidence <= 1.0):
66
72
  raise ValueError("'min_confidence' must be between 0.0 and 1.0")
67
73
 
68
74
  if landmarks_idx is not None and (
69
75
  not isinstance(landmarks_idx, list)
70
- or not all(isinstance(i, int) for i in landmarks_idx)
76
+ or not all(isinstance(i, int) and not isinstance(i, bool) for i in landmarks_idx)
71
77
  ):
72
78
  raise TypeError("'landmarks_idx' must be a list of integers")
73
79
 
@@ -153,8 +159,8 @@ def detect_body_pose_live() -> None:
153
159
  'detection. Install it with: pip install "ImagePRO-Python[mediapipe]"'
154
160
  ) from err
155
161
 
156
- # Initialize camera
157
- cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
162
+ # Initialize camera (backend-agnostic; see DEFAULT_CAMERA_BACKEND)
163
+ cap = cv2.VideoCapture(0, DEFAULT_CAMERA_BACKEND)
158
164
  if not cap.isOpened():
159
165
  raise RuntimeError("Cannot access webcam")
160
166
 
@@ -24,6 +24,10 @@ DEFAULT_MIN_CONFIDENCE = 0.7
24
24
  TOTAL_HAND_LANDMARKS = 21
25
25
  LANDMARK_RADIUS = 3
26
26
  LANDMARK_COLOR = (0, 0, 255) # Red color for landmarks
27
+ # Backend-agnostic capture: the DSHOW backend only exists on Windows, so
28
+ # the default leaves the choice to OpenCV. Tests inject a fake through
29
+ # this hook.
30
+ DEFAULT_CAMERA_BACKEND = None
27
31
 
28
32
 
29
33
  def detect_hands(
@@ -69,15 +73,16 @@ def detect_hands(
69
73
  if not isinstance(image, Image):
70
74
  raise TypeError("'image' must be an Image instance")
71
75
 
72
- if not isinstance(max_hands, int) or max_hands <= 0:
76
+ if not isinstance(max_hands, int) or isinstance(max_hands, bool) or max_hands <= 0:
73
77
  raise ValueError("'max_hands' must be positive")
74
78
 
75
- if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
79
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
80
+ or not (0.0 <= min_confidence <= 1.0):
76
81
  raise ValueError("'min_confidence' must be between 0.0 and 1.0")
77
82
 
78
83
  if landmarks_idx is not None and (
79
84
  not isinstance(landmarks_idx, list)
80
- or not all(isinstance(i, int) for i in landmarks_idx)
85
+ or not all(isinstance(i, int) and not isinstance(i, bool) for i in landmarks_idx)
81
86
  ):
82
87
  raise TypeError("'landmarks_idx' must be a list of integers")
83
88
 
@@ -174,10 +179,11 @@ def detect_hands_live(
174
179
  RuntimeError: If camera cannot be accessed
175
180
  """
176
181
  # Validate parameters
177
- if not isinstance(max_hands, int) or max_hands <= 0:
182
+ if not isinstance(max_hands, int) or isinstance(max_hands, bool) or max_hands <= 0:
178
183
  raise ValueError("'max_hands' must be positive")
179
184
 
180
- if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
185
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
186
+ or not (0.0 <= min_confidence <= 1.0):
181
187
  raise ValueError("'min_confidence' must be between 0.0 and 1.0")
182
188
 
183
189
  try:
@@ -188,8 +194,8 @@ def detect_hands_live(
188
194
  'tracking. Install it with: pip install "ImagePRO-Python[mediapipe]"'
189
195
  ) from err
190
196
 
191
- # Initialize camera
192
- cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
197
+ # Initialize camera (backend-agnostic; see DEFAULT_CAMERA_BACKEND)
198
+ cap = cv2.VideoCapture(0, DEFAULT_CAMERA_BACKEND)
193
199
  if not cap.isOpened():
194
200
  raise RuntimeError("Cannot access webcam")
195
201
 
@@ -23,6 +23,10 @@ if TYPE_CHECKING: # mediapipe is imported lazily inside the functions
23
23
  DEFAULT_MIN_CONFIDENCE = 0.7
24
24
  DEFAULT_THRESHOLD = 0.2
25
25
  RIGHT_EYE_INDICES = [386, 374, 263, 362] # MediaPipe right eye landmarks
26
+ # Backend-agnostic capture: the DSHOW backend only exists on Windows, so
27
+ # the default leaves the choice to OpenCV. Tests inject a fake through
28
+ # this hook.
29
+ DEFAULT_CAMERA_BACKEND = None
26
30
 
27
31
 
28
32
  def analyze_eye_status(
@@ -62,7 +66,7 @@ def analyze_eye_status(
62
66
  if not isinstance(image, Image):
63
67
  raise TypeError("'image' must be an Image instance")
64
68
 
65
- if not isinstance(min_confidence, (int, float)):
69
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)):
66
70
  raise TypeError("'min_confidence' must be a number")
67
71
  if not 0 <= min_confidence <= 1:
68
72
  raise ValueError("'min_confidence' must be between 0 and 1")
@@ -178,7 +182,7 @@ def analyze_eye_status_live(
178
182
  RuntimeError: If webcam cannot be accessed
179
183
  """
180
184
  # Validate inputs
181
- if not isinstance(min_confidence, (int, float)):
185
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)):
182
186
  raise TypeError("'min_confidence' must be a number")
183
187
  if not 0 <= min_confidence <= 1:
184
188
  raise ValueError("'min_confidence' must be between 0 and 1")
@@ -191,8 +195,8 @@ def analyze_eye_status_live(
191
195
  'analysis. Install it with: pip install "ImagePRO-Python[mediapipe]"'
192
196
  ) from err
193
197
 
194
- # Initialize webcam
195
- cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
198
+ # Initialize webcam (backend-agnostic; see DEFAULT_CAMERA_BACKEND)
199
+ cap = cv2.VideoCapture(0, DEFAULT_CAMERA_BACKEND)
196
200
  if not cap.isOpened():
197
201
  raise RuntimeError("Failed to access webcam")
198
202
 
@@ -221,7 +225,8 @@ def analyze_eye_status_live(
221
225
  face_mesh_obj=face_mesh,
222
226
  threshold=threshold
223
227
  )
224
- status = "Open" if result.data else "Closed"
228
+ status = "No face" if result.data is None else \
229
+ ("Open" if result.data else "Closed")
225
230
  except (TypeError, ValueError):
226
231
  status = "No face"
227
232
 
@@ -10,8 +10,6 @@ _src_path = _file_path.parents[3] # Go up to src directory
10
10
  if str(_src_path) not in sys.path:
11
11
  sys.path.insert(0, str(_src_path))
12
12
 
13
- import os
14
-
15
13
  import cv2
16
14
  import numpy as np
17
15
 
@@ -36,7 +34,9 @@ def compare_faces(
36
34
  """Compare two face images to determine if they are the same person.
37
35
 
38
36
  Uses InsightFace's FaceAnalysis model to extract facial embeddings
39
- and compares them using cosine similarity.
37
+ and compares them using cosine similarity. Arrays are passed to the
38
+ model in memory; only path-based images are read from disk, so no
39
+ temporary files are written.
40
40
 
41
41
  Args:
42
42
  image_1: First image to compare.
@@ -57,7 +57,7 @@ def compare_faces(
57
57
 
58
58
  Raises:
59
59
  TypeError: If either image is not an Image instance
60
- FileNotFoundError: If image data cannot be loaded
60
+ FileNotFoundError: If a path-based image cannot be loaded
61
61
  """
62
62
  # Validate inputs
63
63
  if not isinstance(image_1, Image):
@@ -82,69 +82,62 @@ def compare_faces(
82
82
  )
83
83
  app.prepare(ctx_id=0) # Use CPU
84
84
 
85
- def save_temp_image(image: Image, temp_name: str) -> str:
86
- """Save image data to temporary file if needed."""
85
+ def load_rgb_image(image: Image) -> np.ndarray:
86
+ """Return the image as an RGB array, via disk only if required."""
87
87
  if image.source_type == 'path':
88
- return image.path
89
- temp_path = os.path.join(os.getcwd(), temp_name)
90
- cv2.imwrite(temp_path, image._data)
91
- return temp_path
92
-
93
- def load_rgb_image(path: str) -> np.ndarray | None:
94
- """Load image file and convert to RGB."""
95
- img = cv2.imread(str(path))
96
- return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if img is not None else None
97
-
98
- try:
99
- # Save images to disk if needed
100
- path_1 = save_temp_image(image_1, 'tmp1.jpg')
101
- path_2 = save_temp_image(image_2, 'tmp2.jpg')
102
-
103
- # Load and preprocess images
104
- img1 = load_rgb_image(path_1)
105
- img2 = load_rgb_image(path_2)
106
-
107
- if img1 is None or img2 is None:
108
- raise FileNotFoundError("Failed to load one or both images")
109
-
110
- # Detect faces
111
- faces1 = app.get(img1)
112
- faces2 = app.get(img2)
113
-
114
- # Validate detections
115
- if not faces1 or not faces2:
116
- return Result(
117
- image=None,
118
- data=None,
119
- meta={
120
- "source": (image_1, image_2),
121
- "operation": "compare_faces",
122
- "error": "No face detected in one or both images"
123
- }
124
- )
125
-
126
- # Extract embeddings
127
- emb1 = faces1[0].embedding
128
- emb2 = faces2[0].embedding
129
-
130
- # Calculate similarity
131
- similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
132
- is_match = similarity > DEFAULT_SIMILARITY_THRESHOLD
133
-
88
+ # honor the declared colorspace when reading from disk
89
+ img = cv2.imread(str(image.path))
90
+ if img is None:
91
+ raise FileNotFoundError(f"Failed to load image from {image.path}")
92
+ if image.colorspace == "RGB":
93
+ return img
94
+ if image.colorspace == "GRAY":
95
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
96
+ return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
97
+
98
+ # InsightFace accepts in-memory arrays; no temp file is written.
99
+ if image.colorspace == "RGB":
100
+ return image._data
101
+ if image.colorspace == "GRAY":
102
+ return cv2.cvtColor(image._data, cv2.COLOR_GRAY2RGB)
103
+ return cv2.cvtColor(image._data, cv2.COLOR_BGR2RGB)
104
+
105
+ img1 = load_rgb_image(image_1)
106
+ img2 = load_rgb_image(image_2)
107
+
108
+ # Detect faces
109
+ faces1 = app.get(img1)
110
+ faces2 = app.get(img2)
111
+
112
+ # Validate detections
113
+ if not faces1 or not faces2:
134
114
  return Result(
135
115
  image=None,
136
- data=is_match,
116
+ data=None,
137
117
  meta={
138
118
  "source": (image_1, image_2),
139
119
  "operation": "compare_faces",
140
- "similarity": float(similarity),
141
- "threshold": DEFAULT_SIMILARITY_THRESHOLD
120
+ "error": "No face detected in one or both images"
142
121
  }
143
122
  )
144
123
 
145
- finally:
146
- # Cleanup temporary files
147
- if image_1.source_type != 'path' and os.path.exists('tmp1.jpg'):
148
- os.remove('tmp1.jpg')
149
- if image_2.source_type != 'path' and os.path.exists('tmp2.jpg'):
150
- os.remove('tmp2.jpg')
124
+ # Extract embeddings
125
+ emb1 = faces1[0].embedding
126
+ emb2 = faces2[0].embedding
127
+
128
+ # Calculate similarity
129
+ similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
130
+ # Cast to a plain bool: np.dot yields np.bool_, which fails `is True`
131
+ # checks and serializes oddly in CSV/JSON output.
132
+ is_match = bool(similarity > DEFAULT_SIMILARITY_THRESHOLD)
133
+
134
+ return Result(
135
+ image=None,
136
+ data=is_match,
137
+ meta={
138
+ "source": (image_1, image_2),
139
+ "operation": "compare_faces",
140
+ "similarity": float(similarity),
141
+ "threshold": DEFAULT_SIMILARITY_THRESHOLD
142
+ }
143
+ )
@@ -72,10 +72,11 @@ def detect_faces(
72
72
  if not isinstance(image, Image):
73
73
  raise TypeError("'image' must be an Image instance")
74
74
 
75
- if not isinstance(max_faces, int) or max_faces <= 0:
75
+ if not isinstance(max_faces, int) or isinstance(max_faces, bool) or max_faces <= 0:
76
76
  raise ValueError("'max_faces' must be positive")
77
77
 
78
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
78
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
79
+ or not (0 <= min_confidence <= 1):
79
80
  raise ValueError("'min_confidence' must be between 0 and 1")
80
81
 
81
82
  # Get image dimensions
@@ -22,6 +22,10 @@ if TYPE_CHECKING: # mediapipe is imported lazily inside the functions
22
22
  DEFAULT_MAX_FACES = 1
23
23
  DEFAULT_MIN_CONFIDENCE = 0.7
24
24
  TOTAL_FACE_LANDMARKS = 468
25
+ # Backend-agnostic capture: the DSHOW backend only exists on Windows, so
26
+ # the default leaves the choice to OpenCV. Tests inject a fake through
27
+ # this hook.
28
+ DEFAULT_CAMERA_BACKEND = None
25
29
 
26
30
 
27
31
  def analyze_face_mesh(
@@ -38,7 +42,7 @@ def analyze_face_mesh(
38
42
  Can focus on specific landmarks or detect full 468-point mesh.
39
43
 
40
44
  Args:
41
- image: Input image to process.
45
+ image: Input image to process (BGR, RGB, or grayscale).
42
46
  max_faces: Maximum number of faces to detect.
43
47
  Must be positive.
44
48
  Default: 1
@@ -75,15 +79,16 @@ def analyze_face_mesh(
75
79
  if not isinstance(image, Image):
76
80
  raise TypeError("'image' must be an Image instance")
77
81
 
78
- if not isinstance(max_faces, int) or max_faces <= 0:
82
+ if not isinstance(max_faces, int) or isinstance(max_faces, bool) or max_faces <= 0:
79
83
  raise ValueError("'max_faces' must be positive")
80
84
 
81
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
85
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
86
+ or not (0 <= min_confidence <= 1):
82
87
  raise ValueError("'min_confidence' must be between 0 and 1")
83
88
 
84
89
  if landmarks_idx is not None and (
85
90
  not isinstance(landmarks_idx, list)
86
- or not all(isinstance(i, int) for i in landmarks_idx)
91
+ or not all(isinstance(i, int) and not isinstance(i, bool) for i in landmarks_idx)
87
92
  ):
88
93
  raise TypeError("'landmarks_idx' must be a list of integers")
89
94
 
@@ -110,9 +115,14 @@ def analyze_face_mesh(
110
115
  else:
111
116
  face_mesh = face_mesh_obj
112
117
 
113
- # Detect facial landmarks (cvtColor allocates a new RGB array)
114
- img_rgb = cv2.cvtColor(image._data, cv2.COLOR_BGR2RGB)
115
- results = face_mesh.process(img_rgb)
118
+ # MediaPipe expects RGB; grayscale input needs channels added back.
119
+ if image.colorspace == "GRAY":
120
+ model_input = cv2.cvtColor(image._data, cv2.COLOR_GRAY2RGB)
121
+ elif image.colorspace == "RGB":
122
+ model_input = image._data
123
+ else: # BGR is the default
124
+ model_input = cv2.cvtColor(image._data, cv2.COLOR_BGR2RGB)
125
+ results = face_mesh.process(model_input)
116
126
 
117
127
  # Handle no detections
118
128
  if not results.multi_face_landmarks:
@@ -214,8 +224,9 @@ def analyze_face_mesh_live(
214
224
  'analysis. Install it with: pip install "ImagePRO-Python[mediapipe]"'
215
225
  ) from err
216
226
 
217
- # Initialize camera
218
- cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
227
+ # Initialize camera (backend-agnostic; DEFAULT_CAMERA_BACKEND is a
228
+ # module-level hook tests can monkeypatch)
229
+ cap = cv2.VideoCapture(0, DEFAULT_CAMERA_BACKEND)
219
230
  if not cap.isOpened():
220
231
  raise RuntimeError("Cannot access webcam")
221
232
 
@@ -23,6 +23,10 @@ if TYPE_CHECKING: # mediapipe is imported lazily inside the functions
23
23
  DEFAULT_MAX_FACES = 1
24
24
  DEFAULT_MIN_CONFIDENCE = 0.7
25
25
  HEAD_POSE_INDICES = [1, 152, 33, 263, 168] # nose_tip, chin, left_eye, right_eye, nasion
26
+ # Backend-agnostic capture: the DSHOW backend only exists on Windows, so
27
+ # the default leaves the choice to OpenCV. Tests inject a fake through
28
+ # this hook.
29
+ DEFAULT_CAMERA_BACKEND = None
26
30
 
27
31
 
28
32
  def estimate_head_pose(
@@ -37,6 +41,12 @@ def estimate_head_pose(
37
41
  Calculates approximate yaw and pitch angles based on relative
38
42
  positions of key facial landmarks (nose, eyes, chin).
39
43
 
44
+ The values are unscaled proportional scores, not degrees: each is the
45
+ normalized landmark asymmetry multiplied by an arbitrary constant of
46
+ 100. They are useful for thresholds and trends (e.g. detecting when a
47
+ face turns away), not for measuring absolute angles. Mirror-image
48
+ webcam feeds flip the sign of yaw.
49
+
40
50
  Args:
41
51
  image: Input image to process.
42
52
  max_faces: Maximum number of faces to analyze.
@@ -55,6 +65,7 @@ def estimate_head_pose(
55
65
  None if no faces detected
56
66
  - meta: Operation info and parameters
57
67
  Includes error info if detection fails
68
+ - image: None
58
69
 
59
70
  Raises:
60
71
  TypeError: If image is not an Image instance
@@ -64,10 +75,11 @@ def estimate_head_pose(
64
75
  if not isinstance(image, Image):
65
76
  raise TypeError("'image' must be an Image instance")
66
77
 
67
- if not isinstance(max_faces, int) or max_faces <= 0:
78
+ if not isinstance(max_faces, int) or isinstance(max_faces, bool) or max_faces <= 0:
68
79
  raise ValueError("'max_faces' must be positive")
69
80
 
70
- if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
81
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
82
+ or not (0 <= min_confidence <= 1):
71
83
  raise ValueError("'min_confidence' must be between 0 and 1")
72
84
 
73
85
  # Get face landmarks
@@ -120,7 +132,8 @@ def estimate_head_pose(
120
132
  }
121
133
  )
122
134
 
123
- # Calculate angles
135
+ # Calculate angles. The ×100 constant is arbitrary; the values are
136
+ # proportional scores for thresholding, not degrees.
124
137
  yaw = 100 * ((right_x - nasion_x) - (nasion_x - left_x)) # Horizontal rotation
125
138
  pitch = 100 * ((chin_y - nose_y) - (nose_y - nasion_y)) # Vertical rotation
126
139
  pose_data.append([face[0][0], yaw, pitch])
@@ -162,11 +175,11 @@ def estimate_head_pose_live(
162
175
  RuntimeError: If webcam cannot be accessed
163
176
  """
164
177
  # Validate inputs
165
- if not isinstance(max_faces, int):
178
+ if not isinstance(max_faces, int) or isinstance(max_faces, bool):
166
179
  raise TypeError("'max_faces' must be an integer")
167
180
  if max_faces <= 0:
168
181
  raise ValueError("'max_faces' must be positive")
169
- if not isinstance(min_confidence, (int, float)):
182
+ if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)):
170
183
  raise TypeError("'min_confidence' must be a number")
171
184
  if not 0 <= min_confidence <= 1:
172
185
  raise ValueError("'min_confidence' must be between 0 and 1")
@@ -179,8 +192,8 @@ def estimate_head_pose_live(
179
192
  'estimation. Install it with: pip install "ImagePRO-Python[mediapipe]"'
180
193
  ) from err
181
194
 
182
- # Initialize webcam
183
- cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
195
+ # Initialize webcam (backend-agnostic; see DEFAULT_CAMERA_BACKEND)
196
+ cap = cv2.VideoCapture(0, DEFAULT_CAMERA_BACKEND)
184
197
  if not cap.isOpened():
185
198
  raise RuntimeError("Failed to access webcam")
186
199
 
@@ -18,6 +18,7 @@ if TYPE_CHECKING: # ultralytics is imported lazily inside the function
18
18
 
19
19
  # Constants
20
20
  DEFAULT_ACCURACY_LEVEL = 1
21
+ DEFAULT_DEVICE = "cpu" # CUDA builds fail on older GPUs (sm_50 and lower)
21
22
  MODEL_MAPPING = {
22
23
  1: "yolo11n.pt",
23
24
  2: "yolo11s.pt",
@@ -32,7 +33,8 @@ def detect_objects(
32
33
  *,
33
34
  model: YOLO | None = None,
34
35
  accuracy_level: int = DEFAULT_ACCURACY_LEVEL,
35
- show_result: bool = False
36
+ show_result: bool = False,
37
+ device: str = DEFAULT_DEVICE
36
38
  ) -> Result:
37
39
  """Detect objects in an image using YOLO models.
38
40
 
@@ -52,6 +54,12 @@ def detect_objects(
52
54
  Default: 1
53
55
  show_result: Show detection visualization window.
54
56
  Default: False
57
+ device: Torch device for inference ("cpu", "cuda", "0", ...).
58
+ Defaults to "cpu" because CUDA wheels no longer support older
59
+ GPUs (compute capability below 5.0 is unsupported by cu12/cu13
60
+ builds) and fail with cudaErrorNoKernelImageForDevice; pass
61
+ "cuda" explicitly to use the GPU.
62
+ Default: "cpu"
55
63
 
56
64
  Returns:
57
65
  Result object with detections and metadata:
@@ -89,7 +97,7 @@ def detect_objects(
89
97
  model = YOLO(model=model_name)
90
98
 
91
99
  # Run inference (ultralytics does not mutate the input array)
92
- result = model(image._data)[0]
100
+ result = model(image._data, device=device)[0]
93
101
  boxes = result.boxes
94
102
 
95
103
  # Process detections
@@ -110,6 +118,7 @@ def detect_objects(
110
118
  meta={
111
119
  "source": image,
112
120
  "operation": "detect_objects",
113
- "model": model_name or "custom"
121
+ "model": model_name or "custom",
122
+ "device": device
114
123
  }
115
124
  )
@@ -136,7 +136,9 @@ class Pipeline:
136
136
 
137
137
  Operations can be passed as separate arguments or as a single
138
138
  list/tuple. Passing another Pipeline as an operation nests it: the
139
- inner pipeline runs as a single step.
139
+ inner pipeline runs as a single step. An empty pipeline is accepted
140
+ at construction time but `run()` raises ValueError until at least
141
+ one operation is added with `apply()`.
140
142
 
141
143
  Args:
142
144
  *operations (Callable | Step | list | tuple):
@@ -56,7 +56,10 @@ def apply_average_blur(
56
56
  if (
57
57
  not isinstance(kernel_size, tuple)
58
58
  or len(kernel_size) != 2
59
- or not all(isinstance(k, int) and k > 0 for k in kernel_size)
59
+ or not all(
60
+ isinstance(k, int) and not isinstance(k, bool) and k > 0
61
+ for k in kernel_size
62
+ )
60
63
  ):
61
64
  raise ValueError("'kernel_size' must be a tuple of two positive integers.")
62
65
 
@@ -107,7 +110,10 @@ def apply_gaussian_blur(
107
110
  if (
108
111
  not isinstance(kernel_size, tuple)
109
112
  or len(kernel_size) != 2
110
- or not all(isinstance(k, int) and k > 0 and k % 2 == 1 for k in kernel_size)
113
+ or not all(
114
+ isinstance(k, int) and not isinstance(k, bool) and k > 0 and k % 2 == 1
115
+ for k in kernel_size
116
+ )
111
117
  ):
112
118
  raise ValueError("'kernel_size' must be a tuple of two odd positive integers.")
113
119
 
@@ -156,7 +162,8 @@ def apply_median_blur(
156
162
  if not isinstance(image, Image):
157
163
  raise TypeError("'image' must be an Image instance.")
158
164
 
159
- if not isinstance(filter_size, int) or filter_size <= 1 or filter_size % 2 == 0:
165
+ if not isinstance(filter_size, int) or isinstance(filter_size, bool) \
166
+ or filter_size <= 1 or filter_size % 2 == 0:
160
167
  raise ValueError("'filter_size' must be an odd integer greater than 1.")
161
168
 
162
169
  # Apply median filtering
@@ -212,11 +219,11 @@ def apply_bilateral_blur(
212
219
  if not isinstance(image, Image):
213
220
  raise TypeError("'image' must be an Image instance.")
214
221
 
215
- if not isinstance(filter_size, int) or filter_size <= 0:
222
+ if not isinstance(filter_size, int) or isinstance(filter_size, bool) or filter_size <= 0:
216
223
  raise ValueError("'filter_size' must be a positive integer.")
217
- if not isinstance(sigma_color, (int, float)) or sigma_color <= 0:
224
+ if isinstance(sigma_color, bool) or not isinstance(sigma_color, (int, float)) or sigma_color <= 0:
218
225
  raise ValueError("'sigma_color' must be a positive number.")
219
- if not isinstance(sigma_space, (int, float)) or sigma_space <= 0:
226
+ if isinstance(sigma_space, bool) or not isinstance(sigma_space, (int, float)) or sigma_space <= 0:
220
227
  raise ValueError("'sigma_space' must be a positive number.")
221
228
 
222
229
  # Apply bilateral filter