DEV Community

Mate Technologies
Mate Technologies

Posted on

Build a Screen Capture & Scopes Tool with Python, Tkinter, and MSS

In this tutorial, we'll create a small GUI tool that captures your screen in real-time and displays video scopes (vectorscope, histogram, and luma). We'll also allow selecting ROI, sampling colors, and recording video.

Step 1: Install Dependencies

We'll need a few libraries:

pip install tkinter ttkbootstrap numpy mss opencv-python pillow

Enter fullscreen mode Exit fullscreen mode

tkinter: GUI framework (built-in with Python)

ttkbootstrap: Stylish Tkinter widgets

numpy: Efficient numerical arrays

mss: Fast screen capture

opencv-python: Video recording and image processing

pillow: Image handling

Step 2: Create the Main Window

We start with a Tkinter window using ttkbootstrap for a modern look:

import ttkbootstrap as tb

APP_TITLE = "Scopes – Screen Capture"

app = tb.Window(title=APP_TITLE, themename="darkly", size=(1280, 720))
app.grid_columnconfigure(1, weight=1)
app.grid_rowconfigure(0, weight=1)
Enter fullscreen mode Exit fullscreen mode

grid_columnconfigure and grid_rowconfigure allow the canvas to expand with the window.

Step 3: Layout Frames for Controls and Viewer

We'll split the window into controls on the left and screen viewer on the right:

# Controls panel
controls = tb.Frame(app, padding=10)
controls.grid(row=0, column=0, sticky="ns")

# Viewer panel
viewer = tb.Frame(app)
viewer.grid(row=0, column=1, sticky="nsew")
viewer.grid_columnconfigure(0, weight=1)
viewer.grid_rowconfigure(0, weight=1)

# Canvas for drawing
import tkinter as tk
canvas = tk.Canvas(viewer, bg="black", highlightthickness=0)
canvas.grid(row=0, column=0, sticky="nsew")
Enter fullscreen mode Exit fullscreen mode

The canvas will show our scopes and histograms.

Step 4: Add Start/Stop and Record Buttons

We need buttons to start/stop capturing and recording:

running = False
recording = False

def toggle_capture():
    global running
    running = not running
    btn_start.config(text="Stop" if running else "Start")

btn_start = tb.Button(controls, text="Start", bootstyle="success", command=toggle_capture)
btn_start.pack(fill="x", pady=4)

def toggle_record():
    global recording
    recording = not recording
    btn_rec.config(text="Stop REC" if recording else "Record")

btn_rec = tb.Button(controls, text="Record", bootstyle="danger", command=toggle_record)
btn_rec.pack(fill="x", pady=4)
Enter fullscreen mode Exit fullscreen mode

toggle_capture flips the running state.

toggle_record flips the recording state.

Step 5: Add Sliders for Sampling and Gain

Sliders let users control sample steps and gain for the scopes:

tb.Label(controls, text="Sampling Step").pack(anchor="w")
sample_slider = tb.Scale(controls, from_=1, to=10, orient="horizontal")
sample_slider.set(4)
sample_slider.pack(fill="x")

tb.Label(controls, text="Gain").pack(anchor="w")
gain_slider = tb.Scale(controls, from_=1, to=10, orient="horizontal")
gain_slider.set(4)
gain_slider.pack(fill="x")
Enter fullscreen mode Exit fullscreen mode

Step 6: Convert RGB to YUV

Scopes usually work in YUV space:

import numpy as np

def rgb_to_yuv(rgb):
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    y = 0.299*r + 0.587*g + 0.114*b
    u = -0.147*r - 0.289*g + 0.436*b
    v = 0.615*r - 0.515*g - 0.100*b
    return y, u, v
Enter fullscreen mode Exit fullscreen mode

Step 7: Draw Scopes on the Canvas

We draw vectorscope, RGB histogram, and luma histogram:

def draw_scopes(frame):
    canvas.delete("all")
    h, w, _ = frame.shape
    ch, cw = canvas.winfo_height(), canvas.winfo_width()

    step = int(sample_slider.get())
    gain = gain_slider.get()
    small = frame[::step, ::step] / 255.0
    Y, U, V = rgb_to_yuv(small)

    # VECTORSCOPE
    cx, cy, radius = 200, ch//2, 160
    canvas.create_text(cx, 20, text="VECTORSCOPE", fill="#aaa")
    canvas.create_oval(cx-radius, cy-radius, cx+radius, cy+radius, outline="#444")
    xs = cx + U.flatten() * radius * gain
    ys = cy - V.flatten() * radius * gain
    for x, y in zip(xs, ys):
        canvas.create_line(x, y, x+1, y, fill="lime")

    # HISTOGRAM
    hist_x = 420
    hist_w = cw - hist_x - 20
    hist_h = 150
    hist_y = 60
    canvas.create_text(hist_x, 20, text="HISTOGRAM", fill="#aaa", anchor="w")
    for i, col in enumerate(("red", "green", "blue")):
        hist, _ = np.histogram(frame[..., i], bins=256, range=(0, 255))
        hist = hist / hist.max() if hist.max() > 0 else hist
        for x in range(256):
            y0 = hist_y + hist_h
            y1 = hist_y + hist_h - hist[x] * hist_h
            canvas.create_line(hist_x + x * hist_w / 256, y0,
                               hist_x + x * hist_w / 256, y1, fill=col)

    # LUMA
    canvas.create_text(hist_x, hist_y + hist_h + 30, text="LUMA", fill="#aaa", anchor="w")
    hist, _ = np.histogram((Y * 255).astype(np.uint8), bins=256, range=(0, 255))
    hist = hist / hist.max() if hist.max() > 0 else hist
    for x in range(256):
        y0 = hist_y + hist_h + 180
        y1 = y0 - hist[x] * hist_h
        canvas.create_line(hist_x + x * hist_w / 256, y0,
                           hist_x + x * hist_w / 256, y1, fill="white")
Enter fullscreen mode Exit fullscreen mode

Vectorscope: Shows color distribution.

RGB histogram: Shows intensity of each color channel.

Luma: Brightness histogram.

Step 8: Capture the Screen in a Thread

We need a background thread for continuous screen capture:

import threading, time, mss, cv2

latest_frame = None
video_writer = None
FPS = 30

def capture_thread():
    global latest_frame, video_writer
    with mss.mss() as sct:
        monitor = sct.monitors[1]
        while True:
            if running:
                img = np.array(sct.grab(monitor))[:, :, :3]
                latest_frame = img

                if recording:
                    h, w = img.shape[:2]
                    if video_writer is None:
                        video_writer = cv2.VideoWriter("recording.mp4",
                                                       cv2.VideoWriter_fourcc(*"mp4v"),
                                                       FPS, (w, h))
                    if video_writer.isOpened():
                        video_writer.write(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
            time.sleep(1 / FPS)
Enter fullscreen mode Exit fullscreen mode

threading.Thread(target=capture_thread, daemon=True).start()

Step 9: Update the UI Loop

Tkinter doesn't like heavy computation in the main thread, so we update the canvas periodically:

def update_ui():
    if running and latest_frame is not None:
        draw_scopes(latest_frame)
    app.after(33, update_ui)  # ~30 FPS

update_ui()
Enter fullscreen mode Exit fullscreen mode

Step 10: Add ROI and Color Sampling

roi = None
start_pt = None
color_indicators = []

def on_mouse_down(e):
    global start_pt
    start_pt = (e.x_root, e.y_root)

def on_mouse_up(e):
    global roi, start_pt
    if not start_pt: return
    x1, y1 = start_pt
    x2, y2 = e.x_root, e.y_root
    roi = (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
    start_pt = None

canvas.bind("<ButtonPress-1>", on_mouse_down)
canvas.bind("<ButtonRelease-1>", on_mouse_up)

def on_key(e):
    global roi
    if e.keysym == "Escape":
        app.destroy()
    if e.keysym == "space":
        import mss
        x, y = app.winfo_pointerxy()
        with mss.mss() as sct:
            img = sct.grab(sct.monitors[1])
            r, g, b = img.pixel(x, y)
            color_indicators.append((r/255, g/255, b/255))
    if e.keysym == "r":
        roi = None

app.bind("<Key>", on_key)
Enter fullscreen mode Exit fullscreen mode

Mouse drag: Define ROI.

SPACE: Sample color at cursor.

ESC: Quit.

R: Reset ROI.

Step 11: Run the Application

Finally, start the Tkinter event loop:

app.mainloop()
Enter fullscreen mode Exit fullscreen mode

✅ Done! You now have a fully working screen capture and scopes tool in Python.

You can start/stop capture, record video, and analyze colors.

Adjust sampling and gain to fine-tune scopes.

Scopes – Screen Capture

Top comments (0)