DEV Community

Cover image for Split a Storyboard Grid Into Clean Frames With Pillow
Joey
Joey

Posted on

Split a Storyboard Grid Into Clean Frames With Pillow

When I split a storyboard sheet, I want each exported file to start at the edge of the actual frame. Dividing the full image width by the column count also divides the borders and gaps, which can leave strips of neighboring panels in the output.

I calculate the usable area first:

usable width = image width - 2 * margin - (columns - 1) * gap
Enter fullscreen mode Exit fullscreen mode

Then I divide that width by the column count and apply the same calculation to the height.

For this example, the sheet is 1024 x 412 pixels: two rows, three columns, a 20-pixel outer margin, and 12-pixel gaps. Each frame is 320 x 180.

Install Pillow:

python -m pip install Pillow
Enter fullscreen mode Exit fullscreen mode

Save this as split_storyboard.py, put board.png beside it, and run it from that directory. The script creates a new shots folder; for another run, choose a different destination name.

import json
from pathlib import Path

from PIL import Image


def split_grid(source, destination, rows, cols, margin=0, gap=0):
    if rows <= 0 or cols <= 0 or margin < 0 or gap < 0:
        raise ValueError("Use positive rows/cols and non-negative margin/gap.")

    with Image.open(source) as image:
        width, height = image.size
        usable_w = width - 2 * margin - (cols - 1) * gap
        usable_h = height - 2 * margin - (rows - 1) * gap

        if usable_w <= 0 or usable_h <= 0:
            raise ValueError("Margins and gaps leave no usable image area.")

        cell_w, remainder_w = divmod(usable_w, cols)
        cell_h, remainder_h = divmod(usable_h, rows)
        if remainder_w or remainder_h:
            raise ValueError("Grid does not divide evenly. Check margin/gap.")

        output = Path(destination)
        output.mkdir(parents=True, exist_ok=False)
        frames = []

        for row in range(rows):
            for col in range(cols):
                left = margin + col * (cell_w + gap)
                top = margin + row * (cell_h + gap)
                box = (left, top, left + cell_w, top + cell_h)
                name = f"shot_{row * cols + col + 1:02d}.png"
                image.crop(box).save(output / name)
                frames.append({"file": name, "box": list(box)})

        manifest = {
            "source": str(source),
            "size": [width, height],
            "frames": frames,
        }
        (output / "manifest.json").write_text(
            json.dumps(manifest, indent=2), encoding="utf-8"
        )

    print(f"Saved {len(frames)} frames ({cell_w}x{cell_h})")


if __name__ == "__main__":
    split_grid("board.png", "shots", rows=2, cols=3, margin=20, gap=12)
Enter fullscreen mode Exit fullscreen mode

Run:

python split_storyboard.py
Enter fullscreen mode Exit fullscreen mode

For the example dimensions, the output is:

Saved 6 frames (320x180)
Enter fullscreen mode Exit fullscreen mode

Files run from shot_01.png to shot_06.png, reading left to right, then top to bottom. The first crop's box is [20, 20, 340, 200]; the last is [684, 212, 1004, 392]. Both are recorded in manifest.json.

I use divmod() because rounding down would hide a layout mismatch. Change the example's width to 1025 pixels and the script stops with:

ValueError: Grid does not divide evenly. Check margin/gap.
Enter fullscreen mode Exit fullscreen mode

That sends me back to the measurements before I export six slightly wrong frames.

This version expects equal-sized panels, the same margin on all four sides, and one gap size in both directions. A number or caption drawn inside a panel remains part of that panel; excluding the gutters won't remove it.

Top comments (0)