ImagePRO-Python 1.4.0__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 +1 -1
- 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/pipeline.py +3 -1
- 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.4.0.dist-info → imagepro_python-1.5.0.dist-info}/METADATA +2 -2
- imagepro_python-1.5.0.dist-info/RECORD +33 -0
- imagepro_python-1.4.0.dist-info/RECORD +0 -33
- {imagepro_python-1.4.0.dist-info → imagepro_python-1.5.0.dist-info}/WHEEL +0 -0
- {imagepro_python-1.4.0.dist-info → imagepro_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {imagepro_python-1.4.0.dist-info → imagepro_python-1.5.0.dist-info}/top_level.txt +0 -0
ImagePRO/__init__.py
CHANGED
|
@@ -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
|
)
|
ImagePRO/pipeline/pipeline.py
CHANGED
|
@@ -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):
|
ImagePRO/pre_processing/blur.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
@@ -29,7 +29,8 @@ def apply_clahe_contrast(
|
|
|
29
29
|
"""Enhance image contrast using CLAHE (adaptive histogram equalization).
|
|
30
30
|
|
|
31
31
|
CLAHE applies histogram equalization on small regions for better local contrast.
|
|
32
|
-
Works well for images with varying lighting conditions.
|
|
32
|
+
Works well for images with varying lighting conditions. Color images are
|
|
33
|
+
converted to grayscale first; the output is always single-channel.
|
|
33
34
|
|
|
34
35
|
Args:
|
|
35
36
|
image: Input image to enhance.
|
|
@@ -52,18 +53,23 @@ def apply_clahe_contrast(
|
|
|
52
53
|
if not isinstance(image, Image):
|
|
53
54
|
raise TypeError("'image' must be an Image instance")
|
|
54
55
|
|
|
55
|
-
if not isinstance(clip_limit, (int, float))
|
|
56
|
+
if isinstance(clip_limit, bool) or not isinstance(clip_limit, (int, float)) \
|
|
57
|
+
or clip_limit <= 0:
|
|
56
58
|
raise ValueError("'clip_limit' must be a positive number")
|
|
57
59
|
|
|
58
60
|
if (
|
|
59
61
|
not isinstance(tile_grid_size, tuple)
|
|
60
62
|
or len(tile_grid_size) != 2
|
|
61
|
-
or not all(isinstance(i, int) and i > 0 for i in tile_grid_size)
|
|
63
|
+
or not all(isinstance(i, int) and not isinstance(i, bool) and i > 0 for i in tile_grid_size)
|
|
62
64
|
):
|
|
63
65
|
raise TypeError("'tile_grid_size' must be a tuple of two positive integers")
|
|
64
66
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
+
# CLAHE works on a single channel: grayscale BGR/RGB inputs, luma of
|
|
68
|
+
# color ones (channel weights are identical for BGR and RGB).
|
|
69
|
+
if image.colorspace == "GRAY":
|
|
70
|
+
grayscale = image._data
|
|
71
|
+
else:
|
|
72
|
+
grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
|
|
67
73
|
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
|
|
68
74
|
enhanced = clahe.apply(grayscale)
|
|
69
75
|
|
|
@@ -84,7 +90,8 @@ def apply_histogram_equalization(
|
|
|
84
90
|
"""Global histogram equalization for contrast enhancement.
|
|
85
91
|
|
|
86
92
|
Normalizes image intensity for better overall contrast.
|
|
87
|
-
Simple but may overamplify noise in some cases.
|
|
93
|
+
Simple but may overamplify noise in some cases. Color images are
|
|
94
|
+
converted to grayscale first; the output is always single-channel.
|
|
88
95
|
|
|
89
96
|
Args:
|
|
90
97
|
image: Input image to enhance.
|
|
@@ -101,8 +108,13 @@ def apply_histogram_equalization(
|
|
|
101
108
|
if not isinstance(image, Image):
|
|
102
109
|
raise TypeError("'image' must be an Image instance")
|
|
103
110
|
|
|
104
|
-
#
|
|
105
|
-
|
|
111
|
+
# Global equalization works on a single channel: grayscale inputs pass
|
|
112
|
+
# through, color ones are converted first (luma weights are identical
|
|
113
|
+
# for BGR and RGB).
|
|
114
|
+
if image.colorspace == "GRAY":
|
|
115
|
+
grayscale = image._data
|
|
116
|
+
else:
|
|
117
|
+
grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
|
|
106
118
|
enhanced = cv2.equalizeHist(grayscale)
|
|
107
119
|
|
|
108
120
|
return Result(
|
|
@@ -123,7 +135,8 @@ def apply_contrast_stretching(
|
|
|
123
135
|
"""Linear contrast stretching using alpha and beta.
|
|
124
136
|
|
|
125
137
|
Applies the formula: new_pixel = alpha × pixel + beta
|
|
126
|
-
Simple but effective for basic contrast adjustment.
|
|
138
|
+
Simple but effective for basic contrast adjustment. Color images are
|
|
139
|
+
converted to grayscale first; the output is always single-channel.
|
|
127
140
|
|
|
128
141
|
Args:
|
|
129
142
|
image: Input image to enhance.
|
|
@@ -146,14 +159,19 @@ def apply_contrast_stretching(
|
|
|
146
159
|
if not isinstance(image, Image):
|
|
147
160
|
raise TypeError("'image' must be an Image instance")
|
|
148
161
|
|
|
149
|
-
if not isinstance(alpha, (int, float)) or alpha < 0:
|
|
162
|
+
if isinstance(alpha, bool) or not isinstance(alpha, (int, float)) or alpha < 0:
|
|
150
163
|
raise ValueError("'alpha' must be a non-negative number")
|
|
151
164
|
|
|
152
|
-
if not isinstance(beta, int) or not (0 <= beta <= 255):
|
|
165
|
+
if not isinstance(beta, int) or isinstance(beta, bool) or not (0 <= beta <= 255):
|
|
153
166
|
raise ValueError("'beta' must be an integer between 0 and 255")
|
|
154
167
|
|
|
155
|
-
#
|
|
156
|
-
|
|
168
|
+
# Linear stretching on a single channel: grayscale inputs pass through,
|
|
169
|
+
# color ones are converted first (luma weights are identical for BGR
|
|
170
|
+
# and RGB).
|
|
171
|
+
if image.colorspace == "GRAY":
|
|
172
|
+
grayscale = image._data
|
|
173
|
+
else:
|
|
174
|
+
grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
|
|
157
175
|
enhanced = cv2.convertScaleAbs(grayscale, alpha=alpha, beta=beta)
|
|
158
176
|
|
|
159
177
|
return Result(
|
ImagePRO/pre_processing/crop.py
CHANGED
|
@@ -23,6 +23,7 @@ def crop_image(
|
|
|
23
23
|
|
|
24
24
|
Extracts a rectangular region from the image using the specified coordinates.
|
|
25
25
|
The cropped area must be within image bounds and have valid dimensions.
|
|
26
|
+
The crop is an independent copy; editing it never affects the source image.
|
|
26
27
|
|
|
27
28
|
Args:
|
|
28
29
|
image: Input image to crop.
|
|
@@ -47,7 +48,8 @@ def crop_image(
|
|
|
47
48
|
not isinstance(start_point, tuple) or
|
|
48
49
|
not isinstance(end_point, tuple) or
|
|
49
50
|
len(start_point) != 2 or len(end_point) != 2 or
|
|
50
|
-
not all(isinstance(c, int)
|
|
51
|
+
not all(isinstance(c, int) and not isinstance(c, bool)
|
|
52
|
+
for c in start_point + end_point)
|
|
51
53
|
):
|
|
52
54
|
raise TypeError("'start_point' and 'end_point' must be (x, y) tuples of integers")
|
|
53
55
|
|
|
@@ -65,8 +67,9 @@ def crop_image(
|
|
|
65
67
|
if x2 > width or y2 > height:
|
|
66
68
|
raise ValueError(f"Crop area exceeds image bounds ({width}x{height})")
|
|
67
69
|
|
|
68
|
-
# Extract the region
|
|
69
|
-
|
|
70
|
+
# Extract the region as a copy so later in-place edits of the crop can
|
|
71
|
+
# never touch the source image (non-destructive contract)
|
|
72
|
+
cropped = image._data[y1:y2, x1:x2].copy()
|
|
70
73
|
|
|
71
74
|
return Result(
|
|
72
75
|
image=cropped,
|
|
@@ -24,6 +24,45 @@ DEFAULT_MIN_CONFIDENCE = 0.7
|
|
|
24
24
|
DEFAULT_CAMERA_INDEX = 0
|
|
25
25
|
DEFAULT_DELAY = 0.1
|
|
26
26
|
DEFAULT_FACE_ID = "unknown"
|
|
27
|
+
MAX_EMPTY_FRAMES = 250 # Abandon capture after this many frames without a face
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _validate_parameters(
|
|
31
|
+
*,
|
|
32
|
+
num_images: int,
|
|
33
|
+
start_index: int,
|
|
34
|
+
min_confidence: float,
|
|
35
|
+
camera_index: int,
|
|
36
|
+
apply_resize: tuple[int, int] | bool,
|
|
37
|
+
delay: float
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Validate all user-supplied parameters up front, before touching the camera."""
|
|
40
|
+
if not isinstance(num_images, int) or isinstance(num_images, bool) or num_images <= 0:
|
|
41
|
+
raise ValueError("'num_images' must be a positive integer")
|
|
42
|
+
|
|
43
|
+
if not isinstance(start_index, int) or isinstance(start_index, bool) or start_index < 0:
|
|
44
|
+
raise ValueError("'start_index' must be a non-negative integer")
|
|
45
|
+
|
|
46
|
+
if isinstance(min_confidence, bool) or not isinstance(min_confidence, (int, float)) \
|
|
47
|
+
or not (0 <= min_confidence <= 1):
|
|
48
|
+
raise ValueError("'min_confidence' must be between 0 and 1")
|
|
49
|
+
|
|
50
|
+
if not isinstance(camera_index, int) or isinstance(camera_index, bool) or camera_index < 0:
|
|
51
|
+
raise ValueError("'camera_index' must be a non-negative integer")
|
|
52
|
+
|
|
53
|
+
if apply_resize is not False and (
|
|
54
|
+
not isinstance(apply_resize, tuple)
|
|
55
|
+
or len(apply_resize) != 2
|
|
56
|
+
or not all(
|
|
57
|
+
isinstance(dim, int) and not isinstance(dim, bool) and dim > 0
|
|
58
|
+
for dim in apply_resize
|
|
59
|
+
)
|
|
60
|
+
):
|
|
61
|
+
raise ValueError("'apply_resize' must be False or a (width, height) tuple "
|
|
62
|
+
"of two positive integers")
|
|
63
|
+
|
|
64
|
+
if isinstance(delay, bool) or not isinstance(delay, (int, float)) or delay < 0:
|
|
65
|
+
raise ValueError("'delay' must be a non-negative number")
|
|
27
66
|
|
|
28
67
|
|
|
29
68
|
def capture_bulk_pictures(
|
|
@@ -43,12 +82,17 @@ def capture_bulk_pictures(
|
|
|
43
82
|
) -> None:
|
|
44
83
|
"""Generate a dataset by capturing faces from webcam.
|
|
45
84
|
|
|
46
|
-
Captures frames from the webcam, detects faces, applies optional
|
|
47
|
-
and saves cropped face images to disk. This function is
|
|
48
|
-
face recognition datasets with data augmentation.
|
|
85
|
+
Captures frames from the webcam, detects faces, applies optional
|
|
86
|
+
preprocessing, and saves cropped face images to disk. This function is
|
|
87
|
+
useful for creating face recognition datasets with data augmentation.
|
|
88
|
+
|
|
89
|
+
Saved crops go through this pipeline (each step optional):
|
|
90
|
+
median blur → laplacian sharpen → resize → grayscale → random rotate
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
Face detection always runs on the raw camera frame, so the capture loop
|
|
93
|
+
keeps working no matter which preprocessing flags are enabled. The loop
|
|
94
|
+
gives up after MAX_EMPTY_FRAMES consecutive frames without a usable
|
|
95
|
+
capture, instead of blocking forever when no face is in view.
|
|
52
96
|
|
|
53
97
|
Args:
|
|
54
98
|
folder_path: Base directory for dataset.
|
|
@@ -64,7 +108,7 @@ def capture_bulk_pictures(
|
|
|
64
108
|
Default: 0
|
|
65
109
|
apply_blur: Apply median blur (size=3).
|
|
66
110
|
Default: False
|
|
67
|
-
apply_grayscale: Convert to single-channel.
|
|
111
|
+
apply_grayscale: Convert crops to single-channel.
|
|
68
112
|
Default: False
|
|
69
113
|
apply_sharpen: Apply Laplacian (coef=1.0).
|
|
70
114
|
Default: False
|
|
@@ -81,18 +125,19 @@ def capture_bulk_pictures(
|
|
|
81
125
|
FileExistsError: If output folder exists
|
|
82
126
|
RuntimeError: If camera access fails
|
|
83
127
|
"""
|
|
84
|
-
# Validate
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
# Validate everything up front, before creating folders or opening the
|
|
129
|
+
# camera, so an invalid argument never leaves partial state behind.
|
|
130
|
+
_validate_parameters(
|
|
131
|
+
num_images=num_images,
|
|
132
|
+
start_index=start_index,
|
|
133
|
+
min_confidence=min_confidence,
|
|
134
|
+
camera_index=camera_index,
|
|
135
|
+
apply_resize=apply_resize,
|
|
136
|
+
delay=delay
|
|
137
|
+
)
|
|
93
138
|
|
|
94
|
-
if not isinstance(
|
|
95
|
-
raise
|
|
139
|
+
if apply_rotate is not None and not isinstance(apply_rotate, bool):
|
|
140
|
+
raise TypeError("'apply_rotate' must be a boolean")
|
|
96
141
|
|
|
97
142
|
try:
|
|
98
143
|
import mediapipe as mp
|
|
@@ -127,12 +172,20 @@ def capture_bulk_pictures(
|
|
|
127
172
|
)
|
|
128
173
|
|
|
129
174
|
saved_count = 0
|
|
175
|
+
frames_without_face = 0
|
|
130
176
|
try:
|
|
131
177
|
while saved_count < num_images:
|
|
132
178
|
# Capture frame
|
|
133
179
|
success, frame = cap.read()
|
|
134
180
|
if not success:
|
|
135
181
|
print("Failed to capture frame, skipping...")
|
|
182
|
+
frames_without_face += 1
|
|
183
|
+
if frames_without_face >= MAX_EMPTY_FRAMES:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
f"Camera stopped delivering frames after "
|
|
186
|
+
f"{frames_without_face} consecutive failures; "
|
|
187
|
+
f"saved {saved_count}/{num_images} images so far."
|
|
188
|
+
)
|
|
136
189
|
continue
|
|
137
190
|
|
|
138
191
|
# Apply preprocessing pipeline in sequence
|
|
@@ -153,12 +206,6 @@ def capture_bulk_pictures(
|
|
|
153
206
|
coefficient=1.0
|
|
154
207
|
).image
|
|
155
208
|
|
|
156
|
-
if apply_grayscale:
|
|
157
|
-
# Convert to single-channel for reduced storage and faster processing
|
|
158
|
-
processed = grayscale.convert_to_grayscale(
|
|
159
|
-
image=Image.from_array(processed)
|
|
160
|
-
).image
|
|
161
|
-
|
|
162
209
|
if apply_resize is not False:
|
|
163
210
|
# Resize to consistent dimensions (e.g., 224x224 for ML models)
|
|
164
211
|
processed = resize.resize_image(
|
|
@@ -166,6 +213,14 @@ def capture_bulk_pictures(
|
|
|
166
213
|
new_size=apply_resize
|
|
167
214
|
).image
|
|
168
215
|
|
|
216
|
+
if apply_grayscale:
|
|
217
|
+
# Convert to single-channel for reduced storage and faster
|
|
218
|
+
# processing. Rotation then tags the intermediate as GRAY so
|
|
219
|
+
# the face detector can convert back to RGB correctly.
|
|
220
|
+
processed = grayscale.convert_to_grayscale(
|
|
221
|
+
image=Image.from_array(processed)
|
|
222
|
+
).image
|
|
223
|
+
|
|
169
224
|
if apply_rotate:
|
|
170
225
|
# Apply random rotation with scaling for data augmentation
|
|
171
226
|
# Rotation range: -45° to +45° with random scale factors
|
|
@@ -182,9 +237,12 @@ def capture_bulk_pictures(
|
|
|
182
237
|
output_path = face_folder / filename
|
|
183
238
|
|
|
184
239
|
try:
|
|
185
|
-
# Detect face and crop to face region only
|
|
240
|
+
# Detect face and crop to face region only. The detector
|
|
241
|
+
# needs a color image, so pass the untouched camera frame
|
|
242
|
+
# (preprocessing here is augmentation for the saved crop,
|
|
243
|
+
# not a requirement of the detector).
|
|
186
244
|
result = detect_faces(
|
|
187
|
-
image=Image.from_array(
|
|
245
|
+
image=Image.from_array(frame),
|
|
188
246
|
max_faces=1,
|
|
189
247
|
min_confidence=min_confidence,
|
|
190
248
|
face_mesh_obj=face_mesh
|
|
@@ -193,13 +251,22 @@ def capture_bulk_pictures(
|
|
|
193
251
|
# Save cropped face image
|
|
194
252
|
result.save_as_img(str(output_path))
|
|
195
253
|
saved_count += 1
|
|
254
|
+
frames_without_face = 0
|
|
196
255
|
|
|
197
256
|
# Add delay between captures to allow subject movement
|
|
198
257
|
if delay > 0:
|
|
199
258
|
time.sleep(delay)
|
|
200
259
|
|
|
201
260
|
except ValueError:
|
|
202
|
-
# Skip frames with no detected faces
|
|
261
|
+
# Skip frames with no detected faces
|
|
262
|
+
frames_without_face += 1
|
|
263
|
+
if frames_without_face >= MAX_EMPTY_FRAMES:
|
|
264
|
+
print(
|
|
265
|
+
f"No face detected in {frames_without_face} consecutive "
|
|
266
|
+
f"frames; giving up with {saved_count}/{num_images} "
|
|
267
|
+
"images saved."
|
|
268
|
+
)
|
|
269
|
+
break
|
|
203
270
|
continue
|
|
204
271
|
|
|
205
272
|
finally:
|
|
@@ -23,9 +23,13 @@ def show_histogram(image: Image) -> Result:
|
|
|
23
23
|
This function creates the histogram plot of the intensity distribution of
|
|
24
24
|
an image. It handles both grayscale and color images, with automatic
|
|
25
25
|
detection of image type. For color images, it plots a histogram for each
|
|
26
|
-
color channel (BGR or RGB) with appropriate colors.
|
|
27
|
-
|
|
28
|
-
figure
|
|
26
|
+
color channel (BGR or RGB) with appropriate colors.
|
|
27
|
+
|
|
28
|
+
The figure is closed before returning, so batch calls do not accumulate
|
|
29
|
+
figures in memory. To display the plot interactively, call this function
|
|
30
|
+
with ``matplotlib.interactive(True)`` or re-create the figure with
|
|
31
|
+
``plt.figure(1)`` right before ``plt.show()``: pyplot re-opens the most
|
|
32
|
+
recently closed figure number with its contents intact.
|
|
29
33
|
|
|
30
34
|
Args:
|
|
31
35
|
image (Image):
|
|
@@ -54,7 +58,7 @@ def show_histogram(image: Image) -> Result:
|
|
|
54
58
|
channels = list(enumerate(colors))
|
|
55
59
|
elif image.colorspace == "GRAY":
|
|
56
60
|
channels = [(0, "black")]
|
|
57
|
-
labels =
|
|
61
|
+
labels = ("Intensity",)
|
|
58
62
|
else:
|
|
59
63
|
raise ValueError(f"Unknown colorspace: {image.colorspace}")
|
|
60
64
|
|
|
@@ -63,15 +67,13 @@ def show_histogram(image: Image) -> Result:
|
|
|
63
67
|
hist = cv2.calcHist([image._data], [channel], None, [256], [0, 256])
|
|
64
68
|
plt.plot(hist, color=color)
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
plt.ylabel("Frequency")
|
|
71
|
-
|
|
70
|
+
plt.title(f"Histogram of {image.colorspace} Channels")
|
|
71
|
+
plt.legend(labels)
|
|
72
|
+
plt.xlabel("Pixel Intensity")
|
|
73
|
+
plt.ylabel("Frequency")
|
|
72
74
|
plt.xlim([0, 256])
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
result = Result(
|
|
75
77
|
image=None,
|
|
76
78
|
data=plt,
|
|
77
79
|
meta={
|
|
@@ -79,3 +81,6 @@ def show_histogram(image: Image) -> Result:
|
|
|
79
81
|
"operation": "show_histogram"
|
|
80
82
|
}
|
|
81
83
|
)
|
|
84
|
+
# Close the figure so repeated calls in batch jobs do not leak figures.
|
|
85
|
+
plt.close()
|
|
86
|
+
return result
|
|
@@ -46,7 +46,10 @@ def resize_image(
|
|
|
46
46
|
if (
|
|
47
47
|
not isinstance(new_size, tuple)
|
|
48
48
|
or len(new_size) != 2
|
|
49
|
-
or not all(
|
|
49
|
+
or not all(
|
|
50
|
+
isinstance(dim, int) and not isinstance(dim, bool) and dim > 0
|
|
51
|
+
for dim in new_size
|
|
52
|
+
)
|
|
50
53
|
):
|
|
51
54
|
raise ValueError("'new_size' must be a tuple of two positive integers")
|
|
52
55
|
|
|
@@ -136,9 +136,9 @@ def rotate_image_custom(
|
|
|
136
136
|
"""
|
|
137
137
|
if not isinstance(image, Image):
|
|
138
138
|
raise TypeError("'image' must be an Image instance")
|
|
139
|
-
if not isinstance(angle, (int, float)):
|
|
139
|
+
if isinstance(angle, bool) or not isinstance(angle, (int, float)):
|
|
140
140
|
raise TypeError("'angle' must be a number")
|
|
141
|
-
if not isinstance(scale, (int, float)) or scale <= 0:
|
|
141
|
+
if isinstance(scale, bool) or not isinstance(scale, (int, float)) or scale <= 0:
|
|
142
142
|
raise ValueError("'scale' must be a positive number")
|
|
143
143
|
|
|
144
144
|
h, w = image.shape[:2]
|
|
@@ -38,7 +38,7 @@ def apply_laplacian_sharpening(
|
|
|
38
38
|
|
|
39
39
|
Returns:
|
|
40
40
|
Result object with sharpened image and metadata:
|
|
41
|
-
- image: Sharpened image array
|
|
41
|
+
- image: Sharpened image array (same dtype as the input)
|
|
42
42
|
- data: None
|
|
43
43
|
- meta: Operation info and coefficient used
|
|
44
44
|
|
|
@@ -49,19 +49,19 @@ def apply_laplacian_sharpening(
|
|
|
49
49
|
if not isinstance(image, Image):
|
|
50
50
|
raise TypeError("'image' must be an Image instance")
|
|
51
51
|
|
|
52
|
-
if not isinstance(coefficient, (int, float))
|
|
52
|
+
if isinstance(coefficient, bool) or not isinstance(coefficient, (int, float)) \
|
|
53
|
+
or coefficient < 0:
|
|
53
54
|
raise ValueError("'coefficient' must be a non-negative number")
|
|
54
55
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
56
|
+
# Sharpen in float space: image + coefficient * |laplacian|, clipped
|
|
57
|
+
# once at the end. Casting the Laplacian to uint8 before combining
|
|
58
|
+
# would truncate edge responses above 255 and corrupt the output.
|
|
57
59
|
laplacian = cv2.Laplacian(image._data, cv2.CV_64F)
|
|
58
60
|
np.absolute(laplacian, out=laplacian)
|
|
59
|
-
laplacian = laplacian.astype(np.uint8)
|
|
60
61
|
|
|
61
|
-
sharpened =
|
|
62
|
-
sharpened += image._data
|
|
62
|
+
sharpened = image._data.astype(np.float64) + coefficient * laplacian
|
|
63
63
|
np.clip(sharpened, 0, 255, out=sharpened)
|
|
64
|
-
sharpened = sharpened.astype(
|
|
64
|
+
sharpened = sharpened.astype(image._data.dtype)
|
|
65
65
|
|
|
66
66
|
return Result(
|
|
67
67
|
image=sharpened,
|
|
@@ -90,7 +90,7 @@ def apply_unsharp_masking(
|
|
|
90
90
|
|
|
91
91
|
Returns:
|
|
92
92
|
Result object with sharpened image and metadata:
|
|
93
|
-
- image: Sharpened image array
|
|
93
|
+
- image: Sharpened image array (same dtype as the input)
|
|
94
94
|
- data: None
|
|
95
95
|
- meta: Operation info and coefficient used
|
|
96
96
|
|
|
@@ -101,21 +101,20 @@ def apply_unsharp_masking(
|
|
|
101
101
|
if not isinstance(image, Image):
|
|
102
102
|
raise TypeError("'image' must be an Image instance")
|
|
103
103
|
|
|
104
|
-
if not isinstance(coefficient, (int, float))
|
|
104
|
+
if isinstance(coefficient, bool) or not isinstance(coefficient, (int, float)) \
|
|
105
|
+
or coefficient < 0:
|
|
105
106
|
raise ValueError("'coefficient' must be a non-negative number")
|
|
106
107
|
|
|
107
|
-
#
|
|
108
|
+
# Unsharp masking: original + coefficient * (original - blurred),
|
|
109
|
+
# computed in float space and clipped once at the end. cv2.subtract
|
|
110
|
+
# saturates negative mask values and addWeighted on the saturated mask
|
|
111
|
+
# brightens flat regions instead of sharpening, so both are avoided.
|
|
108
112
|
blurred = cv2.blur(image._data, DEFAULT_KERNEL_SIZE)
|
|
109
|
-
mask =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
sharpened =
|
|
113
|
-
|
|
114
|
-
1 + coefficient,
|
|
115
|
-
mask,
|
|
116
|
-
-coefficient,
|
|
117
|
-
0
|
|
118
|
-
)
|
|
113
|
+
mask = image._data.astype(np.float64) - blurred.astype(np.float64)
|
|
114
|
+
|
|
115
|
+
sharpened = image._data.astype(np.float64) + coefficient * mask
|
|
116
|
+
np.clip(sharpened, 0, 255, out=sharpened)
|
|
117
|
+
sharpened = sharpened.astype(image._data.dtype)
|
|
119
118
|
|
|
120
119
|
return Result(
|
|
121
120
|
image=sharpened,
|
ImagePRO/utils/image.py
CHANGED
|
@@ -13,7 +13,7 @@ Colorspace = Literal["BGR", "RGB", "GRAY"]
|
|
|
13
13
|
SourceType = Literal["path", "array"]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@dataclass
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
17
|
class Image:
|
|
18
18
|
"""
|
|
19
19
|
Lightweight, immutable image wrapper for ImagePRO.
|
|
@@ -72,11 +72,18 @@ class Image:
|
|
|
72
72
|
"""
|
|
73
73
|
if not isinstance(path, (str, Path)):
|
|
74
74
|
raise TypeError("'path' must be a string or pathlib.Path.")
|
|
75
|
-
np_image = cv2.imread(str(path))
|
|
76
|
-
if np_image is None:
|
|
77
|
-
raise ValueError(f"Failed to load image from {path}")
|
|
78
75
|
if colorspace not in ("BGR", "RGB", "GRAY"):
|
|
79
76
|
raise ValueError("'colorspace' must be one of 'BGR', 'RGB', 'GRAY'.")
|
|
77
|
+
|
|
78
|
+
# cv2.imread always returns BGR; grayscale output requires the
|
|
79
|
+
# IMREAD_GRAYSCALE flag up front (post-hoc conversion of the BGR
|
|
80
|
+
# result would work numerically but this loads 1 channel directly).
|
|
81
|
+
imread_flag = cv2.IMREAD_GRAYSCALE if colorspace == "GRAY" else cv2.IMREAD_COLOR
|
|
82
|
+
np_image = cv2.imread(str(path), imread_flag)
|
|
83
|
+
if np_image is None:
|
|
84
|
+
raise ValueError(f"Failed to load image from {path}")
|
|
85
|
+
if colorspace == "RGB":
|
|
86
|
+
np_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
|
|
80
87
|
return cls(_data=np_image, path=Path(path), colorspace=colorspace, source_type="path")
|
|
81
88
|
|
|
82
89
|
@classmethod
|
ImagePRO/utils/result.py
CHANGED
|
@@ -77,26 +77,28 @@ class Result:
|
|
|
77
77
|
|
|
78
78
|
# Save single image
|
|
79
79
|
if isinstance(self.image, np.ndarray):
|
|
80
|
+
if not out_path.suffix:
|
|
81
|
+
# OpenCV picks its writer from the file extension, so an
|
|
82
|
+
# extensionless path would fail in imwrite; default to .png.
|
|
83
|
+
out_path = out_path.with_name(f"{out_path.name}.png")
|
|
80
84
|
ok = cv2.imwrite(str(out_path), self.image)
|
|
81
85
|
if not ok:
|
|
82
86
|
raise IOError(f"Failed to save image: {out_path}")
|
|
83
87
|
return self
|
|
84
88
|
|
|
85
89
|
# Save list of images with auto-suffixes
|
|
86
|
-
base =
|
|
90
|
+
base = Path(out_path)
|
|
91
|
+
stem, suffix = base.stem, base.suffix
|
|
92
|
+
if not suffix:
|
|
93
|
+
# cv2.imwrite needs an extension to choose a writer.
|
|
94
|
+
suffix = ".png"
|
|
95
|
+
base = base.with_name(f"{stem}.png")
|
|
87
96
|
for idx, img in enumerate(self.image):
|
|
88
97
|
if idx == 0:
|
|
89
|
-
path_i = base
|
|
98
|
+
path_i = str(base)
|
|
90
99
|
else:
|
|
91
|
-
# Suffix logic: .jpg
|
|
92
|
-
|
|
93
|
-
path_i = base.replace(".jpg", f"_{idx}.jpg")
|
|
94
|
-
else:
|
|
95
|
-
p = Path(base)
|
|
96
|
-
if p.suffix:
|
|
97
|
-
path_i = str(p.with_name(f"{p.stem}_{idx}{p.suffix}"))
|
|
98
|
-
else:
|
|
99
|
-
path_i = f"{base}_{idx}"
|
|
100
|
+
# Suffix logic: out.jpg -> out_1.jpg, out_2.jpg, ...
|
|
101
|
+
path_i = str(base.with_name(f"{stem}_{idx}{suffix}"))
|
|
100
102
|
ok = cv2.imwrite(path_i, img)
|
|
101
103
|
if not ok:
|
|
102
104
|
raise IOError(f"Failed to save image: {path_i}")
|
|
@@ -115,7 +117,9 @@ class Result:
|
|
|
115
117
|
path (str | Path):
|
|
116
118
|
Output CSV file path.
|
|
117
119
|
rows (Optional[list[list[Any]]], optional):
|
|
118
|
-
Data to write as rows. If None, uses self.data.
|
|
120
|
+
Data to write as rows. If None, uses self.data.
|
|
121
|
+
A 2D numpy.ndarray is also accepted (one CSV row per array
|
|
122
|
+
row). Defaults to None.
|
|
119
123
|
|
|
120
124
|
Returns:
|
|
121
125
|
Result: Self, for method chaining.
|
|
@@ -135,7 +139,10 @@ class Result:
|
|
|
135
139
|
try:
|
|
136
140
|
with out_path.open("w", newline="", encoding="utf-8") as f:
|
|
137
141
|
writer = csv.writer(f)
|
|
138
|
-
if isinstance(payload,
|
|
142
|
+
if isinstance(payload, np.ndarray):
|
|
143
|
+
# Write one CSV row per array row
|
|
144
|
+
writer.writerows(payload.tolist())
|
|
145
|
+
elif isinstance(payload, (list, tuple)):
|
|
139
146
|
writer.writerows(flatten_rows(payload))
|
|
140
147
|
else:
|
|
141
148
|
writer.writerow([payload])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ImagePRO-Python
|
|
3
|
-
Version: 1.
|
|
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
|
|
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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ImagePRO/__init__.py,sha256=Rv5QiWX7e-OlR3DoLBaV5394xw6jwmarJrr2d0_RN50,1081
|
|
2
|
+
ImagePRO/human_analysis/__init__.py,sha256=uSU0lM1p1HM6TNGv6u0nPHWy9QY505v43lqTkhClddM,190
|
|
3
|
+
ImagePRO/human_analysis/body_analysis/__init__.py,sha256=uUWG5eOxLQjZoYddmNkRlF66l6vSVyJpUFz1wDddXkk,374
|
|
4
|
+
ImagePRO/human_analysis/body_analysis/body_pose_estimation.py,sha256=gLWM5N2cMUNxyRX-7_z_52MnE8J9JM_UgAPoUWPCFpY,6567
|
|
5
|
+
ImagePRO/human_analysis/body_analysis/hand_tracking.py,sha256=g9rRf2dXA8eaepa0DB7qY01rA2POlpMjtyl3iFXhfck,8011
|
|
6
|
+
ImagePRO/human_analysis/face_analysis/__init__.py,sha256=297IQuoO2Wb_PyigS_2NOuVnM7FpNPceu5jhJp6YdG0,604
|
|
7
|
+
ImagePRO/human_analysis/face_analysis/eye_status_analysis.py,sha256=0bfzRuD6qpdANS33s-2XY5udDXVvoV2mGCwQbT0x0Zc,8150
|
|
8
|
+
ImagePRO/human_analysis/face_analysis/face_comparison.py,sha256=MbQ91LNlA7ySepR2IGUGrkNjTp-L-lj2NZw3HZ4WO84,4671
|
|
9
|
+
ImagePRO/human_analysis/face_analysis/face_detection.py,sha256=kBZfHD79ls6tMZr6avVIXv8NpPyd1Dyq4iXpEeNwFM8,4090
|
|
10
|
+
ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py,sha256=09tkJlzA2aSPDKEgf68O5OLEStP55W9JRb0M8z_P4mA,9369
|
|
11
|
+
ImagePRO/human_analysis/face_analysis/head_pose_estimation.py,sha256=wJryUR4B-uf6aMIdbZxe5894HcPmHmG-THyH376jG-Q,8542
|
|
12
|
+
ImagePRO/object_analysis/__init__.py,sha256=eVQpYlHO89RYKT0CHjPfJjjif5X_Jx3AtAUpR5m0Yms,292
|
|
13
|
+
ImagePRO/object_analysis/object_detection.py,sha256=X7LCJdKMBpBqA4gXDq3W_5AQnf_CA83jTww0Vdqtypo,3903
|
|
14
|
+
ImagePRO/pipeline/__init__.py,sha256=-32hbR492qwxutXau7EfnysXbhW8fKKMywkV0ejIMFE,301
|
|
15
|
+
ImagePRO/pipeline/pipeline.py,sha256=n-Y-FtQbIiXiYsP-RekmCZg4cKlylU_quWyrEGsnSQw,13953
|
|
16
|
+
ImagePRO/pre_processing/__init__.py,sha256=pw6-J5sdbkdBG4RlTxOzQKaspGnlPIGo3m_NXH2bVCE,559
|
|
17
|
+
ImagePRO/pre_processing/blur.py,sha256=RO4na6x3cm80nduFR91WEOasIN5RtiPCfqwwuorLivo,8220
|
|
18
|
+
ImagePRO/pre_processing/contrast.py,sha256=B0BweCI0NnDwd2JcVL6GPUVjF7NX_MKWhiovYsC47h4,6001
|
|
19
|
+
ImagePRO/pre_processing/crop.py,sha256=ByIe2whvqoaEZVRzCMW8HBP7fsoaQDPLQIHPT4NB3Lo,2649
|
|
20
|
+
ImagePRO/pre_processing/dataset_generator.py,sha256=w1CImp1dP0RWwblUP3pcD6v0MG0nr4-a3MWZu_7oPOE,10922
|
|
21
|
+
ImagePRO/pre_processing/grayscale.py,sha256=MRCsFRIaL45sYVHzhuNyZBXsP7PF-yorn0Wt7xONvpY,1781
|
|
22
|
+
ImagePRO/pre_processing/histogram.py,sha256=1Xr7pEL-3UvpiPEpaenPCO4ZSK6gOxmOUFuDcxWmtx4,2927
|
|
23
|
+
ImagePRO/pre_processing/resize.py,sha256=lHLA69cBUN25UNblmSJTk3iG6nKNF_51vQguCbPwxt4,1824
|
|
24
|
+
ImagePRO/pre_processing/rotate.py,sha256=HtbMCh7p_BY5n3tl974Iboyv1XRMhfORPlEBGjjGpCs,4228
|
|
25
|
+
ImagePRO/pre_processing/sharpen.py,sha256=y2dzaesjsKyonvobXCPOiFtD1H5y3_Hdi7GpX1v6jO4,4134
|
|
26
|
+
ImagePRO/utils/__init__.py,sha256=McVd-o9KtKOpAVYbLi19KxsatPPFFF-Qbt0gdgY3Ln8,170
|
|
27
|
+
ImagePRO/utils/image.py,sha256=_q34vEcP2-iJdJkiYFbc2obxyK5EPue-pJlrfV50Arc,4458
|
|
28
|
+
ImagePRO/utils/result.py,sha256=UnOLI8MSJIMCNrvMmhvJgxDXbGAMHmu9hgJ2gw64buQ,6577
|
|
29
|
+
imagepro_python-1.5.0.dist-info/licenses/LICENSE,sha256=sehkHN7dbe5umrINZJyJjIf9VTemZSgTfJlq6iIsbJQ,1069
|
|
30
|
+
imagepro_python-1.5.0.dist-info/METADATA,sha256=WUGiP3SF3DjQ0-Jl8unZDHx1yBonAdujVl3y_UTT5TE,11063
|
|
31
|
+
imagepro_python-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
32
|
+
imagepro_python-1.5.0.dist-info/top_level.txt,sha256=UOuPzsoWrVKXgUupOSI96shJ6wE8Fhz-8bm8oMlLC3s,9
|
|
33
|
+
imagepro_python-1.5.0.dist-info/RECORD,,
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
ImagePRO/__init__.py,sha256=3B8cQH4zhV6EEAvVMKYMsJHok_hpGLTgu4QfP33DV08,1081
|
|
2
|
-
ImagePRO/human_analysis/__init__.py,sha256=uSU0lM1p1HM6TNGv6u0nPHWy9QY505v43lqTkhClddM,190
|
|
3
|
-
ImagePRO/human_analysis/body_analysis/__init__.py,sha256=uUWG5eOxLQjZoYddmNkRlF66l6vSVyJpUFz1wDddXkk,374
|
|
4
|
-
ImagePRO/human_analysis/body_analysis/body_pose_estimation.py,sha256=yqEV18ZEZwV__Oqt1Hi72EGmbTNes-mrY068SBT9RbU,6245
|
|
5
|
-
ImagePRO/human_analysis/body_analysis/hand_tracking.py,sha256=SpdIZNgvaHzINpWu7rgNawSHvuuawzFtQ7JiHHQ0fqA,7578
|
|
6
|
-
ImagePRO/human_analysis/face_analysis/__init__.py,sha256=297IQuoO2Wb_PyigS_2NOuVnM7FpNPceu5jhJp6YdG0,604
|
|
7
|
-
ImagePRO/human_analysis/face_analysis/eye_status_analysis.py,sha256=Q_EUNPpXoF7Vi-Kd0gURNxs9X3U5J4Yi3coL_6lkeb8,7773
|
|
8
|
-
ImagePRO/human_analysis/face_analysis/face_comparison.py,sha256=lIr490PAf8R6C_Zhv-zBN1Un1EPiwxi4W7ahgkqXAVc,4724
|
|
9
|
-
ImagePRO/human_analysis/face_analysis/face_detection.py,sha256=F3Bq_0rAcQklmNAu-JvxGBIxUnWmyM-SHuZawHXJeac,4009
|
|
10
|
-
ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py,sha256=rv08Mx_IBB-hjTh09bFaaPjaOm0tFbBs0brc4mE66mg,8723
|
|
11
|
-
ImagePRO/human_analysis/face_analysis/head_pose_estimation.py,sha256=2_ZVpwavyIzCIH4x3d2bvCPvv-X5gpmSYxtk5jlgCYw,7688
|
|
12
|
-
ImagePRO/object_analysis/__init__.py,sha256=eVQpYlHO89RYKT0CHjPfJjjif5X_Jx3AtAUpR5m0Yms,292
|
|
13
|
-
ImagePRO/object_analysis/object_detection.py,sha256=1qPd3Xfo2Vi4JnBs5wD8-k3WR6ZkYNdT9tco2Ze1ICQ,3385
|
|
14
|
-
ImagePRO/pipeline/__init__.py,sha256=-32hbR492qwxutXau7EfnysXbhW8fKKMywkV0ejIMFE,301
|
|
15
|
-
ImagePRO/pipeline/pipeline.py,sha256=-CRWfWXB_lFdjrqg3gdyYFFvq0vZAednrJnod6b4WYA,13802
|
|
16
|
-
ImagePRO/pre_processing/__init__.py,sha256=pw6-J5sdbkdBG4RlTxOzQKaspGnlPIGo3m_NXH2bVCE,559
|
|
17
|
-
ImagePRO/pre_processing/blur.py,sha256=3zNTNpvLZPdrtRtBziB6X8ys8M7cIvyPDhojTqR63jo,7950
|
|
18
|
-
ImagePRO/pre_processing/contrast.py,sha256=4UT78ggW1rh_iaEI8FObDB4kpEmRRFWjdJES60SnHDM,5064
|
|
19
|
-
ImagePRO/pre_processing/crop.py,sha256=3Nwerzo57kx1sZPr9m355P-qf1NuZSZEA5iUn2Q1704,2406
|
|
20
|
-
ImagePRO/pre_processing/dataset_generator.py,sha256=i7UM4o8hznFUfUgbYI6H01EkQJOqt4IsI9IrjzzN8gQ,7921
|
|
21
|
-
ImagePRO/pre_processing/grayscale.py,sha256=MRCsFRIaL45sYVHzhuNyZBXsP7PF-yorn0Wt7xONvpY,1781
|
|
22
|
-
ImagePRO/pre_processing/histogram.py,sha256=sNTWWZXr5VLpOrF_PTTJ6TD8yv1zR1l9cMyXHe7JFIM,2596
|
|
23
|
-
ImagePRO/pre_processing/resize.py,sha256=Rnde8l_PXck5OWQBEMOHgLeW2YkTz9Xfr6fGmz0sdm8,1760
|
|
24
|
-
ImagePRO/pre_processing/rotate.py,sha256=wQj3nQpN-ilM0HHwovN--UdGEJkPCY0BJ0zAdyBxv-o,4174
|
|
25
|
-
ImagePRO/pre_processing/sharpen.py,sha256=hOkY7QhzgMp9LXQwt56AP4enn24ntXlmFLe57Bxssvw,3682
|
|
26
|
-
ImagePRO/utils/__init__.py,sha256=McVd-o9KtKOpAVYbLi19KxsatPPFFF-Qbt0gdgY3Ln8,170
|
|
27
|
-
ImagePRO/utils/image.py,sha256=Y64mD97rTiJb8_uNYlyG227VZodeb4U1aGezMkj7MnM,4025
|
|
28
|
-
ImagePRO/utils/result.py,sha256=ToQaOmUlTtobJkC65BG0YO_6BkBleMBn-0Qydko7cZc,6138
|
|
29
|
-
imagepro_python-1.4.0.dist-info/licenses/LICENSE,sha256=sehkHN7dbe5umrINZJyJjIf9VTemZSgTfJlq6iIsbJQ,1069
|
|
30
|
-
imagepro_python-1.4.0.dist-info/METADATA,sha256=POgVUYD-3fKBCKxkjSgKTZqCt5BWjvtfDBqlrn7Qg9I,11121
|
|
31
|
-
imagepro_python-1.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
32
|
-
imagepro_python-1.4.0.dist-info/top_level.txt,sha256=UOuPzsoWrVKXgUupOSI96shJ6wE8Fhz-8bm8oMlLC3s,9
|
|
33
|
-
imagepro_python-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|