Skip to main content

A lightweight synthetic dataset image generation library built on top of Pillow.

Project description

Spacial

Lightweight synthetic dataset image generation — powered by Pillow.

Python 3.9+ License: MIT Version

Spacial is a small, focused library for generating synthetic training images and their annotations. You describe what to put on a canvas — backgrounds, shapes, images — and Spacial gives you back pixel-perfect bounding boxes and segmentation masks in plain Python data structures. No frameworks. No hidden config files. No magic.


Contents


Installation

pip install spacial

Requires Python 3.9+ and Pillow ≥ 10.0.


Quick Start

import spacial

# 1. Create a 640×480 canvas
spacial.init(w=640, h=480)

# 2. Dark gradient background
spacial.background("gradient", fill={
    "type": "gradient",
    "start": "#1A1A2E",
    "end": "#16213E",
    "direction": "vertical",
})

# 3. Define a reusable shape template
spacial.shape("car", w=120, h=60)
spacial.shape_add("car", "rectangle", fill="#E63946", x0=0, y0=10, x1=120, y1=60)
spacial.shape_add("car", "rectangle", fill="#222222", x0=20, y0=0, x1=100, y1=20)

# 4. Place two cars on the canvas
spacial.append("car_001", "car", x=50, y=200)
spacial.append("car_002", "car", x=350, y=300)

# 5. Apply effects — blur the whole scene, then sharpen one car
spacial.effect("blur", 1.5)
spacial.effect("contrast", "car_001", 1.8)

# 6. Get annotations
print(spacial.bbox())
# [
#   {"id": "car_001", "class": "car", "bbox": [50, 200, 170, 260]},
#   {"id": "car_002", "class": "car", "bbox": [350, 300, 470, 360]},
# ]

# 7. Save and reset
spacial.save("frame_001.png")
spacial.rm()

Design Philosophy

Spacial is deliberately narrow. It does exactly four things:

  1. Generate images — backgrounds, shapes, composited objects.
  2. Apply effects — per-pixel filters on the full canvas or a single object region.
  3. Generate bounding box annotations — pixel-aligned [x1, y1, x2, y2].
  4. Generate segmentation annotations — per-pixel binary masks.

Everything else — writing COCO JSON, YOLO .txt files, Pascal VOC XML, training loops, data augmentation — is intentionally left to you. Spacial integrates cleanly with whatever export or training pipeline you already have.

Guiding principles:

  • Flat API. Everything is a module-level function. No classes to instantiate, no context managers to juggle.
  • Minimal dependencies. Only Pillow. No NumPy required (though masks are trivial to convert).
  • Beginner friendly. If you can write spacial.append(...) and spacial.save(...), you have a dataset.
  • Standard types. Returns list, dict, and tuple — no custom objects to unwrap.
  • Both colour notations. RGB tuples (255, 0, 0) and hex strings "#FF0000" work everywhere a colour is expected.

API Reference

init

spacial.init(device="cpu", w=1024, h=1024)

Initialise Spacial and create a blank canvas. Call this once at the start of each session, or whenever you need a new canvas size.

Parameter Type Default Description
device str "cpu" "cpu", "cuda", or "mps". GPU options are reserved for a future release and currently behave the same as "cpu".
w int 1024 Canvas width in pixels.
h int 1024 Canvas height in pixels.

background

spacial.background(bg_type, *, fill=..., path=None, seed=None)

Fill the entire canvas with a background. Always call this before placing objects — it overwrites anything underneath.

Parameter Type Description
bg_type str "color", "gradient", "noise", "perlin", or "img"
fill colour or dict Colour value or fill spec (see Fill System). Not used for "img".
path str | None Path to a source image. Required for bg_type="img".
seed int | None Random seed for reproducible "noise" / "perlin" backgrounds.
spacial.background("color", fill=(30, 30, 30))
spacial.background("color", fill="#1A1A2E")

spacial.background("gradient", fill={
    "type": "gradient", "start": "#FF6B6B", "end": "#4ECDC4", "direction": "horizontal",
})

spacial.background("noise",  fill={"type": "noise",  "base": (100, 100, 100), "scale": 0.4}, seed=42)
spacial.background("perlin", fill={"type": "perlin", "base": "#2C3E50", "scale": 0.6, "octaves": 5}, seed=7)

spacial.background("img", path="sky.jpg")

shape / shape_add

spacial.shape(name, *, w, h)
spacial.shape_add(name, primitive, *, fill=..., **params)

Define a reusable shape template by stacking primitives. Templates are preserved across rm() calls so you can reuse them across frames.

shape

Parameter Type Description
name str Unique template name.
w int Template width in pixels.
h int Template height in pixels.

shape_add — primitives

Primitive Required params Description
"circle" cx, cy, r Circle with centre and radius.
"rectangle" x0, y0, x1, y1 Axis-aligned rectangle.
"img" path (optionally x, y) Paste an image at an offset.

All primitives accept a fill parameter (colour or fill spec).

spacial.shape("traffic_light", w=40, h=100)
spacial.shape_add("traffic_light", "rectangle", fill="#222222", x0=0,  y0=0,  x1=40,  y1=100)
spacial.shape_add("traffic_light", "circle",    fill="#FF0000", cx=20, cy=20, r=14)
spacial.shape_add("traffic_light", "circle",    fill="#FFA500", cx=20, cy=50, r=14)
spacial.shape_add("traffic_light", "circle",    fill="#00CC00", cx=20, cy=80, r=14)

append

spacial.append(obj_id, obj_class, *, x=0, y=0, **kwargs)

Place an object on the canvas and record its annotation.

Parameter Type Description
obj_id str Unique instance ID, used in annotation output.
obj_class str A registered shape name, "img", or a free-form label.
x int Left edge of the object in canvas pixels.
y int Top edge of the object in canvas pixels.
**kwargs Forwarded to the renderer: path, w, h, fill, etc.

Class resolution order:

  1. Matches a registered shape name → renders that template.
  2. "img" → loads the image at path=.
  3. Anything else → renders a placeholder rectangle using fill=, w=, h=.
spacial.append("tl_north",     "traffic_light", x=100, y=50)
spacial.append("sponsor_logo", "img",           path="logo.png", x=20, y=20)
spacial.append("unknown_001",  "unknown",       x=300, y=150, fill="#AAAAAA", w=80, h=80)

effect

# Apply to the whole canvas
spacial.effect(effect_name, value)

# Apply only to a single placed object
spacial.effect(effect_name, obj_id, value)

Apply a visual effect either to the entire canvas or to the bounding-box region of one previously placed object. Effects are applied immediately and modify the canvas in place.

Effect table

Name Description Value
"blur" Gaussian blur Radius as a float (e.g. 2.0)
"sharpen" Sharpness adjustment Factor — 1.0 = unchanged, > 1.0 = more
"brightness" Brightness adjustment Factor — 1.0 = unchanged, > 1.0 = more
"contrast" Contrast adjustment Factor — 1.0 = unchanged, > 1.0 = more
"grayscale" Convert to greyscale Value is ignored, pass None or 0
"edge" Edge-detection filter Value is ignored, pass None or 0
# Blur the whole image slightly
spacial.effect("blur", 2.0)

# Boost contrast on a single object
spacial.effect("contrast", "car_001", 1.8)

# Darken the whole scene
spacial.effect("brightness", 0.6)

# Grayscale one object, keep the rest colourful
spacial.effect("grayscale", "bg_obj", None)

Order matters. Effects are non-destructive to annotations (bounding boxes and masks are not changed), but they do permanently alter the canvas pixels at the time of the call. Apply whole-image effects before per-object ones if you want them to compose naturally.


bbox

spacial.bbox(obj_id=None) -> list[dict]

Return bounding-box annotations.

[
    {"id": "car_001", "class": "car", "bbox": [x1, y1, x2, y2]},
    ...
]

Pass obj_id="car_001" to get a single object's annotation. Bounding boxes are pixel coordinates with inclusive corners.


seg

spacial.seg(obj_id=None) -> list[dict]

Return segmentation annotations.

[
    {
        "id":        "car_001",
        "class":     "car",
        "bbox":      [x1, y1, x2, y2],
        "mask":      [...],         # flat list, len == w * h, values 0 or 255
        "mask_size": (w, h)
    },
    ...
]

Converting to a NumPy array (NumPy is not a Spacial dependency, but easy to integrate):

import numpy as np
entries = spacial.seg()
w, h = entries[0]["mask_size"]
mask = np.array(entries[0]["mask"], dtype=np.uint8).reshape(h, w)

save

spacial.save(path)

Save the current canvas to disk. Format is inferred from the extension by Pillow (.png, .jpg, .bmp, .webp, etc.).

spacial.save("output/frame_042.png")

rm

spacial.rm()

Clear the canvas to black and remove all placed objects. Shape templates are preserved so you can reuse them in the next frame.


Fill System

Wherever a fill parameter is accepted, Spacial understands these notations:

Solid colour

fill=(255, 99, 71)   # RGB tuple
fill="#FF6347"       # hex string

Gradient

fill={
    "type":      "gradient",
    "start":     "#FF6B6B",
    "end":       "#4ECDC4",
    "direction": "horizontal",   # or "vertical"
}

Noise — uniform random per-pixel offsets from a base colour

fill={
    "type":  "noise",
    "base":  (128, 128, 128),
    "scale": 0.5,   # 0.0 = no noise, 1.0 = maximum noise
}

Perlin — layered smooth noise, good for terrain-like backgrounds

fill={
    "type":    "perlin",
    "base":    "#2C3E50",
    "scale":   0.6,
    "octaves": 4,   # more octaves = more detail
}

Examples

Minimal YOLO-style loop

import json
import spacial

spacial.init(w=416, h=416)

spacial.shape("ball", w=32, h=32)
spacial.shape_add("ball", "circle", fill="#F72585", cx=16, cy=16, r=15)

dataset = []
for i in range(100):
    spacial.rm()
    spacial.background("noise", fill={"type": "noise", "base": (80, 80, 80), "scale": 0.3}, seed=i)

    x, y = i * 3 % 380, i * 7 % 380
    spacial.append(f"ball_{i:04d}", "ball", x=x, y=y)

    boxes = spacial.bbox()
    spacial.save(f"images/frame_{i:04d}.png")
    dataset.append({"frame": i, "annotations": boxes})

with open("annotations.json", "w") as f:
    json.dump(dataset, f, indent=2)

Multi-class scene with effects

import spacial

spacial.init(w=800, h=600)
spacial.background("perlin", fill={
    "type": "perlin", "base": (34, 85, 34), "scale": 0.5, "octaves": 5,
}, seed=99)

spacial.shape("vehicle", w=100, h=50)
spacial.shape_add("vehicle", "rectangle", fill="#264653", x0=0,  y0=10, x1=100, y1=50)
spacial.shape_add("vehicle", "rectangle", fill="#2A9D8F", x0=15, y0=0,  x1=85,  y1=20)

spacial.shape("pedestrian", w=20, h=50)
spacial.shape_add("pedestrian", "rectangle", fill="#E9C46A", x0=6, y0=0,  x1=14, y1=12)
spacial.shape_add("pedestrian", "rectangle", fill="#F4A261", x0=4, y0=12, x1=16, y1=50)

spacial.append("v_001", "vehicle",    x=50,  y=280)
spacial.append("v_002", "vehicle",    x=400, y=320)
spacial.append("p_001", "pedestrian", x=250, y=260)
spacial.append("p_002", "pedestrian", x=310, y=270)
spacial.append("p_003", "pedestrian", x=600, y=290)

# Slightly blur the whole scene, then sharpen the focal vehicle
spacial.effect("blur", 1.5)
spacial.effect("sharpen", "v_001", 3.0)

print(spacial.bbox())
spacial.save("scene.png")

Pasting real images with segmentation

import spacial

spacial.init(w=512, h=512)
spacial.background("color", fill="#F0F0F0")
spacial.append("product_01", "img", path="product.png", x=128, y=128)

# Boost the product's contrast after placing it
spacial.effect("contrast", "product_01", 1.5)

for entry in spacial.seg():
    w, h  = entry["mask_size"]
    total = w * h
    hit   = sum(1 for v in entry["mask"] if v > 0)
    print(f'{entry["id"]} covers {hit/total:.1%} of the canvas')

spacial.save("product_scene.png")

Exporting Annotations

Spacial returns plain Python dicts so you can convert to any format you need.

YOLO .txt

boxes = spacial.bbox()
W, H  = 640, 480

with open("labels/frame_001.txt", "w") as f:
    class_map = {"car": 0, "pedestrian": 1}
    for obj in boxes:
        x1, y1, x2, y2 = obj["bbox"]
        cx = ((x1 + x2) / 2) / W
        cy = ((y1 + y2) / 2) / H
        bw = (x2 - x1) / W
        bh = (y2 - y1) / H
        cls = class_map.get(obj["class"], 0)
        f.write(f"{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")

COCO-style JSON snippet

boxes = spacial.bbox()
coco_annotations = [
    {
        "id":          i,
        "image_id":    42,
        "category_id": 1,
        "bbox":        [b["bbox"][0], b["bbox"][1],
                        b["bbox"][2] - b["bbox"][0],
                        b["bbox"][3] - b["bbox"][1]],
        "area":        (b["bbox"][2] - b["bbox"][0]) * (b["bbox"][3] - b["bbox"][1]),
        "iscrowd":     0,
    }
    for i, b in enumerate(boxes)
]

Future Roadmap

Planned additions in future releases:

  • Shape nesting — embed one named shape inside another.
  • Transforms — per-object rotation, scaling, and opacity.
  • GPU acceleration — real CUDA/MPS paths for noise generation at high resolutions.
  • Z-ordering — explicit depth control for overlapping objects.
  • Polygon segmentation — return polygon contours alongside binary masks.
  • Text primitive — render text labels directly onto shapes.
  • Physics-based placement — non-overlapping random placement helpers.
  • Effect stacking — apply multiple effects to the same target in one call.

Spacial will never grow into a training framework or annotation exporter. Those concerns belong in your pipeline.


License

MIT © Spacial Contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spacial-0.1.1.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spacial-0.1.1-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

File details

Details for the file spacial-0.1.1.tar.gz.

File metadata

  • Download URL: spacial-0.1.1.tar.gz
  • Upload date:
  • Size: 19.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for spacial-0.1.1.tar.gz
Algorithm Hash digest
SHA256 157e62a3e868f0babfeea05b1c4d664e9d230408d3d7c0ec490745a7ae2a30ae
MD5 1701f556c2ceafc5c857c02b1b76ec37
BLAKE2b-256 d363d56495cc6d631831d8c4d0ea8432eda06fd78c78fe00afb2e4fd3fbbb64f

See more details on using hashes here.

File details

Details for the file spacial-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: spacial-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for spacial-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bd6fc28bc3656683e12e1f818af8c2042af3f6ffb2e27544d47cca6979a653bb
MD5 f23a25bb070a4bedc689930a2bd4ae7e
BLAKE2b-256 e4fe74c14d8f2280132356706cc37be2bb3fd34f19e59b7719418336fecf690a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page