--- license: mit library_name: litert pipeline_tag: image-segmentation tags: - litert - tflite - android - on-device - gpu - drivable-area - lane-segmentation - adas - autonomous-driving - real-time --- # TwinLiteNet — Drivable-area + lane segmentation (LiteRT GPU) On-device **drivable-area and lane-line segmentation** running **fully on the LiteRT `CompiledModel` GPU** delegate (no CPU fallback). [TwinLiteNet](https://github.com/chequanghuy/TwinLiteNet) (2023) is an ultra-light ESPNet-based network with two segmentation heads — the ADAS perception building block "where can I drive" + "where are the lanes". Only **3.1 MB**, ~44 ms/frame on a Pixel 8a. - **Architecture:** ESPNet-C encoder + two seg decoders — pure CNN. - **Weights:** [chequanghuy/TwinLiteNet](https://github.com/chequanghuy/TwinLiteNet) (BDD100K) · MIT. - **Size:** 3.1 MB. ![TwinLiteNet drivable area + lanes](hero.png) *Drivable area (green) + lane lines (red) on a dashcam highway frame. Source: Wikimedia Commons (Public Domain).* ## I/O - **Input:** `[1, 3, 360, 640]` NCHW, RGB, `x/255`. - **Outputs:** two `[1, 2, 360, 640]` logit maps — `drivable_area` and `lane_line`. Take `argmax` over the class dim (2) → binary masks. ## GPU conversion TwinLiteNet is a pure CNN. It converts fully GPU-compatible (**270/270 nodes on the delegate, 1 partition**; device corr 0.99997 / 0.99998 on the two heads, ~44 ms) with **one patch**: the `ConvTranspose2d` upsamplers → **ZeroStuffConvT2d** (nearest-upsample + stride zero-stuff mask + flipped conv; the Mali delegate rejects `TRANSPOSE_CONV`). Exact. CPU-exact vs PyTorch (corr 1.0). ## Minimal usage ### Kotlin (Android, LiteRT CompiledModel GPU) ```kotlin val options = CompiledModel.Options(Accelerator.GPU) val model = CompiledModel.create(context.assets, "twinlite.tflite", options, null) val inBufs = model.createInputBuffers() val outBufs = model.createOutputBuffers() // [0] = drivable area, [1] = lane line inBufs[0].writeFloat(inputNCHW) // [1,3,360,640] RGB, x/255 model.run(inBufs, outBufs) val da = outBufs[0].readFloat() // [2*360*640]; argmax over the 2 classes -> drivable mask val ll = outBufs[1].readFloat() // [2*360*640]; argmax -> lane mask // per pixel p: class = if (da[p] > da[360*640 + p]) 0 else 1 ``` ### Python (LiteRT / ai-edge-litert) ```python import numpy as np from ai_edge_litert.interpreter import Interpreter it = Interpreter(model_path="twinlite.tflite"); it.allocate_tensors() inp, out = it.get_input_details(), it.get_output_details() it.set_tensor(inp[0]["index"], x) # [1,3,360,640] float32, RGB, x/255 it.invoke() outs = sorted(out, key=lambda o: o["index"]) da = it.get_tensor(outs[0]["index"])[0].argmax(0) # [360,640] drivable-area mask ll = it.get_tensor(outs[1]["index"])[0].argmax(0) # [360,640] lane mask ``` ## Conversion Converted with **litert-torch** (`build_twinlite.py`): loads the MIT BDD100K weights, swaps `ConvTranspose2d` → ZeroStuffConvT2d, and exports the two-head graph. ## License MIT (TwinLiteNet / chequanghuy). Trained on BDD100K.