VNet 2D retinal layer β ONNX (browser/WebGPU)
4-class retinal layer semantic segmentation from H&E eye sections. VNet 2D from keras-unet-collection 0.1.13, trained in Keras 2.15. Exported to ONNX for client-side inference with
onnxruntime-web + WebGPU, for the JAX Image Tools viewer.
| file | size | notes |
|---|---|---|
model.fp16w.onnx |
~590 MB | fp16 weights, fp32 compute β use this |
model.json |
β | input shape, class count, preprocessing |
I/O
Single graph, batch 1, static shapes, NHWC throughout. Softmax is inside the graph, so the client only has to argmax.
| tensor | shape | meaning |
|---|---|---|
input |
(1, 512, 512, 1) |
1 (grayscale β mean of RGB) |
vnet_output_activation |
(1, 512, 512, 4) |
per-pixel class probabilities |
Input scaling: 1/255
This is the single easiest thing to get wrong. Nothing in the graph records
it and the wrong value does not raise β the model just predicts one class over
the entire image. It is not even consistent across this family: the ResUNet-a
retinal-layer checkpoints want a caffe mean-subtraction, the VNet one wants 0β1. This model
wants 1/255; model.json records it under preprocess.scale.
Use
import * as ort from 'onnxruntime-web';
const meta = await (await fetch(`${base}/model.json`)).json();
const session = await ort.InferenceSession.create(`${base}/model.fp16w.onnx`, {
executionProviders: ['webgpu'],
});
const out = await session.run({ input: tensor }); // NHWC
// argmax over the last axis -> label map
The graph keeps Keras's NHWC layout, which is the layout canvas pixel data
is already in, so preprocessing needs no transpose. Softmax is inside the graph.
Reference preprocess / argmax / colorize helpers are in
js/segmentation-postprocess.js.
There is no WASM fallback. onnxruntime-web's WASM EP dies with
std::bad_allocon this model at every precision. WebGPU is the only way it runs in a browser, so callers need a real unsupported-browser branch.
Validation
The ONNX graph reproduces the original Keras model at cosine 1.000000 with
100% argmax agreement. mIoU 0.9054 against the held-out masks shipped alongside the checkpoint, with predicted class areas tracking ground truth closely ([145253, 56522, 51745, 8624] vs [148594, 55844, 48880, 8826]). On WebGPU the fp16w label map agrees with the fp32 reference to 99.9996% at an identical mIoU, in ~536 ms per 512Β² tile (M1 Max).
Why fp16w and not a normal fp16/int8 quantization
model.fp16w.onnx stores weights as fp16 and casts them back to fp32, so
every kernel still runs in fp32. It halves the download without touching the
arithmetic.
That indirection is necessary because onnxruntime-web's WebGPU EP accumulates natively in f16, while onnxruntime's CPU/WASM backends quietly upcast to fp32 β so a genuinely fp16 graph validates cleanly offline and then degrades in a browser, silently. Measured on ResUNet-a 2D, agreement with the fp32 label map:
| variant | size | CPU | WebGPU |
|---|---|---|---|
| fp16 weights / fp32 math | 1Γ | 99.995% | 99.9947% |
| true fp16 | 1Γ | 99.990% | 87.10% |
| static int8 QDQ | 0.5Γ | 99.296% | 26.83% |
Unchanged on onnxruntime-web 1.27.0. Always diff a new model against a CPU reference in a browser β the WebGPU EP returns wrong numbers rather than raising.
License / provenance
Weights Β© The Jackson Laboratory. Architecture from keras-unet-collection 0.1.13 (MIT), trained with Keras 2.15 / Python 3.10.
Reproducing the export needs Python 3.10 β keras-unet-collection builds its ASPP resize steps as
Lambdalayers, which Keras 2 serialises as marshalled CPython bytecode; that is not portable across Python minor versions, so 3.11+ fails withbad marshal data (unknown type code).
Export tooling: browser-onnx-tools (export/export_keras_onnx.py), which also applies the WebGPU graph fixes described below.