# Draw a 9:16 Safe-Zone Overlay for Shorts with Python

![Cover: "Check 9:16 safe zones before you upload" with a tinted 9:16 frame showing the top, bottom and right zones](https://cdn.hashnode.com/uploads/covers/6ac7155f8cadc38d9e483af7/7c411a8f-78dd-491c-a405-b0dca7be3b0e.jpg)
*AI-generated example made with MixVio's Burning Bridges AI effect. The lead is an AI-generated portrait, not a real person.*

Shorts, Reels and TikTok all draw their own interface on top of your video: a caption and sound label along the bottom, a column of buttons on the right, and status or search elements at the top. Text or faces in those areas get covered.

Checking that by eye on every export gets old quickly. This post builds a small Python script that grabs a few frames from a 9:16 export with ffmpeg, tints the risky areas with Pillow, and saves one contact sheet you can glance at before uploading.

## The zones

Every platform draws its UI slightly differently, and the layouts change. Rather than chase exact pixel values, I use one conservative set of zones for a 1080x1920 frame:

- **Top 10%** (y < 192): status bar, search, account labels.
- **Bottom 20%** (y >= 1536): caption, sound name, progress bar.
- **Right 15%** (x >= 918): like, comment, share and profile buttons.

Keep titles and faces out of those areas and the video reads cleanly on all three apps. Defining the zones as fractions keeps the script working for 720x1280 exports too.

## What you need

- Python 3 with Pillow (`pip install pillow`).
- ffmpeg on your PATH.
- A 9:16 export to check.

The example export here is a 17.5-second 1080x1920 short built from a 16:9 clip made with [Burning Bridges AI on MixVio](https://mixvio.ai/ai-effects/burning-bridges-ai?utm_source=hashnode&utm_medium=article&utm_campaign=burning-bridges&utm_content=burning-bridges-ai-safezone#effect-workspace). The effect outputs 16:9 only, so the 9:16 version is a letterboxed post-production edit: the 16:9 result sits in the middle of the frame, with a title card at the start and an end card at the end. That makes it a good test case, because the titles live in the letterbox bars, right next to the zones.

## The script

```python
import subprocess
import sys
from PIL import Image, ImageDraw

W, H = 1080, 1920
ZONES = {  # name: (box as fractions x0, y0, x1, y1), RGBA tint
    "top 10%": ((0.0, 0.0, 1.0, 0.10), (255, 220, 0, 90)),
    "bottom 20%": ((0.0, 0.80, 1.0, 1.0), (255, 40, 40, 90)),
    "right 15%": ((0.85, 0.0, 1.0, 1.0), (255, 140, 0, 90)),
}


def grab(video, t, out):
    cmd = ["ffmpeg", "-v", "error", "-y", "-ss", str(t), "-i", video,
           "-frames:v", "1", out]
    subprocess.run(cmd, check=True)
    return Image.open(out).convert("RGBA")


def overlay(frame):
    w, h = frame.size
    layer = Image.new("RGBA", frame.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(layer)
    for name, ((x0, y0, x1, y1), rgba) in ZONES.items():
        box = (int(x0 * w), int(y0 * h), int(x1 * w) - 1, int(y1 * h) - 1)
        d.rectangle(box, fill=rgba, outline=rgba[:3] + (255,), width=4)
    return Image.alpha_composite(frame, layer)


def sheet(video, times, out="safezone.png", scale=0.5):
    tiles = []
    for i, t in enumerate(times):
        img = overlay(grab(video, t, f"_f{i}.png"))
        img = img.resize((int(W * scale), int(H * scale)))
        ImageDraw.Draw(img).text((12, 12), f"{t:.2f} s", fill="white")
        tiles.append(img)
    tw, th = tiles[0].size
    out_img = Image.new("RGBA", (tw * len(tiles), th), "black")
    for i, tile in enumerate(tiles):
        out_img.paste(tile, (i * tw, 0))
    out_img.convert("RGB").save(out)
    return out


if __name__ == "__main__":
    print(sheet(sys.argv[1], [0.05, 11.9, 17.0]))
```

The script has four parts.

### ZONES

Each zone is a box in fractions of the frame (x0, y0, x1, y1) plus an RGBA tint. An alpha of 90 out of 255 is enough to see the zone without hiding what is under it, and a solid outline marks the edge. Overlapping zones (the bottom-right corner) simply get darker, which is a useful hint: that corner is covered by both the caption and the buttons.

### grab()

`grab()` asks ffmpeg for one frame at time `t`. Putting `-ss` before `-i` makes ffmpeg seek quickly, which is accurate enough for QA. It writes a PNG and loads it as RGBA so it can be composited.

### overlay()

`overlay()` draws all zones onto a transparent layer the size of the frame and alpha-composites it over the frame. Drawing on a separate layer keeps the tint even; drawing straight onto the frame with a semi-transparent fill would not blend.

### sheet()

`sheet()` grabs each time stamp, overlays the zones, scales each frame to half size, stamps the time in the corner, and pastes the frames side by side. The output is a single RGB PNG.

## Choosing the sample times

Pick frames where something important is on screen. For this export:

- **0.05 s**: the title card. The first frame doubles as the cover on some platforms.
- **11.9 s**: a frame from the main clip, with the persistent label above it and the small print below.
- **17.0 s**: the end card.

Run it with the export path:

```bash
python3 safezone.py short.mp4
# safezone.png  (1620x960 for three 1080x1920 frames at 0.5 scale)
```

## Reading the result

![QA sheet: three frames of the 9:16 export with the top 10%, bottom 20% and right 15% safe zones tinted: the cover at 0.05 s, a clip frame at 11.9 s and the end card at 17.0 s](https://mixvioai.wordpress.com/wp-content/uploads/2026/10/02_img_as2-sz.jpg)
*AI-generated example made with MixVio's Burning Bridges AI effect. The lead is an AI-generated portrait, not a real person.*

What to look for:

- **Titles outside the tint.** The title on the cover frame sits between the top zone and the clip, and clear of the right column.
- **Faces outside the tint.** The 16:9 clip sits in the middle of the frame, so the lead's face stays clear of every zone. The right edge of the clip falls under the button column; that is acceptable here because only the background is there, but it is exactly what this check is for.
- **End card text.** Calls to action at the very bottom are the most common mistake; the caption covers them.
- **The bottom-right corner.** If anything you care about is in the double-tinted corner, move it.

If something sits in a zone, fix the edit and run the script again. It takes a couple of seconds.

## Variations

- **More frames.** Pass a longer list, or sample evenly with `[i * dur / 8 for i in range(8)]` after reading the duration with ffprobe.
- **Different zones.** If you only publish to one platform, tighten the fractions for that app's current layout.
- **A grid instead of a row.** For long exports, paste the tiles into rows of four.
- **CI check.** Save the sheet next to each export in your render pipeline so every file has a QA image.

## Limits

This is a visual aid, not a guarantee. Platforms move UI elements, captions can grow to several lines, and some devices add their own system bars. Treat the zones as "keep important things out", not "everything outside is always visible".

A note on the example clip, from the effect page: "This is an independent AI effect and is not affiliated with Drake."

*Disclosure: written by the MixVio team with AI assistance; the example video was made with MixVio's Burning Bridges AI effect from an AI-generated portrait.*

