abhishek-gola's picture
added lapsrn an macbeth onnx (#16)
1387a61
|
Raw History Blame Contribute Delete
5.94 kB
# LapSRN x4
Single-image super-resolution with a Laplacian Pyramid Super-Resolution Network. One forward
pass produces both a 2x and a 4x image, because the pyramid reconstructs each scale from the
previous one and both intermediate results are exposed as outputs.
The model was originally distributed as a frozen TensorFlow graph (`LapSRN_x4.pb`, the model
used by opencv_contrib's `dnn_superres` module) and converted to ONNX for use with OpenCV's
DNN module.
## Model Details
- **Architecture**: LapSRN β€” Laplacian pyramid, progressive upsampling with `DepthToSpace`
and recursive residual blocks (24 `Conv`, 4 `DepthToSpace`)
- **Input**: `IteratorGetNext:0`, **NHWC** `[1, H, W, 1]` β€” a single **luma (Y)** channel,
float32 normalized to `[0, 1]`. `H`/`W` are dynamic; the network is fully convolutional.
- **Outputs** (both produced in one pass, NCHW):
- `NCHW_output_2x` β€” `[1, 1, H*2, W*2]`
- `NCHW_output_4x` β€” `[1, 1, H*4, W*4]`
- **Framework**: ONNX (converted from the TensorFlow frozen graph via tf2onnx, opset 13)
- **Original weights**: https://github.com/fannymonori/TF-LapSRN/tree/master/export
- **Paper**: Lai, Huang, Ahuja, Yang, *Deep Laplacian Pyramid Networks for Fast and Accurate
Super-Resolution*, CVPR 2017 β€” https://arxiv.org/abs/1704.03915
> **Note on layout.** The input is NHWC while the outputs are NCHW (hence their names). This
> is not a mistake in the conversion β€” it is what `dnn_superres` feeds and reads, so the
> conversion preserves it. Do not use `blobFromImage` here; it would produce NCHW input.
### Colour handling
The network only sees the Y channel. To get a colour image, the demos follow exactly what
`dnn_superres` does (`preprocess_YCrCb` / `reconstruct_YCrCb`):
1. Convert BGR to YCrCb, scale to float `[0, 1]`.
2. Feed **only Y**, reshaped to `[1, H, W, 1]`.
3. Bicubically upscale Cr and Cb by the same factor.
4. Merge `(Y_hr, Cr_hr, Cb_hr)`, scale back to 8-bit, convert YCrCb to BGR.
## Usage
### Python
```bash
python demo.py --model lapsrn_x4_2026sep.onnx --image example_outputs/input_image.png --output-dir example_outputs
```
Writes `output_image_2x.png` and `output_image_4x.png`. Pass `--scale 2` or `--scale 4` to
run just one head.
### C++
The C++ demo runs inference with OpenCV's DNN module (default engine β€” no ONNX Runtime
needed). Adjust the OpenCV paths to your setup:
```bash
OCV=/path/to/opencv # OpenCV source tree
OCVBUILD=/path/to/opencv/build # OpenCV build directory (generated headers + libs)
g++ -std=c++17 demo.cpp -o demo \
-I$OCV/include \
-I$OCV/modules/core/include \
-I$OCV/modules/dnn/include \
-I$OCV/modules/imgproc/include \
-I$OCV/modules/imgcodecs/include \
-I$OCVBUILD \
-L$OCVBUILD/lib -Wl,-rpath,$OCVBUILD/lib -lopencv_dnn -lopencv_imgcodecs -lopencv_imgproc -lopencv_core
./demo --model lapsrn_x4_2026sep.onnx --image example_outputs/input_image.png --output-dir example_outputs
```
The Python and C++ demos agree to within 1–2 intensity levels on under 1% of pixels β€” NumPy
and OpenCV break rounding ties differently, which the YCrCb to BGR conversion mildly amplifies.
### With the dnn_superres module
This is the same file the module's own test loads, so it also works through the module API:
```cpp
DnnSuperResImpl sr;
sr.readModel("lapsrn_x4_2026sep.onnx");
sr.setModel("lapsrn", 4);
std::vector<Mat> outputs;
sr.upsampleMultioutput(img, outputs, {2, 4}, {"NCHW_output_2x", "NCHW_output_4x"});
```
## Conversion
Exported from the frozen TensorFlow graph with tf2onnx (opset 13) via
[convert_to_onnx.py](./convert_to_onnx.py). Requires `tensorflow`, `tf2onnx` and `onnx`.
```bash
python convert_to_onnx.py --pb LapSRN_x4.pb
```
Three details are specific to this model and are handled by the script:
- **The weights are stored quantized and have to be folded back to float.** The frozen graph
went through TensorFlow's `quantize_weights` transform, so each of the 20 weight tensors is
a `quint8` const plus a min/max pair, reconstituted at runtime by a `Dequantize` node in
`MIN_FIRST` mode. tf2onnx has no mapping for `Dequantize`, so the script evaluates those
nodes in a TF session and rewrites each one as a plain float32 const. This is why the ONNX
(2.7 MB) is about 4x the size of the frozen graph (703 KB).
- **The graph is pruned to the two heads that are used.** The frozen graph carries a third
head, `NCHW_output`, that nothing reads. `extract_sub_graph` on `NCHW_output_2x` /
`NCHW_output_4x` drops it β€” that head and its `perm` const are the only two nodes removed,
so no compute changes (329 to 327 nodes).
- **The output names are stripped of their `:0` suffix.** tf2onnx names ONNX outputs after TF
tensors, so the heads come out as `NCHW_output_2x:0`, and
`net.forward("NCHW_output_2x")` cannot resolve that. The script renames them after
conversion.
Note that `shape_override` uses `None`, not `-1`, for the dynamic H/W dims: the frozen graph's
placeholder has unknown rank, and tf2onnx's shape inference passes the override straight to
`set_shape`, which rejects `-1`.
Re-running the script on `LapSRN_x4.pb` reproduces the shipped model: same opset, node count
and initializers, and **bit-identical inference output**. The only differences are tf2onnx's
auto-generated constant names (`const_fold_opt__611` and friends), whose numbering depends on
graph traversal order.
The input is deliberately left NHWC β€” `--inputs-as-nchw` is *not* used here, unlike the other
`dnn_superres` conversions.
## Example
| | |
|---|---|
| `example_outputs/input_image.png` | 256x256 (`butterfly.png`, the `dnn_superres` test image) |
| `example_outputs/output_image_2x.png` | 512x512, from `NCHW_output_2x` |
| `example_outputs/output_image_4x.png` | 1024x1024, from `NCHW_output_4x` |
## License
See [LICENSE](./LICENSE) β€” the model is released by the TF-LapSRN author under the
Apache License 2.0.