ImagePRO-Python 1.3.1__py3-none-any.whl → 1.5.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.
- ImagePRO/__init__.py +5 -2
- ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +10 -4
- ImagePRO/human_analysis/body_analysis/hand_tracking.py +13 -7
- ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +10 -5
- ImagePRO/human_analysis/face_analysis/face_comparison.py +54 -61
- ImagePRO/human_analysis/face_analysis/face_detection.py +3 -2
- ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +20 -9
- ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +20 -7
- ImagePRO/object_analysis/object_detection.py +12 -3
- ImagePRO/pipeline/__init__.py +9 -0
- ImagePRO/pipeline/pipeline.py +364 -0
- ImagePRO/pre_processing/blur.py +13 -6
- ImagePRO/pre_processing/contrast.py +31 -13
- ImagePRO/pre_processing/crop.py +6 -3
- ImagePRO/pre_processing/dataset_generator.py +93 -26
- ImagePRO/pre_processing/histogram.py +16 -11
- ImagePRO/pre_processing/resize.py +4 -1
- ImagePRO/pre_processing/rotate.py +2 -2
- ImagePRO/pre_processing/sharpen.py +20 -21
- ImagePRO/utils/image.py +11 -4
- ImagePRO/utils/result.py +20 -13
- {imagepro_python-1.3.1.dist-info → imagepro_python-1.5.0.dist-info}/METADATA +25 -2
- imagepro_python-1.5.0.dist-info/RECORD +33 -0
- imagepro_python-1.3.1.dist-info/RECORD +0 -31
- {imagepro_python-1.3.1.dist-info → imagepro_python-1.5.0.dist-info}/WHEEL +0 -0
- {imagepro_python-1.3.1.dist-info → imagepro_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {imagepro_python-1.3.1.dist-info → imagepro_python-1.5.0.dist-info}/top_level.txt +0 -0
ImagePRO/__init__.py
CHANGED
|
@@ -12,10 +12,11 @@ Features:
|
|
|
12
12
|
- Pre-processing: resize, crop, rotate, blur, sharpen, contrast enhancement
|
|
13
13
|
- Human analysis: face mesh, pose estimation, hand tracking
|
|
14
14
|
- Object detection with YOLO models
|
|
15
|
+
- Reusable pipelines for chaining operations
|
|
15
16
|
- Real-time processing capabilities
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
|
-
__version__ = "1.
|
|
19
|
+
__version__ = "1.5.0"
|
|
19
20
|
__author__ = "Parsa Safaie"
|
|
20
21
|
__email__ = "parsasafaie.2568@proton.me"
|
|
21
22
|
|
|
@@ -24,10 +25,12 @@ from . import utils
|
|
|
24
25
|
from . import pre_processing
|
|
25
26
|
from . import human_analysis
|
|
26
27
|
from . import object_analysis
|
|
28
|
+
from . import pipeline
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
31
|
"utils",
|
|
30
32
|
"pre_processing",
|
|
31
33
|
"human_analysis",
|
|
32
|
-
"object_analysis"
|
|
34
|
+
"object_analysis",
|
|
35
|
+
"pipeline"
|
|
33
36
|
]
|
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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 = "
|
|
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
|
|
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
|
|
86
|
-
"""
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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=
|
|
116
|
+
data=None,
|
|
137
117
|
meta={
|
|
138
118
|
"source": (image_1, image_2),
|
|
139
119
|
"operation": "compare_faces",
|
|
140
|
-
"
|
|
141
|
-
"threshold": DEFAULT_SIMILARITY_THRESHOLD
|
|
120
|
+
"error": "No face detected in one or both images"
|
|
142
121
|
}
|
|
143
122
|
)
|
|
144
123
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Pipeline Module
|
|
2
|
+
# Provides a reusable, ordered chain of image processing operations
|
|
3
|
+
#
|
|
4
|
+
# Uses only base dependencies (numpy); all operations handed to a Pipeline
|
|
5
|
+
# may come from any module, including those with optional backends.
|
|
6
|
+
|
|
7
|
+
from .pipeline import Pipeline, Step
|
|
8
|
+
|
|
9
|
+
__all__ = ["Pipeline", "Step"]
|