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