---
title: "How to add a HUD scan overlay with Claude (tested prompt and script)"
description: "Lime-green corner brackets close in from the frame edges and lock around the bottle, a glowing line sweeps down it while \"SCANNING\" counts to 100%, then \"TARGET LOCKED\" holds as the brackets follow the bottle. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/hud-scan-overlay
---

# How to add a HUD scan overlay with Claude

Lime-green corner brackets close in from the frame edges and lock around the bottle, a glowing line sweeps down it while "SCANNING" counts to 100%, then "TARGET LOCKED" holds as the brackets follow the bottle.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add a HUD scan overlay to input.mp4
   Second the brackets start closing in from the frame edges: 0.3
   Seconds the brackets take to lock onto the product: 0.6
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add a hud scan overlay to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting | Default          | What it does                                                |
| ------- | ---------------- | ----------------------------------------------------------- |
| `START` | `0.3`            | Second the brackets start closing in from the frame edges.  |
| `LOCK`  | `0.6`            | Seconds the brackets take to lock onto the product.         |
| `SCAN`  | `1.0`            | Seconds the scan line takes to sweep down the product.      |
| `COLOR` | `(150, 255, 40)` | Line and text color.                                        |
| `PAD`   | `16`             | Gap in pixels between the product and the brackets.         |
| `ARM`   | `0.22`           | Bracket arm length as a share of the product box's width.   |
| `EVERY` | `10`             | Find the product on every Nth frame and glide between them. |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add a HUD scan overlay to input.mp4 that locks onto the product, all lines and text in lime green (150, 255, 40) with a soft 3-pixel dark green (0, 25, 0) shadow behind them. Before 0.3 seconds the video is untouched. Find the product on every 10th frame with rembg (session "u2net", only_mask, mask above 128, largest connected blob from OpenCV), pad its bounding box by 16 pixels on each side and interpolate the box linearly for the frames in between, so the overlay follows the product. From 0.3s, over 0.6 seconds with a cubic ease-out, four L-shaped corner brackets (4 pixels thick, arms 22% of the box width) close in from a box 24 pixels inside the frame edges to the product's box. From 0.9s to 1.9s a 3-pixel horizontal scan line moves at a steady speed from the top of the box to the bottom, 8 pixels in from its sides, with a lime glow fading out over 70 pixels above it (alpha 130 at the line, falling off with the square of the distance); the line is gone after 1.9s. From 0.9s, in a monospaced font 4% of the frame width tall on dark (5, 25, 5) tags at 75% opacity: above the top-left bracket (bottom edge 16 pixels above the box) "SCANNING 0%" counting up to 100% with the sweep, replaced by "TARGET LOCKED" at 1.9s; and 16 pixels below the box, left-aligned with it, the box center as "X 0.512 Y 0.509" (fractions of the frame) over "SIZE 208x588" (box size in pixels), updating every frame. Draw it with Python and Pillow (don't use ffmpeg's drawtext filter), composite it with ffmpeg, keep the audio, and save it as output.mp4 at 30fps.
```

## Run it without Claude

This is the exact script we tested. Save it as `hud-scan-overlay.py` and run:

```
python3 hud-scan-overlay.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""HUD scan overlay: corner brackets close in from the frame edges, lock onto the product and a scan line sweeps down it.

Usage: python3 hud-scan-overlay.py input.mp4 output.mp4
Needs ffmpeg, Pillow, numpy, OpenCV and rembg to find the product
(pip install pillow numpy opencv-python-headless "rembg[cpu]"; rembg downloads its u2net model on first use).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
from rembg import new_session, remove

START = 0.3                 # second the brackets start closing in from the frame edges
LOCK = 0.6                  # seconds the brackets take to close onto the product
SCAN = 1.0                  # seconds the scan line takes to sweep from the top of the product to the bottom
COLOR = (150, 255, 40)      # line and text color
PAD = 16                    # gap between the product and the brackets, in pixels
ARM = 0.22                  # bracket arm length as a share of the product box's width
EVERY = 10                  # find the product on every 10th frame and glide between them
FONTS = ["/System/Library/Fonts/Menlo.ttc",                         # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",     # Linux
         "C:/Windows/Fonts/consola.ttf"]                            # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No monospaced font found: add one to FONTS")

def track(src):
    """The product's box (x0, y0, x1, y1) on every frame: rembg's cut-out on every EVERY-th frame, interpolated."""
    session = new_session("u2net")
    cap = cv2.VideoCapture(src)
    keys, i = {}, 0
    while True:
        ok, bgr = cap.read()
        if not ok:
            break
        if i % EVERY == 0:
            mask = np.array(remove(Image.fromarray(bgr[:, :, ::-1]), session=session, only_mask=True)) > 128
            n, _, stats, _ = cv2.connectedComponentsWithStats(mask.astype(np.uint8))
            x, y, bw, bh, _ = stats[1 + np.argmax(stats[1:, 4])] if n > 1 else (0, 0, w, h, 0)
            keys[i] = (x - PAD, y - PAD, x + bw + PAD, y + bh + PAD)
        i += 1
    idx = sorted(keys)
    return [tuple(np.interp(f, idx, [keys[k][c] for k in idx]) for c in range(4)) for f in range(i)]

def brackets(draw, box):
    x0, y0, x1, y1 = box
    arm = (x1 - x0) * ARM
    for x, y, sx, sy in [(x0, y0, 1, 1), (x1, y0, -1, 1), (x0, y1, 1, -1), (x1, y1, -1, -1)]:
        draw.line([(x, y + sy * arm), (x, y), (x + sx * arm, y)], fill=COLOR, width=4, joint="curve")

def scan_glow(box, p):
    """A fading glow trailing above the scan line, p (0 -> 1) of the way down the box."""
    x0, y0, x1, y1 = box
    y = y0 + (y1 - y0) * p
    glow = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(glow)
    for k in range(70):
        draw.line([(x0 + 8, y - k), (x1 - 8, y - k)], fill=COLOR + (round(130 * (1 - k / 70) ** 2),))
    return glow

def tag(draw, xy, text, anchor):
    """Text on a dark see-through tag so it reads over a bright background."""
    box = draw.multiline_textbbox(xy, text, font=font, anchor=anchor, spacing=5)
    draw.rectangle([box[0] - 6, box[1] - 5, box[2] + 6, box[3] + 5], fill=(5, 25, 5, 190))
    draw.multiline_text(xy, text, font=font, fill=COLOR, anchor=anchor, spacing=5)

def readouts(draw, box, p):
    """Status above the box (a scan percentage, then TARGET LOCKED) and the product's live position below it."""
    x0, y0, x1, y1 = box
    tag(draw, (x0 + 6, y0 - 16), f"SCANNING {round(100 * p)}%" if p < 1 else "TARGET LOCKED", "ld")
    cx, cy = (x0 + x1) / 2 / w, (y0 + y1) / 2 / h
    tag(draw, (x0 + 6, y1 + 16), f"X {cx:.3f} Y {cy:.3f}\nSIZE {round(x1 - x0)}x{round(y1 - y0)}", "la")

def overlay(t, box):
    img = Image.new("RGBA", (w, h))
    if t < START:
        return img
    p = 1 - (1 - min((t - START) / LOCK, 1)) ** 3                 # ease-out from the frame edges to the product
    frame_box = (24, 24, w - 24, h - 24)
    now = tuple(a + (b - a) * p for a, b in zip(frame_box, box))
    lines = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(lines)
    brackets(draw, now)
    sweep = min(max(t - START - LOCK, 0) / SCAN, 1)
    if t >= START + LOCK:
        if sweep < 1:
            img = scan_glow(now, sweep)
            y = now[1] + (now[3] - now[1]) * sweep
            draw.line([(now[0] + 8, y), (now[2] - 8, y)], fill=COLOR, width=3)
        readouts(draw, now, sweep)
    shadow = Image.new("RGBA", (w, h), (0, 25, 0))
    shadow.putalpha(lines.getchannel("A").filter(ImageFilter.GaussianBlur(3)).point(lambda a: min(150, a * 2)))
    img.alpha_composite(shadow)
    img.alpha_composite(lines)
    return img

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def render(src, out, boxes):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps, boxes[min(i, len(boxes) - 1)]).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
font = ImageFont.truetype(FONT_FILE, round(w * 0.04))
render(src, out, track(src))
```

## More hook openers

- [Blur-to-focus reveal **Blur-to-focus reveal**](https://www.recharm.com/effects/blur-to-focus-reveal)
- [Breaking news banner **Breaking news banner**](https://www.recharm.com/effects/breaking-news-banner)
- [Camera shake **Camera shake**](https://www.recharm.com/effects/camera-shake)
- [Color reveal **Color reveal**](https://www.recharm.com/effects/color-reveal)
- [Countdown intro **Countdown intro**](https://www.recharm.com/effects/countdown-intro)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
