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