openvisionkit 0.4.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.
- openvisionkit/__init__.py +1 -0
- openvisionkit/_version.py +24 -0
- openvisionkit/capture/draw_object.py +296 -0
- openvisionkit/capture/image_template.py +61 -0
- openvisionkit/capture/screen_capture.py +13 -0
- openvisionkit/capture/video_recorder.py +128 -0
- openvisionkit/capture/video_template.py +336 -0
- openvisionkit/lib/classifier.py +186 -0
- openvisionkit/lib/face_detector.py +587 -0
- openvisionkit/lib/face_mesh_detector.py +913 -0
- openvisionkit/lib/form_detector.py +465 -0
- openvisionkit/lib/form_roi_annotator.py +679 -0
- openvisionkit/lib/form_roi_detector.py +1078 -0
- openvisionkit/lib/fps_counter.py +38 -0
- openvisionkit/lib/hair_segmentation.py +298 -0
- openvisionkit/lib/hand_detector.py +1230 -0
- openvisionkit/lib/image_detector.py +1095 -0
- openvisionkit/lib/object_detector.py +401 -0
- openvisionkit/lib/pose_detector.py +919 -0
- openvisionkit/lib/selfie_segmentation.py +528 -0
- openvisionkit/lib/text_detector.py +1229 -0
- openvisionkit/utility/live_plot.py +141 -0
- openvisionkit/utility/vision_utilis.py +871 -0
- openvisionkit-0.4.0.dist-info/METADATA +1018 -0
- openvisionkit-0.4.0.dist-info/RECORD +26 -0
- openvisionkit-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FPSCounter:
|
|
7
|
+
"""
|
|
8
|
+
A simple FPS (Frames Per Second) counter for video processing.
|
|
9
|
+
This class calculates and displays the FPS of a video feed. It uses the time difference between frames to compute the FPS and overlays it on the video frame.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
fps_counter = FPSCounter()
|
|
13
|
+
while True:
|
|
14
|
+
ret, frame = video_capture.read()
|
|
15
|
+
if not ret:
|
|
16
|
+
break
|
|
17
|
+
frame, fps = fps_counter.update(frame)
|
|
18
|
+
cv2.imshow('Video Feed', frame)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.prev_time = 0
|
|
23
|
+
|
|
24
|
+
def update(self, frame):
|
|
25
|
+
curr_time = time.time()
|
|
26
|
+
fps = 1 / (curr_time - self.prev_time) if self.prev_time else 0
|
|
27
|
+
self.prev_time = curr_time
|
|
28
|
+
|
|
29
|
+
cv2.putText(
|
|
30
|
+
frame,
|
|
31
|
+
f"FPS: {int(fps)}",
|
|
32
|
+
(10, 30),
|
|
33
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
34
|
+
0.8,
|
|
35
|
+
(0, 255, 0),
|
|
36
|
+
2,
|
|
37
|
+
)
|
|
38
|
+
return frame, fps
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import mediapipe as mp
|
|
5
|
+
import numpy as np
|
|
6
|
+
from mediapipe.tasks.python import vision
|
|
7
|
+
from mediapipe.tasks.python.core.base_options import BaseOptions
|
|
8
|
+
from mediapipe.tasks.python.vision.core.vision_task_running_mode import (
|
|
9
|
+
VisionTaskRunningMode,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_MODEL_DIR = Path(__file__).parent / "models"
|
|
13
|
+
_DEFAULT_MODEL = str(_MODEL_DIR / "hair_segmenter.tflite")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HairSegmentation:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
model_path: str = _DEFAULT_MODEL,
|
|
20
|
+
output_category_mask: bool = True,
|
|
21
|
+
output_confidence_masks: bool = False,
|
|
22
|
+
running_mode: VisionTaskRunningMode = VisionTaskRunningMode.IMAGE,
|
|
23
|
+
):
|
|
24
|
+
base_options = BaseOptions(model_asset_path=model_path)
|
|
25
|
+
|
|
26
|
+
self.options = mp.tasks.vision.ImageSegmenterOptions(
|
|
27
|
+
base_options=base_options,
|
|
28
|
+
output_category_mask=output_category_mask,
|
|
29
|
+
output_confidence_masks=output_confidence_masks,
|
|
30
|
+
running_mode=running_mode,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self.segmentor = vision.ImageSegmenter.create_from_options(self.options)
|
|
34
|
+
|
|
35
|
+
def process(self, image: np.ndarray):
|
|
36
|
+
"""Segment hair in an RGB image and return the raw MediaPipe result.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
image: RGB numpy array (NOT BGR — convert with cv2.cvtColor first).
|
|
40
|
+
Returns:
|
|
41
|
+
MediaPipe ImageSegmenterResult with category_mask.
|
|
42
|
+
"""
|
|
43
|
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image)
|
|
44
|
+
return self.segmentor.segment(mp_image)
|
|
45
|
+
|
|
46
|
+
# ─────────────────────────── NEW METHODS ───────────────────────────
|
|
47
|
+
|
|
48
|
+
def get_hair_mask(self, bgr_image: np.ndarray) -> np.ndarray:
|
|
49
|
+
"""Return a binary uint8 mask where 255 = hair pixels.
|
|
50
|
+
Accepts BGR input (standard OpenCV format) and converts internally.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
bgr_image: BGR numpy array.
|
|
54
|
+
Returns:
|
|
55
|
+
Binary mask numpy array, shape (H, W), dtype uint8.
|
|
56
|
+
"""
|
|
57
|
+
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
|
58
|
+
result = self.process(rgb)
|
|
59
|
+
mask = np.squeeze(result.category_mask.numpy_view())
|
|
60
|
+
return (mask > 0.5).astype(np.uint8) * 255
|
|
61
|
+
|
|
62
|
+
def recolor_hair(self, bgr_image: np.ndarray, color=(180, 0, 255)) -> np.ndarray:
|
|
63
|
+
"""Replace hair pixels with a solid color.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
bgr_image: BGR numpy array.
|
|
67
|
+
color: BGR color tuple to paint over hair.
|
|
68
|
+
Returns:
|
|
69
|
+
BGR numpy array with hair region recolored.
|
|
70
|
+
"""
|
|
71
|
+
mask = self.get_hair_mask(bgr_image)
|
|
72
|
+
out = bgr_image.copy()
|
|
73
|
+
out[mask > 0] = color
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
def blend_hair_color(
|
|
77
|
+
self, bgr_image: np.ndarray, color=(180, 0, 255), alpha=0.5
|
|
78
|
+
) -> np.ndarray:
|
|
79
|
+
"""Alpha-blend a color over the hair region — preserves texture unlike recolor_hair.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
bgr_image: BGR numpy array.
|
|
83
|
+
color: BGR color tuple for the hair overlay.
|
|
84
|
+
alpha: Blend strength (0 = no change, 1 = solid color).
|
|
85
|
+
Returns:
|
|
86
|
+
BGR numpy array with hair color blended.
|
|
87
|
+
"""
|
|
88
|
+
mask = self.get_hair_mask(bgr_image)
|
|
89
|
+
overlay = bgr_image.copy()
|
|
90
|
+
overlay[mask > 0] = color
|
|
91
|
+
hair_region = mask > 0
|
|
92
|
+
out = bgr_image.copy().astype(np.float32)
|
|
93
|
+
out[hair_region] = (1 - alpha) * bgr_image[hair_region].astype(
|
|
94
|
+
np.float32
|
|
95
|
+
) + alpha * np.array(color, dtype=np.float32)
|
|
96
|
+
return out.astype(np.uint8)
|
|
97
|
+
|
|
98
|
+
def get_hair_ratio(self, bgr_image: np.ndarray) -> float:
|
|
99
|
+
"""Return the fraction of image pixels classified as hair.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
bgr_image: BGR numpy array.
|
|
103
|
+
Returns:
|
|
104
|
+
float: 0.0–1.0 (e.g. 0.15 means 15 % of the frame is hair).
|
|
105
|
+
"""
|
|
106
|
+
mask = self.get_hair_mask(bgr_image)
|
|
107
|
+
return float(np.sum(mask > 0)) / float(mask.size)
|
|
108
|
+
|
|
109
|
+
def visualize(
|
|
110
|
+
self, bgr_image: np.ndarray, color=(180, 0, 255), alpha=0.5
|
|
111
|
+
) -> np.ndarray:
|
|
112
|
+
"""Convenience wrapper: blend hair color and return the annotated frame.
|
|
113
|
+
Equivalent to blend_hair_color() with default settings.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
bgr_image: BGR numpy array.
|
|
117
|
+
color: BGR highlight color for hair.
|
|
118
|
+
alpha: Overlay transparency (0–1).
|
|
119
|
+
Returns:
|
|
120
|
+
Annotated BGR numpy array.
|
|
121
|
+
"""
|
|
122
|
+
return self.blend_hair_color(bgr_image, color=color, alpha=alpha)
|
|
123
|
+
|
|
124
|
+
def draw_hair_contours(
|
|
125
|
+
self, bgr_image: np.ndarray, color=(0, 255, 255), thickness=2
|
|
126
|
+
) -> np.ndarray:
|
|
127
|
+
"""Draw the outline of the detected hair region on the image.
|
|
128
|
+
Useful for debugging segmentation quality.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
bgr_image: BGR numpy array.
|
|
132
|
+
color: BGR contour color.
|
|
133
|
+
thickness: Contour line thickness in pixels.
|
|
134
|
+
Returns:
|
|
135
|
+
Annotated BGR numpy array.
|
|
136
|
+
"""
|
|
137
|
+
mask = self.get_hair_mask(bgr_image)
|
|
138
|
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
139
|
+
out = bgr_image.copy()
|
|
140
|
+
cv2.drawContours(out, contours, -1, color, thickness)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
def get_hair_bounding_box(self, bgr_image: np.ndarray):
|
|
144
|
+
"""Return the bounding rect of the hair region as (x, y, w, h).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
bgr_image: BGR numpy array.
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple (x, y, w, h) in pixels; (0, 0, 0, 0) if no hair detected.
|
|
150
|
+
"""
|
|
151
|
+
mask = self.get_hair_mask(bgr_image)
|
|
152
|
+
pts = cv2.findNonZero(mask)
|
|
153
|
+
if pts is None:
|
|
154
|
+
return (0, 0, 0, 0)
|
|
155
|
+
return cv2.boundingRect(pts)
|
|
156
|
+
|
|
157
|
+
def get_hair_top_position(self, bgr_image: np.ndarray) -> int:
|
|
158
|
+
"""Return the y-coordinate of the topmost hair pixel.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
bgr_image: BGR numpy array.
|
|
162
|
+
Returns:
|
|
163
|
+
int: Row index of the first hair pixel from the top; 0 if no hair.
|
|
164
|
+
"""
|
|
165
|
+
mask = self.get_hair_mask(bgr_image)
|
|
166
|
+
rows = np.any(mask > 0, axis=1)
|
|
167
|
+
if not rows.any():
|
|
168
|
+
return 0
|
|
169
|
+
return int(np.argmax(rows))
|
|
170
|
+
|
|
171
|
+
def detect_hair_length_estimate(self, bgr_image: np.ndarray) -> str:
|
|
172
|
+
"""Estimate hair length category based on vertical extent of hair mask.
|
|
173
|
+
|
|
174
|
+
Thresholds (fraction of image height):
|
|
175
|
+
< 15% → "short"
|
|
176
|
+
< 35% → "medium"
|
|
177
|
+
>= 35% → "long"
|
|
178
|
+
no hair → "none"
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
bgr_image: BGR numpy array.
|
|
182
|
+
Returns:
|
|
183
|
+
str: One of "none", "short", "medium", "long".
|
|
184
|
+
"""
|
|
185
|
+
x, y, w, h = self.get_hair_bounding_box(bgr_image)
|
|
186
|
+
if h == 0:
|
|
187
|
+
return "none"
|
|
188
|
+
ratio = h / bgr_image.shape[0]
|
|
189
|
+
if ratio < 0.15:
|
|
190
|
+
return "short"
|
|
191
|
+
if ratio < 0.35:
|
|
192
|
+
return "medium"
|
|
193
|
+
return "long"
|
|
194
|
+
|
|
195
|
+
def get_hair_density_map(self, bgr_image: np.ndarray) -> np.ndarray:
|
|
196
|
+
"""Return a density heatmap of hair coverage as a uint8 image.
|
|
197
|
+
|
|
198
|
+
Applies a Gaussian blur to the binary hair mask to produce a smooth
|
|
199
|
+
density map where brighter pixels indicate denser hair regions.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
bgr_image: BGR numpy array.
|
|
203
|
+
Returns:
|
|
204
|
+
numpy array of shape (H, W), dtype uint8, values 0–255.
|
|
205
|
+
"""
|
|
206
|
+
mask = self.get_hair_mask(bgr_image)
|
|
207
|
+
density = cv2.GaussianBlur(mask.astype(np.float32), (31, 31), 0)
|
|
208
|
+
max_val = density.max()
|
|
209
|
+
if max_val > 0:
|
|
210
|
+
density = density / max_val * 255
|
|
211
|
+
return density.astype(np.uint8)
|
|
212
|
+
|
|
213
|
+
def apply_gradient_color(self, bgr_image: np.ndarray, color1, color2) -> np.ndarray:
|
|
214
|
+
"""Paint a vertical gradient over the hair region.
|
|
215
|
+
|
|
216
|
+
color1 is applied at the top of the bounding box and color2 at the
|
|
217
|
+
bottom; pixels are linearly interpolated between the two colors.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
bgr_image: BGR numpy array.
|
|
221
|
+
color1: BGR tuple for the top of the gradient.
|
|
222
|
+
color2: BGR tuple for the bottom of the gradient.
|
|
223
|
+
Returns:
|
|
224
|
+
BGR numpy array with gradient applied to hair pixels.
|
|
225
|
+
"""
|
|
226
|
+
mask = self.get_hair_mask(bgr_image)
|
|
227
|
+
x, y, w, h = self.get_hair_bounding_box(bgr_image)
|
|
228
|
+
if h == 0:
|
|
229
|
+
return bgr_image.copy()
|
|
230
|
+
out = bgr_image.copy()
|
|
231
|
+
for row in range(y, min(y + h, bgr_image.shape[0])):
|
|
232
|
+
t = (row - y) / h
|
|
233
|
+
c = tuple(int(color1[i] * (1 - t) + color2[i] * t) for i in range(3))
|
|
234
|
+
row_mask = mask[row] > 0
|
|
235
|
+
out[row, row_mask] = c
|
|
236
|
+
return out
|
|
237
|
+
|
|
238
|
+
def apply_highlights(
|
|
239
|
+
self, bgr_image: np.ndarray, highlight_color=(255, 255, 200), intensity=0.4
|
|
240
|
+
) -> np.ndarray:
|
|
241
|
+
"""Simulate hair highlights by brightening a random subset of hair pixels.
|
|
242
|
+
|
|
243
|
+
Selects ~20% of hair pixels, dilates them into streak shapes, then
|
|
244
|
+
alpha-blends the highlight color over those pixels.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
bgr_image: BGR numpy array.
|
|
248
|
+
highlight_color: BGR color tuple for the highlight tint.
|
|
249
|
+
intensity: Blend strength (0 = no effect, 1 = full highlight color).
|
|
250
|
+
Returns:
|
|
251
|
+
BGR numpy array with simulated highlights applied.
|
|
252
|
+
"""
|
|
253
|
+
mask = self.get_hair_mask(bgr_image)
|
|
254
|
+
out = bgr_image.copy()
|
|
255
|
+
hair_pixels = np.argwhere(mask > 0)
|
|
256
|
+
if len(hair_pixels) == 0:
|
|
257
|
+
return out
|
|
258
|
+
rng = np.random.default_rng(0)
|
|
259
|
+
n = max(1, len(hair_pixels) // 5)
|
|
260
|
+
indices = rng.choice(len(hair_pixels), size=n, replace=False)
|
|
261
|
+
sparse = np.zeros_like(mask)
|
|
262
|
+
for idx in indices:
|
|
263
|
+
r, c = hair_pixels[idx]
|
|
264
|
+
sparse[r, c] = 255
|
|
265
|
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
|
266
|
+
sparse = cv2.dilate(sparse, kernel, iterations=2)
|
|
267
|
+
hl = np.array(highlight_color, dtype=np.uint8)
|
|
268
|
+
blend_mask = (sparse > 0) & (mask > 0)
|
|
269
|
+
out[blend_mask] = (out[blend_mask] * (1 - intensity) + hl * intensity).astype(
|
|
270
|
+
np.uint8
|
|
271
|
+
)
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
def _get_mask(self, result, smooth=True):
|
|
275
|
+
mask = result.category_mask.numpy_view()
|
|
276
|
+
mask = (mask * 255).astype(np.uint8)
|
|
277
|
+
return mask
|
|
278
|
+
|
|
279
|
+
def detect(self, image: np.ndarray, smooth=True, mask_color=(255, 0, 255)):
|
|
280
|
+
result = self.process(image)
|
|
281
|
+
mask = self._get_mask(result, smooth)
|
|
282
|
+
if smooth:
|
|
283
|
+
_, mask = cv2.threshold(
|
|
284
|
+
mask,
|
|
285
|
+
1,
|
|
286
|
+
255,
|
|
287
|
+
cv2.THRESH_BINARY,
|
|
288
|
+
)
|
|
289
|
+
overlay = image.copy()
|
|
290
|
+
overlay[mask > 0] = mask_color
|
|
291
|
+
result_frame = cv2.addWeighted(
|
|
292
|
+
overlay,
|
|
293
|
+
0.4,
|
|
294
|
+
image,
|
|
295
|
+
0.6,
|
|
296
|
+
0,
|
|
297
|
+
)
|
|
298
|
+
return result_frame
|