eye-cv 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.
- eye/__init__.py +115 -0
- eye/__init___supervision_original.py +120 -0
- eye/annotators/__init__.py +0 -0
- eye/annotators/base.py +22 -0
- eye/annotators/core.py +2699 -0
- eye/annotators/line.py +107 -0
- eye/annotators/modern.py +529 -0
- eye/annotators/trace.py +142 -0
- eye/annotators/utils.py +177 -0
- eye/assets/__init__.py +2 -0
- eye/assets/downloader.py +95 -0
- eye/assets/list.py +83 -0
- eye/classification/__init__.py +0 -0
- eye/classification/core.py +188 -0
- eye/config.py +2 -0
- eye/core/__init__.py +0 -0
- eye/core/trackers/__init__.py +1 -0
- eye/core/trackers/botsort_tracker.py +336 -0
- eye/core/trackers/bytetrack_tracker.py +284 -0
- eye/core/trackers/sort_tracker.py +200 -0
- eye/core/tracking.py +146 -0
- eye/dataset/__init__.py +0 -0
- eye/dataset/core.py +919 -0
- eye/dataset/formats/__init__.py +0 -0
- eye/dataset/formats/coco.py +258 -0
- eye/dataset/formats/pascal_voc.py +279 -0
- eye/dataset/formats/yolo.py +272 -0
- eye/dataset/utils.py +259 -0
- eye/detection/__init__.py +0 -0
- eye/detection/auto_convert.py +155 -0
- eye/detection/core.py +1529 -0
- eye/detection/detections_enhanced.py +392 -0
- eye/detection/line_zone.py +859 -0
- eye/detection/lmm.py +184 -0
- eye/detection/overlap_filter.py +270 -0
- eye/detection/tools/__init__.py +0 -0
- eye/detection/tools/csv_sink.py +181 -0
- eye/detection/tools/inference_slicer.py +288 -0
- eye/detection/tools/json_sink.py +142 -0
- eye/detection/tools/polygon_zone.py +202 -0
- eye/detection/tools/smoother.py +123 -0
- eye/detection/tools/smoothing.py +179 -0
- eye/detection/tools/smoothing_config.py +202 -0
- eye/detection/tools/transformers.py +247 -0
- eye/detection/utils.py +1175 -0
- eye/draw/__init__.py +0 -0
- eye/draw/color.py +154 -0
- eye/draw/utils.py +374 -0
- eye/filters.py +112 -0
- eye/geometry/__init__.py +0 -0
- eye/geometry/core.py +128 -0
- eye/geometry/utils.py +47 -0
- eye/keypoint/__init__.py +0 -0
- eye/keypoint/annotators.py +442 -0
- eye/keypoint/core.py +687 -0
- eye/keypoint/skeletons.py +2647 -0
- eye/metrics/__init__.py +21 -0
- eye/metrics/core.py +72 -0
- eye/metrics/detection.py +843 -0
- eye/metrics/f1_score.py +648 -0
- eye/metrics/mean_average_precision.py +628 -0
- eye/metrics/mean_average_recall.py +697 -0
- eye/metrics/precision.py +653 -0
- eye/metrics/recall.py +652 -0
- eye/metrics/utils/__init__.py +0 -0
- eye/metrics/utils/object_size.py +158 -0
- eye/metrics/utils/utils.py +9 -0
- eye/py.typed +0 -0
- eye/quick.py +104 -0
- eye/tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/core.py +386 -0
- eye/tracker/byte_tracker/kalman_filter.py +205 -0
- eye/tracker/byte_tracker/matching.py +69 -0
- eye/tracker/byte_tracker/single_object_track.py +178 -0
- eye/tracker/byte_tracker/utils.py +18 -0
- eye/utils/__init__.py +0 -0
- eye/utils/conversion.py +132 -0
- eye/utils/file.py +159 -0
- eye/utils/image.py +794 -0
- eye/utils/internal.py +200 -0
- eye/utils/iterables.py +84 -0
- eye/utils/notebook.py +114 -0
- eye/utils/video.py +307 -0
- eye/utils_eye/__init__.py +1 -0
- eye/utils_eye/geometry.py +71 -0
- eye/utils_eye/nms.py +55 -0
- eye/validators/__init__.py +140 -0
- eye/web.py +271 -0
- eye_cv-1.0.0.dist-info/METADATA +319 -0
- eye_cv-1.0.0.dist-info/RECORD +94 -0
- eye_cv-1.0.0.dist-info/WHEEL +5 -0
- eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
- eye_cv-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Geometry utilities."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Point:
|
|
10
|
+
"""2D point."""
|
|
11
|
+
x: float
|
|
12
|
+
y: float
|
|
13
|
+
|
|
14
|
+
def as_tuple(self) -> Tuple[float, float]:
|
|
15
|
+
return (self.x, self.y)
|
|
16
|
+
|
|
17
|
+
def as_int_tuple(self) -> Tuple[int, int]:
|
|
18
|
+
return (int(self.x), int(self.y))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_polygon_center(polygon: np.ndarray) -> Point:
|
|
22
|
+
"""Calculate center of polygon."""
|
|
23
|
+
center_x = np.mean(polygon[:, 0])
|
|
24
|
+
center_y = np.mean(polygon[:, 1])
|
|
25
|
+
return Point(center_x, center_y)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_point_in_polygon(points: np.ndarray, polygon: np.ndarray) -> np.ndarray:
|
|
29
|
+
"""Check if points are inside polygon.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
points: (N, 2) array of points
|
|
33
|
+
polygon: (M, 2) array of polygon vertices
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Boolean array of length N
|
|
37
|
+
"""
|
|
38
|
+
from matplotlib.path import Path
|
|
39
|
+
path = Path(polygon)
|
|
40
|
+
return path.contains_points(points)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def calculate_iou(box1: np.ndarray, box2: np.ndarray) -> float:
|
|
44
|
+
"""Calculate IoU between two boxes.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
box1, box2: [x1, y1, x2, y2]
|
|
48
|
+
"""
|
|
49
|
+
x1_inter = max(box1[0], box2[0])
|
|
50
|
+
y1_inter = max(box1[1], box2[1])
|
|
51
|
+
x2_inter = min(box1[2], box2[2])
|
|
52
|
+
y2_inter = min(box1[3], box2[3])
|
|
53
|
+
|
|
54
|
+
if x2_inter < x1_inter or y2_inter < y1_inter:
|
|
55
|
+
return 0.0
|
|
56
|
+
|
|
57
|
+
intersection = (x2_inter - x1_inter) * (y2_inter - y1_inter)
|
|
58
|
+
|
|
59
|
+
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
|
|
60
|
+
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
|
|
61
|
+
|
|
62
|
+
union = area1 + area2 - intersection
|
|
63
|
+
|
|
64
|
+
return intersection / (union + 1e-6)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def polygon_area(polygon: np.ndarray) -> float:
|
|
68
|
+
"""Calculate area of polygon using shoelace formula."""
|
|
69
|
+
x = polygon[:, 0]
|
|
70
|
+
y = polygon[:, 1]
|
|
71
|
+
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
eye/utils_eye/nms.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Non-maximum suppression."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def non_max_suppression(
|
|
7
|
+
boxes: np.ndarray,
|
|
8
|
+
scores: np.ndarray,
|
|
9
|
+
iou_threshold: float = 0.5
|
|
10
|
+
) -> np.ndarray:
|
|
11
|
+
"""Apply NMS to remove overlapping boxes.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
boxes: (N, 4) array of boxes [x1, y1, x2, y2]
|
|
15
|
+
scores: (N,) array of scores
|
|
16
|
+
iou_threshold: IoU threshold for suppression
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Indices of boxes to keep
|
|
20
|
+
"""
|
|
21
|
+
if len(boxes) == 0:
|
|
22
|
+
return np.array([], dtype=int)
|
|
23
|
+
|
|
24
|
+
x1 = boxes[:, 0]
|
|
25
|
+
y1 = boxes[:, 1]
|
|
26
|
+
x2 = boxes[:, 2]
|
|
27
|
+
y2 = boxes[:, 3]
|
|
28
|
+
|
|
29
|
+
areas = (x2 - x1) * (y2 - y1)
|
|
30
|
+
order = scores.argsort()[::-1]
|
|
31
|
+
|
|
32
|
+
keep = []
|
|
33
|
+
while len(order) > 0:
|
|
34
|
+
i = order[0]
|
|
35
|
+
keep.append(i)
|
|
36
|
+
|
|
37
|
+
xx1 = np.maximum(x1[i], x1[order[1:]])
|
|
38
|
+
yy1 = np.maximum(y1[i], y1[order[1:]])
|
|
39
|
+
xx2 = np.minimum(x2[i], x2[order[1:]])
|
|
40
|
+
yy2 = np.minimum(y2[i], y2[order[1:]])
|
|
41
|
+
|
|
42
|
+
w = np.maximum(0, xx2 - xx1)
|
|
43
|
+
h = np.maximum(0, yy2 - yy1)
|
|
44
|
+
|
|
45
|
+
intersection = w * h
|
|
46
|
+
iou = intersection / (areas[i] + areas[order[1:]] - intersection + 1e-6)
|
|
47
|
+
|
|
48
|
+
inds = np.where(iou <= iou_threshold)[0]
|
|
49
|
+
order = order[inds + 1]
|
|
50
|
+
|
|
51
|
+
return np.array(keep, dtype=int)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Alias for simpler name
|
|
55
|
+
nms = non_max_suppression
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_xyxy(xyxy: Any) -> None:
|
|
7
|
+
expected_shape = "(_, 4)"
|
|
8
|
+
actual_shape = str(getattr(xyxy, "shape", None))
|
|
9
|
+
is_valid = isinstance(xyxy, np.ndarray) and xyxy.ndim == 2 and xyxy.shape[1] == 4
|
|
10
|
+
if not is_valid:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
f"xyxy must be a 2D np.ndarray with shape {expected_shape}, but got shape "
|
|
13
|
+
f"{actual_shape}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_mask(mask: Any, n: int) -> None:
|
|
18
|
+
expected_shape = f"({n}, H, W)"
|
|
19
|
+
actual_shape = str(getattr(mask, "shape", None))
|
|
20
|
+
is_valid = mask is None or (
|
|
21
|
+
isinstance(mask, np.ndarray) and len(mask.shape) == 3 and mask.shape[0] == n
|
|
22
|
+
)
|
|
23
|
+
if not is_valid:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"mask must be a 3D np.ndarray with shape {expected_shape}, but got shape "
|
|
26
|
+
f"{actual_shape}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_class_id(class_id: Any, n: int) -> None:
|
|
31
|
+
expected_shape = f"({n},)"
|
|
32
|
+
actual_shape = str(getattr(class_id, "shape", None))
|
|
33
|
+
is_valid = class_id is None or (
|
|
34
|
+
isinstance(class_id, np.ndarray) and class_id.shape == (n,)
|
|
35
|
+
)
|
|
36
|
+
if not is_valid:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"class_id must be a 1D np.ndarray with shape {expected_shape}, but got "
|
|
39
|
+
f"shape {actual_shape}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_confidence(confidence: Any, n: int) -> None:
|
|
44
|
+
expected_shape = f"({n},)"
|
|
45
|
+
actual_shape = str(getattr(confidence, "shape", None))
|
|
46
|
+
is_valid = confidence is None or (
|
|
47
|
+
isinstance(confidence, np.ndarray) and confidence.shape == (n,)
|
|
48
|
+
)
|
|
49
|
+
if not is_valid:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"confidence must be a 1D np.ndarray with shape {expected_shape}, but got "
|
|
52
|
+
f"shape {actual_shape}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
|
|
57
|
+
expected_shape = f"({n,m})"
|
|
58
|
+
actual_shape = str(getattr(confidence, "shape", None))
|
|
59
|
+
|
|
60
|
+
if confidence is not None:
|
|
61
|
+
is_valid = isinstance(confidence, np.ndarray) and confidence.shape == (n, m)
|
|
62
|
+
if not is_valid:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"confidence must be a 1D np.ndarray with shape {expected_shape}, but "
|
|
65
|
+
f"got shape {actual_shape}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_tracker_id(tracker_id: Any, n: int) -> None:
|
|
70
|
+
expected_shape = f"({n},)"
|
|
71
|
+
actual_shape = str(getattr(tracker_id, "shape", None))
|
|
72
|
+
is_valid = tracker_id is None or (
|
|
73
|
+
isinstance(tracker_id, np.ndarray) and tracker_id.shape == (n,)
|
|
74
|
+
)
|
|
75
|
+
if not is_valid:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"tracker_id must be a 1D np.ndarray with shape {expected_shape}, but got "
|
|
78
|
+
f"shape {actual_shape}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_data(data: Dict[str, Any], n: int) -> None:
|
|
83
|
+
for key, value in data.items():
|
|
84
|
+
if isinstance(value, list):
|
|
85
|
+
if len(value) != n:
|
|
86
|
+
raise ValueError(f"Length of list for key '{key}' must be {n}")
|
|
87
|
+
elif isinstance(value, np.ndarray):
|
|
88
|
+
if value.ndim == 1 and value.shape[0] != n:
|
|
89
|
+
raise ValueError(f"Shape of np.ndarray for key '{key}' must be ({n},)")
|
|
90
|
+
elif value.ndim > 1 and value.shape[0] != n:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"First dimension of np.ndarray for key '{key}' must have size {n}"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Value for key '{key}' must be a list or np.ndarray")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_xy(xy: Any, n: int, m: int) -> None:
|
|
99
|
+
expected_shape = f"({n, m},)"
|
|
100
|
+
actual_shape = str(getattr(xy, "shape", None))
|
|
101
|
+
|
|
102
|
+
is_valid = isinstance(xy, np.ndarray) and (
|
|
103
|
+
xy.shape == (n, m, 2) or xy.shape == (n, m, 3)
|
|
104
|
+
)
|
|
105
|
+
if not is_valid:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"xy must be a 2D np.ndarray with shape {expected_shape}, but got shape "
|
|
108
|
+
f"{actual_shape}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_detections_fields(
|
|
113
|
+
xyxy: Any,
|
|
114
|
+
mask: Any,
|
|
115
|
+
class_id: Any,
|
|
116
|
+
confidence: Any,
|
|
117
|
+
tracker_id: Any,
|
|
118
|
+
data: Dict[str, Any],
|
|
119
|
+
) -> None:
|
|
120
|
+
validate_xyxy(xyxy)
|
|
121
|
+
n = len(xyxy)
|
|
122
|
+
validate_mask(mask, n)
|
|
123
|
+
validate_class_id(class_id, n)
|
|
124
|
+
validate_confidence(confidence, n)
|
|
125
|
+
validate_tracker_id(tracker_id, n)
|
|
126
|
+
validate_data(data, n)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_keypoints_fields(
|
|
130
|
+
xy: Any,
|
|
131
|
+
class_id: Any,
|
|
132
|
+
confidence: Any,
|
|
133
|
+
data: Dict[str, Any],
|
|
134
|
+
) -> None:
|
|
135
|
+
n = len(xy)
|
|
136
|
+
m = len(xy[0]) if len(xy) > 0 else 0
|
|
137
|
+
validate_xy(xy, n, m)
|
|
138
|
+
validate_class_id(class_id, n)
|
|
139
|
+
validate_keypoint_confidence(confidence, n, m)
|
|
140
|
+
validate_data(data, n)
|
eye/web.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Web-ready API for Flask, FastAPI, Django, etc."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List
|
|
4
|
+
import base64
|
|
5
|
+
import io
|
|
6
|
+
import numpy as np
|
|
7
|
+
import cv2
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class WebDetection:
|
|
14
|
+
"""Web-friendly detection format (JSON serializable)."""
|
|
15
|
+
bbox: List[float] # [x1, y1, x2, y2]
|
|
16
|
+
confidence: float
|
|
17
|
+
class_id: int
|
|
18
|
+
class_name: str
|
|
19
|
+
tracker_id: Optional[int] = None
|
|
20
|
+
mask: Optional[str] = None # Base64 encoded
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class WebResponse:
|
|
25
|
+
"""Standard web API response."""
|
|
26
|
+
success: bool
|
|
27
|
+
detections: List[WebDetection]
|
|
28
|
+
frame_id: int
|
|
29
|
+
processing_time_ms: float
|
|
30
|
+
metadata: Dict[str, Any]
|
|
31
|
+
error: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WebAPI:
|
|
35
|
+
"""Web-ready interface for Eye library.
|
|
36
|
+
|
|
37
|
+
Features:
|
|
38
|
+
- JSON serializable responses
|
|
39
|
+
- Base64 image encoding/decoding
|
|
40
|
+
- CORS-ready
|
|
41
|
+
- Stateless operation
|
|
42
|
+
- Fast processing
|
|
43
|
+
|
|
44
|
+
Perfect for: Flask, FastAPI, Django, Node.js bridges, etc.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
model: Any,
|
|
50
|
+
tracker: Optional[Any] = None,
|
|
51
|
+
class_names: Optional[Dict[int, str]] = None
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
model: Detection model (YOLO, etc.)
|
|
56
|
+
tracker: Optional tracker
|
|
57
|
+
class_names: Dict mapping class IDs to names
|
|
58
|
+
"""
|
|
59
|
+
self.model = model
|
|
60
|
+
self.tracker = tracker
|
|
61
|
+
self.class_names = class_names or {}
|
|
62
|
+
|
|
63
|
+
def process_base64(
|
|
64
|
+
self,
|
|
65
|
+
image_b64: str,
|
|
66
|
+
conf_threshold: float = 0.5,
|
|
67
|
+
frame_id: int = 0
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""Process base64-encoded image.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
image_b64: Base64 encoded image
|
|
73
|
+
conf_threshold: Confidence threshold
|
|
74
|
+
frame_id: Frame identifier
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
JSON-serializable dict
|
|
78
|
+
"""
|
|
79
|
+
import time
|
|
80
|
+
start = time.time()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Decode image
|
|
84
|
+
image = self.decode_base64_image(image_b64)
|
|
85
|
+
|
|
86
|
+
# Process
|
|
87
|
+
response = self.process_image(image, conf_threshold, frame_id)
|
|
88
|
+
response.processing_time_ms = (time.time() - start) * 1000
|
|
89
|
+
|
|
90
|
+
return asdict(response)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return asdict(WebResponse(
|
|
94
|
+
success=False,
|
|
95
|
+
detections=[],
|
|
96
|
+
frame_id=frame_id,
|
|
97
|
+
processing_time_ms=(time.time() - start) * 1000,
|
|
98
|
+
metadata={},
|
|
99
|
+
error=str(e)
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
def process_image(
|
|
103
|
+
self,
|
|
104
|
+
image: np.ndarray,
|
|
105
|
+
conf_threshold: float = 0.5,
|
|
106
|
+
frame_id: int = 0
|
|
107
|
+
) -> WebResponse:
|
|
108
|
+
"""Process numpy image.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
image: Input image (numpy array)
|
|
112
|
+
conf_threshold: Confidence threshold
|
|
113
|
+
frame_id: Frame identifier
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
WebResponse
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
from ..core.auto_detect import auto_convert
|
|
120
|
+
except ImportError:
|
|
121
|
+
from eye.core.auto_detect import auto_convert
|
|
122
|
+
|
|
123
|
+
# Detect
|
|
124
|
+
results = self.model(image, conf=conf_threshold, verbose=False)
|
|
125
|
+
detections = auto_convert(results)
|
|
126
|
+
|
|
127
|
+
# Track
|
|
128
|
+
if self.tracker:
|
|
129
|
+
detections = self.tracker.update(detections)
|
|
130
|
+
|
|
131
|
+
# Convert to web format
|
|
132
|
+
web_dets = []
|
|
133
|
+
for i in range(len(detections)):
|
|
134
|
+
web_det = WebDetection(
|
|
135
|
+
bbox=detections.xyxy[i].tolist(),
|
|
136
|
+
confidence=float(detections.confidence[i]) if detections.confidence is not None else 1.0,
|
|
137
|
+
class_id=int(detections.class_id[i]) if detections.class_id is not None else 0,
|
|
138
|
+
class_name=self.class_names.get(
|
|
139
|
+
int(detections.class_id[i]) if detections.class_id is not None else 0,
|
|
140
|
+
'unknown'
|
|
141
|
+
),
|
|
142
|
+
tracker_id=int(detections.tracker_id[i]) if detections.tracker_id is not None else None
|
|
143
|
+
)
|
|
144
|
+
web_dets.append(web_det)
|
|
145
|
+
|
|
146
|
+
return WebResponse(
|
|
147
|
+
success=True,
|
|
148
|
+
detections=web_dets,
|
|
149
|
+
frame_id=frame_id,
|
|
150
|
+
processing_time_ms=0.0, # Will be set by caller
|
|
151
|
+
metadata={
|
|
152
|
+
'image_shape': list(image.shape),
|
|
153
|
+
'num_detections': len(web_dets)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def decode_base64_image(image_b64: str) -> np.ndarray:
|
|
159
|
+
"""Decode base64 image to numpy array."""
|
|
160
|
+
# Remove data URL prefix if present
|
|
161
|
+
if ',' in image_b64:
|
|
162
|
+
image_b64 = image_b64.split(',')[1]
|
|
163
|
+
|
|
164
|
+
# Decode
|
|
165
|
+
img_bytes = base64.b64decode(image_b64)
|
|
166
|
+
img_array = np.frombuffer(img_bytes, dtype=np.uint8)
|
|
167
|
+
image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
|
|
168
|
+
|
|
169
|
+
return image
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def encode_image_base64(image: np.ndarray, format: str = 'jpg') -> str:
|
|
173
|
+
"""Encode numpy image to base64."""
|
|
174
|
+
_, buffer = cv2.imencode(f'.{format}', image)
|
|
175
|
+
img_b64 = base64.b64encode(buffer).decode('utf-8')
|
|
176
|
+
return f"data:image/{format};base64,{img_b64}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Flask example
|
|
180
|
+
def create_flask_app(model, tracker=None, class_names=None):
|
|
181
|
+
"""Create Flask app with Eye detection endpoint.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> from flask import Flask
|
|
185
|
+
>>> from ultralytics import YOLO
|
|
186
|
+
>>> import eye
|
|
187
|
+
>>>
|
|
188
|
+
>>> model = YOLO("yolo11n.pt")
|
|
189
|
+
>>> app = eye.web.create_flask_app(model, class_names=model.names)
|
|
190
|
+
>>> app.run(host='0.0.0.0', port=5000)
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
from flask import Flask, request, jsonify
|
|
194
|
+
from flask_cors import CORS
|
|
195
|
+
except ImportError:
|
|
196
|
+
raise ImportError("Flask not installed. Run: pip install flask flask-cors")
|
|
197
|
+
|
|
198
|
+
app = Flask(__name__)
|
|
199
|
+
CORS(app) # Enable CORS
|
|
200
|
+
|
|
201
|
+
api = WebAPI(model, tracker, class_names)
|
|
202
|
+
|
|
203
|
+
@app.route('/detect', methods=['POST'])
|
|
204
|
+
def detect():
|
|
205
|
+
"""Detection endpoint."""
|
|
206
|
+
data = request.json
|
|
207
|
+
image_b64 = data.get('image')
|
|
208
|
+
conf_threshold = data.get('confidence', 0.5)
|
|
209
|
+
frame_id = data.get('frame_id', 0)
|
|
210
|
+
|
|
211
|
+
result = api.process_base64(image_b64, conf_threshold, frame_id)
|
|
212
|
+
return jsonify(result)
|
|
213
|
+
|
|
214
|
+
@app.route('/health', methods=['GET'])
|
|
215
|
+
def health():
|
|
216
|
+
"""Health check endpoint."""
|
|
217
|
+
return jsonify({'status': 'healthy'})
|
|
218
|
+
|
|
219
|
+
return app
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# FastAPI example
|
|
223
|
+
def create_fastapi_app(model, tracker=None, class_names=None):
|
|
224
|
+
"""Create FastAPI app with Eye detection endpoint.
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
>>> from fastapi import FastAPI
|
|
228
|
+
>>> from ultralytics import YOLO
|
|
229
|
+
>>> import eye
|
|
230
|
+
>>>
|
|
231
|
+
>>> model = YOLO("yolo11n.pt")
|
|
232
|
+
>>> app = eye.web.create_fastapi_app(model, class_names=model.names)
|
|
233
|
+
>>> # Run with: uvicorn main:app --host 0.0.0.0 --port 8000
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
from fastapi import FastAPI
|
|
237
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
238
|
+
from pydantic import BaseModel
|
|
239
|
+
except ImportError:
|
|
240
|
+
raise ImportError("FastAPI not installed. Run: pip install fastapi uvicorn")
|
|
241
|
+
|
|
242
|
+
app = FastAPI(title="Eye Detection API")
|
|
243
|
+
|
|
244
|
+
# Enable CORS
|
|
245
|
+
app.add_middleware(
|
|
246
|
+
CORSMiddleware,
|
|
247
|
+
allow_origins=["*"],
|
|
248
|
+
allow_credentials=True,
|
|
249
|
+
allow_methods=["*"],
|
|
250
|
+
allow_headers=["*"],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
api = WebAPI(model, tracker, class_names)
|
|
254
|
+
|
|
255
|
+
class DetectionRequest(BaseModel):
|
|
256
|
+
image: str
|
|
257
|
+
confidence: float = 0.5
|
|
258
|
+
frame_id: int = 0
|
|
259
|
+
|
|
260
|
+
@app.post("/detect")
|
|
261
|
+
async def detect(request: DetectionRequest):
|
|
262
|
+
"""Detection endpoint."""
|
|
263
|
+
result = api.process_base64(request.image, request.confidence, request.frame_id)
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
@app.get("/health")
|
|
267
|
+
async def health():
|
|
268
|
+
"""Health check endpoint."""
|
|
269
|
+
return {"status": "healthy"}
|
|
270
|
+
|
|
271
|
+
return app
|