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

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:

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:

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. Requires tensorflow, tf2onnx and onnx.

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 β€” the model is released by the TF-LapSRN author under the Apache License 2.0.