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.
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = "0.1.dev13+ga17e5ab6a.d20260610"
22
+ __version_tuple__ = version_tuple = (0, 1, "dev13", "ga17e5ab6a.d20260610")
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,296 @@
1
+ import uuid
2
+
3
+ import cv2
4
+ import numpy as np
5
+
6
+
7
+ class DrawingObject:
8
+ def __init__(
9
+ self,
10
+ kind="circle",
11
+ origin=None,
12
+ placement="top-left",
13
+ size=(100, 100),
14
+ color=(0, 255, 0),
15
+ margin=20,
16
+ thickness=-1,
17
+ label=None,
18
+ ):
19
+ self.id = str(uuid.uuid4())
20
+
21
+ self.kind = kind
22
+ self.origin = origin
23
+ self.placement = placement
24
+ self.size = size
25
+ self.color = color
26
+ self.margin = margin
27
+ self.thickness = thickness
28
+ self.label = label
29
+
30
+ self.initialized = False
31
+
32
+ self.position = None
33
+ self.initial_position = None
34
+ self.center_point = None
35
+ self.bounding_box = None
36
+
37
+ # interaction state (useful for gestures)
38
+ self.is_hovered = False
39
+ self.is_selected = False
40
+
41
+ # ----------------------------
42
+ # POSITION RESOLUTION
43
+ # ----------------------------
44
+ def resolve_position(self, frame_shape):
45
+ frame_h, frame_w = frame_shape[:2]
46
+ obj_w, obj_h = self.size
47
+
48
+ positions = {
49
+ "top-left": (self.margin, self.margin),
50
+ "top": ((frame_w - obj_w) // 2, self.margin),
51
+ "top-right": (frame_w - obj_w - self.margin, self.margin),
52
+ "left": (self.margin, (frame_h - obj_h) // 2),
53
+ "center": ((frame_w - obj_w) // 2, (frame_h - obj_h) // 2),
54
+ "right": (frame_w - obj_w - self.margin, (frame_h - obj_h) // 2),
55
+ "bottom-left": (self.margin, frame_h - obj_h - self.margin),
56
+ "bottom": ((frame_w - obj_w) // 2, frame_h - obj_h - self.margin),
57
+ "bottom-right": (
58
+ frame_w - obj_w - self.margin,
59
+ frame_h - obj_h - self.margin,
60
+ ),
61
+ }
62
+
63
+ return positions.get(self.placement, positions["top-left"])
64
+
65
+ # ----------------------------
66
+ # UPDATE INTERNAL REFERENCES
67
+ # ----------------------------
68
+ def update_reference_points(self):
69
+ if self.origin is None:
70
+ return
71
+
72
+ x, y = self.origin
73
+ w, h = self.size
74
+
75
+ self.position = {"x": x, "y": y}
76
+
77
+ self.center_point = {
78
+ "x": x + w // 2,
79
+ "y": y + h // 2,
80
+ }
81
+
82
+ self.bounding_box = {
83
+ "x1": x,
84
+ "y1": y,
85
+ "x2": x + w,
86
+ "y2": y + h,
87
+ }
88
+
89
+ # ----------------------------
90
+ # INITIALIZATION
91
+ # ----------------------------
92
+ def initialize_position(self, frame_shape):
93
+ if self.origin is None:
94
+ self.origin = self.resolve_position(frame_shape)
95
+
96
+ self.initial_position = {
97
+ "x": self.origin[0],
98
+ "y": self.origin[1],
99
+ }
100
+
101
+ self.update_reference_points()
102
+ self.initialized = True
103
+
104
+ # ----------------------------
105
+ # MOVE / RESET
106
+ # ----------------------------
107
+ def move_to(self, x, y):
108
+ self.origin = (int(x), int(y))
109
+ self.update_reference_points()
110
+
111
+ def reset_position(self):
112
+ if self.initial_position:
113
+ self.origin = (
114
+ self.initial_position["x"],
115
+ self.initial_position["y"],
116
+ )
117
+ self.update_reference_points()
118
+
119
+ # ----------------------------
120
+ # INTERACTION HELPERS
121
+ # ----------------------------
122
+ def contains_point(self, point):
123
+ if not self.bounding_box:
124
+ return False
125
+
126
+ px, py = point
127
+ return (
128
+ self.bounding_box["x1"] <= px <= self.bounding_box["x2"]
129
+ and self.bounding_box["y1"] <= py <= self.bounding_box["y2"]
130
+ )
131
+
132
+ def set_hover(self, state: bool):
133
+ self.is_hovered = state
134
+
135
+ def set_selected(self, state: bool):
136
+ self.is_selected = state
137
+
138
+ # ----------------------------
139
+ # GETTERS
140
+ # ----------------------------
141
+ def get_position(self):
142
+ return self.position
143
+
144
+ def get_center(self):
145
+ return self.center_point
146
+
147
+ def get_bounds(self):
148
+ return self.bounding_box
149
+
150
+ def get_size(self):
151
+ return self.size
152
+
153
+ def get_id(self):
154
+ return self.id
155
+
156
+ # ----------------------------
157
+ # SERIALIZATION
158
+ # ----------------------------
159
+ def to_dict(self):
160
+ return {
161
+ "id": self.id,
162
+ "kind": self.kind,
163
+ "origin": self.origin,
164
+ "placement": self.placement,
165
+ "size": self.size,
166
+ "color": self.color,
167
+ "label": self.label,
168
+ "initialized": self.initialized,
169
+ }
170
+
171
+ # optional reverse (useful for restoring state)
172
+ @staticmethod
173
+ def from_dict(data: dict):
174
+ obj = DrawingObject(
175
+ kind=data.get("kind", "circle"),
176
+ origin=data.get("origin"),
177
+ placement=data.get("placement", "top-left"),
178
+ size=tuple(data.get("size", (100, 100))),
179
+ color=tuple(data.get("color", (0, 255, 0))),
180
+ label=data.get("label"),
181
+ )
182
+ obj.initialized = data.get("initialized", False)
183
+ obj.update_reference_points()
184
+ return obj
185
+
186
+ # ----------------------------
187
+ # LAYOUT ENGINE
188
+ # ----------------------------
189
+ @staticmethod
190
+ def distribute_evenly(
191
+ drawings,
192
+ frame_shape,
193
+ row="top",
194
+ margin=20,
195
+ padding=10,
196
+ ):
197
+ frame_h, frame_w = frame_shape[:2]
198
+
199
+ count = len(drawings)
200
+ if count == 0:
201
+ return drawings # IMPORTANT FIX
202
+
203
+ total_object_width = sum(d.size[0] for d in drawings)
204
+
205
+ available_width = frame_w - (margin * 2) - total_object_width
206
+
207
+ gap = max(padding, available_width // max(count - 1, 1))
208
+
209
+ current_x = margin
210
+
211
+ for drawing in drawings:
212
+ obj_w, obj_h = drawing.size
213
+
214
+ if row == "top":
215
+ y = margin
216
+ elif row == "center":
217
+ y = (frame_h - obj_h) // 2
218
+ elif row == "bottom":
219
+ y = frame_h - obj_h - margin
220
+ else:
221
+ y = margin
222
+
223
+ drawing.origin = (int(current_x), int(y))
224
+ drawing.initial_position = {"x": int(current_x), "y": int(y)}
225
+ drawing.update_reference_points()
226
+ drawing.initialized = True
227
+
228
+ current_x += obj_w + gap
229
+
230
+ return drawings
231
+
232
+ # ----------------------------
233
+ # DRAWING
234
+ # ----------------------------
235
+ def draw(self, frame):
236
+ if not self.initialized or self.origin is None:
237
+ self.initialize_position(frame.shape)
238
+
239
+ x, y = self.origin
240
+ w, h = self.size
241
+
242
+ frame_h, frame_w = frame.shape[:2]
243
+
244
+ # clamp inside frame
245
+ x = max(0, min(int(x), frame_w - w))
246
+ y = max(0, min(int(y), frame_h - h))
247
+
248
+ self.origin = (x, y)
249
+ self.update_reference_points()
250
+
251
+ # optional highlight when selected/hovered
252
+ stroke = self.thickness
253
+ if self.is_selected:
254
+ stroke = 3
255
+
256
+ if self.kind == "circle":
257
+ center = (x + w // 2, y + h // 2)
258
+ radius = min(w, h) // 2
259
+
260
+ cv2.circle(frame, center, radius, self.color, stroke)
261
+
262
+ elif self.kind == "square":
263
+ side = min(w, h)
264
+ cv2.rectangle(frame, (x, y), (x + side, y + side), self.color, stroke)
265
+
266
+ elif self.kind == "rectangle":
267
+ cv2.rectangle(frame, (x, y), (x + w, y + h), self.color, stroke)
268
+
269
+ elif self.kind == "triangle":
270
+ points = np.array(
271
+ [
272
+ [x + w // 2, y],
273
+ [x, y + h],
274
+ [x + w, y + h],
275
+ ],
276
+ dtype=np.int32,
277
+ )
278
+
279
+ if stroke == -1:
280
+ cv2.fillPoly(frame, [points], self.color)
281
+ else:
282
+ cv2.polylines(frame, [points], True, self.color, stroke)
283
+
284
+ # label
285
+ if self.label:
286
+ cv2.putText(
287
+ frame,
288
+ self.label,
289
+ (x, y - 10),
290
+ cv2.FONT_HERSHEY_SIMPLEX,
291
+ 0.6,
292
+ self.color,
293
+ 2,
294
+ )
295
+
296
+ return frame
@@ -0,0 +1,61 @@
1
+ from collections.abc import Callable
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import pyautogui
6
+
7
+ window_centered = False # Used to center window only once
8
+
9
+
10
+ def image_template(
11
+ image_path: str,
12
+ custom_logic: Callable[[cv2.typing.MatLike], cv2.typing.MatLike] | None = None,
13
+ window_name: str = "Demo",
14
+ center_window: bool = True,
15
+ show_window: bool = True,
16
+ resolution: tuple[int, int] = (1280, 720),
17
+ ):
18
+ """
19
+ REUSABLE TEMPLATE for displaying an image with optional custom processing.
20
+
21
+ Parameters:
22
+ image_path (str): Path to the image file.
23
+ custom_logic (callable, optional): Function that receives the image and returns the modified image.
24
+ window_name (str): Name of the OpenCV window.
25
+ center_window (bool): If True, automatically centers the window on screen. Default = True
26
+ show_window (bool): If True, displays the image window. Default = True
27
+ resolution (tuple[int, int]): Desired image resolution (width, height). Default = (1280, 720)
28
+ """
29
+
30
+ image = cv2.imread(image_path)
31
+ print(image_path)
32
+ if custom_logic is not None:
33
+ image = custom_logic(image)
34
+
35
+ if image is None:
36
+ raise ValueError("Image is None. Check file path or loading logic.")
37
+
38
+ if not isinstance(image, np.ndarray):
39
+ raise TypeError(f"Invalid image type: {type(image)}")
40
+
41
+ if image.size == 0:
42
+ raise ValueError("Empty image array")
43
+
44
+ # Resize image
45
+ hWIDTH, hHEIGHT = resolution
46
+ resized_img = cv2.resize(image, (hWIDTH, hHEIGHT))
47
+
48
+ # ======================
49
+ # NEW: Auto-center window on screen (only once)
50
+ # ======================
51
+ if center_window:
52
+ screen_width, screen_height = pyautogui.size()
53
+ x = int((screen_width - hWIDTH) / 2)
54
+ y = int((screen_height - hHEIGHT) / 2)
55
+ cv2.moveWindow(window_name, x, y)
56
+ # ======================
57
+
58
+ if show_window:
59
+ cv2.imshow(window_name, resized_img)
60
+ cv2.waitKey(0)
61
+ cv2.destroyAllWindows()
@@ -0,0 +1,13 @@
1
+ import cv2
2
+ import mss
3
+ import numpy as np
4
+
5
+
6
+ class ScreenCapture:
7
+ def __init__(self, monitor_index=1):
8
+ self.sct = mss.mss()
9
+ self.monitor = self.sct.monitors[monitor_index]
10
+
11
+ def grab(self):
12
+ frame = np.array(self.sct.grab(self.monitor))
13
+ return cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)
@@ -0,0 +1,128 @@
1
+ import os
2
+ import time
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+
6
+ import cv2
7
+ import imageio
8
+
9
+
10
+ @dataclass
11
+ class VideoRecorder:
12
+ """
13
+ Advanced Video Recorder Engine:
14
+ - MP4 or GIF export
15
+ - Pause / Resume
16
+ - Timer tracking
17
+ - Multi-source ready (webcam + screen overlay)
18
+ - Plug & play with video_capture_template
19
+ """
20
+
21
+ output_path: str = "recordings"
22
+ fps: int = 20
23
+ codec: str = "mp4v"
24
+ output_format: str = "mp4" # "mp4" or "gif"
25
+
26
+ def __post_init__(self):
27
+ os.makedirs(self.output_path, exist_ok=True)
28
+
29
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
30
+
31
+ self.file_path = os.path.join(
32
+ self.output_path, f"record_{timestamp}.{self.output_format}"
33
+ )
34
+
35
+ # VideoWriter (MP4 mode)
36
+ self.writer = None
37
+
38
+ # GIF buffer mode
39
+ self.frames = []
40
+
41
+ self.is_recording = False
42
+ self.is_paused = False
43
+
44
+ self.start_time = None
45
+ self.elapsed_time = 0
46
+
47
+ # ----------------------------
48
+ # START RECORDING
49
+ # ----------------------------
50
+ def start(self, frame_shape=None):
51
+ self.is_recording = True
52
+ self.is_paused = False
53
+ self.start_time = time.time()
54
+
55
+ self.frames = []
56
+
57
+ if self.output_format == "mp4" and frame_shape is not None:
58
+ h, w = frame_shape[:2]
59
+ fourcc = cv2.VideoWriter_fourcc(*self.codec)
60
+ self.writer = cv2.VideoWriter(self.file_path, fourcc, self.fps, (w, h))
61
+
62
+ print(f"🔴 Recording started → {self.file_path}")
63
+
64
+ # ----------------------------
65
+ # WRITE FRAME
66
+ # ----------------------------
67
+ def write(self, frame):
68
+ if not self.is_recording or self.is_paused:
69
+ return
70
+
71
+ # -------------------------
72
+ # Convert BGR → RGB (IMPORTANT)
73
+ # -------------------------
74
+ rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
75
+
76
+ # MP4 mode
77
+ if self.output_format == "mp4" and self.writer:
78
+ self.writer.write(frame) # MP4 expects BGR (OpenCV standard)
79
+
80
+ # GIF mode
81
+ elif self.output_format == "gif":
82
+ self.frames.append(rgb_frame)
83
+
84
+ # ----------------------------
85
+ # TIMER
86
+ # ----------------------------
87
+ def get_elapsed_time(self):
88
+ if self.start_time:
89
+ return round(time.time() - self.start_time, 2)
90
+ return 0
91
+
92
+ # ----------------------------
93
+ # PAUSE / RESUME
94
+ # ----------------------------
95
+ def pause(self):
96
+ self.is_paused = True
97
+ print("⏸ Recording paused")
98
+
99
+ def resume(self):
100
+ self.is_paused = False
101
+ print("▶ Recording resumed")
102
+
103
+ # ----------------------------
104
+ # STOP RECORDING
105
+ # ----------------------------
106
+ def stop(self):
107
+ self.is_recording = False
108
+
109
+ if self.writer:
110
+ self.writer.release()
111
+ print(f"✅ MP4 saved → {self.file_path}")
112
+
113
+ if self.output_format == "gif":
114
+ if not self.frames or len(self.frames) == 0:
115
+ print("⚠️ No frames recorded. Skipping GIF export.")
116
+ return
117
+ safe_fps = self.fps if self.fps and self.fps > 0 else 10
118
+ imageio.mimsave(self.file_path, self.frames, fps=safe_fps)
119
+ print(f"✅ GIF saved → {self.file_path}")
120
+
121
+ # ----------------------------
122
+ # AUDIO SYNC (HOOK)
123
+ # ----------------------------
124
+ def attach_audio(self, audio_path: str):
125
+ """
126
+ Placeholder for ffmpeg-based audio sync.
127
+ """
128
+ print(f"🎙 Audio sync not implemented yet: {audio_path}")