jcm/models/up_or_down_sampling.py (210 lines of code) (raw):

# coding=utf-8 # Copyright 2020 The Google Research Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: skip-file """Layers used for up-sampling or down-sampling images. Many functions are ported from https://github.com/NVlabs/stylegan2. """ import flax.linen as nn from typing import Any, Tuple, Optional, Sequence import jax import jax.nn as jnn import jax.numpy as jnp import numpy as np # Function ported from StyleGAN2 def get_weight(module, shape, weight_var="weight", kernel_init=None): """Get/create weight tensor for a convolution or fully-connected layer.""" return module.param(weight_var, kernel_init, shape) class Conv2d(nn.Module): """Conv2d layer with optimal upsampling and downsampling (StyleGAN2).""" fmaps: int kernel: int up: bool = False down: bool = False resample_kernel: Tuple[int] = (1, 3, 3, 1) use_bias: bool = True weight_var: str = "weight" kernel_init: Optional[Any] = None @nn.compact def __call__(self, x): assert not (self.up and self.down) assert self.kernel >= 1 and self.kernel % 2 == 1 w = get_weight( self, (self.kernel, self.kernel, x.shape[-1], self.fmaps), weight_var=self.weight_var, kernel_init=self.kernel_init, ) if self.up: x = upsample_conv_2d(x, w, data_format="NHWC", k=self.resample_kernel) elif self.down: x = conv_downsample_2d(x, w, data_format="NHWC", k=self.resample_kernel) else: x = jax.lax.conv_general_dilated( x, w, window_strides=(1, 1), padding="SAME", dimension_numbers=("NHWC", "HWIO", "NHWC"), ) if self.use_bias: b = self.param("bias", jnn.initializers.zeros, (x.shape[-1],)) x = x + b.reshape((1, 1, 1, -1)) return x def naive_upsample_2d(x, factor=2): _N, H, W, C = x.shape x = jnp.reshape(x, [-1, H, 1, W, 1, C]) x = jnp.tile(x, [1, 1, factor, 1, factor, 1]) return jnp.reshape(x, [-1, H * factor, W * factor, C]) def naive_downsample_2d(x, factor=2): _N, H, W, C = x.shape x = jnp.reshape(x, [-1, H // factor, factor, W // factor, factor, C]) return jnp.mean(x, axis=[2, 4]) def upsample_conv_2d(x, w, k=None, factor=2, gain=1, data_format="NHWC"): """Fused `upsample_2d()` followed by `tf.nn.conv2d()`. Padding is performed only once at the beginning, not between the operations. The fused op is considerably more efficient than performing the same calculation using standard TensorFlow ops. It supports gradients of arbitrary order. Args: x: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. w: Weight tensor of the shape `[filterH, filterW, inChannels, outChannels]`. Grouped convolution can be performed by `inChannels = x.shape[0] // numGroups`. k: FIR filter of the shape `[firH, firW]` or `[firN]` (separable). The default is `[1] * factor`, which corresponds to nearest-neighbor upsampling. factor: Integer upsampling factor (default: 2). gain: Scaling factor for signal magnitude (default: 1.0). data_format: `'NCHW'` or `'NHWC'` (default: `'NCHW'`). Returns: Tensor of the shape `[N, C, H * factor, W * factor]` or `[N, H * factor, W * factor, C]`, and same datatype as `x`. """ assert isinstance(factor, int) and factor >= 1 # Check weight shape. assert len(w.shape) == 4 convH = w.shape[0] convW = w.shape[1] inC = w.shape[2] outC = w.shape[3] assert convW == convH # Setup filter kernel. if k is None: k = [1] * factor k = _setup_kernel(k) * (gain * (factor**2)) p = (k.shape[0] - factor) - (convW - 1) stride = [factor, factor] # Determine data dimensions. if data_format == "NCHW": num_groups = _shape(x, 1) // inC else: num_groups = _shape(x, 3) // inC # Transpose weights. w = jnp.reshape(w, [convH, convW, inC, num_groups, -1]) w = jnp.transpose(w[::-1, ::-1], [0, 1, 4, 3, 2]) w = jnp.reshape(w, [convH, convW, -1, num_groups * inC]) ## Original TF code. # x = tf.nn.conv2d_transpose( # x, # w, # output_shape=output_shape, # strides=stride, # padding='VALID', # data_format=data_format) ## JAX equivalent x = jax.lax.conv_transpose( x, w, strides=stride, padding="VALID", transpose_kernel=True, dimension_numbers=(data_format, "HWIO", data_format), ) return _simple_upfirdn_2d( x, k, pad0=(p + 1) // 2 + factor - 1, pad1=p // 2 + 1, data_format=data_format ) def conv_downsample_2d(x, w, k=None, factor=2, gain=1, data_format="NHWC"): """Fused `tf.nn.conv2d()` followed by `downsample_2d()`. Padding is performed only once at the beginning, not between the operations. The fused op is considerably more efficient than performing the same calculation using standard TensorFlow ops. It supports gradients of arbitrary order. Args: x: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. w: Weight tensor of the shape `[filterH, filterW, inChannels, outChannels]`. Grouped convolution can be performed by `inChannels = x.shape[0] // numGroups`. k: FIR filter of the shape `[firH, firW]` or `[firN]` (separable). The default is `[1] * factor`, which corresponds to average pooling. factor: Integer downsampling factor (default: 2). gain: Scaling factor for signal magnitude (default: 1.0). data_format: `'NCHW'` or `'NHWC'` (default: `'NCHW'`). Returns: Tensor of the shape `[N, C, H // factor, W // factor]` or `[N, H // factor, W // factor, C]`, and same datatype as `x`. """ assert isinstance(factor, int) and factor >= 1 convH, convW, _inC, _outC = w.shape assert convW == convH if k is None: k = [1] * factor k = _setup_kernel(k) * gain p = (k.shape[0] - factor) + (convW - 1) s = [factor, factor] x = _simple_upfirdn_2d( x, k, pad0=(p + 1) // 2, pad1=p // 2, data_format=data_format ) return jax.lax.conv_general_dilated( x, w, window_strides=s, padding="VALID", dimension_numbers=(data_format, "HWIO", data_format), ) def upfirdn_2d(x, k, upx, upy, downx, downy, padx0, padx1, pady0, pady1): """Pad, upsample, FIR filter, and downsample a batch of 2D images. Accepts a batch of 2D images of the shape `[majorDim, inH, inW, minorDim]` and performs the following operations for each image, batched across `majorDim` and `minorDim`: 1. Pad the image with zeros by the specified number of pixels on each side (`padx0`, `padx1`, `pady0`, `pady1`). Specifying a negative value corresponds to cropping the image. 2. Upsample the image by inserting the zeros after each pixel (`upx`, `upy`). 3. Convolve the image with the specified 2D FIR filter (`k`), shrinking the image so that the footprint of all output pixels lies within the input image. 4. Downsample the image by throwing away pixels (`downx`, `downy`). This sequence of operations bears close resemblance to scipy.signal.upfirdn(). The fused op is considerably more efficient than performing the same calculation using standard TensorFlow ops. It supports gradients of arbitrary order. Args: x: Input tensor of the shape `[majorDim, inH, inW, minorDim]`. k: 2D FIR filter of the shape `[firH, firW]`. upx: Integer upsampling factor along the X-axis (default: 1). upy: Integer upsampling factor along the Y-axis (default: 1). downx: Integer downsampling factor along the X-axis (default: 1). downy: Integer downsampling factor along the Y-axis (default: 1). padx0: Number of pixels to pad on the left side (default: 0). padx1: Number of pixels to pad on the right side (default: 0). pady0: Number of pixels to pad on the top side (default: 0). pady1: Number of pixels to pad on the bottom side (default: 0). impl: Name of the implementation to use. Can be `"ref"` or `"cuda"` (default). Returns: Tensor of the shape `[majorDim, outH, outW, minorDim]`, and same datatype as `x`. """ k = jnp.asarray(k, dtype=np.float32) assert len(x.shape) == 4 inH = x.shape[1] inW = x.shape[2] minorDim = x.shape[3] kernelH, kernelW = k.shape assert inW >= 1 and inH >= 1 assert kernelW >= 1 and kernelH >= 1 assert isinstance(upx, int) and isinstance(upy, int) assert isinstance(downx, int) and isinstance(downy, int) assert isinstance(padx0, int) and isinstance(padx1, int) assert isinstance(pady0, int) and isinstance(pady1, int) # Upsample (insert zeros). x = jnp.reshape(x, (-1, inH, 1, inW, 1, minorDim)) x = jnp.pad(x, [[0, 0], [0, 0], [0, upy - 1], [0, 0], [0, upx - 1], [0, 0]]) x = jnp.reshape(x, [-1, inH * upy, inW * upx, minorDim]) # Pad (crop if negative). x = jnp.pad( x, [ [0, 0], [max(pady0, 0), max(pady1, 0)], [max(padx0, 0), max(padx1, 0)], [0, 0], ], ) x = x[ :, max(-pady0, 0) : x.shape[1] - max(-pady1, 0), max(-padx0, 0) : x.shape[2] - max(-padx1, 0), :, ] # Convolve with filter. x = jnp.transpose(x, [0, 3, 1, 2]) x = jnp.reshape(x, [-1, 1, inH * upy + pady0 + pady1, inW * upx + padx0 + padx1]) w = jnp.array(k[::-1, ::-1, None, None], dtype=x.dtype) x = jax.lax.conv_general_dilated( x, w, window_strides=(1, 1), padding="VALID", dimension_numbers=("NCHW", "HWIO", "NCHW"), ) x = jnp.reshape( x, [ -1, minorDim, inH * upy + pady0 + pady1 - kernelH + 1, inW * upx + padx0 + padx1 - kernelW + 1, ], ) x = jnp.transpose(x, [0, 2, 3, 1]) # Downsample (throw away pixels). return x[:, ::downy, ::downx, :] def _simple_upfirdn_2d(x, k, up=1, down=1, pad0=0, pad1=0, data_format="NCHW"): assert data_format in ["NCHW", "NHWC"] assert len(x.shape) == 4 y = x if data_format == "NCHW": y = jnp.reshape(y, [-1, y.shape[2], y.shape[3], 1]) y = upfirdn_2d( y, k, upx=up, upy=up, downx=down, downy=down, padx0=pad0, padx1=pad1, pady0=pad0, pady1=pad1, ) if data_format == "NCHW": y = jnp.reshape(y, [-1, x.shape[1], y.shape[1], y.shape[2]]) return y def _setup_kernel(k): k = np.asarray(k, dtype=np.float32) if k.ndim == 1: k = np.outer(k, k) k /= np.sum(k) assert k.ndim == 2 assert k.shape[0] == k.shape[1] return k def _shape(x, dim): return x.shape[dim] def upsample_2d(x, k=None, factor=2, gain=1, data_format="NHWC"): r"""Upsample a batch of 2D images with the given filter. Accepts a batch of 2D images of the shape `[N, C, H, W]` or `[N, H, W, C]` and upsamples each image with the given filter. The filter is normalized so that if the input pixels are constant, they will be scaled by the specified `gain`. Pixels outside the image are assumed to be zero, and the filter is padded with zeros so that its shape is a multiple of the upsampling factor. Args: x: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. k: FIR filter of the shape `[firH, firW]` or `[firN]` (separable). The default is `[1] * factor`, which corresponds to nearest-neighbor upsampling. factor: Integer upsampling factor (default: 2). gain: Scaling factor for signal magnitude (default: 1.0). data_format: `'NCHW'` or `'NHWC'` (default: `'NCHW'`). Returns: Tensor of the shape `[N, C, H * factor, W * factor]` or `[N, H * factor, W * factor, C]`, and same datatype as `x`. """ assert isinstance(factor, int) and factor >= 1 if k is None: k = [1] * factor k = _setup_kernel(k) * (gain * (factor**2)) p = k.shape[0] - factor return _simple_upfirdn_2d( x, k, up=factor, pad0=(p + 1) // 2 + factor - 1, pad1=p // 2, data_format=data_format, ) def downsample_2d(x, k=None, factor=2, gain=1, data_format="NHWC"): r"""Downsample a batch of 2D images with the given filter. Accepts a batch of 2D images of the shape `[N, C, H, W]` or `[N, H, W, C]` and downsamples each image with the given filter. The filter is normalized so that if the input pixels are constant, they will be scaled by the specified `gain`. Pixels outside the image are assumed to be zero, and the filter is padded with zeros so that its shape is a multiple of the downsampling factor. Args: x: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. k: FIR filter of the shape `[firH, firW]` or `[firN]` (separable). The default is `[1] * factor`, which corresponds to average pooling. factor: Integer downsampling factor (default: 2). gain: Scaling factor for signal magnitude (default: 1.0). data_format: `'NCHW'` or `'NHWC'` (default: `'NCHW'`). impl: Name of the implementation to use. Can be `"ref"` or `"cuda"` (default). Returns: Tensor of the shape `[N, C, H // factor, W // factor]` or `[N, H // factor, W // factor, C]`, and same datatype as `x`. """ assert isinstance(factor, int) and factor >= 1 if k is None: k = [1] * factor k = _setup_kernel(k) * gain p = k.shape[0] - factor return _simple_upfirdn_2d( x, k, down=factor, pad0=(p + 1) // 2, pad1=p // 2, data_format=data_format )