Metadata-Version: 2.4
Name: spacial
Version: 0.1.2
Summary: A lightweight synthetic dataset image generation library built on top of Pillow.
Author: Spacial Contributors
License: MIT
Project-URL: Homepage, https://github.com/your-org/spacial
Project-URL: Documentation, https://github.com/your-org/spacial/blob/main/docs.md
Project-URL: Issues, https://github.com/your-org/spacial/issues
Keywords: synthetic data,image generation,dataset,bounding box,segmentation,pillow
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Scientific/Engineering :: Image Processing
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: dev

# 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

```bash
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

```python
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. |

```python
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.

```python
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**

```python
# 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. |

```python
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**

```python
# 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=`. |

```python
# 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.

```python
# 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.

```python
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.

```python
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.

```python
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.).

```python
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.

```python
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 |

```python
# 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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
# 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.

```python
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"`
