A lightweight synthetic dataset image generation library built on top of Pillow.
Project description
Spacial
Lightweight synthetic dataset image generation — powered by Pillow.
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
- Quick Start
- Design Philosophy
- API Reference
- Fill System
- Examples
- Exporting Annotations
- Future Roadmap
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:
- Generate images — backgrounds, shapes, composited objects.
- Apply effects — per-pixel filters on the full canvas or a single object region.
- Generate bounding box annotations — pixel-aligned
[x1, y1, x2, y2]. - 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(...)andspacial.save(...), you have a dataset. - Standard types. Returns
list,dict, andtuple— 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:
- Matches a registered shape name → renders that template.
"img"→ loads the image atpath=.- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
157e62a3e868f0babfeea05b1c4d664e9d230408d3d7c0ec490745a7ae2a30ae
|
|
| MD5 |
1701f556c2ceafc5c857c02b1b76ec37
|
|
| BLAKE2b-256 |
d363d56495cc6d631831d8c4d0ea8432eda06fd78c78fe00afb2e4fd3fbbb64f
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd6fc28bc3656683e12e1f818af8c2042af3f6ffb2e27544d47cca6979a653bb
|
|
| MD5 |
f23a25bb070a4bedc689930a2bd4ae7e
|
|
| BLAKE2b-256 |
e4fe74c14d8f2280132356706cc37be2bb3fd34f19e59b7719418336fecf690a
|