Skip to content

Commit a2deba9

Browse files
authored
Create Scopes.py
1 parent 18cb3b0 commit a2deba9

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import tkinter as tk
2+
import ttkbootstrap as tb
3+
import numpy as np
4+
import threading
5+
import time
6+
import mss
7+
import cv2
8+
from PIL import Image, ImageTk
9+
10+
# ================= CONFIG =================
11+
APP_TITLE = "Scopes – Screen Capture"
12+
FPS = 30
13+
14+
# ================= GLOBAL STATE =================
15+
running = False
16+
latest_frame = None
17+
roi = None
18+
recording = False
19+
video_writer = None
20+
color_indicators = []
21+
22+
# ================= APP =================
23+
app = tb.Window(title=APP_TITLE, themename="darkly", size=(1280, 720))
24+
app.grid_columnconfigure(1, weight=1)
25+
app.grid_rowconfigure(0, weight=1)
26+
27+
# ================= UI =================
28+
controls = tb.Frame(app, padding=10)
29+
controls.grid(row=0, column=0, sticky="ns")
30+
31+
viewer = tb.Frame(app)
32+
viewer.grid(row=0, column=1, sticky="nsew")
33+
viewer.grid_columnconfigure(0, weight=1)
34+
viewer.grid_rowconfigure(0, weight=1)
35+
36+
canvas = tk.Canvas(viewer, bg="black", highlightthickness=0)
37+
canvas.grid(row=0, column=0, sticky="nsew")
38+
39+
# ================= CONTROLS =================
40+
tb.Label(controls, text="Capture", font=("Segoe UI", 11, "bold")).pack(anchor="w")
41+
42+
def toggle_capture():
43+
global running
44+
running = not running
45+
btn_start.config(text="Stop" if running else "Start")
46+
47+
btn_start = tb.Button(controls, text="Start", bootstyle="success", command=toggle_capture)
48+
btn_start.pack(fill="x", pady=4)
49+
50+
def toggle_record():
51+
global recording, video_writer
52+
recording = not recording
53+
btn_rec.config(text="Stop REC" if recording else "Record")
54+
55+
if not recording and video_writer:
56+
video_writer.release()
57+
video_writer = None
58+
59+
btn_rec = tb.Button(controls, text="Record", bootstyle="danger", command=toggle_record)
60+
btn_rec.pack(fill="x", pady=4)
61+
62+
tb.Label(controls, text="Sampling Step").pack(anchor="w", pady=(10, 0))
63+
sample_slider = tb.Scale(controls, from_=1, to=10, orient="horizontal")
64+
sample_slider.set(4)
65+
sample_slider.pack(fill="x")
66+
67+
tb.Label(controls, text="Gain").pack(anchor="w", pady=(10, 0))
68+
gain_slider = tb.Scale(controls, from_=1, to=10, orient="horizontal")
69+
gain_slider.set(4)
70+
gain_slider.pack(fill="x")
71+
72+
tb.Separator(controls).pack(fill="x", pady=10)
73+
74+
tb.Label(
75+
controls,
76+
text="Mouse drag = ROI\nSPACE = sample color\nESC = quit",
77+
justify="left"
78+
).pack(anchor="w")
79+
80+
# ================= COLOR =================
81+
def rgb_to_yuv(rgb):
82+
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
83+
y = 0.299*r + 0.587*g + 0.114*b
84+
u = -0.147*r - 0.289*g + 0.436*b
85+
v = 0.615*r - 0.515*g - 0.100*b
86+
return y, u, v
87+
88+
# ================= DRAW =================
89+
def draw_scopes(frame):
90+
canvas.delete("all")
91+
92+
h, w, _ = frame.shape
93+
ch, cw = canvas.winfo_height(), canvas.winfo_width()
94+
if ch < 50 or cw < 50:
95+
return
96+
97+
step = int(sample_slider.get())
98+
gain = gain_slider.get()
99+
100+
small = frame[::step, ::step] / 255.0
101+
Y, U, V = rgb_to_yuv(small)
102+
103+
# ---- VECTORSCOPE ----
104+
cx, cy, radius = 200, ch//2, 160
105+
canvas.create_text(cx, 20, text="VECTORSCOPE", fill="#aaa")
106+
canvas.create_oval(cx-radius, cy-radius, cx+radius, cy+radius, outline="#444")
107+
108+
xs = cx + U.flatten() * radius * gain
109+
ys = cy - V.flatten() * radius * gain
110+
111+
for x, y in zip(xs, ys):
112+
canvas.create_line(x, y, x+1, y, fill="lime")
113+
114+
for r, g, b in color_indicators:
115+
_, u, v = rgb_to_yuv(np.array([[r, g, b]]))
116+
ix = cx + u[0] * radius * gain
117+
iy = cy - v[0] * radius * gain
118+
canvas.create_oval(ix-6, iy-6, ix+6, iy+6, outline="yellow", width=2)
119+
120+
# ---- HISTOGRAM ----
121+
hist_x = 420
122+
hist_w = cw - hist_x - 20
123+
hist_h = 150
124+
hist_y = 60
125+
126+
canvas.create_text(hist_x, 20, text="HISTOGRAM", fill="#aaa", anchor="w")
127+
128+
for i, col in enumerate(("red", "green", "blue")):
129+
hist, _ = np.histogram(frame[..., i], bins=256, range=(0, 255))
130+
hist = hist / hist.max() if hist.max() > 0 else hist
131+
for x in range(256):
132+
y0 = hist_y + hist_h
133+
y1 = hist_y + hist_h - hist[x] * hist_h
134+
canvas.create_line(
135+
hist_x + x * hist_w / 256, y0,
136+
hist_x + x * hist_w / 256, y1,
137+
fill=col
138+
)
139+
140+
# ---- LUMA ----
141+
canvas.create_text(hist_x, hist_y + hist_h + 30, text="LUMA", fill="#aaa", anchor="w")
142+
hist, _ = np.histogram((Y * 255).astype(np.uint8), bins=256, range=(0, 255))
143+
hist = hist / hist.max() if hist.max() > 0 else hist
144+
145+
for x in range(256):
146+
y0 = hist_y + hist_h + 180
147+
y1 = y0 - hist[x] * hist_h
148+
canvas.create_line(
149+
hist_x + x * hist_w / 256, y0,
150+
hist_x + x * hist_w / 256, y1,
151+
fill="white"
152+
)
153+
154+
# ================= CAPTURE THREAD =================
155+
def capture_thread():
156+
global latest_frame, video_writer
157+
with mss.mss() as sct:
158+
monitor = sct.monitors[1]
159+
while True:
160+
if running:
161+
img = np.array(sct.grab(monitor))[:, :, :3]
162+
163+
# Apply ROI safely
164+
if roi:
165+
x1, y1, x2, y2 = roi
166+
# Convert to relative coordinates
167+
x1_rel = max(0, min(x1 - monitor["left"], img.shape[1]-1))
168+
y1_rel = max(0, min(y1 - monitor["top"], img.shape[0]-1))
169+
x2_rel = max(0, min(x2 - monitor["left"], img.shape[1]))
170+
y2_rel = max(0, min(y2 - monitor["top"], img.shape[0]))
171+
172+
if x2_rel > x1_rel and y2_rel > y1_rel:
173+
img = img[y1_rel:y2_rel, x1_rel:x2_rel]
174+
else:
175+
img = None # Invalid ROI
176+
177+
if img is not None:
178+
latest_frame = img
179+
180+
if recording:
181+
h, w = img.shape[:2]
182+
if h > 0 and w > 0: # Make sure dimensions are valid
183+
if video_writer is None:
184+
video_writer = cv2.VideoWriter(
185+
"recording.mp4",
186+
cv2.VideoWriter_fourcc(*"mp4v"),
187+
FPS,
188+
(w, h)
189+
)
190+
if video_writer.isOpened():
191+
video_writer.write(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
192+
193+
time.sleep(1 / FPS)
194+
195+
threading.Thread(target=capture_thread, daemon=True).start()
196+
197+
# ================= UI LOOP =================
198+
def update_ui():
199+
if running and latest_frame is not None:
200+
draw_scopes(latest_frame)
201+
app.after(33, update_ui)
202+
203+
update_ui()
204+
205+
# ================= ROI =================
206+
start_pt = None
207+
208+
def on_mouse_down(e):
209+
global start_pt
210+
start_pt = (e.x_root, e.y_root)
211+
212+
def on_mouse_up(e):
213+
global roi, start_pt
214+
if not start_pt:
215+
return
216+
x1, y1 = start_pt
217+
x2, y2 = e.x_root, e.y_root
218+
roi = (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
219+
start_pt = None
220+
221+
canvas.bind("<ButtonPress-1>", on_mouse_down)
222+
canvas.bind("<ButtonRelease-1>", on_mouse_up)
223+
224+
# ================= INPUT =================
225+
def on_key(e):
226+
global roi
227+
if e.keysym == "Escape":
228+
app.destroy()
229+
if e.keysym == "space":
230+
x, y = app.winfo_pointerxy()
231+
with mss.mss() as sct:
232+
img = sct.grab(sct.monitors[1])
233+
r, g, b = img.pixel(x, y)
234+
color_indicators.append((r/255, g/255, b/255))
235+
if e.keysym == "r":
236+
roi = None
237+
238+
app.bind("<Key>", on_key)
239+
240+
# ================= RUN =================
241+
app.mainloop()

0 commit comments

Comments
 (0)