Metadata-Version: 2.4
Name: spacial
Version: 0.1.0
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

> Lightweight synthetic dataset image generation — powered by Pillow.

[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-0.1.0-orange.svg)]()

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](#installation)
- [Quick Start](#quick-start)
- [Design Philosophy](#design-philosophy)
- [API Reference](#api-reference)
  - [init](#init)
  - [background](#background)
  - [shape / shape_add](#shape--shape_add)
  - [append](#append)
  - [bbox](#bbox)
  - [seg](#seg)
  - [save](#save)
  - [rm](#rm)
- [Fill System](#fill-system)
- [Examples](#examples)
- [Exporting Annotations](#exporting-annotations)
- [Future Roadmap](#future-roadmap)

---

## Installation

```bash
pip install spacial
```

Spacial requires **Python 3.9+** and **Pillow ≥ 10.0**.

---

## Quick Start

```python
import spacial

# 1. Set up 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. 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]},
# ]

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

---

## Design Philosophy

Spacial is deliberately narrow. It does exactly three things:

1. **Generate images** — backgrounds, shapes, composited objects.
2. **Generate bounding box annotations** — pixel-aligned `[x1, y1, x2, y2]`.
3. **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, because it only returns standard Python lists and dicts.

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

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

Initialise Spacial and create a blank canvas.

| Parameter | Type  | Default | Description                                          |
|-----------|-------|---------|------------------------------------------------------|
| `device`  | `str` | `"cpu"` | Compute device: `"cpu"`, `"cuda"`, or `"mps"`. `cuda` and `mps` are reserved for future GPU acceleration and currently behave identically to `cpu`. |
| `w`       | `int` | `1024`  | Canvas width in pixels.                              |
| `h`       | `int` | `1024`  | Canvas height in pixels.                             |

---

### `background`

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

Fill the entire canvas with a background.

| Parameter | Type             | Description                                              |
|-----------|------------------|----------------------------------------------------------|
| `bg_type` | `str`            | `"color"`, `"gradient"`, `"noise"`, `"perlin"`, `"img"` |
| `fill`    | colour or `dict` | Colour value or fill spec (see [Fill System](#fill-system)) |
| `path`    | `str \| None`    | Path to source image — required for `bg_type="img"`.    |
| `seed`    | `int \| None`    | Random seed for reproducible noise/perlin backgrounds.  |

**Examples:**

```python
# Solid colour
spacial.background("color", fill=(30, 30, 30))
spacial.background("color", fill="#1A1A2E")

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

# Seeded noise
spacial.background("noise", fill={
    "type": "noise",
    "base": (100, 100, 100),
    "scale": 0.4,
}, seed=42)

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

# From an existing image file
spacial.background("img", path="sky.jpg")
```

---

### `shape` / `shape_add`

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

Define a reusable shape template by stacking primitives.

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

**Example:**

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

```python
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 any 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. If `obj_class` matches a registered shape name → render that template.
2. If `obj_class == "img"` → load the image at `path=`.
3. Otherwise → render a placeholder rectangle using `fill=`, `w=`, `h=`.

**Examples:**

```python
# Named shape
spacial.append("tl_north", "traffic_light", x=100, y=50)

# Inline image
spacial.append("sponsor_logo", "img", path="logo.png", x=20, y=20)

# Placeholder (useful for layout testing)
spacial.append("unknown_001", "unknown", x=300, y=150, fill="#AAAAAA", w=80, h=80)
```

---

### `bbox`

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

Return bounding-box annotations.

```python
[
    {
        "id":    "car_001",
        "class": "car",
        "bbox":  [x1, y1, x2, y2]   # pixel coordinates, inclusive corners
    },
    ...
]
```

Pass `obj_id="car_001"` to retrieve a single object's annotation.

---

### `seg`

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

Return segmentation annotations.

```python
[
    {
        "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 the mask to a NumPy array** (NumPy is not a Spacial dependency, but it is easy to integrate):

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

```python
spacial.save(path)
```

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

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

---

### `rm`

```python
spacial.rm()
```

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

---

## Fill System

Wherever a `fill` parameter is accepted, Spacial understands two notations:

**Solid colour**

```python
fill=(255, 99, 71)      # RGB tuple
fill="#FF6347"          # hex string (with or without #)
```

**Gradient**

```python
fill={
    "type":      "gradient",
    "start":     "#FF6B6B",     # any colour value
    "end":       "#4ECDC4",
    "direction": "horizontal",  # or "vertical"
}
```

**Noise** (uniform random per-pixel offsets from a base colour)

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

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

---

## Examples

### Minimal YOLO-style loop

```python
import json
import spacial

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

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

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

    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

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

# Define classes
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)  # head
spacial.shape_add("pedestrian", "rectangle", fill="#F4A261", x0=4, y0=12, x1=16, y1=50)  # body

# Populate scene
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)

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

### Pasting real images with segmentation

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

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

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

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

Spacial is intentionally minimal today. Planned additions in future releases:

- **Shape nesting** — embed one named shape inside another to build hierarchical objects.
- **Transforms** — per-object rotation, scaling, and opacity.
- **GPU acceleration** — real CUDA/MPS paths for faster noise generation at high resolutions.
- **Z-ordering** — explicit depth control for overlapping objects.
- **Polygon segmentation** — return polygon contours in addition to binary masks.
- **Single-object annotation queries** — `spacial.bbox("car_001")` already supported; will expand.
- **Text primitive** — render text labels directly onto shapes.
- **Physics-based placement** — non-overlapping random placement helpers.
- **Built-in augmentations** — optional blur, brightness jitter, and crop directly in Spacial.

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

---

## License

MIT © Spacial Contributors
