ImagePRO-Python 0.1.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 ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,129 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import mediapipe as mp
6
+
7
+ # Add parent directory to import custom modules
8
+ parent_dir = Path(__file__).resolve().parent.parent.parent
9
+ sys.path.append(str(parent_dir))
10
+
11
+ from io_handler import IOHandler
12
+
13
+ mp_pose = mp.solutions.pose
14
+
15
+ def detect_body_pose(
16
+ model_accuracy=0.7,
17
+ landmarks_idx=None,
18
+ image_path=None,
19
+ np_image=None,
20
+ result_path=None,
21
+ pose_obj=None
22
+ ):
23
+ """
24
+ Detects body landmarks from an image using MediaPipe Pose.
25
+
26
+ Args:
27
+ model_accuracy (float): Confidence threshold (0.0 to 1.0).
28
+ landmarks_idx (list[int] | None): Indices to extract (default: all 33).
29
+ image_path (str | None): Path to image file.
30
+ np_image (np.ndarray | None): Already loaded image (used if image_path is None).
31
+ result_path (str | None): Path to save image (.jpg) or coordinates (.csv).
32
+ pose_obj (mp.solutions.pose.Pose | None): Optional pre-initialized pose model.
33
+
34
+ Returns:
35
+ str | tuple[np.ndarray, list]: Output message or annotated image and landmark list.
36
+ """
37
+ if landmarks_idx is not None and not isinstance(landmarks_idx, list):
38
+ raise TypeError("'landmarks_idx' must be a list or None.")
39
+
40
+ if result_path:
41
+ if not isinstance(result_path, str):
42
+ raise TypeError("'result_path' must be a string.")
43
+ if not (result_path.endswith('.jpg') or result_path.endswith('.csv')):
44
+ raise ValueError("Supported extensions: '.jpg', '.csv'.")
45
+
46
+ np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
47
+
48
+ if landmarks_idx is None:
49
+ landmarks_idx = list(range(33))
50
+
51
+ if pose_obj is None:
52
+ pose_obj = mp_pose.Pose(
53
+ min_detection_confidence=model_accuracy,
54
+ static_image_mode=True
55
+ )
56
+
57
+ image_rgb = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
58
+ result = pose_obj.process(image_rgb)
59
+
60
+ annotated_image = np_image.copy()
61
+ all_landmarks = []
62
+
63
+ if result.pose_landmarks:
64
+ if result_path is None or result_path.endswith('.jpg'):
65
+ if len(landmarks_idx) == 33:
66
+ mp.solutions.drawing_utils.draw_landmarks(
67
+ annotated_image,
68
+ result.pose_landmarks,
69
+ mp_pose.POSE_CONNECTIONS,
70
+ landmark_drawing_spec=mp.solutions.drawing_styles.get_default_pose_landmarks_style()
71
+ )
72
+ else:
73
+ h, w, _ = annotated_image.shape
74
+ for idx in landmarks_idx:
75
+ lm = result.pose_landmarks.landmark[idx]
76
+ x, y = int(w * lm.x), int(h * lm.y)
77
+ cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
78
+
79
+ if result_path is None or result_path.endswith('.csv'):
80
+ for idx in landmarks_idx:
81
+ lm = result.pose_landmarks.landmark[idx]
82
+ all_landmarks.append([idx, lm.x, lm.y, lm.z])
83
+
84
+ if result_path:
85
+ if result_path.endswith('.jpg'):
86
+ return IOHandler.save_image(annotated_image, result_path)
87
+ elif result_path.endswith('.csv'):
88
+ return IOHandler.save_csv(all_landmarks, result_path)
89
+ else:
90
+ return annotated_image, all_landmarks
91
+
92
+
93
+ def detect_body_pose_live():
94
+ """
95
+ Starts webcam and shows real-time body pose detection.
96
+ Press ESC to exit.
97
+ """
98
+ cap = cv2.VideoCapture(0)
99
+ if not cap.isOpened():
100
+ raise RuntimeError("Failed to open webcam.")
101
+
102
+ pose_obj = mp_pose.Pose(
103
+ min_detection_confidence=0.7,
104
+ static_image_mode=True
105
+ )
106
+
107
+ try:
108
+ while True:
109
+ success, frame = cap.read()
110
+ if not success:
111
+ print("Ignoring empty camera frame.")
112
+ continue
113
+
114
+ try:
115
+ annotated_img, _ = detect_body_pose(np_image=frame, pose_obj=pose_obj)
116
+ except ValueError:
117
+ annotated_img = frame
118
+
119
+ cv2.imshow('ImagePRO - Live Body Pose Detection', annotated_img)
120
+
121
+ if cv2.waitKey(5) & 0xFF == 27:
122
+ break
123
+ finally:
124
+ cap.release()
125
+ cv2.destroyAllWindows()
126
+
127
+
128
+ if __name__ == "__main__":
129
+ detect_body_pose_live()
@@ -0,0 +1,157 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import mediapipe as mp
6
+
7
+ # Add parent directory to import custom modules
8
+ parent_dir = Path(__file__).resolve().parent.parent.parent
9
+ sys.path.append(str(parent_dir))
10
+
11
+ from io_handler import IOHandler
12
+
13
+ mp_hands = mp.solutions.hands
14
+
15
+
16
+ def detect_hands(
17
+ max_hands=2,
18
+ min_confidence=0.7,
19
+ landmarks_idx=None,
20
+ image_path=None,
21
+ np_image=None,
22
+ result_path=None,
23
+ hands_obj=None
24
+ ):
25
+ """
26
+ Detects hand landmarks in an image using MediaPipe.
27
+
28
+ Args:
29
+ max_hands (int): Max number of hands to detect.
30
+ min_confidence (float): Minimum confidence for detection.
31
+ landmarks_idx (list[int] | None): Landmark indices to extract (default: all 21).
32
+ image_path (str | None): Image file path.
33
+ np_image (np.ndarray | None): Preloaded image array.
34
+ result_path (str | None): Path to save output image (.jpg) or landmarks (.csv).
35
+ hands_obj (mp.solutions.hands.Hands | None): Optional pre-initialized model.
36
+
37
+ Returns:
38
+ np.ndarray | list: Annotated image and landmarks list, or saved file confirmation.
39
+ """
40
+ if not isinstance(max_hands, int) or max_hands <= 0:
41
+ raise ValueError("'max_hands' must be a positive integer.")
42
+
43
+ if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
44
+ raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
45
+
46
+ if landmarks_idx is not None and not isinstance(landmarks_idx, list):
47
+ raise TypeError("'landmarks_idx' must be a list or None.")
48
+
49
+ if result_path:
50
+ if not isinstance(result_path, str):
51
+ raise TypeError("'result_path' must be a string.")
52
+ if not (result_path.endswith('.jpg') or result_path.endswith('.csv')):
53
+ raise ValueError("Only '.jpg' and '.csv' extensions are supported.")
54
+
55
+ np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
56
+
57
+ if hands_obj is None:
58
+ hands_obj = mp_hands.Hands(
59
+ min_detection_confidence=min_confidence,
60
+ max_num_hands=max_hands,
61
+ static_image_mode=True
62
+ )
63
+
64
+ if landmarks_idx is None:
65
+ landmarks_idx = list(range(21))
66
+
67
+ rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
68
+ results = hands_obj.process(rgb_image)
69
+
70
+ annotated_image = np_image.copy()
71
+ all_landmarks = []
72
+
73
+ if results.multi_hand_landmarks:
74
+ for hand_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
75
+ if result_path is None or result_path.endswith('.jpg'):
76
+ if len(landmarks_idx) == 21:
77
+ mp.solutions.drawing_utils.draw_landmarks(
78
+ image=annotated_image,
79
+ landmark_list=hand_landmarks,
80
+ connections=mp_hands.HAND_CONNECTIONS,
81
+ landmark_drawing_spec=mp.solutions.drawing_styles.get_default_hand_landmarks_style(),
82
+ connection_drawing_spec=mp.solutions.drawing_styles.get_default_hand_connections_style()
83
+ )
84
+ else:
85
+ h, w, _ = annotated_image.shape
86
+ for idx in landmarks_idx:
87
+ lm = hand_landmarks.landmark[idx]
88
+ x, y = int(w * lm.x), int(h * lm.y)
89
+ cv2.circle(annotated_image, (x, y), 3, (0, 0, 255), -1)
90
+
91
+ if result_path is None or result_path.endswith('.csv'):
92
+ hand_data = []
93
+ for idx in landmarks_idx:
94
+ lm = hand_landmarks.landmark[idx]
95
+ hand_data.append([hand_id, idx, lm.x, lm.y, lm.z])
96
+ all_landmarks.append(hand_data)
97
+ else:
98
+ return np_image, []
99
+
100
+ if result_path:
101
+ if result_path.endswith('.jpg'):
102
+ return IOHandler.save_image(annotated_image, result_path)
103
+ elif result_path.endswith('.csv'):
104
+ flat_landmarks = [item for sublist in all_landmarks for item in sublist]
105
+ return IOHandler.save_csv(flat_landmarks, result_path)
106
+ else:
107
+ return annotated_image, all_landmarks
108
+
109
+
110
+ def detect_hands_live(max_hands=2, min_confidence=0.7):
111
+ """
112
+ Real-time hand detection via webcam.
113
+
114
+ Args:
115
+ max_hands (int): Maximum hands to detect.
116
+ min_confidence (float): Detection confidence threshold.
117
+ """
118
+ if not isinstance(max_hands, int) or max_hands <= 0:
119
+ raise ValueError("'max_hands' must be a positive integer.")
120
+ if not isinstance(min_confidence, (int, float)) or not (0.0 <= min_confidence <= 1.0):
121
+ raise ValueError("'min_confidence' must be a float between 0.0 and 1.0.")
122
+
123
+ cap = cv2.VideoCapture(0)
124
+ if not cap.isOpened():
125
+ raise RuntimeError("Failed to open webcam.")
126
+
127
+ hands_obj = mp_hands.Hands(
128
+ min_detection_confidence=min_confidence,
129
+ max_num_hands=max_hands,
130
+ static_image_mode=True
131
+ )
132
+
133
+ try:
134
+ while True:
135
+ success, frame = cap.read()
136
+ if not success:
137
+ print("Ignoring empty camera frame.")
138
+ continue
139
+
140
+ annotated_img, _ = detect_hands(
141
+ max_hands=max_hands,
142
+ min_confidence=min_confidence,
143
+ np_image=frame,
144
+ hands_obj=hands_obj
145
+ )
146
+
147
+ cv2.imshow('Live hand detector - ImagePRO', annotated_img)
148
+
149
+ if cv2.waitKey(5) & 0xFF == 27:
150
+ break
151
+ finally:
152
+ cap.release()
153
+ cv2.destroyAllWindows()
154
+
155
+
156
+ if __name__ == "__main__":
157
+ detect_hands_live(max_hands=2)
File without changes
@@ -0,0 +1,152 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import mediapipe as mp
6
+
7
+ # Add parent directory to sys.path
8
+ parent_dir = Path(__file__).resolve().parent.parent.parent
9
+ sys.path.append(str(parent_dir))
10
+
11
+ from io_handler import IOHandler
12
+ from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
13
+
14
+ mp_face_mesh = mp.solutions.face_mesh
15
+
16
+
17
+ def analyze_eye_status(
18
+ min_confidence=0.7,
19
+ image_path=None,
20
+ np_image=None,
21
+ face_mesh_obj=None,
22
+ threshold=0.2
23
+ ):
24
+ """
25
+ Analyze eye open/closed status using Eye Aspect Ratio (EAR).
26
+
27
+ Args:
28
+ min_confidence (float): Minimum confidence for FaceMesh detection.
29
+ image_path (str, optional): Path to input image.
30
+ np_image (np.ndarray, optional): Image array.
31
+ face_mesh_obj (FaceMesh, optional): Reusable instance of FaceMesh.
32
+ threshold (float): EAR threshold below which eye is considered closed.
33
+
34
+ Returns:
35
+ bool: True if eye is open, False if closed.
36
+
37
+ Raises:
38
+ ValueError: If landmarks are not detected or inputs are invalid.
39
+ """
40
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
41
+ raise ValueError("'min_confidence' must be between 0 and 1.")
42
+
43
+ np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
44
+ h, w = np_image.shape[:2]
45
+
46
+ if face_mesh_obj is None:
47
+ face_mesh_obj = mp_face_mesh.FaceMesh(
48
+ min_detection_confidence=min_confidence,
49
+ refine_landmarks=True,
50
+ static_image_mode=True
51
+ )
52
+
53
+ # Landmark indices for right eye (from MediaPipe's model)
54
+ # 386 (top eyelid), 374 (bottom eyelid), 263 (outer corner), 362 (inner corner)
55
+ indices = [386, 374, 263, 362]
56
+
57
+ _, landmarks = analyze_face_mesh(
58
+ max_faces=1,
59
+ min_confidence=min_confidence,
60
+ landmarks_idx=indices,
61
+ np_image=np_image,
62
+ face_mesh_obj=face_mesh_obj
63
+ )
64
+
65
+ if not landmarks:
66
+ raise ValueError("No face landmarks detected.")
67
+
68
+ eye_points = {lm[1]: lm for lm in landmarks[0]}
69
+
70
+ try:
71
+ top_y = eye_points[386][3] * h
72
+ bottom_y = eye_points[374][3] * h
73
+ left_x = eye_points[263][2] * w
74
+ right_x = eye_points[362][2] * w
75
+ except KeyError:
76
+ raise ValueError("Missing necessary eye landmarks.")
77
+
78
+ vertical_dist = abs(bottom_y - top_y)
79
+ horizontal_dist = abs(right_x - left_x)
80
+
81
+ if horizontal_dist == 0:
82
+ return False # avoid division by zero
83
+
84
+ ear = vertical_dist / horizontal_dist
85
+ return ear > threshold
86
+
87
+
88
+ def analyze_eye_status_live(min_confidence=0.7, threshold=0.2):
89
+ """
90
+ Perform live eye open/closed detection using webcam input.
91
+
92
+ Args:
93
+ min_confidence (float): Minimum confidence for detection.
94
+ threshold (float): EAR threshold to consider eyes open.
95
+
96
+ Raises:
97
+ ValueError: If confidence value is invalid.
98
+ RuntimeError: If webcam cannot be accessed.
99
+ """
100
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
101
+ raise ValueError("'min_confidence' must be between 0 and 1.")
102
+
103
+ cap = cv2.VideoCapture(0)
104
+ if not cap.isOpened():
105
+ raise RuntimeError("Cannot access webcam.")
106
+
107
+ face_mesh = mp_face_mesh.FaceMesh(
108
+ max_num_faces=1,
109
+ min_detection_confidence=min_confidence,
110
+ refine_landmarks=True,
111
+ static_image_mode=True
112
+ )
113
+
114
+ try:
115
+ while True:
116
+ success, frame = cap.read()
117
+ if not success:
118
+ print("Skipping empty frame.")
119
+ continue
120
+
121
+ try:
122
+ is_open = analyze_eye_status(
123
+ min_confidence=min_confidence,
124
+ np_image=frame,
125
+ face_mesh_obj=face_mesh,
126
+ threshold=threshold
127
+ )
128
+ status = "Open" if is_open else "Closed"
129
+ except ValueError:
130
+ status = "No face"
131
+
132
+ cv2.putText(
133
+ frame,
134
+ f"Eye: {status}",
135
+ (10, 40),
136
+ cv2.FONT_HERSHEY_SIMPLEX,
137
+ 0.7,
138
+ (0, 255, 0) if status == "Open" else (0, 0, 255),
139
+ 2
140
+ )
141
+
142
+ cv2.imshow("ImagePRO - Eye Status (ESC to Exit)", frame)
143
+ if cv2.waitKey(5) & 0xFF == 27:
144
+ break
145
+
146
+ finally:
147
+ cap.release()
148
+ cv2.destroyAllWindows()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ analyze_eye_status_live()
@@ -0,0 +1,84 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+
7
+ # Add parent directory to sys.path for custom module imports
8
+ parent_dir = Path(__file__).resolve().parent.parent.parent
9
+ sys.path.append(str(parent_dir))
10
+
11
+ from io_handler import IOHandler
12
+ from human_analysis.face_analysis.face_mesh_analysis import analyze_face_mesh
13
+
14
+
15
+ def detect_faces(
16
+ max_faces=1,
17
+ min_confidence=0.7,
18
+ image_path=None,
19
+ np_image=None,
20
+ result_path=None,
21
+ face_mesh_obj=None
22
+ ):
23
+ """
24
+ Detect and crop face regions using MediaPipe face landmarks.
25
+
26
+ Args:
27
+ max_faces (int): Maximum number of faces to detect.
28
+ min_confidence (float): Confidence threshold (0–1).
29
+ image_path (str, optional): Path to input image.
30
+ np_image (np.ndarray, optional): Image array.
31
+ result_path (str, optional): Save path for cropped face(s).
32
+ face_mesh_obj (FaceMesh, optional): Reusable MediaPipe instance.
33
+
34
+ Returns:
35
+ str or list[np.ndarray]: Confirmation message if saved, else list of cropped face images.
36
+
37
+ Raises:
38
+ TypeError, ValueError: On invalid inputs or if no faces detected.
39
+ """
40
+ if not isinstance(max_faces, int) or max_faces <= 0:
41
+ raise ValueError("'max_faces' must be a positive integer.")
42
+
43
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
44
+ raise ValueError("'min_confidence' must be a float between 0 and 1.")
45
+
46
+ np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
47
+ height, width = np_image.shape[:2]
48
+
49
+ # Selected face outline landmark indices (based on MediaPipe's 468-point model)
50
+ face_outline_indices = [
51
+ 10, 338, 297, 332, 284, 251, 389, 356, 454, 323,
52
+ 361, 288, 397, 365, 379, 378, 400, 377, 152, 148,
53
+ 176, 149, 150, 136, 164, 163, 153, 157
54
+ ]
55
+
56
+ # Run face mesh analysis
57
+ _, raw_landmarks = analyze_face_mesh(
58
+ max_faces=max_faces,
59
+ min_confidence=min_confidence,
60
+ landmarks_idx=face_outline_indices,
61
+ np_image=np_image,
62
+ face_mesh_obj=face_mesh_obj
63
+ )
64
+
65
+ if not raw_landmarks:
66
+ raise ValueError("No face landmarks detected in the input image.")
67
+
68
+ # Convert normalized landmark coordinates to pixel positions
69
+ all_polygons = []
70
+ for face in raw_landmarks:
71
+ polygon = [
72
+ (int(x * width), int(y * height))
73
+ for _, _, x, y, _ in face
74
+ ]
75
+ all_polygons.append(np.array(polygon, dtype=np.int32))
76
+
77
+ # Crop faces using boundingRect
78
+ cropped_faces = []
79
+ for polygon in all_polygons:
80
+ x, y, w, h = cv2.boundingRect(polygon)
81
+ cropped = np_image[y:y + h, x:x + w]
82
+ cropped_faces.append(cropped)
83
+
84
+ return IOHandler.save_image(cropped_faces, result_path)
@@ -0,0 +1,172 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import mediapipe as mp
6
+
7
+ # Add parent directory to path for custom module imports
8
+ parent_dir = Path(__file__).resolve().parent.parent.parent
9
+ sys.path.append(str(parent_dir))
10
+
11
+ from io_handler import IOHandler
12
+
13
+ mp_face_mesh = mp.solutions.face_mesh
14
+ mp_drawing_utils = mp.solutions.drawing_utils
15
+ mp_drawing_styles = mp.solutions.drawing_styles
16
+
17
+
18
+ def analyze_face_mesh(
19
+ max_faces=1,
20
+ min_confidence=0.7,
21
+ landmarks_idx=None,
22
+ image_path=None,
23
+ np_image=None,
24
+ result_path=None,
25
+ face_mesh_obj=None
26
+ ):
27
+ """
28
+ Detect facial landmarks using MediaPipe FaceMesh.
29
+
30
+ Args:
31
+ max_faces (int): Maximum number of faces to detect.
32
+ min_confidence (float): Detection confidence threshold.
33
+ landmarks_idx (list, optional): Indices of landmarks to extract or draw.
34
+ image_path (str, optional): Path to input image file.
35
+ np_image (np.ndarray, optional): Image array.
36
+ result_path (str, optional): Save path (supports .jpg or .csv).
37
+ face_mesh_obj (FaceMesh, optional): External instance to reuse.
38
+
39
+ Returns:
40
+ str or tuple or np.ndarray or list:
41
+ - If `result_path` ends with `.jpg`: save/return annotated image.
42
+ - If `result_path` ends with `.csv`: save/return coordinates.
43
+ - If no result_path: returns (image, landmarks list).
44
+ """
45
+ if not isinstance(max_faces, int) or max_faces <= 0:
46
+ raise ValueError("'max_faces' must be a positive integer.")
47
+
48
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
49
+ raise ValueError("'min_confidence' must be a float between 0 and 1.")
50
+
51
+ if landmarks_idx is not None and not isinstance(landmarks_idx, list):
52
+ raise TypeError("'landmarks_idx' must be a list of integers or None.")
53
+
54
+ if result_path and not isinstance(result_path, str):
55
+ raise TypeError("'result_path' must be a string or None.")
56
+
57
+ if result_path and not result_path.endswith(('.jpg', '.csv')):
58
+ raise ValueError("Only '.jpg' or '.csv' formats are supported.")
59
+
60
+ if face_mesh_obj is None:
61
+ face_Mesh = mp_face_mesh.FaceMesh(
62
+ max_num_faces=max_faces,
63
+ min_detection_confidence=min_confidence,
64
+ refine_landmarks=True,
65
+ static_image_mode=True
66
+ )
67
+ else:
68
+ face_Mesh = face_mesh_obj
69
+
70
+ np_image = IOHandler.load_image(image_path=image_path, np_image=np_image)
71
+ rgb_image = cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB)
72
+ results = face_Mesh.process(rgb_image)
73
+
74
+ if not results.multi_face_landmarks:
75
+ raise ValueError("No face landmarks detected.")
76
+
77
+ landmarks_idx = landmarks_idx or list(range(468))
78
+ annotated = np_image.copy()
79
+ all_landmarks = []
80
+
81
+ for face_id, face_landmarks in enumerate(results.multi_face_landmarks):
82
+ if result_path is None or result_path.endswith('.jpg'):
83
+ if len(landmarks_idx) == 468:
84
+ mp_drawing_utils.draw_landmarks(
85
+ image=annotated,
86
+ landmark_list=face_landmarks,
87
+ connections=mp_face_mesh.FACEMESH_TESSELATION,
88
+ landmark_drawing_spec=None,
89
+ connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()
90
+ )
91
+ else:
92
+ h, w = annotated.shape[:2]
93
+ for idx in landmarks_idx:
94
+ lm = face_landmarks.landmark[idx]
95
+ cx, cy = int(w * lm.x), int(h * lm.y)
96
+ cv2.circle(annotated, (cx, cy), 3, (0, 0, 255), -1)
97
+
98
+ if result_path is None or result_path.endswith('.csv'):
99
+ face_data = [
100
+ [face_id, idx, lm.x, lm.y, lm.z]
101
+ for idx in landmarks_idx
102
+ for lm in [face_landmarks.landmark[idx]]
103
+ ]
104
+ all_landmarks.append(face_data)
105
+
106
+ # Output handling
107
+ if result_path:
108
+ if result_path.endswith('.jpg'):
109
+ return IOHandler.save_image(annotated, result_path)
110
+ elif result_path.endswith('.csv'):
111
+ flat_data = [row for face in all_landmarks for row in face]
112
+ return IOHandler.save_csv(flat_data, result_path)
113
+
114
+ return annotated, all_landmarks
115
+
116
+
117
+ def analyze_face_mesh_live(max_faces=1, min_confidence=0.7):
118
+ """
119
+ Launch webcam with real-time face landmark overlay.
120
+
121
+ Args:
122
+ max_faces (int): Max faces to detect.
123
+ min_confidence (float): Detection confidence threshold.
124
+
125
+ Raises:
126
+ RuntimeError: If webcam can't be opened.
127
+ """
128
+ if not isinstance(max_faces, int) or max_faces <= 0:
129
+ raise ValueError("'max_faces' must be a positive integer.")
130
+
131
+ if not isinstance(min_confidence, (int, float)) or not (0 <= min_confidence <= 1):
132
+ raise ValueError("'min_confidence' must be between 0 and 1.")
133
+
134
+ cap = cv2.VideoCapture(0)
135
+ if not cap.isOpened():
136
+ raise RuntimeError("Unable to open webcam.")
137
+
138
+ face_Mesh = mp_face_mesh.FaceMesh(
139
+ max_num_faces=max_faces,
140
+ min_detection_confidence=min_confidence,
141
+ refine_landmarks=True,
142
+ static_image_mode=True
143
+ )
144
+
145
+ try:
146
+ while True:
147
+ success, frame = cap.read()
148
+ if not success:
149
+ print("Skipping empty frame.")
150
+ continue
151
+
152
+ try:
153
+ result_image, _ = analyze_face_mesh(
154
+ max_faces=max_faces,
155
+ min_confidence=min_confidence,
156
+ np_image=frame,
157
+ face_mesh_obj=face_Mesh
158
+ )
159
+ except ValueError:
160
+ result_image = frame
161
+
162
+ cv2.imshow("ImagePRO - Face Mesh", result_image)
163
+
164
+ if cv2.waitKey(1) & 0xFF == 27: # ESC key
165
+ break
166
+ finally:
167
+ cap.release()
168
+ cv2.destroyAllWindows()
169
+
170
+
171
+ if __name__ == "__main__":
172
+ analyze_face_mesh_live(max_faces=1)