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
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/):
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):
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(...)
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; callmodel.warmup()before the first frame (it is idempotent).- The JIT cache is tt-metal's (
~/.cache/tt-metal-cache, orTT_METAL_CACHE); inside the container it is~/.cache/tt-model/sceneseg-p150/cacheof 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 / JPEGbytes, aPIL.Image, a uint8 array or torch tensor in RGB order (HxWx3,3xHxW;HxWgrey is replicated, a 4th channel is dropped), a float array in [0, 1], or att_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 (
bgr8image topic). An OpenCV or ROSbgr8array must be flipped first:model(bgr[:, :, ::-1]). Encoded files and PIL images carry their own channel order. (This is independent ofvariant, 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
/predictimage 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.withcalls 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 andchannel_order, warm variants, warm-up times, runtime parameter defaults, plusgraph(the device graph description),traceandknobs(the build-timeSCENESEG_*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
rgbprofile 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.mdlists 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 enablestest_tt_graph_host.pyandtest_tt_layers_host.py;test_reference_cpu.py(the CPU reference against ONNX Runtime) needsonnxruntime. - 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=rgbfor 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.jsonat 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.