File size: 5,939 Bytes
1387a61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# 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.