A lightweight synthetic dataset image generation library built on top of Pillow.
Project description
Spacial — Documentation
Spacial is a lightweight synthetic dataset image generation library. It generates images and their annotations (bounding boxes, oriented bounding boxes, segmentation masks) using Pillow. No export formats, no training framework, no scene graph — just pixels and coordinates.
Installation
pip install pillow
Spacial ships as a single __init__.py module. Drop it into your project or install it as a local package.
Quick start
import spacial
spacial.init(w=640, h=480)
spacial.background("color", fill=(30, 30, 30))
spacial.shape("badge", w=80, h=80)
spacial.shape_add("badge", "circle", fill="#FF4500", xpos=2, ypos=2, width=76, height=76)
spacial.shape_add("badge", "rectangle", fill=(0, 0, 0), xpos=20, ypos=60, width=40, height=8)
spacial.shape("tag", w=100, h=30)
spacial.shape_add("tag", "rectangle", fill=(40, 40, 40), xpos=0, ypos=0, width=100, height=30)
spacial.shape_add("tag", "text", fill="#FFFFFF", xpos=6, ypos=4, text="A1", font_size=18)
spacial.append("road", "surface", x=0, y=360, z=0, w=640, h=120, fill=(60, 60, 60))
spacial.append("logo_01", "badge", x=50, y=50, z=1)
spacial.append("logo_02", "badge", x=200, y=50, z=1, scale=1.5, rotation=20)
spacial.append("label_a", "tag", x=50, y=140, z=2)
print(spacial.bbox())
print(spacial.obb())
spacial.save("output.png")
spacial.rm()
API reference
spacial.init(device="cpu", w=1024, h=1024)
Initialise Spacial for a new generation session. Must be called before anything else.
| Parameter | Type | Default | Description |
|---|---|---|---|
device |
str |
"cpu" |
Compute device: "cpu", "cuda", or "mps". GPU support is reserved for a future release. |
w |
int |
1024 |
Canvas width in pixels. |
h |
int |
1024 |
Canvas height in pixels. |
spacial.init(device="cpu", w=640, h=480)
spacial.new()
Re-initialise the canvas from scratch, discarding everything — the image, all placed objects, and all shape templates. Canvas dimensions and device are preserved from the last init() call.
Use new() when you want a completely blank slate between images in a loop. Contrast with rm(), which keeps shape templates intact.
spacial.init(w=640, h=480)
for i in range(100):
spacial.new() # wipe canvas + shapes + objects
spacial.background("color", fill=(20, 20, 20))
spacial.shape("dot", w=32, h=32)
spacial.shape_add("dot", "circle", fill="#FF0000",
xpos=0, ypos=0, width=32, height=32)
spacial.append(f"dot_{i:03d}", "dot", x=i * 6, y=50)
spacial.save(f"frame_{i:03d}.png")
| Clears | new() |
rm() |
|---|---|---|
| Canvas image | ✓ | ✓ |
| Placed objects | ✓ | ✓ |
| Shape templates | ✓ | — |
spacial.background(bg_type, *, fill=..., path=None, seed=None)
Fill the canvas with a background. Must be called after init().
| Parameter | Type | Description |
|---|---|---|
bg_type |
str |
"color", "gradient", "noise", "perlin", or "img". |
fill |
colour or dict | Colour or fill spec (see below). |
path |
str |
Path to an image file (required for bg_type="img"). |
seed |
int |
Random seed for reproducible noise/perlin backgrounds. |
Fill specifications
# Solid colour
spacial.background("color", fill=(30, 30, 30))
spacial.background("color", fill="#1A1A2E")
# Gradient
spacial.background("gradient", fill={
"type": "gradient",
"start": "#FF6B6B",
"end": "#4ECDC4",
"direction": "vertical", # or "horizontal"
})
# Uniform noise
spacial.background("noise", fill={
"type": "noise",
"base": (128, 128, 128),
"scale": 0.4, # 0.0–1.0
})
# Perlin-like noise
spacial.background("perlin", fill={
"type": "perlin",
"base": (100, 120, 140),
"scale": 0.5,
"octaves": 4,
})
# Image file
spacial.background("img", path="sky.jpg")
spacial.shape(name, *, w, h)
Register a reusable shape template. Shapes are composited from one or more primitives added via shape_add().
| Parameter | Type | Description |
|---|---|---|
name |
str |
Unique identifier for the template. |
w |
int |
Template width in pixels. |
h |
int |
Template height in pixels. |
spacial.shape("badge", w=80, h=80)
spacial.shape_add(name, primitive, *, fill=..., xpos=0, ypos=0, width=None, height=None, points=None, text=None, font_size=16, font_path=None, path=None, rotation=0.0)
Add a primitive element to an existing shape template.
All position and size arguments use a single consistent coordinate system: xpos/ypos is the top-left corner of the primitive within the template, and width/height define its size.
| Parameter | Type | Description |
|---|---|---|
name |
str |
Shape template to modify (must exist). |
primitive |
str |
"circle", "rectangle", "triangle", "polygon", "text", or "img". |
fill |
colour | Colour as an RGB tuple or "#RRGGBB" hex string. Ignored for "img". |
xpos |
int |
Left edge of the primitive within the template (default 0). For "triangle" and "polygon", added as an X offset to all points. |
ypos |
int |
Top edge of the primitive within the template (default 0). For "triangle" and "polygon", added as a Y offset to all points. |
width |
int |
Width of the primitive in pixels. For "circle", this is the horizontal diameter. |
height |
int |
Height of the primitive in pixels. For "circle", this is the vertical diameter. |
points |
list[tuple[int, int]] |
Vertex list for "triangle" (exactly 3) and "polygon" (3 or more). Coordinates are relative to the template origin. |
text |
str |
The string to render. Required for "text". |
font_size |
int |
Font size in points for "text" (default 16). |
font_path |
str |
Path to a TrueType/OpenType font file. Falls back to a system default when omitted. |
path |
str |
Path to an image file (required for "img"). |
rotation |
float |
Clockwise rotation of this primitive in degrees around its own centre (default 0.0). Applies to all primitives. |
Primitives
# Circle
spacial.shape("badge", w=80, h=80)
spacial.shape_add("badge", "circle", fill="#FF4500",
xpos=2, ypos=2, width=76, height=76)
# Rectangle — with optional per-element rotation
spacial.shape("card", w=120, h=60)
spacial.shape_add("card", "rectangle", fill=(200, 200, 200),
xpos=0, ypos=0, width=120, height=60)
spacial.shape_add("card", "rectangle", fill=(255, 80, 0),
xpos=10, ypos=10, width=40, height=8, rotation=45)
# Triangle (exactly 3 points)
spacial.shape("arrow", w=60, h=60)
spacial.shape_add("arrow", "triangle", fill="#00FF88",
points=[(30, 0), (60, 60), (0, 60)])
# Polygon (3 or more points — here a hexagon)
spacial.shape("hex", w=80, h=80)
spacial.shape_add("hex", "polygon", fill="#8844EE",
points=[(40,0),(80,20),(80,60),(40,80),(0,60),(0,20)])
# Text
spacial.shape("label", w=120, h=40)
spacial.shape_add("label", "rectangle", fill=(40, 40, 40),
xpos=0, ypos=0, width=120, height=40)
spacial.shape_add("label", "text", fill="#FFFFFF",
xpos=6, ypos=8, text="A1", font_size=22)
# Rotated text label
spacial.shape("rotlabel", w=120, h=40)
spacial.shape_add("rotlabel", "text", fill="#FF0000",
xpos=4, ypos=8, text="WARN", font_size=18, rotation=15)
# Image
spacial.shape("icon", w=64, h=64)
spacial.shape_add("icon", "img", path="logo.png", xpos=0, ypos=0)
Note on circles: pass equal
widthandheightfor a perfect circle; different values produce an ellipse.
Note on rotation:
rotationrotates the individual primitive around its own centre inside the template. To rotate the whole placed object, use therotationparameter onappend().
spacial.append(obj_id, obj_class, *, x=0, y=0, z=0, scale=1.0, rotation=0.0, **kwargs)
Place an object on the canvas and record its annotation.
Objects are composited in z-order: higher z values appear on top. Objects with equal z are drawn in insertion order. After every append() call the canvas is automatically re-composited, so appending a low-z object after a high-z one still produces correct layering.
| Parameter | Type | Description |
|---|---|---|
obj_id |
str |
Unique identifier for this instance (used in annotations). |
obj_class |
str |
A registered shape name, "img", or any label (renders a placeholder rectangle). |
x |
int |
Left edge of the object on the canvas (before scale/rotation). |
y |
int |
Top edge of the object on the canvas (before scale/rotation). |
z |
int |
Z-layer depth (default 0). Higher values appear on top. |
scale |
float |
Uniform scale factor (default 1.0). 2.0 doubles the size; 0.5 halves it. |
rotation |
float |
Clockwise rotation of the whole object in degrees around its centre (default 0.0). |
**kwargs |
Forwarded to the renderer: path=, w=, h=, fill=. |
# Z-layering: road behind car, label on top
spacial.append("road", "surface", x=0, y=300, z=0, w=640, h=180, fill=(70,70,70))
spacial.append("car_01", "car", x=100, y=240, z=1, scale=1.2, rotation=5)
spacial.append("plate", "label", x=130, y=280, z=2)
# Inline image
spacial.append("logo", "img", path="logo.png", x=10, y=10, z=3)
spacial.effect(effect_name, *args)
Apply a visual effect to the whole canvas or a single placed object.
# Whole-canvas effect
spacial.effect("blur", 3.0)
# Per-object effect
spacial.effect("contrast", "car_001", 1.8)
| Effect | Value | Description |
|---|---|---|
"blur" |
radius float ≥ 0 |
Gaussian blur |
"sharpen" |
factor float |
Sharpness (1.0 = no change) |
"brightness" |
factor float |
Brightness (1.0 = no change) |
"contrast" |
factor float |
Contrast (1.0 = no change) |
"grayscale" |
any | Convert to greyscale |
"edge" |
any | Edge-detection filter |
spacial.bbox(obj_id=None)
Return axis-aligned bounding-box annotations.
spacial.bbox()
# [{'id': 'car_001', 'class': 'car', 'bbox': [100, 200, 220, 260]}, ...]
spacial.bbox("car_001")
# [{'id': 'car_001', 'class': 'car', 'bbox': [100, 200, 220, 260]}]
Each entry: {"id": str, "class": str, "bbox": [x1, y1, x2, y2]}.
For rotated objects the bbox is the axis-aligned envelope of the rotated shape. Use obb() to get the tighter oriented box.
spacial.obb(obj_id=None)
Return oriented bounding-box annotations. The OBB is defined by the four corner points of the object before axis-alignment, expressed in canvas pixel coordinates. For unrotated objects the corners coincide with the axis-aligned bounding box.
spacial.obb()
# [{'id': 'car_001', 'class': 'car',
# 'bbox': [95, 188, 227, 272],
# 'corners': [[100.0, 200.0], [220.0, 200.0],
# [220.0, 260.0], [100.0, 260.0]]}, ...]
spacial.obb("car_001")
# single-entry list for that object
Each entry: {"id": str, "class": str, "bbox": [x1, y1, x2, y2], "corners": [[x, y], [x, y], [x, y], [x, y]]}.
Corner order is top-left → top-right → bottom-right → bottom-left of the unrotated object; after rotation the points follow the same winding but are in their rotated canvas positions.
spacial.seg(obj_id=None)
Return segmentation annotations. The mask is a flat list of 0/255 values in row-major order matching the full canvas size.
entries = spacial.seg()
entries[0]["mask_size"] # (1024, 1024)
# Convert to numpy:
import numpy as np
mask = np.array(entries[0]["mask"]).reshape(*entries[0]["mask_size"][::-1])
Each entry: {"id", "class", "bbox", "mask": list[int], "mask_size": (w, h)}.
spacial.save(path)
Save the current canvas to disk. Format is inferred from the file extension (.png, .jpg, .webp, etc.).
spacial.save("dataset/frame_001.png")
spacial.rm()
Clear the canvas and all placed objects. Shape templates are preserved so you can immediately start placing again without re-registering shapes.
spacial.rm()
rm() vs new() — which to use?
rm() |
new() |
|
|---|---|---|
| Clears image | ✓ | ✓ |
| Clears placed objects | ✓ | ✓ |
| Clears shape templates | — | ✓ |
| Use when… | Generating multiple images with the same shapes | Starting completely fresh |
# Efficient: define shapes once, render many images
spacial.init(w=512, h=512)
spacial.shape("dot", w=20, h=20)
spacial.shape_add("dot", "circle", fill="red", xpos=0, ypos=0, width=20, height=20)
for i in range(50):
spacial.rm() # keep "dot" template
spacial.background("color", fill=(0, 0, 0))
spacial.append(f"d{i}", "dot", x=i * 10, y=100)
spacial.save(f"out_{i}.png")
# Clean slate: start fresh each time
for scene in scenes:
spacial.new() # shapes must be re-registered
build_scene(scene)
spacial.save(scene.path)
Z-layering
Every append() call accepts a z parameter. Objects with higher z values are rendered on top. The canvas is re-composited automatically after each append, so the order in which you call append() does not affect the final z-order.
spacial.init(w=640, h=480)
spacial.background("color", fill=(20, 20, 40))
spacial.shape("block", w=100, h=100)
spacial.shape_add("block", "rectangle", fill=(200, 80, 0),
xpos=0, ypos=0, width=100, height=100)
# Append the foreground object first, background second —
# z-order still produces correct layering
spacial.append("fg", "block", x=100, y=100, z=2) # on top
spacial.append("bg", "block", x=130, y=130, z=0) # behind
Rotation
Rotation can be applied at two levels:
Per-primitive (inside shape_add): rotates a single element within the shape template around its own centre. Other elements in the same template are unaffected.
spacial.shape("crosshair", w=60, h=60)
spacial.shape_add("crosshair", "rectangle", fill="#FF0000",
xpos=0, ypos=27, width=60, height=6) # horizontal bar
spacial.shape_add("crosshair", "rectangle", fill="#FF0000",
xpos=27, ypos=0, width=6, height=60, rotation=0) # vertical bar
spacial.shape_add("crosshair", "circle", fill="#FFFFFF",
xpos=22, ypos=22, width=16, height=16)
Per-object (on append): rotates the entire rendered shape around its centre when compositing onto the canvas. The oriented bounding box returned by obb() reflects this rotation.
spacial.append("badge_tilted", "badge", x=200, y=100, rotation=30, scale=1.2)
print(spacial.obb("badge_tilted"))
# corners will reflect the 30° rotation
Scale
The scale parameter on append() applies a uniform scale to the whole rendered shape before compositing. x/y still refer to the pre-scale top-left corner; the object expands outward from there, then is rotated around its scaled centre.
spacial.append("big", "badge", x=50, y=50, scale=2.0)
spacial.append("small", "badge", x=300, y=50, scale=0.5)
spacial.append("normal","badge", x=50, y=250, scale=1.0) # default
New primitives
triangle
Defined by exactly three (x, y) vertices relative to the template origin. xpos/ypos act as an additional offset applied to all points.
spacial.shape("warning", w=80, h=70)
spacial.shape_add("warning", "triangle", fill="#FFD700",
points=[(40, 0), (80, 70), (0, 70)])
spacial.shape_add("warning", "text", fill="#000000",
xpos=30, ypos=24, text="!", font_size=28)
polygon
Defined by three or more (x, y) vertices. Any convex or concave shape is supported.
# Hexagon
spacial.shape("hex", w=80, h=80)
spacial.shape_add("hex", "polygon", fill="#8844EE",
points=[(40,0),(80,20),(80,60),(40,80),(0,60),(0,20)])
# Star (10 vertices alternating outer/inner radius)
import math
pts = []
for i in range(10):
angle = math.radians(i * 36 - 90)
r = 40 if i % 2 == 0 else 16
pts.append((int(40 + r * math.cos(angle)), int(40 + r * math.sin(angle))))
spacial.shape("star", w=80, h=80)
spacial.shape_add("star", "polygon", fill="#FFD700", points=pts)
text
Renders a string using a TrueType font (or Pillow's built-in default). Combine with rectangle to build labels, badges, or overlays.
spacial.shape("tag", w=140, h=36)
spacial.shape_add("tag", "rectangle", fill=(30, 30, 30),
xpos=0, ypos=0, width=140, height=36)
spacial.shape_add("tag", "text", fill="#00FF88",
xpos=8, ypos=6, text="CAR-001",
font_size=20, font_path="/path/to/font.ttf")
Colour reference
Colours can be passed as:
- An RGB tuple:
(255, 80, 0) - A hex string:
"#FF5000"or"FF5000"
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.2.tar.gz.
File metadata
- Download URL: spacial-0.1.2.tar.gz
- Upload date:
- Size: 22.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ecb73059acce872bdb4325c57e445818e0c4d0bfe958e50bed1a079cd485a104
|
|
| MD5 |
e8508eccbc1c997dd61912d8449cbbdc
|
|
| BLAKE2b-256 |
dc117d8c53434580fb0951525998115e22fdfb4df076c5abb75c7f549a045481
|
File details
Details for the file spacial-0.1.2-py3-none-any.whl.
File metadata
- Download URL: spacial-0.1.2-py3-none-any.whl
- Upload date:
- Size: 17.4 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 |
fb89267ebd7aef96c4c611803939127905fc79578ca53aa394753598caa5f221
|
|
| MD5 |
95421311d7e3e1dc51ff0ffc7ed1c7f2
|
|
| BLAKE2b-256 |
8ef9a35456b76d99c76494b0234f5b61a384294d76a060686b90e732bc45e36c
|