|
Download code/PYTHON.md from changh95/sceneseg-p150: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/changh95/sceneseg-p150/resolve/main/code/PYTHON.md
- Command line
-
hf download hf://changh95/sceneseg-p150/code/PYTHON.md
-
curl -L -o PYTHON.md https://huggingface.co/changh95/sceneseg-p150/resolve/main/code/PYTHON.md
12.6 kB
| # Python API: SceneSeg (Autoware VisionPilot) on Blackhole | |
| Use this API from Python code (a pipeline, a notebook, a ROS 2 node wrapper). You do not need the HTTP server: the | |
| API and the server share the decoders, the device graph and the post-processing, so the outputs and the speed are | |
| the same. | |
| ## Install | |
| Install the package on top of an environment that already has `ttnn` (a tt-metal `python_env` at `44d66500520` | |
| with `patches/tt-metal-eth-dispatch.patch`, or the tt-model container). From the root of the model repository (the | |
| directory that holds `pyproject.toml`, `README.md` and `code/`): | |
| ```bash | |
| pip install -e . # the Python API | |
| pip install -e ".[server,test]" # + the HTTP server and the tests | |
| ``` | |
| The pip project is the repository's top-level `pyproject.toml`; it installs the package from `code/tt_sceneseg` (there is no | |
| `pyproject.toml` inside `code/`, because the container build copies `code/` over the tt-metal tree). | |
| The package carries `tt_sceneseg.ttaw`, the shared code of the Autoware ports to Blackhole (device open, trace runner, | |
| decoders, model base class, HTTP app), vendored at the version recorded in `code/tt_sceneseg/ttaw/VENDORED.json`. | |
| | You want to run | Extras | | |
| |---|---| | |
| | the Python API | none | | |
| | the HTTP server (`tt_sceneseg.server.app`, see `SERVING.md`) | `server` | | |
| | host tests (no device; device tests are skipped): see "Tests" | `server,test` | | |
| | device tests: `python -m pytest -q -s code/tt_sceneseg/tests/test_pcc_device.py code/tt_sceneseg/tests/test_e2e_device.py` | `test` | | |
| ## Quickstart | |
| Run from the repository root (`code/tt_sceneseg/samples/highway_normal_1.png` is a path relative to it): | |
| ```python | |
| from tt_sceneseg import SceneSeg | |
| with SceneSeg.from_pretrained(device_id=0) as model: # weights -> your HF cache, trace captured | |
| out = model("code/tt_sceneseg/samples/highway_normal_1.png") | |
| mask = out.mask # uint8 (H, W) at the input size: 255 = foreground (the VisionPilot topic) | |
| print(out.class_counts()) # pixels per class of out.class_map (background / foreground / road) | |
| print(out.to_dict()["meta"]) # the same JSON as POST /predict | |
| ``` | |
| `examples/quickstart.py` runs this snippet and writes the `/predict` JSON, the mask and a tinted picture. | |
| ## `SceneSeg.from_pretrained(...)` | |
| ```python | |
| SceneSeg.from_pretrained( | |
| model_id=None, # HF repo or a local directory with the weights files; default changh95/sceneseg-p150 (weights/) | |
| *, | |
| revision=None, # default for the default repo: the validated weights commit a6829b246a0 | |
| variant=None, # load-time variant (bgr, rgb); default $SCENESEG_VARIANT or "bgr" | |
| device_id=None, # chip to open; default $TT_DEVICE_ID or 0 | |
| device=None, # an already-opened ttnn device (tt_sceneseg.device.open_device); close() does not close it | |
| dispatch=None, # "eth" (p150 target, 12x10 grid) | "worker" (A/B only, 11x10) | "auto"; default $SCENESEG_DISPATCH or "eth" | |
| num_command_queues=None, # default $SCENESEG_NUM_CQS or 1 | |
| weights_dir=None, # explicit local weights directory; no Hub access | |
| warmup_variants="default", # trace variants to capture now; see "Warm-up" | |
| verbose=False, | |
| **compile_params, # none for this model: the network input is fixed at 640x320 | |
| ) -> SceneSeg | |
| ``` | |
| What it does: resolves the weights first, so a Hub problem never claims the chip (`weights_dir` > | |
| `$SCENESEG_WEIGHTS_DIR` > a local `model_id` directory > the HF snapshot at the pinned revision, restricted to | |
| `weights/SceneSeg_FP32.onnx, weights/provenance.json, weights/LICENSE, weights/NOTICE`, with an offline fallback to the cache), opens the chip (ETH dispatch, 12×10, 1 CQ; | |
| the other open parameters are `DEVICE_DEFAULTS` in `tt_sceneseg/device.py`, overridable with `SCENESEG_*`), builds the | |
| graph of the selected channel order, then compiles and captures the metal trace. If ETH dispatch cannot open | |
| (tt-metal without the patch), it warns and falls back to WORKER dispatch. | |
| `variant` is the **input channel order**, a load-time choice folded into the first conv's weights on the device: | |
| | variant | what it is | | |
| |---|---| | |
| | `bgr` (default) | exactly what VisionPilot's `scene_seg_model` node computes: the deployed C++ pre-processing feeds B, G, R planes (with the ImageNet constants reordered to match) to the network | | |
| | `rgb` | the order the network was trained with (`scene_seg_trainer.py` @ `ca58cb50`): clearly better masks, most of all on two-wheelers (README "Channel order") | | |
| Both run the same device graph and are validated against the fp32 CPU reference of their own order. | |
| ## Warm-up | |
| `from_pretrained` returns a warm model: it builds the graph, runs it once eagerly (this first run compiles the | |
| kernels and prepares the conv weights on the device) and captures the whole forward as ONE metal trace, so no call | |
| compiles anything. There are no shape buckets: every image, whatever its size, is squashed on the host to the same | |
| 640x320 uint8 canvas, so one trace serves every input. | |
| - Measured on the shared host (2026-10-13, `logs/sceneseg/republish/final/`): with the JIT cache warm, a load takes | |
| 18.5-18.7 s (the served app: graph build 11.4 s, which prepares the S1 and K11 weights on the host, and warm-up + | |
| capture 2.8 s). With an empty JIT cache the first load took 275 s (measured before R8.1, which saves about 21 s of | |
| graph build). The first call took 28-30 ms, the later ones 22.5-24 ms on the shipped sample. | |
| - `warmup_variants="none"` (server: `SCENESEG_WARMUP=none`) skips the capture at load; call `model.warmup()` before | |
| the first frame (it is idempotent). | |
| - The JIT cache is tt-metal's (`~/.cache/tt-metal-cache`, or `TT_METAL_CACHE`); inside the container it is | |
| `~/.cache/tt-model/sceneseg-p150/cache` of the host. | |
| ## Call: `model(image, **params)` | |
| | Argument | Type | Description | | |
| |---|---|---| | |
| | `image` (first positional) or `images=[image]` | path, `bytes`, `PIL.Image`, uint8 array / tensor, float array, `CameraImage` | one camera image of any size (see "Input types") | | |
| | `class_map` | bool, default `True` | also encode the 3-class map in `to_dict()` (the counts and `out.class_map` are always there). The node itself has no runtime parameter: its decision is a plain argmax | | |
| ### Input types | |
| - The image (first argument, or `images=[x]` with exactly one entry): a path to a PNG / JPEG file, encoded PNG / JPEG | |
| `bytes`, a `PIL.Image`, a uint8 array or torch tensor in **RGB** order (`HxWx3`, `3xHxW`; `HxW` grey is replicated, | |
| a 4th channel is dropped), a float array in [0, 1], or a `tt_sceneseg.io.CameraImage`. Any size from 2x2 up. | |
| - **Channel order of the input.** Arrays are read as RGB and converted to BGR8, which is what the VisionPilot node | |
| receives (`bgr8` image topic). An OpenCV or ROS `bgr8` array must be flipped first: `model(bgr[:, :, ::-1])`. | |
| Encoded files and PIL images carry their own channel order. (This is independent of `variant`, which selects | |
| the plane order the *network* is fed.) | |
| - The node squashes the image to 640x320 with OpenCV's `cv::resize` (INTER_LINEAR, aspect ratio not kept); the port | |
| reproduces it bit for bit on the host. SceneSeg's Python tooling also had a bicubic resize; it is not offered, | |
| because the deployed node never used it. | |
| - There is no calibration, no camera model and no temporal state: one image per call, as one node instance | |
| subscribes to one image topic. Inline intrinsics or transforms in a `/predict` image entry are accepted by the | |
| shared schema and ignored. | |
| ## Output | |
| `model(...)` returns a `tt_sceneseg.Output` (= `SceneSegMask`, a `ttaw.outputs.Mask2D`); `out.to_dict()` is exactly the | |
| `POST /predict` body (SERVING.md section 3.2). | |
| | field | type | meaning | | |
| |---|---|---| | |
| | `mask` | uint8 `[H, W]` | what the node publishes on `/autoseg/scene_seg/mask`: mono8 at the input's size, 255 where the per-pixel argmax is class 1 (foreground), else 0; resized back from 640x320 with INTER_NEAREST | | |
| | `class_map` | uint8 `[H, W]` | the 3-class argmax on the same grid: 0 background (incl. sky), 1 foreground objects, 2 drivable road (a by-product the node does not publish) | | |
| | `class_names` | tuple | `("background", "foreground", "road")` | | |
| | `meta` | dict | `channel_order`, `source_hw`, `network_hw` `[320, 640]`, `foreground_class` 1, `mask_value` 255, `foreground_fraction` | | |
| | `timing_ms` | dict | `preprocess`, `device` (H2D + trace + D2H), `postprocess`, `total` of this call | | |
| `out.class_counts()` gives the pixels per class of `class_map`; `out.to_dicts()` the same as | |
| `[{"label", "label_id", "pixels"}]`. Ties between classes resolve to the lowest index, as the node's strict `>` scan does. | |
| ## Lifetime and information | |
| - `model.close()` releases the trace and the device tensors and closes the chip if the model opened it; idempotent. | |
| `with` calls it for you; an unclosed model is closed when Python exits. | |
| - `model.info`: model name, weights (repo, revision, path, license), device (`dispatch`, `grid`, `cores`, | |
| `num_command_queues`, `fallback`), variant and `channel_order`, warm variants, warm-up times, runtime parameter | |
| defaults, plus `graph` (the device graph description), `trace` and `knobs` (the build-time `SCENESEG_*` knobs). | |
| - Calls from several threads are safe: the device calls are serialised by a lock. One model per process per chip. | |
| ## Speed | |
| Warm calls, batch 1, ETH dispatch, 1 CQ, 12x10, `bgr`, the published numerics (`code/scripts/bench.py`, 100 | |
| iterations, 2026-10-13 on a shared host; `OPT_BASELINE.md` "After optimization"): | |
| | stage | shipped sample 727x414 PNG | PandaSet 1920x1080 JPEG | | |
| |---|---:|---:| | |
| | `model(path)` end to end, p50 (p99) | 22.54 (23.38) ms | 30.00 (32.01) ms | | |
| | host decode + squash to 640x320 (PIL decode; exact `cv::resize` as a numba twin on 4 threads) | 8.53 ms | 13.75 ms | | |
| | H2D of the uint8 640x320 canvas (320 rows of 1,920 B) | 0.21 ms | 0.34 ms | | |
| | device trace, one forward | 12.63 ms | 12.64 ms | | |
| | D2H (the uint8 class map) | 0.23 ms | 0.30 ms | | |
| | host post-processing (INTER_NEAREST to the source size, numba twin) | 0.62 ms | 3.06 ms | | |
| - Back-to-back replays take 12.58 ms per frame (79.5 frames/s of device throughput); a synchronous call gives | |
| 44.4 frames/s on the sample. The `rgb` profile replays in the same time (e2e p50 22.66 ms on the sample). | |
| - The first release measured 90.85 ms (sample) and 116.12 ms (PandaSet) end to end and 61.06 ms per replay; | |
| `OPT_REPORT.md` lists every step in between. | |
| - A raw RGB array skips the decode. The device path does not depend on the image size; the host stages grow with it. | |
| ## Tests | |
| From the repository root, with `code/` on `PYTHONPATH` (or the package installed): | |
| - Host (no device, no ttnn call): `TT_VISIBLE_DEVICES=none python -m pytest -q | |
| code/tt_sceneseg/tests/test_bundle_host.py code/tt_sceneseg/tests/test_io_host.py | |
| code/tt_sceneseg/tests/test_server_host.py code/tt_sceneseg/tests/test_host_preprocess.py | |
| code/tt_sceneseg/tests/test_host_postprocess.py code/tt_sceneseg/tests/test_reference_host.py`. Inside the | |
| tt-models workspace the suite runs on its fake ttnn (`-p fake_ttnn_plugin`), which also enables | |
| `test_tt_graph_host.py` and `test_tt_layers_host.py`; `test_reference_cpu.py` (the CPU reference against ONNX | |
| Runtime) needs `onnxruntime`. | |
| - Device (one p150): `python -m pytest -q -s code/tt_sceneseg/tests/test_pcc_device.py | |
| code/tt_sceneseg/tests/test_e2e_device.py` (per-module PCC against the fp32 CPU reference, end-to-end agreement on | |
| the shipped tutorial and comma10k samples, replay == eager, API == `/predict`, the stored-reference smoke gates; | |
| `SCENESEG_VARIANT=rgb` for the other order). The public-frame gates (23 frames with logits, 415 held-out frames) | |
| run only inside the tt-models workspace, where their data live: PandaSet, nuScenes and KITTI frames, of which | |
| nuScenes and KITTI may not be redistributed. There, a missing input fails the test instead of skipping it. | |
| - The gates are frozen in `tests/*.gates.json` at the first green run and are never loosened. | |
| ## Limits | |
| - Batch 1 on the chip; one model per process; one image per call. | |
| - The network input is fixed at 640x320 (the SceneContext reshape hard-codes the 10x20 stride-32 grid, batch 1); any | |
| source image size is accepted, squashed on the host without keeping the aspect ratio, and the mask is resized back | |
| with nearest neighbour. | |
| - Only `SceneSeg_FP32.onnx`, the network the node deploys. The INT8 QAT ONNX (never deployed; it disagrees with FP32) | |
| and SceneSegLite (a different 19-class network) are not supported. | |