Metadata-Version: 2.4
Name: spacial
Version: 0.1.1
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)
  - [effect](#effect)
  - [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
```

Requires **Python 3.9+** and **Pillow ≥ 10.0**.

---

## Quick Start

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

1. **Generate images** — backgrounds, shapes, composited objects.
2. **Apply effects** — per-pixel filters on the full canvas or a single object region.
3. **Generate bounding box annotations** — pixel-aligned `[x1, y1, x2, y2]`.
4. **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(...)` 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. 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`

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

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

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

```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 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:**
1. Matches a registered shape name → renders that template.
2. `"img"` → loads the image at `path=`.
3. Anything else → renders a placeholder rectangle using `fill=`, `w=`, `h=`.

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

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

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

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

Return bounding-box annotations.

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

```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 to a NumPy array** (NumPy is not a Spacial dependency, but 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. 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 preserved so you can reuse them in the next frame.

---

## Fill System

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

**Solid colour**
```python
fill=(255, 99, 71)   # RGB tuple
fill="#FF6347"       # hex string
```

**Gradient**
```python
fill={
    "type":      "gradient",
    "start":     "#FF6B6B",
    "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)

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

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

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

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

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

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
