Skip to main content

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 width and height for a perfect circle; different values produce an ellipse.

Note on rotation: rotation rotates the individual primitive around its own centre inside the template. To rotate the whole placed object, use the rotation parameter on append().


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


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.2.tar.gz (22.6 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.2-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

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

Hashes for spacial-0.1.2.tar.gz
Algorithm Hash digest
SHA256 ecb73059acce872bdb4325c57e445818e0c4d0bfe958e50bed1a079cd485a104
MD5 e8508eccbc1c997dd61912d8449cbbdc
BLAKE2b-256 dc117d8c53434580fb0951525998115e22fdfb4df076c5abb75c7f549a045481

See more details on using hashes here.

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

Hashes for spacial-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 fb89267ebd7aef96c4c611803939127905fc79578ca53aa394753598caa5f221
MD5 95421311d7e3e1dc51ff0ffc7ed1c7f2
BLAKE2b-256 8ef9a35456b76d99c76494b0234f5b61a384294d76a060686b90e732bc45e36c

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