File size: 8,490 Bytes
8f40b86
 
 
 
 
 
 
 
 
 
 
 
0483bf4
 
8f40b86
 
 
 
 
968f908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f6ea97
 
968f908
 
 
 
 
 
 
 
 
 
 
2f6ea97
0483bf4
 
 
 
 
968f908
 
 
 
 
 
 
 
 
 
 
 
 
0483bf4
 
968f908
 
 
 
 
 
 
 
2f6ea97
968f908
 
 
 
 
 
 
 
 
 
 
 
0483bf4
 
968f908
 
 
 
0483bf4
 
968f908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0483bf4
968f908
 
8e3b8cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
---
title: Splasher
emoji: 💦
colorFrom: indigo
colorTo: blue
sdk: docker
app_port: 7860
pinned: false
license: mit
short_description: Label synchronized 3D/image channels into a 2D BEV grid
---

# Splasher

> **Live demo** — this Hugging Face Space runs `splasher demo --serve` (the synthetic
> source) inside a Docker container. The 3D cloud, camera and BEV grid are all interactive.
> Note: the demo serves a **single shared session**, so concurrent visitors edit the same
> grid. See the [GitHub repo](https://github.com/augustin-bresset/splasher) to run it locally.

A **labeling** tool with a generic core: you give it a *synchronous dataset* — at each
timestamp, a **pack of named channels** (3D point cloud, camera image, pose, …) — and you
label either a **top-down 2D grid (BEV)**, the **3D points** directly, or both.

First use case: **traversability**. But nothing is hard-wired: no imposed class schema, no
imposed world semantics, and **no mandatory dependency on any dataset format**. apairo is
just one optional input adapter.

## Idea

- Several synchronized channels, shown as references: you roam freely in the 3D cloud and
  look at the camera images.
- You choose which **channels** to display (the *Channels* dock: show/hide each cloud or
  camera available in the source — multiple cameras and clouds supported).
- You **draw the grid of cells** (top-down view): its extent and cell size, created
  explicitly via **"New grid"**. Undo is **per frame**.
- You **drag a rectangle** with the mouse on the top-down view. Depending on the target:
  - **Grid**: fills the covered cells with the active class (output = raster of IDs).
  - **Points**: assigns the class to the 3D points in the rectangle (output = per-point labels).
- Interaction: **left-drag = apply**, **right-drag = erase / deselect**,
  **Shift-drag or middle-button = pan**, **wheel = zoom**. An **ego marker** (X = forward,
  Y = left) is drawn at the origin in both the BEV and the 3D views, using the same axis
  convention so the two read consistently.
- **Selection** mode (desktop-style): left-drag adds cells to the selection, right-drag
  removes them (non-contiguous selections allowed), then you **apply** the class to the whole
  selection at once. Changing the grid asks for **confirmation** if a labeling already exists.
- **Accumulation**: you can accumulate ±N frames **registered by their poses** into the
  current frame's frame of reference (a denser cloud helps labeling). The grid and labels
  stay **per frame**: a brush stroke on the accumulated cloud is **de-accumulated** back to
  each source frame. (Requires a `POSE` channel.)

## Architecture (swappable GUI)

Layers from generic to specific — each depends only on the previous one:

```
splasher/
  core/      pure numpy model (grid, label targets, BEV projection, accumulation…)
  engine/    headless Session: all the state + operations, with no UI dependency.
             Returns a *semantic* ViewState (points + per-point labels + channel,
             BEV field, grid raster, selection, images) — not pixels.
  server/    FastAPI backend on the same Session + serves the web front; desktop app.
  web/       web front (vanilla, zero build) — the ONLY front (packaged in the wheel).
    vendor/  Three.js hosted locally (offline).
```

**One front, one engine.** The web front is served by the backend; the desktop app
(`splasher` without `--serve`) opens that same front in a **native webview** (pywebview,
Spotify/Electron style) — so the desktop *is* the web front, in a window.

The `Session` (`splasher.engine.Session`) is the flexibility point: it draws nothing, it
exposes `view_state()` + commands (`paint_rect`, `select_rect`, `apply_selection`,
`set_frame`, `set_accum_radius`, `commit_grid`, `save`/`load`, …). Plugging in another front
= consuming this `Session` (the web does it over HTTP). **Colorization stays on the front
side**: each front draws the primitives its own way (reference helpers in
`splasher.engine.render`, mirrored in JS under `splasher/web/src`).

## Installation

```bash
cd ~/dev/splasher
uv sync                 # core + engine only (numpy)
uv sync --extra app     # + desktop app (FastAPI + uvicorn + pywebview)
uv sync --extra api     # + headless web/API server (FastAPI + uvicorn)
uv sync --extra apairo  # + apairo adapter (optional)
```

## Demo (zero external data)

```bash
uv run python examples/demo_arraysource.py   # desktop app (native window, `app` extra)
splasher                                      # empty: file-viewer mode (browse + open files)
splasher demo                                 # synthetic dataset
splasher demo --serve                         # web server → http://127.0.0.1:8077 (`api` extra)
```

**File viewer**: launched empty, *Open file…* browses the filesystem and opens individual
point clouds (`.npy`/`.bin`/`.pcd`) and images into resizable views, with a clear error for
unreadable / unsupported files. Independent from the dataset/labeling mode.

Dark brutalist black-&-blue design. Labelable top-down (BEV) view (underlay mode:
height / density / intensity) + **3D/camera panels you can add, resize, and each bind to a
channel** (3D color-by: height / intensity). Sensor placements (from `ChannelSpec.placement`)
are drawn as markers in 3D. Classes are fully editable (⚙ in the *Classes* panel). Three.js
is hosted locally (`splasher/web/vendor`) → works offline.

> Native desktop window: the `app` extra ships a Qt WebEngine (Chromium) backend, so
> `uv sync --extra app && splasher demo` opens a real native window out of the box. If no
> backend can start, it falls back to the default browser and keeps serving.

## Input

The core consumes a `Source`: `__len__`, `__getitem__(i) -> Frame`, `channels()`.
`ArraySource` builds one from in-memory numpy arrays. `ApairoSource` (`apairo` extra) wraps
any synchronous apairo dataset.

### apairo

```bash
uv sync --extra apairo --extra app                     # adapter + desktop
splasher /path/to/dataset --adapter apairo             # all channels
splasher /path/to/dataset --adapter apairo --channels lidar,cam_front,pose   # only these
splasher /path/to/dataset --adapter apairo --reference lidar --tolerance 0.05  # sync an async dataset
```

Two ways to pick what you work on: **load everything** (a synchronous dataset) and choose
what to *display* in the UI (the *Clouds (BEV)* toggles + **Add view** per channel), or
**select channels at load time** with `--channels a,b,c`. An asynchronous dataset needs
`--reference <channel>` (and optionally `--tolerance`) to be synchronized first.

## API (back/front mode)

`splasher demo --serve [--host H --port P]` starts a FastAPI server driven by the same
`Session` as the desktop app. Main endpoints (each command returns the updated `ViewState`
so a front renders in a single round-trip):

| Method | Route | Purpose |
|--------|-------|---------|
| `GET`  | `/api/session` | ~static description (channels, classes, n_frames) |
| `GET`  | `/api/view`    | current render state |
| `POST` | `/api/frame`, `/api/class`, `/api/tool`, `/api/targets`, `/api/accum`, `/api/visibility` | settings |
| `POST` | `/api/paint`, `/api/select`, `/api/selection/apply`, `/api/selection/clear`, `/api/clear`, `/api/undo` | labeling |
| `POST` | `/api/grid`, `/api/save`, `/api/load` | grid & I/O |

numpy arrays travel as `{dtype, shape, data(base64)}` (`splasher.server.protocol`), decodable
directly into a `TypedArray` on the JavaScript side. Interactive docs at `/docs`.

## File-viewer mode

Launched empty (`splasher`), *Open file…* browses the filesystem (type a path, **Tab** to
complete) and opens point clouds (`.npy`/`.bin`/`.pcd`) and images (`.png`/`.jpg`/… or
`.npy` `HxWxC`) into resizable views. The **Clouds (BEV)** panel selects which open clouds
feed the BEV (multiple = combined), with a color mode (height/intensity/normal). The BEV
grid and its labels are **independent** of the displayed cloud — switching/combining clouds
never wipes them. **Export** writes the grid raster to a single `.npy` (default name
`<cloud>_bev.npy`); **Save** writes a full session folder.

## Development

```bash
uv sync --extra api          # core + engine + server (for the test suite)
uv run --extra api pytest -q # run the tests
```

The core/engine import without any UI dependency; tests are pure-numpy + FastAPI's
`TestClient`. CI runs the suite on Python 3.11 and 3.12 (`.github/workflows/ci.yml`).