From 186c67876de055333cda01c34e0f207ea7c2312b Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 23 Feb 2017 18:17:15 +0000 Subject: [PATCH 01/52] Created notebook to work through converting the functions to tf --- dtcwt_tf.ipynb | 969 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 969 insertions(+) create mode 100644 dtcwt_tf.ipynb diff --git a/dtcwt_tf.ipynb b/dtcwt_tf.ipynb new file mode 100644 index 0000000..5ba3ad4 --- /dev/null +++ b/dtcwt_tf.ipynb @@ -0,0 +1,969 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import dtcwt\n", + "import tensorflow as tf\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "import os\n", + "%matplotlib notebook\n", + "sns.set_style(\"white\")\n", + "\n", + "from dtcwt.coeffs import biort as _biort, qshift as _qshift\n", + "from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT\n", + "from dtcwt.utils import appropriate_complex_type_for, asfarray\n", + "\n", + "from dtcwt.numpy.lowlevel import colfilter as colf, coldfilt as cold, colifilt as coli" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "im = np.load(os.path.join('tests', 'mandrill.npz'))['mandrill']\n", + "plt.imshow(im, cmap='gray', interpolation='none')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tf.reset_default_graph()\n", + "g = tf.get_default_graph()\n", + "dir(g)\n", + "g.get_collection('variables')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Pyramid(object):\n", + " \"\"\"A representation of a transform domain signal.\n", + " Backends are free to implement any class which respects this interface for\n", + " storing transform-domain signals. The inverse transform may accept a\n", + " backend-specific version of this class but should always accept any class\n", + " which corresponds to this interface.\n", + " .. py:attribute:: lowpass\n", + " A NumPy-compatible array containing the coarsest scale lowpass signal.\n", + " .. py:attribute:: highpasses\n", + " A tuple where each element is the complex subband coefficients for\n", + " corresponding scales finest to coarsest.\n", + " .. py:attribute:: scales\n", + " *(optional)* A tuple where each element is a NumPy-compatible array\n", + " containing the lowpass signal for corresponding scales finest to\n", + " coarsest. This is not required for the inverse and may be *None*.\n", + " \"\"\"\n", + " def __init__(self, lowpass, highpasses, scales=None):\n", + " self.lowpass = tf.Variable(lowpass, trainable=False, dtype=tf.float32)\n", + " self.highpasses = tuple(tf.Variable(x, trainable=False, dtype=tf.complex64) \n", + " if x is not None else None for x in highpasses)\n", + " self.scales = tuple(tf.Variable(x, trainable=False, dtype=tf.float32) \n", + " for x in scales) if scales is not None else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import absolute_import, division\n", + "\n", + "__all__ = [ 'colfilter', 'colifilt', 'coldfilt', ]\n", + "\n", + "import numpy as np\n", + "from six.moves import xrange\n", + "from dtcwt.utils import as_column_vector, asfarray, appropriate_complex_type_for, reflect\n", + "\n", + "def _centered(arr, newsize):\n", + " # Return the center newsize portion of the array.\n", + " # (Shamelessly cribbed from scipy.)\n", + " newsize = np.asanyarray(newsize)\n", + " currsize = np.array(arr.shape)\n", + " startind = (currsize - newsize) // 2\n", + " endind = startind + newsize\n", + " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", + " return arr[tuple(myslice)]\n", + "\n", + "# This is to allow easy replacement of these later with, possibly, GPU versions\n", + "_rfft = np.fft.rfft\n", + "_irfft = np.fft.irfft\n", + "\n", + "def _column_convolve(X, h):\n", + " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", + " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", + " *h*, the output will have the dtype of *X* appropriately expanded to a\n", + " floating point type if necessary.\n", + " We assume that h is small and so direct convolution is the most efficient.\n", + " \"\"\"\n", + " Xshape = np.asanyarray(X.shape)\n", + " h = h.flatten().astype(X.dtype)\n", + " h_size = h.shape[0]\n", + "\n", + " full_size = X.shape[0] + h_size - 1\n", + " Xshape[0] = full_size\n", + "\n", + " out = np.zeros(Xshape, dtype=X.dtype)\n", + " for idx in xrange(h_size):\n", + " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", + "\n", + " outShape = Xshape.copy()\n", + " outShape[0] = abs(X.shape[0] - h_size) + 1\n", + " return _centered(out, outShape)\n", + "\n", + "def colfilter2(X, h):\n", + " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [1 0].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " # Interpret all inputs as arrays\n", + " X = asfarray(X)\n", + " h = as_column_vector(h)\n", + "\n", + " r, c = X.shape\n", + " m = h.shape[0]\n", + " m2 = np.fix(m*0.5)\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Use 'reflect' so r < m2 works OK.\n", + " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", + "\n", + " # Perform filtering on the columns of the extended matrix X(xe,:), keeping\n", + " # only the 'valid' output samples, so Y is the same size as X if m is odd.\n", + " Y = _column_convolve(X[xe,:], h)\n", + "\n", + " return X[xe,:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def colfilter(X, h):\n", + " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [1 0].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m//2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0],[m2, m2], [0, 0]], 'SYMMETRIC')\n", + " r, c = X.get_shape().as_list()[1:3]\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [-1, 1, 1, 1])\n", + " X = tf.reshape(X, [-1, r, c, 1])\n", + " \n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r,c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", + " return tf.reshape(y, [-1,r,c])\n", + "\n", + "def rowfilter(X, h):\n", + " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [0 1].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m//2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", + " r, c = X.get_shape().as_list()[1:3]\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [1, -1, 1, 1])\n", + " X = tf.reshape(X, [-1, r, c, 1])\n", + " \n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r,c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", + " return tf.reshape(y, [-1,r,c])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "f = Transform2d()\n", + "h1o = tf.constant(f.qshift[0][::-1], dtype=tf.float32)\n", + "i = tf.placeholder(tf.float32, shape=[None, 512, 512])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compare the 2\n", + "im_hat = colf(im, f.qshift[0].astype('float32'))\n", + "y1 = colfilter(i, h1o)\n", + "y2 = rowfilter(tf.transpose(i, perm=[0,2,1]),h1o)\n", + "with tf.Session() as sess:\n", + " im_hat1,im_hat2 = (k[0] for k in sess.run([y1,y2], feed_dict={i:[im]}))\n", + " \n", + "np.testing.assert_array_almost_equal(im_hat, im_hat2.T, decimal=4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(nrows=1,ncols=2,figsize=(10,5))\n", + "fig.tight_layout()\n", + "axes[0].imshow(im_hat1, cmap='gray', interpolation='none')\n", + "axes[1].imshow(im_hat2, cmap='gray', interpolation='none')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def q2c(y):\n", + " \"\"\"\n", + " Convert from quads in y to complex numbers in z.\n", + " \"\"\"\n", + "\n", + " # Arrange pixels from the corners of the quads into\n", + " # 2 subimages of alternate real and imag pixels.\n", + " # a----b\n", + " # | |\n", + " # | |\n", + " # c----d\n", + "\n", + " # Combine (a,b) and (d,c) to form two complex subimages.\n", + " a,b,c,d = y[0::2, 0::2], y[0::2,1::2], y[1::2,0::2], y[1::2,1::2]\n", + " \n", + " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", + " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", + "\n", + " # Form the 2 highpasses in z.\n", + " return (p-q, p+q)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Transform2d(object):\n", + " \"\"\"\n", + " An implementation of the 2D DT-CWT via NumPy. *biort* and *qshift* are the\n", + " wavelets which parameterise the transform.\n", + " If *biort* or *qshift* are strings, they are used as an argument to the\n", + " :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions.\n", + " Otherwise, they are interpreted as tuples of vectors giving filter\n", + " coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In\n", + " the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b).\n", + " \"\"\"\n", + " def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT):\n", + " # Load bi-orthogonal wavelets\n", + " try:\n", + " self.biort = _biort(biort)\n", + " except TypeError:\n", + " self.biort = biort\n", + "\n", + " # Load quarter sample shift wavelets\n", + " try:\n", + " self.qshift = _qshift(qshift)\n", + " except TypeError:\n", + " self.qshift = qshift\n", + "\n", + " def forward(self, X, nlevels=3, include_scale=False):\n", + " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", + " :param X: 2D real array\n", + " :param nlevels: Number of levels of wavelet decomposition\n", + " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", + " .. codeauthor:: Rich Wareham , Aug 2013\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", + " \"\"\"\n", + " # If biort has 6 elements instead of 4, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " if len(self.biort) == 4:\n", + " # h0o, g0o, h1o, g1o = self.biort \n", + " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", + " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", + " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", + " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", + " elif len(self.biort) == 6:\n", + " #h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", + " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", + " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", + " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", + " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", + " h2o = tf.Variable(self.biort[4], trainable=False, name='dtcwt/h2o')\n", + " g2o = tf.Variable(self.biort[5], trainable=False, name='dtcwt/g2o')\n", + " else:\n", + " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", + "\n", + " # If qshift has 12 elements instead of 8, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " \n", + " # We have to reverse the qshift filters, as tensorflow's conv2d is\n", + " # really cross-correlation. Note that we didn't have to do this for\n", + " # biorthogonal filters as they are already symmetric.\n", + " if len(self.qshift) == 8:\n", + " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", + " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", + " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", + " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", + " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", + " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", + " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", + " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", + " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", + " elif len(self.qshift) == 12:\n", + " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10]\n", + " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", + " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", + " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", + " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", + " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", + " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", + " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", + " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", + " h2a = tf.Variable(self.qshift[8][::-1], trainable=False, name='dtcwt/h2a')\n", + " h2b = tf.Variable(self.qshift[9][::-1], trainable=False, name='dtcwt/h2b')\n", + " else:\n", + " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", + "\n", + " # Check the shape of the input\n", + " original_size = X.get_shape().as_list()[1:3]\n", + "\n", + " # The next few lines of code check to see if the image is odd in size, if so an extra ...\n", + " # row/column will be added to the bottom/right of the image\n", + " initial_row_extend = 0 #initialise\n", + " initial_col_extend = 0\n", + " if original_size[0] % 2 != 0:\n", + " # if X.shape[0] is not divisable by 2 then we need to extend X by adding a row at the bottom\n", + " bottom_row = tf.slice(X, [0, original_size[0]-1,0], [-1, 1, -1])\n", + " X = tf.concat([X, bottom_row], axis=1)\n", + " #X = np.vstack((X, X[[-1],:])) # Any further extension will be done in due course.\n", + " initial_row_extend = 1\n", + "\n", + " if original_size[1] % 2 != 0:\n", + " # if X.shape[1] is not divisable by 2 then we need to extend X by adding a col to the left\n", + " right_column = tf.slice(X, [0, 0, original_size[1]-1], [-1, -1, 1])\n", + " X = tf.concat([X, right_column], axis=2)\n", + " #X = np.hstack((X, X[:,[-1]]))\n", + " initial_col_extend = 1\n", + "\n", + " extended_size = X.get_shape().as_list()[1:3]\n", + " \n", + " if nlevels == 0:\n", + " if include_scale:\n", + " return Pyramid(X, (), ())\n", + " else:\n", + " return Pyramid(X, ())\n", + "\n", + " # initialise\n", + " Yh = [None,] * nlevels\n", + " if include_scale:\n", + " # this is only required if the user specifies a third output component.\n", + " Yscale = [None,] * nlevels\n", + "\n", + " #complex_dtype = appropriate_complex_type_for(X)\n", + " \n", + " if nlevels >= 1:\n", + " # Do odd top-level filters on cols.\n", + " Lo = tf.transpose(colfilter(X,h0o), perm=[0,2,1])\n", + " Hi = tf.transpose(colfilter(X,h1o), perm=[0,2,1])\n", + " if len(self.biort) >= 6:\n", + " Ba = tf.tranpsoe(colfilter(X,h2o), perm=[0,2,1])\n", + "\n", + " # Do odd top-level filters on rows.\n", + " LoLo = tf.transpose(colfilter(Lo,h0o), perm=[0,2,1])\n", + " LoLo_shape = LoLo.get_shape().as_list()[1:3]\n", + " Yh[0] = tf.Variable(np.zeros((LoLo_shape[0]>>1, LoLo_shape[1] >>1, 6), dtype=tf.complex64))\n", + " Yh[0][:,:,0], Yh[0][:,:,5] = q2c(tf.transpose(colfilter(Hi,h0o), perm=[0,2,1])) # Horizontal pair\n", + " Yh[0][:,:,2], Yh[0][:,:,3] = q2c(tf.transpose(colfilter(Lo,h1o), perm=[0,2,1])) # Vertical pair\n", + " if len(self.biort) >= 6:\n", + " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.transpose(colfilter(Ba,h2o), perm=[0,2,1])) # Diagonal pair\n", + " else:\n", + " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.tranpose(colfilter(Hi,h1o), perm=[0,2,1])) # Diagonal pair\n", + "\n", + " if include_scale:\n", + " Yscale[0] = LoLo\n", + " \n", + " if include_scale:\n", + " return Pyramid(LoLo, Yh, Yscale)\n", + " else:\n", + " return Pyramid(LoLo, Yh, ())\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "P = f.forward(i)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "im = sess.run(x, feed_dict={i: [im]})\n", + "im2.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + " for level in xrange(1, nlevels):\n", + " row_size, col_size = LoLo.shape\n", + " if row_size % 4 != 0:\n", + " # Extend by 2 rows if no. of rows of LoLo are not divisable by 4\n", + " LoLo = np.vstack((LoLo[:1,:], LoLo, LoLo[-1:,:]))\n", + "\n", + " if col_size % 4 != 0:\n", + " # Extend by 2 cols if no. of cols of LoLo are not divisable by 4\n", + " LoLo = np.hstack((LoLo[:,:1], LoLo, LoLo[:,-1:]))\n", + "\n", + " # Do even Qshift filters on rows.\n", + " Lo = coldfilt(LoLo,h0b,h0a).T\n", + " Hi = coldfilt(LoLo,h1b,h1a).T\n", + " if len(self.qshift) >= 12:\n", + " Ba = coldfilt(LoLo,h2b,h2a).T\n", + "\n", + " # Do even Qshift filters on columns.\n", + " LoLo = coldfilt(Lo,h0b,h0a).T\n", + "\n", + " Yh[level] = np.zeros((LoLo.shape[0]>>1, LoLo.shape[1]>>1, 6), dtype=complex_dtype)\n", + " Yh[level][:,:,0:6:5] = q2c(coldfilt(Hi,h0b,h0a).T) # Horizontal\n", + " Yh[level][:,:,2:4:1] = q2c(coldfilt(Lo,h1b,h1a).T) # Vertical\n", + " if len(self.qshift) >= 12:\n", + " Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal \n", + " else:\n", + " Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal \n", + "\n", + " if include_scale:\n", + " Yscale[level] = LoLo\n", + " \n", + " return X" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + " if len(X.shape) >= 3:\n", + " raise ValueError('The entered image is {0}, please enter each image slice separately.'.\n", + " format('x'.join(list(str(s) for s in X.shape))))\n", + "\n", + " \n", + " for level in xrange(1, nlevels):\n", + " row_size, col_size = LoLo.shape\n", + " if row_size % 4 != 0:\n", + " # Extend by 2 rows if no. of rows of LoLo are not divisable by 4\n", + " LoLo = np.vstack((LoLo[:1,:], LoLo, LoLo[-1:,:]))\n", + "\n", + " if col_size % 4 != 0:\n", + " # Extend by 2 cols if no. of cols of LoLo are not divisable by 4\n", + " LoLo = np.hstack((LoLo[:,:1], LoLo, LoLo[:,-1:]))\n", + "\n", + " # Do even Qshift filters on rows.\n", + " Lo = coldfilt(LoLo,h0b,h0a).T\n", + " Hi = coldfilt(LoLo,h1b,h1a).T\n", + " if len(self.qshift) >= 12:\n", + " Ba = coldfilt(LoLo,h2b,h2a).T\n", + "\n", + " # Do even Qshift filters on columns.\n", + " LoLo = coldfilt(Lo,h0b,h0a).T\n", + "\n", + " Yh[level] = np.zeros((LoLo.shape[0]>>1, LoLo.shape[1]>>1, 6), dtype=complex_dtype)\n", + " Yh[level][:,:,0:6:5] = q2c(coldfilt(Hi,h0b,h0a).T) # Horizontal\n", + " Yh[level][:,:,2:4:1] = q2c(coldfilt(Lo,h1b,h1a).T) # Vertical\n", + " if len(self.qshift) >= 12:\n", + " Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal \n", + " else:\n", + " Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal \n", + "\n", + " if include_scale:\n", + " Yscale[level] = LoLo\n", + "\n", + " Yl = LoLo\n", + "\n", + " if initial_row_extend == 1 and initial_col_extend == 1:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The bottom row and rightmost column have been duplicated, prior to decomposition.')\n", + "\n", + " if initial_row_extend == 1 and initial_col_extend == 0:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The bottom row has been duplicated, prior to decomposition.')\n", + "\n", + " if initial_row_extend == 0 and initial_col_extend == 1:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The rightmost column has been duplicated, prior to decomposition.')\n", + "\n", + " if include_scale:\n", + " return Pyramid(Yl, tuple(Yh), tuple(Yscale))\n", + " else:\n", + " return Pyramid(Yl, tuple(Yh))\n", + "\"\"\"\n", + "\n", + "# def inverse(self, pyramid, gain_mask=None):\n", + " \"\"\"Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D\n", + " reconstruction.\n", + " :param pyramid: A :py:class:`dtcwt.Pyramid`-like class holding the transform domain representation to invert.\n", + " :param gain_mask: Gain to be applied to each subband.\n", + " :returns: A numpy-array compatible instance with the reconstruction.\n", + " The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction\n", + " *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for\n", + " band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are\n", + " zero-indexed.\n", + " .. codeauthor:: Rich Wareham , Aug 2013\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002\n", + " \"\"\"\n", + "\"\"\"\n", + " Yl = pyramid.lowpass\n", + " Yh = pyramid.highpasses\n", + "\n", + " a = len(Yh) # No of levels.\n", + "\n", + " if gain_mask is None:\n", + " gain_mask = np.ones((6,a)) # Default gain_mask.\n", + "\n", + " gain_mask = np.array(gain_mask)\n", + "\n", + " # If biort has 6 elements instead of 4, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " # FIXME: there's probably a nicer way to do this\n", + " if len(self.biort) == 4:\n", + " h0o, g0o, h1o, g1o = self.biort\n", + " elif len(self.biort) == 6:\n", + " h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", + " else:\n", + " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", + "\n", + " # If qshift has 12 elements instead of 8, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " # FIXME: there's probably a nicer way to do this\n", + " if len(self.qshift) == 8:\n", + " h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", + " elif len(self.qshift) == 12:\n", + " h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b, g2a, g2b = self.qshift\n", + " else:\n", + " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", + "\n", + " current_level = a\n", + " Z = Yl\n", + "\n", + " while current_level >= 2: # this ensures that for level 1 we never do the following\n", + " lh = c2q(Yh[current_level-1][:,:,[0, 5]], gain_mask[[0, 5], current_level-1])\n", + " hl = c2q(Yh[current_level-1][:,:,[2, 3]], gain_mask[[2, 3], current_level-1])\n", + " hh = c2q(Yh[current_level-1][:,:,[1, 4]], gain_mask[[1, 4], current_level-1])\n", + "\n", + " # Do even Qshift filters on columns.\n", + " y1 = colifilt(Z,g0b,g0a) + colifilt(lh,g1b,g1a)\n", + "\n", + " if len(self.qshift) >= 12:\n", + " y2 = colifilt(hl,g0b,g0a)\n", + " y2bp = colifilt(hh,g2b,g2a)\n", + "\n", + " # Do even Qshift filters on rows.\n", + " Z = (colifilt(y1.T,g0b,g0a) + colifilt(y2.T,g1b,g1a) + colifilt(y2bp.T, g2b, g2a)).T\n", + " else:\n", + " y2 = colifilt(hl,g0b,g0a) + colifilt(hh,g1b,g1a)\n", + "\n", + " # Do even Qshift filters on rows.\n", + " Z = (colifilt(y1.T,g0b,g0a) + colifilt(y2.T,g1b,g1a)).T\n", + "\n", + " # Check size of Z and crop as required\n", + " [row_size, col_size] = Z.shape\n", + " S = 2*np.array(Yh[current_level-2].shape)\n", + " if row_size != S[0]: # check to see if this result needs to be cropped for the rows\n", + " Z = Z[1:-1,:]\n", + " if col_size != S[1]: # check to see if this result needs to be cropped for the cols\n", + " Z = Z[:,1:-1]\n", + "\n", + " if np.any(np.array(Z.shape) != S[:2]):\n", + " raise ValueError('Sizes of highpasses are not valid for DTWAVEIFM2')\n", + " \n", + " current_level = current_level - 1\n", + "\n", + " if current_level == 1:\n", + " lh = c2q(Yh[current_level-1][:,:,[0, 5]],gain_mask[[0, 5],current_level-1])\n", + " hl = c2q(Yh[current_level-1][:,:,[2, 3]],gain_mask[[2, 3],current_level-1])\n", + " hh = c2q(Yh[current_level-1][:,:,[1, 4]],gain_mask[[1, 4],current_level-1])\n", + "\n", + " # Do odd top-level filters on columns.\n", + " y1 = colfilter(Z,g0o) + colfilter(lh,g1o)\n", + "\n", + " if len(self.biort) >= 6:\n", + " y2 = colfilter(hl,g0o)\n", + " y2bp = colfilter(hh,g2o)\n", + "\n", + " # Do odd top-level filters on rows.\n", + " Z = (colfilter(y1.T,g0o) + colfilter(y2.T,g1o) + colfilter(y2bp.T, g2o)).T\n", + " else:\n", + " y2 = colfilter(hl,g0o) + colfilter(hh,g1o)\n", + "\n", + " # Do odd top-level filters on rows.\n", + " Z = (colfilter(y1.T,g0o) + colfilter(y2.T,g1o)).T\n", + "\n", + " return Z\n", + "\"\"\"\n", + "#==========================================================================================\n", + "# ********** INTERNAL FUNCTIONS **********\n", + "#==========================================================================================\n", + "\n", + "\n", + "\n", + "def c2q(w,gain):\n", + " \"\"\"\n", + " Scale by gain and convert from complex w(:,:,1:2) to real quad-numbers\n", + " in z.\n", + " Arrange pixels from the real and imag parts of the 2 highpasses\n", + " into 4 separate subimages .\n", + " A----B Re Im of w(:,:,1)\n", + " | |\n", + " | |\n", + " C----D Re Im of w(:,:,2)\n", + " \"\"\"\n", + "\n", + " x = np.zeros((w.shape[0] << 1, w.shape[1] << 1), dtype=w.real.dtype)\n", + "\n", + " sc = np.sqrt(0.5) * gain\n", + " P = w[:,:,0]*sc[0] + w[:,:,1]*sc[1]\n", + " Q = w[:,:,0]*sc[0] - w[:,:,1]*sc[1]\n", + "\n", + " # Recover each of the 4 corners of the quads.\n", + " x[0::2, 0::2] = P.real # a = (A+C)*sc\n", + " x[0::2, 1::2] = P.imag # b = (B+D)*sc\n", + " x[1::2, 0::2] = Q.imag # c = (B-D)*sc\n", + " x[1::2, 1::2] = -Q.real # d = (C-A)*sc\n", + "\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _centered(arr, newsize):\n", + " # Return the center newsize portion of the array.\n", + " # (Shamelessly cribbed from scipy.)\n", + " newsize = np.asanyarray(newsize)\n", + " currsize = np.array(arr.shape)\n", + " startind = (currsize - newsize) // 2\n", + " endind = startind + newsize\n", + " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", + " return arr[tuple(myslice)]\n", + "\n", + "# This is to allow easy replacement of these later with, possibly, GPU versions\n", + "_rfft = np.fft.rfft\n", + "_irfft = np.fft.irfft\n", + "\n", + "def _column_convolve(X, h):\n", + " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", + " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", + " *h*, the output will have the dtype of *X* appropriately expanded to a\n", + " floating point type if necessary.\n", + " We assume that h is small and so direct convolution is the most efficient.\n", + " \"\"\"\n", + " Xshape = np.asanyarray(X.shape)\n", + " h = h.flatten().astype(X.dtype)\n", + " h_size = h.shape[0]\n", + "\n", + " full_size = X.shape[0] + h_size - 1\n", + " Xshape[0] = full_size\n", + "\n", + " out = np.zeros(Xshape, dtype=X.dtype)\n", + " for idx in xrange(h_size):\n", + " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", + "\n", + " outShape = Xshape.copy()\n", + " outShape[0] = abs(X.shape[0] - h_size) + 1\n", + " return _centered(out, outShape)\n", + "\n", + "\n", + " return Y\n", + "\n", + "def coldfilt(X, ha, hb):\n", + " \"\"\"Filter the columns of image X using the two filters ha and hb =\n", + " reverse(ha). ha operates on the odd samples of X and hb on the even\n", + " samples. Both filters should be even length, and h should be approx linear\n", + " phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| >\n", + " |h(m/2 + 1)|`).\n", + " .. code-block:: text\n", + " ext top edge bottom edge ext\n", + " Level 1: ! | ! | !\n", + " odd filt on . b b b b a a a a a a a a b b b b\n", + " odd filt on . a a a a b b b b b b b b a a a a\n", + " Level 2: ! | ! | !\n", + " +q filt on x b b a a a a b b\n", + " -q filt on o a a b b b b a a\n", + " The output is decimated by two from the input sample rate and the results\n", + " from the two filters, Ya and Yb, are interleaved to give Y. Symmetric\n", + " extension with repeated end samples is used on the composite X columns\n", + " before each filter is applied.\n", + " Raises ValueError if the number of rows in X is not a multiple of 4, the\n", + " length of ha does not match hb or the lengths of ha or hb are non-even.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + " # Make sure all inputs are arrays\n", + " X = asfarray(X)\n", + " ha = asfarray(ha)\n", + " hb = asfarray(hb)\n", + "\n", + " r, c = X.shape\n", + " if r % 4 != 0:\n", + " raise ValueError('No. of rows in X must be a multiple of 4')\n", + "\n", + " if ha.shape != hb.shape:\n", + " raise ValueError('Shapes of ha and hb must be the same')\n", + "\n", + " if ha.shape[0] % 2 != 0:\n", + " raise ValueError('Lengths of ha and hb must be even')\n", + "\n", + " m = ha.shape[0]\n", + " m2 = np.fix(m*0.5)\n", + "\n", + " # Set up vector for symmetric extension of X with repeated end samples.\n", + " xe = reflect(np.arange(-m, r+m), -0.5, r-0.5)\n", + "\n", + " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", + " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", + " hao = as_column_vector(ha[0:m:2])\n", + " hae = as_column_vector(ha[1:m:2])\n", + " hbo = as_column_vector(hb[0:m:2])\n", + " hbe = as_column_vector(hb[1:m:2])\n", + " t = np.arange(5, r+2*m-2, 4)\n", + " r2 = r//2;\n", + " Y = np.zeros((r2,c), dtype=X.dtype)\n", + "\n", + " if np.sum(ha*hb) > 0:\n", + " s1 = slice(0, r2, 2)\n", + " s2 = slice(1, r2, 2)\n", + " else:\n", + " s2 = slice(0, r2, 2)\n", + " s1 = slice(1, r2, 2)\n", + "\n", + " # Perform filtering on columns of extended matrix X(xe,:) in 4 ways.\n", + " Y[s1,:] = _column_convolve(X[xe[t-1],:],hao) + _column_convolve(X[xe[t-3],:],hae)\n", + " Y[s2,:] = _column_convolve(X[xe[t],:],hbo) + _column_convolve(X[xe[t-2],:],hbe)\n", + "\n", + " return Y\n", + "\n", + "def colifilt(X, ha, hb):\n", + " \"\"\" Filter the columns of image X using the two filters ha and hb =\n", + " reverse(ha). ha operates on the odd samples of X and hb on the even\n", + " samples. Both filters should be even length, and h should be approx linear\n", + " phase with a quarter sample advance from its mid pt (i.e `:math:`|h(m/2)| >\n", + " |h(m/2 + 1)|`).\n", + " .. code-block:: text\n", + " ext left edge right edge ext\n", + " Level 2: ! | ! | !\n", + " +q filt on x b b a a a a b b\n", + " -q filt on o a a b b b b a a\n", + " Level 1: ! | ! | !\n", + " odd filt on . b b b b a a a a a a a a b b b b\n", + " odd filt on . a a a a b b b b b b b b a a a a\n", + " The output is interpolated by two from the input sample rate and the\n", + " results from the two filters, Ya and Yb, are interleaved to give Y.\n", + " Symmetric extension with repeated end samples is used on the composite X\n", + " columns before each filter is applied.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + " # Make sure all inputs are arrays\n", + " X = asfarray(X)\n", + " ha = asfarray(ha)\n", + " hb = asfarray(hb)\n", + "\n", + " r, c = X.shape\n", + " if r % 2 != 0:\n", + " raise ValueError('No. of rows in X must be a multiple of 2')\n", + "\n", + " if ha.shape != hb.shape:\n", + " raise ValueError('Shapes of ha and hb must be the same')\n", + "\n", + " if ha.shape[0] % 2 != 0:\n", + " raise ValueError('Lengths of ha and hb must be even')\n", + "\n", + " m = ha.shape[0]\n", + " m2 = np.fix(m*0.5)\n", + "\n", + " Y = np.zeros((r*2,c), dtype=X.dtype)\n", + " if not np.any(np.nonzero(X[:])[0]):\n", + " return Y\n", + "\n", + " if m2 % 2 == 0:\n", + " # m/2 is even, so set up t to start on d samples.\n", + " # Set up vector for symmetric extension of X with repeated end samples.\n", + " # Use 'reflect' so r < m2 works OK.\n", + " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", + "\n", + " t = np.arange(3, r+m, 2)\n", + " if np.sum(ha*hb) > 0:\n", + " ta = t\n", + " tb = t - 1\n", + " else:\n", + " ta = t - 1\n", + " tb = t\n", + "\n", + " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", + " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", + " hao = as_column_vector(ha[0:m:2])\n", + " hae = as_column_vector(ha[1:m:2])\n", + " hbo = as_column_vector(hb[0:m:2])\n", + " hbe = as_column_vector(hb[1:m:2])\n", + "\n", + " s = np.arange(0,r*2,4)\n", + "\n", + " Y[s,:] = _column_convolve(X[xe[tb-2],:],hae)\n", + " Y[s+1,:] = _column_convolve(X[xe[ta-2],:],hbe)\n", + " Y[s+2,:] = _column_convolve(X[xe[tb ],:],hao)\n", + " Y[s+3,:] = _column_convolve(X[xe[ta ],:],hbo)\n", + " else:\n", + " # m/2 is odd, so set up t to start on b samples.\n", + " # Set up vector for symmetric extension of X with repeated end samples.\n", + " # Use 'reflect' so r < m2 works OK.\n", + " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", + "\n", + " t = np.arange(2, r+m-1, 2)\n", + " if np.sum(ha*hb) > 0:\n", + " ta = t\n", + " tb = t - 1\n", + " else:\n", + " ta = t - 1\n", + " tb = t\n", + "\n", + " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", + " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", + " hao = as_column_vector(ha[0:m:2])\n", + " hae = as_column_vector(ha[1:m:2])\n", + " hbo = as_column_vector(hb[0:m:2])\n", + " hbe = as_column_vector(hb[1:m:2])\n", + "\n", + " s = np.arange(0,r*2,4)\n", + "\n", + " Y[s,:] = _column_convolve(X[xe[tb],:],hao)\n", + " Y[s+1,:] = _column_convolve(X[xe[ta],:],hbo)\n", + " Y[s+2,:] = _column_convolve(X[xe[tb],:],hae)\n", + " Y[s+3,:] = _column_convolve(X[xe[ta],:],hbe)\n", + "\n", + " return Y" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "tf", + "language": "python", + "name": "tf" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + }, + "toc": { + "nav_menu": { + "height": "12px", + "width": "252px" + }, + "navigate_menu": true, + "number_sections": true, + "sideBar": true, + "threshold": 4, + "toc_cell": false, + "toc_section_display": "block", + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 8fe0e6c2a356c87cdd25095661fe5f2da88f66db Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 23 Feb 2017 21:44:02 +0000 Subject: [PATCH 02/52] Created two versions of conv and tested each --- dtcwt_tf.ipynb | 616 +++++++++++++++++++++++++++++++--------------- time tester.ipynb | 171 +++++++++++++ 2 files changed, 584 insertions(+), 203 deletions(-) create mode 100644 time tester.ipynb diff --git a/dtcwt_tf.ipynb b/dtcwt_tf.ipynb index 5ba3ad4..d2df24c 100644 --- a/dtcwt_tf.ipynb +++ b/dtcwt_tf.ipynb @@ -3,7 +3,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "import dtcwt\n", @@ -25,7 +28,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "im = np.load(os.path.join('tests', 'mandrill.npz'))['mandrill']\n", @@ -35,19 +41,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tf.reset_default_graph()\n", - "g = tf.get_default_graph()\n", - "dir(g)\n", - "g.get_collection('variables')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "class Pyramid(object):\n", @@ -77,7 +74,197 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "def q2c(y):\n", + " \"\"\"\n", + " Convert from quads in y to complex numbers in z.\n", + " \"\"\"\n", + "\n", + " # Arrange pixels from the corners of the quads into\n", + " # 2 subimages of alternate real and imag pixels.\n", + " # a----b\n", + " # | |\n", + " # | |\n", + " # c----d\n", + "\n", + " # Combine (a,b) and (d,c) to form two complex subimages.\n", + " a,b,c,d = y[0::2, 0::2], y[0::2,1::2], y[1::2,0::2], y[1::2,1::2]\n", + " \n", + " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", + " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", + "\n", + " # Form the 2 highpasses in z.\n", + " return (p-q, p+q)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "class Transform2d(object):\n", + " \"\"\"\n", + " An implementation of the 2D DT-CWT via NumPy. *biort* and *qshift* are the\n", + " wavelets which parameterise the transform.\n", + " If *biort* or *qshift* are strings, they are used as an argument to the\n", + " :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions.\n", + " Otherwise, they are interpreted as tuples of vectors giving filter\n", + " coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In\n", + " the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b).\n", + " \"\"\"\n", + " def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT):\n", + " # Load bi-orthogonal wavelets\n", + " try:\n", + " self.biort = _biort(biort)\n", + " except TypeError:\n", + " self.biort = biort\n", + "\n", + " # Load quarter sample shift wavelets\n", + " try:\n", + " self.qshift = _qshift(qshift)\n", + " except TypeError:\n", + " self.qshift = qshift\n", + "\n", + " def forward(self, X, nlevels=3, include_scale=False):\n", + " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", + " :param X: 2D real array\n", + " :param nlevels: Number of levels of wavelet decomposition\n", + " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", + " .. codeauthor:: Rich Wareham , Aug 2013\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", + " \"\"\"\n", + " # If biort has 6 elements instead of 4, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " if len(self.biort) == 4:\n", + " # h0o, g0o, h1o, g1o = self.biort \n", + " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", + " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", + " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", + " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", + " elif len(self.biort) == 6:\n", + " #h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", + " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", + " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", + " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", + " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", + " h2o = tf.Variable(self.biort[4], trainable=False, name='dtcwt/h2o')\n", + " g2o = tf.Variable(self.biort[5], trainable=False, name='dtcwt/g2o')\n", + " else:\n", + " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", + "\n", + " # If qshift has 12 elements instead of 8, then it's a modified\n", + " # rotationally symmetric wavelet\n", + " \n", + " # We have to reverse the qshift filters, as tensorflow's conv2d is\n", + " # really cross-correlation. Note that we didn't have to do this for\n", + " # biorthogonal filters as they are already symmetric.\n", + " if len(self.qshift) == 8:\n", + " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", + " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", + " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", + " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", + " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", + " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", + " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", + " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", + " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", + " elif len(self.qshift) == 12:\n", + " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10]\n", + " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", + " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", + " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", + " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", + " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", + " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", + " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", + " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", + " h2a = tf.Variable(self.qshift[8][::-1], trainable=False, name='dtcwt/h2a')\n", + " h2b = tf.Variable(self.qshift[9][::-1], trainable=False, name='dtcwt/h2b')\n", + " else:\n", + " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", + "\n", + " # Check the shape of the input\n", + " original_size = X.get_shape().as_list()[1:3]\n", + "\n", + " # The next few lines of code check to see if the image is odd in size, if so an extra ...\n", + " # row/column will be added to the bottom/right of the image\n", + " initial_row_extend = 0 #initialise\n", + " initial_col_extend = 0\n", + " if original_size[0] % 2 != 0:\n", + " # if X.shape[0] is not divisable by 2 then we need to extend X by adding a row at the bottom\n", + " bottom_row = tf.slice(X, [0, original_size[0]-1,0], [-1, 1, -1])\n", + " X = tf.concat([X, bottom_row], axis=1)\n", + " #X = np.vstack((X, X[[-1],:])) # Any further extension will be done in due course.\n", + " initial_row_extend = 1\n", + "\n", + " if original_size[1] % 2 != 0:\n", + " # if X.shape[1] is not divisable by 2 then we need to extend X by adding a col to the left\n", + " right_column = tf.slice(X, [0, 0, original_size[1]-1], [-1, -1, 1])\n", + " X = tf.concat([X, right_column], axis=2)\n", + " #X = np.hstack((X, X[:,[-1]]))\n", + " initial_col_extend = 1\n", + "\n", + " extended_size = X.get_shape().as_list()[1:3]\n", + " \n", + " if nlevels == 0:\n", + " if include_scale:\n", + " return Pyramid(X, (), ())\n", + " else:\n", + " return Pyramid(X, ())\n", + "\n", + " # initialise\n", + " Yh = [None,] * nlevels\n", + " if include_scale:\n", + " # this is only required if the user specifies a third output component.\n", + " Yscale = [None,] * nlevels\n", + "\n", + " #complex_dtype = appropriate_complex_type_for(X)\n", + " \n", + " if nlevels >= 1:\n", + " # Do odd top-level filters on cols.\n", + " Lo = tf.transpose(colfilter(X,h0o), perm=[0,2,1])\n", + " Hi = tf.transpose(colfilter(X,h1o), perm=[0,2,1])\n", + " if len(self.biort) >= 6:\n", + " Ba = tf.tranpsoe(colfilter(X,h2o), perm=[0,2,1])\n", + "\n", + " # Do odd top-level filters on rows.\n", + " LoLo = tf.transpose(colfilter(Lo,h0o), perm=[0,2,1])\n", + " LoLo_shape = LoLo.get_shape().as_list()[1:3]\n", + " Yh[0] = tf.Variable(np.zeros((LoLo_shape[0]>>1, LoLo_shape[1] >>1, 6), dtype=tf.complex64))\n", + " Yh[0][:,:,0], Yh[0][:,:,5] = q2c(tf.transpose(colfilter(Hi,h0o), perm=[0,2,1])) # Horizontal pair\n", + " Yh[0][:,:,2], Yh[0][:,:,3] = q2c(tf.transpose(colfilter(Lo,h1o), perm=[0,2,1])) # Vertical pair\n", + " if len(self.biort) >= 6:\n", + " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.transpose(colfilter(Ba,h2o), perm=[0,2,1])) # Diagonal pair\n", + " else:\n", + " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.tranpose(colfilter(Hi,h1o), perm=[0,2,1])) # Diagonal pair\n", + "\n", + " if include_scale:\n", + " Yscale[0] = LoLo\n", + " \n", + " if include_scale:\n", + " return Pyramid(LoLo, Yh, Yscale)\n", + " else:\n", + " return Pyramid(LoLo, Yh, ())\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "from __future__ import absolute_import, division\n", @@ -160,7 +347,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "def colfilter(X, h):\n", @@ -197,6 +387,40 @@ " # Drop the last dimension\n", " return tf.reshape(y, [-1,r,c])\n", "\n", + "def colfilter2(X, h):\n", + " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [1 0].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m//2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0],[m2, m2], [0, 0]], 'SYMMETRIC')\n", + " N, r, c = X.get_shape().as_list()\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [m, c, 1, 1])\n", + " X = tf.reshape(X, [-1, r, c, 1])\n", + " \n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r,c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", + " return tf.reshape(y, [-1,r,c])\n", + "\n", "def rowfilter(X, h):\n", " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", " If len(h) is odd, each output sample is aligned with each input sample\n", @@ -229,32 +453,113 @@ " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", " r,c = y.get_shape().as_list()[1:3]\n", " # Drop the last dimension\n", + " return tf.reshape(y, [-1,r,c])\n", + "\n", + "def rowfilter2(X, h):\n", + " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [0 1].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m//2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", + " r, c = X.get_shape().as_list()[1:3]\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [1, -1, 1, 1])\n", + " X = tf.reshape(X, [-1, r, c, 1])\n", + " \n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r,c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", " return tf.reshape(y, [-1,r,c])" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "tf.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "tf.reset_default_graph()\n", + "g = tf.get_default_graph()\n", + "dir(g)\n", + "g.get_collection('variables')\n", + "sess = tf.InteractiveSession()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "f = Transform2d()\n", "h1o = tf.constant(f.qshift[0][::-1], dtype=tf.float32)\n", - "i = tf.placeholder(tf.float32, shape=[None, 512, 512])" + "h1o2 = tf.tile(tf.constant(f.qshift[0][::-1], dtype=tf.float32), [1, 512])\n", + "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", + "im_hat = colf(im, f.qshift[0].astype('float32'))\n", + "y1 = colfilter(colfilter(colfilter(in_, h1o),h1o),h1o)\n", + "y2 = rowfilter(in_,h1o)\n", + "y3 = colfilter2(in_, h1o2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "h1o2.shape" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "# Compare the 2\n", - "im_hat = colf(im, f.qshift[0].astype('float32'))\n", - "y1 = colfilter(i, h1o)\n", - "y2 = rowfilter(tf.transpose(i, perm=[0,2,1]),h1o)\n", - "with tf.Session() as sess:\n", - " im_hat1,im_hat2 = (k[0] for k in sess.run([y1,y2], feed_dict={i:[im]}))\n", + "im_hat1,im_hat2 = (k[0] for k in sess.run([y1,y2], feed_dict={in_:[im]}))\n", " \n", "np.testing.assert_array_almost_equal(im_hat, im_hat2.T, decimal=4)" ] @@ -262,200 +567,93 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ - "fig, axes = plt.subplots(nrows=1,ncols=2,figsize=(10,5))\n", - "fig.tight_layout()\n", - "axes[0].imshow(im_hat1, cmap='gray', interpolation='none')\n", - "axes[1].imshow(im_hat2, cmap='gray', interpolation='none')" + "import time\n", + "h = f.qshift[0].astype('float32')\n", + "time1 = time.time()\n", + "for i in range(1000):\n", + " colf(colf(colf(im, h),h),h)\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ - "def q2c(y):\n", - " \"\"\"\n", - " Convert from quads in y to complex numbers in z.\n", - " \"\"\"\n", - "\n", - " # Arrange pixels from the corners of the quads into\n", - " # 2 subimages of alternate real and imag pixels.\n", - " # a----b\n", - " # | |\n", - " # | |\n", - " # c----d\n", - "\n", - " # Combine (a,b) and (d,c) to form two complex subimages.\n", - " a,b,c,d = y[0::2, 0::2], y[0::2,1::2], y[1::2,0::2], y[1::2,1::2]\n", - " \n", - " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", - " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", + "batch = np.stack([im]*100,axis=0)\n", "\n", - " # Form the 2 highpasses in z.\n", - " return (p-q, p+q)" + "time1 = time.time()\n", + "for i in range(10):\n", + " b = sess.run(y1, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ - "class Transform2d(object):\n", - " \"\"\"\n", - " An implementation of the 2D DT-CWT via NumPy. *biort* and *qshift* are the\n", - " wavelets which parameterise the transform.\n", - " If *biort* or *qshift* are strings, they are used as an argument to the\n", - " :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions.\n", - " Otherwise, they are interpreted as tuples of vectors giving filter\n", - " coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In\n", - " the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b).\n", - " \"\"\"\n", - " def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT):\n", - " # Load bi-orthogonal wavelets\n", - " try:\n", - " self.biort = _biort(biort)\n", - " except TypeError:\n", - " self.biort = biort\n", - "\n", - " # Load quarter sample shift wavelets\n", - " try:\n", - " self.qshift = _qshift(qshift)\n", - " except TypeError:\n", - " self.qshift = qshift\n", - "\n", - " def forward(self, X, nlevels=3, include_scale=False):\n", - " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", - " :param X: 2D real array\n", - " :param nlevels: Number of levels of wavelet decomposition\n", - " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", - " .. codeauthor:: Rich Wareham , Aug 2013\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", - " \"\"\"\n", - " # If biort has 6 elements instead of 4, then it's a modified\n", - " # rotationally symmetric wavelet\n", - " if len(self.biort) == 4:\n", - " # h0o, g0o, h1o, g1o = self.biort \n", - " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", - " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", - " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", - " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", - " elif len(self.biort) == 6:\n", - " #h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", - " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", - " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", - " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", - " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", - " h2o = tf.Variable(self.biort[4], trainable=False, name='dtcwt/h2o')\n", - " g2o = tf.Variable(self.biort[5], trainable=False, name='dtcwt/g2o')\n", - " else:\n", - " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", - "\n", - " # If qshift has 12 elements instead of 8, then it's a modified\n", - " # rotationally symmetric wavelet\n", - " \n", - " # We have to reverse the qshift filters, as tensorflow's conv2d is\n", - " # really cross-correlation. Note that we didn't have to do this for\n", - " # biorthogonal filters as they are already symmetric.\n", - " if len(self.qshift) == 8:\n", - " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", - " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", - " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", - " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", - " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", - " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", - " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", - " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", - " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", - " elif len(self.qshift) == 12:\n", - " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10]\n", - " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", - " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", - " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", - " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", - " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", - " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", - " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", - " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", - " h2a = tf.Variable(self.qshift[8][::-1], trainable=False, name='dtcwt/h2a')\n", - " h2b = tf.Variable(self.qshift[9][::-1], trainable=False, name='dtcwt/h2b')\n", - " else:\n", - " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", - "\n", - " # Check the shape of the input\n", - " original_size = X.get_shape().as_list()[1:3]\n", - "\n", - " # The next few lines of code check to see if the image is odd in size, if so an extra ...\n", - " # row/column will be added to the bottom/right of the image\n", - " initial_row_extend = 0 #initialise\n", - " initial_col_extend = 0\n", - " if original_size[0] % 2 != 0:\n", - " # if X.shape[0] is not divisable by 2 then we need to extend X by adding a row at the bottom\n", - " bottom_row = tf.slice(X, [0, original_size[0]-1,0], [-1, 1, -1])\n", - " X = tf.concat([X, bottom_row], axis=1)\n", - " #X = np.vstack((X, X[[-1],:])) # Any further extension will be done in due course.\n", - " initial_row_extend = 1\n", - "\n", - " if original_size[1] % 2 != 0:\n", - " # if X.shape[1] is not divisable by 2 then we need to extend X by adding a col to the left\n", - " right_column = tf.slice(X, [0, 0, original_size[1]-1], [-1, -1, 1])\n", - " X = tf.concat([X, right_column], axis=2)\n", - " #X = np.hstack((X, X[:,[-1]]))\n", - " initial_col_extend = 1\n", - "\n", - " extended_size = X.get_shape().as_list()[1:3]\n", - " \n", - " if nlevels == 0:\n", - " if include_scale:\n", - " return Pyramid(X, (), ())\n", - " else:\n", - " return Pyramid(X, ())\n", - "\n", - " # initialise\n", - " Yh = [None,] * nlevels\n", - " if include_scale:\n", - " # this is only required if the user specifies a third output component.\n", - " Yscale = [None,] * nlevels\n", - "\n", - " #complex_dtype = appropriate_complex_type_for(X)\n", - " \n", - " if nlevels >= 1:\n", - " # Do odd top-level filters on cols.\n", - " Lo = tf.transpose(colfilter(X,h0o), perm=[0,2,1])\n", - " Hi = tf.transpose(colfilter(X,h1o), perm=[0,2,1])\n", - " if len(self.biort) >= 6:\n", - " Ba = tf.tranpsoe(colfilter(X,h2o), perm=[0,2,1])\n", - "\n", - " # Do odd top-level filters on rows.\n", - " LoLo = tf.transpose(colfilter(Lo,h0o), perm=[0,2,1])\n", - " LoLo_shape = LoLo.get_shape().as_list()[1:3]\n", - " Yh[0] = tf.Variable(np.zeros((LoLo_shape[0]>>1, LoLo_shape[1] >>1, 6), dtype=tf.complex64))\n", - " Yh[0][:,:,0], Yh[0][:,:,5] = q2c(tf.transpose(colfilter(Hi,h0o), perm=[0,2,1])) # Horizontal pair\n", - " Yh[0][:,:,2], Yh[0][:,:,3] = q2c(tf.transpose(colfilter(Lo,h1o), perm=[0,2,1])) # Vertical pair\n", - " if len(self.biort) >= 6:\n", - " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.transpose(colfilter(Ba,h2o), perm=[0,2,1])) # Diagonal pair\n", - " else:\n", - " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.tranpose(colfilter(Hi,h1o), perm=[0,2,1])) # Diagonal pair\n", - "\n", - " if include_scale:\n", - " Yscale[0] = LoLo\n", - " \n", - " if include_scale:\n", - " return Pyramid(LoLo, Yh, Yscale)\n", - " else:\n", - " return Pyramid(LoLo, Yh, ())\n", - " " + "time1 = time.time()\n", + "for i in range(100):\n", + " b = sess.run(y2, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "time1 = time.time()\n", + "for i in range(100):\n", + " b = sess.run(y3, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(nrows=1,ncols=2,figsize=(10,5))\n", + "fig.tight_layout()\n", + "axes[0].imshow(im_hat1, cmap='gray', interpolation='none')\n", + "axes[1].imshow(im_hat2, cmap='gray', interpolation='none')" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "P = f.forward(i)" @@ -464,7 +662,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "im = sess.run(x, feed_dict={i: [im]})\n", @@ -474,7 +675,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ " for level in xrange(1, nlevels):\n", @@ -513,7 +717,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "\"\"\"\n", @@ -720,7 +927,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "deletable": true, + "editable": true + }, "outputs": [], "source": [ "def _centered(arr, newsize):\n", @@ -935,9 +1145,9 @@ ], "metadata": { "kernelspec": { - "display_name": "tf", + "display_name": "deconv_tf_vis", "language": "python", - "name": "tf" + "name": "deconv_tf_vis" }, "language_info": { "codemirror_mode": { diff --git a/time tester.ipynb b/time tester.ipynb new file mode 100644 index 0000000..902efba --- /dev/null +++ b/time tester.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# Notebook to compare the timings between the numpy implementation and\n", + "# the tensorflow implementation of the dtcwt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# colf = numpy implementation\n", + "# colfilter = tf implementation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# First test a simple convolution\n", + "f = Transform2d()\n", + "h1o = tf.constant(f.qshift[0][::-1], dtype=tf.float32)\n", + "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", + "im_hat = colf(im, f.qshift[0].astype('float32'))\n", + "y1 = colfilter(in_, h1o)\n", + "y2 = rowfilter(in_,h1o)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "import time\n", + "h = f.qshift[0].astype('float32')\n", + "time1 = time.time()\n", + "for i in range(1000):\n", + " colf(im, h)\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "batch = np.stack([im]*100,axis=0)\n", + "\n", + "time1 = time.time()\n", + "for i in range(10):\n", + " b = sess.run(y1, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# On an M60 tesla, these were about the same time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# Now compare when we have to do multiple convolutions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "time1 = time.time()\n", + "for i in range(1000):\n", + " colf(colf(colf(im, h),h),h)\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "y1 = colfilter(colfilter(colfilter(in_, h1o),h1o),h1o)\n", + "time1 = time.time()\n", + "for i in range(10):\n", + " b = sess.run(y1, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "deconv_tf_vis", + "language": "python", + "name": "deconv_tf_vis" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 92203372628442e6fba59cbd17a10f71a80c4c24 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 1 Mar 2017 16:16:12 +0000 Subject: [PATCH 03/52] Added tf code for the forward 2d transform --- tests/test_tfcoldfilt.py | 42 ++++++++++++++++++++++++++++++ tests/test_tfcolfilter.py | 51 ++++++++++++++++++++++++++++++++++++ tests/test_tfcolifilt.py | 55 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 tests/test_tfcoldfilt.py create mode 100644 tests/test_tfcolfilter.py create mode 100644 tests/test_tfcolifilt.py diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py new file mode 100644 index 0000000..a639e9c --- /dev/null +++ b/tests/test_tfcoldfilt.py @@ -0,0 +1,42 @@ +import os + +import numpy as np +from dtcwt.numpy.lowlevel import coldfilt + +from pytest import raises + +import tests.datasets as datasets + +def setup(): + global mandrill + mandrill = datasets.mandrill() + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + +def test_odd_filter(): + with raises(ValueError): + coldfilt(mandrill, (-1,2,-1), (-1,2,1)) + +def test_different_size(): + with raises(ValueError): + coldfilt(mandrill, (-0.5,-1,2,1,0.5), (-1,2,-1)) + +def test_bad_input_size(): + with raises(ValueError): + coldfilt(mandrill[:511,:], (-1,1), (1,-1)) + +def test_good_input_size(): + coldfilt(mandrill[:,:511], (-1,1), (1,-1)) + +def test_good_input_size_non_orthogonal(): + coldfilt(mandrill[:,:511], (1,1), (1,1)) + +def test_output_size(): + Y = coldfilt(mandrill, (-1,1), (1,-1)) + assert Y.shape == (mandrill.shape[0]/2, mandrill.shape[1]) + +# vim:sw=4:sts=4:et diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py new file mode 100644 index 0000000..e113fa6 --- /dev/null +++ b/tests/test_tfcolfilter.py @@ -0,0 +1,51 @@ +import os + +import numpy as np +import tensorflow as tf +from dtcwt.coeffs import biort, qshift +from dtcwt.tf.lowlevel import colfilter + +import tests.datasets as datasets + +def setup(): + global mandrill, mandrill_t + mandrill = datasets.mandrill() + mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + assert mandrill_t.get_shape() == (1, 512, 512) + +def test_odd_size(): + h = tf.constant([-1,2,-1], dtype=tf.float32) + y_op = colfilter(mandrill_t, h) + assert y_op.get_shape()[1:] == mandrill.shape + +def test_even_size(): + h = tf.constant([-1,-1], dtype=tf.float32) + y_op = colfilter(mandrill_t, h) + assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) + +def test_qshift(): + h = tf.constant(qshift('qshift_a')[0], dtype=tf.float32) + y_op = colfilter(mandrill, h) + assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) + +def test_biort(): + h = tf.constant(biort('antonini')[0], dtype=tf.float32) + y_op = colfilter(mandrill, h) + assert y_op.get_shape()[1:] == mandrill.shape + +def test_even_size(): + h = tf.constant([-1,-1], dtype=tf.float32) + y_op = colfilter(mandrill_t, h) + assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) + with tf.Session() as sess: + y = sess.run(y_op) + assert not np.any(y[:] != 0.0) + + +# vim:sw=4:sts=4:et diff --git a/tests/test_tfcolifilt.py b/tests/test_tfcolifilt.py new file mode 100644 index 0000000..33e41a4 --- /dev/null +++ b/tests/test_tfcolifilt.py @@ -0,0 +1,55 @@ +import os + +import numpy as np +from dtcwt.numpy.lowlevel import colifilt + +from pytest import raises + +import tests.datasets as datasets + +def setup(): + global mandrill + mandrill = datasets.mandrill() + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + +def test_odd_filter(): + with raises(ValueError): + colifilt(mandrill, (-1,2,-1), (-1,2,1)) + +def test_different_size_h(): + with raises(ValueError): + colifilt(mandrill, (-1,2,1), (-0.5,-1,2,-1,0.5)) + +def test_zero_input(): + Y = colifilt(np.zeros_like(mandrill), (-1,1), (1,-1)) + assert np.all(Y[:0] == 0) + +def test_bad_input_size(): + with raises(ValueError): + colifilt(mandrill[:511,:], (-1,1), (1,-1)) + +def test_good_input_size(): + colifilt(mandrill[:,:511], (-1,1), (1,-1)) + +def test_output_size(): + Y = colifilt(mandrill, (-1,1), (1,-1)) + assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + +def test_non_orthogonal_input(): + Y = colifilt(mandrill, (1,1), (1,1)) + assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + +def test_output_size_non_mult_4(): + Y = colifilt(mandrill, (-1,0,0,1), (1,0,0,-1)) + assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + +def test_non_orthogonal_input_non_mult_4(): + Y = colifilt(mandrill, (1,0,0,1), (1,0,0,1)) + assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + +# vim:sw=4:sts=4:et From 5a74d4485c6ce174370934d6d05ca74a511b5af6 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 1 Mar 2017 16:20:05 +0000 Subject: [PATCH 04/52] The last commit was incorrect. This adds the tf functions --- dtcwt/tf/common.py | 36 ++ dtcwt/tf/lowlevel.py | 214 +++++++ dtcwt/tf/transform2d.py | 297 ++++++++++ dtcwt_tf.ipynb | 1243 +++++++++++++++++++++++++-------------- 4 files changed, 1333 insertions(+), 457 deletions(-) create mode 100644 dtcwt/tf/common.py create mode 100644 dtcwt/tf/lowlevel.py create mode 100644 dtcwt/tf/transform2d.py diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py new file mode 100644 index 0000000..1daf8ab --- /dev/null +++ b/dtcwt/tf/common.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import + +from dtcwt.numpy import Pyramid +import tensorflow as tf + +class Pyramid_tf(object): + """A representation of a transform domain signal. + Backends are free to implement any class which respects this interface for + storing transform-domain signals. The inverse transform may accept a + backend-specific version of this class but should always accept any class + which corresponds to this interface. + .. py:attribute:: lowpass + A NumPy-compatible array containing the coarsest scale lowpass signal. + .. py:attribute:: highpasses + A tuple where each element is the complex subband coefficients for + corresponding scales finest to coarsest. + .. py:attribute:: scales + *(optional)* A tuple where each element is a NumPy-compatible array + containing the lowpass signal for corresponding scales finest to + coarsest. This is not required for the inverse and may be *None*. + """ + def __init__(self, lowpass, highpasses, scales=None): + self.lowpass = lowpass + self.highpasses = highpasses + self.scales = scales + + def eval(self, sess, placeholder, data): + lo = sess.run(self.lowpass, {placeholder : data}) + hi = sess.run(self.highpasses, {placeholder : data}) + if self.scales is not None: + scales = sess.run(self.scales, {placeholder : data}) + else: + scales = None + + return Pyramid(lo, hi, scales) + diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py new file mode 100644 index 0000000..9c9d480 --- /dev/null +++ b/dtcwt/tf/lowlevel.py @@ -0,0 +1,214 @@ +from __future__ import absolute_import + +import tensorflow as tf +import numpy as np + +def colfilter(X, h): + """Filter the columns of image *X* using filter vector *h*, without decimation. + If len(h) is odd, each output sample is aligned with each input sample + and *Y* is the same size as *X*. If len(h) is even, each output sample is + aligned with the mid point of each pair of input samples, and Y.shape = + X.shape + [1 0]. + :param X: an image whose columns are to be filtered + :param h: the filter coefficients. + :returns Y: the filtered image. + .. codeauthor:: Rich Wareham , August 2013 + .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 + .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 + """ + + m = h.get_shape().as_list()[0] + m2 = m // 2 + + # Symmetrically extend with repeat of end samples. + # Pad only the second dimension of the tensor X (the columns) + X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') + + # Reshape h to be a col filter. We have to flip h too as the tf conv2d + # operation is cross-correlation, not true convolution + h = tf.reshape(h[::-1], [-1, 1, 1, 1]) + + # Reshape X from [batch, rows, cols] to [batch, rows, cols, 1] for conv2d + X = tf.expand_dims(X, axis=-1) + + Y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID') + + # Drop the last dimension + return tf.unstack(Y, num=1, axis=-1)[0] + +def rowfilter(X, h): + """Filter the rows of image *X* using filter vector *h*, without decimation. + If len(h) is odd, each output sample is aligned with each input sample + and *Y* is the same size as *X*. If len(h) is even, each output sample is + aligned with the mid point of each pair of input samples, and Y.shape = + X.shape + [0 1]. + :param X: an image whose columns are to be filtered + :param h: the filter coefficients. + :returns Y: the filtered image. + .. codeauthor:: Rich Wareham , August 2013 + .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 + .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 + """ + + m = h.get_shape().as_list()[0] + m2 = m // 2 + + # Symmetrically extend with repeat of end samples. + # Pad only the second dimension of the tensor X (the columns) + X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') + + # Reshape h to be a row filter. We have to flip h too as the tf conv2d + # operation is cross-correlation, not true convolution + h = tf.reshape(h[::-1], [1, -1, 1, 1]) + + # Reshape X from [batch, rows, cols] to [batch, rows, cols, 1] for conv2d + X = tf.expand_dims(X, axis=-1) + + Y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID') + + # Drop the last dimension + return tf.unstack(Y, num=1, axis=-1)[0] + + +def coldfilt(X, ha, hb, a_first=True): + """Filter the columns of image X using the two filters ha and hb = + reverse(ha). + ha operates on the odd samples of X and hb on the even samples. + Both filters should be even length, and h should be approx linear + phase with a quarter sample (i.e. an :math:`e^{j \pi/4}`) advance from + its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`). + .. code-block:: text + ext top edge bottom edge ext + Level 1: ! | ! | ! + odd filt on . b b b b a a a a a a a a b b b b + odd filt on . a a a a b b b b b b b b a a a a + Level 2: ! | ! | ! + +q filt on x b b a a a a b b + -q filt on o a a b b b b a a + The output is decimated by two from the input sample rate and the results + from the two filters, Ya and Yb, are interleaved to give Y. + Symmetric extension with repeated end samples is used on the composite X columns + before each filter is applied. + Raises ValueError if the number of rows in X is not a multiple of 4, the + length of ha does not match hb or the lengths of ha or hb are non-even. + .. codeauthor:: Rich Wareham , August 2013 + .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 + .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 + """ + + r, c = X.get_shape().as_list()[1:] + r2 = r // 2 + if r % 4 != 0: + raise ValueError('No. of rows in X must be a multiple of 4') + + if ha.shape != hb.shape: + raise ValueError('Shapes of ha and hb must be the same') + + m = ha.get_shape().as_list()[0] + if m % 2 != 0: + raise ValueError('Lengths of ha and hb must be even') + + # Symmetrically extend with repeat of end samples. + # Pad only the second dimension of the tensor X (the columns). + X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') + + # Take the odd and even columns of X + X_odd = tf.expand_dims(X[:,2:r+2*m-2:2,:], axis=-1) + X_even =tf.expand_dims(X[:,3:r+2*m-2:2,:], axis=-1) + + # Transform ha and hb to be col filters. We must reverse them as tf conv is + # cross correlation, not true convolution + ha = tf.reshape(ha[::-1], [m,1,1,1]) + hb = tf.reshape(hb[::-1], [m,1,1,1]) + + # Do the 2d convolution, but only evaluated at every second sample + # for both X_odd and X_even + a_rows = tf.nn.conv2d(X_odd, ha, strides=[1,2,1,1], padding='VALID') + b_rows = tf.nn.conv2d(X_even, hb, strides=[1,2,1,1], padding='VALID') + + # We interleave the two results into a tensor of size [Batch, r/2, c] + # Concat a_rows and b_rows (both of shape [Batch, r/4, c, 1]) + Y = tf.cond(tf.reduce_sum(ha*hb) > 0, + lambda: tf.concat([a_rows,b_rows],axis=-1), + lambda: tf.concat([b_rows,a_rows],axis=-1)) + + # Permute result to be shape [Batch, r/4, 2, c] + Y = tf.transpose(Y, perm=[0,1,3,2]) + + # Reshape result to be shape [Batch, r/2, c]. This reshaping interleaves + # the columns + Y = tf.reshape(Y, [-1, r2, c]) + + return Y + + +def rowdfilt(X, ha, hb): + """Filter the rows of image X using the two filters ha and hb = + reverse(ha). ha operates on the odd samples of X and hb on the even + samples. Both filters should be even length, and h should be approx linear + phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| > + |h(m/2 + 1)|`). + .. code-block:: text + ext top edge bottom edge ext + Level 1: ! | ! | ! + odd filt on . b b b b a a a a a a a a b b b b + odd filt on . a a a a b b b b b b b b a a a a + Level 2: ! | ! | ! + +q filt on x b b a a a a b b + -q filt on o a a b b b b a a + The output is decimated by two from the input sample rate and the results + from the two filters, Ya and Yb, are interleaved to give Y. Symmetric + extension with repeated end samples is used on the composite X rows + before each filter is applied. + Raises ValueError if the number of columns in X is not a multiple of 4, the + length of ha does not match hb or the lengths of ha or hb are non-even. + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , August 2013 + .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 + .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 + """ + + r, c = X.get_shape().as_list()[1:] + c2 = c // 2 + if c % 4 != 0: + raise ValueError('No. of rows in X must be a multiple of 4') + + if ha.shape != hb.shape: + raise ValueError('Shapes of ha and hb must be the same') + + m = ha.get_shape().as_list()[0] + if m % 2 != 0: + raise ValueError('Lengths of ha and hb must be even') + + # Symmetrically extend with repeat of end samples. + # Pad only the second dimension of the tensor X (the rows). + # SYMMETRIC extension means the edge sample is repeated twice, whereas + # REFLECT only has the edge sample once + X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC') + + # Take the odd and even columns of X + X_odd = tf.expand_dims(X[:,:,2:c+2*m-2:2], axis=-1) + X_even =tf.expand_dims(X[:,:,3:c+2*m-2:2], axis=-1) + + # Transform ha and hb to be col filters. We must reverse them as tf conv is + # cross correlation, not true convolution + ha = tf.reshape(ha[::-1], [m,1,1,1]) + hb = tf.reshape(hb[::-1], [m,1,1,1]) + + # Do the 2d convolution, but only evaluated at every second sample + # for both X_odd and X_even + a_cols = tf.nn.conv2d(X_odd, ha, strides=[1,1,2,1], padding='VALID') + b_cols = tf.nn.conv2d(X_even, hb, strides=[1,1,2,1], padding='VALID') + + # We interleave the two results into a tensor of size [Batch, r/2, c] + # Concat a_cols and b_cols (both of shape [Batch, r, c/4, 1]) + Y = tf.cond(tf.reduce_sum(ha*hb) > 0, + lambda: tf.concat([a_cols,b_cols],axis=-1), + lambda: tf.concat([b_cols,a_cols],axis=-1)) + + # Reshape result to be shape [Batch, r, c/2]. This reshaping interleaves + # the columns + Y = tf.reshape(Y, [-1, r, c2]) + + return Y + diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py new file mode 100644 index 0000000..55ffb05 --- /dev/null +++ b/dtcwt/tf/transform2d.py @@ -0,0 +1,297 @@ +from __future__ import absolute_import + +import numpy as np +import tensorflow as tf +import logging + +from dtcwt.coeffs import biort as _biort, qshift as _qshift +from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT + +from dtcwt.tf.common import Pyramid_tf +from dtcwt.tf.lowlevel import * + +class Transform2d(object): + """ + An implementation of the 2D DT-CWT via Tensorflow. + *biort* and *qshift* are the wavelets which parameterise the transform. + If *biort* or *qshift* are strings, they are used as an argument to the + :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions. + Otherwise, they are interpreted as tuples of vectors giving filter + coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In + the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). + + Creating an object of this class loads the necessary filters onto the + tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with + a placeholder will create a forward transform for an input of the placeholder's + size. You can evaluate the resulting ops several times feeding different + images into the placeholder *assuming* they have the same resolution. For + a different resolution image, call the :py:func:`Transform2d.forward` + function again. + """ + + def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): + # Load bi-orthogonal wavelets + try: + self.biort = _biort(biort) + b = self.biort + except TypeError: + self.biort = biort + b = self.biort + + # Load quarter sample shift wavelets + try: + self.qshift = _qshift(qshift) + q = self.qshift + except TypeError: + self.qshift = qshift + q = self.qshift + + ### Load the ops onto the graph for the filter banks + + # If biort has 6 elements instead of 4, then it's a modified + # rotationally symmetric wavelet + # h0o - analysis low pass filter + # g0o - synthesis low pass filter + # h1o - analysis high pass filter + # g1o - synthesis high pass filter + # h2o - analysis band pass filter for 45 deg wavelets + # g2o - synthesis band pass filter for 45 deg wavelets + if len(b) == 4: + # h0o, g0o, h1o, g1o = b + self.h0o = tf.constant(b[0], dtype=tf.float32, name='dtcwt/h0o') + self.g0o = tf.constant(b[1], dtype=tf.float32, name='dtcwt/g0o') + self.h1o = tf.constant(b[2], dtype=tf.float32, name='dtcwt/h1o') + self.g1o = tf.constant(b[3], dtype=tf.float32, name='dtcwt/g1o') + elif len(b) == 6: + #h0o, g0o, h1o, g1o, h2o, g2o = b + self.h0o = tf.constant(b[0], dtype=tf.float32, name='dtcwt/h0o') + self.g0o = tf.constant(b[1], dtype=tf.float32, name='dtcwt/g0o') + self.h1o = tf.constant(b[2], dtype=tf.float32, name='dtcwt/h1o') + self.g1o = tf.constant(b[3], dtype=tf.float32, name='dtcwt/g1o') + self.h2o = tf.constant(b[4], dtype=tf.float32, name='dtcwt/h2o') + self.g2o = tf.constant(b[5], dtype=tf.float32, name='dtcwt/g2o') + else: + raise ValueError('Biort wavelet must have 6 or 4 components.') + + + # If qshift has 12 elements instead of 8, then it's a modified + # rotationally symmetric wavelet + # h0a - analysis low pass filter tree a + # h0b - analysis low pass filter tree b + # h1a - analysis high pass filter tree a + # h1b - analysis high pass filter tree b + # h2a - analysis band pass filter tree a (for 45 deg wavelets) + # h2b - analysis band pass filter tree b (for 45 deg wavelets) + # g.. - synthesis equivalents + if len(q) == 8: + #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = q + self.h0a = tf.constant(q[0], dtype=tf.float32, name='dtcwt/h0a') + self.h0b = tf.constant(q[1], dtype=tf.float32, name='dtcwt/h0b') + self.g0a = tf.constant(q[2], dtype=tf.float32, name='dtcwt/g0a') + self.g0b = tf.constant(q[3], dtype=tf.float32, name='dtcwt/g0b') + self.h1a = tf.constant(q[4], dtype=tf.float32, name='dtcwt/h1a') + self.h1b = tf.constant(q[5], dtype=tf.float32, name='dtcwt/h1b') + self.g1a = tf.constant(q[6], dtype=tf.float32, name='dtcwt/g1a') + self.g1b = tf.constant(q[7], dtype=tf.float32, name='dtcwt/g1b') + elif len(q) == 12: + #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = q[:10] + self.h0a = tf.constant(q[0], dtype=tf.float32, name='dtcwt/h0a') + self.h0b = tf.constant(q[1], dtype=tf.float32, name='dtcwt/h0b') + self.g0a = tf.constant(q[2], dtype=tf.float32, name='dtcwt/g0a') + self.g0b = tf.constant(q[3], dtype=tf.float32, name='dtcwt/g0b') + self.h1a = tf.constant(q[4], dtype=tf.float32, name='dtcwt/h1a') + self.h1b = tf.constant(q[5], dtype=tf.float32, name='dtcwt/h1b') + self.g1a = tf.constant(q[6], dtype=tf.float32, name='dtcwt/g1a') + self.g1b = tf.constant(q[7], dtype=tf.float32, name='dtcwt/g1b') + self.h2a = tf.constant(q[8], dtype=tf.float32, name='dtcwt/h2a') + self.h2b = tf.constant(q[9], dtype=tf.float32, name='dtcwt/h2b') + else: + raise ValueError('Qshift wavelet must have 12 or 8 components.') + + + + def forward(self, X, nlevels=3, include_scale=False): + """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. + :param X: 3D real array of size [Batch, rows, cols] + :param nlevels: Number of levels of wavelet decomposition + :param include_scale: True if you want to receive the lowpass coefficients at + intermediate layers. + :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 + """ + + # Check the shape of the input + original_size = X.get_shape().as_list()[1:] + + if len(original_size) >= 3: + raise ValueError('The entered image is {0}, please enter each image slice separately.'. + format('x'.join(list(str(s) for s in X.get_shape().as_list())))) + + + ############################## Resize ################################# + # The next few lines of code check to see if the image is odd in size, + # if so an extra ... row/column will be added to the bottom/right of the + # image + initial_row_extend = 0 + initial_col_extend = 0 + # If the row count of X is not divisible by 2 then we need to + # extend X by adding a row at the bottom + if original_size[0] % 2 != 0: + bottom_row = tf.slice(X, [0, original_size[0] - 1, 0], [-1, 1, -1]) + X = tf.concat([X, bottom_row], axis=1) + initial_row_extend = 1 + + # If the col count of X is not divisible by 2 then we need to + # extend X by adding a col to the right + if original_size[1] % 2 != 0: + right_col = tf.slice(X, [0, 0, original_size[1] - 1], [-1, -1, 1]) + X = tf.concat([X, right_col], axis=2) + initial_col_extend = 1 + + extended_size = X.get_shape().as_list()[1:3] + + if nlevels == 0: + if include_scale: + return Pyramid_ops(X, (), ()) + else: + return Pyramid_ops(X, ()) + + + ############################ Initialise ############################### + Yh = [None,] * nlevels + if include_scale: + # this is only required if the user specifies a third output component. + Yscale = [None,] * nlevels + + ############################# Level 1 ################################# + # Uses the biorthogonal filters + if nlevels >= 1: + # Do odd top-level filters on cols. + Lo = colfilter(X, self.h0o) + Hi = colfilter(X, self.h1o) + if len(self.biort) >= 6: + Ba = colfilter(X, self.h2o) + + # Do odd top-level filters on rows. + LoLo = rowfilter(Lo, self.h0o) + LoLo_shape = LoLo.get_shape().as_list()[1:3] + + # Horizontal wavelet pair (15 & 165 degrees) + horiz = q2c(rowfilter(Hi, self.h0o)) + + # Vertical wavelet pair (75 & 105 degrees) + vertic = q2c(rowfilter(Lo, self.h1o)) + + # Diagonal wavelet pair (45 & 135 degrees) + if len(self.biort) >= 6: + diag = q2c(rowfilter(Ba, self.h2o)) + else: + diag = q2c(rowfilter(Hi, self.h1o)) + + # Pack all 6 tensors into one + Yh[0] = tf.stack( + [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], + axis=3) + + if include_scale: + Yscale[0] = LoLo + + + ############################# Level 2+ ################################ + # Uses the qshift filters + for level in xrange(1, nlevels): + row_size, col_size = LoLo_shape[0], LoLo_shape[1] + # If the row count of LoLo is not divisible by 4 (it will be + # divisible by 2), add 2 extra rows to make it so + if row_size % 4 != 0: + bottom_row = tf.slice(LoLo, [0, row_size - 2, 0], [-1, 2, -1]) + LoLo = tf.concat([LoLo, bottom_row], axis=1) + + # If the col count of LoLo is not divisible by 4 (it will be + # divisible by 2), add 2 extra cols to make it so + if col_size % 4 != 0: + right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2]) + LoLo = tf.concat([LoLo, right_col], axis=2) + + # Do even Qshift filters on cols. + Lo = coldfilt(LoLo, self.h0b, self.h0a) + Hi = coldfilt(LoLo, self.h1b, self.h1a) + if len(self.qshift) >= 12: + Ba = coldfilt(LoLo, self.h2b, self.h2a) + + # Do even Qshift filters on rows. + LoLo = rowdfilt(Lo, self.h0b, self.h0a) + LoLo_shape = LoLo.get_shape().as_list()[1:3] + + # Horizontal wavelet pair (15 & 165 degrees) + horiz = q2c(rowdfilt(Hi, self.h0b, self.h0a)) + + # Vertical wavelet pair (75 & 105 degrees) + vertic = q2c(rowdfilt(Lo, self.h1b, self.h1a)) + + # Diagonal wavelet pair (45 & 135 degrees) + if len(self.qshift) >= 12: + diag = q2c(rowdfilt(Ba, self.h2b, self.h2a)) + else: + diag = q2c(rowdfilt(Hi, self.h1b, self.h1a)) + + # Pack all 6 tensors into one + Yh[level] = tf.stack( + [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], + axis=3) + + if include_scale: + Yscale[level] = LoLo + + Yl = LoLo + + if initial_row_extend == 1 and initial_col_extend == 1: + logging.warn('The image entered is now a {0} NOT a {1}.'.format( + 'x'.join(list(str(s) for s in extended_size)), + 'x'.join(list(str(s) for s in original_size)))) + logging.warn( + 'The bottom row and rightmost column have been duplicated, prior to decomposition.') + + if initial_row_extend == 1 and initial_col_extend == 0: + logging.warn('The image entered is now a {0} NOT a {1}.'.format( + 'x'.join(list(str(s) for s in extended_size)), + 'x'.join(list(str(s) for s in original_size)))) + logging.warn( + 'The bottom row has been duplicated, prior to decomposition.') + + if initial_row_extend == 0 and initial_col_extend == 1: + logging.warn('The image entered is now a {0} NOT a {1}.'.format( + 'x'.join(list(str(s) for s in extended_size)), + 'x'.join(list(str(s) for s in original_size)))) + logging.warn( + 'The rightmost column has been duplicated, prior to decomposition.') + + if include_scale: + return Pyramid_ops(Yl, tuple(Yh), tuple(Yscale)) + else: + return Pyramid_ops(Yl, tuple(Yh)) + + +def q2c(y): + """ + Convert from quads in y to complex numbers in z. + """ + + # Arrange pixels from the corners of the quads into + # 2 subimages of alternate real and imag pixels. + # a----b + # | | + # | | + # c----d + # Combine (a,b) and (d,c) to form two complex subimages. + a,b,c,d = y[:, 0::2, 0::2], y[:, 0::2,1::2], y[:, 1::2,0::2], y[:, 1::2,1::2] + + p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2) + q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2) + + # Form the 2 highpasses in z. + return (p-q, p+q) + diff --git a/dtcwt_tf.ipynb b/dtcwt_tf.ipynb index d2df24c..987a6de 100644 --- a/dtcwt_tf.ipynb +++ b/dtcwt_tf.ipynb @@ -47,28 +47,49 @@ }, "outputs": [], "source": [ - "class Pyramid(object):\n", - " \"\"\"A representation of a transform domain signal.\n", - " Backends are free to implement any class which respects this interface for\n", - " storing transform-domain signals. The inverse transform may accept a\n", - " backend-specific version of this class but should always accept any class\n", - " which corresponds to this interface.\n", - " .. py:attribute:: lowpass\n", - " A NumPy-compatible array containing the coarsest scale lowpass signal.\n", - " .. py:attribute:: highpasses\n", - " A tuple where each element is the complex subband coefficients for\n", - " corresponding scales finest to coarsest.\n", - " .. py:attribute:: scales\n", - " *(optional)* A tuple where each element is a NumPy-compatible array\n", - " containing the lowpass signal for corresponding scales finest to\n", - " coarsest. This is not required for the inverse and may be *None*.\n", + "from __future__ import absolute_import, division\n", + "\n", + "__all__ = [ 'colfilter', 'colifilt', 'coldfilt', ]\n", + "\n", + "import numpy as np\n", + "from six.moves import xrange\n", + "from dtcwt.utils import as_column_vector, asfarray, appropriate_complex_type_for, reflect\n", + "\n", + "def _centered(arr, newsize):\n", + " # Return the center newsize portion of the array.\n", + " # (Shamelessly cribbed from scipy.)\n", + " newsize = np.asanyarray(newsize)\n", + " currsize = np.array(arr.shape)\n", + " startind = (currsize - newsize) // 2\n", + " endind = startind + newsize\n", + " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", + " return arr[tuple(myslice)]\n", + "\n", + "# This is to allow easy replacement of these later with, possibly, GPU versions\n", + "_rfft = np.fft.rfft\n", + "_irfft = np.fft.irfft\n", + "\n", + "def _column_convolve(X, h):\n", + " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", + " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", + " *h*, the output will have the dtype of *X* appropriately expanded to a\n", + " floating point type if necessary.\n", + " We assume that h is small and so direct convolution is the most efficient.\n", " \"\"\"\n", - " def __init__(self, lowpass, highpasses, scales=None):\n", - " self.lowpass = tf.Variable(lowpass, trainable=False, dtype=tf.float32)\n", - " self.highpasses = tuple(tf.Variable(x, trainable=False, dtype=tf.complex64) \n", - " if x is not None else None for x in highpasses)\n", - " self.scales = tuple(tf.Variable(x, trainable=False, dtype=tf.float32) \n", - " for x in scales) if scales is not None else None" + " Xshape = np.asanyarray(X.shape)\n", + " h = h.flatten().astype(X.dtype)\n", + " h_size = h.shape[0]\n", + "\n", + " full_size = X.shape[0] + h_size - 1\n", + " Xshape[0] = full_size\n", + "\n", + " out = np.zeros(Xshape, dtype=X.dtype)\n", + " for idx in xrange(h_size):\n", + " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", + "\n", + " outShape = Xshape.copy()\n", + " outShape[0] = abs(X.shape[0] - h_size) + 1\n", + " return _centered(out, outShape)\n" ] }, { @@ -79,27 +100,146 @@ "editable": true }, "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true + }, "source": [ - "def q2c(y):\n", - " \"\"\"\n", - " Convert from quads in y to complex numbers in z.\n", - " \"\"\"\n", - "\n", - " # Arrange pixels from the corners of the quads into\n", - " # 2 subimages of alternate real and imag pixels.\n", - " # a----b\n", - " # | |\n", - " # | |\n", - " # c----d\n", + "# Test outputs give expected results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "tf.reset_default_graph()\n", + "g = tf.get_default_graph()\n", + "dir(g)\n", + "g.get_collection('variables')\n", + "sess = tf.InteractiveSession(config=tf.ConfigProto(log_device_placement=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "#h1o = tf.Variable(f.qshift[0][::-1],trainable=False, dtype=tf.float32)\n", + "f = dtcwt.Transform2d()\n", + "h1o = tf.constant(f.qshift[0][::-1],dtype=tf.float32)\n", + " \n", + "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", + "init_op = tf.global_variables_initializer()\n", "\n", - " # Combine (a,b) and (d,c) to form two complex subimages.\n", - " a,b,c,d = y[0::2, 0::2], y[0::2,1::2], y[1::2,0::2], y[1::2,1::2]\n", + "qshift = f.qshift[0].astype('float32')\n", + "im_hat = colf(colf(colf(im, qshift),qshift),qshift)\n", + "y1 = colfilter(colfilter(colfilter(in_, h1o), h1o), h1o)\n", + "y2 = rowfilter(rowfilter(rowfilter(in_, h1o), h1o), h1o)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "# Compare the 2\n", + "sess.run(init_op)\n", + "im_hat1 = sess.run(y1, feed_dict={in_:[im]})[0]\n", + "im_hat2 = sess.run(y2, feed_dict={in_:[im.T]})[0]\n", " \n", - " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", - " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", + "np.testing.assert_array_almost_equal(im_hat, im_hat1, decimal=4)\n", + "np.testing.assert_array_almost_equal(im_hat, im_hat2.T, decimal=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true + }, + "source": [ + "# Compare the execution times for direct filtering and GPU filtering" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "import time\n", + "h = f.qshift[0].astype('float32')\n", + "time1 = time.time()\n", + "for i in range(1000):\n", + " colf(colf(colf(im, h),h),h)\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "batch = np.stack([im]*100,axis=0)\n", "\n", - " # Form the 2 highpasses in z.\n", - " return (p-q, p+q)" + "time1 = time.time()\n", + "for i in range(10):\n", + " b = sess.run(y1, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": true, + "editable": true + }, + "outputs": [], + "source": [ + "batch = np.stack([im]*100,axis=0)\n", + "\n", + "time1 = time.time()\n", + "for i in range(10):\n", + " b = sess.run(y2, feed_dict={in_:batch})\n", + "time2 = time.time()\n", + "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "deletable": true, + "editable": true + }, + "source": [ + "# Redefine the Transform" ] }, { @@ -113,14 +253,23 @@ "source": [ "class Transform2d(object):\n", " \"\"\"\n", - " An implementation of the 2D DT-CWT via NumPy. *biort* and *qshift* are the\n", - " wavelets which parameterise the transform.\n", + " An implementation of the 2D DT-CWT via Tensorflow. \n", + " *biort* and *qshift* are the wavelets which parameterise the transform.\n", " If *biort* or *qshift* are strings, they are used as an argument to the\n", " :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions.\n", " Otherwise, they are interpreted as tuples of vectors giving filter\n", " coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In\n", " the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b).\n", + " \n", + " Creating an object of this class loads the necessary filters onto the \n", + " tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with \n", + " a placeholder will create a forward transform for an input of the placeholder's\n", + " size. You can evaluate the resulting ops several times feeding different\n", + " images into the placeholder *assuming* they have the same resolution. For \n", + " a different resolution image, call the :py:func:`Transform2d.forward` \n", + " function again.\n", " \"\"\"\n", + "\n", " def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT):\n", " # Load bi-orthogonal wavelets\n", " try:\n", @@ -134,407 +283,254 @@ " except TypeError:\n", " self.qshift = qshift\n", "\n", - " def forward(self, X, nlevels=3, include_scale=False):\n", - " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", - " :param X: 2D real array\n", - " :param nlevels: Number of levels of wavelet decomposition\n", - " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", - " .. codeauthor:: Rich Wareham , Aug 2013\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", - " \"\"\"\n", + " ### Load the ops onto the graph for the filter banks\n", + "\n", " # If biort has 6 elements instead of 4, then it's a modified\n", " # rotationally symmetric wavelet\n", + " # h0o - analysis low pass filter\n", + " # g0o - synthesis low pass filter\n", + " # h1o - analysis high pass filter\n", + " # g1o - synthesis high pass filter\n", + " # h2o - analysis band pass filter for 45 deg wavelets\n", + " # g2o - synthesis band pass filter for 45 deg wavelets\n", " if len(self.biort) == 4:\n", " # h0o, g0o, h1o, g1o = self.biort \n", - " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", - " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", - " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", - " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", + " self.h0o = tf.constant(self.biort[0], dtype=tf.float32, name='dtcwt/h0o')\n", + " self.g0o = tf.constant(self.biort[1], dtype=tf.float32, name='dtcwt/g0o')\n", + " self.h1o = tf.constant(self.biort[2], dtype=tf.float32, name='dtcwt/h1o')\n", + " self.g1o = tf.constant(self.biort[3], dtype=tf.float32, name='dtcwt/g1o')\n", " elif len(self.biort) == 6:\n", " #h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", - " h0o = tf.Variable(self.biort[0], trainable=False, name='dtcwt/h0o')\n", - " g0o = tf.Variable(self.biort[1], trainable=False, name='dtcwt/g0o')\n", - " h1o = tf.Variable(self.biort[2], trainable=False, name='dtcwt/h1o')\n", - " g1o = tf.Variable(self.biort[3], trainable=False, name='dtcwt/g1o')\n", - " h2o = tf.Variable(self.biort[4], trainable=False, name='dtcwt/h2o')\n", - " g2o = tf.Variable(self.biort[5], trainable=False, name='dtcwt/g2o')\n", + " self.h0o = tf.constant(self.biort[0], dtype=tf.float32, name='dtcwt/h0o')\n", + " self.g0o = tf.constant(self.biort[1], dtype=tf.float32, name='dtcwt/g0o')\n", + " self.h1o = tf.constant(self.biort[2], dtype=tf.float32, name='dtcwt/h1o')\n", + " self.g1o = tf.constant(self.biort[3], dtype=tf.float32, name='dtcwt/g1o')\n", + " self.h2o = tf.constant(self.biort[4], dtype=tf.float32, name='dtcwt/h2o')\n", + " self.g2o = tf.constant(self.biort[5], dtype=tf.float32, name='dtcwt/g2o')\n", " else:\n", " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", "\n", + " \n", " # If qshift has 12 elements instead of 8, then it's a modified\n", - " # rotationally symmetric wavelet\n", + " # rotationally symmetric wavelet \n", + " # h0a - analysis low pass filter tree a\n", + " # h0b - analysis low pass filter tree b\n", + " # h1a - analysis high pass filter tree a\n", + " # h1b - analysis high pass filter tree b\n", + " # h2a - analysis band pass filter tree a (for 45 deg wavelets)\n", + " # h2b - analysis band pass filter tree b (for 45 deg wavelets)\n", + " # g.. - synthesis equivalents\n", " \n", " # We have to reverse the qshift filters, as tensorflow's conv2d is\n", " # really cross-correlation. Note that we didn't have to do this for\n", " # biorthogonal filters as they are already symmetric.\n", " if len(self.qshift) == 8:\n", " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", - " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", - " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", - " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", - " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", - " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", - " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", - " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", - " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", + " self.h0a = tf.constant(self.qshift[0][::-1], dtype=tf.float32, name='dtcwt/h0a')\n", + " self.h0b = tf.constant(self.qshift[1][::-1], dtype=tf.float32, name='dtcwt/h0b')\n", + " self.g0a = tf.constant(self.qshift[2][::-1], dtype=tf.float32, name='dtcwt/g0a')\n", + " self.g0b = tf.constant(self.qshift[3][::-1], dtype=tf.float32, name='dtcwt/g0b')\n", + " self.h1a = tf.constant(self.qshift[4][::-1], dtype=tf.float32, name='dtcwt/h1a')\n", + " self.h1b = tf.constant(self.qshift[5][::-1], dtype=tf.float32, name='dtcwt/h1b')\n", + " self.g1a = tf.constant(self.qshift[6][::-1], dtype=tf.float32, name='dtcwt/g1a')\n", + " self.g1b = tf.constant(self.qshift[7][::-1], dtype=tf.float32, name='dtcwt/g1b')\n", " elif len(self.qshift) == 12:\n", " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10]\n", - " h0a = tf.Variable(self.qshift[0][::-1], trainable=False, name='dtcwt/h0a')\n", - " h0b = tf.Variable(self.qshift[1][::-1], trainable=False, name='dtcwt/h0b')\n", - " g0a = tf.Variable(self.qshift[2][::-1], trainable=False, name='dtcwt/g0a')\n", - " g0a = tf.Variable(self.qshift[3][::-1], trainable=False, name='dtcwt/g0b')\n", - " h1a = tf.Variable(self.qshift[4][::-1], trainable=False, name='dtcwt/h1a')\n", - " h1b = tf.Variable(self.qshift[5][::-1], trainable=False, name='dtcwt/h1b')\n", - " g1a = tf.Variable(self.qshift[6][::-1], trainable=False, name='dtcwt/g1a')\n", - " g1b = tf.Variable(self.qshift[7][::-1], trainable=False, name='dtcwt/g1b')\n", - " h2a = tf.Variable(self.qshift[8][::-1], trainable=False, name='dtcwt/h2a')\n", - " h2b = tf.Variable(self.qshift[9][::-1], trainable=False, name='dtcwt/h2b')\n", + " self.h0a = tf.constant(self.qshift[0][::-1], dtype=tf.float32, name='dtcwt/h0a')\n", + " self.h0b = tf.constant(self.qshift[1][::-1], dtype=tf.float32, name='dtcwt/h0b')\n", + " self.g0a = tf.constant(self.qshift[2][::-1], dtype=tf.float32, name='dtcwt/g0a')\n", + " self.g0b = tf.constant(self.qshift[3][::-1], dtype=tf.float32, name='dtcwt/g0b')\n", + " self.h1a = tf.constant(self.qshift[4][::-1], dtype=tf.float32, name='dtcwt/h1a')\n", + " self.h1b = tf.constant(self.qshift[5][::-1], dtype=tf.float32, name='dtcwt/h1b')\n", + " self.g1a = tf.constant(self.qshift[6][::-1], dtype=tf.float32, name='dtcwt/g1a')\n", + " self.g1b = tf.constant(self.qshift[7][::-1], dtype=tf.float32, name='dtcwt/g1b')\n", + " self.h2a = tf.constant(self.qshift[8][::-1], dtype=tf.float32, name='dtcwt/h2a')\n", + " self.h2b = tf.constant(self.qshift[9][::-1], dtype=tf.float32, name='dtcwt/h2b')\n", " else:\n", " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", "\n", + " \n", + " \n", + " def forward(self, X, nlevels=3, include_scale=False):\n", + " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", + " :param X: 3D real array of size [Batch, rows, cols]\n", + " :param nlevels: Number of levels of wavelet decomposition\n", + " :param include_scale: True if you want to receive the lowpass coefficients at\n", + " intermediate layers.\n", + " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", + " .. codeauthor:: Fergal Cotter , Feb 2017\n", + " .. codeauthor:: Rich Wareham , Aug 2013\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", + " \"\"\"\n", + "\n", " # Check the shape of the input\n", - " original_size = X.get_shape().as_list()[1:3]\n", + " original_size = X.get_shape().as_list()[1:]\n", + " \n", + " if len(original_size) >= 3:\n", + " raise ValueError('The entered image is {0}, please enter each image slice separately.'.\n", + " format('x'.join(list(str(s) for s in X.shape))))\n", + "\n", "\n", - " # The next few lines of code check to see if the image is odd in size, if so an extra ...\n", - " # row/column will be added to the bottom/right of the image\n", + " ############################## Resize #################################\n", + " # The next few lines of code check to see if the image is odd in size, \n", + " # if so an extra ... row/column will be added to the bottom/right of the \n", + " # image\n", " initial_row_extend = 0 #initialise\n", " initial_col_extend = 0\n", " if original_size[0] % 2 != 0:\n", - " # if X.shape[0] is not divisable by 2 then we need to extend X by adding a row at the bottom\n", - " bottom_row = tf.slice(X, [0, original_size[0]-1,0], [-1, 1, -1])\n", + " # if X.shape[0] is not divisable by 2 then we need to extend X by \n", + " # adding a row at the bottom\n", + " bottom_row = tf.slice(X, [0, original_size[0] - 1, 0], [-1, 1, -1])\n", " X = tf.concat([X, bottom_row], axis=1)\n", - " #X = np.vstack((X, X[[-1],:])) # Any further extension will be done in due course.\n", " initial_row_extend = 1\n", "\n", " if original_size[1] % 2 != 0:\n", - " # if X.shape[1] is not divisable by 2 then we need to extend X by adding a col to the left\n", - " right_column = tf.slice(X, [0, 0, original_size[1]-1], [-1, -1, 1])\n", - " X = tf.concat([X, right_column], axis=2)\n", - " #X = np.hstack((X, X[:,[-1]]))\n", + " # if X.shape[1] is not divisable by 2 then we need to extend X by \n", + " # adding a col to the right\n", + " right_col = tf.slice(X, [0, 0, original_size[1] - 1], [-1, -1, 1])\n", + " X = tf.concat([X, right_col], axis=2)\n", " initial_col_extend = 1\n", "\n", " extended_size = X.get_shape().as_list()[1:3]\n", - " \n", + "\n", " if nlevels == 0:\n", " if include_scale:\n", - " return Pyramid(X, (), ())\n", + " return Pyramid_ops(X, (), ())\n", " else:\n", - " return Pyramid(X, ())\n", + " return Pyramid_ops(X, ())\n", "\n", - " # initialise\n", + " \n", + " ############################ Initialise ###############################\n", " Yh = [None,] * nlevels\n", " if include_scale:\n", " # this is only required if the user specifies a third output component.\n", " Yscale = [None,] * nlevels\n", "\n", - " #complex_dtype = appropriate_complex_type_for(X)\n", - " \n", + " ############################# Level 1 #################################\n", + " # Uses the biorthogonal filters\n", " if nlevels >= 1:\n", " # Do odd top-level filters on cols.\n", - " Lo = tf.transpose(colfilter(X,h0o), perm=[0,2,1])\n", - " Hi = tf.transpose(colfilter(X,h1o), perm=[0,2,1])\n", + " Lo = colfilter(X, self.h0o)\n", + " Hi = colfilter(X, self.h1o)\n", " if len(self.biort) >= 6:\n", - " Ba = tf.tranpsoe(colfilter(X,h2o), perm=[0,2,1])\n", + " Ba = colfilter(X, self.h2o)\n", "\n", " # Do odd top-level filters on rows.\n", - " LoLo = tf.transpose(colfilter(Lo,h0o), perm=[0,2,1])\n", - " LoLo_shape = LoLo.get_shape().as_list()[1:3]\n", - " Yh[0] = tf.Variable(np.zeros((LoLo_shape[0]>>1, LoLo_shape[1] >>1, 6), dtype=tf.complex64))\n", - " Yh[0][:,:,0], Yh[0][:,:,5] = q2c(tf.transpose(colfilter(Hi,h0o), perm=[0,2,1])) # Horizontal pair\n", - " Yh[0][:,:,2], Yh[0][:,:,3] = q2c(tf.transpose(colfilter(Lo,h1o), perm=[0,2,1])) # Vertical pair\n", + " LoLo = rowfilter(Lo, self.h0o)\n", + " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", + " \n", + " # Horizontal wavelet pair (15 & 165 degrees)\n", + " horiz = q2c(rowfilter(Hi, self.h0o)) \n", + " \n", + " # Vertical wavelet pair (75 & 105 degrees)\n", + " vertic = q2c(rowfilter(Lo, self.h1o)) \n", + " \n", + " # Diagonal wavelet pair (45 & 135 degrees)\n", " if len(self.biort) >= 6:\n", - " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.transpose(colfilter(Ba,h2o), perm=[0,2,1])) # Diagonal pair\n", + " diag = q2c(rowfilter(Ba, self.h2o)) \n", " else:\n", - " Yh[0][:,:,1], Yh[0][:,:,4] = q2c(tf.tranpose(colfilter(Hi,h1o), perm=[0,2,1])) # Diagonal pair\n", - "\n", + " diag = q2c(rowfilter(Hi, self.h1o)) \n", + " \n", + " # Pack all 6 tensors into one \n", + " Yh[0] = tf.stack(\n", + " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", + " axis=3)\n", + " \n", " if include_scale:\n", " Yscale[0] = LoLo\n", - " \n", - " if include_scale:\n", - " return Pyramid(LoLo, Yh, Yscale)\n", - " else:\n", - " return Pyramid(LoLo, Yh, ())\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "from __future__ import absolute_import, division\n", - "\n", - "__all__ = [ 'colfilter', 'colifilt', 'coldfilt', ]\n", - "\n", - "import numpy as np\n", - "from six.moves import xrange\n", - "from dtcwt.utils import as_column_vector, asfarray, appropriate_complex_type_for, reflect\n", - "\n", - "def _centered(arr, newsize):\n", - " # Return the center newsize portion of the array.\n", - " # (Shamelessly cribbed from scipy.)\n", - " newsize = np.asanyarray(newsize)\n", - " currsize = np.array(arr.shape)\n", - " startind = (currsize - newsize) // 2\n", - " endind = startind + newsize\n", - " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", - " return arr[tuple(myslice)]\n", - "\n", - "# This is to allow easy replacement of these later with, possibly, GPU versions\n", - "_rfft = np.fft.rfft\n", - "_irfft = np.fft.irfft\n", - "\n", - "def _column_convolve(X, h):\n", - " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", - " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", - " *h*, the output will have the dtype of *X* appropriately expanded to a\n", - " floating point type if necessary.\n", - " We assume that h is small and so direct convolution is the most efficient.\n", - " \"\"\"\n", - " Xshape = np.asanyarray(X.shape)\n", - " h = h.flatten().astype(X.dtype)\n", - " h_size = h.shape[0]\n", - "\n", - " full_size = X.shape[0] + h_size - 1\n", - " Xshape[0] = full_size\n", - "\n", - " out = np.zeros(Xshape, dtype=X.dtype)\n", - " for idx in xrange(h_size):\n", - " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", - "\n", - " outShape = Xshape.copy()\n", - " outShape[0] = abs(X.shape[0] - h_size) + 1\n", - " return _centered(out, outShape)\n", - "\n", - "def colfilter2(X, h):\n", - " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [1 0].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " # Interpret all inputs as arrays\n", - " X = asfarray(X)\n", - " h = as_column_vector(h)\n", - "\n", - " r, c = X.shape\n", - " m = h.shape[0]\n", - " m2 = np.fix(m*0.5)\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Use 'reflect' so r < m2 works OK.\n", - " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", - "\n", - " # Perform filtering on the columns of the extended matrix X(xe,:), keeping\n", - " # only the 'valid' output samples, so Y is the same size as X if m is odd.\n", - " Y = _column_convolve(X[xe,:], h)\n", - "\n", - " return X[xe,:]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "def colfilter(X, h):\n", - " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [1 0].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m//2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0],[m2, m2], [0, 0]], 'SYMMETRIC')\n", - " r, c = X.get_shape().as_list()[1:3]\n", - "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [-1, 1, 1, 1])\n", - " X = tf.reshape(X, [-1, r, c, 1])\n", - " \n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r,c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1,r,c])\n", - "\n", - "def colfilter2(X, h):\n", - " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [1 0].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m//2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0],[m2, m2], [0, 0]], 'SYMMETRIC')\n", - " N, r, c = X.get_shape().as_list()\n", - "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [m, c, 1, 1])\n", - " X = tf.reshape(X, [-1, r, c, 1])\n", - " \n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r,c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1,r,c])\n", - "\n", - "def rowfilter(X, h):\n", - " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [0 1].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m//2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", - " r, c = X.get_shape().as_list()[1:3]\n", + " \n", + " \n", + " ############################# Level 2+ ################################\n", + " # Uses the qshift filters \n", + " for level in xrange(1, nlevels):\n", + " row_size, col_size = LoLo_shape[0], LoLo_shape[1]\n", + " if row_size % 4 != 0:\n", + " bottom_row = tf.slice(LoLo, [0, row_size - 2, 0], [-1, 2, -1])\n", + " LoLo = tf.concat([LoLo, bottom_row], axis=1)\n", "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [1, -1, 1, 1])\n", - " X = tf.reshape(X, [-1, r, c, 1])\n", - " \n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r,c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1,r,c])\n", + " if col_size % 4 != 0:\n", + " right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2])\n", + " LoLo = tf.concat([LoLo, right_col], axis=2)\n", "\n", - "def rowfilter2(X, h):\n", - " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [0 1].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", + " # Do even Qshift filters on cols.\n", + " Lo = coldfilt(LoLo, self.h0b, self.h0a)\n", + " Hi = coldfilt(LoLo, self.h1b, self.h1a)\n", + " if len(self.qshift) >= 12:\n", + " Ba = coldfilt(LoLo, self.h2b, self.h2a)\n", "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m//2\n", + " # Do even Qshift filters on rows.\n", + " LoLo = rowdfilt(Lo, self.h0b, self.h0a)\n", + " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", + " \n", + " # Horizontal wavelet pair (15 & 165 degrees)\n", + " horiz = q2c(rowdfilt(Hi, self.h0b, self.h0a)) \n", + " \n", + " # Vertical wavelet pair (75 & 105 degrees)\n", + " vertic = q2c(rowdfilt(Lo, self.h1b, self.h1a)) \n", + " \n", + " # Diagonal wavelet pair (45 & 135 degrees)\n", + " if len(self.qshift) >= 12:\n", + " diag = q2c(rowdfilt(Ba, self.h2b, self.h2a)) \n", + " else:\n", + " diag = q2c(rowdfilt(Hi, self.h1b, self.h1a)) \n", + " \n", + " # Pack all 6 tensors into one \n", + " Yh[level] = tf.stack(\n", + " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", + " axis=3)\n", + " \n", + " if include_scale:\n", + " Yscale[level] = LoLo\n", + " \n", + " Yl = LoLo\n", + " \n", + " if initial_row_extend == 1 and initial_col_extend == 1:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The bottom row and rightmost column have been duplicated, prior to decomposition.')\n", "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", - " r, c = X.get_shape().as_list()[1:3]\n", + " if initial_row_extend == 1 and initial_col_extend == 0:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The bottom row has been duplicated, prior to decomposition.')\n", "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [1, -1, 1, 1])\n", - " X = tf.reshape(X, [-1, r, c, 1])\n", - " \n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r,c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1,r,c])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "tf.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "tf.reset_default_graph()\n", - "g = tf.get_default_graph()\n", - "dir(g)\n", - "g.get_collection('variables')\n", - "sess = tf.InteractiveSession()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "f = Transform2d()\n", - "h1o = tf.constant(f.qshift[0][::-1], dtype=tf.float32)\n", - "h1o2 = tf.tile(tf.constant(f.qshift[0][::-1], dtype=tf.float32), [1, 512])\n", - "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", - "im_hat = colf(im, f.qshift[0].astype('float32'))\n", - "y1 = colfilter(colfilter(colfilter(in_, h1o),h1o),h1o)\n", - "y2 = rowfilter(in_,h1o)\n", - "y3 = colfilter2(in_, h1o2)" + " if initial_row_extend == 0 and initial_col_extend == 1:\n", + " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", + " 'x'.join(list(str(s) for s in extended_size)),\n", + " 'x'.join(list(str(s) for s in original_size))))\n", + " logging.warn(\n", + " 'The rightmost column has been duplicated, prior to decomposition.')\n", + "\n", + " if include_scale:\n", + " return Pyramid_ops(Yl, tuple(Yh), tuple(Yscale))\n", + " else:\n", + " return Pyramid_ops(Yl, tuple(Yh))\n", + " \n", + "\n", + "def q2c(y):\n", + " \"\"\"\n", + " Convert from quads in y to complex numbers in z.\n", + " \"\"\"\n", + "\n", + " # Arrange pixels from the corners of the quads into\n", + " # 2 subimages of alternate real and imag pixels.\n", + " # a----b\n", + " # | |\n", + " # | |\n", + " # c----d\n", + " # Combine (a,b) and (d,c) to form two complex subimages.\n", + " a,b,c,d = y[:, 0::2, 0::2], y[:, 0::2,1::2], y[:, 1::2,0::2], y[:, 1::2,1::2]\n", + " \n", + " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", + " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", + "\n", + " # Form the 2 highpasses in z.\n", + " return (p-q, p+q) " ] }, { @@ -546,22 +542,45 @@ }, "outputs": [], "source": [ - "h1o2.shape" + "def med_level(X, h0b, h0a, h1b, h1a):\n", + " # Do even Qshift filters on cols.\n", + " Lo = coldfilt(X, h0b, h0a)\n", + " Hi = coldfilt(X, h1b, h1a)\n", + " if False >= 12:\n", + " Ba = coldfilt(X, h2b, h2a)\n", + "\n", + " # Do even Qshift filters on rows.\n", + " LoLo = rowdfilt(Lo, h0b, h0a)\n", + " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", + "\n", + " # Horizontal wavelet pair (15 & 165 degrees)\n", + " horiz = q2c(rowdfilt(Hi, h0b, h0a)) \n", + "\n", + " # Vertical wavelet pair (75 & 105 degrees)\n", + " vertic = q2c(rowdfilt(Lo, h1b, h1a)) \n", + "\n", + " # Diagonal wavelet pair (45 & 135 degrees)\n", + " if False >= 12:\n", + " diag = q2c(rowdfilt(Ba, h2b, h2a)) \n", + " else:\n", + " diag = q2c(rowdfilt(Hi, h1b, h1a)) \n", + "\n", + " # Pack all 6 tensors into one \n", + " Yh = tf.stack(\n", + " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", + " axis=3)\n", + " \n", + " return LoLo, Yh" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, + "metadata": {}, "outputs": [], "source": [ - "# Compare the 2\n", - "im_hat1,im_hat2 = (k[0] for k in sess.run([y1,y2], feed_dict={in_:[im]}))\n", - " \n", - "np.testing.assert_array_almost_equal(im_hat, im_hat2.T, decimal=4)" + "a = tf.reduce_sum(f.h1b*f.h1a)\n", + "print(sess.run(a))" ] }, { @@ -573,13 +592,11 @@ }, "outputs": [], "source": [ - "import time\n", - "h = f.qshift[0].astype('float32')\n", - "time1 = time.time()\n", - "for i in range(1000):\n", - " colf(colf(colf(im, h),h),h)\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + "in_t = tf.expand_dims(tf.constant(p2.scales[0]),axis=0)\n", + "a_op, b_op = med_level(in_t, f.h0b, f.h0a, f.h1b, f.h1a)\n", + "a, b = sess.run([a_op,b_op])\n", + "np.testing.assert_array_almost_equal(a[0], p2.scales[1], decimal=4)\n", + "np.testing.assert_array_almost_equal(b[0], p2.highpasses[1], decimal=4)" ] }, { @@ -591,13 +608,23 @@ }, "outputs": [], "source": [ - "batch = np.stack([im]*100,axis=0)\n", + "tf.reset_default_graph()\n", + "sess = tf.InteractiveSession(config=tf.ConfigProto(log_device_placement=True))\n", "\n", - "time1 = time.time()\n", - "for i in range(10):\n", - " b = sess.run(y1, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + "f = Transform2d(biort='near_sym_a',qshift='qshift_b') \n", + "f2 = dtcwt.Transform2d(biort='near_sym_a',qshift='qshift_b')\n", + "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", + "\n", + "\n", + "h0b, h0a = sess.run([f.h0b, f.h0a])\n", + "out_1 = cold(im.T,h0b[::-1],h0a[::-1]).T\n", + "out_2 = cold(im, h0b[::-1],h0a[::-1])\n", + "in_t = tf.expand_dims(tf.constant(im),axis=0)\n", + "a_r = rowdfilt(in_t,f.h0b,f.h0a)\n", + "a_c = coldfilt(in_t,f.h0b,f.h0a)\n", + "a,a2 = sess.run([a_r,a_c])\n", + "np.testing.assert_array_almost_equal(a[0], out_1, decimal=4)\n", + "np.testing.assert_array_almost_equal(a2[0], out_2, decimal=4)" ] }, { @@ -609,11 +636,7 @@ }, "outputs": [], "source": [ - "time1 = time.time()\n", - "for i in range(100):\n", - " b = sess.run(y2, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + "a.shape" ] }, { @@ -625,11 +648,13 @@ }, "outputs": [], "source": [ - "time1 = time.time()\n", - "for i in range(100):\n", - " b = sess.run(y3, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" + "p_op = f.forward(in_, nlevels=3)\n", + "p2 = f2.forward(im, nlevels=3,include_scale=True)\n", + "p = p_op.eval(sess, in_, [im])\n", + "\n", + "lo = p.lowpass[0]\n", + "hi1 = p.highpasses[1][0]\n", + "hi2 = p2.highpasses[1]" ] }, { @@ -641,10 +666,14 @@ }, "outputs": [], "source": [ - "fig, axes = plt.subplots(nrows=1,ncols=2,figsize=(10,5))\n", - "fig.tight_layout()\n", - "axes[0].imshow(im_hat1, cmap='gray', interpolation='none')\n", - "axes[1].imshow(im_hat2, cmap='gray', interpolation='none')" + "# Check that the results are the same\n", + "print(p.lowpass.shape)\n", + "print(p2.lowpass.shape)\n", + "print(p.highpasses[0].shape)\n", + "print(p2.highpasses[0].shape)\n", + "np.testing.assert_array_almost_equal(p.lowpass[0], p2.lowpass, decimal=4)\n", + "for i in range(3):\n", + " np.testing.assert_array_almost_equal(p.highpasses[i][0], p2.highpasses[i], decimal=4)" ] }, { @@ -656,7 +685,10 @@ }, "outputs": [], "source": [ - "P = f.forward(i)" + "b = tf.reshape(f.h0a, [1,5,2,1])\n", + "c = tf.stack([f.h0a[0::2], f.h0a[1::2]], axis=-1)\n", + "c = tf.reshape(c, [5,2])\n", + "a_e, b_e, c_e = sess.run([f.h0a, b,c])" ] }, { @@ -668,8 +700,17 @@ }, "outputs": [], "source": [ - "im = sess.run(x, feed_dict={i: [im]})\n", - "im2.shape" + "# Interleaving Columns\n", + "a = np.random.randn(10,2)\n", + "b = np.random.randn(10,2)\n", + "Y = np.zeros((20,2))\n", + "Y[0::2,:], Y[1::2,:] = a,b\n", + "a_t = tf.constant(a,dtype=tf.float32)\n", + "b_t = tf.constant(b,dtype=tf.float32)\n", + "Y_t1 = tf.stack([a_t,b_t], axis=1)\n", + "Y_t = tf.reshape(Y_t1, [20,2])\n", + "Y2 = sess.run(Y_t)\n", + "np.testing.assert_array_almost_equal(Y, Y2, decimal=4)" ] }, { @@ -681,37 +722,14 @@ }, "outputs": [], "source": [ - " for level in xrange(1, nlevels):\n", - " row_size, col_size = LoLo.shape\n", - " if row_size % 4 != 0:\n", - " # Extend by 2 rows if no. of rows of LoLo are not divisable by 4\n", - " LoLo = np.vstack((LoLo[:1,:], LoLo, LoLo[-1:,:]))\n", - "\n", - " if col_size % 4 != 0:\n", - " # Extend by 2 cols if no. of cols of LoLo are not divisable by 4\n", - " LoLo = np.hstack((LoLo[:,:1], LoLo, LoLo[:,-1:]))\n", - "\n", - " # Do even Qshift filters on rows.\n", - " Lo = coldfilt(LoLo,h0b,h0a).T\n", - " Hi = coldfilt(LoLo,h1b,h1a).T\n", - " if len(self.qshift) >= 12:\n", - " Ba = coldfilt(LoLo,h2b,h2a).T\n", - "\n", - " # Do even Qshift filters on columns.\n", - " LoLo = coldfilt(Lo,h0b,h0a).T\n", - "\n", - " Yh[level] = np.zeros((LoLo.shape[0]>>1, LoLo.shape[1]>>1, 6), dtype=complex_dtype)\n", - " Yh[level][:,:,0:6:5] = q2c(coldfilt(Hi,h0b,h0a).T) # Horizontal\n", - " Yh[level][:,:,2:4:1] = q2c(coldfilt(Lo,h1b,h1a).T) # Vertical\n", - " if len(self.qshift) >= 12:\n", - " Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal \n", - " else:\n", - " Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal \n", - "\n", - " if include_scale:\n", - " Yscale[level] = LoLo\n", - " \n", - " return X" + "Y_t2 = tf.reshape(Y_t, [10,2,2])\n", + "a2_t, b2_t = tf.unstack(Y_t2,axis=1)\n", + "a3_t, b3_t = Y_t[0::2,:], Y_t[1::2,:]\n", + "a2,b2 = sess.run([a2_t,b2_t])\n", + "a3,b3 = sess.run([a3_t,b3_t])\n", + "t = np.arange(0,10,2,dtype=np.int32)\n", + "a4_t = Y_t[t,:]\n", + "a4 = sess.run(a4_t)" ] }, { @@ -1141,13 +1159,318 @@ "\n", " return Y" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def colfilter(X, h):\n", + " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [1 0].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m // 2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC')\n", + " r, c = X.get_shape().as_list()[1:3]\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [-1, 1, 1, 1])\n", + " X = tf.expand_dims(X, axis=-1)\n", + "\n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r, c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", + " return tf.reshape(y, [-1, r, c])\n", + "\n", + "\n", + "def rowfilter(X, h):\n", + " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", + " If len(h) is odd, each output sample is aligned with each input sample\n", + " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", + " aligned with the mid point of each pair of input samples, and Y.shape =\n", + " X.shape + [0 1].\n", + " :param X: an image whose columns are to be filtered\n", + " :param h: the filter coefficients.\n", + " :returns Y: the filtered image.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " m = h.get_shape().as_list()[0]\n", + " m2 = m // 2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns)\n", + " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", + " r, c = X.get_shape().as_list()[1:3]\n", + "\n", + " # X currently has shape [batch, rows, cols]\n", + " # h currently has shape [f_rows]\n", + " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", + " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", + " h = tf.reshape(h, [1, -1, 1, 1])\n", + " X = tf.expand_dims(X, axis=-1)\n", + "\n", + " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", + " r, c = y.get_shape().as_list()[1:3]\n", + " # Drop the last dimension\n", + " return tf.reshape(y, [-1, r, c])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def coldfilt(X, ha, hb, a_first=True):\n", + " \"\"\"Filter the columns of image X using the two filters ha and hb =\n", + " reverse(ha). \n", + " ha operates on the odd samples of X and hb on the even samples. \n", + " Both filters should be even length, and h should be approx linear\n", + " phase with a quarter sample (i.e. an :math:`e^{j \\pi/4}`) advance from \n", + " its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`).\n", + " .. code-block:: text\n", + " ext top edge bottom edge ext\n", + " Level 1: ! | ! | !\n", + " odd filt on . b b b b a a a a a a a a b b b b\n", + " odd filt on . a a a a b b b b b b b b a a a a\n", + " Level 2: ! | ! | !\n", + " +q filt on x b b a a a a b b\n", + " -q filt on o a a b b b b a a\n", + " The output is decimated by two from the input sample rate and the results\n", + " from the two filters, Ya and Yb, are interleaved to give Y. \n", + " Symmetric extension with repeated end samples is used on the composite X columns\n", + " before each filter is applied.\n", + " Raises ValueError if the number of rows in X is not a multiple of 4, the\n", + " length of ha does not match hb or the lengths of ha or hb are non-even.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " r, c = X.get_shape().as_list()[1:]\n", + " r2 = r // 2\n", + " if r % 4 != 0:\n", + " raise ValueError('No. of rows in X must be a multiple of 4')\n", + "\n", + " if ha.shape != hb.shape:\n", + " raise ValueError('Shapes of ha and hb must be the same')\n", + "\n", + " if ha.get_shape().as_list()[0] % 2 != 0:\n", + " raise ValueError('Lengths of ha and hb must be even')\n", + "\n", + " m = ha.get_shape().as_list()[0]\n", + " m2 = m // 2\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the columns).\n", + " # SYMMETRIC extension means the edge sample is repeated twice, whereas\n", + " # REFLECT only has the edge sample once \n", + " X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') \n", + " '''\n", + " # Perform filtering on columns of extended matrix X current shape: [Batch, r+2*m, c]\n", + " # We split X into 4 polyphase representations, and apply ha to the odd phases and apply hb to the even phases.\n", + " # These will each be of size [Batch, r/4 + m/2 - 1, c, 1] \n", + " phase1 = tf.expand_dims(X[:,2:r+2*m-2:4,:], axis=-1)\n", + " phase2 = tf.expand_dims(X[:,3:r+2*m-2:4,:], axis=-1)\n", + " phase3 = tf.expand_dims(X[:,4:r+2*m-2:4,:], axis=-1)\n", + " phase4 = tf.expand_dims(X[:,5:r+2*m-2:4,:], axis=-1)\n", + " \n", + " # To massage them into the shape needed for conv2d, we pack:\n", + " # the odd phases into X_odd of size [Batch, r/4+m/2-1, c, 2] and \n", + " # the even phases into X_even of size [Batch, r/4+m/2-1, c, 2] and\n", + " # then apply convolution, using 'valid' padding\n", + " \n", + " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", + " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", + " hao = tf.reshape(ha[0:m:2], [-1, 1, 1, 1])\n", + " hae = tf.reshape(ha[1:m:2], [-1, 1, 1, 1])\n", + " hbo = tf.reshape(hb[0:m:2], [-1, 1, 1, 1])\n", + " hbe = tf.reshape(hb[1:m:2], [-1, 1, 1, 1]) \n", + " print(hao.shape)\n", + " print(X.shape)\n", + " a_rows = tf.nn.conv2d(phase3, hae, strides=[1,1,1,1], padding='VALID') + \\\n", + " tf.nn.conv2d(phase1, hao, strides=[1,1,1,1], padding='VALID')\n", + " b_rows = tf.nn.conv2d(phase2, hae, strides=[1,1,1,1], padding='VALID') + \\\n", + " tf.nn.conv2d(phase4, hao, strides=[1,1,1,1], padding='VALID')\n", + " return a_rows, b_rows\n", + " \"\"\"\n", + " '''\n", + " X_odd = tf.expand_dims(X[:,2:r+2*m-2:2,:], axis=-1)\n", + " X_even =tf.expand_dims(X[:,3:r+2*m-2:2,:], axis=-1)\n", + " ha = tf.reshape(ha, [m,1,1,1])\n", + " hb = tf.reshape(hb, [m,1,1,1])\n", + " a_rows = tf.nn.conv2d(X_odd, ha, strides=[1,2,1,1], padding='VALID')\n", + " b_rows = tf.nn.conv2d(X_even, hb, strides=[1,2,1,1], padding='VALID')\n", + " \n", + " # We interleave the two results into a tensor of size [Batch, r/2, c]\n", + " # Concat a_rows and b_rows (both of shape [Batch, r/4, c, 1]) \n", + " Y = tf.cond(tf.reduce_sum(ha*hb) > 0,\n", + " lambda: tf.concat([a_rows,b_rows],axis=-1),\n", + " lambda: tf.concat([b_rows,a_rows],axis=-1))\n", + " '''\n", + " if a_first:\n", + " Y = tf.concat([a_rows,b_rows],axis=-1)\n", + " else:\n", + " Y = tf.concat([b_rows,a_rows],axis=-1)\n", + " '''\n", + " \n", + " # Permute result to be shape [Batch, r/4, 2, c]\n", + " Y = tf.transpose(Y, perm=[0,1,3,2])\n", + " \n", + " # Reshape result to be shape [Batch, r/2, c]. This reshaping interleaves\n", + " # the columns\n", + " Y = tf.reshape(Y, [-1, r2, c]) \n", + " \n", + " return Y\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def rowdfilt(X, ha, hb, a_first=True):\n", + " \"\"\"Filter the rows of image X using the two filters ha and hb =\n", + " reverse(ha). ha operates on the odd samples of X and hb on the even\n", + " samples. Both filters should be even length, and h should be approx linear\n", + " phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| >\n", + " |h(m/2 + 1)|`).\n", + " .. code-block:: text\n", + " ext top edge bottom edge ext\n", + " Level 1: ! | ! | !\n", + " odd filt on . b b b b a a a a a a a a b b b b\n", + " odd filt on . a a a a b b b b b b b b a a a a\n", + " Level 2: ! | ! | !\n", + " +q filt on x b b a a a a b b\n", + " -q filt on o a a b b b b a a\n", + " The output is decimated by two from the input sample rate and the results\n", + " from the two filters, Ya and Yb, are interleaved to give Y. Symmetric\n", + " extension with repeated end samples is used on the composite X rows\n", + " before each filter is applied.\n", + " Raises ValueError if the number of columns in X is not a multiple of 4, the\n", + " length of ha does not match hb or the lengths of ha or hb are non-even.\n", + " .. codeauthor:: Rich Wareham , August 2013\n", + " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", + " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", + " \"\"\"\n", + "\n", + " r, c = X.get_shape().as_list()[1:]\n", + " c2 = c // 2\n", + " if c % 4 != 0:\n", + " raise ValueError('No. of rows in X must be a multiple of 4')\n", + "\n", + " if ha.shape != hb.shape:\n", + " raise ValueError('Shapes of ha and hb must be the same')\n", + "\n", + " if ha.get_shape().as_list()[0] % 2 != 0:\n", + " raise ValueError('Lengths of ha and hb must be even')\n", + "\n", + " m = ha.get_shape().as_list()[0]\n", + "\n", + " # Symmetrically extend with repeat of end samples.\n", + " # Pad only the second dimension of the tensor X (the rows).\n", + " # SYMMETRIC extension means the edge sample is repeated twice, whereas\n", + " # REFLECT only has the edge sample once\n", + " \n", + " X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC')\n", + " '''\n", + " # Perform filtering on columns of extended matrix X current shape: [Batch, r, c+2*m]\n", + " # We split X into 4 polyphase representations, and apply ha to the odd phases and apply hb to the even phases.\n", + " # These will each be of size [Batch, r, c/4 + m/2 - 1, 1]\n", + " t = np.arange(5, c+2*m-2, 4, dtype=np.int32)\n", + " phase1 = tf.expand_dims(X[:,:,t-3], axis=-1)\n", + " phase2 = tf.expand_dims(X[:,:,t-2], axis=-1)\n", + " phase3 = tf.expand_dims(X[:,:,t-1], axis=-1)\n", + " phase4 = tf.expand_dims(X[:,:,t], axis=-1)\n", + " \n", + " # To massage them into the shape needed for conv2d, we pack:\n", + " # the odd phases into X_odd of size [Batch, r, c/4 + m/2 -1, 2] and \n", + " # the even phases into X_even of size [Batch, r, c/4 + m/2 -1, 2] and\n", + " # then apply convolution, using 'valid' padding\n", + " \n", + " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", + " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", + " hao = tf.reshape(ha[0:m:2], [1, -1, 1, 1])\n", + " hae = tf.reshape(ha[1:m:2], [1, -1, 1, 1])\n", + " hbo = tf.reshape(hb[0:m:2], [1, -1, 1, 1])\n", + " hbe = tf.reshape(hb[1:m:2], [1, -1, 1, 1]) \n", + " a_cols = tf.nn.conv2d(phase1, hae, strides=[1,1,1,1], padding='VALID') + \\\n", + " tf.nn.conv2d(phase3, hao, strides=[1,1,1,1], padding='VALID')\n", + " b_cols = tf.nn.conv2d(phase2, hae, strides=[1,1,1,1], padding='VALID') + \\\n", + " tf.nn.conv2d(phase4, hao, strides=[1,1,1,1], padding='VALID')\n", + " \n", + " \"\"\"\n", + " # Could also try:\n", + " \n", + " odd_phases = tf.concat([phase3, phase1], axis=-1)\n", + " even_phases = tf.concat([phase4, phase2], axis=-1)\n", + " ha_split = tf.reshape(ha, [1,m2,2,1])\n", + " hb_split = tf.reshape(hb, [1,m2,2,1])\n", + " a_cols = tf.nn.conv2d(odd_phases, ha_split, strides=[1,1,1,1], padding='VALID')\n", + " b_cols = tf.nn.conv2d(even_phases, hb_split, strides=[1,1,1,1], padding='VALID') \n", + " \"\"\"\n", + " '''\n", + " X_odd = tf.expand_dims(X[:,:,2:c+2*m-2:2], axis=-1)\n", + " X_even =tf.expand_dims(X[:,:,3:c+2*m-2:2], axis=-1)\n", + " ha = tf.reshape(ha, [1,m,1,1])\n", + " hb = tf.reshape(hb, [1,m,1,1])\n", + " a_cols = tf.nn.conv2d(X_odd, ha, strides=[1,1,2,1], padding='VALID')\n", + " b_cols = tf.nn.conv2d(X_even, hb, strides=[1,1,2,1], padding='VALID')\n", + " \n", + " # We interleave the two results into a tensor of size [Batch, r/2, c]\n", + " # Concat a_cols and b_cols (both of shape [Batch, r, c/4, 1]) \n", + " Y = tf.cond(tf.reduce_sum(ha*hb) > 0,\n", + " lambda: tf.concat([a_cols,b_cols],axis=-1),\n", + " lambda: tf.concat([b_cols,a_cols],axis=-1))\n", + " '''\n", + " if a_first:\n", + " Y = tf.concat([a_cols,b_cols],axis=-1)\n", + " else:\n", + " Y = tf.concat([b_cols,a_cols],axis=-1)\n", + " ''' \n", + " # Reshape result to be shape [Batch, r, c/2]. This reshaping interleaves\n", + " # the columns\n", + " Y = tf.reshape(Y, [-1, r, c2]) \n", + " \n", + " return Y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "deconv_tf_vis", + "display_name": "tf", "language": "python", - "name": "deconv_tf_vis" + "name": "tf" }, "language_info": { "codemirror_mode": { @@ -1161,6 +1484,12 @@ "pygments_lexer": "ipython3" }, "toc": { + "colors": { + "hover_highlight": "#DAA520", + "running_highlight": "#FF0000", + "selected_highlight": "#FFD700" + }, + "moveMenuLeft": true, "nav_menu": { "height": "12px", "width": "252px" From a47b1326a0b9383c4995fd16c7a948139425d2fc Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 2 Mar 2017 12:31:14 +0000 Subject: [PATCH 05/52] Added tests for tf column and row conv funcs Modified the lowlevel colfilter, coldfilt, rowfilter, rowdfilt functions to allow filters of any shape (tensor, np array or list). This makes it more like the numpy implementation. Wrote tests and confirmed they pass for current implementation. --- dtcwt/tf/lowlevel.py | 151 ++++++++++++++++++++++---------------- tests/test_tfcoldfilt.py | 32 +++++--- tests/test_tfcolfilter.py | 34 ++++++--- tests/test_tfrowdfilt.py | 56 ++++++++++++++ tests/test_tfrowfilter.py | 68 +++++++++++++++++ 5 files changed, 260 insertions(+), 81 deletions(-) create mode 100644 tests/test_tfrowdfilt.py create mode 100644 tests/test_tfrowfilter.py diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index 9c9d480..4eec68d 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -2,6 +2,51 @@ import tensorflow as tf import numpy as np +from dtcwt.utils import asfarray, as_column_vector + +def _as_row_tensor(h): + if isinstance(h, tf.Tensor): + h = tf.reshape(h, [1, -1]) + else: + h = as_column_vector(h).T + h = tf.constant(h, tf.float32) + return h + +def _as_col_tensor(h): + if isinstance(h, tf.Tensor): + h = tf.reshape(h, [-1, 1]) + else: + h = as_column_vector(h) + h = tf.constant(h, tf.float32) + return h + +def _conv_2d(X, h, strides=[1,1,1,1]): + """Perform 2d convolution in tensorflow. X will to be manipulated to be of + shape [batch, height, width, ch], and h to be of shape + [height, width, ch, num]. This function does the necessary reshaping before + calling the conv2d function, and does the reshaping on the output, returning + Y of shape [batch, height, width]""" + + # Check the shape of X is what we expect + if len(X.shape) != 3: + raise ValueError('X needs to be of shape [batch, height, width] for conv_2d') + + # Check the shape of h is what we expect + if len(h.shape) != 2: + raise ValueError('Filter inputs must only have height and width for conv_2d') + + # Add in the unit dimensions for conv + X = tf.expand_dims(X, axis=-1) + h = tf.expand_dims(tf.expand_dims(h, axis=-1),axis=-1) + + # Have to reverse h as tensorflow 2d conv is actually cross-correlation + h = tf.reverse(h, axis=[0,1]) + Y = tf.nn.conv2d(X, h, strides=strides, padding='VALID') + + # Remove the final dimension, returning a result of shape [batch, height, width] + Y = tf.squeeze(Y, axis=-1) + + return Y def colfilter(X, h): """Filter the columns of image *X* using filter vector *h*, without decimation. @@ -16,25 +61,21 @@ def colfilter(X, h): .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 """ - - m = h.get_shape().as_list()[0] + # Make the function flexible to accepting h in multiple forms + h_t = _as_col_tensor(h) + m = h_t.get_shape().as_list()[0] m2 = m // 2 + print(m2) + print(h_t.shape) # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') - # Reshape h to be a col filter. We have to flip h too as the tf conv2d - # operation is cross-correlation, not true convolution - h = tf.reshape(h[::-1], [-1, 1, 1, 1]) - - # Reshape X from [batch, rows, cols] to [batch, rows, cols, 1] for conv2d - X = tf.expand_dims(X, axis=-1) + Y = _conv_2d(X, h_t, strides=[1,1,1,1]) - Y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID') + return Y - # Drop the last dimension - return tf.unstack(Y, num=1, axis=-1)[0] def rowfilter(X, h): """Filter the rows of image *X* using filter vector *h*, without decimation. @@ -42,32 +83,26 @@ def rowfilter(X, h): and *Y* is the same size as *X*. If len(h) is even, each output sample is aligned with the mid point of each pair of input samples, and Y.shape = X.shape + [0 1]. - :param X: an image whose columns are to be filtered + :param X: a tensor of images whose rows are to be filtered :param h: the filter coefficients. :returns Y: the filtered image. + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 """ - - m = h.get_shape().as_list()[0] + # Make the function flexible to accepting h in multiple forms + h_t = _as_row_tensor(h) + m = h_t.get_shape().as_list()[1] m2 = m // 2 # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') - # Reshape h to be a row filter. We have to flip h too as the tf conv2d - # operation is cross-correlation, not true convolution - h = tf.reshape(h[::-1], [1, -1, 1, 1]) - - # Reshape X from [batch, rows, cols] to [batch, rows, cols, 1] for conv2d - X = tf.expand_dims(X, axis=-1) - - Y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID') + Y = _conv_2d(X, h_t, strides=[1,1,1,1]) - # Drop the last dimension - return tf.unstack(Y, num=1, axis=-1)[0] + return Y def coldfilt(X, ha, hb, a_first=True): @@ -91,6 +126,7 @@ def coldfilt(X, ha, hb, a_first=True): before each filter is applied. Raises ValueError if the number of rows in X is not a multiple of 4, the length of ha does not match hb or the lengths of ha or hb are non-even. + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 @@ -101,10 +137,12 @@ def coldfilt(X, ha, hb, a_first=True): if r % 4 != 0: raise ValueError('No. of rows in X must be a multiple of 4') - if ha.shape != hb.shape: + ha_t = _as_col_tensor(ha) + hb_t = _as_col_tensor(hb) + if ha_t.shape != hb_t.shape: raise ValueError('Shapes of ha and hb must be the same') - m = ha.get_shape().as_list()[0] + m = ha_t.get_shape().as_list()[0] if m % 2 != 0: raise ValueError('Lengths of ha and hb must be even') @@ -113,28 +151,20 @@ def coldfilt(X, ha, hb, a_first=True): X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') # Take the odd and even columns of X - X_odd = tf.expand_dims(X[:,2:r+2*m-2:2,:], axis=-1) - X_even =tf.expand_dims(X[:,3:r+2*m-2:2,:], axis=-1) - - # Transform ha and hb to be col filters. We must reverse them as tf conv is - # cross correlation, not true convolution - ha = tf.reshape(ha[::-1], [m,1,1,1]) - hb = tf.reshape(hb[::-1], [m,1,1,1]) + X_odd = X[:,2:r+2*m-2:2,:] + X_even =X[:,3:r+2*m-2:2,:] # Do the 2d convolution, but only evaluated at every second sample # for both X_odd and X_even - a_rows = tf.nn.conv2d(X_odd, ha, strides=[1,2,1,1], padding='VALID') - b_rows = tf.nn.conv2d(X_even, hb, strides=[1,2,1,1], padding='VALID') - - # We interleave the two results into a tensor of size [Batch, r/2, c] - # Concat a_rows and b_rows (both of shape [Batch, r/4, c, 1]) - Y = tf.cond(tf.reduce_sum(ha*hb) > 0, - lambda: tf.concat([a_rows,b_rows],axis=-1), - lambda: tf.concat([b_rows,a_rows],axis=-1)) - - # Permute result to be shape [Batch, r/4, 2, c] - Y = tf.transpose(Y, perm=[0,1,3,2]) + a_rows = _conv_2d(X_odd, ha_t, strides=[1,2,1,1]) + b_rows = _conv_2d(X_even, hb_t, strides=[1,2,1,1]) + # Stack a_rows and b_rows (both of shape [Batch, r/4, c]) along the third + # dimension to make a tensor of shape [Batch, r/4, 2, c]. + Y = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, + lambda: tf.stack([a_rows,b_rows],axis=2), + lambda: tf.stack([b_rows,a_rows],axis=2)) + # Reshape result to be shape [Batch, r/2, c]. This reshaping interleaves # the columns Y = tf.reshape(Y, [-1, r2, c]) @@ -172,11 +202,13 @@ def rowdfilt(X, ha, hb): c2 = c // 2 if c % 4 != 0: raise ValueError('No. of rows in X must be a multiple of 4') - - if ha.shape != hb.shape: + + ha_t = _as_row_tensor(ha) + hb_t = _as_row_tensor(hb) + if ha_t.shape != hb_t.shape: raise ValueError('Shapes of ha and hb must be the same') - m = ha.get_shape().as_list()[0] + m = ha_t.get_shape().as_list()[1] if m % 2 != 0: raise ValueError('Lengths of ha and hb must be even') @@ -187,25 +219,20 @@ def rowdfilt(X, ha, hb): X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC') # Take the odd and even columns of X - X_odd = tf.expand_dims(X[:,:,2:c+2*m-2:2], axis=-1) - X_even =tf.expand_dims(X[:,:,3:c+2*m-2:2], axis=-1) - - # Transform ha and hb to be col filters. We must reverse them as tf conv is - # cross correlation, not true convolution - ha = tf.reshape(ha[::-1], [m,1,1,1]) - hb = tf.reshape(hb[::-1], [m,1,1,1]) + X_odd = X[:,:,2:c+2*m-2:2] + X_even= X[:,:,3:c+2*m-2:2] # Do the 2d convolution, but only evaluated at every second sample # for both X_odd and X_even - a_cols = tf.nn.conv2d(X_odd, ha, strides=[1,1,2,1], padding='VALID') - b_cols = tf.nn.conv2d(X_even, hb, strides=[1,1,2,1], padding='VALID') + a_cols = _conv_2d(X_odd, ha_t, strides=[1,1,2,1]) + b_cols = _conv_2d(X_even, hb_t, strides=[1,1,2,1]) - # We interleave the two results into a tensor of size [Batch, r/2, c] - # Concat a_cols and b_cols (both of shape [Batch, r, c/4, 1]) - Y = tf.cond(tf.reduce_sum(ha*hb) > 0, - lambda: tf.concat([a_cols,b_cols],axis=-1), - lambda: tf.concat([b_cols,a_cols],axis=-1)) - + # Stack a_cols and b_cols (both of shape [Batch, r, c/4]) along the fourth + # dimension to make a tensor of shape [Batch, r, c/4, 2]. + Y = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, + lambda: tf.stack([a_cols,b_cols],axis=3), + lambda: tf.stack([b_cols,a_cols],axis=3)) + # Reshape result to be shape [Batch, r, c/2]. This reshaping interleaves # the columns Y = tf.reshape(Y, [-1, r, c2]) diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py index a639e9c..3091432 100644 --- a/tests/test_tfcoldfilt.py +++ b/tests/test_tfcoldfilt.py @@ -1,42 +1,56 @@ import os import numpy as np -from dtcwt.numpy.lowlevel import coldfilt +import tensorflow as tf +from dtcwt.coeffs import biort, qshift +from dtcwt.tf.lowlevel import coldfilt +from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt from pytest import raises import tests.datasets as datasets def setup(): - global mandrill + global mandrill, mandrill_t mandrill = datasets.mandrill() + mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 assert mandrill.max() <= 1 assert mandrill.dtype == np.float32 + assert mandrill_t.get_shape() == (1, 512, 512) def test_odd_filter(): with raises(ValueError): - coldfilt(mandrill, (-1,2,-1), (-1,2,1)) + coldfilt(mandrill_t, (-1,2,-1), (-1,2,1)) def test_different_size(): with raises(ValueError): - coldfilt(mandrill, (-0.5,-1,2,1,0.5), (-1,2,-1)) + coldfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) def test_bad_input_size(): with raises(ValueError): - coldfilt(mandrill[:511,:], (-1,1), (1,-1)) + coldfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) def test_good_input_size(): - coldfilt(mandrill[:,:511], (-1,1), (1,-1)) + coldfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) def test_good_input_size_non_orthogonal(): - coldfilt(mandrill[:,:511], (1,1), (1,1)) + coldfilt(mandrill_t[:,:,:511], (1,1), (1,1)) def test_output_size(): - Y = coldfilt(mandrill, (-1,1), (1,-1)) - assert Y.shape == (mandrill.shape[0]/2, mandrill.shape[1]) + y_op = coldfilt(mandrill_t, (-1,1), (1,-1)) + assert y_op.shape[1:] == (mandrill.shape[0]/2, mandrill.shape[1]) + +def test_equal_numpy_qshift(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + ref = np_coldfilt(mandrill, ha, hb) + y_op = coldfilt(mandrill_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) # vim:sw=4:sts=4:et diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index e113fa6..cadab46 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -4,6 +4,7 @@ import tensorflow as tf from dtcwt.coeffs import biort, qshift from dtcwt.tf.lowlevel import colfilter +from dtcwt.numpy.lowlevel import colfilter as np_colfilter import tests.datasets as datasets @@ -20,32 +21,45 @@ def test_mandrill_loaded(): assert mandrill_t.get_shape() == (1, 512, 512) def test_odd_size(): - h = tf.constant([-1,2,-1], dtype=tf.float32) - y_op = colfilter(mandrill_t, h) + y_op = colfilter(mandrill_t, [-1,2,-1]) assert y_op.get_shape()[1:] == mandrill.shape def test_even_size(): - h = tf.constant([-1,-1], dtype=tf.float32) - y_op = colfilter(mandrill_t, h) + y_op = colfilter(mandrill_t, [-1,-1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) def test_qshift(): - h = tf.constant(qshift('qshift_a')[0], dtype=tf.float32) - y_op = colfilter(mandrill, h) + h = qshift('qshift_a')[0] + y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) def test_biort(): - h = tf.constant(biort('antonini')[0], dtype=tf.float32) - y_op = colfilter(mandrill, h) + h = biort('antonini')[0] + y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == mandrill.shape def test_even_size(): - h = tf.constant([-1,-1], dtype=tf.float32) - y_op = colfilter(mandrill_t, h) + zero_t = tf.zeros([1, *mandrill.shape], tf.float32) + y_op = colfilter(zero_t, [-1,1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) with tf.Session() as sess: y = sess.run(y_op) assert not np.any(y[:] != 0.0) +def test_equal_numpy_biort(): + h = biort('near_sym_b')[0] + ref = np_colfilter(mandrill, h) + y_op = colfilter(mandrill_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift(): + h = qshift('qshift_c')[0] + ref = np_colfilter(mandrill, h) + y_op = colfilter(mandrill_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) # vim:sw=4:sts=4:et diff --git a/tests/test_tfrowdfilt.py b/tests/test_tfrowdfilt.py new file mode 100644 index 0000000..8453fc2 --- /dev/null +++ b/tests/test_tfrowdfilt.py @@ -0,0 +1,56 @@ +import os + +import numpy as np +import tensorflow as tf +from dtcwt.coeffs import biort, qshift +from dtcwt.tf.lowlevel import rowdfilt +from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt + +from pytest import raises + +import tests.datasets as datasets + +def setup(): + global mandrill, mandrill_t + mandrill = datasets.mandrill() + mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + assert mandrill_t.get_shape() == (1, 512, 512) + +def test_odd_filter(): + with raises(ValueError): + rowdfilt(mandrill_t, (-1,2,-1), (-1,2,1)) + +def test_different_size(): + with raises(ValueError): + rowdfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) + +def test_bad_input_size(): + with raises(ValueError): + rowdfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) + +def test_good_input_size(): + rowdfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) + +def test_good_input_size_non_orthogonal(): + rowdfilt(mandrill_t[:,:511,:], (1,1), (1,1)) + +def test_output_size(): + y_op = rowdfilt(mandrill_t, (-1,1), (1,-1)) + assert y_op.shape[1:] == (mandrill.shape[0], mandrill.shape[1]/2) + +def test_equal_numpy_qshift(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + ref = np_coldfilt(mandrill.T, ha, hb).T + y_op = rowdfilt(mandrill_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +# vim:sw=4:sts=4:et diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py new file mode 100644 index 0000000..b00db5b --- /dev/null +++ b/tests/test_tfrowfilter.py @@ -0,0 +1,68 @@ +import os + +import numpy as np +import tensorflow as tf +from dtcwt.coeffs import biort, qshift +from dtcwt.tf.lowlevel import rowfilter +from dtcwt.numpy.lowlevel import colfilter as np_colfilter + +import tests.datasets as datasets + +def setup(): + global mandrill, mandrill_t + mandrill = datasets.mandrill() + mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + assert mandrill_t.get_shape() == (1, 512, 512) + +def test_odd_size(): + y_op = rowfilter(mandrill_t, [-1, 2, -1]) + assert y_op.get_shape()[1:] == mandrill.shape + +def test_even_size(): + y_op = rowfilter(mandrill_t, [-1, -1]) + assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) + +def test_qshift(): + h = qshift('qshift_a')[0] + y_op = rowfilter(mandrill_t, h) + assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) + +def test_biort(): + h = biort('antonini')[0] + y_op = rowfilter(mandrill_t, h) + assert y_op.get_shape()[1:] == mandrill.shape + +def test_even_size(): + h = tf.constant([-1,1], dtype=tf.float32) + zero_t = tf.zeros([1, *mandrill.shape], tf.float32) + y_op = rowfilter(zero_t, h) + assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) + with tf.Session() as sess: + y = sess.run(y_op) + assert not np.any(y[:] != 0.0) + +def test_equal_numpy_biort(): + h = biort('near_sym_b')[0] + ref = np_colfilter(mandrill.T, h).T + y_op = rowfilter(mandrill_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift(): + h = qshift('qshift_c')[0] + ref = np_colfilter(mandrill.T, h).T + y_op = rowfilter(mandrill_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + + + +# vim:sw=4:sts=4:et From 27326e3e2b1a85f1c166252d1bee1b2227d6e01b Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 2 Mar 2017 15:01:50 +0000 Subject: [PATCH 06/52] Started testing on the transform2d method in tf --- dtcwt/tf/__init__.py | 13 ++++ dtcwt/tf/common.py | 21 ++++-- dtcwt/tf/transform2d.py | 132 +++++++++++++----------------------- tests/test_tfTransform2d.py | 102 ++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 92 deletions(-) create mode 100644 dtcwt/tf/__init__.py create mode 100644 tests/test_tfTransform2d.py diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py new file mode 100644 index 0000000..e5a987c --- /dev/null +++ b/dtcwt/tf/__init__.py @@ -0,0 +1,13 @@ +""" +A backend which uses NumPy to perform the filtering. This backend should always +be available. + +""" + +from .common import Pyramid_tf +from .transform2d import Transform2d + +__all__ = [ + 'Pyramid', + 'Transform2d', +] diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 1daf8ab..0c30450 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -25,12 +25,21 @@ def __init__(self, lowpass, highpasses, scales=None): self.scales = scales def eval(self, sess, placeholder, data): - lo = sess.run(self.lowpass, {placeholder : data}) - hi = sess.run(self.highpasses, {placeholder : data}) - if self.scales is not None: - scales = sess.run(self.scales, {placeholder : data}) - else: - scales = None + try: + lo = sess.run(self.lowpass, {placeholder : data}) + hi = sess.run(self.highpasses, {placeholder : data}) + if self.scales is not None: + scales = sess.run(self.scales, {placeholder : data}) + else: + scales = None + except ValueError: + lo = sess.run(self.lowpass, {placeholder : [data]}) + hi = sess.run(self.highpasses, {placeholder : [data]}) + if self.scales is not None: + scales = sess.run(self.scales, {placeholder : [data]}) + else: + scales = None + return Pyramid(lo, hi, scales) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 55ffb05..90c8857 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -6,6 +6,7 @@ from dtcwt.coeffs import biort as _biort, qshift as _qshift from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT +from dtcwt.utils import asfarray from dtcwt.tf.common import Pyramid_tf from dtcwt.tf.lowlevel import * @@ -33,82 +34,15 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): # Load bi-orthogonal wavelets try: self.biort = _biort(biort) - b = self.biort except TypeError: self.biort = biort - b = self.biort # Load quarter sample shift wavelets try: self.qshift = _qshift(qshift) - q = self.qshift except TypeError: self.qshift = qshift - q = self.qshift - ### Load the ops onto the graph for the filter banks - - # If biort has 6 elements instead of 4, then it's a modified - # rotationally symmetric wavelet - # h0o - analysis low pass filter - # g0o - synthesis low pass filter - # h1o - analysis high pass filter - # g1o - synthesis high pass filter - # h2o - analysis band pass filter for 45 deg wavelets - # g2o - synthesis band pass filter for 45 deg wavelets - if len(b) == 4: - # h0o, g0o, h1o, g1o = b - self.h0o = tf.constant(b[0], dtype=tf.float32, name='dtcwt/h0o') - self.g0o = tf.constant(b[1], dtype=tf.float32, name='dtcwt/g0o') - self.h1o = tf.constant(b[2], dtype=tf.float32, name='dtcwt/h1o') - self.g1o = tf.constant(b[3], dtype=tf.float32, name='dtcwt/g1o') - elif len(b) == 6: - #h0o, g0o, h1o, g1o, h2o, g2o = b - self.h0o = tf.constant(b[0], dtype=tf.float32, name='dtcwt/h0o') - self.g0o = tf.constant(b[1], dtype=tf.float32, name='dtcwt/g0o') - self.h1o = tf.constant(b[2], dtype=tf.float32, name='dtcwt/h1o') - self.g1o = tf.constant(b[3], dtype=tf.float32, name='dtcwt/g1o') - self.h2o = tf.constant(b[4], dtype=tf.float32, name='dtcwt/h2o') - self.g2o = tf.constant(b[5], dtype=tf.float32, name='dtcwt/g2o') - else: - raise ValueError('Biort wavelet must have 6 or 4 components.') - - - # If qshift has 12 elements instead of 8, then it's a modified - # rotationally symmetric wavelet - # h0a - analysis low pass filter tree a - # h0b - analysis low pass filter tree b - # h1a - analysis high pass filter tree a - # h1b - analysis high pass filter tree b - # h2a - analysis band pass filter tree a (for 45 deg wavelets) - # h2b - analysis band pass filter tree b (for 45 deg wavelets) - # g.. - synthesis equivalents - if len(q) == 8: - #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = q - self.h0a = tf.constant(q[0], dtype=tf.float32, name='dtcwt/h0a') - self.h0b = tf.constant(q[1], dtype=tf.float32, name='dtcwt/h0b') - self.g0a = tf.constant(q[2], dtype=tf.float32, name='dtcwt/g0a') - self.g0b = tf.constant(q[3], dtype=tf.float32, name='dtcwt/g0b') - self.h1a = tf.constant(q[4], dtype=tf.float32, name='dtcwt/h1a') - self.h1b = tf.constant(q[5], dtype=tf.float32, name='dtcwt/h1b') - self.g1a = tf.constant(q[6], dtype=tf.float32, name='dtcwt/g1a') - self.g1b = tf.constant(q[7], dtype=tf.float32, name='dtcwt/g1b') - elif len(q) == 12: - #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = q[:10] - self.h0a = tf.constant(q[0], dtype=tf.float32, name='dtcwt/h0a') - self.h0b = tf.constant(q[1], dtype=tf.float32, name='dtcwt/h0b') - self.g0a = tf.constant(q[2], dtype=tf.float32, name='dtcwt/g0a') - self.g0b = tf.constant(q[3], dtype=tf.float32, name='dtcwt/g0b') - self.h1a = tf.constant(q[4], dtype=tf.float32, name='dtcwt/h1a') - self.h1b = tf.constant(q[5], dtype=tf.float32, name='dtcwt/h1b') - self.g1a = tf.constant(q[6], dtype=tf.float32, name='dtcwt/g1a') - self.g1b = tf.constant(q[7], dtype=tf.float32, name='dtcwt/g1b') - self.h2a = tf.constant(q[8], dtype=tf.float32, name='dtcwt/h2a') - self.h2b = tf.constant(q[9], dtype=tf.float32, name='dtcwt/h2b') - else: - raise ValueError('Qshift wavelet must have 12 or 8 components.') - - def forward(self, X, nlevels=3, include_scale=False): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. @@ -123,12 +57,38 @@ def forward(self, X, nlevels=3, include_scale=False): .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 """ - # Check the shape of the input + # If biort has 6 elements instead of 4, then it's a modified + # rotationally symmetric wavelet + # FIXME: there's probably a nicer way to do this + if len(self.biort) == 4: + h0o, g0o, h1o, g1o = self.biort + elif len(self.biort) == 6: + h0o, g0o, h1o, g1o, h2o, g2o = self.biort + else: + raise ValueError('Biort wavelet must have 6 or 4 components.') + + # If qshift has 12 elements instead of 8, then it's a modified + # rotationally symmetric wavelet + # FIXME: there's probably a nicer way to do this + if len(self.qshift) == 8: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift + elif len(self.qshift) == 12: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10] + else: + raise ValueError('Qshift wavelet must have 12 or 8 components.') + + # Check the shape and form of the input + if not isinstance(X, tf.Tensor): + raise ValueError('Please provide the forward function with ' + + 'a tensorflow placeholder or variable of size [batch, width,' + + 'height] (batch can be None if you do not wish to specify it).') + original_size = X.get_shape().as_list()[1:] if len(original_size) >= 3: - raise ValueError('The entered image is {0}, please enter each image slice separately.'. - format('x'.join(list(str(s) for s in X.get_shape().as_list())))) + raise ValueError('The entered variable has too many dimensions {}. If ' + 'the final dimension are colour channels, please enter each ' + + 'channel separately.'.format(original_size)) ############################## Resize ################################# @@ -170,26 +130,26 @@ def forward(self, X, nlevels=3, include_scale=False): # Uses the biorthogonal filters if nlevels >= 1: # Do odd top-level filters on cols. - Lo = colfilter(X, self.h0o) - Hi = colfilter(X, self.h1o) + Lo = colfilter(X, h0o) + Hi = colfilter(X, h1o) if len(self.biort) >= 6: - Ba = colfilter(X, self.h2o) + Ba = colfilter(X, h2o) # Do odd top-level filters on rows. - LoLo = rowfilter(Lo, self.h0o) + LoLo = rowfilter(Lo, h0o) LoLo_shape = LoLo.get_shape().as_list()[1:3] # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowfilter(Hi, self.h0o)) + horiz = q2c(rowfilter(Hi, h0o)) # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowfilter(Lo, self.h1o)) + vertic = q2c(rowfilter(Lo, h1o)) # Diagonal wavelet pair (45 & 135 degrees) if len(self.biort) >= 6: - diag = q2c(rowfilter(Ba, self.h2o)) + diag = q2c(rowfilter(Ba, h2o)) else: - diag = q2c(rowfilter(Hi, self.h1o)) + diag = q2c(rowfilter(Hi, h1o)) # Pack all 6 tensors into one Yh[0] = tf.stack( @@ -217,26 +177,26 @@ def forward(self, X, nlevels=3, include_scale=False): LoLo = tf.concat([LoLo, right_col], axis=2) # Do even Qshift filters on cols. - Lo = coldfilt(LoLo, self.h0b, self.h0a) - Hi = coldfilt(LoLo, self.h1b, self.h1a) + Lo = coldfilt(LoLo, h0b, h0a) + Hi = coldfilt(LoLo, h1b, h1a) if len(self.qshift) >= 12: - Ba = coldfilt(LoLo, self.h2b, self.h2a) + Ba = coldfilt(LoLo, h2b, h2a) # Do even Qshift filters on rows. - LoLo = rowdfilt(Lo, self.h0b, self.h0a) + LoLo = rowdfilt(Lo, h0b, h0a) LoLo_shape = LoLo.get_shape().as_list()[1:3] # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowdfilt(Hi, self.h0b, self.h0a)) + horiz = q2c(rowdfilt(Hi, h0b, h0a)) # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowdfilt(Lo, self.h1b, self.h1a)) + vertic = q2c(rowdfilt(Lo, h1b, h1a)) # Diagonal wavelet pair (45 & 135 degrees) if len(self.qshift) >= 12: - diag = q2c(rowdfilt(Ba, self.h2b, self.h2a)) + diag = q2c(rowdfilt(Ba, h2b, h2a)) else: - diag = q2c(rowdfilt(Hi, self.h1b, self.h1a)) + diag = q2c(rowdfilt(Hi, h1b, h1a)) # Pack all 6 tensors into one Yh[level] = tf.stack( diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py new file mode 100644 index 0000000..8bcc6dc --- /dev/null +++ b/tests/test_tfTransform2d.py @@ -0,0 +1,102 @@ +import os +from pytest import raises + +import numpy as np +import tensorflow as tf +from dtcwt.tf import Transform2d +from dtcwt.numpy import Pyramid +from dtcwt.coeffs import biort, qshift +import tests.datasets as datasets + +TOLERANCE = 1e-12 + +def setup(): + global mandrill, in_p + mandrill = datasets.mandrill() + in_p = tf.placeholder(tf.float32, [None, 512, 512]) + +def test_mandrill_loaded(): + assert mandrill.shape == (512, 512) + assert mandrill.min() >= 0 + assert mandrill.max() <= 1 + assert mandrill.dtype == np.float32 + assert in_p.get_shape() == (1, 512, 512) + +def test_simple(): + t = Transform2d(in_p) + with tf.Session() as sess: + Yl, Yh = t.eval(sess, in_p, mandrill) +""" +def test_specific_wavelet(): + Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), qshift=qshift('qshift_06')) + +def test_1d(): + Yl, Yh = dtwavexfm2(mandrill[0,:]) + +def test_3d(): + with raises(ValueError): + Yl, Yh = dtwavexfm2(np.dstack((mandrill, mandrill))) + +def test_simple_w_scale(): + Yl, Yh, Yscale = dtwavexfm2(mandrill, include_scale=True) + + assert len(Yscale) > 0 + for x in Yscale: + assert x is not None + +def test_odd_rows(): + Yl, Yh = dtwavexfm2(mandrill[:509,:]) + +def test_odd_rows_w_scale(): + Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:], include_scale=True) + +def test_odd_cols(): + Yl, Yh = dtwavexfm2(mandrill[:,:509]) + +def test_odd_cols_w_scale(): + Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) + + +def test_odd_rows_and_cols(): + Yl, Yh = dtwavexfm2(mandrill[:,:509]) + +def test_odd_rows_and_cols_w_scale(): + Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) + +def test_rot_symm_modified(): + # This test only checks there is no error running these functions, not that they work + Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) + Z = dtwaveifm2(Yl, Yh, biort='near_sym_b_bp', qshift='qshift_b_bp') + +def test_0_levels(): + Yl, Yh = dtwavexfm2(mandrill, nlevels=0) + assert np.all(np.abs(Yl - mandrill) < TOLERANCE) + assert len(Yh) == 0 + +def test_0_levels_w_scale(): + Yl, Yh, Yscale = dtwavexfm2(mandrill, nlevels=0, include_scale=True) + assert np.all(np.abs(Yl - mandrill) < TOLERANCE) + assert len(Yh) == 0 + assert len(Yscale) == 0 + +def test_integer_input(): + # Check that an integer input is correctly coerced into a floating point + # array + Yl, Yh = dtwavexfm2([[1,2,3,4], [1,2,3,4]]) + assert np.any(Yl != 0) + +def test_integer_perfect_recon(): + # Check that an integer input is correctly coerced into a floating point + # array and reconstructed + A = np.array([[1,2,3,4], [5,6,7,8]], dtype=np.int32) + Yl, Yh = dtwavexfm2(A) + B = dtwaveifm2(Yl, Yh) + assert np.max(np.abs(A-B)) < 1e-5 + +def test_float32_input(): + # Check that an float32 input is correctly output as float32 + Yl, Yh = dtwavexfm2(mandrill.astype(np.float32)) + assert np.issubsctype(Yl.dtype, np.float32) + assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) +""" +# vim:sw=4:sts=4:et From cb18e9370777109ad04fe41752095f6839c5d4fe Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 2 Mar 2017 23:58:07 +0000 Subject: [PATCH 07/52] Added forward wrapper to transform2d --- dtcwt/tf/transform2d.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 90c8857..5ea213a 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -43,8 +43,23 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): except TypeError: self.qshift = qshift + self.forward_ops = [] + + def forward(self, X, nlevels=3, include_scale=False, graph=tf.get_default_graph()): + # Give info back to the user recommending they don't use the forward function + logging.info("""Calling the forward function will create operations on + the graph each time it is called, then create a session and execute + them. This is quite time consuming and wasteful. It is better to + call Transform2d.forward_op(), which returns a Pyramid of ops. To + evaluate this for a given input, call the Pyramid's .eval() + function, providing the input to it.""") + + #with graph as g + + + - def forward(self, X, nlevels=3, include_scale=False): + def forward_op(self, X, nlevels=3, include_scale=False, graph=tf.get_default_graph()): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [Batch, rows, cols] :param nlevels: Number of levels of wavelet decomposition From 820b5d286505e4fa71a03e79cbd53af38140bfa2 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 10:52:53 +0000 Subject: [PATCH 08/52] Continued designing Transform2d class --- dtcwt/tf/transform2d.py | 161 +++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 34 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 5ea213a..0bec8af 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -7,11 +7,94 @@ from dtcwt.coeffs import biort as _biort, qshift as _qshift from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT from dtcwt.utils import asfarray +from dtcwt.numpy import Transform2d as Transform2dNumPy -from dtcwt.tf.common import Pyramid_tf from dtcwt.tf.lowlevel import * -class Transform2d(object): +def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False, queue=None): + t = Transform2d(biort=biort, qshift=qshift, queue=queue) + r = t.forward(X, nlevels=nlevels, include_scale=include_scale) + if include_scale: + return r.lowpass, r.highpasses, r.scales + else: + return r.lowpass, r.highpasses + + +class Pyramid(object): + """A representation of a transform domain signal. + Backends are free to implement any class which respects this interface for + storing transform-domain signals. The inverse transform may accept a + backend-specific version of this class but should always accept any class + which corresponds to this interface. + .. py:attribute:: lowpass + A NumPy-compatible array containing the coarsest scale lowpass signal. + .. py:attribute:: highpasses + A tuple where each element is the complex subband coefficients for + corresponding scales finest to coarsest. + .. py:attribute:: scales + *(optional)* A tuple where each element is a NumPy-compatible array + containing the lowpass signal for corresponding scales finest to + coarsest. This is not required for the inverse and may be *None*. + """ + def __init__(self, p_holder, lowpass, highpasses, scales=None, data=None): + self.lowpass_op = lowpass + self.highpasses_ops = highpasses + self.scales_ops = scales + self.p_holder = p_holder + self.data = data + + @property + def lowpass(self): + if not hasattr(self, '_lowpass'): + with tf.Session() as sess: + self._lowpass = sess.run(self.lowpass_op, {self.p_holder : self.data}) \ + if self.lowpass_op is not None else None + return self._lowpass + + @property + def highpasses(self): + if not hasattr(self, '_highpasses'): + with tf.Session() as sess: + self._highpasses = tuple( + sess.run(layer_highpass, {self.p_holder : self.data}) for + layer_highpass in self.highpasses_ops) if \ + self.highpasses_ops is not None else None + return self._highpasses + + @property + def scales(self): + if not hasattr(self, '_scales'): + with tf.Session() as sess: + self._scales = tuple( + sess.run(layer_scale, {self.p_holder : self.data}) for + layer_scale in self.scales) if \ + self.scales_ops is not None else None + + return self._scales + + ''' + def eval(self, sess, placeholder, data): + try: + lo = sess.run(self.lowpass, {placeholder : data}) + hi = sess.run(self.highpasses, {placeholder : data}) + if self.scales is not None: + scales = sess.run(self.scales, {placeholder : data}) + else: + + scales = None + except ValueError: + lo = sess.run(self.lowpass, {placeholder : [data]}) + hi = sess.run(self.highpasses, {placeholder : [data]}) + if self.scales is not None: + scales = sess.run(self.scales, {placeholder : [data]}) + else: + scales = None + + + return Pyramid(lo, hi, scales) + ''' + +class Transform2d(Transform2dNumPy): """ An implementation of the 2D DT-CWT via Tensorflow. *biort* and *qshift* are the wavelets which parameterise the transform. @@ -30,36 +113,46 @@ class Transform2d(object): function again. """ - def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): - # Load bi-orthogonal wavelets - try: - self.biort = _biort(biort) - except TypeError: - self.biort = biort + def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, + graph=tf.get_default_graph()): + super(Transform2d, self).__init__(biort=biort, qshift=qshift) + self.graph = graph - # Load quarter sample shift wavelets - try: - self.qshift = _qshift(qshift) - except TypeError: - self.qshift = qshift - self.forward_ops = [] + def forward(self, X, nlevels=3, include_scale=False): + # Check the shape and form of the input + if not isinstance(X, tf.Tensor) and not isinstance(X, tf.Variable): + self.in_data = X + X_shape = X.shape + if len(X.shape) >= 3: + raise ValueError('''The entered variable has incorrect dimensions {}. + If X is a numpy array (or any non tensorflow object), it + must be of shape [height, width]. For colour images, please + enter each channel separately. If you wish to enter a batch + of images, please instead provide either a tf.Placeholder + or a tf.Variable input of size [batch, height, width]. + '''.format(original_size)) + X = tf.placeholder([None, X_shape[0], X_shape[1]], tf.float32) + + else: + self.in_data = None + X_shape = X.get_shape().as_list() + if len(X_shape) != 3: + raise ValueError('''The entered variable has incorrect dimensions {}. + If X is a tf placeholder or variable, it must be of shape + [batch, height, width] (batch can be None). For colour images, + please enter each channel separately. + '''.format(original_size)) - def forward(self, X, nlevels=3, include_scale=False, graph=tf.get_default_graph()): - # Give info back to the user recommending they don't use the forward function - logging.info("""Calling the forward function will create operations on - the graph each time it is called, then create a session and execute - them. This is quite time consuming and wasteful. It is better to - call Transform2d.forward_op(), which returns a Pyramid of ops. To - evaluate this for a given input, call the Pyramid's .eval() - function, providing the input to it.""") + original_size = X.get_shape().as_list()[1:] - #with graph as g + name = 'dtcwt_{}x{}'.format(original_size[0], original_size[1]) + with self.graph.name_scope(name): + return forward_op(X, nlevels, include_scale, self.in_data) - - def forward_op(self, X, nlevels=3, include_scale=False, graph=tf.get_default_graph()): + def forward_op(self, X, nlevels=3, include_scale=False, in_data=None): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [Batch, rows, cols] :param nlevels: Number of levels of wavelet decomposition @@ -93,13 +186,13 @@ def forward_op(self, X, nlevels=3, include_scale=False, graph=tf.get_default_gra raise ValueError('Qshift wavelet must have 12 or 8 components.') # Check the shape and form of the input - if not isinstance(X, tf.Tensor): - raise ValueError('Please provide the forward function with ' + - 'a tensorflow placeholder or variable of size [batch, width,' + - 'height] (batch can be None if you do not wish to specify it).') + if not isinstance(X, tf.Tensor) and not isinstance(X, tf.Variable): + raise ValueError('''Please provide the forward function with + a tensorflow placeholder or variable of size [batch, width, + height] (batch can be None if you do not wish to specify it).''') original_size = X.get_shape().as_list()[1:] - + if len(original_size) >= 3: raise ValueError('The entered variable has too many dimensions {}. If ' 'the final dimension are colour channels, please enter each ' + @@ -130,9 +223,9 @@ def forward_op(self, X, nlevels=3, include_scale=False, graph=tf.get_default_gra if nlevels == 0: if include_scale: - return Pyramid_ops(X, (), ()) + return Pyramid(X, (), ()) else: - return Pyramid_ops(X, ()) + return Pyramid(X, ()) ############################ Initialise ############################### @@ -245,9 +338,9 @@ def forward_op(self, X, nlevels=3, include_scale=False, graph=tf.get_default_gra 'The rightmost column has been duplicated, prior to decomposition.') if include_scale: - return Pyramid_ops(Yl, tuple(Yh), tuple(Yscale)) + return Pyramid(Yl, tuple(Yh), tuple(Yscale)) else: - return Pyramid_ops(Yl, tuple(Yh)) + return Pyramid(Yl, tuple(Yh)) def q2c(y): From 341ab65d16ce1a858ec0590b5209d35808c255f0 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 16:41:00 +0000 Subject: [PATCH 09/52] Updated test scripts Wrote new script for transform 2d Added checks in other tf scripts to make them not run if tensorflow is not installed. --- tests/test_tfTransform2d.py | 74 +++++++++++++++++++++++++++++++------ tests/test_tfcoldfilt.py | 4 ++ tests/test_tfcolfilter.py | 4 ++ tests/test_tfcolifilt.py | 4 ++ tests/test_tfrowdfilt.py | 8 +++- tests/test_tfrowfilter.py | 4 ++ tests/util.py | 2 + 7 files changed, 86 insertions(+), 14 deletions(-) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 8bcc6dc..832fd18 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -1,38 +1,45 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + from pytest import raises import numpy as np import tensorflow as tf -from dtcwt.tf import Transform2d +from dtcwt.tf import Transform2d, dtwavexfm2 +from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.numpy import Pyramid from dtcwt.coeffs import biort, qshift import tests.datasets as datasets +#from .util import skip_if_no_tf -TOLERANCE = 1e-12 +PRECISION_DECIMAL = 5 def setup(): - global mandrill, in_p + global mandrill, in_p, pyramid_ops mandrill = datasets.mandrill() in_p = tf.placeholder(tf.float32, [None, 512, 512]) + f = Transform2d() + pyramid_ops = f.forward(in_p, include_scale=True) def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 assert mandrill.max() <= 1 assert mandrill.dtype == np.float32 - assert in_p.get_shape() == (1, 512, 512) def test_simple(): - t = Transform2d(in_p) - with tf.Session() as sess: - Yl, Yh = t.eval(sess, in_p, mandrill) -""" + Yl, Yh = dtwavexfm2(mandrill) + def test_specific_wavelet(): Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), qshift=qshift('qshift_06')) +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_1d(): Yl, Yh = dtwavexfm2(mandrill[0,:]) +@pytest.mark.skip(reason='Not currently implemented') def test_3d(): with raises(ValueError): Yl, Yh = dtwavexfm2(np.dstack((mandrill, mandrill))) @@ -56,13 +63,13 @@ def test_odd_cols(): def test_odd_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) - def test_odd_rows_and_cols(): Yl, Yh = dtwavexfm2(mandrill[:,:509]) def test_odd_rows_and_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) +@pytest.mark.skip(reason='Inverse not currently implemented') def test_rot_symm_modified(): # This test only checks there is no error running these functions, not that they work Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) @@ -70,21 +77,23 @@ def test_rot_symm_modified(): def test_0_levels(): Yl, Yh = dtwavexfm2(mandrill, nlevels=0) - assert np.all(np.abs(Yl - mandrill) < TOLERANCE) + np.testing.assert_array_almost_equal(Yl, mandrill, PRECISION_DECIMAL) assert len(Yh) == 0 def test_0_levels_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, nlevels=0, include_scale=True) - assert np.all(np.abs(Yl - mandrill) < TOLERANCE) + np.testing.assert_array_almost_equal(Yl, mandrill, PRECISION_DECIMAL) assert len(Yh) == 0 assert len(Yscale) == 0 +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_input(): # Check that an integer input is correctly coerced into a floating point # array Yl, Yh = dtwavexfm2([[1,2,3,4], [1,2,3,4]]) assert np.any(Yl != 0) +@pytest.mark.skip(reason='Inverse not currently implemented') def test_integer_perfect_recon(): # Check that an integer input is correctly coerced into a floating point # array and reconstructed @@ -98,5 +107,46 @@ def test_float32_input(): Yl, Yh = dtwavexfm2(mandrill.astype(np.float32)) assert np.issubsctype(Yl.dtype, np.float32) assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) -""" + +def test_multiple_inputs(): + y = pyramid_ops.eval(mandrill) + y3 = pyramid_ops.eval([mandrill, mandrill, mandrill]) + assert y3.lowpass.shape == (3, *y.lowpass.shape) + for hi3, hi in zip(y3.highpasses, y.highpasses): + assert hi3.shape == (3, *hi.shape) + for s3, s in zip(y3.scales, y.scales): + assert s3.shape == (3, *s.shape) + +def test_results_match1(): + f_np = Transform2d_np() + p_np = f_np.forward(mandrill, include_scale=True) + + p_tf = pyramid_ops.eval(mandrill) + + np.testing.assert_array_almost_equal( + p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) + [np.testing.assert_array_almost_equal( + h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in + zip(p_np.highpasses, p_tf.highpasses)] + [np.testing.assert_array_almost_equal( + s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in + zip(p_np.scales, p_tf.scales)] + +def test_results_match2(): + im = mandrill[100:400,50:450] + f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') + p_np = f_np.forward(im, nlevels=3, include_scale=True) + + f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') + p_tf = f_tf.forward(im, nlevels=3, include_scale=True) + + np.testing.assert_array_almost_equal( + p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) + [np.testing.assert_array_almost_equal( + h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in + zip(p_np.highpasses, p_tf.highpasses)] + [np.testing.assert_array_almost_equal( + s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in + zip(p_np.scales, p_tf.scales)] + # vim:sw=4:sts=4:et diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py index 3091432..b0f8cdc 100644 --- a/tests/test_tfcoldfilt.py +++ b/tests/test_tfcoldfilt.py @@ -1,5 +1,9 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + import numpy as np import tensorflow as tf from dtcwt.coeffs import biort, qshift diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index cadab46..8e9cc76 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -1,5 +1,9 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + import numpy as np import tensorflow as tf from dtcwt.coeffs import biort, qshift diff --git a/tests/test_tfcolifilt.py b/tests/test_tfcolifilt.py index 33e41a4..0b7ab27 100644 --- a/tests/test_tfcolifilt.py +++ b/tests/test_tfcolifilt.py @@ -1,5 +1,9 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + import numpy as np from dtcwt.numpy.lowlevel import colifilt diff --git a/tests/test_tfrowdfilt.py b/tests/test_tfrowdfilt.py index 8453fc2..896f106 100644 --- a/tests/test_tfrowdfilt.py +++ b/tests/test_tfrowdfilt.py @@ -1,13 +1,17 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + +from pytest import raises + import numpy as np import tensorflow as tf from dtcwt.coeffs import biort, qshift from dtcwt.tf.lowlevel import rowdfilt from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt -from pytest import raises - import tests.datasets as datasets def setup(): diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index b00db5b..464dd13 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -1,5 +1,9 @@ import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + import numpy as np import tensorflow as tf from dtcwt.coeffs import biort, qshift diff --git a/tests/util.py b/tests/util.py index 14bb01f..7db65d9 100644 --- a/tests/util.py +++ b/tests/util.py @@ -3,6 +3,7 @@ import pytest from dtcwt.opencl.lowlevel import _HAVE_CL as HAVE_CL +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF from six.moves import xrange @@ -65,4 +66,5 @@ def summarise_cube(M, apron=4): ) skip_if_no_cl = pytest.mark.skipif(not HAVE_CL, reason="OpenCL not present") +skip_if_no_tf = pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") From dad089f87b66e23075408c7ab81fbce60af48f01 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 16:43:10 +0000 Subject: [PATCH 10/52] Updated tf core code from tests --- dtcwt/tf/__init__.py | 2 +- dtcwt/tf/lowlevel.py | 8 +- dtcwt/tf/transform2d.py | 186 ++++++++++++++++++++++++---------------- 3 files changed, 118 insertions(+), 78 deletions(-) diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index e5a987c..c7958f2 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -5,7 +5,7 @@ """ from .common import Pyramid_tf -from .transform2d import Transform2d +from .transform2d import Transform2d, dtwavexfm2 __all__ = [ 'Pyramid', diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index 4eec68d..11568c2 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -4,6 +4,12 @@ import numpy as np from dtcwt.utils import asfarray, as_column_vector +try: + import tensorflow as tf + _HAVE_TF = True +except ImportError: + _HAVE_TF = False + def _as_row_tensor(h): if isinstance(h, tf.Tensor): h = tf.reshape(h, [1, -1]) @@ -65,8 +71,6 @@ def colfilter(X, h): h_t = _as_col_tensor(h) m = h_t.get_shape().as_list()[0] m2 = m // 2 - print(m2) - print(h_t.shape) # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 0bec8af..7c8f22f 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -4,15 +4,18 @@ import tensorflow as tf import logging +from six.moves import xrange + from dtcwt.coeffs import biort as _biort, qshift as _qshift from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT from dtcwt.utils import asfarray from dtcwt.numpy import Transform2d as Transform2dNumPy +from dtcwt.numpy import Pyramid as Pyramid_np from dtcwt.tf.lowlevel import * def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False, queue=None): - t = Transform2d(biort=biort, qshift=qshift, queue=queue) + t = Transform2d(biort=biort, qshift=qshift) r = t.forward(X, nlevels=nlevels, include_scale=include_scale) if include_scale: return r.lowpass, r.highpasses, r.scales @@ -20,7 +23,7 @@ def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include return r.lowpass, r.highpasses -class Pyramid(object): +class Pyramid_tf(object): """A representation of a transform domain signal. Backends are free to implement any class which respects this interface for storing transform-domain signals. The inverse transform may accept a @@ -36,63 +39,58 @@ class Pyramid(object): containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. """ - def __init__(self, p_holder, lowpass, highpasses, scales=None, data=None): + def __init__(self, p_holder, lowpass, highpasses, scales=None, + graph=tf.get_default_graph()): self.lowpass_op = lowpass self.highpasses_ops = highpasses self.scales_ops = scales self.p_holder = p_holder - self.data = data - - @property - def lowpass(self): - if not hasattr(self, '_lowpass'): - with tf.Session() as sess: - self._lowpass = sess.run(self.lowpass_op, {self.p_holder : self.data}) \ - if self.lowpass_op is not None else None - return self._lowpass - - @property - def highpasses(self): - if not hasattr(self, '_highpasses'): - with tf.Session() as sess: - self._highpasses = tuple( - sess.run(layer_highpass, {self.p_holder : self.data}) for - layer_highpass in self.highpasses_ops) if \ - self.highpasses_ops is not None else None - return self._highpasses - - @property - def scales(self): - if not hasattr(self, '_scales'): - with tf.Session() as sess: - self._scales = tuple( - sess.run(layer_scale, {self.p_holder : self.data}) for - layer_scale in self.scales) if \ - self.scales_ops is not None else None - - return self._scales - - ''' - def eval(self, sess, placeholder, data): - try: - lo = sess.run(self.lowpass, {placeholder : data}) - hi = sess.run(self.highpasses, {placeholder : data}) - if self.scales is not None: - scales = sess.run(self.scales, {placeholder : data}) - else: - - scales = None - except ValueError: - lo = sess.run(self.lowpass, {placeholder : [data]}) - hi = sess.run(self.highpasses, {placeholder : [data]}) - if self.scales is not None: - scales = sess.run(self.scales, {placeholder : [data]}) - else: - scales = None + self.graph = graph + def _get_lowpass(self, data): + if self.lowpass_op is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = sess.run(self.lowpass_op, {self.p_holder : data}) + except ValueError: + y = sess.run(self.lowpass_op, {self.p_holder : [data]})[0] + return y + + def _get_highpasses(self, data): + if self.highpasses_ops is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = tuple( + [sess.run(layer_hp, {self.p_holder : data}) + for layer_hp in self.highpasses_ops]) + except ValueError: + y = tuple( + [sess.run(layer_hp, {self.p_holder : [data]})[0] + for layer_hp in self.highpasses_ops]) + return y + + def _get_scales(self, data): + if self.scales_ops is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = tuple( + sess.run(layer_scale, {self.p_holder : data}) + for layer_scale in self.scales_ops) + except ValueError: + y = tuple( + sess.run(layer_scale, {self.p_holder : [data]})[0] + for layer_scale in self.scales_ops) + return y + + def eval(self, data): + lo = self._get_lowpass(data) + hi = self._get_highpasses(data) + scales = self._get_scales(data) + return Pyramid_np(lo, hi, scales) - return Pyramid(lo, hi, scales) - ''' class Transform2d(Transform2dNumPy): """ @@ -113,17 +111,37 @@ class Transform2d(Transform2dNumPy): function again. """ - def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, - graph=tf.get_default_graph()): + def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): super(Transform2d, self).__init__(biort=biort, qshift=qshift) - self.graph = graph + # Use our own graph when the user calls forward with numpy arrays + self.np_graph = tf.Graph() + self.pyramids = {} + def _find_pyramid(self, shape): + find_key = '{}x{}'.format(shape[0], shape[1]) + for key,val in self.pyramids.items(): + if find_key == key: + return val + return None def forward(self, X, nlevels=3, include_scale=False): - # Check the shape and form of the input + ''' + Perform a forward transform on an image. Can provide the forward + transform with either an np array (naive usage), or a tensorflow + variable or placeholder (designed usage). + If a numpy array is provided, the forward function will create a graph + of the right size to match the input (or check if it has previously + created one), and then feed the input into the graph and evaluate it. + This operation will return a Pyramid() object similar to running the + numpy version would. + If a tensorflow variable or placeholder is provided, the forward + function will create a graph of the right size, and return + a Pyramid_ops() object. + ''' + + # Check if a numpy array was provided if not isinstance(X, tf.Tensor) and not isinstance(X, tf.Variable): - self.in_data = X - X_shape = X.shape + X = np.atleast_2d(asfarray(X)) if len(X.shape) >= 3: raise ValueError('''The entered variable has incorrect dimensions {}. If X is a numpy array (or any non tensorflow object), it @@ -132,10 +150,27 @@ def forward(self, X, nlevels=3, include_scale=False): of images, please instead provide either a tf.Placeholder or a tf.Variable input of size [batch, height, width]. '''.format(original_size)) - X = tf.placeholder([None, X_shape[0], X_shape[1]], tf.float32) + # Check if the ops already exist for an input of the given size + p_ops = self._find_pyramid(X.shape) + + # If not, create a graph + if p_ops is None: + ph = tf.placeholder(tf.float32, [None, X.shape[0], X.shape[1]]) + size = '{}x{}'.format(X.shape[0], X.shape[1]) + name = 'dtcwt_{}'.format(size) + with self.np_graph.name_scope(name): + p_ops = self._create_graph_ops(ph, nlevels, include_scale) + + # keep record of the pyramid so we can use it later if need be + self.pyramids[size] = p_ops + + # Evaluate the graph with the given input + with self.np_graph.as_default(): + return p_ops.eval(X) + + # A tensorflow object was provided else: - self.in_data = None X_shape = X.get_shape().as_list() if len(X_shape) != 3: raise ValueError('''The entered variable has incorrect dimensions {}. @@ -144,15 +179,13 @@ def forward(self, X, nlevels=3, include_scale=False): please enter each channel separately. '''.format(original_size)) - original_size = X.get_shape().as_list()[1:] - - name = 'dtcwt_{}x{}'.format(original_size[0], original_size[1]) - - with self.graph.name_scope(name): - return forward_op(X, nlevels, include_scale, self.in_data) - - - def forward_op(self, X, nlevels=3, include_scale=False, in_data=None): + original_size = X.get_shape().as_list()[1:] + size = '{}x{}'.format(original_size[0], original_size[1]) + name = 'dtcwt_{}'.format(size) + with tf.name_scope(name): + return self._create_graph_ops(X, nlevels, include_scale) + + def _create_graph_ops(self, X, nlevels=3, include_scale=False): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [Batch, rows, cols] :param nlevels: Number of levels of wavelet decomposition @@ -198,7 +231,8 @@ def forward_op(self, X, nlevels=3, include_scale=False, in_data=None): 'the final dimension are colour channels, please enter each ' + 'channel separately.'.format(original_size)) - + # Save the input placeholder/variable + X_in = X ############################## Resize ################################# # The next few lines of code check to see if the image is odd in size, # if so an extra ... row/column will be added to the bottom/right of the @@ -223,9 +257,9 @@ def forward_op(self, X, nlevels=3, include_scale=False, in_data=None): if nlevels == 0: if include_scale: - return Pyramid(X, (), ()) + return Pyramid_tf(X_in, X, (), ()) else: - return Pyramid(X, ()) + return Pyramid_tf(X_in, X, ()) ############################ Initialise ############################### @@ -338,9 +372,11 @@ def forward_op(self, X, nlevels=3, include_scale=False, in_data=None): 'The rightmost column has been duplicated, prior to decomposition.') if include_scale: - return Pyramid(Yl, tuple(Yh), tuple(Yscale)) + return Pyramid_tf(X_in, Yl, tuple(Yh), tuple(Yscale)) else: - return Pyramid(Yl, tuple(Yh)) + return Pyramid_tf(X_in, Yl, tuple(Yh)) + + def q2c(y): From b9b57de0b152d8c73e9e6affc96d22c7552b1a55 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 16:46:41 +0000 Subject: [PATCH 11/52] Removed ipython notebook from repo --- dtcwt_tf.ipynb | 1508 ------------------------------------------------ 1 file changed, 1508 deletions(-) delete mode 100644 dtcwt_tf.ipynb diff --git a/dtcwt_tf.ipynb b/dtcwt_tf.ipynb deleted file mode 100644 index 987a6de..0000000 --- a/dtcwt_tf.ipynb +++ /dev/null @@ -1,1508 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "import dtcwt\n", - "import tensorflow as tf\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "import os\n", - "%matplotlib notebook\n", - "sns.set_style(\"white\")\n", - "\n", - "from dtcwt.coeffs import biort as _biort, qshift as _qshift\n", - "from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT\n", - "from dtcwt.utils import appropriate_complex_type_for, asfarray\n", - "\n", - "from dtcwt.numpy.lowlevel import colfilter as colf, coldfilt as cold, colifilt as coli" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "im = np.load(os.path.join('tests', 'mandrill.npz'))['mandrill']\n", - "plt.imshow(im, cmap='gray', interpolation='none')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "from __future__ import absolute_import, division\n", - "\n", - "__all__ = [ 'colfilter', 'colifilt', 'coldfilt', ]\n", - "\n", - "import numpy as np\n", - "from six.moves import xrange\n", - "from dtcwt.utils import as_column_vector, asfarray, appropriate_complex_type_for, reflect\n", - "\n", - "def _centered(arr, newsize):\n", - " # Return the center newsize portion of the array.\n", - " # (Shamelessly cribbed from scipy.)\n", - " newsize = np.asanyarray(newsize)\n", - " currsize = np.array(arr.shape)\n", - " startind = (currsize - newsize) // 2\n", - " endind = startind + newsize\n", - " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", - " return arr[tuple(myslice)]\n", - "\n", - "# This is to allow easy replacement of these later with, possibly, GPU versions\n", - "_rfft = np.fft.rfft\n", - "_irfft = np.fft.irfft\n", - "\n", - "def _column_convolve(X, h):\n", - " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", - " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", - " *h*, the output will have the dtype of *X* appropriately expanded to a\n", - " floating point type if necessary.\n", - " We assume that h is small and so direct convolution is the most efficient.\n", - " \"\"\"\n", - " Xshape = np.asanyarray(X.shape)\n", - " h = h.flatten().astype(X.dtype)\n", - " h_size = h.shape[0]\n", - "\n", - " full_size = X.shape[0] + h_size - 1\n", - " Xshape[0] = full_size\n", - "\n", - " out = np.zeros(Xshape, dtype=X.dtype)\n", - " for idx in xrange(h_size):\n", - " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", - "\n", - " outShape = Xshape.copy()\n", - " outShape[0] = abs(X.shape[0] - h_size) + 1\n", - " return _centered(out, outShape)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "# Test outputs give expected results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "tf.reset_default_graph()\n", - "g = tf.get_default_graph()\n", - "dir(g)\n", - "g.get_collection('variables')\n", - "sess = tf.InteractiveSession(config=tf.ConfigProto(log_device_placement=True))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "#h1o = tf.Variable(f.qshift[0][::-1],trainable=False, dtype=tf.float32)\n", - "f = dtcwt.Transform2d()\n", - "h1o = tf.constant(f.qshift[0][::-1],dtype=tf.float32)\n", - " \n", - "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", - "init_op = tf.global_variables_initializer()\n", - "\n", - "qshift = f.qshift[0].astype('float32')\n", - "im_hat = colf(colf(colf(im, qshift),qshift),qshift)\n", - "y1 = colfilter(colfilter(colfilter(in_, h1o), h1o), h1o)\n", - "y2 = rowfilter(rowfilter(rowfilter(in_, h1o), h1o), h1o)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# Compare the 2\n", - "sess.run(init_op)\n", - "im_hat1 = sess.run(y1, feed_dict={in_:[im]})[0]\n", - "im_hat2 = sess.run(y2, feed_dict={in_:[im.T]})[0]\n", - " \n", - "np.testing.assert_array_almost_equal(im_hat, im_hat1, decimal=4)\n", - "np.testing.assert_array_almost_equal(im_hat, im_hat2.T, decimal=4)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "# Compare the execution times for direct filtering and GPU filtering" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "import time\n", - "h = f.qshift[0].astype('float32')\n", - "time1 = time.time()\n", - "for i in range(1000):\n", - " colf(colf(colf(im, h),h),h)\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "batch = np.stack([im]*100,axis=0)\n", - "\n", - "time1 = time.time()\n", - "for i in range(10):\n", - " b = sess.run(y1, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "batch = np.stack([im]*100,axis=0)\n", - "\n", - "time1 = time.time()\n", - "for i in range(10):\n", - " b = sess.run(y2, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "# Redefine the Transform" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "class Transform2d(object):\n", - " \"\"\"\n", - " An implementation of the 2D DT-CWT via Tensorflow. \n", - " *biort* and *qshift* are the wavelets which parameterise the transform.\n", - " If *biort* or *qshift* are strings, they are used as an argument to the\n", - " :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions.\n", - " Otherwise, they are interpreted as tuples of vectors giving filter\n", - " coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In\n", - " the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b).\n", - " \n", - " Creating an object of this class loads the necessary filters onto the \n", - " tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with \n", - " a placeholder will create a forward transform for an input of the placeholder's\n", - " size. You can evaluate the resulting ops several times feeding different\n", - " images into the placeholder *assuming* they have the same resolution. For \n", - " a different resolution image, call the :py:func:`Transform2d.forward` \n", - " function again.\n", - " \"\"\"\n", - "\n", - " def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT):\n", - " # Load bi-orthogonal wavelets\n", - " try:\n", - " self.biort = _biort(biort)\n", - " except TypeError:\n", - " self.biort = biort\n", - "\n", - " # Load quarter sample shift wavelets\n", - " try:\n", - " self.qshift = _qshift(qshift)\n", - " except TypeError:\n", - " self.qshift = qshift\n", - "\n", - " ### Load the ops onto the graph for the filter banks\n", - "\n", - " # If biort has 6 elements instead of 4, then it's a modified\n", - " # rotationally symmetric wavelet\n", - " # h0o - analysis low pass filter\n", - " # g0o - synthesis low pass filter\n", - " # h1o - analysis high pass filter\n", - " # g1o - synthesis high pass filter\n", - " # h2o - analysis band pass filter for 45 deg wavelets\n", - " # g2o - synthesis band pass filter for 45 deg wavelets\n", - " if len(self.biort) == 4:\n", - " # h0o, g0o, h1o, g1o = self.biort \n", - " self.h0o = tf.constant(self.biort[0], dtype=tf.float32, name='dtcwt/h0o')\n", - " self.g0o = tf.constant(self.biort[1], dtype=tf.float32, name='dtcwt/g0o')\n", - " self.h1o = tf.constant(self.biort[2], dtype=tf.float32, name='dtcwt/h1o')\n", - " self.g1o = tf.constant(self.biort[3], dtype=tf.float32, name='dtcwt/g1o')\n", - " elif len(self.biort) == 6:\n", - " #h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", - " self.h0o = tf.constant(self.biort[0], dtype=tf.float32, name='dtcwt/h0o')\n", - " self.g0o = tf.constant(self.biort[1], dtype=tf.float32, name='dtcwt/g0o')\n", - " self.h1o = tf.constant(self.biort[2], dtype=tf.float32, name='dtcwt/h1o')\n", - " self.g1o = tf.constant(self.biort[3], dtype=tf.float32, name='dtcwt/g1o')\n", - " self.h2o = tf.constant(self.biort[4], dtype=tf.float32, name='dtcwt/h2o')\n", - " self.g2o = tf.constant(self.biort[5], dtype=tf.float32, name='dtcwt/g2o')\n", - " else:\n", - " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", - "\n", - " \n", - " # If qshift has 12 elements instead of 8, then it's a modified\n", - " # rotationally symmetric wavelet \n", - " # h0a - analysis low pass filter tree a\n", - " # h0b - analysis low pass filter tree b\n", - " # h1a - analysis high pass filter tree a\n", - " # h1b - analysis high pass filter tree b\n", - " # h2a - analysis band pass filter tree a (for 45 deg wavelets)\n", - " # h2b - analysis band pass filter tree b (for 45 deg wavelets)\n", - " # g.. - synthesis equivalents\n", - " \n", - " # We have to reverse the qshift filters, as tensorflow's conv2d is\n", - " # really cross-correlation. Note that we didn't have to do this for\n", - " # biorthogonal filters as they are already symmetric.\n", - " if len(self.qshift) == 8:\n", - " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", - " self.h0a = tf.constant(self.qshift[0][::-1], dtype=tf.float32, name='dtcwt/h0a')\n", - " self.h0b = tf.constant(self.qshift[1][::-1], dtype=tf.float32, name='dtcwt/h0b')\n", - " self.g0a = tf.constant(self.qshift[2][::-1], dtype=tf.float32, name='dtcwt/g0a')\n", - " self.g0b = tf.constant(self.qshift[3][::-1], dtype=tf.float32, name='dtcwt/g0b')\n", - " self.h1a = tf.constant(self.qshift[4][::-1], dtype=tf.float32, name='dtcwt/h1a')\n", - " self.h1b = tf.constant(self.qshift[5][::-1], dtype=tf.float32, name='dtcwt/h1b')\n", - " self.g1a = tf.constant(self.qshift[6][::-1], dtype=tf.float32, name='dtcwt/g1a')\n", - " self.g1b = tf.constant(self.qshift[7][::-1], dtype=tf.float32, name='dtcwt/g1b')\n", - " elif len(self.qshift) == 12:\n", - " #h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b = self.qshift[:10]\n", - " self.h0a = tf.constant(self.qshift[0][::-1], dtype=tf.float32, name='dtcwt/h0a')\n", - " self.h0b = tf.constant(self.qshift[1][::-1], dtype=tf.float32, name='dtcwt/h0b')\n", - " self.g0a = tf.constant(self.qshift[2][::-1], dtype=tf.float32, name='dtcwt/g0a')\n", - " self.g0b = tf.constant(self.qshift[3][::-1], dtype=tf.float32, name='dtcwt/g0b')\n", - " self.h1a = tf.constant(self.qshift[4][::-1], dtype=tf.float32, name='dtcwt/h1a')\n", - " self.h1b = tf.constant(self.qshift[5][::-1], dtype=tf.float32, name='dtcwt/h1b')\n", - " self.g1a = tf.constant(self.qshift[6][::-1], dtype=tf.float32, name='dtcwt/g1a')\n", - " self.g1b = tf.constant(self.qshift[7][::-1], dtype=tf.float32, name='dtcwt/g1b')\n", - " self.h2a = tf.constant(self.qshift[8][::-1], dtype=tf.float32, name='dtcwt/h2a')\n", - " self.h2b = tf.constant(self.qshift[9][::-1], dtype=tf.float32, name='dtcwt/h2b')\n", - " else:\n", - " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", - "\n", - " \n", - " \n", - " def forward(self, X, nlevels=3, include_scale=False):\n", - " \"\"\"Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*.\n", - " :param X: 3D real array of size [Batch, rows, cols]\n", - " :param nlevels: Number of levels of wavelet decomposition\n", - " :param include_scale: True if you want to receive the lowpass coefficients at\n", - " intermediate layers.\n", - " :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal\n", - " .. codeauthor:: Fergal Cotter , Feb 2017\n", - " .. codeauthor:: Rich Wareham , Aug 2013\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001\n", - " \"\"\"\n", - "\n", - " # Check the shape of the input\n", - " original_size = X.get_shape().as_list()[1:]\n", - " \n", - " if len(original_size) >= 3:\n", - " raise ValueError('The entered image is {0}, please enter each image slice separately.'.\n", - " format('x'.join(list(str(s) for s in X.shape))))\n", - "\n", - "\n", - " ############################## Resize #################################\n", - " # The next few lines of code check to see if the image is odd in size, \n", - " # if so an extra ... row/column will be added to the bottom/right of the \n", - " # image\n", - " initial_row_extend = 0 #initialise\n", - " initial_col_extend = 0\n", - " if original_size[0] % 2 != 0:\n", - " # if X.shape[0] is not divisable by 2 then we need to extend X by \n", - " # adding a row at the bottom\n", - " bottom_row = tf.slice(X, [0, original_size[0] - 1, 0], [-1, 1, -1])\n", - " X = tf.concat([X, bottom_row], axis=1)\n", - " initial_row_extend = 1\n", - "\n", - " if original_size[1] % 2 != 0:\n", - " # if X.shape[1] is not divisable by 2 then we need to extend X by \n", - " # adding a col to the right\n", - " right_col = tf.slice(X, [0, 0, original_size[1] - 1], [-1, -1, 1])\n", - " X = tf.concat([X, right_col], axis=2)\n", - " initial_col_extend = 1\n", - "\n", - " extended_size = X.get_shape().as_list()[1:3]\n", - "\n", - " if nlevels == 0:\n", - " if include_scale:\n", - " return Pyramid_ops(X, (), ())\n", - " else:\n", - " return Pyramid_ops(X, ())\n", - "\n", - " \n", - " ############################ Initialise ###############################\n", - " Yh = [None,] * nlevels\n", - " if include_scale:\n", - " # this is only required if the user specifies a third output component.\n", - " Yscale = [None,] * nlevels\n", - "\n", - " ############################# Level 1 #################################\n", - " # Uses the biorthogonal filters\n", - " if nlevels >= 1:\n", - " # Do odd top-level filters on cols.\n", - " Lo = colfilter(X, self.h0o)\n", - " Hi = colfilter(X, self.h1o)\n", - " if len(self.biort) >= 6:\n", - " Ba = colfilter(X, self.h2o)\n", - "\n", - " # Do odd top-level filters on rows.\n", - " LoLo = rowfilter(Lo, self.h0o)\n", - " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", - " \n", - " # Horizontal wavelet pair (15 & 165 degrees)\n", - " horiz = q2c(rowfilter(Hi, self.h0o)) \n", - " \n", - " # Vertical wavelet pair (75 & 105 degrees)\n", - " vertic = q2c(rowfilter(Lo, self.h1o)) \n", - " \n", - " # Diagonal wavelet pair (45 & 135 degrees)\n", - " if len(self.biort) >= 6:\n", - " diag = q2c(rowfilter(Ba, self.h2o)) \n", - " else:\n", - " diag = q2c(rowfilter(Hi, self.h1o)) \n", - " \n", - " # Pack all 6 tensors into one \n", - " Yh[0] = tf.stack(\n", - " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", - " axis=3)\n", - " \n", - " if include_scale:\n", - " Yscale[0] = LoLo\n", - " \n", - " \n", - " ############################# Level 2+ ################################\n", - " # Uses the qshift filters \n", - " for level in xrange(1, nlevels):\n", - " row_size, col_size = LoLo_shape[0], LoLo_shape[1]\n", - " if row_size % 4 != 0:\n", - " bottom_row = tf.slice(LoLo, [0, row_size - 2, 0], [-1, 2, -1])\n", - " LoLo = tf.concat([LoLo, bottom_row], axis=1)\n", - "\n", - " if col_size % 4 != 0:\n", - " right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2])\n", - " LoLo = tf.concat([LoLo, right_col], axis=2)\n", - "\n", - " # Do even Qshift filters on cols.\n", - " Lo = coldfilt(LoLo, self.h0b, self.h0a)\n", - " Hi = coldfilt(LoLo, self.h1b, self.h1a)\n", - " if len(self.qshift) >= 12:\n", - " Ba = coldfilt(LoLo, self.h2b, self.h2a)\n", - "\n", - " # Do even Qshift filters on rows.\n", - " LoLo = rowdfilt(Lo, self.h0b, self.h0a)\n", - " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", - " \n", - " # Horizontal wavelet pair (15 & 165 degrees)\n", - " horiz = q2c(rowdfilt(Hi, self.h0b, self.h0a)) \n", - " \n", - " # Vertical wavelet pair (75 & 105 degrees)\n", - " vertic = q2c(rowdfilt(Lo, self.h1b, self.h1a)) \n", - " \n", - " # Diagonal wavelet pair (45 & 135 degrees)\n", - " if len(self.qshift) >= 12:\n", - " diag = q2c(rowdfilt(Ba, self.h2b, self.h2a)) \n", - " else:\n", - " diag = q2c(rowdfilt(Hi, self.h1b, self.h1a)) \n", - " \n", - " # Pack all 6 tensors into one \n", - " Yh[level] = tf.stack(\n", - " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", - " axis=3)\n", - " \n", - " if include_scale:\n", - " Yscale[level] = LoLo\n", - " \n", - " Yl = LoLo\n", - " \n", - " if initial_row_extend == 1 and initial_col_extend == 1:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The bottom row and rightmost column have been duplicated, prior to decomposition.')\n", - "\n", - " if initial_row_extend == 1 and initial_col_extend == 0:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The bottom row has been duplicated, prior to decomposition.')\n", - "\n", - " if initial_row_extend == 0 and initial_col_extend == 1:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The rightmost column has been duplicated, prior to decomposition.')\n", - "\n", - " if include_scale:\n", - " return Pyramid_ops(Yl, tuple(Yh), tuple(Yscale))\n", - " else:\n", - " return Pyramid_ops(Yl, tuple(Yh))\n", - " \n", - "\n", - "def q2c(y):\n", - " \"\"\"\n", - " Convert from quads in y to complex numbers in z.\n", - " \"\"\"\n", - "\n", - " # Arrange pixels from the corners of the quads into\n", - " # 2 subimages of alternate real and imag pixels.\n", - " # a----b\n", - " # | |\n", - " # | |\n", - " # c----d\n", - " # Combine (a,b) and (d,c) to form two complex subimages.\n", - " a,b,c,d = y[:, 0::2, 0::2], y[:, 0::2,1::2], y[:, 1::2,0::2], y[:, 1::2,1::2]\n", - " \n", - " p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2)\n", - " q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2)\n", - "\n", - " # Form the 2 highpasses in z.\n", - " return (p-q, p+q) " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "def med_level(X, h0b, h0a, h1b, h1a):\n", - " # Do even Qshift filters on cols.\n", - " Lo = coldfilt(X, h0b, h0a)\n", - " Hi = coldfilt(X, h1b, h1a)\n", - " if False >= 12:\n", - " Ba = coldfilt(X, h2b, h2a)\n", - "\n", - " # Do even Qshift filters on rows.\n", - " LoLo = rowdfilt(Lo, h0b, h0a)\n", - " LoLo_shape = LoLo.get_shape().as_list()[1:3] \n", - "\n", - " # Horizontal wavelet pair (15 & 165 degrees)\n", - " horiz = q2c(rowdfilt(Hi, h0b, h0a)) \n", - "\n", - " # Vertical wavelet pair (75 & 105 degrees)\n", - " vertic = q2c(rowdfilt(Lo, h1b, h1a)) \n", - "\n", - " # Diagonal wavelet pair (45 & 135 degrees)\n", - " if False >= 12:\n", - " diag = q2c(rowdfilt(Ba, h2b, h2a)) \n", - " else:\n", - " diag = q2c(rowdfilt(Hi, h1b, h1a)) \n", - "\n", - " # Pack all 6 tensors into one \n", - " Yh = tf.stack(\n", - " [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]],\n", - " axis=3)\n", - " \n", - " return LoLo, Yh" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "a = tf.reduce_sum(f.h1b*f.h1a)\n", - "print(sess.run(a))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "in_t = tf.expand_dims(tf.constant(p2.scales[0]),axis=0)\n", - "a_op, b_op = med_level(in_t, f.h0b, f.h0a, f.h1b, f.h1a)\n", - "a, b = sess.run([a_op,b_op])\n", - "np.testing.assert_array_almost_equal(a[0], p2.scales[1], decimal=4)\n", - "np.testing.assert_array_almost_equal(b[0], p2.highpasses[1], decimal=4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "tf.reset_default_graph()\n", - "sess = tf.InteractiveSession(config=tf.ConfigProto(log_device_placement=True))\n", - "\n", - "f = Transform2d(biort='near_sym_a',qshift='qshift_b') \n", - "f2 = dtcwt.Transform2d(biort='near_sym_a',qshift='qshift_b')\n", - "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", - "\n", - "\n", - "h0b, h0a = sess.run([f.h0b, f.h0a])\n", - "out_1 = cold(im.T,h0b[::-1],h0a[::-1]).T\n", - "out_2 = cold(im, h0b[::-1],h0a[::-1])\n", - "in_t = tf.expand_dims(tf.constant(im),axis=0)\n", - "a_r = rowdfilt(in_t,f.h0b,f.h0a)\n", - "a_c = coldfilt(in_t,f.h0b,f.h0a)\n", - "a,a2 = sess.run([a_r,a_c])\n", - "np.testing.assert_array_almost_equal(a[0], out_1, decimal=4)\n", - "np.testing.assert_array_almost_equal(a2[0], out_2, decimal=4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "a.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "p_op = f.forward(in_, nlevels=3)\n", - "p2 = f2.forward(im, nlevels=3,include_scale=True)\n", - "p = p_op.eval(sess, in_, [im])\n", - "\n", - "lo = p.lowpass[0]\n", - "hi1 = p.highpasses[1][0]\n", - "hi2 = p2.highpasses[1]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# Check that the results are the same\n", - "print(p.lowpass.shape)\n", - "print(p2.lowpass.shape)\n", - "print(p.highpasses[0].shape)\n", - "print(p2.highpasses[0].shape)\n", - "np.testing.assert_array_almost_equal(p.lowpass[0], p2.lowpass, decimal=4)\n", - "for i in range(3):\n", - " np.testing.assert_array_almost_equal(p.highpasses[i][0], p2.highpasses[i], decimal=4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "b = tf.reshape(f.h0a, [1,5,2,1])\n", - "c = tf.stack([f.h0a[0::2], f.h0a[1::2]], axis=-1)\n", - "c = tf.reshape(c, [5,2])\n", - "a_e, b_e, c_e = sess.run([f.h0a, b,c])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# Interleaving Columns\n", - "a = np.random.randn(10,2)\n", - "b = np.random.randn(10,2)\n", - "Y = np.zeros((20,2))\n", - "Y[0::2,:], Y[1::2,:] = a,b\n", - "a_t = tf.constant(a,dtype=tf.float32)\n", - "b_t = tf.constant(b,dtype=tf.float32)\n", - "Y_t1 = tf.stack([a_t,b_t], axis=1)\n", - "Y_t = tf.reshape(Y_t1, [20,2])\n", - "Y2 = sess.run(Y_t)\n", - "np.testing.assert_array_almost_equal(Y, Y2, decimal=4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "Y_t2 = tf.reshape(Y_t, [10,2,2])\n", - "a2_t, b2_t = tf.unstack(Y_t2,axis=1)\n", - "a3_t, b3_t = Y_t[0::2,:], Y_t[1::2,:]\n", - "a2,b2 = sess.run([a2_t,b2_t])\n", - "a3,b3 = sess.run([a3_t,b3_t])\n", - "t = np.arange(0,10,2,dtype=np.int32)\n", - "a4_t = Y_t[t,:]\n", - "a4 = sess.run(a4_t)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "\"\"\"\n", - " if len(X.shape) >= 3:\n", - " raise ValueError('The entered image is {0}, please enter each image slice separately.'.\n", - " format('x'.join(list(str(s) for s in X.shape))))\n", - "\n", - " \n", - " for level in xrange(1, nlevels):\n", - " row_size, col_size = LoLo.shape\n", - " if row_size % 4 != 0:\n", - " # Extend by 2 rows if no. of rows of LoLo are not divisable by 4\n", - " LoLo = np.vstack((LoLo[:1,:], LoLo, LoLo[-1:,:]))\n", - "\n", - " if col_size % 4 != 0:\n", - " # Extend by 2 cols if no. of cols of LoLo are not divisable by 4\n", - " LoLo = np.hstack((LoLo[:,:1], LoLo, LoLo[:,-1:]))\n", - "\n", - " # Do even Qshift filters on rows.\n", - " Lo = coldfilt(LoLo,h0b,h0a).T\n", - " Hi = coldfilt(LoLo,h1b,h1a).T\n", - " if len(self.qshift) >= 12:\n", - " Ba = coldfilt(LoLo,h2b,h2a).T\n", - "\n", - " # Do even Qshift filters on columns.\n", - " LoLo = coldfilt(Lo,h0b,h0a).T\n", - "\n", - " Yh[level] = np.zeros((LoLo.shape[0]>>1, LoLo.shape[1]>>1, 6), dtype=complex_dtype)\n", - " Yh[level][:,:,0:6:5] = q2c(coldfilt(Hi,h0b,h0a).T) # Horizontal\n", - " Yh[level][:,:,2:4:1] = q2c(coldfilt(Lo,h1b,h1a).T) # Vertical\n", - " if len(self.qshift) >= 12:\n", - " Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal \n", - " else:\n", - " Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal \n", - "\n", - " if include_scale:\n", - " Yscale[level] = LoLo\n", - "\n", - " Yl = LoLo\n", - "\n", - " if initial_row_extend == 1 and initial_col_extend == 1:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The bottom row and rightmost column have been duplicated, prior to decomposition.')\n", - "\n", - " if initial_row_extend == 1 and initial_col_extend == 0:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The bottom row has been duplicated, prior to decomposition.')\n", - "\n", - " if initial_row_extend == 0 and initial_col_extend == 1:\n", - " logging.warn('The image entered is now a {0} NOT a {1}.'.format(\n", - " 'x'.join(list(str(s) for s in extended_size)),\n", - " 'x'.join(list(str(s) for s in original_size))))\n", - " logging.warn(\n", - " 'The rightmost column has been duplicated, prior to decomposition.')\n", - "\n", - " if include_scale:\n", - " return Pyramid(Yl, tuple(Yh), tuple(Yscale))\n", - " else:\n", - " return Pyramid(Yl, tuple(Yh))\n", - "\"\"\"\n", - "\n", - "# def inverse(self, pyramid, gain_mask=None):\n", - " \"\"\"Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D\n", - " reconstruction.\n", - " :param pyramid: A :py:class:`dtcwt.Pyramid`-like class holding the transform domain representation to invert.\n", - " :param gain_mask: Gain to be applied to each subband.\n", - " :returns: A numpy-array compatible instance with the reconstruction.\n", - " The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction\n", - " *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for\n", - " band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are\n", - " zero-indexed.\n", - " .. codeauthor:: Rich Wareham , Aug 2013\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002\n", - " \"\"\"\n", - "\"\"\"\n", - " Yl = pyramid.lowpass\n", - " Yh = pyramid.highpasses\n", - "\n", - " a = len(Yh) # No of levels.\n", - "\n", - " if gain_mask is None:\n", - " gain_mask = np.ones((6,a)) # Default gain_mask.\n", - "\n", - " gain_mask = np.array(gain_mask)\n", - "\n", - " # If biort has 6 elements instead of 4, then it's a modified\n", - " # rotationally symmetric wavelet\n", - " # FIXME: there's probably a nicer way to do this\n", - " if len(self.biort) == 4:\n", - " h0o, g0o, h1o, g1o = self.biort\n", - " elif len(self.biort) == 6:\n", - " h0o, g0o, h1o, g1o, h2o, g2o = self.biort\n", - " else:\n", - " raise ValueError('Biort wavelet must have 6 or 4 components.')\n", - "\n", - " # If qshift has 12 elements instead of 8, then it's a modified\n", - " # rotationally symmetric wavelet\n", - " # FIXME: there's probably a nicer way to do this\n", - " if len(self.qshift) == 8:\n", - " h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift\n", - " elif len(self.qshift) == 12:\n", - " h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b, g2a, g2b = self.qshift\n", - " else:\n", - " raise ValueError('Qshift wavelet must have 12 or 8 components.')\n", - "\n", - " current_level = a\n", - " Z = Yl\n", - "\n", - " while current_level >= 2: # this ensures that for level 1 we never do the following\n", - " lh = c2q(Yh[current_level-1][:,:,[0, 5]], gain_mask[[0, 5], current_level-1])\n", - " hl = c2q(Yh[current_level-1][:,:,[2, 3]], gain_mask[[2, 3], current_level-1])\n", - " hh = c2q(Yh[current_level-1][:,:,[1, 4]], gain_mask[[1, 4], current_level-1])\n", - "\n", - " # Do even Qshift filters on columns.\n", - " y1 = colifilt(Z,g0b,g0a) + colifilt(lh,g1b,g1a)\n", - "\n", - " if len(self.qshift) >= 12:\n", - " y2 = colifilt(hl,g0b,g0a)\n", - " y2bp = colifilt(hh,g2b,g2a)\n", - "\n", - " # Do even Qshift filters on rows.\n", - " Z = (colifilt(y1.T,g0b,g0a) + colifilt(y2.T,g1b,g1a) + colifilt(y2bp.T, g2b, g2a)).T\n", - " else:\n", - " y2 = colifilt(hl,g0b,g0a) + colifilt(hh,g1b,g1a)\n", - "\n", - " # Do even Qshift filters on rows.\n", - " Z = (colifilt(y1.T,g0b,g0a) + colifilt(y2.T,g1b,g1a)).T\n", - "\n", - " # Check size of Z and crop as required\n", - " [row_size, col_size] = Z.shape\n", - " S = 2*np.array(Yh[current_level-2].shape)\n", - " if row_size != S[0]: # check to see if this result needs to be cropped for the rows\n", - " Z = Z[1:-1,:]\n", - " if col_size != S[1]: # check to see if this result needs to be cropped for the cols\n", - " Z = Z[:,1:-1]\n", - "\n", - " if np.any(np.array(Z.shape) != S[:2]):\n", - " raise ValueError('Sizes of highpasses are not valid for DTWAVEIFM2')\n", - " \n", - " current_level = current_level - 1\n", - "\n", - " if current_level == 1:\n", - " lh = c2q(Yh[current_level-1][:,:,[0, 5]],gain_mask[[0, 5],current_level-1])\n", - " hl = c2q(Yh[current_level-1][:,:,[2, 3]],gain_mask[[2, 3],current_level-1])\n", - " hh = c2q(Yh[current_level-1][:,:,[1, 4]],gain_mask[[1, 4],current_level-1])\n", - "\n", - " # Do odd top-level filters on columns.\n", - " y1 = colfilter(Z,g0o) + colfilter(lh,g1o)\n", - "\n", - " if len(self.biort) >= 6:\n", - " y2 = colfilter(hl,g0o)\n", - " y2bp = colfilter(hh,g2o)\n", - "\n", - " # Do odd top-level filters on rows.\n", - " Z = (colfilter(y1.T,g0o) + colfilter(y2.T,g1o) + colfilter(y2bp.T, g2o)).T\n", - " else:\n", - " y2 = colfilter(hl,g0o) + colfilter(hh,g1o)\n", - "\n", - " # Do odd top-level filters on rows.\n", - " Z = (colfilter(y1.T,g0o) + colfilter(y2.T,g1o)).T\n", - "\n", - " return Z\n", - "\"\"\"\n", - "#==========================================================================================\n", - "# ********** INTERNAL FUNCTIONS **********\n", - "#==========================================================================================\n", - "\n", - "\n", - "\n", - "def c2q(w,gain):\n", - " \"\"\"\n", - " Scale by gain and convert from complex w(:,:,1:2) to real quad-numbers\n", - " in z.\n", - " Arrange pixels from the real and imag parts of the 2 highpasses\n", - " into 4 separate subimages .\n", - " A----B Re Im of w(:,:,1)\n", - " | |\n", - " | |\n", - " C----D Re Im of w(:,:,2)\n", - " \"\"\"\n", - "\n", - " x = np.zeros((w.shape[0] << 1, w.shape[1] << 1), dtype=w.real.dtype)\n", - "\n", - " sc = np.sqrt(0.5) * gain\n", - " P = w[:,:,0]*sc[0] + w[:,:,1]*sc[1]\n", - " Q = w[:,:,0]*sc[0] - w[:,:,1]*sc[1]\n", - "\n", - " # Recover each of the 4 corners of the quads.\n", - " x[0::2, 0::2] = P.real # a = (A+C)*sc\n", - " x[0::2, 1::2] = P.imag # b = (B+D)*sc\n", - " x[1::2, 0::2] = Q.imag # c = (B-D)*sc\n", - " x[1::2, 1::2] = -Q.real # d = (C-A)*sc\n", - "\n", - " return x" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "def _centered(arr, newsize):\n", - " # Return the center newsize portion of the array.\n", - " # (Shamelessly cribbed from scipy.)\n", - " newsize = np.asanyarray(newsize)\n", - " currsize = np.array(arr.shape)\n", - " startind = (currsize - newsize) // 2\n", - " endind = startind + newsize\n", - " myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]\n", - " return arr[tuple(myslice)]\n", - "\n", - "# This is to allow easy replacement of these later with, possibly, GPU versions\n", - "_rfft = np.fft.rfft\n", - "_irfft = np.fft.irfft\n", - "\n", - "def _column_convolve(X, h):\n", - " \"\"\"Convolve the columns of *X* with *h* returning only the 'valid' section,\n", - " i.e. those values unaffected by zero padding. Irrespective of the ftype of\n", - " *h*, the output will have the dtype of *X* appropriately expanded to a\n", - " floating point type if necessary.\n", - " We assume that h is small and so direct convolution is the most efficient.\n", - " \"\"\"\n", - " Xshape = np.asanyarray(X.shape)\n", - " h = h.flatten().astype(X.dtype)\n", - " h_size = h.shape[0]\n", - "\n", - " full_size = X.shape[0] + h_size - 1\n", - " Xshape[0] = full_size\n", - "\n", - " out = np.zeros(Xshape, dtype=X.dtype)\n", - " for idx in xrange(h_size):\n", - " out[idx:(idx+X.shape[0]),...] += X * h[idx]\n", - "\n", - " outShape = Xshape.copy()\n", - " outShape[0] = abs(X.shape[0] - h_size) + 1\n", - " return _centered(out, outShape)\n", - "\n", - "\n", - " return Y\n", - "\n", - "def coldfilt(X, ha, hb):\n", - " \"\"\"Filter the columns of image X using the two filters ha and hb =\n", - " reverse(ha). ha operates on the odd samples of X and hb on the even\n", - " samples. Both filters should be even length, and h should be approx linear\n", - " phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| >\n", - " |h(m/2 + 1)|`).\n", - " .. code-block:: text\n", - " ext top edge bottom edge ext\n", - " Level 1: ! | ! | !\n", - " odd filt on . b b b b a a a a a a a a b b b b\n", - " odd filt on . a a a a b b b b b b b b a a a a\n", - " Level 2: ! | ! | !\n", - " +q filt on x b b a a a a b b\n", - " -q filt on o a a b b b b a a\n", - " The output is decimated by two from the input sample rate and the results\n", - " from the two filters, Ya and Yb, are interleaved to give Y. Symmetric\n", - " extension with repeated end samples is used on the composite X columns\n", - " before each filter is applied.\n", - " Raises ValueError if the number of rows in X is not a multiple of 4, the\n", - " length of ha does not match hb or the lengths of ha or hb are non-even.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - " # Make sure all inputs are arrays\n", - " X = asfarray(X)\n", - " ha = asfarray(ha)\n", - " hb = asfarray(hb)\n", - "\n", - " r, c = X.shape\n", - " if r % 4 != 0:\n", - " raise ValueError('No. of rows in X must be a multiple of 4')\n", - "\n", - " if ha.shape != hb.shape:\n", - " raise ValueError('Shapes of ha and hb must be the same')\n", - "\n", - " if ha.shape[0] % 2 != 0:\n", - " raise ValueError('Lengths of ha and hb must be even')\n", - "\n", - " m = ha.shape[0]\n", - " m2 = np.fix(m*0.5)\n", - "\n", - " # Set up vector for symmetric extension of X with repeated end samples.\n", - " xe = reflect(np.arange(-m, r+m), -0.5, r-0.5)\n", - "\n", - " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", - " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", - " hao = as_column_vector(ha[0:m:2])\n", - " hae = as_column_vector(ha[1:m:2])\n", - " hbo = as_column_vector(hb[0:m:2])\n", - " hbe = as_column_vector(hb[1:m:2])\n", - " t = np.arange(5, r+2*m-2, 4)\n", - " r2 = r//2;\n", - " Y = np.zeros((r2,c), dtype=X.dtype)\n", - "\n", - " if np.sum(ha*hb) > 0:\n", - " s1 = slice(0, r2, 2)\n", - " s2 = slice(1, r2, 2)\n", - " else:\n", - " s2 = slice(0, r2, 2)\n", - " s1 = slice(1, r2, 2)\n", - "\n", - " # Perform filtering on columns of extended matrix X(xe,:) in 4 ways.\n", - " Y[s1,:] = _column_convolve(X[xe[t-1],:],hao) + _column_convolve(X[xe[t-3],:],hae)\n", - " Y[s2,:] = _column_convolve(X[xe[t],:],hbo) + _column_convolve(X[xe[t-2],:],hbe)\n", - "\n", - " return Y\n", - "\n", - "def colifilt(X, ha, hb):\n", - " \"\"\" Filter the columns of image X using the two filters ha and hb =\n", - " reverse(ha). ha operates on the odd samples of X and hb on the even\n", - " samples. Both filters should be even length, and h should be approx linear\n", - " phase with a quarter sample advance from its mid pt (i.e `:math:`|h(m/2)| >\n", - " |h(m/2 + 1)|`).\n", - " .. code-block:: text\n", - " ext left edge right edge ext\n", - " Level 2: ! | ! | !\n", - " +q filt on x b b a a a a b b\n", - " -q filt on o a a b b b b a a\n", - " Level 1: ! | ! | !\n", - " odd filt on . b b b b a a a a a a a a b b b b\n", - " odd filt on . a a a a b b b b b b b b a a a a\n", - " The output is interpolated by two from the input sample rate and the\n", - " results from the two filters, Ya and Yb, are interleaved to give Y.\n", - " Symmetric extension with repeated end samples is used on the composite X\n", - " columns before each filter is applied.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - " # Make sure all inputs are arrays\n", - " X = asfarray(X)\n", - " ha = asfarray(ha)\n", - " hb = asfarray(hb)\n", - "\n", - " r, c = X.shape\n", - " if r % 2 != 0:\n", - " raise ValueError('No. of rows in X must be a multiple of 2')\n", - "\n", - " if ha.shape != hb.shape:\n", - " raise ValueError('Shapes of ha and hb must be the same')\n", - "\n", - " if ha.shape[0] % 2 != 0:\n", - " raise ValueError('Lengths of ha and hb must be even')\n", - "\n", - " m = ha.shape[0]\n", - " m2 = np.fix(m*0.5)\n", - "\n", - " Y = np.zeros((r*2,c), dtype=X.dtype)\n", - " if not np.any(np.nonzero(X[:])[0]):\n", - " return Y\n", - "\n", - " if m2 % 2 == 0:\n", - " # m/2 is even, so set up t to start on d samples.\n", - " # Set up vector for symmetric extension of X with repeated end samples.\n", - " # Use 'reflect' so r < m2 works OK.\n", - " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", - "\n", - " t = np.arange(3, r+m, 2)\n", - " if np.sum(ha*hb) > 0:\n", - " ta = t\n", - " tb = t - 1\n", - " else:\n", - " ta = t - 1\n", - " tb = t\n", - "\n", - " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", - " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", - " hao = as_column_vector(ha[0:m:2])\n", - " hae = as_column_vector(ha[1:m:2])\n", - " hbo = as_column_vector(hb[0:m:2])\n", - " hbe = as_column_vector(hb[1:m:2])\n", - "\n", - " s = np.arange(0,r*2,4)\n", - "\n", - " Y[s,:] = _column_convolve(X[xe[tb-2],:],hae)\n", - " Y[s+1,:] = _column_convolve(X[xe[ta-2],:],hbe)\n", - " Y[s+2,:] = _column_convolve(X[xe[tb ],:],hao)\n", - " Y[s+3,:] = _column_convolve(X[xe[ta ],:],hbo)\n", - " else:\n", - " # m/2 is odd, so set up t to start on b samples.\n", - " # Set up vector for symmetric extension of X with repeated end samples.\n", - " # Use 'reflect' so r < m2 works OK.\n", - " xe = reflect(np.arange(-m2, r+m2, dtype=np.int), -0.5, r-0.5)\n", - "\n", - " t = np.arange(2, r+m-1, 2)\n", - " if np.sum(ha*hb) > 0:\n", - " ta = t\n", - " tb = t - 1\n", - " else:\n", - " ta = t - 1\n", - " tb = t\n", - "\n", - " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", - " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", - " hao = as_column_vector(ha[0:m:2])\n", - " hae = as_column_vector(ha[1:m:2])\n", - " hbo = as_column_vector(hb[0:m:2])\n", - " hbe = as_column_vector(hb[1:m:2])\n", - "\n", - " s = np.arange(0,r*2,4)\n", - "\n", - " Y[s,:] = _column_convolve(X[xe[tb],:],hao)\n", - " Y[s+1,:] = _column_convolve(X[xe[ta],:],hbo)\n", - " Y[s+2,:] = _column_convolve(X[xe[tb],:],hae)\n", - " Y[s+3,:] = _column_convolve(X[xe[ta],:],hbe)\n", - "\n", - " return Y" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def colfilter(X, h):\n", - " \"\"\"Filter the columns of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [1 0].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m // 2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC')\n", - " r, c = X.get_shape().as_list()[1:3]\n", - "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [-1, 1, 1, 1])\n", - " X = tf.expand_dims(X, axis=-1)\n", - "\n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r, c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1, r, c])\n", - "\n", - "\n", - "def rowfilter(X, h):\n", - " \"\"\"Filter the rows of image *X* using filter vector *h*, without decimation.\n", - " If len(h) is odd, each output sample is aligned with each input sample\n", - " and *Y* is the same size as *X*. If len(h) is even, each output sample is\n", - " aligned with the mid point of each pair of input samples, and Y.shape =\n", - " X.shape + [0 1].\n", - " :param X: an image whose columns are to be filtered\n", - " :param h: the filter coefficients.\n", - " :returns Y: the filtered image.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " m = h.get_shape().as_list()[0]\n", - " m2 = m // 2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns)\n", - " X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC')\n", - " r, c = X.get_shape().as_list()[1:3]\n", - "\n", - " # X currently has shape [batch, rows, cols]\n", - " # h currently has shape [f_rows]\n", - " # For conv2d to work, X needs to be in shape [batch, rows, cols, in_channels]\n", - " # and h needs to be in shape [f_rows, f_cols, in_channels, out_channels]\n", - " h = tf.reshape(h, [1, -1, 1, 1])\n", - " X = tf.expand_dims(X, axis=-1)\n", - "\n", - " y = tf.nn.conv2d(X, h, strides=[1, 1, 1, 1], padding='VALID')\n", - " r, c = y.get_shape().as_list()[1:3]\n", - " # Drop the last dimension\n", - " return tf.reshape(y, [-1, r, c])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def coldfilt(X, ha, hb, a_first=True):\n", - " \"\"\"Filter the columns of image X using the two filters ha and hb =\n", - " reverse(ha). \n", - " ha operates on the odd samples of X and hb on the even samples. \n", - " Both filters should be even length, and h should be approx linear\n", - " phase with a quarter sample (i.e. an :math:`e^{j \\pi/4}`) advance from \n", - " its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`).\n", - " .. code-block:: text\n", - " ext top edge bottom edge ext\n", - " Level 1: ! | ! | !\n", - " odd filt on . b b b b a a a a a a a a b b b b\n", - " odd filt on . a a a a b b b b b b b b a a a a\n", - " Level 2: ! | ! | !\n", - " +q filt on x b b a a a a b b\n", - " -q filt on o a a b b b b a a\n", - " The output is decimated by two from the input sample rate and the results\n", - " from the two filters, Ya and Yb, are interleaved to give Y. \n", - " Symmetric extension with repeated end samples is used on the composite X columns\n", - " before each filter is applied.\n", - " Raises ValueError if the number of rows in X is not a multiple of 4, the\n", - " length of ha does not match hb or the lengths of ha or hb are non-even.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " r, c = X.get_shape().as_list()[1:]\n", - " r2 = r // 2\n", - " if r % 4 != 0:\n", - " raise ValueError('No. of rows in X must be a multiple of 4')\n", - "\n", - " if ha.shape != hb.shape:\n", - " raise ValueError('Shapes of ha and hb must be the same')\n", - "\n", - " if ha.get_shape().as_list()[0] % 2 != 0:\n", - " raise ValueError('Lengths of ha and hb must be even')\n", - "\n", - " m = ha.get_shape().as_list()[0]\n", - " m2 = m // 2\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the columns).\n", - " # SYMMETRIC extension means the edge sample is repeated twice, whereas\n", - " # REFLECT only has the edge sample once \n", - " X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') \n", - " '''\n", - " # Perform filtering on columns of extended matrix X current shape: [Batch, r+2*m, c]\n", - " # We split X into 4 polyphase representations, and apply ha to the odd phases and apply hb to the even phases.\n", - " # These will each be of size [Batch, r/4 + m/2 - 1, c, 1] \n", - " phase1 = tf.expand_dims(X[:,2:r+2*m-2:4,:], axis=-1)\n", - " phase2 = tf.expand_dims(X[:,3:r+2*m-2:4,:], axis=-1)\n", - " phase3 = tf.expand_dims(X[:,4:r+2*m-2:4,:], axis=-1)\n", - " phase4 = tf.expand_dims(X[:,5:r+2*m-2:4,:], axis=-1)\n", - " \n", - " # To massage them into the shape needed for conv2d, we pack:\n", - " # the odd phases into X_odd of size [Batch, r/4+m/2-1, c, 2] and \n", - " # the even phases into X_even of size [Batch, r/4+m/2-1, c, 2] and\n", - " # then apply convolution, using 'valid' padding\n", - " \n", - " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", - " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", - " hao = tf.reshape(ha[0:m:2], [-1, 1, 1, 1])\n", - " hae = tf.reshape(ha[1:m:2], [-1, 1, 1, 1])\n", - " hbo = tf.reshape(hb[0:m:2], [-1, 1, 1, 1])\n", - " hbe = tf.reshape(hb[1:m:2], [-1, 1, 1, 1]) \n", - " print(hao.shape)\n", - " print(X.shape)\n", - " a_rows = tf.nn.conv2d(phase3, hae, strides=[1,1,1,1], padding='VALID') + \\\n", - " tf.nn.conv2d(phase1, hao, strides=[1,1,1,1], padding='VALID')\n", - " b_rows = tf.nn.conv2d(phase2, hae, strides=[1,1,1,1], padding='VALID') + \\\n", - " tf.nn.conv2d(phase4, hao, strides=[1,1,1,1], padding='VALID')\n", - " return a_rows, b_rows\n", - " \"\"\"\n", - " '''\n", - " X_odd = tf.expand_dims(X[:,2:r+2*m-2:2,:], axis=-1)\n", - " X_even =tf.expand_dims(X[:,3:r+2*m-2:2,:], axis=-1)\n", - " ha = tf.reshape(ha, [m,1,1,1])\n", - " hb = tf.reshape(hb, [m,1,1,1])\n", - " a_rows = tf.nn.conv2d(X_odd, ha, strides=[1,2,1,1], padding='VALID')\n", - " b_rows = tf.nn.conv2d(X_even, hb, strides=[1,2,1,1], padding='VALID')\n", - " \n", - " # We interleave the two results into a tensor of size [Batch, r/2, c]\n", - " # Concat a_rows and b_rows (both of shape [Batch, r/4, c, 1]) \n", - " Y = tf.cond(tf.reduce_sum(ha*hb) > 0,\n", - " lambda: tf.concat([a_rows,b_rows],axis=-1),\n", - " lambda: tf.concat([b_rows,a_rows],axis=-1))\n", - " '''\n", - " if a_first:\n", - " Y = tf.concat([a_rows,b_rows],axis=-1)\n", - " else:\n", - " Y = tf.concat([b_rows,a_rows],axis=-1)\n", - " '''\n", - " \n", - " # Permute result to be shape [Batch, r/4, 2, c]\n", - " Y = tf.transpose(Y, perm=[0,1,3,2])\n", - " \n", - " # Reshape result to be shape [Batch, r/2, c]. This reshaping interleaves\n", - " # the columns\n", - " Y = tf.reshape(Y, [-1, r2, c]) \n", - " \n", - " return Y\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def rowdfilt(X, ha, hb, a_first=True):\n", - " \"\"\"Filter the rows of image X using the two filters ha and hb =\n", - " reverse(ha). ha operates on the odd samples of X and hb on the even\n", - " samples. Both filters should be even length, and h should be approx linear\n", - " phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| >\n", - " |h(m/2 + 1)|`).\n", - " .. code-block:: text\n", - " ext top edge bottom edge ext\n", - " Level 1: ! | ! | !\n", - " odd filt on . b b b b a a a a a a a a b b b b\n", - " odd filt on . a a a a b b b b b b b b a a a a\n", - " Level 2: ! | ! | !\n", - " +q filt on x b b a a a a b b\n", - " -q filt on o a a b b b b a a\n", - " The output is decimated by two from the input sample rate and the results\n", - " from the two filters, Ya and Yb, are interleaved to give Y. Symmetric\n", - " extension with repeated end samples is used on the composite X rows\n", - " before each filter is applied.\n", - " Raises ValueError if the number of columns in X is not a multiple of 4, the\n", - " length of ha does not match hb or the lengths of ha or hb are non-even.\n", - " .. codeauthor:: Rich Wareham , August 2013\n", - " .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000\n", - " .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000\n", - " \"\"\"\n", - "\n", - " r, c = X.get_shape().as_list()[1:]\n", - " c2 = c // 2\n", - " if c % 4 != 0:\n", - " raise ValueError('No. of rows in X must be a multiple of 4')\n", - "\n", - " if ha.shape != hb.shape:\n", - " raise ValueError('Shapes of ha and hb must be the same')\n", - "\n", - " if ha.get_shape().as_list()[0] % 2 != 0:\n", - " raise ValueError('Lengths of ha and hb must be even')\n", - "\n", - " m = ha.get_shape().as_list()[0]\n", - "\n", - " # Symmetrically extend with repeat of end samples.\n", - " # Pad only the second dimension of the tensor X (the rows).\n", - " # SYMMETRIC extension means the edge sample is repeated twice, whereas\n", - " # REFLECT only has the edge sample once\n", - " \n", - " X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC')\n", - " '''\n", - " # Perform filtering on columns of extended matrix X current shape: [Batch, r, c+2*m]\n", - " # We split X into 4 polyphase representations, and apply ha to the odd phases and apply hb to the even phases.\n", - " # These will each be of size [Batch, r, c/4 + m/2 - 1, 1]\n", - " t = np.arange(5, c+2*m-2, 4, dtype=np.int32)\n", - " phase1 = tf.expand_dims(X[:,:,t-3], axis=-1)\n", - " phase2 = tf.expand_dims(X[:,:,t-2], axis=-1)\n", - " phase3 = tf.expand_dims(X[:,:,t-1], axis=-1)\n", - " phase4 = tf.expand_dims(X[:,:,t], axis=-1)\n", - " \n", - " # To massage them into the shape needed for conv2d, we pack:\n", - " # the odd phases into X_odd of size [Batch, r, c/4 + m/2 -1, 2] and \n", - " # the even phases into X_even of size [Batch, r, c/4 + m/2 -1, 2] and\n", - " # then apply convolution, using 'valid' padding\n", - " \n", - " # Select odd and even samples from ha and hb. Note that due to 0-indexing\n", - " # 'odd' and 'even' are not perhaps what you might expect them to be.\n", - " hao = tf.reshape(ha[0:m:2], [1, -1, 1, 1])\n", - " hae = tf.reshape(ha[1:m:2], [1, -1, 1, 1])\n", - " hbo = tf.reshape(hb[0:m:2], [1, -1, 1, 1])\n", - " hbe = tf.reshape(hb[1:m:2], [1, -1, 1, 1]) \n", - " a_cols = tf.nn.conv2d(phase1, hae, strides=[1,1,1,1], padding='VALID') + \\\n", - " tf.nn.conv2d(phase3, hao, strides=[1,1,1,1], padding='VALID')\n", - " b_cols = tf.nn.conv2d(phase2, hae, strides=[1,1,1,1], padding='VALID') + \\\n", - " tf.nn.conv2d(phase4, hao, strides=[1,1,1,1], padding='VALID')\n", - " \n", - " \"\"\"\n", - " # Could also try:\n", - " \n", - " odd_phases = tf.concat([phase3, phase1], axis=-1)\n", - " even_phases = tf.concat([phase4, phase2], axis=-1)\n", - " ha_split = tf.reshape(ha, [1,m2,2,1])\n", - " hb_split = tf.reshape(hb, [1,m2,2,1])\n", - " a_cols = tf.nn.conv2d(odd_phases, ha_split, strides=[1,1,1,1], padding='VALID')\n", - " b_cols = tf.nn.conv2d(even_phases, hb_split, strides=[1,1,1,1], padding='VALID') \n", - " \"\"\"\n", - " '''\n", - " X_odd = tf.expand_dims(X[:,:,2:c+2*m-2:2], axis=-1)\n", - " X_even =tf.expand_dims(X[:,:,3:c+2*m-2:2], axis=-1)\n", - " ha = tf.reshape(ha, [1,m,1,1])\n", - " hb = tf.reshape(hb, [1,m,1,1])\n", - " a_cols = tf.nn.conv2d(X_odd, ha, strides=[1,1,2,1], padding='VALID')\n", - " b_cols = tf.nn.conv2d(X_even, hb, strides=[1,1,2,1], padding='VALID')\n", - " \n", - " # We interleave the two results into a tensor of size [Batch, r/2, c]\n", - " # Concat a_cols and b_cols (both of shape [Batch, r, c/4, 1]) \n", - " Y = tf.cond(tf.reduce_sum(ha*hb) > 0,\n", - " lambda: tf.concat([a_cols,b_cols],axis=-1),\n", - " lambda: tf.concat([b_cols,a_cols],axis=-1))\n", - " '''\n", - " if a_first:\n", - " Y = tf.concat([a_cols,b_cols],axis=-1)\n", - " else:\n", - " Y = tf.concat([b_cols,a_cols],axis=-1)\n", - " ''' \n", - " # Reshape result to be shape [Batch, r, c/2]. This reshaping interleaves\n", - " # the columns\n", - " Y = tf.reshape(Y, [-1, r, c2]) \n", - " \n", - " return Y" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "tf", - "language": "python", - "name": "tf" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - }, - "toc": { - "colors": { - "hover_highlight": "#DAA520", - "running_highlight": "#FF0000", - "selected_highlight": "#FFD700" - }, - "moveMenuLeft": true, - "nav_menu": { - "height": "12px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 0c6c7873b75a97a420d2715b5eb6196dd0ba1d53 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 16:50:58 +0000 Subject: [PATCH 12/52] Removed time tester notebook from repo --- time tester.ipynb | 171 ---------------------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 time tester.ipynb diff --git a/time tester.ipynb b/time tester.ipynb deleted file mode 100644 index 902efba..0000000 --- a/time tester.ipynb +++ /dev/null @@ -1,171 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# Notebook to compare the timings between the numpy implementation and\n", - "# the tensorflow implementation of the dtcwt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# colf = numpy implementation\n", - "# colfilter = tf implementation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# First test a simple convolution\n", - "f = Transform2d()\n", - "h1o = tf.constant(f.qshift[0][::-1], dtype=tf.float32)\n", - "in_ = tf.placeholder(tf.float32, shape=[None, 512, 512])\n", - "im_hat = colf(im, f.qshift[0].astype('float32'))\n", - "y1 = colfilter(in_, h1o)\n", - "y2 = rowfilter(in_,h1o)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "import time\n", - "h = f.qshift[0].astype('float32')\n", - "time1 = time.time()\n", - "for i in range(1000):\n", - " colf(im, h)\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "batch = np.stack([im]*100,axis=0)\n", - "\n", - "time1 = time.time()\n", - "for i in range(10):\n", - " b = sess.run(y1, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# On an M60 tesla, these were about the same time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "# Now compare when we have to do multiple convolutions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "time1 = time.time()\n", - "for i in range(1000):\n", - " colf(colf(colf(im, h),h),h)\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "y1 = colfilter(colfilter(colfilter(in_, h1o),h1o),h1o)\n", - "time1 = time.time()\n", - "for i in range(10):\n", - " b = sess.run(y1, feed_dict={in_:batch})\n", - "time2 = time.time()\n", - "print('Took {:3f} ms'.format((time2-time1)*1000.0))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "deconv_tf_vis", - "language": "python", - "name": "deconv_tf_vis" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 9baf1ffdfafbb30eebc153c98688df6331bd4e24 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 17:22:15 +0000 Subject: [PATCH 13/52] Added more tests to rowfilter/colfilter funcs --- tests/test_tfcolfilter.py | 26 +++++++++++++++++++++++--- tests/test_tfrowfilter.py | 24 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index 8e9cc76..a2704bc 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -43,14 +43,14 @@ def test_biort(): assert y_op.get_shape()[1:] == mandrill.shape def test_even_size(): - zero_t = tf.zeros([1, *mandrill.shape], tf.float32) + zero_t = tf.zeros([1, mandrill.shape[0], mandrill.shape[1]], tf.float32) y_op = colfilter(zero_t, [-1,1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) with tf.Session() as sess: y = sess.run(y_op) assert not np.any(y[:] != 0.0) -def test_equal_numpy_biort(): +def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill, h) y_op = colfilter(mandrill_t, h) @@ -58,7 +58,17 @@ def test_equal_numpy_biort(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) -def test_equal_numpy_qshift(): +def test_equal_numpy_biort2(): + h = biort('near_sym_b')[0] + im = mandrill[52:407, 30:401] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im, h) + y_op = colfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] ref = np_colfilter(mandrill, h) y_op = colfilter(mandrill_t, h) @@ -66,4 +76,14 @@ def test_equal_numpy_qshift(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +def test_equal_numpy_qshift2(): + h = qshift('qshift_c')[0] + im = mandrill[52:407, 30:401] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im, h) + y_op = colfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + # vim:sw=4:sts=4:et diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index 464dd13..81b50cc 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -51,7 +51,7 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) -def test_equal_numpy_biort(): +def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill.T, h).T y_op = rowfilter(mandrill_t, h) @@ -59,7 +59,17 @@ def test_equal_numpy_biort(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) -def test_equal_numpy_qshift(): +def test_equal_numpy_biort2(): + h = biort('near_sym_b')[0] + im = mandrill[15:307, 40:267] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im.T, h).T + y_op = rowfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] ref = np_colfilter(mandrill.T, h).T y_op = rowfilter(mandrill_t, h) @@ -67,6 +77,14 @@ def test_equal_numpy_qshift(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) - +def test_equal_numpy_qshift2(): + h = qshift('qshift_c')[0] + im = mandrill[15:307, 40:267] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im.T, h).T + y_op = rowfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) # vim:sw=4:sts=4:et From 9c914f60c0f8a1f7d98f133af0806b0dbdfcb015 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 17:35:40 +0000 Subject: [PATCH 14/52] Added tests for small inputs for tf filters Currently have to skip these tests as tensorflow can't pad more than half of the width of an input. --- tests/test_tfcoldfilt.py | 25 ++++++++++++++++++++++++- tests/test_tfcolfilter.py | 11 +++++++++++ tests/test_tfrowdfilt.py | 25 ++++++++++++++++++++++++- tests/test_tfrowfilter.py | 11 +++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py index b0f8cdc..b280a65 100644 --- a/tests/test_tfcoldfilt.py +++ b/tests/test_tfcoldfilt.py @@ -48,7 +48,19 @@ def test_output_size(): y_op = coldfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0]/2, mandrill.shape[1]) -def test_equal_numpy_qshift(): +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +def test_equal_small_in(): + ha = qshift('qshift_b')[0] + hb = qshift('qshift_b')[1] + im = mandrill[0:4,0:4] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_coldfilt(im, ha, hb) + y_op = coldfilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] ref = np_coldfilt(mandrill, ha, hb) @@ -57,4 +69,15 @@ def test_equal_numpy_qshift(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +def test_equal_numpy_qshift2(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + im = mandrill[:508, :502] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_coldfilt(im, ha, hb) + y_op = coldfilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + # vim:sw=4:sts=4:et diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index a2704bc..96564f7 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -50,6 +50,17 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +def test_equal_small_in(): + h = qshift('qshift_b')[0] + im = mandrill[0:4,0:4] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im, h) + y_op = colfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill, h) diff --git a/tests/test_tfrowdfilt.py b/tests/test_tfrowdfilt.py index 896f106..9e90649 100644 --- a/tests/test_tfrowdfilt.py +++ b/tests/test_tfrowdfilt.py @@ -48,7 +48,19 @@ def test_output_size(): y_op = rowdfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0], mandrill.shape[1]/2) -def test_equal_numpy_qshift(): +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +def test_equal_small_in(): + ha = qshift('qshift_b')[0] + hb = qshift('qshift_b')[1] + im = mandrill[0:4,0:4] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_coldfilt(im.T, ha, hb).T + y_op = rowdfilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] ref = np_coldfilt(mandrill.T, ha, hb).T @@ -57,4 +69,15 @@ def test_equal_numpy_qshift(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +def test_equal_numpy_qshift2(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + im = mandrill[:508, :504] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_coldfilt(im.T, ha, hb).T + y_op = rowdfilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + # vim:sw=4:sts=4:et diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index 81b50cc..1bf28c6 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -51,6 +51,17 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +def test_equal_small_in(): + h = qshift('qshift_b')[0] + im = mandrill[0:4,0:4] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colfilter(im.T, h).T + y_op = rowfilter(im_t, h) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill.T, h).T From b6d6dc7e65f467f42b65a461b759f73cd6f08fe5 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 3 Mar 2017 17:48:44 +0000 Subject: [PATCH 15/52] Fixed bug in transform2d in padding signals In between qshift filtering, if the rows or cols weren't a multiple of 4, I was adding the end row/end col twice, when the numpy implementation adds the first row & end row/first row & end col. --- dtcwt/tf/transform2d.py | 13 ++++++++----- tests/test_tfTransform2d.py | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 7c8f22f..c5f892a 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -279,7 +279,7 @@ def _create_graph_ops(self, X, nlevels=3, include_scale=False): # Do odd top-level filters on rows. LoLo = rowfilter(Lo, h0o) - LoLo_shape = LoLo.get_shape().as_list()[1:3] + LoLo_shape = LoLo.get_shape().as_list()[1:] # Horizontal wavelet pair (15 & 165 degrees) horiz = q2c(rowfilter(Hi, h0o)) @@ -309,14 +309,17 @@ def _create_graph_ops(self, X, nlevels=3, include_scale=False): # If the row count of LoLo is not divisible by 4 (it will be # divisible by 2), add 2 extra rows to make it so if row_size % 4 != 0: - bottom_row = tf.slice(LoLo, [0, row_size - 2, 0], [-1, 2, -1]) - LoLo = tf.concat([LoLo, bottom_row], axis=1) + LoLo = tf.pad(LoLo, [[0, 0], [1, 1], [0, 0]], 'SYMMETRIC') + #top_row = tf.slice(LoLo, [0, 0, 0], [-1, 1, -1]) + #bottom_row = tf.slice(LoLo, [0, row_size - 1, 0], [-1, 1, -1]) + #LoLo = tf.concat([top_row, LoLoLoLo, bottom_row], axis=1) # If the col count of LoLo is not divisible by 4 (it will be # divisible by 2), add 2 extra cols to make it so if col_size % 4 != 0: - right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2]) - LoLo = tf.concat([LoLo, right_col], axis=2) + LoLo = tf.pad(LoLo, [[0, 0], [0, 0], [1, 1]], 'SYMMETRIC') + #right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2]) + #LoLo = tf.concat([LoLo, right_col], axis=2) # Do even Qshift filters on cols. Lo = coldfilt(LoLo, h0b, h0a) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 832fd18..753e99b 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -135,10 +135,10 @@ def test_results_match1(): def test_results_match2(): im = mandrill[100:400,50:450] f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') - p_np = f_np.forward(im, nlevels=3, include_scale=True) + p_np = f_np.forward(im, nlevels=4, include_scale=True) f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') - p_tf = f_tf.forward(im, nlevels=3, include_scale=True) + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) np.testing.assert_array_almost_equal( p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) From 18a543d10fe0b16df002482801a896a88ce4a5cf Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Sat, 4 Mar 2017 02:11:00 +0000 Subject: [PATCH 16/52] Implemented colifilt --- dtcwt/tf/lowlevel.py | 120 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index 11568c2..2c94279 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -54,6 +54,36 @@ def _conv_2d(X, h, strides=[1,1,1,1]): return Y +def _conv_2d_transpose(X, h, out_shape, strides=[1,1,1,1]): + """Perform 2d convolution in tensorflow. X will to be manipulated to be of + shape [batch, height, width, ch], and h to be of shape + [height, width, ch, num]. This function does the necessary reshaping before + calling the conv2d function, and does the reshaping on the output, returning + Y of shape [batch, height, width]""" + + # Check the shape of X is what we expect + if len(X.shape) != 3: + raise ValueError('X needs to be of shape [batch, height, width] for conv_2d') + # Check the shape of h is what we expect + if len(h.shape) != 2: + raise ValueError('Filter inputs must only have height and width for conv_2d') + + # Add in the unit dimensions for conv + X = tf.expand_dims(X, axis=-1) + h = tf.expand_dims(tf.expand_dims(h, axis=-1),axis=-1) + + # Have to reverse h as tensorflow 2d conv is actually cross-correlation + h = tf.reverse(h, axis=[0,1]) + # Transpose h as we will be using the transpose convolution + h = tf.transpose(h, perm=[1, 0, 2, 3]) + + Y = tf.nn.conv2d(X, h, output_shape=out_shape, strides=strides, padding='VALID') + + # Remove the final dimension, returning a result of shape [batch, height, width] + Y = tf.squeeze(Y, axis=-1) + + return Y + def colfilter(X, h): """Filter the columns of image *X* using filter vector *h*, without decimation. If len(h) is odd, each output sample is aligned with each input sample @@ -243,3 +273,93 @@ def rowdfilt(X, ha, hb): return Y + +def colifilt(X, ha, hb): + """ Filter the columns of image X using the two filters ha and hb = + reverse(ha). ha operates on the odd samples of X and hb on the even + samples. Both filters should be even length, and h should be approx linear + phase with a quarter sample advance from its mid pt (i.e `:math:`|h(m/2)| > + |h(m/2 + 1)|`). + + .. code-block:: text + + ext left edge right edge ext + Level 2: ! | ! | ! + +q filt on x b b a a a a b b + -q filt on o a a b b b b a a + Level 1: ! | ! | ! + odd filt on . b b b b a a a a a a a a b b b b + odd filt on . a a a a b b b b b b b b a a a a + + The output is interpolated by two from the input sample rate and the + results from the two filters, Ya and Yb, are interleaved to give Y. + Symmetric extension with repeated end samples is used on the composite X + columns before each filter is applied. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , August 2013 + .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 + .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 + """ + + + r, c = X.get_shape().as_list()[1:] + r2 = r // 2 + if r % 2 != 0: + raise ValueError('No. of rows in X must be a multiple of 2') + + ha_t = _as_col_tensor(ha) + hb_t = _as_col_tensor(hb) + if ha_t.shape != hb_t.shape: + raise ValueError('Shapes of ha and hb must be the same') + + m = ha_t.get_shape().as_list()[0] + m2 = m // 2 + if ha.shape[0] % 2 != 0: + raise ValueError('Lengths of ha and hb must be even') + + X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') + + ha_odd_t = ha_t[::2,:] + ha_even_t = ha_t[1::2,:] + hb_odd_t = hb_t[::2,:] + hb_even_t = hb_t[1::2,:] + + if m2 % 2 == 0: + # m/2 is even, so set up t to start on d samples. + # Set up vector for symmetric extension of X with repeated end samples. + + # Take the odd and even columns of X + X1,X2 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, + lambda: (X[:,1:r+m-2:2,:], X[:,0:r+m-3:2,:]), + lambda: (X[:,0:r+m-3:2,:], X[:,1:r+m-2:2,:])) + X3,X4 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, + lambda: (X[:,3:r+m:2,:], X[:,2:r+m-1:2,:]), + lambda: (X[:,2:r+m-1:2,:], X[:,3:r+m:2,:])) + + y1 = _conv_2d(X2, ha_even_t) + y2 = _conv_2d(X1, hb_even_t) + y3 = _conv_2d(X4, ha_odd_t) + y4 = _conv_2d(X3, hb_odd_t) + + else: + # m/2 is even, so set up t to start on d samples. + # Set up vector for symmetric extension of X with repeated end samples. + + # Take the odd and even columns of X + X1,X2 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, + lambda: (X[:,2:r+m-1:2,:], X[:,1:r+m-2:2,:]), + lambda: (X[:,1:r+m-2:2,:], X[:,2:r+m-1:2,:])) + + y1 = _conv_2d(X2, ha_odd_t) + y2 = _conv_2d(X1, hb_odd_t) + y3 = _conv_2d(X2, ha_even_t) + y4 = _conv_2d(X1, hb_even_t) + + # Stack 4 tensors of shape [batch, r2, c] into one tensor [batch, r2, 4, c] + Y = tf.stack([y1,y2,y3,y4], axis=2) + + # Reshape to be [batch, 2*4, c]. This interleaves the rows + Y = tf.reshape(Y, [-1,2*r,c]) + + return Y From c9a2a8273867bec510035ffaf133f6ee4243d82b Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Sun, 5 Mar 2017 02:43:05 +0000 Subject: [PATCH 17/52] Added tests and functionality for colifilt --- dtcwt/tf/lowlevel.py | 2 +- tests/test_tfcolifilt.py | 71 +++++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index 2c94279..ee6dfc9 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -315,7 +315,7 @@ def colifilt(X, ha, hb): m = ha_t.get_shape().as_list()[0] m2 = m // 2 - if ha.shape[0] % 2 != 0: + if ha_t.get_shape().as_list()[0] % 2 != 0: raise ValueError('Lengths of ha and hb must be even') X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') diff --git a/tests/test_tfcolifilt.py b/tests/test_tfcolifilt.py index 0b7ab27..e13e9b5 100644 --- a/tests/test_tfcolifilt.py +++ b/tests/test_tfcolifilt.py @@ -5,55 +5,94 @@ pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") import numpy as np -from dtcwt.numpy.lowlevel import colifilt +import tensorflow as tf +from dtcwt.tf.lowlevel import colifilt +from dtcwt.coeffs import qshift +from dtcwt.numpy.lowlevel import colifilt as np_colifilt from pytest import raises import tests.datasets as datasets def setup(): - global mandrill + global mandrill, mandrill_t mandrill = datasets.mandrill() + mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 assert mandrill.max() <= 1 assert mandrill.dtype == np.float32 + assert mandrill_t.get_shape() == (1, 512, 512) def test_odd_filter(): with raises(ValueError): - colifilt(mandrill, (-1,2,-1), (-1,2,1)) + colifilt(mandrill_t, (-1,2,-1), (-1,2,1)) def test_different_size_h(): with raises(ValueError): - colifilt(mandrill, (-1,2,1), (-0.5,-1,2,-1,0.5)) + colifilt(mandrill_t, (-1,2,1), (-0.5,-1,2,-1,0.5)) def test_zero_input(): - Y = colifilt(np.zeros_like(mandrill), (-1,1), (1,-1)) - assert np.all(Y[:0] == 0) + Y = colifilt(mandrill_t, (-1,1), (1,-1)) + with tf.Session() as sess: + y = sess.run(Y, {mandrill_t : [np.zeros_like(mandrill)]})[0] + assert np.all(y[:0] == 0) def test_bad_input_size(): with raises(ValueError): - colifilt(mandrill[:511,:], (-1,1), (1,-1)) + colifilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) def test_good_input_size(): - colifilt(mandrill[:,:511], (-1,1), (1,-1)) + colifilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) def test_output_size(): - Y = colifilt(mandrill, (-1,1), (1,-1)) - assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + Y = colifilt(mandrill_t, (-1,1), (1,-1)) + assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) def test_non_orthogonal_input(): - Y = colifilt(mandrill, (1,1), (1,1)) - assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + Y = colifilt(mandrill_t, (1,1), (1,1)) + assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) def test_output_size_non_mult_4(): - Y = colifilt(mandrill, (-1,0,0,1), (1,0,0,-1)) - assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + Y = colifilt(mandrill_t, (-1,0,0,1), (1,0,0,-1)) + assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) def test_non_orthogonal_input_non_mult_4(): - Y = colifilt(mandrill, (1,0,0,1), (1,0,0,1)) - assert Y.shape == (mandrill.shape[0]*2, mandrill.shape[1]) + Y = colifilt(mandrill_t, (1,0,0,1), (1,0,0,1)) + assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) + +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +def test_equal_small_in(): + ha = qshift('qshift_b')[0] + hb = qshift('qshift_b')[1] + im = mandrill[0:4,0:4] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_coldfilt(im, ha, hb) + y_op = coldfilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift1(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + ref = np_colifilt(mandrill, ha, hb) + y_op = colifilt(mandrill_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + +def test_equal_numpy_qshift2(): + ha = qshift('qshift_c')[0] + hb = qshift('qshift_c')[1] + im = mandrill[:508, :502] + im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) + ref = np_colifilt(im, ha, hb) + y_op = colifilt(im_t, ha, hb) + with tf.Session() as sess: + y = sess.run(y_op) + np.testing.assert_array_almost_equal(y[0], ref, decimal=4) # vim:sw=4:sts=4:et From 8f2e6893f9d8a3e053f0f11535d1fff28a8401aa Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 6 Mar 2017 00:45:03 +0000 Subject: [PATCH 18/52] Wrote code and tests for inverse transform Not yet passing the tests however --- dtcwt/tf/__init__.py | 2 +- dtcwt/tf/common.py | 119 +++++++++---- dtcwt/tf/transform2d.py | 345 +++++++++++++++++++++++++++--------- tests/test_tfTransform2d.py | 12 +- 4 files changed, 363 insertions(+), 115 deletions(-) diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index c7958f2..f60203e 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -5,7 +5,7 @@ """ from .common import Pyramid_tf -from .transform2d import Transform2d, dtwavexfm2 +from .transform2d import Transform2d, dtwavexfm2, dtwaveifm2 __all__ = [ 'Pyramid', diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 0c30450..7cf22eb 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -1,7 +1,14 @@ from __future__ import absolute_import -from dtcwt.numpy import Pyramid +import numpy as np import tensorflow as tf +import logging + +from dtcwt.coeffs import biort as _biort, qshift as _qshift +from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT +from dtcwt.utils import asfarray + +from dtcwt.numpy import Pyramid as Pyramid_np class Pyramid_tf(object): """A representation of a transform domain signal. @@ -9,37 +16,89 @@ class Pyramid_tf(object): storing transform-domain signals. The inverse transform may accept a backend-specific version of this class but should always accept any class which corresponds to this interface. - .. py:attribute:: lowpass - A NumPy-compatible array containing the coarsest scale lowpass signal. - .. py:attribute:: highpasses - A tuple where each element is the complex subband coefficients for - corresponding scales finest to coarsest. + + .. py:attribute:: X + A placeholder which the user can use when they want to evaluate the + forward dtcwt. + .. py:attribute:: lowpass_op + A tensorflow tensor that can be evaluated in a session to return + the coarsest scale lowpass signal for the input, X. + .. py:attribute:: highpasses_op + A tuple of tensorflow tensors, where each element is the complex + subband coefficients for corresponding scales finest to coarsest. .. py:attribute:: scales - *(optional)* A tuple where each element is a NumPy-compatible array + *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. """ - def __init__(self, lowpass, highpasses, scales=None): - self.lowpass = lowpass - self.highpasses = highpasses - self.scales = scales + def __init__(self, X, lowpass, highpasses, scales=None, + graph=tf.get_default_graph()): + self.X = X + self.lowpass_op = lowpass + self.highpasses_ops = highpasses + self.scales_ops = scales + self.graph = graph + + def _get_lowpass(self, data): + if self.lowpass_op is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = sess.run(self.lowpass_op, {self.X : data}) + except ValueError: + y = sess.run(self.lowpass_op, {self.X : [data]})[0] + return y - def eval(self, sess, placeholder, data): - try: - lo = sess.run(self.lowpass, {placeholder : data}) - hi = sess.run(self.highpasses, {placeholder : data}) - if self.scales is not None: - scales = sess.run(self.scales, {placeholder : data}) - else: - scales = None - except ValueError: - lo = sess.run(self.lowpass, {placeholder : [data]}) - hi = sess.run(self.highpasses, {placeholder : [data]}) - if self.scales is not None: - scales = sess.run(self.scales, {placeholder : [data]}) - else: - scales = None - - - return Pyramid(lo, hi, scales) - + def _get_highpasses(self, data): + if self.highpasses_ops is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = tuple( + [sess.run(layer_hp, {self.X : data}) + for layer_hp in self.highpasses_ops]) + except ValueError: + y = tuple( + [sess.run(layer_hp, {self.X : [data]})[0] + for layer_hp in self.highpasses_ops]) + return y + + def _get_scales(self, data): + if self.scales_ops is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + y = tuple( + sess.run(layer_scale, {self.X : data}) + for layer_scale in self.scales_ops) + except ValueError: + y = tuple( + sess.run(layer_scale, {self.X : [data]})[0] + for layer_scale in self.scales_ops) + return y + + def _get_X(self, Yl, Yh): + if self.X is None: + return None + with tf.Session(graph=self.graph) as sess: + try: + # Use dictionary comprehension to feed in our Yl and our + # multiple layers of Yh + data = [Yl, *list(Yh)] + placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + X = sess.run(self.X, {i : d for i,d in zip(placeholders,data)}) + except ValueError: + data = [Yl, *list(Yh)] + placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)}) + return X + + def eval_fwd(self, X): + lo = self._get_lowpass(X) + hi = self._get_highpasses(X) + scales = self._get_scales(X) + return Pyramid_np(lo, hi, scales) + + def eval_inv(self, Yl, Yh): + return self._get_X(Yl, Yh) + diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index c5f892a..f91e794 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -11,10 +11,11 @@ from dtcwt.utils import asfarray from dtcwt.numpy import Transform2d as Transform2dNumPy from dtcwt.numpy import Pyramid as Pyramid_np +from dtcwt.tf import Pyramid_tf from dtcwt.tf.lowlevel import * -def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False, queue=None): +def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False): t = Transform2d(biort=biort, qshift=qshift) r = t.forward(X, nlevels=nlevels, include_scale=include_scale) if include_scale: @@ -22,74 +23,10 @@ def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include else: return r.lowpass, r.highpasses - -class Pyramid_tf(object): - """A representation of a transform domain signal. - Backends are free to implement any class which respects this interface for - storing transform-domain signals. The inverse transform may accept a - backend-specific version of this class but should always accept any class - which corresponds to this interface. - .. py:attribute:: lowpass - A NumPy-compatible array containing the coarsest scale lowpass signal. - .. py:attribute:: highpasses - A tuple where each element is the complex subband coefficients for - corresponding scales finest to coarsest. - .. py:attribute:: scales - *(optional)* A tuple where each element is a NumPy-compatible array - containing the lowpass signal for corresponding scales finest to - coarsest. This is not required for the inverse and may be *None*. - """ - def __init__(self, p_holder, lowpass, highpasses, scales=None, - graph=tf.get_default_graph()): - self.lowpass_op = lowpass - self.highpasses_ops = highpasses - self.scales_ops = scales - self.p_holder = p_holder - self.graph = graph - - def _get_lowpass(self, data): - if self.lowpass_op is None: - return None - with tf.Session(graph=self.graph) as sess: - try: - y = sess.run(self.lowpass_op, {self.p_holder : data}) - except ValueError: - y = sess.run(self.lowpass_op, {self.p_holder : [data]})[0] - return y - - def _get_highpasses(self, data): - if self.highpasses_ops is None: - return None - with tf.Session(graph=self.graph) as sess: - try: - y = tuple( - [sess.run(layer_hp, {self.p_holder : data}) - for layer_hp in self.highpasses_ops]) - except ValueError: - y = tuple( - [sess.run(layer_hp, {self.p_holder : [data]})[0] - for layer_hp in self.highpasses_ops]) - return y - - def _get_scales(self, data): - if self.scales_ops is None: - return None - with tf.Session(graph=self.graph) as sess: - try: - y = tuple( - sess.run(layer_scale, {self.p_holder : data}) - for layer_scale in self.scales_ops) - except ValueError: - y = tuple( - sess.run(layer_scale, {self.p_holder : [data]})[0] - for layer_scale in self.scales_ops) - return y - - def eval(self, data): - lo = self._get_lowpass(data) - hi = self._get_highpasses(data) - scales = self._get_scales(data) - return Pyramid_np(lo, hi, scales) +def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, gain_mask=None): + t = Transform2d(biort=biort, qshift=qshift) + r = t.inverse(Pyramid_np(Yl, Yh), gain_mask=gain_mask) + return r class Transform2d(Transform2dNumPy): @@ -115,15 +52,35 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): super(Transform2d, self).__init__(biort=biort, qshift=qshift) # Use our own graph when the user calls forward with numpy arrays self.np_graph = tf.Graph() - self.pyramids = {} + self.forward_graphs = {} + self.inverse_graphs = {} - def _find_pyramid(self, shape): + def _find_forward_graph(self, shape): + ''' See if we can reuse an old graph for the forward transform ''' find_key = '{}x{}'.format(shape[0], shape[1]) - for key,val in self.pyramids.items(): + for key,val in self.forward_graphs.items(): if find_key == key: return val return None + def _add_forward_graph(self, p_ops, shape): + ''' Keep record of the pyramid so we can use it later if need be ''' + find_key = '{}x{}'.format(shape[0], shape[1]) + self.forward_graphs[find_key] = p_ops + + def _find_inverse_graph(self, Lo_shape, nlevels): + ''' See if we can reuse an old graph for the inverse transform ''' + find_key = '{}x{}'.format(Lo_shape[0], Lo_shape[1]) + for key,val in self.forward_graphs.items(): + if find_key == key: + return val + return None + + def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): + ''' Keep record of the pyramid so we can use it later if need be ''' + find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) + self.inverse_graphs[find_key] = p_ops + def forward(self, X, nlevels=3, include_scale=False): ''' Perform a forward transform on an image. Can provide the forward @@ -152,22 +109,21 @@ def forward(self, X, nlevels=3, include_scale=False): '''.format(original_size)) # Check if the ops already exist for an input of the given size - p_ops = self._find_pyramid(X.shape) + p_ops = self._find_forward_graph(X.shape) # If not, create a graph if p_ops is None: ph = tf.placeholder(tf.float32, [None, X.shape[0], X.shape[1]]) size = '{}x{}'.format(X.shape[0], X.shape[1]) - name = 'dtcwt_{}'.format(size) + name = 'dtcwt_fwd_{}'.format(size) with self.np_graph.name_scope(name): - p_ops = self._create_graph_ops(ph, nlevels, include_scale) + p_ops = self._forward_ops(ph, nlevels, include_scale) - # keep record of the pyramid so we can use it later if need be - self.pyramids[size] = p_ops + self._add_forward_graph(p_ops, X.shape) # Evaluate the graph with the given input with self.np_graph.as_default(): - return p_ops.eval(X) + return p_ops.eval_fwd(X) # A tensorflow object was provided else: @@ -181,11 +137,77 @@ def forward(self, X, nlevels=3, include_scale=False): original_size = X.get_shape().as_list()[1:] size = '{}x{}'.format(original_size[0], original_size[1]) - name = 'dtcwt_{}'.format(size) + name = 'dtcwt_fwd_{}'.format(size) with tf.name_scope(name): - return self._create_graph_ops(X, nlevels, include_scale) + return self._forward_ops(X, nlevels, include_scale) + + def inverse(self, pyramid, gain_mask=None): + ''' + Perform an inverse transform on an image. Can provide the inverse + transform with either an np array (naive usage), or a tensorflow + variable or placeholder (designed usage). + + :param pyramid: A :py:class:`dtcwt.Pyramid` or + `:py:class:`dtcwt.tf.Pyramid_tf` like class holding the transform + domain representation to invert + :param gain_mask: Gain to be applied to each subband. Should have shape + [6, nlevels]. + :returns: Either a tf.Variable or a numpy array compatible with the + reconstruction. A tf.Variable is returned if the pyramid input was + a Pyramid_tf class. If it wasn't, then, we return a numpy array (note + that this is inefficient, as in both cases we have to construct the + graph - in the second case, we then execute it and discard it). + + The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction + *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for + band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are + zero-indexed. + + ''' + + # Check if a numpy array was provided + if isinstance(pyramid, Pyramid_np) or (hasattr(pyramid, 'lowpass') + and hasattr(pyramid, 'highpasses')): + + Yl, Yh = pyramid.lowpass, pyramid.highpasses + + # Check if the ops already exist for an input of the given size + nlevels = len(Yh) + p_ops = self._find_inverse_graph(Yl.shape, nlevels) + + # If not, create a graph + if p_ops is None: + Lo_ph = tf.placeholder(tf.float32, [None, Yl.shape[0], Yl.shape[1]]) + Hi_ph = tuple( + tf.placeholder(tf.complex64, [None, *level.shape]) for + level in Yh) + p_in = Pyramid_tf(None, Lo_ph, Hi_ph) + size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) + name = 'dtcwt_inv_{}'.format(size) + + with self.np_graph.name_scope(name): + p_ops = self._inverse_ops(p_in, gain_mask) + + # keep record of the pyramid so we can use it later if need be + self.forward_graphs[size] = p_ops + + # Evaluate the graph with the given input + with self.np_graph.as_default(): + return p_ops.eval_inv(Yl, Yh) + + # A tensorflow object was provided + elif isinstance(pyramid, Pyramid_tf): + s = pyramid.lowpass_op.get_shape().as_list()[1:] + nlevels = len(pyramid.highpasses_ops) + size = '{}x{}_up_{}'.format(s[0], s[1], nlevels) + name = 'dtcwt_inv_{}'.format(size) + with tf.name_scope(name): + return self._inverse_ops(pyramid, gain_mask) + else: + raise ValueError('''Unknown pyramid provided to inverse transform''') - def _create_graph_ops(self, X, nlevels=3, include_scale=False): + + def _forward_ops(self, X, nlevels=3, include_scale=False): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [Batch, rows, cols] :param nlevels: Number of levels of wavelet decomposition @@ -380,6 +402,125 @@ def _create_graph_ops(self, X, nlevels=3, include_scale=False): return Pyramid_tf(X_in, Yl, tuple(Yh)) + def _inverse_ops(self, pyramid, gain_mask=None): + """Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D + reconstruction. + + :param pyramid: A :py:class:`dtcwt.tf.Pyramid_tf`-like class holding the + transform domain representation to invert. + :param gain_mask: Gain to be applied to each subband. + + :returns: A :py:class:`dtcwt.tf.Pyramid_tf` class which can be + evaluated to get the inverted signal, X. + + The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction + *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for + band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are + zero-indexed. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + Yl = pyramid.lowpass_op + Yh = pyramid.highpasses_ops + + a = len(Yh) # No of levels. + + if gain_mask is None: + gain_mask = np.ones((6, a)) # Default gain_mask. + + gain_mask = np.array(gain_mask) + + # If biort has 6 elements instead of 4, then it's a modified + # rotationally symmetric wavelet + # FIXME: there's probably a nicer way to do this + if len(self.biort) == 4: + h0o, g0o, h1o, g1o = self.biort + elif len(self.biort) == 6: + h0o, g0o, h1o, g1o, h2o, g2o = self.biort + else: + raise ValueError('Biort wavelet must have 6 or 4 components.') + + # If qshift has 12 elements instead of 8, then it's a modified + # rotationally symmetric wavelet + # FIXME: there's probably a nicer way to do this + if len(self.qshift) == 8: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift + elif len(self.qshift) == 12: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b, g2a, g2b = self.qshift + else: + raise ValueError('Qshift wavelet must have 12 or 8 components.') + + current_level = a + Z = Yl + + while current_level >= 2: # this ensures that for level 1 we never do the following + lh = c2q(Yh[current_level-1][:,:,:,0:6:5], gain_mask[[0, 5], current_level-1]) + hl = c2q(Yh[current_level-1][:,:,:,2:4:1], gain_mask[[2, 3], current_level-1]) + hh = c2q(Yh[current_level-1][:,:,:,1:5:3], gain_mask[[1, 4], current_level-1]) + + # Do even Qshift filters on columns. + y1 = colifilt(Z, g0b, g0a) + colifilt(lh, g1b, g1a) + + if len(self.qshift) >= 12: + y2 = colifilt(hl, g0b, g0a) + y2bp = colifilt(hh, g2b, g2a) + + # Do even Qshift filters on rows. + Z = tf.transpose(colifilt(tf.transpose(y1,perm=[0,2,1]), g0b, g0a) + + colifilt(tf.transpose(y2,perm=[0,2,1]), g1b, g1a) + + colifilt(tf.transpose(y2bp,perm=[0,2,1]), g2b, g2a), + perm=[0,2,1]) + else: + y2 = colifilt(hl, g0b, g0a) + colifilt(hh, g1b, g1a) + + # Do even Qshift filters on rows. + Z = tf.transpose(colifilt(tf.transpose(y1, perm=[0,2,1]), g0b, g0a) + + colifilt(tf.transpose(y2, perm=[0,2,1]), g1b, g1a), + perm=[0,2,1]) + + # Check size of Z and crop as required + Z_r, Z_c = Z.get_shape().as_list()[1:3] + S_r, S_c = Yh[current_level-2].get_shape().as_list()[1:3] + # check to see if this result needs to be cropped for the rows + if Z_r != S_r * 2: + Z = Z[:,1:-1,:] + # check to see if this result needs to be cropped for the cols + if Z_c != S_c*2: + Z = Z[:,:,1:-1] + + # Assert that the size matches at this stage + Z_r, Z_c = Z.get_shape().as_list()[1:3] + if Z_r != S_r * 2 or Z_c != S_c*2: + raise ValueError('Sizes of highpasses are not valid for DTWAVEIFM2') + + current_level = current_level - 1 + + if current_level == 1: + lh = c2q(Yh[current_level-1][:,:,:,0:6:5],gain_mask[[0, 5],current_level-1]) + hl = c2q(Yh[current_level-1][:,:,:,2:4:1],gain_mask[[2, 3],current_level-1]) + hh = c2q(Yh[current_level-1][:,:,:,1:5:3],gain_mask[[1, 4],current_level-1]) + + # Do odd top-level filters on columns. + y1 = colfilter(Z, g0o) + colfilter(lh, g1o) + + if len(self.biort) >= 6: + y2 = colfilter(hl, g0o) + y2bp = colfilter(hh, g2o) + + # Do odd top-level filters on rows. + Z = rowfilter(y1, g0o) + rowfilter(y2, g1o) + rowfilter(y2bp, g2o) + else: + y2 = colfilter(hl, g0o) + colfilter(hh, g1o) + + # Do odd top-level filters on rows. + Z = rowfilter(y1, g0o) + rowfilter(y2, g1o) + + return Pyramid_tf(Z, Yl, Yh) + def q2c(y): @@ -402,3 +543,45 @@ def q2c(y): # Form the 2 highpasses in z. return (p-q, p+q) + +def c2q(w, gain): + """ + Scale by gain and convert from complex w(:,:,1:2) to real quad-numbers + in z. + + Arrange pixels from the real and imag parts of the 2 highpasses + into 4 separate subimages . + A----B Re Im of w(:,:,1) + | | + | | + C----D Re Im of w(:,:,2) + + """ + + # Input has shape [batch, r, c, 2] + r,c = w.get_shape().as_list()[1:3] + + sc = np.sqrt(0.5) * gain + P = w[:,:,:,0]*sc[0] + w[:,:,:,1]*sc[1] + Q = w[:,:,:,0]*sc[0] - w[:,:,:,1]*sc[1] + + # Recover each of the 4 corners of the quads. + x1 = tf.real(P) + x2 = tf.imag(P) + x3 = tf.real(Q) + x4 = -tf.imag(Q) + + # Stack 2 inputs of shape [batch, r, c] to [batch, r, 2, c] + x_rows1 = tf.stack([x1,x3], axis=2) + # Reshaping interleaves the results + x_rows1 = tf.reshape(x_rows1, [-1, 2*r, c]) + # Do the same for the even columns + x_rows2 = tf.stack([x2,x3], axis=2) + x_rows2 = tf.reshape(x_rows2, [-1, 2*r, c]) + + # Stack the two [batch, 2*r, c] tensors to [batch, 2*r, c, 2] + x_cols = tf.stack([x_rows1, x_rows2], axis=-1) + y = tf.reshape(x_cols, [-1, 2*r, 2*c]) + + return y + diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 753e99b..9c1e114 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -7,7 +7,7 @@ import numpy as np import tensorflow as tf -from dtcwt.tf import Transform2d, dtwavexfm2 +from dtcwt.tf import Transform2d, dtwavexfm2, dtwaveifm2 from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.numpy import Pyramid from dtcwt.coeffs import biort, qshift @@ -69,7 +69,6 @@ def test_odd_rows_and_cols(): def test_odd_rows_and_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) -@pytest.mark.skip(reason='Inverse not currently implemented') def test_rot_symm_modified(): # This test only checks there is no error running these functions, not that they work Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) @@ -93,7 +92,7 @@ def test_integer_input(): Yl, Yh = dtwavexfm2([[1,2,3,4], [1,2,3,4]]) assert np.any(Yl != 0) -@pytest.mark.skip(reason='Inverse not currently implemented') +@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_perfect_recon(): # Check that an integer input is correctly coerced into a floating point # array and reconstructed @@ -102,6 +101,13 @@ def test_integer_perfect_recon(): B = dtwaveifm2(Yl, Yh) assert np.max(np.abs(A-B)) < 1e-5 +def test_mandrill_perfect_recon(): + # Check that an integer input is correctly coerced into a floating point + # array and reconstructed + Yl, Yh = dtwavexfm2(mandrill) + B = dtwaveifm2(Yl, Yh) + assert np.max(np.abs(mandrill-B)) < 1e-5 + def test_float32_input(): # Check that an float32 input is correctly output as float32 Yl, Yh = dtwavexfm2(mandrill.astype(np.float32)) From 0c4362ae1b6d82241eb58f4dae9311d15bddcc7c Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 6 Mar 2017 14:00:16 +0000 Subject: [PATCH 19/52] Fixed bug in c2q causing inverse errors --- dtcwt/tf/transform2d.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index f91e794..29ec73b 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -179,7 +179,8 @@ def inverse(self, pyramid, gain_mask=None): if p_ops is None: Lo_ph = tf.placeholder(tf.float32, [None, Yl.shape[0], Yl.shape[1]]) Hi_ph = tuple( - tf.placeholder(tf.complex64, [None, *level.shape]) for + tf.placeholder(tf.complex64, [None, level.shape[1], + level.shape[2], level.shape[3]]) for level in Yh) p_in = Pyramid_tf(None, Lo_ph, Hi_ph) size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) @@ -568,15 +569,15 @@ def c2q(w, gain): # Recover each of the 4 corners of the quads. x1 = tf.real(P) x2 = tf.imag(P) - x3 = tf.real(Q) - x4 = -tf.imag(Q) + x3 = tf.imag(Q) + x4 = -tf.real(Q) # Stack 2 inputs of shape [batch, r, c] to [batch, r, 2, c] x_rows1 = tf.stack([x1,x3], axis=2) # Reshaping interleaves the results x_rows1 = tf.reshape(x_rows1, [-1, 2*r, c]) # Do the same for the even columns - x_rows2 = tf.stack([x2,x3], axis=2) + x_rows2 = tf.stack([x2,x4], axis=2) x_rows2 = tf.reshape(x_rows2, [-1, 2*r, c]) # Stack the two [batch, 2*r, c] tensors to [batch, 2*r, c, 2] From 4e8c77b6f381df9e256b3e45dcbaeac316b1e942 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 6 Mar 2017 14:00:40 +0000 Subject: [PATCH 20/52] Improved comments for Pyramid_tf --- dtcwt/tf/common.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 7cf22eb..f1cc56f 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -11,11 +11,12 @@ from dtcwt.numpy import Pyramid as Pyramid_np class Pyramid_tf(object): - """A representation of a transform domain signal. + """A tensorflow representation of a transform domain signal. Backends are free to implement any class which respects this interface for - storing transform-domain signals. The inverse transform may accept a - backend-specific version of this class but should always accept any class - which corresponds to this interface. + storing transform-domain signals, so long as the attributes have the + correct names and are tensorflow tensors (or placeholders). + The inverse transform may accept a backend-specific version of this class + but should always accept any class which corresponds to this interface. .. py:attribute:: X A placeholder which the user can use when they want to evaluate the @@ -30,6 +31,14 @@ class Pyramid_tf(object): *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. + .. py:method:: eval_fwd(X) + A helper method to evaluate the forward transform, feeding *X* as input + to the tensorflow session. Assumes that the object was returned from + the Transform2d().forward() method. + .. py:method:: eval_inv(Yl, Yh) + A helper method to evaluate the inverse transform, feeding *Yl* and + *Yh* to the tensorflow session. Assumes that the object was returned + from the Trasnform2d().inverse() method. """ def __init__(self, X, lowpass, highpasses, scales=None, graph=tf.get_default_graph()): @@ -90,7 +99,7 @@ def _get_X(self, Yl, Yh): except ValueError: data = [Yl, *list(Yh)] placeholders = [self.lowpass_op, *list(self.highpasses_ops)] - X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)}) + X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] return X def eval_fwd(self, X): From c2ad39e1a96b8c5c0e1cf28ef9c1fe26e6e79f33 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 6 Mar 2017 14:13:34 +0000 Subject: [PATCH 21/52] Wrote two more tests for inverse transform --- dtcwt/tf/transform2d.py | 5 ++--- tests/test_tfTransform2d.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 29ec73b..b5ab0cd 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -179,9 +179,8 @@ def inverse(self, pyramid, gain_mask=None): if p_ops is None: Lo_ph = tf.placeholder(tf.float32, [None, Yl.shape[0], Yl.shape[1]]) Hi_ph = tuple( - tf.placeholder(tf.complex64, [None, level.shape[1], - level.shape[2], level.shape[3]]) for - level in Yh) + tf.placeholder(tf.complex64, [None, *level.shape]) + for level in Yh) p_in = Pyramid_tf(None, Lo_ph, Hi_ph) size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) name = 'dtcwt_inv_{}'.format(size) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 9c1e114..cd168e4 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -154,5 +154,35 @@ def test_results_match2(): [np.testing.assert_array_almost_equal( s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in zip(p_np.scales, p_tf.scales)] - + +def test_results_match3(): + im = mandrill + f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np) + + f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) + X_tf = f_tf.inverse(p_tf) + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + +def test_results_match4(): + im = mandrill + gain_mask = np.ones((6,4)) + gain_mask[4,2] = 0; + gain_mask[2,1] = 0; + + f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np, gain_mask) + + f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) + X_tf = f_tf.inverse(p_tf, gain_mask) + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + # vim:sw=4:sts=4:et From 5d27d6dc3a915f9833d9ea3aa48e23ef4ff33d42 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 19 Apr 2017 13:45:22 +0100 Subject: [PATCH 22/52] Updated test suite and upgraded comments --- tests/requirements.txt | 1 + tests/test_tfTransform2d.py | 121 +++++++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index d631253..7b87548 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,3 +3,4 @@ pytest pytest-capturelog pytest-cov coverage +scipy diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index cd168e4..668910a 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -12,6 +12,7 @@ from dtcwt.numpy import Pyramid from dtcwt.coeffs import biort, qshift import tests.datasets as datasets +from scipy import stats #from .util import skip_if_no_tf PRECISION_DECIMAL = 5 @@ -22,6 +23,8 @@ def setup(): in_p = tf.placeholder(tf.float32, [None, 512, 512]) f = Transform2d() pyramid_ops = f.forward(in_p, include_scale=True) + # Make sure we run tests on cpu rather than gpus + os.environ["CUDA_VISIBLE_DEVICES"] = "" def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -114,20 +117,41 @@ def test_float32_input(): assert np.issubsctype(Yl.dtype, np.float32) assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) +def test_eval_fwd(): + y = pyramid_ops.eval_fwd(mandrill) + assert y3.lowpass.shape == (3, *y.lowpass.shape) + for hi3, hi in zip(y3.highpasses, y.highpasses): + assert hi3.shape == (3, *hi.shape) + for s3, s in zip(y3.scales, y.scales): + assert s3.shape == (3, *s.shape) + def test_multiple_inputs(): - y = pyramid_ops.eval(mandrill) - y3 = pyramid_ops.eval([mandrill, mandrill, mandrill]) + y = pyramid_ops.eval_fwd(mandrill) + y3 = pyramid_ops.eval_fwd([mandrill, mandrill, mandrill]) assert y3.lowpass.shape == (3, *y.lowpass.shape) for hi3, hi in zip(y3.highpasses, y.highpasses): assert hi3.shape == (3, *hi.shape) for s3, s in zip(y3.scales, y.scales): assert s3.shape == (3, *s.shape) -def test_results_match1(): - f_np = Transform2d_np() - p_np = f_np.forward(mandrill, include_scale=True) - - p_tf = pyramid_ops.eval(mandrill) +@pytest.mark.parametrize("test_input,biort,qshift", [ + (datasets.mandrill(),'antonini','qshift_a'), + (datasets.mandrill()[100:400,40:450],'legall','qshift_a'), + (datasets.mandrill(),'near_sym_a','qshift_c'), + (datasets.mandrill()[100:375,30:322],'near_sym_b','qshift_d'), + (datasets.mandrill(),'near_sym_b_bp', 'qshift_b_bp') +]) +def test_results_match(test_input, biort, qshift): + """ + Compare forward transform with numpy forward transform for mandrill image + """ + im=test_input + f_np = Transform2d_np(biort=biort,qshift=qshift) + p_np = f_np.forward(im, include_scale=True) + + in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) + f_tf = Transform2d(biort=biort,qshift=qshift) + p_tf = f_tf.forward(in_p, include_scale=True).eval_fwd(im) np.testing.assert_array_almost_equal( p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) @@ -138,51 +162,76 @@ def test_results_match1(): s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in zip(p_np.scales, p_tf.scales)] -def test_results_match2(): - im = mandrill[100:400,50:450] - f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') - p_np = f_np.forward(im, nlevels=4, include_scale=True) - f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') - p_tf = f_tf.forward(im, nlevels=4, include_scale=True) - - np.testing.assert_array_almost_equal( - p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) - [np.testing.assert_array_almost_equal( - h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in - zip(p_np.highpasses, p_tf.highpasses)] - [np.testing.assert_array_almost_equal( - s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in - zip(p_np.scales, p_tf.scales)] - -def test_results_match3(): - im = mandrill - f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') +@pytest.mark.parametrize("test_input,biort,qshift", [ + (datasets.mandrill(),'antonini','qshift_c'), + (datasets.mandrill()[100:411,44:460],'near_sym_a','qshift_a'), + (datasets.mandrill(),'legall','qshift_c'), + (datasets.mandrill()[100:378,20:322],'near_sym_b','qshift_06'), + (datasets.mandrill(),'near_sym_b_bp', 'qshift_b_bp') +]) +def test_results_match_inverse(test_input,biort,qshift): + im = test_input + f_np = Transform2d_np(biort=biort, qshift=qshift) p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np) - - f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') - p_tf = f_tf.forward(im, nlevels=4, include_scale=True) - X_tf = f_tf.inverse(p_tf) + + # Use a zero input and the fwd transform to get the shape of + # the pyramid easily + in_ = tf.zeros([1, im.shape[0], im.shape[1]]) + f_tf = Transform2d(biort=biort, qshift=qshift) + p_tf = f_tf.forward(in_, nlevels=4, include_scale=True) + + # Create ops for the inverse transform + pi_tf = f_tf.inverse(p_tf) + X_tf = pi_tf.eval_inv(p_np.lowpass, p_np.highpasses) np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) -def test_results_match4(): +@pytest.mark.parametrize("biort,qshift,gain_mask", [ + ('antonini','qshift_c',stats.bernoulli(0.8).rvs(size=(6,4))), + ('near_sym_a','qshift_a',stats.bernoulli(0.8).rvs(size=(6,4))), + ('legall','qshift_c',stats.bernoulli(0.8).rvs(size=(6,4))), + ('near_sym_b','qshift_06',stats.bernoulli(0.8).rvs(size=(6,4))), + ('near_sym_b_bp', 'qshift_b_bp',stats.bernoulli(0.8).rvs(size=(6,4))) +]) +def test_results_match_invmask(biort,qshift,gain_mask): im = mandrill - gain_mask = np.ones((6,4)) - gain_mask[4,2] = 0; - gain_mask[2,1] = 0; - f_np = Transform2d_np(biort='near_sym_b', qshift='qshift_c') + f_np = Transform2d_np(biort=biort, qshift=qshift) p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np, gain_mask) - f_tf = Transform2d(biort='near_sym_b', qshift='qshift_c') + f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(im, nlevels=4, include_scale=True) X_tf = f_tf.inverse(p_tf, gain_mask) np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) + +@pytest.mark.parametrize("test_input,biort,qshift", [ + (datasets.mandrill(),'antonini','qshift_06'), + (datasets.mandrill()[100:411,44:460],'near_sym_b','qshift_a'), + (datasets.mandrill(),'near_sym_b','qshift_c'), + (datasets.mandrill()[100:378,20:322],'near_sym_a','qshift_a'), + (datasets.mandrill(),'near_sym_b_bp', 'qshift_b_bp') +]) +def test_results_match_endtoend(test_input,biort,qshift): + im = test_input + f_np = Transform2d_np(biort=biort, qshift=qshift) + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np) + + in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) + f_tf = Transform2d(biort=biort, qshift=qshift) + p_tf = f_tf.forward(in_p, nlevels=4, include_scale=True) + pi_tf = f_tf.inverse(p_tf) + with tf.Session() as sess: + X_tf = sess.run(pi_tf.X, feed_dict={in_p: [im]})[0] + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + # vim:sw=4:sts=4:et From ffb1d9b0d2aa12962a580ab36d1473535513b25d Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 19 Apr 2017 13:46:04 +0100 Subject: [PATCH 23/52] Added option of providing sess to pyramid eval funcs --- dtcwt/tf/common.py | 75 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index f1cc56f..358ceb8 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -48,20 +48,28 @@ def __init__(self, X, lowpass, highpasses, scales=None, self.scales_ops = scales self.graph = graph - def _get_lowpass(self, data): + def _get_lowpass(self, data, sess=None): if self.lowpass_op is None: return None - with tf.Session(graph=self.graph) as sess: + + if sess is None: + sess = tf.Session(graph=self.graph) + + with sess: try: y = sess.run(self.lowpass_op, {self.X : data}) except ValueError: y = sess.run(self.lowpass_op, {self.X : [data]})[0] return y - def _get_highpasses(self, data): + def _get_highpasses(self, data, sess=None): if self.highpasses_ops is None: return None - with tf.Session(graph=self.graph) as sess: + + if sess is None: + sess = tf.Session(graph=self.graph) + + with sess: try: y = tuple( [sess.run(layer_hp, {self.X : data}) @@ -72,10 +80,14 @@ def _get_highpasses(self, data): for layer_hp in self.highpasses_ops]) return y - def _get_scales(self, data): + def _get_scales(self, data, sess=None): if self.scales_ops is None: return None - with tf.Session(graph=self.graph) as sess: + + if sess is None: + sess = tf.Session(graph=self.graph) + + with sess: try: y = tuple( sess.run(layer_scale, {self.X : data}) @@ -86,10 +98,14 @@ def _get_scales(self, data): for layer_scale in self.scales_ops) return y - def _get_X(self, Yl, Yh): + def _get_X(self, Yl, Yh, sess=None): if self.X is None: return None - with tf.Session(graph=self.graph) as sess: + + if sess is None: + sess = tf.Session(graph=self.graph) + + with sess: try: # Use dictionary comprehension to feed in our Yl and our # multiple layers of Yh @@ -102,12 +118,43 @@ def _get_X(self, Yl, Yh): X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] return X - def eval_fwd(self, X): - lo = self._get_lowpass(X) - hi = self._get_highpasses(X) - scales = self._get_scales(X) + def eval_fwd(self, X, sess=None): + """ + A helper function to evaluate the forward transform on a given array of + input data. + + :param X: A numpy array of shape [, height, width], where height + and width match the size of the placeholder fed to the forward + transform. + :param sess: Tensorflow session to use. If none is provided a temporary + session will be used. + + :returns: A :py:class:`dtcwt.Pyramid` of the data. The variables in + this pyramid will typically be only 2-dimensional (when calling the + numpy forward transform), but these will be 3 dimensional. + """ + lo = self._get_lowpass(X, sess) + hi = self._get_highpasses(X, sess) + scales = self._get_scales(X, sess) return Pyramid_np(lo, hi, scales) - def eval_inv(self, Yl, Yh): - return self._get_X(Yl, Yh) + def eval_inv(self, Yl, Yh, sess=None): + """ + A helper function to evaluate the inverse transform on given wavelet + coefficients. + + :param Yl: A numpy array of shape [, h/(2**scale), w/(2**scale)], + where (h,w) was the size of the input image. + :param Yh: A tuple or list of the highpass coefficients. Each entry in + the tuple or list represents the scale the coefficients belong to. The + size of the coefficients must match the outputs of the forward + transform. I.e. Yh[0] should have shape [, 6, h/2, w/2], where the + input image had shape (h, w). should be the same across all + scales, and should match the size of the Yl first dimension. + :param sess: Tensorflow session to use. If none is provided a temporary + session will be used. + + :returns: A numpy array of the inverted data. + """ + return self._get_X(Yl, Yh, sess) From d4588c5176e255d2aeda8d8f4efc7a26f0e9337e Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Tue, 2 May 2017 11:57:29 +0100 Subject: [PATCH 24/52] Added functionality to handle multichannel inputs --- dtcwt/tf/common.py | 24 +++++ dtcwt/tf/transform2d.py | 154 +++++++++++++++++++++++++---- tests/test_tfinputshapes.py | 190 ++++++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 18 deletions(-) create mode 100644 tests/test_tfinputshapes.py diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 358ceb8..588bb8d 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -31,6 +31,9 @@ class Pyramid_tf(object): *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. + .. py:method:: apply_reshaping(fn) + A helper method to apply a tensor reshaping to all of the elements in + the pyramid. .. py:method:: eval_fwd(X) A helper method to evaluate the forward transform, feeding *X* as input to the tensorflow session. Assumes that the object was returned from @@ -118,6 +121,23 @@ def _get_X(self, Yl, Yh, sess=None): X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] return X + + def apply_reshaping(self, fn): + """ + A helper function to apply a tensor transformation on all of the + elements in the pyramid. E.g. reshape all of them in the same way. + + :param fn: function to apply to each of the lowpass_op, highpasses_ops and + scale_ops tensors + """ + self.lowpass_op = fn(self.lowpass_op) + self.highpasses_ops = tuple( + [fn(h_scale) for h_scale in self.highpasses_ops]) + if not self.scales_ops is None: + self.scales_ops = tuple( + [fn(s_scale) for s_scale in self.scales_ops]) + + def eval_fwd(self, X, sess=None): """ A helper function to evaluate the forward transform on a given array of @@ -133,6 +153,10 @@ def eval_fwd(self, X, sess=None): this pyramid will typically be only 2-dimensional (when calling the numpy forward transform), but these will be 3 dimensional. """ + if len(X.shape) == 2 and len(self.lowpass_op.get_shape()) == 3: + logging.warn('Fed with a 2d shape input. For efficient calculation' + + ' feed batches of inputs.') + lo = self._get_lowpass(X, sess) hi = self._get_highpasses(X, sess) scales = self._get_scales(X, sess) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index b5ab0cd..c9bda11 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -55,6 +55,7 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): self.forward_graphs = {} self.inverse_graphs = {} + def _find_forward_graph(self, shape): ''' See if we can reuse an old graph for the forward transform ''' find_key = '{}x{}'.format(shape[0], shape[1]) @@ -63,11 +64,13 @@ def _find_forward_graph(self, shape): return val return None + def _add_forward_graph(self, p_ops, shape): ''' Keep record of the pyramid so we can use it later if need be ''' find_key = '{}x{}'.format(shape[0], shape[1]) self.forward_graphs[find_key] = p_ops + def _find_inverse_graph(self, Lo_shape, nlevels): ''' See if we can reuse an old graph for the inverse transform ''' find_key = '{}x{}'.format(Lo_shape[0], Lo_shape[1]) @@ -76,28 +79,114 @@ def _find_inverse_graph(self, Lo_shape, nlevels): return val return None + def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): ''' Keep record of the pyramid so we can use it later if need be ''' find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) self.inverse_graphs[find_key] = p_ops - def forward(self, X, nlevels=3, include_scale=False): + + def forward_channels(self, X, nlevels=3, include_scale=False): + ''' + Perform a forward transform on an image with multiplice channels. + Must provide with a tensorflow variable or placeholder (unlike the more + general :py:method:`Transform2d.forward`). + :param X: Input image which you wish to transform. Input must be of + shape [batch, height, width, channels]. + :param nlevels: Number of levels of the dtcwt transform to calculate. + :param include_scale: Whether or not to return the lowpass results at + each sclae of the transform, or only at the highest scale (as is custom + for multiresolution analysis) + :returns: A tuple of Yl, Yh, Yscale. + The Yl corresponds to the lowpass + of the image, and has shape [batch, channels, height, width] of type + tf.float32. + Yh corresponds to the highpasses for the image, and is a list of length + nlevels, with each entry having shape [batch, channels, height', width', + 6] of type tf.complex64. + Yscale corresponds to the lowpass outputs at each scale of the + transform, and is a list of length nlevels, with each entry having + shape [batch, channels, height', width'] of type tf.float32. + ''' + if not tf.is_numeric_tensor(X): + raise ValueError( + '''The provided input must be a tensorflow variable or placeholder''') + else: + X_shape = X.get_shape().as_list() + if len(X_shape) != 4: + raise ValueError('''The entered variable has incorrect dimensions {}. + It must be of shape [batch, height, width, channels] + (batch can be None).'''.format(X_shape)) + + original_size = X.get_shape().as_list()[1:-1] + size = '{}x{}'.format(original_size[0], original_size[1]) + name = 'dtcwt_fwd_{}'.format(size) + with tf.name_scope(name): + # Put the channel axis first + X = tf.transpose(X, perm=[3,0,1,2]) + f = lambda x: self._forward_ops(x, nlevels, include_scale, + return_tuple=True) + + # Calculate the dtcwt for each of the channels independently + # This will return tensors of shape: + # Yl: [c, batch, height, width] + # Yh: list of length nlevels, each of shape [c, batch, height, width, 6] + # Yscale: list of length nlevels, each of shape [c, batch, height, width] + if include_scale: + shape = (tf.float32, # lowpass object + tuple(tf.complex64 for k in range(nlevels)), # highpasses + tuple(tf.float32 for k in range(nlevels))) + Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) + # Transpose the tensors to put the channel after the batch + Yl = tf.transpose(Yl, perm=[1,0,2,3]) + Yh = tuple( + [tf.transpose(x, perm=[1,0,2,3,4]) for x in Yh]) + Yscale = tuple( + [tf.transpose(x, perm=[1,0,2,3]) for x in Yscale]) + return Yl, Yh, Yscale + + else: + shape = (tf.float32, + tuple(tf.complex64 for k in range(nlevels))) + Yl, Yh = tf.map_fn(f, X, dtype=shape) + # Transpose the tensors to put the channel after the batch + Yl = tf.transpose(Yl, perm=[1,0,2,3]) + Yh = tuple( + [tf.transpose(x, perm=[1,0,2,3,4]) for x in Yh]) + return Yl, Yh + + + def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): ''' Perform a forward transform on an image. Can provide the forward transform with either an np array (naive usage), or a tensorflow variable or placeholder (designed usage). + :param X: Input image which you wish to transform. Can be a numpy + array, tensorflow Variable or Tensorflow placeholder. See comments + below. + :param nlevels: Number of levels of the dtcwt transform to calculate. + :param include_scale: Whether or not to return the lowpass results at + each sclae of the transform, or only at the highest scale (as is custom + for multiresolution analysis) + :param return_tuple: If true, returns a tuple of lowpass, highpasses + and scales (if include_scale is True) rather than a Pyramid object. + :returns: A :py:class:`Pyramid_tf` object or a :py:class:`Pyramid` + object, depending on the type of input data provided. + + Data Types for the :py:param:`X`: If a numpy array is provided, the forward function will create a graph of the right size to match the input (or check if it has previously created one), and then feed the input into the graph and evaluate it. - This operation will return a Pyramid() object similar to running the - numpy version would. + This operation will return a :py:class:`Pyramid` object similar to + how running the numpy version would. + If a tensorflow variable or placeholder is provided, the forward function will create a graph of the right size, and return a Pyramid_ops() object. ''' # Check if a numpy array was provided - if not isinstance(X, tf.Tensor) and not isinstance(X, tf.Variable): + if not tf.is_numeric_tensor(X): X = np.atleast_2d(asfarray(X)) if len(X.shape) >= 3: raise ValueError('''The entered variable has incorrect dimensions {}. @@ -106,7 +195,7 @@ def forward(self, X, nlevels=3, include_scale=False): enter each channel separately. If you wish to enter a batch of images, please instead provide either a tf.Placeholder or a tf.Variable input of size [batch, height, width]. - '''.format(original_size)) + '''.format(X.shape)) # Check if the ops already exist for an input of the given size p_ops = self._find_forward_graph(X.shape) @@ -128,18 +217,30 @@ def forward(self, X, nlevels=3, include_scale=False): # A tensorflow object was provided else: X_shape = X.get_shape().as_list() - if len(X_shape) != 3: + if len(X_shape) > 3: raise ValueError('''The entered variable has incorrect dimensions {}. If X is a tf placeholder or variable, it must be of shape - [batch, height, width] (batch can be None). For colour images, - please enter each channel separately. - '''.format(original_size)) + [batch, height, width] (batch can be None) or + [height, width]. For colour images, please enter each + channel separately.'''.format(X_shape)) + + # If a batch wasn't provided, add a none dimension and remove it + # later + if len(X_shape) == 2: + logging.warn('Fed with a 2d shape input. For efficient calculation' + + ' feed batches of inputs. Input was reshaped to' + + ' have a 1 in the first dimension.') + X = tf.expand_dims(X,axis=0) + original_size = X.get_shape().as_list()[1:] size = '{}x{}'.format(original_size[0], original_size[1]) name = 'dtcwt_fwd_{}'.format(size) with tf.name_scope(name): - return self._forward_ops(X, nlevels, include_scale) + p_tf = self._forward_ops(X, nlevels, include_scale, + return_tuple) + + return p_tf def inverse(self, pyramid, gain_mask=None): ''' @@ -207,14 +308,19 @@ def inverse(self, pyramid, gain_mask=None): raise ValueError('''Unknown pyramid provided to inverse transform''') - def _forward_ops(self, X, nlevels=3, include_scale=False): + def _forward_ops(self, X, nlevels=3, include_scale=False, + return_tuple=False): """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [Batch, rows, cols] :param nlevels: Number of levels of wavelet decomposition :param include_scale: True if you want to receive the lowpass coefficients at intermediate layers. - :returns: A :py:class:`dtcwt.Pyramid` compatible object representing the transform-domain signal - .. codeauthor:: Fergal Cotter , Feb 2017 + :param return_tuple: If true, instead of returning + a :py:class`dtcwt.Pyramid_tf` object, return a tuple of (lowpass, + highpasses, scales) + :returns: A :py:class:`dtcwt.Pyramid_tf` compatible + object representing the transform-domain signal .. codeauthor:: Fergal + Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 @@ -241,7 +347,7 @@ def _forward_ops(self, X, nlevels=3, include_scale=False): raise ValueError('Qshift wavelet must have 12 or 8 components.') # Check the shape and form of the input - if not isinstance(X, tf.Tensor) and not isinstance(X, tf.Variable): + if not tf.is_numeric_tensor(X): raise ValueError('''Please provide the forward function with a tensorflow placeholder or variable of size [batch, width, height] (batch can be None if you do not wish to specify it).''') @@ -279,9 +385,15 @@ def _forward_ops(self, X, nlevels=3, include_scale=False): if nlevels == 0: if include_scale: - return Pyramid_tf(X_in, X, (), ()) + if return_tuple: + return X_in, (), () + else: + return Pyramid_tf(X_in, X, (), ()) else: - return Pyramid_tf(X_in, X, ()) + if return_tuple: + return X_in, () + else: + return Pyramid_tf(X_in, X, ()) ############################ Initialise ############################### @@ -397,9 +509,15 @@ def _forward_ops(self, X, nlevels=3, include_scale=False): 'The rightmost column has been duplicated, prior to decomposition.') if include_scale: - return Pyramid_tf(X_in, Yl, tuple(Yh), tuple(Yscale)) + if return_tuple: + return Yl, tuple(Yh), tuple(Yscale) + else: + return Pyramid_tf(X_in, Yl, tuple(Yh), tuple(Yscale)) else: - return Pyramid_tf(X_in, Yl, tuple(Yh)) + if return_tuple: + return Yl, tuple(Yh) + else: + return Pyramid_tf(X_in, Yl, tuple(Yh)) def _inverse_ops(self, pyramid, gain_mask=None): diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py new file mode 100644 index 0000000..7eb4946 --- /dev/null +++ b/tests/test_tfinputshapes.py @@ -0,0 +1,190 @@ +import os +import pytest +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") + +from pytest import raises + +import numpy as np +import tensorflow as tf +from dtcwt.tf import Transform2d, dtwavexfm2, dtwaveifm2 +import tests.datasets as datasets + +PRECISION_DECIMAL = 5 + +def setup(): + # Make sure we run tests on cpu rather than gpus + os.environ["CUDA_VISIBLE_DEVICES"] = "" + +@pytest.mark.parametrize("nlevels, include_scale", [ + (2,False), + (2,True), + (4,False), + (3,True) +]) +def test_2d_input(nlevels, include_scale): + in_ = tf.placeholder(tf.float32, [512, 512]) + t = Transform2d() + # Calling forward with a 2d input will throw a warning + p = t.forward(in_, nlevels, include_scale) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert p.lowpass_op.get_shape().as_list() == [1, extent, extent] + assert p.lowpass_op.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert p.highpasses_ops[i].get_shape().as_list() == [1, extent, extent, 6] + assert p.highpasses_ops[i].dtype == tf.complex64 + if include_scale: + assert p.scales_ops[i].get_shape().as_list() == [1, 2*extent, 2*extent] + assert p.scales_ops[i].dtype == tf.float32 + + +@pytest.mark.parametrize("nlevels, include_scale", [ + (2,False), + (2,True), + (4,False), + (3,True) +]) +def test_apply_reshaping(nlevels, include_scale): + # Test the reshaping function of the Pyramid_tf class. This should apply + # the same tf op to all of its operations. A good example would be to + # remove the batch dimension from each op. + in_ = tf.placeholder(tf.float32, [512, 512]) + t = Transform2d() + # Calling forward with a 2d input will throw a warning + p = t.forward(in_, nlevels, include_scale) + f = lambda x: tf.squeeze(x, squeeze_dims=0) + p.apply_reshaping(f) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert p.lowpass_op.get_shape().as_list() == [extent, extent] + assert p.lowpass_op.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert p.highpasses_ops[i].get_shape().as_list() == [extent, extent, 6] + assert p.highpasses_ops[i].dtype == tf.complex64 + if include_scale: + assert p.scales_ops[i].get_shape().as_list() == [2*extent, 2*extent] + assert p.scales_ops[i].dtype == tf.float32 + + +@pytest.mark.parametrize("nlevels, include_scale", [ + (2,False), + (2,True), + (4,False), + (3,True) +]) +def test_2d_input_tuple(nlevels, include_scale): + in_ = tf.placeholder(tf.float32, [512, 512]) + t = Transform2d() + # Calling forward with a 2d input will throw a warning + if include_scale: + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + else: + Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert Yl.get_shape().as_list() == [1, extent, extent] + assert Yl.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert Yh[i].get_shape().as_list() == [1, extent, extent, 6] + assert Yh[i].dtype == tf.complex64 + if include_scale: + assert Yscale[i].get_shape().as_list() == [1, 2*extent, 2*extent] + assert Yscale[i].dtype == tf.float32 + + + +@pytest.mark.parametrize("nlevels, include_scale, batch_size", [ + (2,False,None), + (2,True,10), + (4,False,None), + (3,True,2) +]) +def test_batch_input(nlevels, include_scale, batch_size): + in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) + t = Transform2d() + p = t.forward(in_, nlevels, include_scale) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert p.lowpass_op.get_shape().as_list() == [batch_size, extent, extent] + assert p.lowpass_op.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert p.highpasses_ops[i].get_shape().as_list() == [batch_size, extent, extent, 6] + assert p.highpasses_ops[i].dtype == tf.complex64 + if include_scale: + assert p.scales_ops[i].get_shape().as_list() == [batch_size, 2*extent, 2*extent] + assert p.scales_ops[i].dtype == tf.float32 + + +@pytest.mark.parametrize("nlevels, include_scale, batch_size", [ + (2,False,None), + (2,True,10), + (4,False,None), + (3,True,2) +]) +def test_batch_input_tuple(nlevels, include_scale, batch_size): + in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) + t = Transform2d() + if include_scale: + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + else: + Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert Yl.get_shape().as_list() == [batch_size, extent, extent] + assert Yl.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert Yh[i].get_shape().as_list() == [batch_size, extent, extent, 6] + assert Yh[i].dtype == tf.complex64 + if include_scale: + assert Yscale[i].get_shape().as_list() == [batch_size, 2*extent, 2*extent] + assert Yscale[i].dtype == tf.float32 + +@pytest.mark.parametrize("nlevels, include_scale, channels", [ + (2,False,5), + (2,True,2), + (4,False,10), + (3,True,6) +]) +def test_multichannel(nlevels, include_scale, channels): + in_ = tf.placeholder(tf.float32, [None, 512, 512, channels]) + t = Transform2d() + if include_scale: + Yl, Yh, Yscale = t.forward_channels(in_, nlevels, include_scale) + else: + Yl, Yh = t.forward_channels(in_, nlevels, include_scale) + + # At level 1, the lowpass output will be the same size as the input. At + # levels above that, it will be half the size per level + extent = 512 * 2**(-(nlevels-1)) + assert Yl.get_shape().as_list() == [None, channels, extent, extent] + assert Yl.dtype == tf.float32 + + for i in range(nlevels): + extent = 512 * 2**(-(i+1)) + assert Yh[i].get_shape().as_list() == [None, channels, extent, extent, 6] + assert Yh[i].dtype == tf.complex64 + if include_scale: + assert Yscale[i].get_shape().as_list() == [None, channels, 2*extent, 2*extent] + assert Yscale[i].dtype == tf.float32 + From 7954b0fd0de3429086198c6aa5db0600ed2f2272 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Tue, 2 May 2017 13:45:09 +0100 Subject: [PATCH 25/52] Changed shape of multichannel output made it [batch, height, width, channels, 6] rather than [batch, height, width, channels] --- dtcwt/tf/transform2d.py | 18 +++++++++--------- tests/test_tfinputshapes.py | 7 ++++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index c9bda11..6d442cd 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -99,14 +99,14 @@ def forward_channels(self, X, nlevels=3, include_scale=False): for multiresolution analysis) :returns: A tuple of Yl, Yh, Yscale. The Yl corresponds to the lowpass - of the image, and has shape [batch, channels, height, width] of type + of the image, and has shape [batch, height, width, channels] of type tf.float32. Yh corresponds to the highpasses for the image, and is a list of length - nlevels, with each entry having shape [batch, channels, height', width', - 6] of type tf.complex64. + nlevels, with each entry having shape [batch, height', width', + channels, 6] of type tf.complex64. Yscale corresponds to the lowpass outputs at each scale of the transform, and is a list of length nlevels, with each entry having - shape [batch, channels, height', width'] of type tf.float32. + shape [batch, height', width', channels] of type tf.float32. ''' if not tf.is_numeric_tensor(X): raise ValueError( @@ -138,11 +138,11 @@ def forward_channels(self, X, nlevels=3, include_scale=False): tuple(tf.float32 for k in range(nlevels))) Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) # Transpose the tensors to put the channel after the batch - Yl = tf.transpose(Yl, perm=[1,0,2,3]) + Yl = tf.transpose(Yl, perm=[1,2,3,0]) Yh = tuple( - [tf.transpose(x, perm=[1,0,2,3,4]) for x in Yh]) + [tf.transpose(x, perm=[1,2,3,0,4]) for x in Yh]) Yscale = tuple( - [tf.transpose(x, perm=[1,0,2,3]) for x in Yscale]) + [tf.transpose(x, perm=[1,2,3,0]) for x in Yscale]) return Yl, Yh, Yscale else: @@ -150,9 +150,9 @@ def forward_channels(self, X, nlevels=3, include_scale=False): tuple(tf.complex64 for k in range(nlevels))) Yl, Yh = tf.map_fn(f, X, dtype=shape) # Transpose the tensors to put the channel after the batch - Yl = tf.transpose(Yl, perm=[1,0,2,3]) + Yl = tf.transpose(Yl, perm=[1,2,3,0]) Yh = tuple( - [tf.transpose(x, perm=[1,0,2,3,4]) for x in Yh]) + [tf.transpose(x, perm=[1,2,3,0,4]) for x in Yh]) return Yl, Yh diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index 7eb4946..b35d4f0 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -177,14 +177,15 @@ def test_multichannel(nlevels, include_scale, channels): # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level extent = 512 * 2**(-(nlevels-1)) - assert Yl.get_shape().as_list() == [None, channels, extent, extent] + assert Yl.get_shape().as_list() == [None, extent, extent, channels] assert Yl.dtype == tf.float32 for i in range(nlevels): extent = 512 * 2**(-(i+1)) - assert Yh[i].get_shape().as_list() == [None, channels, extent, extent, 6] + assert Yh[i].get_shape().as_list() == [None, extent, extent, channels, 6] assert Yh[i].dtype == tf.complex64 if include_scale: - assert Yscale[i].get_shape().as_list() == [None, channels, 2*extent, 2*extent] + assert Yscale[i].get_shape().as_list() == [ + None, 2*extent, 2*extent, channels] assert Yscale[i].dtype == tf.float32 From 99ca6069afb6d376ed58776c1467f4c4c8262778 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Tue, 2 May 2017 14:36:15 +0100 Subject: [PATCH 26/52] Added tensorflow to tox list of required packages --- tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index 7b87548..2884a79 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,3 +4,4 @@ pytest-capturelog pytest-cov coverage scipy +tensorflow From 471941e0793f172eb25627c7cbfe4993e6ef28c1 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Tue, 2 May 2017 15:04:52 +0100 Subject: [PATCH 27/52] Fixed check in eval_fwd to accept lists of arrays --- dtcwt/tf/common.py | 4 ---- tests/test_tfTransform2d.py | 5 ----- 2 files changed, 9 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 588bb8d..f7ab214 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -153,10 +153,6 @@ def eval_fwd(self, X, sess=None): this pyramid will typically be only 2-dimensional (when calling the numpy forward transform), but these will be 3 dimensional. """ - if len(X.shape) == 2 and len(self.lowpass_op.get_shape()) == 3: - logging.warn('Fed with a 2d shape input. For efficient calculation' - + ' feed batches of inputs.') - lo = self._get_lowpass(X, sess) hi = self._get_highpasses(X, sess) scales = self._get_scales(X, sess) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 668910a..828b5e4 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -119,11 +119,6 @@ def test_float32_input(): def test_eval_fwd(): y = pyramid_ops.eval_fwd(mandrill) - assert y3.lowpass.shape == (3, *y.lowpass.shape) - for hi3, hi in zip(y3.highpasses, y.highpasses): - assert hi3.shape == (3, *hi.shape) - for s3, s in zip(y3.scales, y.scales): - assert s3.shape == (3, *s.shape) def test_multiple_inputs(): y = pyramid_ops.eval_fwd(mandrill) From ff41630d73370383cba94b39cb162cb0ff8f71c1 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Tue, 2 May 2017 18:12:07 +0100 Subject: [PATCH 28/52] Updated tf tests to only run if tensorflow is installed --- dtcwt/tf/common.py | 7 +++++- dtcwt/tf/lowlevel.py | 7 +++--- dtcwt/tf/transform2d.py | 8 ++++++- dtcwt/utils.py | 6 +++++ tests/requirements.txt | 1 - tests/test_tfTransform2d.py | 44 ++++++++++++++++++++++++++++++++----- tests/test_tfcoldfilt.py | 25 ++++++++++++++++----- tests/test_tfcolfilter.py | 26 +++++++++++++++++----- tests/test_tfcolifilt.py | 28 ++++++++++++++++++----- tests/test_tfinputshapes.py | 22 ++++++++++++++----- tests/test_tfrowdfilt.py | 26 ++++++++++++++++------ tests/test_tfrowfilter.py | 26 +++++++++++++++++----- tests/tf-requirements.txt | 2 ++ tests/util.py | 2 +- tox.ini | 3 ++- 15 files changed, 182 insertions(+), 51 deletions(-) create mode 100644 tests/tf-requirements.txt diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index f7ab214..27f36c5 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import numpy as np -import tensorflow as tf import logging from dtcwt.coeffs import biort as _biort, qshift as _qshift @@ -10,6 +9,12 @@ from dtcwt.numpy import Pyramid as Pyramid_np +try: + import tensorflow as tf +except ImportError: + # The lack of tensorflow will be caught by the low-level routines. + pass + class Pyramid_tf(object): """A tensorflow representation of a transform domain signal. Backends are free to implement any class which respects this interface for diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index ee6dfc9..777ecbf 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -1,15 +1,14 @@ from __future__ import absolute_import -import tensorflow as tf -import numpy as np -from dtcwt.utils import asfarray, as_column_vector - try: import tensorflow as tf _HAVE_TF = True except ImportError: _HAVE_TF = False +import numpy as np +from dtcwt.utils import asfarray, as_column_vector + def _as_row_tensor(h): if isinstance(h, tf.Tensor): h = tf.reshape(h, [1, -1]) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 6d442cd..8ae815b 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import numpy as np -import tensorflow as tf import logging from six.moves import xrange @@ -15,6 +14,13 @@ from dtcwt.tf.lowlevel import * +try: + import tensorflow as tf +except ImportError: + # The lack of tensorflow will be caught by the low-level routines. + pass + + def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False): t = Transform2d(biort=biort, qshift=qshift) r = t.forward(X, nlevels=nlevels, include_scale=include_scale) diff --git a/dtcwt/utils.py b/dtcwt/utils.py index ca0ed32..b6c9406 100644 --- a/dtcwt/utils.py +++ b/dtcwt/utils.py @@ -5,6 +5,12 @@ import functools import numpy as np +try: + import tensorflow as tf + _HAVE_TF = True +except ImportError: + _HAVE_TF = False + def drawedge(theta,r,w,N): """Generate an image of size N * N pels, of an edge going from 0 to 1 in height at theta degrees to the horizontal (top of image = 1 if angle = 0). diff --git a/tests/requirements.txt b/tests/requirements.txt index 2884a79..7b87548 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,4 +4,3 @@ pytest-capturelog pytest-cov coverage scipy -tensorflow diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 828b5e4..587cc37 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -1,24 +1,31 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") from pytest import raises import numpy as np -import tensorflow as tf -from dtcwt.tf import Transform2d, dtwavexfm2, dtwaveifm2 +from importlib import import_module from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.numpy import Pyramid from dtcwt.coeffs import biort, qshift import tests.datasets as datasets from scipy import stats -#from .util import skip_if_no_tf +from .util import skip_if_no_tf PRECISION_DECIMAL = 5 -def setup(): +@skip_if_no_tf +def test_setup(): global mandrill, in_p, pyramid_ops + global tf, Transform2d, dtwavexfm2, dtwaveifm2 + # Import the tensorflow modules + tf = import_module('tensorflow') + dtcwt_tf = import_module('dtcwt.tf') + Transform2d = getattr(dtcwt_tf, 'Transform2d') + dtwavexfm2 = getattr(dtcwt_tf, 'dtwavexfm2') + dtwaveifm2 = getattr(dtcwt_tf, 'dtwaveifm2') + + mandrill = datasets.mandrill() in_p = tf.placeholder(tf.float32, [None, 512, 512]) f = Transform2d() @@ -26,27 +33,33 @@ def setup(): # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 assert mandrill.max() <= 1 assert mandrill.dtype == np.float32 +@skip_if_no_tf def test_simple(): Yl, Yh = dtwavexfm2(mandrill) +@skip_if_no_tf def test_specific_wavelet(): Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), qshift=qshift('qshift_06')) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_1d(): Yl, Yh = dtwavexfm2(mandrill[0,:]) +@skip_if_no_tf @pytest.mark.skip(reason='Not currently implemented') def test_3d(): with raises(ValueError): Yl, Yh = dtwavexfm2(np.dstack((mandrill, mandrill))) +@skip_if_no_tf def test_simple_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, include_scale=True) @@ -54,40 +67,50 @@ def test_simple_w_scale(): for x in Yscale: assert x is not None +@skip_if_no_tf def test_odd_rows(): Yl, Yh = dtwavexfm2(mandrill[:509,:]) +@skip_if_no_tf def test_odd_rows_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:], include_scale=True) +@skip_if_no_tf def test_odd_cols(): Yl, Yh = dtwavexfm2(mandrill[:,:509]) +@skip_if_no_tf def test_odd_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) +@skip_if_no_tf def test_odd_rows_and_cols(): Yl, Yh = dtwavexfm2(mandrill[:,:509]) +@skip_if_no_tf def test_odd_rows_and_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) +@skip_if_no_tf def test_rot_symm_modified(): # This test only checks there is no error running these functions, not that they work Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) Z = dtwaveifm2(Yl, Yh, biort='near_sym_b_bp', qshift='qshift_b_bp') +@skip_if_no_tf def test_0_levels(): Yl, Yh = dtwavexfm2(mandrill, nlevels=0) np.testing.assert_array_almost_equal(Yl, mandrill, PRECISION_DECIMAL) assert len(Yh) == 0 +@skip_if_no_tf def test_0_levels_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, nlevels=0, include_scale=True) np.testing.assert_array_almost_equal(Yl, mandrill, PRECISION_DECIMAL) assert len(Yh) == 0 assert len(Yscale) == 0 +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_input(): # Check that an integer input is correctly coerced into a floating point @@ -95,6 +118,7 @@ def test_integer_input(): Yl, Yh = dtwavexfm2([[1,2,3,4], [1,2,3,4]]) assert np.any(Yl != 0) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_perfect_recon(): # Check that an integer input is correctly coerced into a floating point @@ -104,6 +128,7 @@ def test_integer_perfect_recon(): B = dtwaveifm2(Yl, Yh) assert np.max(np.abs(A-B)) < 1e-5 +@skip_if_no_tf def test_mandrill_perfect_recon(): # Check that an integer input is correctly coerced into a floating point # array and reconstructed @@ -111,15 +136,18 @@ def test_mandrill_perfect_recon(): B = dtwaveifm2(Yl, Yh) assert np.max(np.abs(mandrill-B)) < 1e-5 +@skip_if_no_tf def test_float32_input(): # Check that an float32 input is correctly output as float32 Yl, Yh = dtwavexfm2(mandrill.astype(np.float32)) assert np.issubsctype(Yl.dtype, np.float32) assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) +@skip_if_no_tf def test_eval_fwd(): y = pyramid_ops.eval_fwd(mandrill) +@skip_if_no_tf def test_multiple_inputs(): y = pyramid_ops.eval_fwd(mandrill) y3 = pyramid_ops.eval_fwd([mandrill, mandrill, mandrill]) @@ -129,6 +157,7 @@ def test_multiple_inputs(): for s3, s in zip(y3.scales, y.scales): assert s3.shape == (3, *s.shape) +@skip_if_no_tf @pytest.mark.parametrize("test_input,biort,qshift", [ (datasets.mandrill(),'antonini','qshift_a'), (datasets.mandrill()[100:400,40:450],'legall','qshift_a'), @@ -158,6 +187,7 @@ def test_results_match(test_input, biort, qshift): zip(p_np.scales, p_tf.scales)] +@skip_if_no_tf @pytest.mark.parametrize("test_input,biort,qshift", [ (datasets.mandrill(),'antonini','qshift_c'), (datasets.mandrill()[100:411,44:460],'near_sym_a','qshift_a'), @@ -184,6 +214,7 @@ def test_results_match_inverse(test_input,biort,qshift): np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) +@skip_if_no_tf @pytest.mark.parametrize("biort,qshift,gain_mask", [ ('antonini','qshift_c',stats.bernoulli(0.8).rvs(size=(6,4))), ('near_sym_a','qshift_a',stats.bernoulli(0.8).rvs(size=(6,4))), @@ -205,6 +236,7 @@ def test_results_match_invmask(biort,qshift,gain_mask): np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) +@skip_if_no_tf @pytest.mark.parametrize("test_input,biort,qshift", [ (datasets.mandrill(),'antonini','qshift_06'), (datasets.mandrill()[100:411,44:460],'near_sym_b','qshift_a'), diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py index b280a65..3dd8254 100644 --- a/tests/test_tfcoldfilt.py +++ b/tests/test_tfcoldfilt.py @@ -1,24 +1,28 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") import numpy as np -import tensorflow as tf from dtcwt.coeffs import biort, qshift -from dtcwt.tf.lowlevel import coldfilt from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt +from importlib import import_module from pytest import raises +from .util import skip_if_no_tf import tests.datasets as datasets -def setup(): - global mandrill, mandrill_t +@skip_if_no_tf +def test_setup(): + global mandrill, mandrill_t, tf, coldfilt + tf = import_module('tensorflow') + lowlevel = import_module('dtcwt.tf.lowlevel') + coldfilt = getattr(lowlevel, 'coldfilt') + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 @@ -26,28 +30,35 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) +@skip_if_no_tf def test_odd_filter(): with raises(ValueError): coldfilt(mandrill_t, (-1,2,-1), (-1,2,1)) +@skip_if_no_tf def test_different_size(): with raises(ValueError): coldfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) +@skip_if_no_tf def test_bad_input_size(): with raises(ValueError): coldfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) +@skip_if_no_tf def test_good_input_size(): coldfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) +@skip_if_no_tf def test_good_input_size_non_orthogonal(): coldfilt(mandrill_t[:,:,:511], (1,1), (1,1)) +@skip_if_no_tf def test_output_size(): y_op = coldfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0]/2, mandrill.shape[1]) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] @@ -60,6 +71,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] @@ -69,6 +81,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index 96564f7..6e7f286 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -1,22 +1,26 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") import numpy as np -import tensorflow as tf from dtcwt.coeffs import biort, qshift -from dtcwt.tf.lowlevel import colfilter from dtcwt.numpy.lowlevel import colfilter as np_colfilter +from importlib import import_module +from .util import skip_if_no_tf import tests.datasets as datasets -def setup(): - global mandrill, mandrill_t +@skip_if_no_tf +def test_setup(): + global mandrill, mandrill_t, tf, colfilter + tf = import_module('tensorflow') + lowlevel = import_module('dtcwt.tf.lowlevel') + colfilter = getattr(lowlevel, 'colfilter') + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 @@ -24,24 +28,29 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) +@skip_if_no_tf def test_odd_size(): y_op = colfilter(mandrill_t, [-1,2,-1]) assert y_op.get_shape()[1:] == mandrill.shape +@skip_if_no_tf def test_even_size(): y_op = colfilter(mandrill_t, [-1,-1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) +@skip_if_no_tf def test_qshift(): h = qshift('qshift_a')[0] y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) +@skip_if_no_tf def test_biort(): h = biort('antonini')[0] y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == mandrill.shape +@skip_if_no_tf def test_even_size(): zero_t = tf.zeros([1, mandrill.shape[0], mandrill.shape[1]], tf.float32) y_op = colfilter(zero_t, [-1,1]) @@ -50,6 +59,7 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): h = qshift('qshift_b')[0] @@ -61,6 +71,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill, h) @@ -69,6 +80,7 @@ def test_equal_numpy_biort1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_biort2(): h = biort('near_sym_b')[0] im = mandrill[52:407, 30:401] @@ -79,6 +91,7 @@ def test_equal_numpy_biort2(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] ref = np_colfilter(mandrill, h) @@ -87,6 +100,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift2(): h = qshift('qshift_c')[0] im = mandrill[52:407, 30:401] diff --git a/tests/test_tfcolifilt.py b/tests/test_tfcolifilt.py index e13e9b5..224bf4c 100644 --- a/tests/test_tfcolifilt.py +++ b/tests/test_tfcolifilt.py @@ -1,24 +1,28 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") import numpy as np -import tensorflow as tf -from dtcwt.tf.lowlevel import colifilt from dtcwt.coeffs import qshift from dtcwt.numpy.lowlevel import colifilt as np_colifilt +from importlib import import_module from pytest import raises +from .util import skip_if_no_tf import tests.datasets as datasets -def setup(): - global mandrill, mandrill_t +@skip_if_no_tf +def test_setup(): + global mandrill, mandrill_t, tf, colifilt + tf = import_module('tensorflow') + lowlevel = import_module('dtcwt.tf.lowlevel') + colifilt = getattr(lowlevel, 'colifilt') + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 @@ -26,43 +30,53 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) +@skip_if_no_tf def test_odd_filter(): with raises(ValueError): colifilt(mandrill_t, (-1,2,-1), (-1,2,1)) +@skip_if_no_tf def test_different_size_h(): with raises(ValueError): colifilt(mandrill_t, (-1,2,1), (-0.5,-1,2,-1,0.5)) +@skip_if_no_tf def test_zero_input(): Y = colifilt(mandrill_t, (-1,1), (1,-1)) with tf.Session() as sess: y = sess.run(Y, {mandrill_t : [np.zeros_like(mandrill)]})[0] assert np.all(y[:0] == 0) +@skip_if_no_tf def test_bad_input_size(): with raises(ValueError): colifilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) +@skip_if_no_tf def test_good_input_size(): colifilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) +@skip_if_no_tf def test_output_size(): Y = colifilt(mandrill_t, (-1,1), (1,-1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) +@skip_if_no_tf def test_non_orthogonal_input(): Y = colifilt(mandrill_t, (1,1), (1,1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) +@skip_if_no_tf def test_output_size_non_mult_4(): Y = colifilt(mandrill_t, (-1,0,0,1), (1,0,0,-1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) +@skip_if_no_tf def test_non_orthogonal_input_non_mult_4(): Y = colifilt(mandrill_t, (1,0,0,1), (1,0,0,1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] @@ -75,6 +89,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] @@ -84,6 +99,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index b35d4f0..e07d173 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -1,21 +1,28 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") from pytest import raises import numpy as np -import tensorflow as tf -from dtcwt.tf import Transform2d, dtwavexfm2, dtwaveifm2 import tests.datasets as datasets +from importlib import import_module +from .util import skip_if_no_tf PRECISION_DECIMAL = 5 -def setup(): +@skip_if_no_tf +def test_setup(): + global tf, Transform2d, dtwavexfm2, dtwaveifm2 + tf = import_module('tensorflow') + dtcwt_tf = import_module('dtcwt.tf') + Transform2d = getattr(dtcwt_tf, 'Transform2d') + dtwavexfm2 = getattr(dtcwt_tf, 'dtwavexfm2') + dtwaveifm2 = getattr(dtcwt_tf, 'dtwaveifm2') + # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale", [ (2,False), (2,True), @@ -43,6 +50,7 @@ def test_2d_input(nlevels, include_scale): assert p.scales_ops[i].dtype == tf.float32 +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale", [ (2,False), (2,True), @@ -75,6 +83,7 @@ def test_apply_reshaping(nlevels, include_scale): assert p.scales_ops[i].dtype == tf.float32 +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale", [ (2,False), (2,True), @@ -106,6 +115,7 @@ def test_2d_input_tuple(nlevels, include_scale): +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale, batch_size", [ (2,False,None), (2,True,10), @@ -132,6 +142,7 @@ def test_batch_input(nlevels, include_scale, batch_size): assert p.scales_ops[i].dtype == tf.float32 +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale, batch_size", [ (2,False,None), (2,True,10), @@ -160,6 +171,7 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): assert Yscale[i].get_shape().as_list() == [batch_size, 2*extent, 2*extent] assert Yscale[i].dtype == tf.float32 +@skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale, channels", [ (2,False,5), (2,True,2), diff --git a/tests/test_tfrowdfilt.py b/tests/test_tfrowdfilt.py index 9e90649..b8c356e 100644 --- a/tests/test_tfrowdfilt.py +++ b/tests/test_tfrowdfilt.py @@ -1,24 +1,27 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") - from pytest import raises import numpy as np -import tensorflow as tf +from importlib import import_module from dtcwt.coeffs import biort, qshift -from dtcwt.tf.lowlevel import rowdfilt from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt +from .util import skip_if_no_tf import tests.datasets as datasets -def setup(): - global mandrill, mandrill_t +@skip_if_no_tf +def test_setup(): + global mandrill, mandrill_t, rowdfilt, tf + tf = import_module('tensorflow') + lowlevel = import_module('dtcwt.tf.lowlevel') + rowdfilt = getattr(lowlevel, 'rowdfilt') + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 @@ -26,28 +29,35 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) +@skip_if_no_tf def test_odd_filter(): with raises(ValueError): rowdfilt(mandrill_t, (-1,2,-1), (-1,2,1)) +@skip_if_no_tf def test_different_size(): with raises(ValueError): rowdfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) +@skip_if_no_tf def test_bad_input_size(): with raises(ValueError): rowdfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) +@skip_if_no_tf def test_good_input_size(): rowdfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) +@skip_if_no_tf def test_good_input_size_non_orthogonal(): rowdfilt(mandrill_t[:,:511,:], (1,1), (1,1)) +@skip_if_no_tf def test_output_size(): y_op = rowdfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0], mandrill.shape[1]/2) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] @@ -60,6 +70,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] @@ -69,6 +80,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] hb = qshift('qshift_c')[1] diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index 1bf28c6..7ee9c1a 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -1,22 +1,26 @@ import os import pytest -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF -pytest.mark.skipif(not HAVE_TF, reason="Tensorflow not present") import numpy as np -import tensorflow as tf +from importlib import import_module from dtcwt.coeffs import biort, qshift -from dtcwt.tf.lowlevel import rowfilter from dtcwt.numpy.lowlevel import colfilter as np_colfilter +from .util import skip_if_no_tf import tests.datasets as datasets -def setup(): - global mandrill, mandrill_t +@skip_if_no_tf +def test_setup(): + global mandrill, mandrill_t, rowfilter, tf + tf = import_module('tensorflow') + lowlevel = import_module('dtcwt.tf.lowlevel') + rowfilter = getattr(lowlevel, 'rowfilter') + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) +@skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) assert mandrill.min() >= 0 @@ -24,24 +28,29 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) +@skip_if_no_tf def test_odd_size(): y_op = rowfilter(mandrill_t, [-1, 2, -1]) assert y_op.get_shape()[1:] == mandrill.shape +@skip_if_no_tf def test_even_size(): y_op = rowfilter(mandrill_t, [-1, -1]) assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) +@skip_if_no_tf def test_qshift(): h = qshift('qshift_a')[0] y_op = rowfilter(mandrill_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) +@skip_if_no_tf def test_biort(): h = biort('antonini')[0] y_op = rowfilter(mandrill_t, h) assert y_op.get_shape()[1:] == mandrill.shape +@skip_if_no_tf def test_even_size(): h = tf.constant([-1,1], dtype=tf.float32) zero_t = tf.zeros([1, *mandrill.shape], tf.float32) @@ -51,6 +60,7 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) +@skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): h = qshift('qshift_b')[0] @@ -62,6 +72,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] ref = np_colfilter(mandrill.T, h).T @@ -70,6 +81,7 @@ def test_equal_numpy_biort1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_biort2(): h = biort('near_sym_b')[0] im = mandrill[15:307, 40:267] @@ -80,6 +92,7 @@ def test_equal_numpy_biort2(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] ref = np_colfilter(mandrill.T, h).T @@ -88,6 +101,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) +@skip_if_no_tf def test_equal_numpy_qshift2(): h = qshift('qshift_c')[0] im = mandrill[15:307, 40:267] diff --git a/tests/tf-requirements.txt b/tests/tf-requirements.txt new file mode 100644 index 0000000..340bc51 --- /dev/null +++ b/tests/tf-requirements.txt @@ -0,0 +1,2 @@ +# Tensorflow specific requirements +tensorflow diff --git a/tests/util.py b/tests/util.py index 7db65d9..c377c8e 100644 --- a/tests/util.py +++ b/tests/util.py @@ -3,7 +3,7 @@ import pytest from dtcwt.opencl.lowlevel import _HAVE_CL as HAVE_CL -from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF +from dtcwt.utils import _HAVE_TF as HAVE_TF from six.moves import xrange diff --git a/tox.ini b/tox.ini index fd9479f..437f386 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py{27,3}{,-opencl},docs +envlist=py{27,3}{,-opencl,-tf},docs [testenv:docs] deps= @@ -18,4 +18,5 @@ commands= # We can't list these in deps since pyopencl moans if numpy is not # fully installed at pip-install time. py{27,3}-opencl: pip install -rtests/opencl-requirements.txt + py{27,3}-tf: pip install -rtests/tf-requirements.txt py.test --cov=dtcwt/ --cov-report=term {posargs} From 09e4eca07c1ef1e05250e81bc2ee8c26798e1c3a Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 15 May 2017 13:24:13 +0100 Subject: [PATCH 29/52] Added readme about tf operation --- README_tf.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 README_tf.md diff --git a/README_tf.md b/README_tf.md new file mode 100644 index 0000000..df981a5 --- /dev/null +++ b/README_tf.md @@ -0,0 +1,76 @@ +# Using GPU acceleration with tensorflow for the DTCWT + +Normally you would import the dtcwt library and set up a forward transform. +E.g. +```python +import dtcwt +t = dtcwt.Transform2d(biort='near_sym_a',qshift='qshift_b') +p = t.forward(X, nlevels) +low, highs = p.lowpass, p.highpasses +``` +To use the tensorflow acceleration, you must however import the specific +module, and use the slightly modified functions. E.g. +```python +import dtcwt.tf +t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') +p = t.forward(X, nlevels) +low, highs = p.lowpass, p.highpasses +``` +In this instance, X was a numpy array. The library will create all the ops on +the graph, and feed the numpy array into it, create a session and evaluate it. +This provides little advantage over the straightforward numpy operation, but is +there for compatability. + +For real speed-up, you want to feed batches of images into the library. An +example would be: +```python +import dtcwt.tf +t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') +imgs = tf.placeholder(tf.float32, [None, 100,100]) +p = t.forward(imgs, nlevels) +low_op, high_ops = p.lowpass_op, p.highpasses_ops +sess = tf.Session() +low = sess.run(low_op, {imgs:X}) +``` +Having to evaluate each op independently would be quite annoying, so I've made +a helpful routine for it, called eval_fwd +```python +import dtcwt.tf +t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') +imgs = tf.placeholder(tf.float32, [None, 100,100]) +p_tf = t.forward(imgs, nlevels) # returns a dtcwt.Pyramid_tf object +sess = tf.Session() +X = np.random.randn(10,100,100) +p = p_tf.eval_fwd(X) # returns a dtcwt.Pyramid object +lows, highs = p.lowpass, p.highpasses +assert lows.shape[0] == 10 +``` +In this example, the returned pyramid object, p, now has a batch of lowpass and +highpasses. + +For added help, the forward transform can also accept channels of inputs (where +the regular dtcwt only accepts single channel input) through a special module +called forward_channels. At this point, it is likely you will not be wanting to +handle a pyramid, so instead this function returns a tuple of tensors. The tuple will be +formed of: + +(lowpass, (highpass[0], highpass[1], ... highpass[nlevels-1])), + +or if the include_scale option is true, then: + +(lowpass, (highpass[0], highpass[1], ... highpass[nlevels-1])), + (scale[0], scale[1], ... scale[nlevels-1])) + +i.e. +```python +import dtcwt.tf +t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') +imgs = tf.placeholder(tf.float32, [None, 100,100,3]) +yl,yh,yscale = t.forward_channels(imgs, nlevels,include_scale=True) +sess = tf.Session() +X = np.random.randn(10,100,100,3) +lows = sess.run(yl, {imgs:X}) +``` + + + From 8e230905dbfb5cc8fb496e53811f1be260ce4413 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 29 May 2017 01:09:32 +0100 Subject: [PATCH 30/52] Added inverse channels function --- dtcwt/tf/common.py | 86 +++-- dtcwt/tf/transform2d.py | 671 +++++++++++++++++++++++------------- tests/test_tfTransform2d.py | 213 ++++++++++-- 3 files changed, 652 insertions(+), 318 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 27f36c5..c876c61 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -25,15 +25,15 @@ class Pyramid_tf(object): .. py:attribute:: X A placeholder which the user can use when they want to evaluate the - forward dtcwt. + forward dtcwt. .. py:attribute:: lowpass_op A tensorflow tensor that can be evaluated in a session to return - the coarsest scale lowpass signal for the input, X. + the coarsest scale lowpass signal for the input, X. .. py:attribute:: highpasses_op A tuple of tensorflow tensors, where each element is the complex subband coefficients for corresponding scales finest to coarsest. .. py:attribute:: scales - *(optional)* A tuple where each element is a tensorflow tensor + *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. .. py:method:: apply_reshaping(fn) @@ -60,16 +60,21 @@ def _get_lowpass(self, data, sess=None): if self.lowpass_op is None: return None + close_after = False if sess is None: sess = tf.Session(graph=self.graph) + close_after = True + + try: + y = sess.run(self.lowpass_op, {self.X : data}) + except ValueError: + y = sess.run(self.lowpass_op, {self.X : [data]})[0] + + if close_after: + sess.close() - with sess: - try: - y = sess.run(self.lowpass_op, {self.X : data}) - except ValueError: - y = sess.run(self.lowpass_op, {self.X : [data]})[0] return y - + def _get_highpasses(self, data, sess=None): if self.highpasses_ops is None: return None @@ -78,13 +83,13 @@ def _get_highpasses(self, data, sess=None): sess = tf.Session(graph=self.graph) with sess: - try: + try: y = tuple( - [sess.run(layer_hp, {self.X : data}) + [sess.run(layer_hp, {self.X : data}) for layer_hp in self.highpasses_ops]) except ValueError: y = tuple( - [sess.run(layer_hp, {self.X : [data]})[0] + [sess.run(layer_hp, {self.X : [data]})[0] for layer_hp in self.highpasses_ops]) return y @@ -92,38 +97,47 @@ def _get_scales(self, data, sess=None): if self.scales_ops is None: return None + close_after = False if sess is None: sess = tf.Session(graph=self.graph) - - with sess: - try: - y = tuple( - sess.run(layer_scale, {self.X : data}) - for layer_scale in self.scales_ops) - except ValueError: - y = tuple( - sess.run(layer_scale, {self.X : [data]})[0] - for layer_scale in self.scales_ops) + close_after = True + + try: + y = tuple( + sess.run(layer_scale, {self.X : data}) + for layer_scale in self.scales_ops) + except ValueError: + y = tuple( + sess.run(layer_scale, {self.X : [data]})[0] + for layer_scale in self.scales_ops) + + if close_after: + sess.close() return y def _get_X(self, Yl, Yh, sess=None): if self.X is None: return None + close_after = False if sess is None: sess = tf.Session(graph=self.graph) + close_after = True + + try: + # Use dictionary comprehension to feed in our Yl and our + # multiple layers of Yh + data = [Yl, *list(Yh)] + placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + X = sess.run(self.X, {i : d for i,d in zip(placeholders,data)}) + except ValueError: + data = [Yl, *list(Yh)] + placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] + + if close_after: + sess.close() - with sess: - try: - # Use dictionary comprehension to feed in our Yl and our - # multiple layers of Yh - data = [Yl, *list(Yh)] - placeholders = [self.lowpass_op, *list(self.highpasses_ops)] - X = sess.run(self.X, {i : d for i,d in zip(placeholders,data)}) - except ValueError: - data = [Yl, *list(Yh)] - placeholders = [self.lowpass_op, *list(self.highpasses_ops)] - X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] return X @@ -153,7 +167,7 @@ def eval_fwd(self, X, sess=None): transform. :param sess: Tensorflow session to use. If none is provided a temporary session will be used. - + :returns: A :py:class:`dtcwt.Pyramid` of the data. The variables in this pyramid will typically be only 2-dimensional (when calling the numpy forward transform), but these will be 3 dimensional. @@ -166,7 +180,7 @@ def eval_fwd(self, X, sess=None): def eval_inv(self, Yl, Yh, sess=None): """ A helper function to evaluate the inverse transform on given wavelet - coefficients. + coefficients. :param Yl: A numpy array of shape [, h/(2**scale), w/(2**scale)], where (h,w) was the size of the input image. @@ -179,7 +193,7 @@ def eval_inv(self, Yl, Yh, sess=None): :param sess: Tensorflow session to use. If none is provided a temporary session will be used. - :returns: A numpy array of the inverted data. + :returns: A numpy array of the inverted data. """ return self._get_X(Yl, Yh, sess) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 8ae815b..6c80b03 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -37,21 +37,27 @@ def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, gain_mask=Non class Transform2d(Transform2dNumPy): """ - An implementation of the 2D DT-CWT via Tensorflow. + An implementation of the 2D DT-CWT via Tensorflow. + *biort* and *qshift* are the wavelets which parameterise the transform. If *biort* or *qshift* are strings, they are used as an argument to the :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions. Otherwise, they are interpreted as tuples of vectors giving filter coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). - - Creating an object of this class loads the necessary filters onto the - tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with - a placeholder will create a forward transform for an input of the placeholder's - size. You can evaluate the resulting ops several times feeding different - images into the placeholder *assuming* they have the same resolution. For - a different resolution image, call the :py:func:`Transform2d.forward` - function again. + + Creating an object of this class loads the necessary filters onto the + tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with + a placeholder will create a forward transform for an input of the + placeholder's size. You can evaluate the resulting ops several times + feeding different images into the placeholder *assuming* they have the same + resolution. For a different resolution image, call the + :py:func:`Transform2d.forward` function again. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 """ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): @@ -61,121 +67,49 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): self.forward_graphs = {} self.inverse_graphs = {} - def _find_forward_graph(self, shape): ''' See if we can reuse an old graph for the forward transform ''' find_key = '{}x{}'.format(shape[0], shape[1]) - for key,val in self.forward_graphs.items(): + for key, val in self.forward_graphs.items(): if find_key == key: return val return None - def _add_forward_graph(self, p_ops, shape): ''' Keep record of the pyramid so we can use it later if need be ''' find_key = '{}x{}'.format(shape[0], shape[1]) self.forward_graphs[find_key] = p_ops - def _find_inverse_graph(self, Lo_shape, nlevels): ''' See if we can reuse an old graph for the inverse transform ''' find_key = '{}x{}'.format(Lo_shape[0], Lo_shape[1]) - for key,val in self.forward_graphs.items(): + for key, val in self.forward_graphs.items(): if find_key == key: return val return None - def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): ''' Keep record of the pyramid so we can use it later if need be ''' find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) self.inverse_graphs[find_key] = p_ops - - def forward_channels(self, X, nlevels=3, include_scale=False): - ''' - Perform a forward transform on an image with multiplice channels. - Must provide with a tensorflow variable or placeholder (unlike the more - general :py:method:`Transform2d.forward`). - :param X: Input image which you wish to transform. Input must be of - shape [batch, height, width, channels]. - :param nlevels: Number of levels of the dtcwt transform to calculate. - :param include_scale: Whether or not to return the lowpass results at - each sclae of the transform, or only at the highest scale (as is custom - for multiresolution analysis) - :returns: A tuple of Yl, Yh, Yscale. - The Yl corresponds to the lowpass - of the image, and has shape [batch, height, width, channels] of type - tf.float32. - Yh corresponds to the highpasses for the image, and is a list of length - nlevels, with each entry having shape [batch, height', width', - channels, 6] of type tf.complex64. - Yscale corresponds to the lowpass outputs at each scale of the - transform, and is a list of length nlevels, with each entry having - shape [batch, height', width', channels] of type tf.float32. + def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): ''' - if not tf.is_numeric_tensor(X): - raise ValueError( - '''The provided input must be a tensorflow variable or placeholder''') - else: - X_shape = X.get_shape().as_list() - if len(X_shape) != 4: - raise ValueError('''The entered variable has incorrect dimensions {}. - It must be of shape [batch, height, width, channels] - (batch can be None).'''.format(X_shape)) - - original_size = X.get_shape().as_list()[1:-1] - size = '{}x{}'.format(original_size[0], original_size[1]) - name = 'dtcwt_fwd_{}'.format(size) - with tf.name_scope(name): - # Put the channel axis first - X = tf.transpose(X, perm=[3,0,1,2]) - f = lambda x: self._forward_ops(x, nlevels, include_scale, - return_tuple=True) - - # Calculate the dtcwt for each of the channels independently - # This will return tensors of shape: - # Yl: [c, batch, height, width] - # Yh: list of length nlevels, each of shape [c, batch, height, width, 6] - # Yscale: list of length nlevels, each of shape [c, batch, height, width] - if include_scale: - shape = (tf.float32, # lowpass object - tuple(tf.complex64 for k in range(nlevels)), # highpasses - tuple(tf.float32 for k in range(nlevels))) - Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) - # Transpose the tensors to put the channel after the batch - Yl = tf.transpose(Yl, perm=[1,2,3,0]) - Yh = tuple( - [tf.transpose(x, perm=[1,2,3,0,4]) for x in Yh]) - Yscale = tuple( - [tf.transpose(x, perm=[1,2,3,0]) for x in Yscale]) - return Yl, Yh, Yscale - - else: - shape = (tf.float32, - tuple(tf.complex64 for k in range(nlevels))) - Yl, Yh = tf.map_fn(f, X, dtype=shape) - # Transpose the tensors to put the channel after the batch - Yl = tf.transpose(Yl, perm=[1,2,3,0]) - Yh = tuple( - [tf.transpose(x, perm=[1,2,3,0,4]) for x in Yh]) - return Yl, Yh + Perform a forward transform on an image. + Can provide the forward transform with either an np array (naive + usage), or a tensorflow variable or placeholder (designed usage). - def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): - ''' - Perform a forward transform on an image. Can provide the forward - transform with either an np array (naive usage), or a tensorflow - variable or placeholder (designed usage). :param X: Input image which you wish to transform. Can be a numpy - array, tensorflow Variable or Tensorflow placeholder. See comments - below. + array, tensorflow Variable or tensorflow placeholder. See comments + below. :param nlevels: Number of levels of the dtcwt transform to calculate. :param include_scale: Whether or not to return the lowpass results at - each sclae of the transform, or only at the highest scale (as is custom - for multiresolution analysis) + each scale of the transform, or only at the highest scale (as is + custom for multiresolution analysis) :param return_tuple: If true, returns a tuple of lowpass, highpasses - and scales (if include_scale is True) rather than a Pyramid object. + and scales (if include_scale is True), rather than a Pyramid object. + :returns: A :py:class:`Pyramid_tf` object or a :py:class:`Pyramid` object, depending on the type of input data provided. @@ -183,25 +117,30 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): If a numpy array is provided, the forward function will create a graph of the right size to match the input (or check if it has previously created one), and then feed the input into the graph and evaluate it. - This operation will return a :py:class:`Pyramid` object similar to + This operation will return a :py:class:`Pyramid` object similar to how running the numpy version would. If a tensorflow variable or placeholder is provided, the forward function will create a graph of the right size, and return a Pyramid_ops() object. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 ''' # Check if a numpy array was provided if not tf.is_numeric_tensor(X): X = np.atleast_2d(asfarray(X)) - if len(X.shape) >= 3: + if len(X.shape) >= 3: raise ValueError('''The entered variable has incorrect dimensions {}. If X is a numpy array (or any non tensorflow object), it must be of shape [height, width]. For colour images, please enter each channel separately. If you wish to enter a batch of images, please instead provide either a tf.Placeholder or a tf.Variable input of size [batch, height, width]. - '''.format(X.shape)) + '''.format(X.shape)) # Check if the ops already exist for an input of the given size p_ops = self._find_forward_graph(X.shape) @@ -221,23 +160,23 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): return p_ops.eval_fwd(X) # A tensorflow object was provided - else: + else: X_shape = X.get_shape().as_list() - if len(X_shape) > 3: - raise ValueError('''The entered variable has incorrect dimensions {}. + if len(X_shape) > 3: + raise ValueError( + '''The entered variable has incorrect dimensions {}. If X is a tf placeholder or variable, it must be of shape - [batch, height, width] (batch can be None) or + [batch, height, width] (batch can be None) or [height, width]. For colour images, please enter each - channel separately.'''.format(X_shape)) + channel separately.'''.format(X_shape)) # If a batch wasn't provided, add a none dimension and remove it # later if len(X_shape) == 2: - logging.warn('Fed with a 2d shape input. For efficient calculation' - + ' feed batches of inputs. Input was reshaped to' - + ' have a 1 in the first dimension.') - X = tf.expand_dims(X,axis=0) - + logging.warn('Fed with a 2d shape input. For efficient ' + + 'calculation feed batches of inputs. Input was ' + + 'reshaped to have a 1 in the first dimension.') + X = tf.expand_dims(X, axis=0) original_size = X.get_shape().as_list()[1:] size = '{}x{}'.format(original_size[0], original_size[1]) @@ -248,33 +187,155 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): return p_tf + def forward_channels(self, X, nlevels=3, include_scale=False, + data_format="nhwc"): + ''' + Perform a forward transform on an image with multiple channels. + + Must provide with a tensorflow variable or placeholder (unlike the more + general :py:method:`Transform2d.forward`). + + :param X: Input image which you wish to transform. + :param nlevels: Number of levels of the dtcwt transform to calculate. + :param include_scale: Whether or not to return the lowpass results at + each sclae of the transform, or only at the highest scale (as is + custom for multiresolution analysis) + :param data_format: An optional string of the form "nchw" or "nhwc", + specifying the data format of the input. If format is "nchw" (the + default), then data is in the form [batch, channels, h, w]. If the + format is "nhwc", then the data is in the form [batch, h, w, c]. + + :returns: A tuple of (Yl, Yh, Yscale). The order of output axes + will match the input axes (i.e. the position of the channel + dimension). I.e. (note that the spatial sizes will change) + Yl: [batch, c, h, w] OR [batch, h, w, c] + Yh: [batch, c, h, w, 6] OR [batch, h, w, c, 6] + Yscale: [batch, c, h, w, 6] OR [batch, h, w, c, 6] + + Yl corresponds to the lowpass of the image, and has shape + [batch, channels, height, width] of type tf.float32. + Yh corresponds to the highpasses for the image, and is a list of length + nlevels, with each entry having shape + [batch, channels, height', width', 6] of type tf.complex64. + Yscale corresponds to the lowpass outputs at each scale of the + transform, and is a list of length nlevels, with each entry having + shape [batch, channels, height', width'] of type tf.float32. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 + ''' + data_format = data_format.lower() + if data_format != "nchw" and data_format != "nhwc": + raise ValueError('The data format must be either "ncwh" or ' + + '"nhwc", not {}'.format(data_format)) + if not tf.is_numeric_tensor(X): + raise ValueError('The provided input must be a tensorflow ' + + 'variable or placeholder') + else: + X_shape = X.get_shape().as_list() + if len(X_shape) != 4: + raise ValueError( + '''The entered variable has incorrect dimensions {}. + It must be of shape [batch, channels, height, width] + (batch can be None).'''.format(X_shape)) + + original_size = X.get_shape().as_list()[1:-1] + size = '{}x{}'.format(original_size[0], original_size[1]) + name = 'dtcwt_fwd_{}'.format(size) + with tf.name_scope(name): + # Put the channel axis first + if data_format == "nhwc": + X = tf.transpose(X, perm=[3, 0, 1, 2]) + else: + X = tf.transpose(X, perm=[1, 0, 2, 3]) + + f = lambda x: self._forward_ops(x, nlevels, include_scale, + return_tuple=True) + + # Calculate the dtcwt for each of the channels independently + # This will return tensors of shape: + # Yl: A tensor of shape [c, batch, h', w'] + # Yh: list of length nlevels, each of shape + # [c, batch, h'', w'', 6] + # Yscale: list of length nlevels, each of shape + # [c, batch, h''', w'''] + if include_scale: + # (lowpass, highpasses, scales) + shape = (tf.float32, + tuple(tf.complex64 for k in range(nlevels)), + tuple(tf.float32 for k in range(nlevels))) + Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) + # Transpose the tensors to put the channel after the batch + if data_format == "nhwc": + Yl = tf.transpose(Yl, perm=[1, 2, 3, 0]) + Yh = tuple( + [tf.transpose(x, perm=[1, 2, 3, 0, 4]) for x in Yh]) + Yscale = tuple( + [tf.transpose(x, perm=[1, 2, 3, 0]) + for x in Yscale]) + else: + Yl = tf.transpose(Yl, perm=[1, 0, 2, 3]) + Yh = tuple( + [tf.transpose(x, perm=[1, 0, 2, 3, 4]) for x in Yh]) + Yscale = tuple( + [tf.transpose(x, perm=[1, 0, 2, 3]) + for x in Yscale]) + + return Yl, Yh, Yscale + + else: + shape = (tf.float32, + tuple(tf.complex64 for k in range(nlevels))) + Yl, Yh = tf.map_fn(f, X, dtype=shape) + # Transpose the tensors to put the channel after the batch + if data_format == "nhwc": + Yl = tf.transpose(Yl, perm=[1, 2, 3, 0]) + Yh = tuple( + [tf.transpose(x, perm=[1, 2, 3, 0, 4]) for x in Yh]) + else: + Yl = tf.transpose(Yl, perm=[1, 0, 2, 3]) + Yh = tuple( + [tf.transpose(x, perm=[1, 0, 2, 3, 4]) for x in Yh]) + + return Yl, Yh + def inverse(self, pyramid, gain_mask=None): ''' - Perform an inverse transform on an image. Can provide the inverse - transform with either an np array (naive usage), or a tensorflow - variable or placeholder (designed usage). + Perform an inverse transform on an image. + + Can provide the inverse transform with either an np array (naive + usage), or a tensorflow variable or placeholder (designed usage). :param pyramid: A :py:class:`dtcwt.Pyramid` or - `:py:class:`dtcwt.tf.Pyramid_tf` like class holding the transform - domain representation to invert + `:py:class:`dtcwt.tf.Pyramid_tf` like class holding the transform + domain representation to invert :param gain_mask: Gain to be applied to each subband. Should have shape - [6, nlevels]. + [6, nlevels]. + :returns: Either a tf.Variable or a numpy array compatible with the - reconstruction. A tf.Variable is returned if the pyramid input was - a Pyramid_tf class. If it wasn't, then, we return a numpy array (note - that this is inefficient, as in both cases we have to construct the - graph - in the second case, we then execute it and discard it). + reconstruction. + + A tf.Variable is returned if the pyramid input was a Pyramid_tf class. + If it wasn't, then, we return a numpy array (note that this is + inefficient, as in both cases we have to construct the graph - in the + second case, we then execute it and discard it). - The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction - *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for - band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are - zero-indexed. + The (*d*, *l*)-th element of *gain_mask* is gain for subband with + direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is + performed for band (d,l). Default *gain_mask* is all ones. Note that + both *d* and *l* are zero-indexed. + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 ''' # Check if a numpy array was provided - if isinstance(pyramid, Pyramid_np) or (hasattr(pyramid, 'lowpass') - and hasattr(pyramid, 'highpasses')): + if isinstance(pyramid, Pyramid_np) or \ + hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): Yl, Yh = pyramid.lowpass, pyramid.highpasses @@ -284,13 +345,14 @@ def inverse(self, pyramid, gain_mask=None): # If not, create a graph if p_ops is None: - Lo_ph = tf.placeholder(tf.float32, [None, Yl.shape[0], Yl.shape[1]]) + Lo_ph = tf.placeholder(tf.float32, + [None, Yl.shape[0], Yl.shape[1]]) Hi_ph = tuple( - tf.placeholder(tf.complex64, [None, *level.shape]) - for level in Yh) + tf.placeholder(tf.complex64, [None, *level.shape]) + for level in Yh) p_in = Pyramid_tf(None, Lo_ph, Hi_ph) size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) - name = 'dtcwt_inv_{}'.format(size) + name = 'dtcwt_inv_{}'.format(size) with self.np_graph.name_scope(name): p_ops = self._inverse_ops(p_in, gain_mask) @@ -303,7 +365,7 @@ def inverse(self, pyramid, gain_mask=None): return p_ops.eval_inv(Yl, Yh) # A tensorflow object was provided - elif isinstance(pyramid, Pyramid_tf): + elif isinstance(pyramid, Pyramid_tf): s = pyramid.lowpass_op.get_shape().as_list()[1:] nlevels = len(pyramid.highpasses_ops) size = '{}x{}_up_{}'.format(s[0], s[1], nlevels) @@ -311,25 +373,129 @@ def inverse(self, pyramid, gain_mask=None): with tf.name_scope(name): return self._inverse_ops(pyramid, gain_mask) else: - raise ValueError('''Unknown pyramid provided to inverse transform''') - + raise ValueError( + '''Unknown pyramid provided to inverse transform''') + + def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): + ''' + Perform an inverse transform on an image with multiple channels. + + Must provide with a tensorflow variable or placeholder (unlike the more + general :py:method:`Transform2d.inverse`). + + :param Yl: Lowpass data. + :param Yh: A list-like structure of length nlevels. At each level, the + tensor should be of size [batch, h', w', c, 6] and of tf.complex64 + type. The sizes must match up with what would be created from the + forward transform. + :param gain_mask: Gain to be applied to each subband. Should have shape + [6, nlevels]. + :param data_format: An optional string of the form "nchw" or "nhwc", + specifying the data format of the input. If format is "nchw" (the + default), then data are in the form [batch, channels, h, w] for Yl + and [batch, channels, h, w, 6] for Yh. If the format is "nhwc", then + the data are in the form [batch, h, w, c] for Yl and + [batch, h, w, c, 6] for Yh. + + :returns: A tf.Variable, X, compatible with the reconstruction. + + The (*d*, *l*)-th element of *gain_mask* is gain for subband with + direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is + performed for band (d,l). Default *gain_mask* is all ones. Note that + both *d* and *l* are zero-indexed. + + .. codeauthor:: Fergal Cotter , Feb 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 + .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 + ''' + # Input checking + data_format = data_format.lower() + if data_format != "nchw" and data_format != "nhwc": + raise ValueError('The data format must be either "ncwh" or ' + + '"nhwc", not {}'.format(data_format)) + if data_format == "nhwc": + channel_ax = 3 + else: + channel_ax = 1 + + if not tf.is_numeric_tensor(Yl): + raise ValueError('The provided lowpass input must be a ' + + 'tensorflow variable or placeholder') + Yl_shape = Yl.get_shape().as_list() + if len(Yl_shape) != 4: + raise ValueError( + '''The entered lowpass variable has incorrect dimensions {}. + for data_format of {}.'''.format(Yl_shape, data_format)) + + for scale in Yh: + if not tf.is_numeric_tensor(scale): + raise ValueError('The provided highpass inputs must be a ' + + 'tensorflow variable or placeholder') + if scale.dtype != tf.complex64: + raise ValueError('The provided highpass inputs must be ' + + 'complex numbers of 32 point precision.') + Yh_shape = scale.get_shape().as_list() + if len(Yh_shape) != 5 or Yh_shape[-1] != 6: + raise ValueError( + '''The entered highpass variable has incorrect dimensions {} + for data_format of {}.'''.format(Yh_shape, data_format)) + + # Move all of the channels into the batch dimension for the lowpass + # input. This may involve transposing, depending on the data format + s = Yl.get_shape().as_list() + num_channels = s[channel_ax] + nlevels = len(Yh) + if data_format == "nhwc": + size = '{}x{}_up_{}'.format(s[1], s[2], nlevels) + Yl_new = tf.transpose(Yl, [0, 3, 1, 2]) + Yl_new = tf.reshape(Yl_new, [-1, s[1], s[2]]) + else: + size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) + Yl_new = tf.reshape(Yl, [-1, s[2], s[3]]) + + # Move all of the channels into the batch dimension for the highpass + # input. This may involve transposing, depending on the data format + Yh_new = [] + for scale in Yh: + s = scale.get_shape().as_list() + if s[channel_ax] != num_channels: + raise ValueError( + '''The number of channels has to be consistent for all + inputs across the channel axis {}. You fed in Yl: {} + and Yh: {}'''.format(channel_ax, Yl, Yh)) + if data_format == "nhwc": + scale = tf.transpose(scale, [0, 3, 1, 2, 4]) + Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) + else: + Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) + + pyramid = Pyramid_tf(None, Yl_new, Yh_new) + + name = 'dtcwt_inv_{}_{}channels'.format(size, num_channels) + with tf.name_scope(name): + P = self._inverse_ops(pyramid, gain_mask) + s = P.X.get_shape().as_list() + X = tf.reshape(P.X, [-1, num_channels, s[1], s[2]]) + if data_format == "nhwc": + X = tf.transpose(X, [0, 2, 3, 1]) + return X def _forward_ops(self, X, nlevels=3, include_scale=False, - return_tuple=False): - """Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. - :param X: 3D real array of size [Batch, rows, cols] + return_tuple=False): + """ + Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. + + :param X: 3D real array of size [batch, h, w] :param nlevels: Number of levels of wavelet decomposition - :param include_scale: True if you want to receive the lowpass coefficients at - intermediate layers. + :param include_scale: True if you want to receive the lowpass + coefficients at intermediate layers. :param return_tuple: If true, instead of returning - a :py:class`dtcwt.Pyramid_tf` object, return a tuple of (lowpass, - highpasses, scales) + a :py:class`dtcwt.Pyramid_tf` object, return a tuple of + (lowpass, highpasses, scales) + :returns: A :py:class:`dtcwt.Pyramid_tf` compatible - object representing the transform-domain signal .. codeauthor:: Fergal - Cotter , Feb 2017 - .. codeauthor:: Rich Wareham , Aug 2013 - .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 - .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 + object representing the transform-domain signal """ # If biort has 6 elements instead of 4, then it's a modified @@ -354,33 +520,36 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, # Check the shape and form of the input if not tf.is_numeric_tensor(X): - raise ValueError('''Please provide the forward function with - a tensorflow placeholder or variable of size [batch, width, - height] (batch can be None if you do not wish to specify it).''') + raise ValueError( + '''Please provide the forward function with a tensorflow + placeholder or variable of size [batch, width, height] (batch + can be None if you do not wish to specify it).''') original_size = X.get_shape().as_list()[1:] if len(original_size) >= 3: - raise ValueError('The entered variable has too many dimensions {}. If ' - 'the final dimension are colour channels, please enter each ' + - 'channel separately.'.format(original_size)) + raise ValueError( + '''The entered variable has too many dimensions {}. If + the final dimension are colour channels, please enter each + channel separately.'''.format(original_size)) # Save the input placeholder/variable X_in = X - ############################## Resize ################################# - # The next few lines of code check to see if the image is odd in size, - # if so an extra ... row/column will be added to the bottom/right of the + + # ############################ Resize ################################# + # The next few lines of code check to see if the image is odd in size, + # if so an extra ... row/column will be added to the bottom/right of the # image - initial_row_extend = 0 + initial_row_extend = 0 initial_col_extend = 0 - # If the row count of X is not divisible by 2 then we need to + # If the row count of X is not divisible by 2 then we need to # extend X by adding a row at the bottom if original_size[0] % 2 != 0: bottom_row = tf.slice(X, [0, original_size[0] - 1, 0], [-1, 1, -1]) X = tf.concat([X, bottom_row], axis=1) initial_row_extend = 1 - # If the col count of X is not divisible by 2 then we need to + # If the col count of X is not divisible by 2 then we need to # extend X by adding a col to the right if original_size[1] % 2 != 0: right_col = tf.slice(X, [0, 0, original_size[1] - 1], [-1, -1, 1]) @@ -398,17 +567,17 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, else: if return_tuple: return X_in, () - else: + else: return Pyramid_tf(X_in, X, ()) - - ############################ Initialise ############################### - Yh = [None,] * nlevels + # ########################### Initialise ############################### + Yh = [None, ] * nlevels if include_scale: - # this is only required if the user specifies a third output component. - Yscale = [None,] * nlevels + # This is only required if the user specifies a third output + # component. + Yscale = [None, ] * nlevels - ############################# Level 1 ################################# + # ############################ Level 1 ################################# # Uses the biorthogonal filters if nlevels >= 1: # Do odd top-level filters on cols. @@ -420,46 +589,40 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, # Do odd top-level filters on rows. LoLo = rowfilter(Lo, h0o) LoLo_shape = LoLo.get_shape().as_list()[1:] - + # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowfilter(Hi, h0o)) - + horiz = q2c(rowfilter(Hi, h0o)) + # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowfilter(Lo, h1o)) - + vertic = q2c(rowfilter(Lo, h1o)) + # Diagonal wavelet pair (45 & 135 degrees) if len(self.biort) >= 6: - diag = q2c(rowfilter(Ba, h2o)) + diag = q2c(rowfilter(Ba, h2o)) else: - diag = q2c(rowfilter(Hi, h1o)) - - # Pack all 6 tensors into one + diag = q2c(rowfilter(Hi, h1o)) + + # Pack all 6 tensors into one Yh[0] = tf.stack( [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], axis=3) - + if include_scale: Yscale[0] = LoLo - - - ############################# Level 2+ ################################ - # Uses the qshift filters + + # ############################ Level 2+ ################################ + # Uses the qshift filters for level in xrange(1, nlevels): row_size, col_size = LoLo_shape[0], LoLo_shape[1] # If the row count of LoLo is not divisible by 4 (it will be # divisible by 2), add 2 extra rows to make it so if row_size % 4 != 0: - LoLo = tf.pad(LoLo, [[0, 0], [1, 1], [0, 0]], 'SYMMETRIC') - #top_row = tf.slice(LoLo, [0, 0, 0], [-1, 1, -1]) - #bottom_row = tf.slice(LoLo, [0, row_size - 1, 0], [-1, 1, -1]) - #LoLo = tf.concat([top_row, LoLoLoLo, bottom_row], axis=1) + LoLo = tf.pad(LoLo, [[0, 0], [1, 1], [0, 0]], 'SYMMETRIC') # If the col count of LoLo is not divisible by 4 (it will be # divisible by 2), add 2 extra cols to make it so if col_size % 4 != 0: - LoLo = tf.pad(LoLo, [[0, 0], [0, 0], [1, 1]], 'SYMMETRIC') - #right_col = tf.slice(LoLo, [0, 0, col_size - 2], [-1, -1, 2]) - #LoLo = tf.concat([LoLo, right_col], axis=2) + LoLo = tf.pad(LoLo, [[0, 0], [0, 0], [1, 1]], 'SYMMETRIC') # Do even Qshift filters on cols. Lo = coldfilt(LoLo, h0b, h0a) @@ -469,36 +632,37 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, # Do even Qshift filters on rows. LoLo = rowdfilt(Lo, h0b, h0a) - LoLo_shape = LoLo.get_shape().as_list()[1:3] - + LoLo_shape = LoLo.get_shape().as_list()[1:3] + # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowdfilt(Hi, h0b, h0a)) - + horiz = q2c(rowdfilt(Hi, h0b, h0a)) + # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowdfilt(Lo, h1b, h1a)) - + vertic = q2c(rowdfilt(Lo, h1b, h1a)) + # Diagonal wavelet pair (45 & 135 degrees) if len(self.qshift) >= 12: - diag = q2c(rowdfilt(Ba, h2b, h2a)) + diag = q2c(rowdfilt(Ba, h2b, h2a)) else: - diag = q2c(rowdfilt(Hi, h1b, h1a)) - - # Pack all 6 tensors into one + diag = q2c(rowdfilt(Hi, h1b, h1a)) + + # Pack all 6 tensors into one Yh[level] = tf.stack( [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], axis=3) - + if include_scale: Yscale[level] = LoLo - + Yl = LoLo - + if initial_row_extend == 1 and initial_col_extend == 1: logging.warn('The image entered is now a {0} NOT a {1}.'.format( 'x'.join(list(str(s) for s in extended_size)), 'x'.join(list(str(s) for s in original_size)))) logging.warn( - 'The bottom row and rightmost column have been duplicated, prior to decomposition.') + '''The bottom row and rightmost column have been duplicated, + prior to decomposition.''') if initial_row_extend == 1 and initial_col_extend == 0: logging.warn('The image entered is now a {0} NOT a {1}.'.format( @@ -512,7 +676,8 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, 'x'.join(list(str(s) for s in extended_size)), 'x'.join(list(str(s) for s in original_size)))) logging.warn( - 'The rightmost column has been duplicated, prior to decomposition.') + '''The rightmost column has been duplicated, prior to + decomposition.''') if include_scale: if return_tuple: @@ -524,23 +689,22 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, return Yl, tuple(Yh) else: return Pyramid_tf(X_in, Yl, tuple(Yh)) - def _inverse_ops(self, pyramid, gain_mask=None): """Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D reconstruction. - :param pyramid: A :py:class:`dtcwt.tf.Pyramid_tf`-like class holding the - transform domain representation to invert. + :param pyramid: A :py:class:`dtcwt.tf.Pyramid_tf`-like class holding the + transform domain representation to invert. :param gain_mask: Gain to be applied to each subband. :returns: A :py:class:`dtcwt.tf.Pyramid_tf` class which can be - evaluated to get the inverted signal, X. + evaluated to get the inverted signal, X. - The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction - *d* at level *l*. If gain_mask[d,l] == 0, no computation is performed for - band (d,l). Default *gain_mask* is all ones. Note that both *d* and *l* are - zero-indexed. + The (*d*, *l*)-th element of *gain_mask* is gain for subband with + direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is + performed for band (d,l). Default *gain_mask* is all ones. Note that + both *d* and *l* are zero-indexed. .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 @@ -551,10 +715,10 @@ def _inverse_ops(self, pyramid, gain_mask=None): Yl = pyramid.lowpass_op Yh = pyramid.highpasses_ops - a = len(Yh) # No of levels. + a = len(Yh) # No of levels. if gain_mask is None: - gain_mask = np.ones((6, a)) # Default gain_mask. + gain_mask = np.ones((6, a)) # Default gain_mask. gain_mask = np.array(gain_mask) @@ -574,17 +738,25 @@ def _inverse_ops(self, pyramid, gain_mask=None): if len(self.qshift) == 8: h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = self.qshift elif len(self.qshift) == 12: - h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b, h2a, h2b, g2a, g2b = self.qshift + h0a, h0b, g0a, g0b, h1a, h1b, \ + g1a, g1b, h2a, h2b, g2a, g2b = self.qshift else: raise ValueError('Qshift wavelet must have 12 or 8 components.') current_level = a Z = Yl - while current_level >= 2: # this ensures that for level 1 we never do the following - lh = c2q(Yh[current_level-1][:,:,:,0:6:5], gain_mask[[0, 5], current_level-1]) - hl = c2q(Yh[current_level-1][:,:,:,2:4:1], gain_mask[[2, 3], current_level-1]) - hh = c2q(Yh[current_level-1][:,:,:,1:5:3], gain_mask[[1, 4], current_level-1]) + # This ensures that for level 1 we never do the following + while current_level >= 2: + lh = c2q(Yh[current_level - 1][:, :, :, 0:6:5], + gain_mask[[0, 5], + current_level - 1]) + hl = c2q(Yh[current_level - 1][:, :, :, 2:4:1], + gain_mask[[2, 3], + current_level - 1]) + hh = c2q(Yh[current_level - 1][:, :, :, 1:5:3], + gain_mask[[1, 4], + current_level - 1]) # Do even Qshift filters on columns. y1 = colifilt(Z, g0b, g0a) + colifilt(lh, g1b, g1a) @@ -594,39 +766,48 @@ def _inverse_ops(self, pyramid, gain_mask=None): y2bp = colifilt(hh, g2b, g2a) # Do even Qshift filters on rows. - Z = tf.transpose(colifilt(tf.transpose(y1,perm=[0,2,1]), g0b, g0a) + - colifilt(tf.transpose(y2,perm=[0,2,1]), g1b, g1a) + - colifilt(tf.transpose(y2bp,perm=[0,2,1]), g2b, g2a), - perm=[0,2,1]) + Z = tf.transpose( + colifilt(tf.transpose(y1, perm=[0, 2, 1]), g0b, g0a) + + colifilt(tf.transpose(y2, perm=[0, 2, 1]), g1b, g1a) + + colifilt(tf.transpose(y2bp, perm=[0, 2, 1]), g2b, g2a), + perm=[0, 2, 1]) else: y2 = colifilt(hl, g0b, g0a) + colifilt(hh, g1b, g1a) # Do even Qshift filters on rows. - Z = tf.transpose(colifilt(tf.transpose(y1, perm=[0,2,1]), g0b, g0a) + - colifilt(tf.transpose(y2, perm=[0,2,1]), g1b, g1a), - perm=[0,2,1]) + Z = tf.transpose( + colifilt(tf.transpose(y1, perm=[0, 2, 1]), g0b, g0a) + + colifilt(tf.transpose(y2, perm=[0, 2, 1]), g1b, g1a), + perm=[0, 2, 1]) # Check size of Z and crop as required Z_r, Z_c = Z.get_shape().as_list()[1:3] - S_r, S_c = Yh[current_level-2].get_shape().as_list()[1:3] + S_r, S_c = Yh[current_level - 2].get_shape().as_list()[1:3] # check to see if this result needs to be cropped for the rows - if Z_r != S_r * 2: - Z = Z[:,1:-1,:] + if Z_r != S_r * 2: + Z = Z[:, 1:-1, :] # check to see if this result needs to be cropped for the cols - if Z_c != S_c*2: - Z = Z[:,:,1:-1] + if Z_c != S_c * 2: + Z = Z[:, :, 1:-1] # Assert that the size matches at this stage Z_r, Z_c = Z.get_shape().as_list()[1:3] - if Z_r != S_r * 2 or Z_c != S_c*2: - raise ValueError('Sizes of highpasses are not valid for DTWAVEIFM2') - + if Z_r != S_r * 2 or Z_c != S_c * 2: + raise ValueError( + 'Sizes of highpasses are not valid for DTWAVEIFM2') + current_level = current_level - 1 if current_level == 1: - lh = c2q(Yh[current_level-1][:,:,:,0:6:5],gain_mask[[0, 5],current_level-1]) - hl = c2q(Yh[current_level-1][:,:,:,2:4:1],gain_mask[[2, 3],current_level-1]) - hh = c2q(Yh[current_level-1][:,:,:,1:5:3],gain_mask[[1, 4],current_level-1]) + lh = c2q(Yh[current_level - 1][:, :, :, 0:6:5], + gain_mask[[0, 5], + current_level - 1]) + hl = c2q(Yh[current_level - 1][:, :, :, 2:4:1], + gain_mask[[2, 3], + current_level - 1]) + hh = c2q(Yh[current_level - 1][:, :, :, 1:5:3], + gain_mask[[1, 4], + current_level - 1]) # Do odd top-level filters on columns. y1 = colfilter(Z, g0o) + colfilter(lh, g1o) @@ -636,7 +817,8 @@ def _inverse_ops(self, pyramid, gain_mask=None): y2bp = colfilter(hh, g2o) # Do odd top-level filters on rows. - Z = rowfilter(y1, g0o) + rowfilter(y2, g1o) + rowfilter(y2bp, g2o) + Z = rowfilter(y1, g0o) + rowfilter(y2, g1o) + \ + rowfilter(y2bp, g2o) else: y2 = colfilter(hl, g0o) + colfilter(hh, g1o) @@ -645,7 +827,6 @@ def _inverse_ops(self, pyramid, gain_mask=None): return Pyramid_tf(Z, Yl, Yh) - def q2c(y): """ @@ -659,13 +840,14 @@ def q2c(y): # | | # c----d # Combine (a,b) and (d,c) to form two complex subimages. - a,b,c,d = y[:, 0::2, 0::2], y[:, 0::2,1::2], y[:, 1::2,0::2], y[:, 1::2,1::2] - - p = tf.complex(a/np.sqrt(2), b/np.sqrt(2)) # p = (a + jb) / sqrt(2) - q = tf.complex(d/np.sqrt(2), -c/np.sqrt(2)) # q = (d - jc) / sqrt(2) + a, b = y[:, 0::2, 0::2], y[:, 0::2, 1::2] + c, d = y[:, 1::2, 0::2], y[:, 1::2, 1::2] + + p = tf.complex(a / np.sqrt(2), b / np.sqrt(2)) # p = (a + jb) / sqrt(2) + q = tf.complex(d / np.sqrt(2), -c / np.sqrt(2)) # q = (d - jc) / sqrt(2) # Form the 2 highpasses in z. - return (p-q, p+q) + return (p - q, p + q) def c2q(w, gain): @@ -683,11 +865,11 @@ def c2q(w, gain): """ # Input has shape [batch, r, c, 2] - r,c = w.get_shape().as_list()[1:3] + r, c = w.get_shape().as_list()[1:3] sc = np.sqrt(0.5) * gain - P = w[:,:,:,0]*sc[0] + w[:,:,:,1]*sc[1] - Q = w[:,:,:,0]*sc[0] - w[:,:,:,1]*sc[1] + P = w[:, :, :, 0] * sc[0] + w[:, :, :, 1] * sc[1] + Q = w[:, :, :, 0] * sc[0] - w[:, :, :, 1] * sc[1] # Recover each of the 4 corners of the quads. x1 = tf.real(P) @@ -696,16 +878,15 @@ def c2q(w, gain): x4 = -tf.real(Q) # Stack 2 inputs of shape [batch, r, c] to [batch, r, 2, c] - x_rows1 = tf.stack([x1,x3], axis=2) + x_rows1 = tf.stack([x1, x3], axis=2) # Reshaping interleaves the results - x_rows1 = tf.reshape(x_rows1, [-1, 2*r, c]) + x_rows1 = tf.reshape(x_rows1, [-1, 2 * r, c]) # Do the same for the even columns - x_rows2 = tf.stack([x2,x4], axis=2) - x_rows2 = tf.reshape(x_rows2, [-1, 2*r, c]) + x_rows2 = tf.stack([x2, x4], axis=2) + x_rows2 = tf.reshape(x_rows2, [-1, 2 * r, c]) # Stack the two [batch, 2*r, c] tensors to [batch, 2*r, c, 2] x_cols = tf.stack([x_rows1, x_rows2], axis=-1) - y = tf.reshape(x_cols, [-1, 2*r, 2*c]) + y = tf.reshape(x_cols, [-1, 2 * r, 2 * c]) return y - diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 587cc37..41d47f2 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -6,16 +6,17 @@ import numpy as np from importlib import import_module from dtcwt.numpy import Transform2d as Transform2d_np -from dtcwt.numpy import Pyramid from dtcwt.coeffs import biort, qshift import tests.datasets as datasets from scipy import stats from .util import skip_if_no_tf +import time PRECISION_DECIMAL = 5 + @skip_if_no_tf -def test_setup(): +def setup(): global mandrill, in_p, pyramid_ops global tf, Transform2d, dtwavexfm2, dtwaveifm2 # Import the tensorflow modules @@ -25,7 +26,6 @@ def test_setup(): dtwavexfm2 = getattr(dtcwt_tf, 'dtwavexfm2') dtwaveifm2 = getattr(dtcwt_tf, 'dtwaveifm2') - mandrill = datasets.mandrill() in_p = tf.placeholder(tf.float32, [None, 512, 512]) f = Transform2d() @@ -33,6 +33,7 @@ def test_setup(): # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -40,25 +41,30 @@ def test_mandrill_loaded(): assert mandrill.max() <= 1 assert mandrill.dtype == np.float32 + @skip_if_no_tf def test_simple(): Yl, Yh = dtwavexfm2(mandrill) + @skip_if_no_tf def test_specific_wavelet(): Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), qshift=qshift('qshift_06')) + @skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_1d(): Yl, Yh = dtwavexfm2(mandrill[0,:]) + @skip_if_no_tf @pytest.mark.skip(reason='Not currently implemented') def test_3d(): with raises(ValueError): Yl, Yh = dtwavexfm2(np.dstack((mandrill, mandrill))) + @skip_if_no_tf def test_simple_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, include_scale=True) @@ -67,42 +73,53 @@ def test_simple_w_scale(): for x in Yscale: assert x is not None + @skip_if_no_tf def test_odd_rows(): Yl, Yh = dtwavexfm2(mandrill[:509,:]) + @skip_if_no_tf def test_odd_rows_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:], include_scale=True) + @skip_if_no_tf def test_odd_cols(): Yl, Yh = dtwavexfm2(mandrill[:,:509]) + @skip_if_no_tf def test_odd_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) + @skip_if_no_tf def test_odd_rows_and_cols(): Yl, Yh = dtwavexfm2(mandrill[:,:509]) + @skip_if_no_tf def test_odd_rows_and_cols_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill[:509,:509], include_scale=True) + @skip_if_no_tf def test_rot_symm_modified(): - # This test only checks there is no error running these functions, not that they work - Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) + # This test only checks there is no error running these functions, + # not that they work + Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', + qshift='qshift_b_bp', include_scale=True) Z = dtwaveifm2(Yl, Yh, biort='near_sym_b_bp', qshift='qshift_b_bp') + @skip_if_no_tf def test_0_levels(): Yl, Yh = dtwavexfm2(mandrill, nlevels=0) np.testing.assert_array_almost_equal(Yl, mandrill, PRECISION_DECIMAL) assert len(Yh) == 0 + @skip_if_no_tf def test_0_levels_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, nlevels=0, include_scale=True) @@ -110,6 +127,7 @@ def test_0_levels_w_scale(): assert len(Yh) == 0 assert len(Yscale) == 0 + @skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_input(): @@ -118,6 +136,7 @@ def test_integer_input(): Yl, Yh = dtwavexfm2([[1,2,3,4], [1,2,3,4]]) assert np.any(Yl != 0) + @skip_if_no_tf @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_perfect_recon(): @@ -126,7 +145,8 @@ def test_integer_perfect_recon(): A = np.array([[1,2,3,4], [5,6,7,8]], dtype=np.int32) Yl, Yh = dtwavexfm2(A) B = dtwaveifm2(Yl, Yh) - assert np.max(np.abs(A-B)) < 1e-5 + assert np.max(np.abs(A - B)) < 1e-5 + @skip_if_no_tf def test_mandrill_perfect_recon(): @@ -134,7 +154,8 @@ def test_mandrill_perfect_recon(): # array and reconstructed Yl, Yh = dtwavexfm2(mandrill) B = dtwaveifm2(Yl, Yh) - assert np.max(np.abs(mandrill-B)) < 1e-5 + assert np.max(np.abs(mandrill - B)) < 1e-5 + @skip_if_no_tf def test_float32_input(): @@ -143,10 +164,12 @@ def test_float32_input(): assert np.issubsctype(Yl.dtype, np.float32) assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) + @skip_if_no_tf def test_eval_fwd(): y = pyramid_ops.eval_fwd(mandrill) + @skip_if_no_tf def test_multiple_inputs(): y = pyramid_ops.eval_fwd(mandrill) @@ -157,6 +180,7 @@ def test_multiple_inputs(): for s3, s in zip(y3.scales, y.scales): assert s3.shape == (3, *s.shape) + @skip_if_no_tf @pytest.mark.parametrize("test_input,biort,qshift", [ (datasets.mandrill(),'antonini','qshift_a'), @@ -169,22 +193,22 @@ def test_results_match(test_input, biort, qshift): """ Compare forward transform with numpy forward transform for mandrill image """ - im=test_input + im = test_input f_np = Transform2d_np(biort=biort,qshift=qshift) - p_np = f_np.forward(im, include_scale=True) + p_np = f_np.forward(im, include_scale=True) - in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) + in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort,qshift=qshift) p_tf = f_tf.forward(in_p, include_scale=True).eval_fwd(im) - + np.testing.assert_array_almost_equal( - p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) + p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) [np.testing.assert_array_almost_equal( - h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in - zip(p_np.highpasses, p_tf.highpasses)] + h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in + zip(p_np.highpasses, p_tf.highpasses)] [np.testing.assert_array_almost_equal( - s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in - zip(p_np.scales, p_tf.scales)] + s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in + zip(p_np.scales, p_tf.scales)] @skip_if_no_tf @@ -198,21 +222,22 @@ def test_results_match(test_input, biort, qshift): def test_results_match_inverse(test_input,biort,qshift): im = test_input f_np = Transform2d_np(biort=biort, qshift=qshift) - p_np = f_np.forward(im, nlevels=4, include_scale=True) + p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np) - - # Use a zero input and the fwd transform to get the shape of + + # Use a zero input and the fwd transform to get the shape of # the pyramid easily in_ = tf.zeros([1, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(in_, nlevels=4, include_scale=True) - # Create ops for the inverse transform + # Create ops for the inverse transform pi_tf = f_tf.inverse(p_tf) X_tf = pi_tf.eval_inv(p_np.lowpass, p_np.highpasses) - + np.testing.assert_array_almost_equal( - X_np, X_tf, decimal=PRECISION_DECIMAL) + X_np, X_tf, decimal=PRECISION_DECIMAL) + @skip_if_no_tf @pytest.mark.parametrize("biort,qshift,gain_mask", [ @@ -226,31 +251,32 @@ def test_results_match_invmask(biort,qshift,gain_mask): im = mandrill f_np = Transform2d_np(biort=biort, qshift=qshift) - p_np = f_np.forward(im, nlevels=4, include_scale=True) + p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np, gain_mask) f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(im, nlevels=4, include_scale=True) X_tf = f_tf.inverse(p_tf, gain_mask) - + np.testing.assert_array_almost_equal( - X_np, X_tf, decimal=PRECISION_DECIMAL) + X_np, X_tf, decimal=PRECISION_DECIMAL) + @skip_if_no_tf -@pytest.mark.parametrize("test_input,biort,qshift", [ - (datasets.mandrill(),'antonini','qshift_06'), - (datasets.mandrill()[100:411,44:460],'near_sym_b','qshift_a'), - (datasets.mandrill(),'near_sym_b','qshift_c'), - (datasets.mandrill()[100:378,20:322],'near_sym_a','qshift_a'), - (datasets.mandrill(),'near_sym_b_bp', 'qshift_b_bp') +@pytest.mark.parametrize("test_input, biort, qshift", [ + (datasets.mandrill(), 'antonini', 'qshift_06'), + (datasets.mandrill()[100:411, 44:460], 'near_sym_b', 'qshift_a'), + (datasets.mandrill(), 'near_sym_b', 'qshift_c'), + (datasets.mandrill()[100:378, 20:322], 'near_sym_a', 'qshift_a'), + (datasets.mandrill(), 'near_sym_b_bp', 'qshift_b_bp') ]) -def test_results_match_endtoend(test_input,biort,qshift): +def test_results_match_endtoend(test_input, biort, qshift): im = test_input f_np = Transform2d_np(biort=biort, qshift=qshift) - p_np = f_np.forward(im, nlevels=4, include_scale=True) + p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np) - - in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) + + in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(in_p, nlevels=4, include_scale=True) pi_tf = f_tf.inverse(p_tf) @@ -258,7 +284,120 @@ def test_results_match_endtoend(test_input,biort,qshift): X_tf = sess.run(pi_tf.X, feed_dict={in_p: [im]})[0] np.testing.assert_array_almost_equal( - X_np, X_tf, decimal=PRECISION_DECIMAL) + X_np, X_tf, decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +@pytest.mark.parametrize("data_format", [ + ("nhwc"), + ("nchw") +]) +def test_forward_channels(data_format): + batch = 5 + c = 3 + nlevels = 3 + sess = tf.Session() + + if data_format == "nhwc": + ims = np.random.randn(batch, 100, 100, c) + in_p = tf.placeholder(tf.float32, [None, 100, 100, c]) + else: + ims = np.random.randn(batch, c, 100, 100) + in_p = tf.placeholder(tf.float32, [None, c, 100, 100]) + + # Transform a set of images with forward_channels + f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') + start = time.time() + Yl, Yh, Yscale = f_tf.forward_channels( + in_p, nlevels=nlevels, include_scale=True, data_format=data_format) + Yl, Yh, Yscale = sess.run([Yl, Yh, Yscale], {in_p: ims}) + print("That took {:.2f}s".format(time.time() - start)) + + # Now do it channel by channel + in_p2 = tf.placeholder(tf.float32, [None, 100, 100]) + p_tf = f_tf.forward(in_p2, nlevels=nlevels, include_scale=True) + for i in range(c): + if data_format == "nhwc": + Yl1, Yh1, Yscale1 = sess.run([p_tf.lowpass_op, + p_tf.highpasses_ops, + p_tf.scales_ops], + {in_p2: ims[:,:,:,i]}) + np.testing.assert_array_almost_equal( + Yl[:,:,:,i], Yl1, decimal=4) + for j in range(nlevels): + np.testing.assert_array_almost_equal( + Yh[j][:,:,:,i,:], Yh1[j], decimal=4) + np.testing.assert_array_almost_equal( + Yscale[j][:,:,:,i], Yscale1[j], decimal=4) + else: + Yl1, Yh1, Yscale1 = sess.run([p_tf.lowpass_op, + p_tf.highpasses_ops, + p_tf.scales_ops], + {in_p2: ims[:,i]}) + np.testing.assert_array_almost_equal( + Yl[:,i], Yl1, decimal=4) + for j in range(nlevels): + np.testing.assert_array_almost_equal( + Yh[j][:,i], Yh1[j], decimal=4) + np.testing.assert_array_almost_equal( + Yscale[j][:,i], Yscale1[j], decimal=4) + sess.close() + + +@skip_if_no_tf +@pytest.mark.parametrize("data_format", [ + ("nhwc"), + ("nchw"), +]) +def test_inverse_channels(data_format): + batch = 5 + c = 3 + nlevels = 3 + sess = tf.Session() + + # Create the tensors of the right shape by calling the forward function + if data_format == "nhwc": + ims = np.random.randn(batch, 100, 100, c) + in_p = tf.placeholder(tf.float32, [None, 100, 100, c]) + f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') + Yl, Yh = f_tf.forward_channels( + in_p, nlevels=nlevels, include_scale=False, data_format=data_format) + else: + ims = np.random.randn(batch, c, 100, 100) + in_p = tf.placeholder(tf.float32, [None, c, 100, 100]) + f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') + Yl, Yh = f_tf.forward_channels( + in_p, nlevels=nlevels, include_scale=False, data_format=data_format) + + # Call the inverse_channels function + start = time.time() + X = f_tf.inverse_channels(Yl, Yh, data_format=data_format) + X, Yl, Yh = sess.run([X, Yl, Yh], {in_p: ims}) + print("That took {:.2f}s".format(time.time() - start)) + + # Now do it channel by channel + in_p2 = tf.zeros((batch, 100, 100), tf.float32) + p_tf = f_tf.forward(in_p2, nlevels=nlevels, include_scale=False) + p_tf = f_tf.inverse(p_tf) + for i in range(c): + Yh1 = [] + if data_format == "nhwc": + Yl1 = Yl[:,:,:,i] + for j in range(nlevels): + Yh1.append(Yh[j][:,:,:,i]) + else: + Yl1 = Yl[:,i] + for j in range(nlevels): + Yh1.append(Yh[j][:,i]) + + # Use the eval_inv function to feed the data into the right variables + X1 = p_tf.eval_inv(Yl1, Yh1, sess) + + if data_format == "nhwc": + np.testing.assert_array_almost_equal(X[:,:,:,i], X1, decimal=4) + else: + np.testing.assert_array_almost_equal(X[:,i], X1, decimal=4) + + sess.close() - # vim:sw=4:sts=4:et From 647b3fc56e6538b354ba93fb40967b6f46cca309 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 1 Jun 2017 08:58:07 +0100 Subject: [PATCH 31/52] Started writing undecimated version of the transform. Little success so far. --- dtcwt/_version.py | 2 +- dtcwt/tf/lowlevel.py | 288 +++++++++++++++++++++++++--------------- dtcwt/tf/transform2d.py | 87 ++++++++---- 3 files changed, 245 insertions(+), 132 deletions(-) diff --git a/dtcwt/_version.py b/dtcwt/_version.py index 39f7286..56ae676 100644 --- a/dtcwt/_version.py +++ b/dtcwt/_version.py @@ -1,2 +1,2 @@ # IMPORTANT: before release, remove the 'devN' tag from the release name -__version__ = '0.12.0dev1' +__version__ = '0.12.0rc3' diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index 777ecbf..bcadfd3 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -6,8 +6,8 @@ except ImportError: _HAVE_TF = False -import numpy as np -from dtcwt.utils import asfarray, as_column_vector +from dtcwt.utils import as_column_vector + def _as_row_tensor(h): if isinstance(h, tf.Tensor): @@ -17,6 +17,7 @@ def _as_row_tensor(h): h = tf.constant(h, tf.float32) return h + def _as_col_tensor(h): if isinstance(h, tf.Tensor): h = tf.reshape(h, [-1, 1]) @@ -25,20 +26,26 @@ def _as_col_tensor(h): h = tf.constant(h, tf.float32) return h + def _conv_2d(X, h, strides=[1,1,1,1]): - """Perform 2d convolution in tensorflow. X will to be manipulated to be of - shape [batch, height, width, ch], and h to be of shape - [height, width, ch, num]. This function does the necessary reshaping before - calling the conv2d function, and does the reshaping on the output, returning - Y of shape [batch, height, width]""" - + """ + Perform 2d convolution in tensorflow. + + X will to be manipulated to be of shape [batch, height, width, ch], + and h to be of shape [height, width, ch, num]. This function does the + necessary reshaping before calling the conv2d function, and does the + reshaping on the output, returning Y of shape [batch, height, width] + """ + # Check the shape of X is what we expect if len(X.shape) != 3: - raise ValueError('X needs to be of shape [batch, height, width] for conv_2d') - + raise ValueError('X needs to be of shape [batch, height, width] ' + + 'for conv_2d') + # Check the shape of h is what we expect if len(h.shape) != 2: - raise ValueError('Filter inputs must only have height and width for conv_2d') + raise ValueError('Filter inputs must only have height and width ' + + 'for conv_2d') # Add in the unit dimensions for conv X = tf.expand_dims(X, axis=-1) @@ -48,24 +55,31 @@ def _conv_2d(X, h, strides=[1,1,1,1]): h = tf.reverse(h, axis=[0,1]) Y = tf.nn.conv2d(X, h, strides=strides, padding='VALID') - # Remove the final dimension, returning a result of shape [batch, height, width] + # Remove the final dimension, returning a result of shape + # [batch, height, width] Y = tf.squeeze(Y, axis=-1) return Y + def _conv_2d_transpose(X, h, out_shape, strides=[1,1,1,1]): - """Perform 2d convolution in tensorflow. X will to be manipulated to be of - shape [batch, height, width, ch], and h to be of shape - [height, width, ch, num]. This function does the necessary reshaping before - calling the conv2d function, and does the reshaping on the output, returning - Y of shape [batch, height, width]""" - + """ + Perform 2d transpose convolution in tensorflow. + + X will to be manipulated to be of shape [batch, height, width, ch], and h to + be of shape [height, width, ch, num]. This function does the necessary + reshaping before calling the conv2d function, and does the reshaping on the + output, returning Y of shape [batch, height, width] + """ + # Check the shape of X is what we expect if len(X.shape) != 3: - raise ValueError('X needs to be of shape [batch, height, width] for conv_2d') + raise ValueError('X needs to be of shape [batch, height, width] ' + + 'for conv_2d') # Check the shape of h is what we expect if len(h.shape) != 2: - raise ValueError('Filter inputs must only have height and width for conv_2d') + raise ValueError('Filter inputs must only have height and width ' + + 'for conv_2d') # Add in the unit dimensions for conv X = tf.expand_dims(X, axis=-1) @@ -76,22 +90,33 @@ def _conv_2d_transpose(X, h, out_shape, strides=[1,1,1,1]): # Transpose h as we will be using the transpose convolution h = tf.transpose(h, perm=[1, 0, 2, 3]) - Y = tf.nn.conv2d(X, h, output_shape=out_shape, strides=strides, padding='VALID') + Y = tf.nn.conv2d(X, h, output_shape=out_shape, strides=strides, + padding='VALID') - # Remove the final dimension, returning a result of shape [batch, height, width] + # Remove the final dimension, returning a result of shape + # [batch, height, width] Y = tf.squeeze(Y, axis=-1) return Y -def colfilter(X, h): - """Filter the columns of image *X* using filter vector *h*, without decimation. - If len(h) is odd, each output sample is aligned with each input sample - and *Y* is the same size as *X*. If len(h) is even, each output sample is - aligned with the mid point of each pair of input samples, and Y.shape = - X.shape + [1 0]. + +def colfilter(X, h, align=False): + """ + Filter the columns of image *X* using filter vector *h*, without decimation. + :param X: an image whose columns are to be filtered :param h: the filter coefficients. + :param align: If true, then will have Y keep the same output shape as X, + even if h has even length. Makes no difference if len(h) is odd. + :returns Y: the filtered image. + + If len(h) is odd, each output sample is aligned with each input sample + and *Y* is the same size as *X*. + If len(h) is even, each output sample is aligned with the mid point of + each pair of input samples, and Y.shape = X.shape + [1 0]. + + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 @@ -103,22 +128,32 @@ def colfilter(X, h): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) - X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') + if m % 2 == 0 and align: + X = tf.pad(X, [[0, 0], [m2 - 1, m2], [0, 0]], 'SYMMETRIC') + else: + X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') Y = _conv_2d(X, h_t, strides=[1,1,1,1]) return Y -def rowfilter(X, h): - """Filter the rows of image *X* using filter vector *h*, without decimation. - If len(h) is odd, each output sample is aligned with each input sample - and *Y* is the same size as *X*. If len(h) is even, each output sample is - aligned with the mid point of each pair of input samples, and Y.shape = - X.shape + [0 1]. +def rowfilter(X, h, align=False): + """ + Filter the rows of image *X* using filter vector *h*, without decimation. + :param X: a tensor of images whose rows are to be filtered :param h: the filter coefficients. + :param align: If true, then will have Y keep the same output shape as X, + even if h has even length. Makes no difference if len(h) is odd. + :returns Y: the filtered image. + + If len(h) is odd, each output sample is aligned with each input sample + and *Y* is the same size as *X*. + If len(h) is even, each output sample is aligned with the mid point of each + pair of input samples, and Y.shape = X.shape + [0 1]. + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 @@ -131,20 +166,30 @@ def rowfilter(X, h): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) - X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') + if m % 2 == 0 and align: + X = tf.pad(X, [[0, 0], [0, 0], [m2 - 1, m2]], 'SYMMETRIC') + else: + X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') Y = _conv_2d(X, h_t, strides=[1,1,1,1]) return Y -def coldfilt(X, ha, hb, a_first=True): - """Filter the columns of image X using the two filters ha and hb = - reverse(ha). - ha operates on the odd samples of X and hb on the even samples. +def coldfilt(X, ha, hb, no_decimate=False): + """ + Filter the columns of image X using the two filters ha and hb = + reverse(ha). + + :param X: The input, of size [batch, h, w] + :param ha: Filter to be used on the odd samples of x. + :param hb: Filter to bue used on the even samples of x. + :param no_decimate: If true, keep the same input size + Both filters should be even length, and h should be approx linear - phase with a quarter sample (i.e. an :math:`e^{j \pi/4}`) advance from + phase with a quarter sample (i.e. an :math:`e^{j \pi/4}`) advance from its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`). + .. code-block:: text ext top edge bottom edge ext Level 1: ! | ! | ! @@ -153,12 +198,15 @@ def coldfilt(X, ha, hb, a_first=True): Level 2: ! | ! | ! +q filt on x b b a a a a b b -q filt on o a a b b b b a a + The output is decimated by two from the input sample rate and the results - from the two filters, Ya and Yb, are interleaved to give Y. - Symmetric extension with repeated end samples is used on the composite X columns - before each filter is applied. - Raises ValueError if the number of rows in X is not a multiple of 4, the - length of ha does not match hb or the lengths of ha or hb are non-even. + from the two filters, Ya and Yb, are interleaved to give Y. + Symmetric extension with repeated end samples is used on the composite X + columns before each filter is applied. + + :raises ValueError if the number of rows in X is not a multiple of 4, the + length of ha does not match hb or the lengths of ha or hb are non-even. + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 @@ -179,38 +227,49 @@ def coldfilt(X, ha, hb, a_first=True): if m % 2 != 0: raise ValueError('Lengths of ha and hb must be even') + # Do the 2d convolution, but only evaluated at every second sample + # for both X_odd and X_even + rows = r2 + if no_decimate: + pass + # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns). - X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') + X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') # Take the odd and even columns of X - X_odd = X[:,2:r+2*m-2:2,:] - X_even =X[:,3:r+2*m-2:2,:] + X_odd = X[:, 2:r + 2 * m - 2:2, :] + X_even = X[:, 3:r + 2 * m - 2:2, :] - # Do the 2d convolution, but only evaluated at every second sample - # for both X_odd and X_even a_rows = _conv_2d(X_odd, ha_t, strides=[1,2,1,1]) b_rows = _conv_2d(X_even, hb_t, strides=[1,2,1,1]) - + # Stack a_rows and b_rows (both of shape [Batch, r/4, c]) along the third - # dimension to make a tensor of shape [Batch, r/4, 2, c]. - Y = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, - lambda: tf.stack([a_rows,b_rows],axis=2), - lambda: tf.stack([b_rows,a_rows],axis=2)) + # dimension to make a tensor of shape [Batch, r/4, 2, c]. + Y = tf.cond(tf.reduce_sum(ha_t * hb_t) > 0, + lambda: tf.stack([a_rows, b_rows],axis=2), + lambda: tf.stack([b_rows, a_rows],axis=2)) # Reshape result to be shape [Batch, r/2, c]. This reshaping interleaves # the columns - Y = tf.reshape(Y, [-1, r2, c]) - + Y = tf.reshape(Y, [-1, rows, c]) + return Y -def rowdfilt(X, ha, hb): - """Filter the rows of image X using the two filters ha and hb = - reverse(ha). ha operates on the odd samples of X and hb on the even - samples. Both filters should be even length, and h should be approx linear +def rowdfilt(X, ha, hb, no_decimate=False): + """ + Filter the rows of image X using the two filters ha and hb = reverse(ha). + + :param X: The input, of size [batch, h, w] + :param ha: Filter to be used on the odd samples of x. + :param hb: Filter to bue used on the even samples of x. + :param no_decimate: If true, keep the same input size + + Both filters should be even length, and h should be approx linear phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`). + .. code-block:: text ext top edge bottom edge ext Level 1: ! | ! | ! @@ -219,12 +278,15 @@ def rowdfilt(X, ha, hb): Level 2: ! | ! | ! +q filt on x b b a a a a b b -q filt on o a a b b b b a a + The output is decimated by two from the input sample rate and the results from the two filters, Ya and Yb, are interleaved to give Y. Symmetric extension with repeated end samples is used on the composite X rows before each filter is applied. - Raises ValueError if the number of columns in X is not a multiple of 4, the - length of ha does not match hb or the lengths of ha or hb are non-even. + + :raises ValueError if the number of columns in X is not a multiple of 4, the + length of ha does not match hb or the lengths of ha or hb are non-even. + .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , August 2013 .. codeauthor:: Cian Shaffrey, Cambridge University, August 2000 @@ -235,7 +297,7 @@ def rowdfilt(X, ha, hb): c2 = c // 2 if c % 4 != 0: raise ValueError('No. of rows in X must be a multiple of 4') - + ha_t = _as_row_tensor(ha) hb_t = _as_row_tensor(hb) if ha_t.shape != hb_t.shape: @@ -248,35 +310,46 @@ def rowdfilt(X, ha, hb): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the rows). # SYMMETRIC extension means the edge sample is repeated twice, whereas - # REFLECT only has the edge sample once + # REFLECT only has the edge sample once X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC') # Take the odd and even columns of X - X_odd = X[:,:,2:c+2*m-2:2] - X_even= X[:,:,3:c+2*m-2:2] + X_odd = X[:,:,2:c + 2 * m - 2:2] + X_even = X[:,:,3:c + 2 * m - 2:2] # Do the 2d convolution, but only evaluated at every second sample # for both X_odd and X_even + cols = c2 + if no_decimate: + pass + a_cols = _conv_2d(X_odd, ha_t, strides=[1,1,2,1]) b_cols = _conv_2d(X_even, hb_t, strides=[1,1,2,1]) - - # Stack a_cols and b_cols (both of shape [Batch, r, c/4]) along the fourth - # dimension to make a tensor of shape [Batch, r, c/4, 2]. - Y = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, - lambda: tf.stack([a_cols,b_cols],axis=3), - lambda: tf.stack([b_cols,a_cols],axis=3)) + + # Stack a_cols and b_cols (both of shape [Batch, r, c/4]) along the fourth + # dimension to make a tensor of shape [Batch, r, c/4, 2]. + Y = tf.cond(tf.reduce_sum(ha_t * hb_t) > 0, + lambda: tf.stack([a_cols, b_cols], axis=3), + lambda: tf.stack([b_cols, a_cols], axis=3)) # Reshape result to be shape [Batch, r, c/2]. This reshaping interleaves # the columns - Y = tf.reshape(Y, [-1, r, c2]) - + Y = tf.reshape(Y, [-1, r, cols]) + return Y -def colifilt(X, ha, hb): - """ Filter the columns of image X using the two filters ha and hb = - reverse(ha). ha operates on the odd samples of X and hb on the even - samples. Both filters should be even length, and h should be approx linear +def colifilt(X, ha, hb, no_decimate=False): + """ + Filter the columns of image X using the two filters ha and hb = + reverse(ha). + + :param X: The input, of size [batch, h, w] + :param ha: Filter to be used on the odd samples of x. + :param hb: Filter to bue used on the even samples of x. + :param no_decimate: If true, keep the same input size + + Both filters should be even length, and h should be approx linear phase with a quarter sample advance from its mid pt (i.e `:math:`|h(m/2)| > |h(m/2 + 1)|`). @@ -301,9 +374,12 @@ def colifilt(X, ha, hb): .. codeauthor:: Nick Kingsbury, Cambridge University, August 2000 """ - + # A quick hack to handle undecimated inputs. Simply take every second sample + # as if it had been decimated. + if no_decimate: + X = X[:,::2,:] + r, c = X.get_shape().as_list()[1:] - r2 = r // 2 if r % 2 != 0: raise ValueError('No. of rows in X must be a multiple of 2') @@ -311,7 +387,8 @@ def colifilt(X, ha, hb): hb_t = _as_col_tensor(hb) if ha_t.shape != hb_t.shape: raise ValueError('Shapes of ha and hb must be the same') - + + m = ha_t.get_shape().as_list()[0] m2 = m // 2 if ha_t.get_shape().as_list()[0] % 2 != 0: @@ -319,36 +396,39 @@ def colifilt(X, ha, hb): X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') - ha_odd_t = ha_t[::2,:] + ha_odd_t = ha_t[::2,:] ha_even_t = ha_t[1::2,:] - hb_odd_t = hb_t[::2,:] + hb_odd_t = hb_t[::2,:] hb_even_t = hb_t[1::2,:] - - if m2 % 2 == 0: + + if m2 % 2 == 0: # m/2 is even, so set up t to start on d samples. # Set up vector for symmetric extension of X with repeated end samples. - - # Take the odd and even columns of X - X1,X2 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, - lambda: (X[:,1:r+m-2:2,:], X[:,0:r+m-3:2,:]), - lambda: (X[:,0:r+m-3:2,:], X[:,1:r+m-2:2,:])) - X3,X4 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, - lambda: (X[:,3:r+m:2,:], X[:,2:r+m-1:2,:]), - lambda: (X[:,2:r+m-1:2,:], X[:,3:r+m:2,:])) + + # Take the odd and even columns of X + X1, X2 = tf.cond( + tf.reduce_sum(ha_t * hb_t) > 0, + lambda: (X[:, 1:r + m - 2:2, :], X[:, 0:r + m - 3:2, :]), + lambda: (X[:, 0:r + m - 3:2, :], X[:, 1:r + m - 2:2, :])) + X3, X4 = tf.cond( + tf.reduce_sum(ha_t * hb_t) > 0, + lambda: (X[:, 3:r + m:2, :], X[:, 2:r + m - 1:2, :]), + lambda: (X[:, 2:r + m - 1:2, :], X[:, 3:r + m:2, :])) y1 = _conv_2d(X2, ha_even_t) y2 = _conv_2d(X1, hb_even_t) y3 = _conv_2d(X4, ha_odd_t) y4 = _conv_2d(X3, hb_odd_t) - + else: - # m/2 is even, so set up t to start on d samples. + # m/2 is odd, so set up t to start on d samples. # Set up vector for symmetric extension of X with repeated end samples. - + # Take the odd and even columns of X - X1,X2 = tf.cond(tf.reduce_sum(ha_t*hb_t) > 0, - lambda: (X[:,2:r+m-1:2,:], X[:,1:r+m-2:2,:]), - lambda: (X[:,1:r+m-2:2,:], X[:,2:r+m-1:2,:])) + X1, X2 = tf.cond( + tf.reduce_sum(ha_t * hb_t) > 0, + lambda: (X[:, 2:r + m - 1:2, :], X[:, 1:r + m - 2:2, :]), + lambda: (X[:, 1:r + m - 2:2, :], X[:, 2:r + m - 1:2, :])) y1 = _conv_2d(X2, ha_odd_t) y2 = _conv_2d(X1, hb_odd_t) @@ -357,8 +437,8 @@ def colifilt(X, ha, hb): # Stack 4 tensors of shape [batch, r2, c] into one tensor [batch, r2, 4, c] Y = tf.stack([y1,y2,y3,y4], axis=2) - - # Reshape to be [batch, 2*4, c]. This interleaves the rows + + # Reshape to be [batch, r * 2, c]. This interleaves the rows Y = tf.reshape(Y, [-1,2*r,c]) - + return Y diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 6c80b03..de8d4b1 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -93,7 +93,8 @@ def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) self.inverse_graphs[find_key] = p_ops - def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): + def forward(self, X, nlevels=3, include_scale=False, return_tuple=False, + undecimated=False, max_dec_scale=1): ''' Perform a forward transform on an image. @@ -151,7 +152,8 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): size = '{}x{}'.format(X.shape[0], X.shape[1]) name = 'dtcwt_fwd_{}'.format(size) with self.np_graph.name_scope(name): - p_ops = self._forward_ops(ph, nlevels, include_scale) + p_ops = self._forward_ops( + ph, nlevels, include_scale, False, undecimated, max_dec_scale) self._add_forward_graph(p_ops, X.shape) @@ -182,13 +184,15 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False): size = '{}x{}'.format(original_size[0], original_size[1]) name = 'dtcwt_fwd_{}'.format(size) with tf.name_scope(name): - p_tf = self._forward_ops(X, nlevels, include_scale, - return_tuple) + p_tf = self._forward_ops( + X, nlevels, include_scale, return_tuple, undecimated, + max_dec_scale) return p_tf def forward_channels(self, X, nlevels=3, include_scale=False, - data_format="nhwc"): + data_format="nhwc", undecimated=False, + max_dec_scale=1): ''' Perform a forward transform on an image with multiple channels. @@ -252,7 +256,9 @@ def forward_channels(self, X, nlevels=3, include_scale=False, X = tf.transpose(X, perm=[1, 0, 2, 3]) f = lambda x: self._forward_ops(x, nlevels, include_scale, - return_tuple=True) + return_tuple=True, + undecimated=undecimated, + max_dec_scale=max_dec_scale) # Calculate the dtcwt for each of the channels independently # This will return tensors of shape: @@ -345,8 +351,8 @@ def inverse(self, pyramid, gain_mask=None): # If not, create a graph if p_ops is None: - Lo_ph = tf.placeholder(tf.float32, - [None, Yl.shape[0], Yl.shape[1]]) + Lo_ph = tf.placeholder( + tf.float32, [None, Yl.shape[0], Yl.shape[1]]) Hi_ph = tuple( tf.placeholder(tf.complex64, [None, *level.shape]) for level in Yh) @@ -482,7 +488,8 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): return X def _forward_ops(self, X, nlevels=3, include_scale=False, - return_tuple=False): + return_tuple=False, undecimated=False, + max_dec_scale=1): """ Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. @@ -493,6 +500,10 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, :param return_tuple: If true, instead of returning a :py:class`dtcwt.Pyramid_tf` object, return a tuple of (lowpass, highpasses, scales) + :param undecimated: If true, will stop decimating the transform + :param max_undec_scale: The maximum undecimated scale. Will be used if + undecimated is set to True. Beyond this scale, stop decimating + (useful if you want to partially decimate) :returns: A :py:class:`dtcwt.Pyramid_tf` compatible object representing the transform-domain signal @@ -624,27 +635,31 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, if col_size % 4 != 0: LoLo = tf.pad(LoLo, [[0, 0], [0, 0], [1, 1]], 'SYMMETRIC') + no_decimate = False + if undecimated and level >= max_dec_scale: + no_decimate = True + # Do even Qshift filters on cols. - Lo = coldfilt(LoLo, h0b, h0a) - Hi = coldfilt(LoLo, h1b, h1a) + Lo = coldfilt(LoLo, h0b, h0a, no_decimate) + Hi = coldfilt(LoLo, h1b, h1a, no_decimate) if len(self.qshift) >= 12: - Ba = coldfilt(LoLo, h2b, h2a) + Ba = coldfilt(LoLo, h2b, h2a, no_decimate) # Do even Qshift filters on rows. - LoLo = rowdfilt(Lo, h0b, h0a) + LoLo = rowdfilt(Lo, h0b, h0a, no_decimate) LoLo_shape = LoLo.get_shape().as_list()[1:3] # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowdfilt(Hi, h0b, h0a)) + horiz = q2c(rowdfilt(Hi, h0b, h0a, no_decimate)) # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowdfilt(Lo, h1b, h1a)) + vertic = q2c(rowdfilt(Lo, h1b, h1a, no_decimate)) # Diagonal wavelet pair (45 & 135 degrees) if len(self.qshift) >= 12: - diag = q2c(rowdfilt(Ba, h2b, h2a)) + diag = q2c(rowdfilt(Ba, h2b, h2a, no_decimate)) else: - diag = q2c(rowdfilt(Hi, h1b, h1a)) + diag = q2c(rowdfilt(Hi, h1b, h1a, no_decimate)) # Pack all 6 tensors into one Yh[level] = tf.stack( @@ -697,6 +712,10 @@ def _inverse_ops(self, pyramid, gain_mask=None): :param pyramid: A :py:class:`dtcwt.tf.Pyramid_tf`-like class holding the transform domain representation to invert. :param gain_mask: Gain to be applied to each subband. + :param undecimated: If true, will stop decimating the transform + :param max_undec_scale: The maximum undecimated scale. Will be used if + undecimated is set to True. Beyond this scale, stop decimating + (useful if you want to partially decimate) :returns: A :py:class:`dtcwt.tf.Pyramid_tf` class which can be evaluated to get the inverted signal, X. @@ -759,25 +778,38 @@ def _inverse_ops(self, pyramid, gain_mask=None): current_level - 1]) # Do even Qshift filters on columns. - y1 = colifilt(Z, g0b, g0a) + colifilt(lh, g1b, g1a) + no_decimate = False + this_size = Yh[current_level - 1].get_shape().as_list() + next_size = Yh[current_level - 2].get_shape().as_list() + if this_size[1:3] == next_size[1:3]: + no_decimate = True + + y1 = colifilt(Z, g0b, g0a, no_decimate) + \ + colifilt(lh, g1b, g1a, no_decimate) if len(self.qshift) >= 12: - y2 = colifilt(hl, g0b, g0a) - y2bp = colifilt(hh, g2b, g2a) + y2 = colifilt(hl, g0b, g0a, no_decimate) + y2bp = colifilt(hh, g2b, g2a, no_decimate) # Do even Qshift filters on rows. + y1T = tf.transpose(y1, perm=[0, 2, 1]) + y2T = tf.transpose(y2, perm=[0, 2, 1]) + y2bpT = tf.transpose(y2bp, perm=[0, 2, 1]) Z = tf.transpose( - colifilt(tf.transpose(y1, perm=[0, 2, 1]), g0b, g0a) + - colifilt(tf.transpose(y2, perm=[0, 2, 1]), g1b, g1a) + - colifilt(tf.transpose(y2bp, perm=[0, 2, 1]), g2b, g2a), + colifilt(y1T, g0b, g0a, no_decimate) + + colifilt(y2T, g1b, g1a, no_decimate) + + colifilt(y2bpT, g2b, g2a, no_decimate), perm=[0, 2, 1]) else: - y2 = colifilt(hl, g0b, g0a) + colifilt(hh, g1b, g1a) + y2 = colifilt(hl, g0b, g0a, no_decimate) + \ + colifilt(hh, g1b, g1a, no_decimate) # Do even Qshift filters on rows. + y1T = tf.transpose(y1, perm=[0, 2, 1]) + y2T = tf.transpose(y2, perm=[0, 2, 1]) Z = tf.transpose( - colifilt(tf.transpose(y1, perm=[0, 2, 1]), g0b, g0a) + - colifilt(tf.transpose(y2, perm=[0, 2, 1]), g1b, g1a), + colifilt(y1T, g0b, g0a, no_decimate) + + colifilt(y2T, g1b, g1a, no_decimate), perm=[0, 2, 1]) # Check size of Z and crop as required @@ -794,7 +826,8 @@ def _inverse_ops(self, pyramid, gain_mask=None): Z_r, Z_c = Z.get_shape().as_list()[1:3] if Z_r != S_r * 2 or Z_c != S_c * 2: raise ValueError( - 'Sizes of highpasses are not valid for DTWAVEIFM2') + 'Sizes of highpasses {}x{} are not '.format(Z_r, Z_c) + + 'compatible with {}x{} from next level'.format(S_r, S_c)) current_level = current_level - 1 From 7b85523543e35298e40290db3260ae9915217ef9 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 7 Jun 2017 00:49:54 +0100 Subject: [PATCH 32/52] Added support for flexible padding. Can now do more scales --- dtcwt/tf/lowlevel.py | 75 ++++++++++++++++++++++++++----------- dtcwt/tf/transform2d.py | 47 ++++++++++------------- tests/test_tfTransform2d.py | 3 -- 3 files changed, 73 insertions(+), 52 deletions(-) diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index bcadfd3..ea9d8f6 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -7,6 +7,7 @@ _HAVE_TF = False from dtcwt.utils import as_column_vector +import numpy as np def _as_row_tensor(h): @@ -100,6 +101,33 @@ def _conv_2d_transpose(X, h, out_shape, strides=[1,1,1,1]): return Y +def _tf_pad(x, szs, padding='SYMMETRIC'): + """ + Tensorflow can't handle padding by more than the dimension of the image. + This wrapper allows us to build padding up successively. + """ + def get_size(x): + # Often the batch will be None. Convert these to 0s + x_szs = x.get_shape().as_list() + x_szs = [0 if val is None else val for val in x_szs] + return x_szs + + x_szs = get_size(x) + gt = [[sz[0] > x_sz, sz[1] > x_sz] for sz,x_sz in zip(szs, x_szs)] + while np.any(gt): + # This creates an intermediate padding amount that will bring in + # dimensions that are too big by the size of x. + szs_step = np.int32(gt) * np.stack([x_szs, x_szs], axis=-1) + x = tf.pad(x, szs_step, padding) + szs = szs - szs_step + x_szs = get_size(x) + gt = [[sz[0] > x_sz, sz[1] > x_sz] for sz,x_sz in zip(szs, x_szs)] + + # Pad by the remaining amount + x = tf.pad(x, szs, 'SYMMETRIC') + return x + + def colfilter(X, h, align=False): """ Filter the columns of image *X* using filter vector *h*, without decimation. @@ -129,9 +157,9 @@ def colfilter(X, h, align=False): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) if m % 2 == 0 and align: - X = tf.pad(X, [[0, 0], [m2 - 1, m2], [0, 0]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [m2 - 1, m2], [0, 0]], 'SYMMETRIC') else: - X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') Y = _conv_2d(X, h_t, strides=[1,1,1,1]) @@ -167,9 +195,9 @@ def rowfilter(X, h, align=False): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns) if m % 2 == 0 and align: - X = tf.pad(X, [[0, 0], [0, 0], [m2 - 1, m2]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [0, 0], [m2 - 1, m2]], 'SYMMETRIC') else: - X = tf.pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [0, 0], [m2, m2]], 'SYMMETRIC') Y = _conv_2d(X, h_t, strides=[1,1,1,1]) @@ -216,16 +244,19 @@ def coldfilt(X, ha, hb, no_decimate=False): r, c = X.get_shape().as_list()[1:] r2 = r // 2 if r % 4 != 0: - raise ValueError('No. of rows in X must be a multiple of 4') + raise ValueError('No. of rows in X must be a multiple of 4\n' + + 'X was {}'.format(X.get_shape().as_list())) ha_t = _as_col_tensor(ha) hb_t = _as_col_tensor(hb) if ha_t.shape != hb_t.shape: - raise ValueError('Shapes of ha and hb must be the same') + raise ValueError('Shapes of ha and hb must be the same\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) m = ha_t.get_shape().as_list()[0] if m % 2 != 0: - raise ValueError('Lengths of ha and hb must be even') + raise ValueError('Lengths of ha and hb must be even\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) # Do the 2d convolution, but only evaluated at every second sample # for both X_odd and X_even @@ -235,7 +266,7 @@ def coldfilt(X, ha, hb, no_decimate=False): # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the columns). - X = tf.pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [m, m], [0, 0]], 'SYMMETRIC') # Take the odd and even columns of X X_odd = X[:, 2:r + 2 * m - 2:2, :] @@ -296,22 +327,25 @@ def rowdfilt(X, ha, hb, no_decimate=False): r, c = X.get_shape().as_list()[1:] c2 = c // 2 if c % 4 != 0: - raise ValueError('No. of rows in X must be a multiple of 4') + raise ValueError('No. of rows in X must be a multiple of 4\n' + + 'X was {}'.format(X.get_shape().as_list())) ha_t = _as_row_tensor(ha) hb_t = _as_row_tensor(hb) if ha_t.shape != hb_t.shape: - raise ValueError('Shapes of ha and hb must be the same') + raise ValueError('Shapes of ha and hb must be the same\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) m = ha_t.get_shape().as_list()[1] if m % 2 != 0: - raise ValueError('Lengths of ha and hb must be even') + raise ValueError('Lengths of ha and hb must be even\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) # Symmetrically extend with repeat of end samples. # Pad only the second dimension of the tensor X (the rows). # SYMMETRIC extension means the edge sample is repeated twice, whereas # REFLECT only has the edge sample once - X = tf.pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [0, 0], [m, m]], 'SYMMETRIC') # Take the odd and even columns of X X_odd = X[:,:,2:c + 2 * m - 2:2] @@ -347,7 +381,7 @@ def colifilt(X, ha, hb, no_decimate=False): :param X: The input, of size [batch, h, w] :param ha: Filter to be used on the odd samples of x. :param hb: Filter to bue used on the even samples of x. - :param no_decimate: If true, keep the same input size + :param no_decimate: Not implemented yet Both filters should be even length, and h should be approx linear phase with a quarter sample advance from its mid pt (i.e `:math:`|h(m/2)| > @@ -376,25 +410,24 @@ def colifilt(X, ha, hb, no_decimate=False): # A quick hack to handle undecimated inputs. Simply take every second sample # as if it had been decimated. - if no_decimate: - X = X[:,::2,:] - r, c = X.get_shape().as_list()[1:] if r % 2 != 0: - raise ValueError('No. of rows in X must be a multiple of 2') + raise ValueError('No. of rows in X must be a multiple of 2.\n' + + 'X was {}'.format(X.get_shape().as_list())) ha_t = _as_col_tensor(ha) hb_t = _as_col_tensor(hb) if ha_t.shape != hb_t.shape: - raise ValueError('Shapes of ha and hb must be the same') - + raise ValueError('Shapes of ha and hb must be the same.\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) m = ha_t.get_shape().as_list()[0] m2 = m // 2 if ha_t.get_shape().as_list()[0] % 2 != 0: - raise ValueError('Lengths of ha and hb must be even') + raise ValueError('Lengths of ha and hb must be even.\n' + + 'ha was {}, hb was {}'.format(ha_t.shape, hb_t.shape)) - X = tf.pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') + X = _tf_pad(X, [[0, 0], [m2, m2], [0, 0]], 'SYMMETRIC') ha_odd_t = ha_t[::2,:] ha_even_t = ha_t[1::2,:] diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index de8d4b1..51674dc 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -635,31 +635,27 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, if col_size % 4 != 0: LoLo = tf.pad(LoLo, [[0, 0], [0, 0], [1, 1]], 'SYMMETRIC') - no_decimate = False - if undecimated and level >= max_dec_scale: - no_decimate = True - # Do even Qshift filters on cols. - Lo = coldfilt(LoLo, h0b, h0a, no_decimate) - Hi = coldfilt(LoLo, h1b, h1a, no_decimate) + Lo = coldfilt(LoLo, h0b, h0a) + Hi = coldfilt(LoLo, h1b, h1a) if len(self.qshift) >= 12: - Ba = coldfilt(LoLo, h2b, h2a, no_decimate) + Ba = coldfilt(LoLo, h2b, h2a) # Do even Qshift filters on rows. - LoLo = rowdfilt(Lo, h0b, h0a, no_decimate) + LoLo = rowdfilt(Lo, h0b, h0a) LoLo_shape = LoLo.get_shape().as_list()[1:3] # Horizontal wavelet pair (15 & 165 degrees) - horiz = q2c(rowdfilt(Hi, h0b, h0a, no_decimate)) + horiz = q2c(rowdfilt(Hi, h0b, h0a)) # Vertical wavelet pair (75 & 105 degrees) - vertic = q2c(rowdfilt(Lo, h1b, h1a, no_decimate)) + vertic = q2c(rowdfilt(Lo, h1b, h1a)) # Diagonal wavelet pair (45 & 135 degrees) if len(self.qshift) >= 12: - diag = q2c(rowdfilt(Ba, h2b, h2a, no_decimate)) + diag = q2c(rowdfilt(Ba, h2b, h2a)) else: - diag = q2c(rowdfilt(Hi, h1b, h1a, no_decimate)) + diag = q2c(rowdfilt(Hi, h1b, h1a)) # Pack all 6 tensors into one Yh[level] = tf.stack( @@ -781,35 +777,30 @@ def _inverse_ops(self, pyramid, gain_mask=None): no_decimate = False this_size = Yh[current_level - 1].get_shape().as_list() next_size = Yh[current_level - 2].get_shape().as_list() - if this_size[1:3] == next_size[1:3]: - no_decimate = True - - y1 = colifilt(Z, g0b, g0a, no_decimate) + \ - colifilt(lh, g1b, g1a, no_decimate) + y1 = colifilt(Z, g0b, g0a) + colifilt(lh, g1b, g1a) if len(self.qshift) >= 12: - y2 = colifilt(hl, g0b, g0a, no_decimate) - y2bp = colifilt(hh, g2b, g2a, no_decimate) + y2 = colifilt(hl, g0b, g0a) + y2bp = colifilt(hh, g2b, g2a) # Do even Qshift filters on rows. y1T = tf.transpose(y1, perm=[0, 2, 1]) y2T = tf.transpose(y2, perm=[0, 2, 1]) y2bpT = tf.transpose(y2bp, perm=[0, 2, 1]) Z = tf.transpose( - colifilt(y1T, g0b, g0a, no_decimate) + - colifilt(y2T, g1b, g1a, no_decimate) + - colifilt(y2bpT, g2b, g2a, no_decimate), + colifilt(y1T, g0b, g0a) + + colifilt(y2T, g1b, g1a) + + colifilt(y2bpT, g2b, g2a), perm=[0, 2, 1]) else: - y2 = colifilt(hl, g0b, g0a, no_decimate) + \ - colifilt(hh, g1b, g1a, no_decimate) + y2 = colifilt(hl, g0b, g0a) + colifilt(hh, g1b, g1a) # Do even Qshift filters on rows. y1T = tf.transpose(y1, perm=[0, 2, 1]) y2T = tf.transpose(y2, perm=[0, 2, 1]) Z = tf.transpose( - colifilt(y1T, g0b, g0a, no_decimate) + - colifilt(y2T, g1b, g1a, no_decimate), + colifilt(y1T, g0b, g0a) + + colifilt(y2T, g1b, g1a), perm=[0, 2, 1]) # Check size of Z and crop as required @@ -911,11 +902,11 @@ def c2q(w, gain): x4 = -tf.real(Q) # Stack 2 inputs of shape [batch, r, c] to [batch, r, 2, c] - x_rows1 = tf.stack([x1, x3], axis=2) + x_rows1 = tf.stack([x1, x3], axis=-2) # Reshaping interleaves the results x_rows1 = tf.reshape(x_rows1, [-1, 2 * r, c]) # Do the same for the even columns - x_rows2 = tf.stack([x2, x4], axis=2) + x_rows2 = tf.stack([x2, x4], axis=-2) x_rows2 = tf.reshape(x_rows2, [-1, 2 * r, c]) # Stack the two [batch, 2*r, c] tensors to [batch, 2*r, c, 2] diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 41d47f2..e6db0dd 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -53,7 +53,6 @@ def test_specific_wavelet(): @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_1d(): Yl, Yh = dtwavexfm2(mandrill[0,:]) @@ -129,7 +128,6 @@ def test_0_levels_w_scale(): @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_input(): # Check that an integer input is correctly coerced into a floating point # array @@ -138,7 +136,6 @@ def test_integer_input(): @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_integer_perfect_recon(): # Check that an integer input is correctly coerced into a floating point # array and reconstructed From df18ae10291c0d8f4c985996884e8fb656aa34a8 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 7 Jun 2017 00:54:33 +0100 Subject: [PATCH 33/52] Updated version number --- dtcwt/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtcwt/_version.py b/dtcwt/_version.py index 56ae676..e633b3c 100644 --- a/dtcwt/_version.py +++ b/dtcwt/_version.py @@ -1,2 +1,2 @@ # IMPORTANT: before release, remove the 'devN' tag from the release name -__version__ = '0.12.0rc3' +__version__ = '0.12.0rc4' From cbabda06558d5317eb38126a8a223d9f0b8362b5 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 2 Aug 2017 20:39:56 +0100 Subject: [PATCH 34/52] Refactored. Also set variable names now for fwd_channels --- dtcwt/tf/common.py | 71 ++++++++++++++++++------------------- dtcwt/tf/transform2d.py | 59 +++++++++++++++--------------- tests/test_tfTransform2d.py | 9 ++--- tests/test_tfcoldfilt.py | 22 +++++++----- tests/test_tfcolfilter.py | 23 +++++++----- tests/test_tfcolifilt.py | 26 +++++++++----- tests/test_tfinputshapes.py | 44 +++++++++++++---------- tests/test_tfrowdfilt.py | 21 +++++++---- tests/test_tfrowfilter.py | 22 ++++++++---- 9 files changed, 169 insertions(+), 128 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index c876c61..44aaf03 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -1,12 +1,5 @@ from __future__ import absolute_import -import numpy as np -import logging - -from dtcwt.coeffs import biort as _biort, qshift as _qshift -from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT -from dtcwt.utils import asfarray - from dtcwt.numpy import Pyramid as Pyramid_np try: @@ -15,6 +8,7 @@ # The lack of tensorflow will be caught by the low-level routines. pass + class Pyramid_tf(object): """A tensorflow representation of a transform domain signal. Backends are free to implement any class which respects this interface for @@ -66,9 +60,9 @@ def _get_lowpass(self, data, sess=None): close_after = True try: - y = sess.run(self.lowpass_op, {self.X : data}) + y = sess.run(self.lowpass_op, {self.X: data}) except ValueError: - y = sess.run(self.lowpass_op, {self.X : [data]})[0] + y = sess.run(self.lowpass_op, {self.X: [data]})[0] if close_after: sess.close() @@ -79,18 +73,23 @@ def _get_highpasses(self, data, sess=None): if self.highpasses_ops is None: return None + # Only close sessions if we had to create them + close_after = False if sess is None: sess = tf.Session(graph=self.graph) + close_after = True - with sess: - try: - y = tuple( - [sess.run(layer_hp, {self.X : data}) - for layer_hp in self.highpasses_ops]) - except ValueError: - y = tuple( - [sess.run(layer_hp, {self.X : [data]})[0] - for layer_hp in self.highpasses_ops]) + try: + y = tuple( + [sess.run(layer_hp, {self.X: data}) + for layer_hp in self.highpasses_ops]) + except ValueError: + y = tuple( + [sess.run(layer_hp, {self.X: [data]})[0] + for layer_hp in self.highpasses_ops]) + + if close_after: + sess.close() return y def _get_scales(self, data, sess=None): @@ -104,12 +103,12 @@ def _get_scales(self, data, sess=None): try: y = tuple( - sess.run(layer_scale, {self.X : data}) - for layer_scale in self.scales_ops) + sess.run(layer_scale, {self.X: data}) + for layer_scale in self.scales_ops) except ValueError: y = tuple( - sess.run(layer_scale, {self.X : [data]})[0] - for layer_scale in self.scales_ops) + sess.run(layer_scale, {self.X: [data]})[0] + for layer_scale in self.scales_ops) if close_after: sess.close() @@ -129,33 +128,31 @@ def _get_X(self, Yl, Yh, sess=None): # multiple layers of Yh data = [Yl, *list(Yh)] placeholders = [self.lowpass_op, *list(self.highpasses_ops)] - X = sess.run(self.X, {i : d for i,d in zip(placeholders,data)}) + X = sess.run(self.X, {i: d for i,d in zip(placeholders,data)}) except ValueError: data = [Yl, *list(Yh)] placeholders = [self.lowpass_op, *list(self.highpasses_ops)] - X = sess.run(self.X, {i : [d] for i,d in zip(placeholders,data)})[0] + X = sess.run(self.X, {i: [d] for i,d in zip(placeholders,data)})[0] if close_after: sess.close() return X - def apply_reshaping(self, fn): """ A helper function to apply a tensor transformation on all of the elements in the pyramid. E.g. reshape all of them in the same way. - :param fn: function to apply to each of the lowpass_op, highpasses_ops and - scale_ops tensors + :param fn: function to apply to each of the lowpass_op, highpasses_ops + and scale_ops tensors """ self.lowpass_op = fn(self.lowpass_op) self.highpasses_ops = tuple( - [fn(h_scale) for h_scale in self.highpasses_ops]) - if not self.scales_ops is None: + [fn(h_scale) for h_scale in self.highpasses_ops]) + if self.scales_ops is not None: self.scales_ops = tuple( - [fn(s_scale) for s_scale in self.scales_ops]) - + [fn(s_scale) for s_scale in self.scales_ops]) def eval_fwd(self, X, sess=None): """ @@ -185,15 +182,15 @@ def eval_inv(self, Yl, Yh, sess=None): :param Yl: A numpy array of shape [, h/(2**scale), w/(2**scale)], where (h,w) was the size of the input image. :param Yh: A tuple or list of the highpass coefficients. Each entry in - the tuple or list represents the scale the coefficients belong to. The - size of the coefficients must match the outputs of the forward - transform. I.e. Yh[0] should have shape [, 6, h/2, w/2], where the - input image had shape (h, w). should be the same across all - scales, and should match the size of the Yl first dimension. + the tuple or list represents the scale the coefficients belong to. + The size of the coefficients must match the outputs of the forward + transform. I.e. Yh[0] should have shape [, 6, h/2, w/2], + where the input image had shape (h, w). should be the same + across all scales, and should match the size of the Yl first + dimension. :param sess: Tensorflow session to use. If none is provided a temporary session will be used. :returns: A numpy array of the inverted data. """ return self._get_X(Yl, Yh, sess) - diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 51674dc..1ea822e 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -5,7 +5,6 @@ from six.moves import xrange -from dtcwt.coeffs import biort as _biort, qshift as _qshift from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT from dtcwt.utils import asfarray from dtcwt.numpy import Transform2d as Transform2dNumPy @@ -29,6 +28,7 @@ def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include else: return r.lowpass, r.highpasses + def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, gain_mask=None): t = Transform2d(biort=biort, qshift=qshift) r = t.inverse(Pyramid_np(Yl, Yh), gain_mask=gain_mask) @@ -153,7 +153,8 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False, name = 'dtcwt_fwd_{}'.format(size) with self.np_graph.name_scope(name): p_ops = self._forward_ops( - ph, nlevels, include_scale, False, undecimated, max_dec_scale) + ph, nlevels, include_scale, False, undecimated, + max_dec_scale) self._add_forward_graph(p_ops, X.shape) @@ -262,11 +263,11 @@ def forward_channels(self, X, nlevels=3, include_scale=False, # Calculate the dtcwt for each of the channels independently # This will return tensors of shape: - # Yl: A tensor of shape [c, batch, h', w'] - # Yh: list of length nlevels, each of shape - # [c, batch, h'', w'', 6] - # Yscale: list of length nlevels, each of shape - # [c, batch, h''', w'''] + # Yl: A tensor of shape [c, batch, h', w'] + # Yh: list of length nlevels, each of shape + # [c, batch, h'', w'', 6] + # Yscale: list of length nlevels, each of shape + # [c, batch, h''', w'''] if include_scale: # (lowpass, highpasses, scales) shape = (tf.float32, @@ -275,19 +276,18 @@ def forward_channels(self, X, nlevels=3, include_scale=False, Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) # Transpose the tensors to put the channel after the batch if data_format == "nhwc": - Yl = tf.transpose(Yl, perm=[1, 2, 3, 0]) - Yh = tuple( - [tf.transpose(x, perm=[1, 2, 3, 0, 4]) for x in Yh]) - Yscale = tuple( - [tf.transpose(x, perm=[1, 2, 3, 0]) - for x in Yscale]) + perm_r = [1, 2, 3, 0] + perm_c = [1, 2, 3, 0, 4] else: - Yl = tf.transpose(Yl, perm=[1, 0, 2, 3]) - Yh = tuple( - [tf.transpose(x, perm=[1, 0, 2, 3, 4]) for x in Yh]) - Yscale = tuple( - [tf.transpose(x, perm=[1, 0, 2, 3]) - for x in Yscale]) + perm_r = [1, 0, 2, 3] + perm_c = [1, 0, 2, 3, 4] + Yl = tf.transpose(Yl, perm=perm_r, name='Yl') + Yh = tuple( + [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) + for i, x in enumerate(Yh)]) + Yscale = tuple( + [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) + for i, x in enumerate(Yscale)]) return Yl, Yh, Yscale @@ -297,13 +297,15 @@ def forward_channels(self, X, nlevels=3, include_scale=False, Yl, Yh = tf.map_fn(f, X, dtype=shape) # Transpose the tensors to put the channel after the batch if data_format == "nhwc": - Yl = tf.transpose(Yl, perm=[1, 2, 3, 0]) - Yh = tuple( - [tf.transpose(x, perm=[1, 2, 3, 0, 4]) for x in Yh]) + perm_r = [1, 2, 3, 0] + perm_c = [1, 2, 3, 0, 4] else: - Yl = tf.transpose(Yl, perm=[1, 0, 2, 3]) - Yh = tuple( - [tf.transpose(x, perm=[1, 0, 2, 3, 4]) for x in Yh]) + perm_r = [1, 0, 2, 3] + perm_c = [1, 0, 2, 3, 4] + Yl = tf.transpose(Yl, perm=perm_r, name='Yl') + Yh = tuple( + [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) + for i, x in enumerate(Yh)]) return Yl, Yh @@ -380,7 +382,7 @@ def inverse(self, pyramid, gain_mask=None): return self._inverse_ops(pyramid, gain_mask) else: raise ValueError( - '''Unknown pyramid provided to inverse transform''') + 'Unknown pyramid provided to inverse transform') def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): ''' @@ -484,7 +486,7 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): s = P.X.get_shape().as_list() X = tf.reshape(P.X, [-1, num_channels, s[1], s[2]]) if data_format == "nhwc": - X = tf.transpose(X, [0, 2, 3, 1]) + X = tf.transpose(X, [0, 2, 3, 1], name='X') return X def _forward_ops(self, X, nlevels=3, include_scale=False, @@ -774,9 +776,6 @@ def _inverse_ops(self, pyramid, gain_mask=None): current_level - 1]) # Do even Qshift filters on columns. - no_decimate = False - this_size = Yh[current_level - 1].get_shape().as_list() - next_size = Yh[current_level - 2].get_shape().as_list() y1 = colifilt(Z, g0b, g0a) + colifilt(lh, g1b, g1a) if len(self.qshift) >= 12: diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index e6db0dd..7e910b7 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -49,7 +49,8 @@ def test_simple(): @skip_if_no_tf def test_specific_wavelet(): - Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), qshift=qshift('qshift_06')) + Yl, Yh = dtwavexfm2(mandrill, biort=biort('antonini'), + qshift=qshift('qshift_06')) @skip_if_no_tf @@ -67,7 +68,6 @@ def test_3d(): @skip_if_no_tf def test_simple_w_scale(): Yl, Yh, Yscale = dtwavexfm2(mandrill, include_scale=True) - assert len(Yscale) > 0 for x in Yscale: assert x is not None @@ -109,7 +109,7 @@ def test_rot_symm_modified(): # not that they work Yl, Yh, Yscale = dtwavexfm2(mandrill, biort='near_sym_b_bp', qshift='qshift_b_bp', include_scale=True) - Z = dtwaveifm2(Yl, Yh, biort='near_sym_b_bp', qshift='qshift_b_bp') + dtwaveifm2(Yl, Yh, biort='near_sym_b_bp', qshift='qshift_b_bp') @skip_if_no_tf @@ -164,7 +164,8 @@ def test_float32_input(): @skip_if_no_tf def test_eval_fwd(): - y = pyramid_ops.eval_fwd(mandrill) + # Test it runs without error + pyramid_ops.eval_fwd(mandrill) @skip_if_no_tf diff --git a/tests/test_tfcoldfilt.py b/tests/test_tfcoldfilt.py index 3dd8254..096038c 100644 --- a/tests/test_tfcoldfilt.py +++ b/tests/test_tfcoldfilt.py @@ -1,17 +1,14 @@ -import os - -import pytest +from pytest import raises import numpy as np -from dtcwt.coeffs import biort, qshift +from dtcwt.coeffs import qshift from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt from importlib import import_module -from pytest import raises -from .util import skip_if_no_tf - +from tests.util import skip_if_no_tf import tests.datasets as datasets + @skip_if_no_tf def test_setup(): global mandrill, mandrill_t, tf, coldfilt @@ -22,6 +19,7 @@ def test_setup(): mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -30,36 +28,42 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) + @skip_if_no_tf def test_odd_filter(): with raises(ValueError): coldfilt(mandrill_t, (-1,2,-1), (-1,2,1)) + @skip_if_no_tf def test_different_size(): with raises(ValueError): coldfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) + @skip_if_no_tf def test_bad_input_size(): with raises(ValueError): coldfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) + @skip_if_no_tf def test_good_input_size(): coldfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) + @skip_if_no_tf def test_good_input_size_non_orthogonal(): coldfilt(mandrill_t[:,:,:511], (1,1), (1,1)) + @skip_if_no_tf def test_output_size(): y_op = coldfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0]/2, mandrill.shape[1]) + @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] hb = qshift('qshift_b')[1] @@ -71,6 +75,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] @@ -81,6 +86,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] diff --git a/tests/test_tfcolfilter.py b/tests/test_tfcolfilter.py index 6e7f286..6a3a9f3 100644 --- a/tests/test_tfcolfilter.py +++ b/tests/test_tfcolfilter.py @@ -1,25 +1,23 @@ -import os - -import pytest - import numpy as np from dtcwt.coeffs import biort, qshift from dtcwt.numpy.lowlevel import colfilter as np_colfilter from importlib import import_module -from .util import skip_if_no_tf +from tests.util import skip_if_no_tf import tests.datasets as datasets + @skip_if_no_tf def test_setup(): global mandrill, mandrill_t, tf, colfilter tf = import_module('tensorflow') lowlevel = import_module('dtcwt.tf.lowlevel') colfilter = getattr(lowlevel, 'colfilter') - + mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -28,30 +26,35 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) + @skip_if_no_tf def test_odd_size(): y_op = colfilter(mandrill_t, [-1,2,-1]) assert y_op.get_shape()[1:] == mandrill.shape + @skip_if_no_tf def test_even_size(): y_op = colfilter(mandrill_t, [-1,-1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) + @skip_if_no_tf def test_qshift(): h = qshift('qshift_a')[0] y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) + @skip_if_no_tf def test_biort(): h = biort('antonini')[0] y_op = colfilter(mandrill_t, h) assert y_op.get_shape()[1:] == mandrill.shape + @skip_if_no_tf -def test_even_size(): +def test_even_size_batch(): zero_t = tf.zeros([1, mandrill.shape[0], mandrill.shape[1]], tf.float32) y_op = colfilter(zero_t, [-1,1]) assert y_op.get_shape()[1:] == (mandrill.shape[0]+1, mandrill.shape[1]) @@ -59,8 +62,8 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) + @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): h = qshift('qshift_b')[0] im = mandrill[0:4,0:4] @@ -71,6 +74,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] @@ -80,6 +84,7 @@ def test_equal_numpy_biort1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_biort2(): h = biort('near_sym_b')[0] @@ -91,6 +96,7 @@ def test_equal_numpy_biort2(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] @@ -100,6 +106,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift2(): h = qshift('qshift_c')[0] diff --git a/tests/test_tfcolifilt.py b/tests/test_tfcolifilt.py index 224bf4c..3cc57c8 100644 --- a/tests/test_tfcolifilt.py +++ b/tests/test_tfcolifilt.py @@ -1,17 +1,15 @@ -import os - import pytest +from pytest import raises import numpy as np from dtcwt.coeffs import qshift from dtcwt.numpy.lowlevel import colifilt as np_colifilt from importlib import import_module -from pytest import raises - -from .util import skip_if_no_tf +from tests.util import skip_if_no_tf import tests.datasets as datasets + @skip_if_no_tf def test_setup(): global mandrill, mandrill_t, tf, colifilt @@ -22,6 +20,7 @@ def test_setup(): mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -30,16 +29,19 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) + @skip_if_no_tf def test_odd_filter(): with raises(ValueError): colifilt(mandrill_t, (-1,2,-1), (-1,2,1)) + @skip_if_no_tf def test_different_size_h(): with raises(ValueError): colifilt(mandrill_t, (-1,2,1), (-0.5,-1,2,-1,0.5)) + @skip_if_no_tf def test_zero_input(): Y = colifilt(mandrill_t, (-1,1), (1,-1)) @@ -47,48 +49,55 @@ def test_zero_input(): y = sess.run(Y, {mandrill_t : [np.zeros_like(mandrill)]})[0] assert np.all(y[:0] == 0) + @skip_if_no_tf def test_bad_input_size(): with raises(ValueError): colifilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) + @skip_if_no_tf def test_good_input_size(): colifilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) + @skip_if_no_tf def test_output_size(): Y = colifilt(mandrill_t, (-1,1), (1,-1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) + @skip_if_no_tf def test_non_orthogonal_input(): Y = colifilt(mandrill_t, (1,1), (1,1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) + @skip_if_no_tf def test_output_size_non_mult_4(): Y = colifilt(mandrill_t, (-1,0,0,1), (1,0,0,-1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) + @skip_if_no_tf def test_non_orthogonal_input_non_mult_4(): Y = colifilt(mandrill_t, (1,0,0,1), (1,0,0,1)) assert Y.shape[1:] == (mandrill.shape[0]*2, mandrill.shape[1]) + @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] hb = qshift('qshift_b')[1] im = mandrill[0:4,0:4] im_t = tf.expand_dims(tf.constant(im, tf.float32), axis=0) - ref = np_coldfilt(im, ha, hb) - y_op = coldfilt(im_t, ha, hb) + ref = np_colifilt(im, ha, hb) + y_op = colifilt(im_t, ha, hb) with tf.Session() as sess: y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] @@ -99,6 +108,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index e07d173..43f79d4 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -1,15 +1,12 @@ import os import pytest -from pytest import raises - -import numpy as np -import tests.datasets as datasets from importlib import import_module from .util import skip_if_no_tf PRECISION_DECIMAL = 5 + @skip_if_no_tf def test_setup(): global tf, Transform2d, dtwavexfm2, dtwaveifm2 @@ -22,6 +19,7 @@ def test_setup(): # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" + @skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale", [ (2,False), @@ -43,12 +41,15 @@ def test_2d_input(nlevels, include_scale): for i in range(nlevels): extent = 512 * 2**(-(i+1)) - assert p.highpasses_ops[i].get_shape().as_list() == [1, extent, extent, 6] - assert p.highpasses_ops[i].dtype == tf.complex64 + assert (p.highpasses_ops[i].get_shape().as_list() == + [1, extent, extent, 6]) + assert (p.highpasses_ops[i].dtype == + tf.complex64) if include_scale: - assert p.scales_ops[i].get_shape().as_list() == [1, 2*extent, 2*extent] + assert (p.scales_ops[i].get_shape().as_list() == + [1, 2*extent, 2*extent]) assert p.scales_ops[i].dtype == tf.float32 - + @skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale", [ @@ -95,7 +96,8 @@ def test_2d_input_tuple(nlevels, include_scale): t = Transform2d() # Calling forward with a 2d input will throw a warning if include_scale: - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, + return_tuple=True) else: Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) @@ -112,7 +114,6 @@ def test_2d_input_tuple(nlevels, include_scale): if include_scale: assert Yscale[i].get_shape().as_list() == [1, 2*extent, 2*extent] assert Yscale[i].dtype == tf.float32 - @skip_if_no_tf @@ -135,12 +136,14 @@ def test_batch_input(nlevels, include_scale, batch_size): for i in range(nlevels): extent = 512 * 2**(-(i+1)) - assert p.highpasses_ops[i].get_shape().as_list() == [batch_size, extent, extent, 6] + assert (p.highpasses_ops[i].get_shape().as_list() == + [batch_size, extent, extent, 6]) assert p.highpasses_ops[i].dtype == tf.complex64 if include_scale: - assert p.scales_ops[i].get_shape().as_list() == [batch_size, 2*extent, 2*extent] + assert (p.scales_ops[i].get_shape().as_list() == + [batch_size, 2*extent, 2*extent]) assert p.scales_ops[i].dtype == tf.float32 - + @skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale, batch_size", [ @@ -153,7 +156,8 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) t = Transform2d() if include_scale: - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, + return_tuple=True) else: Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) @@ -168,9 +172,11 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): assert Yh[i].get_shape().as_list() == [batch_size, extent, extent, 6] assert Yh[i].dtype == tf.complex64 if include_scale: - assert Yscale[i].get_shape().as_list() == [batch_size, 2*extent, 2*extent] + assert (Yscale[i].get_shape().as_list() == + [batch_size, 2*extent, 2*extent]) assert Yscale[i].dtype == tf.float32 - + + @skip_if_no_tf @pytest.mark.parametrize("nlevels, include_scale, channels", [ (2,False,5), @@ -194,10 +200,10 @@ def test_multichannel(nlevels, include_scale, channels): for i in range(nlevels): extent = 512 * 2**(-(i+1)) - assert Yh[i].get_shape().as_list() == [None, extent, extent, channels, 6] + assert (Yh[i].get_shape().as_list() == + [None, extent, extent, channels, 6]) assert Yh[i].dtype == tf.complex64 if include_scale: - assert Yscale[i].get_shape().as_list() == [ + assert Yscale[i].get_shape().as_list() == [ None, 2*extent, 2*extent, channels] assert Yscale[i].dtype == tf.float32 - diff --git a/tests/test_tfrowdfilt.py b/tests/test_tfrowdfilt.py index b8c356e..45b4592 100644 --- a/tests/test_tfrowdfilt.py +++ b/tests/test_tfrowdfilt.py @@ -1,26 +1,24 @@ -import os - -import pytest from pytest import raises import numpy as np from importlib import import_module -from dtcwt.coeffs import biort, qshift +from dtcwt.coeffs import qshift from dtcwt.numpy.lowlevel import coldfilt as np_coldfilt -from .util import skip_if_no_tf +from tests.util import skip_if_no_tf import tests.datasets as datasets + @skip_if_no_tf def test_setup(): global mandrill, mandrill_t, rowdfilt, tf tf = import_module('tensorflow') lowlevel = import_module('dtcwt.tf.lowlevel') rowdfilt = getattr(lowlevel, 'rowdfilt') - mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -29,36 +27,43 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) + @skip_if_no_tf def test_odd_filter(): with raises(ValueError): rowdfilt(mandrill_t, (-1,2,-1), (-1,2,1)) + @skip_if_no_tf def test_different_size(): with raises(ValueError): rowdfilt(mandrill_t, (-0.5,-1,2,1,0.5), (-1,2,-1)) + @skip_if_no_tf def test_bad_input_size(): with raises(ValueError): rowdfilt(mandrill_t[:,:,:511], (-1,1), (1,-1)) + @skip_if_no_tf def test_good_input_size(): rowdfilt(mandrill_t[:,:511,:], (-1,1), (1,-1)) + @skip_if_no_tf def test_good_input_size_non_orthogonal(): rowdfilt(mandrill_t[:,:511,:], (1,1), (1,1)) + @skip_if_no_tf def test_output_size(): y_op = rowdfilt(mandrill_t, (-1,1), (1,-1)) assert y_op.shape[1:] == (mandrill.shape[0], mandrill.shape[1]/2) + @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +# @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): ha = qshift('qshift_b')[0] hb = qshift('qshift_b')[1] @@ -70,6 +75,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift1(): ha = qshift('qshift_c')[0] @@ -80,6 +86,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift2(): ha = qshift('qshift_c')[0] diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index 7ee9c1a..9a1712c 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -1,15 +1,12 @@ -import os - -import pytest - import numpy as np from importlib import import_module from dtcwt.coeffs import biort, qshift from dtcwt.numpy.lowlevel import colfilter as np_colfilter -from .util import skip_if_no_tf +from tests.util import skip_if_no_tf import tests.datasets as datasets + @skip_if_no_tf def test_setup(): global mandrill, mandrill_t, rowfilter, tf @@ -20,6 +17,7 @@ def test_setup(): mandrill = datasets.mandrill() mandrill_t = tf.expand_dims(tf.constant(mandrill, dtype=tf.float32),axis=0) + @skip_if_no_tf def test_mandrill_loaded(): assert mandrill.shape == (512, 512) @@ -28,30 +26,35 @@ def test_mandrill_loaded(): assert mandrill.dtype == np.float32 assert mandrill_t.get_shape() == (1, 512, 512) + @skip_if_no_tf def test_odd_size(): y_op = rowfilter(mandrill_t, [-1, 2, -1]) assert y_op.get_shape()[1:] == mandrill.shape + @skip_if_no_tf def test_even_size(): y_op = rowfilter(mandrill_t, [-1, -1]) assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) + @skip_if_no_tf def test_qshift(): h = qshift('qshift_a')[0] y_op = rowfilter(mandrill_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) + @skip_if_no_tf def test_biort(): h = biort('antonini')[0] y_op = rowfilter(mandrill_t, h) assert y_op.get_shape()[1:] == mandrill.shape + @skip_if_no_tf -def test_even_size(): +def test_even_size_batch(): h = tf.constant([-1,1], dtype=tf.float32) zero_t = tf.zeros([1, *mandrill.shape], tf.float32) y_op = rowfilter(zero_t, h) @@ -60,8 +63,9 @@ def test_even_size(): y = sess.run(y_op) assert not np.any(y[:] != 0.0) + @skip_if_no_tf -@pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') +# @pytest.mark.skip(reason='Cant pad by more than half the dimension of the input') def test_equal_small_in(): h = qshift('qshift_b')[0] im = mandrill[0:4,0:4] @@ -72,6 +76,7 @@ def test_equal_small_in(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_biort1(): h = biort('near_sym_b')[0] @@ -81,6 +86,7 @@ def test_equal_numpy_biort1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_biort2(): h = biort('near_sym_b')[0] @@ -92,6 +98,7 @@ def test_equal_numpy_biort2(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift1(): h = qshift('qshift_c')[0] @@ -101,6 +108,7 @@ def test_equal_numpy_qshift1(): y = sess.run(y_op) np.testing.assert_array_almost_equal(y[0], ref, decimal=4) + @skip_if_no_tf def test_equal_numpy_qshift2(): h = qshift('qshift_c')[0] From e79ebefd3efa77eea809590a7b6e87f0d0cbf1dd Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 7 Aug 2017 14:41:40 +0100 Subject: [PATCH 35/52] Added tf info to backends.rst --- docs/backends.rst | 54 +++++++++++++++++++++++++++++++++++--------- docs/reference.rst | 9 ++++++++ dtcwt/__init__.py | 6 +++++ dtcwt/tf/__init__.py | 5 ++-- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/docs/backends.rst b/docs/backends.rst index 6f5208b..95ae793 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -1,10 +1,11 @@ Multiple Backend Support ======================== -The ``dtcwt`` library currently provides two backends for computing the wavelet -transform: a `NumPy `_ based implementation and an OpenCL +The ``dtcwt`` library currently provides three backends for computing the wavelet +transform: a `NumPy `_ based implementation, an OpenCL implementation which uses the `PyOpenCL `_ -bindings for Python. +bindings for Python, and a Tensorflow implementation which uses the `Tensorflow +`_ bindings for Python. NumPy ''''' @@ -26,27 +27,51 @@ may not be full-featured. OpenCL support depends on the `PyOpenCL `_ package being installed and an OpenCL implementation being installed on your machine. Attempting to use an -OpenCL backen without both of these being present will result in a runtime (but +OpenCL backend without both of these being present will result in a runtime (but not import-time) exception. +Tensorflow +'''''''''' + +If you want to take advantage of having a GPU on your machine, +some transforms and algorithms have been implemented with a Tensorflow backend. +This backend, if present will provide an identical API to the NumPy backend. +NumPy-based input may be passed in to a tensorflow backend, in which case it +will be converted to a tensorflow variable, the transform performed, and then +converted back to a NumPy variable afterwards. + +Tensorflow support depends on the `Tensorflow +`_ python package being installed in the +current python environment, as well as the necessary CUDA + CUDNN libraries +installed). Attempting to use a Tensorflow backend without the python package +available will result in a runtime (but not import-time) exception. Attempting +to use the Tensorflow backend without the CUDA and CUDNN libraries properly +installed and linked will result in the Tensorflow backend being used, but +operations will be run on the CPU rather than the GPU. + +If you do not have a GPU, some speedup was still seen for using Tensorflow with +the CPU vs the plain NumPy backend. + Which backend should I use? ''''''''''''''''''''''''''' -The top-level transform routines, such as :py:class`dtcwt.Transform2d`, will +The top-level transform routines, such as :py:class:`dtcwt.Transform2d`, will automatically use the NumPy backend. If you are not primarily focussed on speed, this is the correct choice since the NumPy backend has the fullest feature support, is the best tested and behaves correctly given single- and double-precision input. If you care about speed and need only single-precision calculations, the OpenCL -backend can provide significant speed-up. On the author's system, the 2D -transform sees around a times 10 speed improvement. +or Tensorflow backends can provide significant speed-up. +On the author's system, the 2D transform sees around a times 10 speed +improvement for the OpenCL backend, and a 8-10 times speed up for the Tensorflow +backend. Using a backend ''''''''''''''' -The NumPy and OpenCL backends live in the :py:mod:`dtcwt.numpy` -and :py:mod:`dtcwt.opencl` modules respectively. Both provide +The NumPy, OpenCL and Tensorflow backends live in the :py:mod:`dtcwt.numpy`, +:py:mod:`dtcwt.opencl`, and :py:mod:`dtcwt.tf` modules respectively. All provide implementations of some subset of the DTCWT library functionality. Access to the 2D transform is via a :py:class:`dtcwt.Transform2d` instance. For @@ -74,8 +99,15 @@ switch to the OpenCL backend dtcwt.push_backend('opencl') # ... Transform2d, etc now use OpenCL ... -As is suggested by the name, changing the backend manipulates a stack behind -the scenes and so one can temporarily switch backend using +and to switch to the Tensorflow backend + +.. code-block:: python + + dtcwt.push_backend('tf') + # ... Transform2d, etc now use Tensorflow ... + +As is suggested by the name, changing the backend manipulates a stack behind the +scenes and so one can temporarily switch backend using :py:func:`dtcwt.push_backend` and :py:func:`dtcwt.pop_backend` .. code-block:: python diff --git a/docs/reference.rst b/docs/reference.rst index 483f835..feaf3ad 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -74,3 +74,12 @@ OpenCL .. automodule:: dtcwt.opencl.lowlevel :members: +Tensorflow +'''''''''' + +.. automodule:: dtcwt.tf + :members: + :inherited-members: + +.. automodule:: dtcwt.tf.lowlevel + :members: diff --git a/dtcwt/__init__.py b/dtcwt/__init__.py index 1971d59..ad397c0 100644 --- a/dtcwt/__init__.py +++ b/dtcwt/__init__.py @@ -38,6 +38,12 @@ 'Transform3d': dtcwt.numpy.Transform3d, 'Pyramid': dtcwt.opencl.Pyramid, }, + 'tf': { + 'Transform1d': dtcwt.numpy.Transform1d, + 'Transform2d': dtcwt.tf.Transform2d, + 'Transform3d': dtcwt.numpy.Transform3d, + 'Pyramid': dtcwt.tf.Pyramid, + }, } def _update_from_current_backend(): diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index f60203e..686e11a 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -1,6 +1,7 @@ """ -A backend which uses NumPy to perform the filtering. This backend should always -be available. +Provide low-level Tensorflow accelerated operations. This backend requires that +Tensorflow be installed. Works best with a GPU but still offers good +improvements with a CPU. """ From 551bb586119ca45f561e6f0602b963eb56f3b76f Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 7 Aug 2017 14:52:59 +0100 Subject: [PATCH 36/52] Fixed compatability import and removed splat from tests --- dtcwt/__init__.py | 3 ++- tests/test_tfTransform2d.py | 2 +- tests/test_tfrowfilter.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dtcwt/__init__.py b/dtcwt/__init__.py index ad397c0..c1f2674 100644 --- a/dtcwt/__init__.py +++ b/dtcwt/__init__.py @@ -19,6 +19,7 @@ import dtcwt.numpy import dtcwt.opencl +import dtcwt.tf # An array of dictionaries. Each dictionary stores the top-level module # variables for that backend. @@ -42,7 +43,7 @@ 'Transform1d': dtcwt.numpy.Transform1d, 'Transform2d': dtcwt.tf.Transform2d, 'Transform3d': dtcwt.numpy.Transform3d, - 'Pyramid': dtcwt.tf.Pyramid, + 'Pyramid': dtcwt.numpy.Pyramid, }, } diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 7e910b7..7a3806d 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -172,7 +172,7 @@ def test_eval_fwd(): def test_multiple_inputs(): y = pyramid_ops.eval_fwd(mandrill) y3 = pyramid_ops.eval_fwd([mandrill, mandrill, mandrill]) - assert y3.lowpass.shape == (3, *y.lowpass.shape) + assert y3.lowpass.shape == (3,) + y.lowpass.shape for hi3, hi in zip(y3.highpasses, y.highpasses): assert hi3.shape == (3, *hi.shape) for s3, s in zip(y3.scales, y.scales): diff --git a/tests/test_tfrowfilter.py b/tests/test_tfrowfilter.py index 9a1712c..8980cfe 100644 --- a/tests/test_tfrowfilter.py +++ b/tests/test_tfrowfilter.py @@ -56,7 +56,7 @@ def test_biort(): @skip_if_no_tf def test_even_size_batch(): h = tf.constant([-1,1], dtype=tf.float32) - zero_t = tf.zeros([1, *mandrill.shape], tf.float32) + zero_t = tf.zeros((1,) + mandrill.shape, tf.float32) y_op = rowfilter(zero_t, h) assert y_op.get_shape()[1:] == (mandrill.shape[0], mandrill.shape[1]+1) with tf.Session() as sess: From 268c86a40465c82d2a40d06fc411cf48e30afd56 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 7 Aug 2017 14:58:48 +0100 Subject: [PATCH 37/52] Removed splat from tf routines --- dtcwt/tf/common.py | 8 ++++---- dtcwt/tf/transform2d.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 44aaf03..4e3035b 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -126,12 +126,12 @@ def _get_X(self, Yl, Yh, sess=None): try: # Use dictionary comprehension to feed in our Yl and our # multiple layers of Yh - data = [Yl, *list(Yh)] - placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + data = [Yl,] + list(Yh) + placeholders = [self.lowpass_op, ] + list(self.highpasses_ops) X = sess.run(self.X, {i: d for i,d in zip(placeholders,data)}) except ValueError: - data = [Yl, *list(Yh)] - placeholders = [self.lowpass_op, *list(self.highpasses_ops)] + data = [Yl,] + list(Yh) + placeholders = [self.lowpass_op, ] + list(self.highpasses_ops) X = sess.run(self.X, {i: [d] for i,d in zip(placeholders,data)})[0] if close_after: diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 1ea822e..6b638cf 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -356,7 +356,7 @@ def inverse(self, pyramid, gain_mask=None): Lo_ph = tf.placeholder( tf.float32, [None, Yl.shape[0], Yl.shape[1]]) Hi_ph = tuple( - tf.placeholder(tf.complex64, [None, *level.shape]) + tf.placeholder(tf.complex64, (None,) + level.shape) for level in Yh) p_in = Pyramid_tf(None, Lo_ph, Hi_ph) size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) From 2bcfd5f015b0ffc1fd7c9c5e275d8a88d9fe47d1 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Fri, 11 Aug 2017 00:14:09 +0100 Subject: [PATCH 38/52] Add more tests to speed analysis nb --- dtcwt/tf/transform2d.py | 8 +- tests/Speed Tests.ipynb | 997 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1002 insertions(+), 3 deletions(-) create mode 100644 tests/Speed Tests.ipynb diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 1ea822e..adee2e7 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -210,9 +210,11 @@ def forward_channels(self, X, nlevels=3, include_scale=False, default), then data is in the form [batch, channels, h, w]. If the format is "nhwc", then the data is in the form [batch, h, w, c]. - :returns: A tuple of (Yl, Yh, Yscale). The order of output axes - will match the input axes (i.e. the position of the channel - dimension). I.e. (note that the spatial sizes will change) + :returns: tuple + A tuple of (Yl, Yh) or (Yl, Yh, Yscale) if include_scale was true. + The order of output axes will match the input axes (i.e. the + position of the channel dimension). I.e. (note that the spatial + sizes will change) Yl: [batch, c, h, w] OR [batch, h, w, c] Yh: [batch, c, h, w, 6] OR [batch, h, w, c, 6] Yscale: [batch, c, h, w, 6] OR [batch, h, w, c, 6] diff --git a/tests/Speed Tests.ipynb b/tests/Speed Tests.ipynb new file mode 100644 index 0000000..be67482 --- /dev/null +++ b/tests/Speed Tests.ipynb @@ -0,0 +1,997 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Speed Comparisons between the Numpy an TF implementations of the DTCWT\n", + "Operations were performed on a system with a GTX 1080 GPU and Intel Xeon CPU E5-2660 CPU" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-10T22:55:10.246398Z", + "start_time": "2017-08-10T22:55:08.676078Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import datasets\n", + "import dtcwt\n", + "import dtcwt.tf\n", + "import tensorflow as tf\n", + "from time import time\n", + "import numpy as np\n", + "import os\n", + "import py3nvml\n", + "plt.style.use('seaborn')\n", + "py3nvml.grab_gpus(1);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analysis of Small Images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "## DTCWT on a single small image (64x64)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:30:51.360177Z", + "start_time": "2017-08-07T11:30:49.828104Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "# Create the input\n", + "h, w = 64, 64\n", + "in_ = np.random.randn(1,h,w)\n", + "\n", + "# Set up the transforms\n", + "nlevels = 3\n", + "tf.reset_default_graph()\n", + "fwd = dtcwt.Transform2d() # Numpy Transform\n", + "fwd_tf = dtcwt.tf.Transform2d() # Tensorflow Transform\n", + "\n", + "in_placeholder = tf.placeholder(tf.float32, [None, h, w])\n", + "out_tf = fwd_tf.forward(in_placeholder, nlevels=nlevels)\n", + "out_fft = tf.fft2d(tf.cast(in_placeholder, tf.complex64))\n", + "\n", + "sess = tf.Session()\n", + "sess.run(tf.global_variables_initializer())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Numpy Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:30:56.101356Z", + "start_time": "2017-08-07T11:30:51.362870Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.63 ms ± 592 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "small_np = %timeit -o for i in in_: fwd.forward(i, nlevels=nlevels)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TF implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:30:56.989200Z", + "start_time": "2017-08-07T11:30:56.103634Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.26 ms ± 444 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "small_tf = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Comparison) Using an FFT in Tensorflow\n", + "We can safely assume that something like the FFT is an optimized, fast operation to do. This is a good yardstick to gauge the overheads with working on a GPU" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:30:57.470088Z", + "start_time": "2017-08-07T11:30:56.992097Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.82 ms ± 68.2 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit sess.run(out_fft, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DTCWT on a batch of small images (100x64x64)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:30:57.510947Z", + "start_time": "2017-08-07T11:30:57.473103Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "in_ = np.random.randn(100,h,w)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Numpy Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:02.709545Z", + "start_time": "2017-08-07T11:30:57.513113Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "642 ms ± 68.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "small_np_batch = %timeit -o for i in in_: fwd.forward(i, nlevels=nlevels)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TF implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:04.972253Z", + "start_time": "2017-08-07T11:31:02.713718Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.77 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "small_tf_batch = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Comparison) Using an FFT in Tensorflow" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:10.279027Z", + "start_time": "2017-08-07T11:31:04.975413Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6.53 ms ± 439 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit sess.run(out_fft, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Small Image Conclusion" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:10.504659Z", + "start_time": "2017-08-07T11:31:10.282293Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfQAAAHiCAYAAAAeWT4MAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XlcVPX+x/H3yAgqooIhaFpuZeWuoOE1KRQ1laLMtAXT\nW6npzWvmbpmiYotdvdqi/OzR7pIbZlbuYrmbkmXZpl5NBQxBEJTN8/uj29xIxREdBr68no9Hj0ee\nmTnzmTlzeM2cmQGbZVmWAABAqVbO3QMAAICrR9ABADAAQQcAwAAEHQAAAxB0AAAMQNABADAAQYfL\nzJkzR+PHj3f3GCXKwYMHde+996ply5Z677333D0OXGzHjh3q0KGD499hYWHaunXrRc87YcIEvf76\n68U1Ggxkd/cAKL1atmzp+P+zZ8/K09NTHh4ekqRJkyZp0KBB7hrNISoqSvfcc4969erl7lEkSfPm\nzVPbtm21YsUKt1x/VFSUEhISZLfbZbPZVLduXXXt2lX9+vWTp6enJkyYoJUrV0qScnNzZVmWPD09\nJUmtW7fWvHnzlJOTo7lz52rlypVKTk6Wn5+f2rZtqyFDhighIUGvv/66PvvsM8d19u/fX4mJiRcs\nCwkJ0ZtvvulYdrHH0D333FMcd0uJEB0d7e4RUMoRdBTZ3r17Hf8fFhamKVOmqF27dm6cqOTKy8uT\n3W7X8ePH1b1796tax9WaMGGCevXqpaysLH3zzTeKiYnRli1b9M477yg6OtoRltmzZ+s///mPpk+f\nXuDyQ4cOVVJSkqZPn67bbrtNZ8+e1ccff6xt27apQ4cOOnjwoE6dOiU/Pz/l5eXpwIEDqlixYoFl\nCQkJevrpp3kMAdcQh9zhMrNnz9aIESMkSb/++qsaNWqkpUuXKjQ0VMHBwVqwYIH27duniIgIBQUF\nXfAKZcmSJbr77rsVHBysxx9/XMeOHZMkWZalmJgYhYSEqFWrVoqIiNCPP/54wfXPmDFDu3fvVnR0\ntFq2bOlY/549e9SzZ0+1bt1aPXv21J49ey55G8LCwjR37lx169ZNwcHBGjt2rLKzsx2nb9y4Uffe\ne6+CgoLUp08fHThwoMBlY2NjFRERoRYtWqhv377asWOHY55Dhw4pIyNDo0aN0u2336677rpLb7zx\nhs6fPy9JWrZsmfr06aOYmBi1bdtWs2fPLrAsKChIHTt21J49e7Rs2TKFhoYqJCREy5cvd2r7VKpU\nSW3bttWbb76phIQEbdq06bKX2bp1q7Zu3ao33nhDzZo1k91ul4+Pjx555BH16tVLAQEBqlOnjnbt\n2iVJ+u6779SwYUMFBwcXWHb+/Hk1bdrUqTn/7HL310MPPaSXXnpJwcHBCgsLU3x8/CXXFRsbqzvu\nuEMtW7ZUly5dtG3bNkm/P26HDh2qESNGqGXLloqIiNChQ4c0d+5chYSEKDQ0VF9++aVjPUuXLtXd\nd9+tli1bqmPHjlq4cOEV3y5JGjNmjGbMmCHpf4fq/+///k8hISFq37691q1bp/j4eHXp0kVt2rTR\nnDlzHJfdt2+fevfuraCgILVv317R0dHKyclxnP7ll1+qS5cuat26tSZOnKhHH31Uixcvdpx+tfsa\nSgaCjmL19ddfa82aNZoxY4ZiYmI0Z84cvfPOO1q1apU+++wz7dy5U5K0bt06zZ07V6+99pq2bdum\n1q1b69lnn5X0+w+n3bt3a/Xq1frqq680c+ZMVatW7YLreuaZZxQUFKQJEyZo7969mjBhgtLS0jRw\n4EBFRUVpx44d6t+/vwYOHKjU1NRLzrxy5Uq99dZbWrt2rQ4dOqQ33nhD0u9hGjdunKKjo7Vjxw71\n7t1bgwcPLvCDdNWqVYqNjdXu3bv13nvvFZinXr16mjx5sjIyMrRu3Tq9//77WrFihZYuXeq4/L59\n+1SnTh1t2bJFTz31lGNZo0aNtGPHDvXo0UPDhw/XN998o7Vr1+qVV15RdHS0MjMznd4mtWrVUpMm\nTbR79+7Lnnfr1q1q1qyZatasecnz/Dneu3btUlBQkFq3bl1gWfPmzVW+fHmnZ/yDM/dXvXr1tH37\ndj3xxBMaP368LvbbrQ8ePKgPP/xQS5Ys0d69e/XWW2/p+uuvd5z+xxO1Xbt26dZbb9Xjjz+u8+fP\na/PmzRoyZIgmTJjgOG/16tU1d+5c7dmzR9OmTdO0adO0f//+K75tf/Xbb78pOztbmzdv1tChQ/Xc\nc8/p448/1tKlS/Xhhx/qjTfe0NGjRyVJ5cqV09ixY7V9+3YtXLhQ27Zt0/z58yVJp06d0tChQ/Xs\ns89qx44dqlevXoEjI9diX0PJQNBRrIYMGSIvLy+1b99elSpVUo8ePVS9enUFBAQoKChI3333nSRp\n4cKFGjBggBo0aCC73a5Bgwbp+++/17Fjx2S325WZmamDBw/Ksiw1aNBANWrUcOr6N23apBtvvFGR\nkZGy2+3q0aOH6tevr40bN17yMo888ohq1qypatWq6amnntKqVaskSYsWLVLv3r3VvHlzeXh46L77\n7lP58uWVkJDguGxUVJRq1qypChUqXLDe/Px8ffrpp3r22WdVuXJl1a5dW/3799fHH3/sOE+NGjUU\nFRUlu93uWEft2rXVs2dPeXh4qFu3bjpx4oSGDBkiT09PtW/fXp6enjpy5IhT98efr+f06dOXPV9a\nWpr8/f0LPU9wcLDjycHu3bsdQf/zsjZt2lzRfJJz91etWrX04IMPOrbHyZMn9dtvv12wLg8PD+Xk\n5OiXX35Rbm6uateurRtuuMFxelBQkO644w7Z7XZ17dpVqampGjBggMqXL69u3brp2LFjSk9PlyTd\neeeduuGGG2Sz2dSmTRv97W9/c+rJ0eXY7XY99dRTjutMTU1V3759VblyZd10001q2LChfvjhB0lS\nkyZN1KJFC9ntdtWuXVu9e/d2PIHavHmzbrrpJnXu3Fl2u119+/bVdddd57geV+1rKH68h45iVb16\ndcf/e3l5XfDvrKwsSdLx48cVExOjl156yXG6ZVlKSkpSSEiIHnnkEUVHR+vYsWPq3LmzRo8ercqV\nK1/2+pOTk1WrVq0Cy2rVqqWkpKRLXubPr0Zr1aql5ORkx4xxcXH64IMPHKfn5uY6Tv/rZf8qNTVV\nubm5Beb56yyBgYEXXO7P99kfkf/zD2gvL68reoUuSUlJSQU+5Hgp1apV0+HDhws9T3BwsMaPH6/T\np0/r66+/1vTp0+Xt7a2TJ0/q9OnT2rNnjx577LErmk9y7v768/1QsWJFSXI8pv7sxhtv1Lhx4zR7\n9mz9/PPPat++vcaMGaOAgABJF97Hvr6+jg/r/XGfZ2VlqUqVKoqPj9frr7+uw4cP6/z58zp37pxu\nvvnmK759f1WtWrULrvOv+8sf2/nQoUN68cUX9e233+rs2bPKz89X48aNJf3+mP/z48hmsxX4t6v2\nNRQ/XqGjRKpZs6YmTZqk3bt3O/7bt2+fWrVqJUnq27evli1bpk8//VSHDx/WvHnznFpvjRo1dPz4\n8QLLTpw44fhBfjEnTpxw/P/x48cdr1Bq1qypQYMGFZjx66+/Vo8ePRznt9lsl1yvr6+vypcvX2Ce\nv85S2OWvlRMnTmj//v0KCgq67HnbtWunffv2KTEx8ZLnqVOnjmrUqKFFixapZs2a8vb2liS1aNFC\nixYtUmZmplq0aHHFczpzf12JiIgILViwQBs3bpTNZrvgw3/OyMnJ0dChQ/X3v/9dW7Zs0e7du9Wh\nQ4eLHuZ3pYkTJ6p+/fpavXq19uzZo2eeecYxg7+/f4EnPZZlFdh+rtrXUPwIOkqkPn36KDY2Vj/9\n9JOk3z8M9cfXnvbt26evv/5aubm5qlixojw9PVWu3MUfytddd53jfUZJCg0N1eHDh7Vy5Url5eXp\n008/1c8//6w777zzkrPMnz9fiYmJSktL05w5c9StWzdJUq9evbRw4UJ9/fXXsixLWVlZ2rRpk86c\nOePUbfTw8FDXrl01Y8YMnTlzRseOHdPbb79dbF/VOnv2rHbu3KnBgwerWbNmCg0Nvexl2rVrp3bt\n2mnIkCH69ttvlZeXpzNnzmjBggVasmSJ43xBQUF65513CjxJaN26td555x01adLkom9BXM61vL8O\nHjyobdu2KScnR56envLy8rrkY6gwOTk5ysnJkZ+fn+x2u+Lj47Vly5YrXs/VyszMlLe3t7y9vfXL\nL79owYIFjtNCQ0P1ww8/aN26dcrLy9OHH35Y4G2Ia7Wvwf3YMiiRwsPD9cQTT2j48OFq1aqVevTo\noc2bN0v6/YfXc889pzZt2uiuu+5StWrV9Pjjj190PX379tXq1asVHBysKVOmyNfXV3PmzNHbb7+t\ntm3bat68eZozZ478/PwuOUuPHj3097//XZ06ddINN9zg+HBa06ZNNXnyZEVHRys4OFidO3fWsmXL\nruh2Pv/886pYsaI6deqkhx9+WD169FDPnj2vaB1X6o9P2bdr104xMTHq3Lmz5s2b5/QP6lmzZik0\nNNTxocOIiAh9++23Bb5uFhwcrJSUFLVu3dqxLCgoSCkpKQoODi7y7Nfq/srJydGrr76qtm3bqn37\n9jp16pSGDx9+xeupXLmynnvuOQ0bNkzBwcH65JNPFBYWdsXruVqjR4/WJ598olatWun55593POmU\nJD8/P/373//WK6+8orZt2+rnn39WkyZNHB9KvFb7GtzPZhX3sSGgFOG70TDN+fPn1aFDB02fPl23\n3367u8fBNcQrdAAw3BdffKH09HTl5OQ4vr9elM8xoGTjU+4AYLiEhASNGDFCOTk5atiwoV5//fUi\nfY4BJRuH3AEAMACH3AEAMABBBwDAAKX6PfSTJzPcPYLb+PpWUmrqhb8BCyUP26r0YFuVDmV5O/n7\n+1zyNF6hl1J2u4e7R4CT2FalB9uqdGA7XRxBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAAD\nEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDA\nAAQdAAADEHQAAAxgd/cAAMxW6eUYd4/gPG8vVcrMdvcUl5U1apy7R0AJxCt0AAAMQNABADAAQQcA\nwAAEHQAAAxB0AAAMQNABADAAQQcAwAAEHQAAAxB0AAAMQNABADAAQQcAwAAEHQAAAxB0AAAMQNAB\nADAAQQcAwAAEHQAAAxB0AAAMQNABADAAQQcAwAAEHQAAAxB0AAAMQNABADCAS4Oenp6uoUOHqmvX\nrrr77ru1d+9epaWlqX///urcubP69++v06dPS5Isy9KUKVMUHh6uiIgI7d+/35WjAQBgFJcGferU\nqbrjjjv0+eefa8WKFWrQoIFiY2MVEhKiNWvWKCQkRLGxsZKkzZs36/Dhw1qzZo0mT56siRMnunI0\nAACM4rKgZ2RkaNeuXXrggQckSZ6enqpSpYrWr1+vyMhISVJkZKTWrVsnSY7lNptNLVq0UHp6upKT\nk101HgAARnFZ0H/99Vf5+flp7NixioyM1Pjx45WVlaWUlBTVqFFDkuTv76+UlBRJUlJSkgIDAx2X\nDwwMVFJSkqvGAwDAKHZXrTgvL0/fffednn/+eTVv3lxTpkxxHF7/g81mk81mK/J1+PpWkt3ucbWj\nllr+/j7uHgFOKtPbytvL3RNcEe9SMK93WX48/VeZ3qcuwWVBDwwMVGBgoJo3by5J6tq1q2JjY1W9\nenUlJyerRo0aSk5Olp+fnyQpICBAiYmJjssnJiYqICCg0OtITc1y1fglnr+/j06ezHD3GHBCWd9W\nlTKz3T2C07y9vZRZCubNKsOPJ6ls71OFPZFx2SF3f39/BQYG6uDBg5Kkbdu2qUGDBgoLC1NcXJwk\nKS4uTh07dpQkx3LLspSQkCAfHx/HoXkAAFA4l71Cl6Tnn39eI0aMUG5ururUqaNp06bp/PnzGjZs\nmJYsWaJatWpp5syZkqTQ0FDFx8crPDxcFStWVExMjCtHAwDAKDbLsix3D1FUZfWQi1S2DzmVNmV9\nW1V6ufQ8OS81h9xHjXP3CG5VlvcptxxyBwAAxYegAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIO\nAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICg\nAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg\n6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIAB\nCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBg\nAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAawu3LlYWFh8vb2Vrly5eTh4aFly5YpLS1N\nzzzzjI4dO6brr79eM2fOVNWqVWVZlqZOnar4+HhVqFBBL774oho3buzK8QAAMIbLX6G/++67WrFi\nhZYtWyZJio2NVUhIiNasWaOQkBDFxsZKkjZv3qzDhw9rzZo1mjx5siZOnOjq0QAAMEaxH3Jfv369\nIiMjJUmRkZFat25dgeU2m00tWrRQenq6kpOTi3s8AABKJZcecpekxx9/XDabTb1791bv3r2VkpKi\nGjVqSJL8/f2VkpIiSUpKSlJgYKDjcoGBgUpKSnKc92J8fSvJbvdw7Q0owfz9fdw9ApxUpreVt5e7\nJ7gi3qVgXu+y/Hj6rzK9T12CS4O+YMECBQQEKCUlRf3791f9+vULnG6z2WSz2Yq8/tTUrKsdsdTy\n9/fRyZMZ7h4DTijr26pSZra7R3Cat7eXMkvBvFll+PEkle19qrAnMi495B4QECBJql69usLDw7Vv\n3z5Vr17dcSg9OTlZfn5+jvMmJiY6LpuYmOi4PAAAKJzLgp6VlaUzZ844/n/Lli266aabFBYWpri4\nOElSXFycOnbsKEmO5ZZlKSEhQT4+PoUebgcAAP/jskPuKSkpGjJkiCQpPz9fPXr0UIcOHdS0aVMN\nGzZMS5YsUa1atTRz5kxJUmhoqOLj4xUeHq6KFSsqJibGVaMBAGAcm2VZlruHKKqy+h6KVLbfQypt\nyvq2qvRy6XlyXmreQx81zt0juFVZ3qfc9h46AAAoHgQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAAD\nEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDA\nAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEA\nMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQA\nAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQd\nAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAzg8qDn5+crMjJSAwcOlCQdPXpUvXr1Unh4uIYN\nG6acnBxJUk5OjoYNG6bw8HD16tVLv/76q6tHAwDAGC4P+nvvvacGDRo4/j19+nT169dPa9euVZUq\nVbRkyRJJ0uLFi1WlShWtXbtW/fr10/Tp0109GgAAxnBp0BMTE7Vp0yY98MADkiTLsrR9+3Z16dJF\nknTfffdp/fr1kqQNGzbovvvukyR16dJF27Ztk2VZrhwPAABjuDToMTExGjlypMqV+/1qUlNTVaVK\nFdntdklSYGCgkpKSJElJSUmqWbOmJMlut8vHx0epqamuHA8AAGPYXbXijRs3ys/PT02aNNGOHTtc\nch2+vpVkt3u4ZN2lgb+/j7tHgJPK9Lby9nL3BFfEuxTM612WH0//Vab3qUtwWdD37NmjDRs2aPPm\nzcrOztaZM2c0depUpaenKy8vT3a7XYmJiQoICJAkBQQE6MSJEwoMDFReXp4yMjLk6+tb6HWkpma5\navwSz9/fRydPZrh7DDihrG+rSpnZ7h7Bad7eXsosBfNmleHHk1S296nCnsi47JD7s88+q82bN2vD\nhg3617/+pdtvv12vvvqq2rZtq9WrV0uSli9frrCwMElSWFiYli9fLklavXq1br/9dtlsNleNBwCA\nUYr9e+gjR47U22+/rfDwcKWlpalXr16SpAceeEBpaWkKDw/X22+/rREjRhT3aAAAlFo2qxR/lLys\nHnKRyvYhp9KmrG+rSi/HuHsEp5WaQ+6jxrl7BLcqy/uUWw65AwCA4kPQAQAwAEEHAMAABB0AAAMQ\ndAAADEDQAQAwAEEHAMAABB0AAAMQdAAADOD0H2c5d+6cTp48KS8vL9WoUcOVMwEAgCtUaNDPnz+v\nuLg4LV68WAcOHFDlypWVk5Mju92uTp06qV+/fqpXr15xzQoAAC6h0KD36dNHLVu21NixY9W4cWN5\nePz+t8dTUlL0xRdfaMKECerTp4+6d+9eLMMCAICLKzToc+bMkZ+f3wXLq1evrsjISEVGRurUqVMu\nGw4AADin0A/FXSzmKSkpSkhIKPQ8AACgeDn1KfeHH35YGRkZSk9PV2RkpMaPH6+XXnrJ1bMBAAAn\nORX0rKws+fj4aOPGjYqIiNDKlSv15Zdfuno2AADgJKeCnpOTI0nasWOH/va3v6lcuXKOD8gBAAD3\ncyrobdq0Ubdu3fTVV1+pTZs2Sk9PV7ly/E4aAABKCqd+scwLL7ygAwcOqE6dOipfvrwyMjI0ZcoU\nV88GAACc5FTQbTab6tatq8TERCUmJkqSPD09XToYAABwnlNBf++99zRjxgxVrVrVcajdZrNp/fr1\nLh0OAAA4x6mgv/vuu/r8888VEBDg6nkAAEAROPXJtsDAQGIOAEAJ5tQr9Kefflrjx49XaGiovLy8\nHMtDQ0NdNhgAAHCeU0HfuHGjNm7cqMOHDxd4D52gAwBQMjgV9LVr12rDhg2qUKGCq+cBAABF4NR7\n6HXq1JHd7lT7AQCAGzhV6RtvvFGPPfaYOnXqVOD754888ojLBgMAAM5zKui5ubm64YYb9OOPP7p6\nHgAAUAROBX3atGmungMAAFyFQt9D//bbbwu9cE5Ojn755ZdrOhAAALhyhb5Cj42NVVZWlnr06KHm\nzZvruuuuU3Z2tg4dOqQvvvhC8fHxGjNmjBo0aFBc8wIAgIsoNOizZs3Svn37tGjRIr3++utKTExU\nxYoVdfPNN6tTp0768MMPVbly5eKaFQAAXMJl30Nv1qyZmjVrVhyzAACAInLqe+gAAKBkI+gAABiA\noAMAYACCDgCAAZwKekpKikaMGOH4Va8HDhzQggULXDoYAABwnlNBf+6559S6dWulp6dLkurXr6/5\n8+e7dDAAAOA8p4KelJSkhx56SB4eHpIkT09Px99FBwAA7udUlf/6p1PT09NlWZZLBgIAAFfOqT/O\nEh4ergkTJigzM1PLli3T/Pnz1bNnT1fPBgAAnORU0J988kl9/PHHSk9PV3x8vKKionTvvfe6ejYA\nAOAkp4IuSffcc4/uueceV84CAACKyKmgp6Sk6IMPPtCRI0eUl5fnWP7vf//bZYMBAADnORX0wYMH\n67bbblNISIjjk+4AAKDkcCroZ8+e1QsvvODqWQAAQBE59bW15s2b64cffnD1LAAAoIiceoXep08f\nPfroowoMDJSXl5dj+ZIlS1w2GAAAcJ5TQR85cqQGDRqk2267jffQAQAogZwKupeXlx5//HFXzwIA\nAIrIqffQ77jjDm3evNnVswAAgCJy6hX6Rx99pNjYWHl7e8vT01OWZclms2nbtm2ung8AADjBqaAv\nXbrU1XMAAICr4FTQr7/+elfPAQAArkKhQR85cqReeeUV9ezZUzab7YLTC/vaWnZ2th555BHl5OQo\nPz9fXbp00dChQ3X06FENHz5caWlpaty4sV5++WV5enoqJydHo0aN0v79+1WtWjXNmDFDtWvXvvpb\nCABAGVBo0B977DFJ0ujRo694xZ6ennr33Xfl7e2t3NxcPfzww+rQoYPefvtt9evXT927d9eECRO0\nZMkSPfzww1q8eLGqVKmitWvXatWqVZo+fbpmzpxZtFsFAEAZU+in3OfPny9JatOmzUX/K4zNZpO3\nt7ckKS8vT3l5ebLZbNq+fbu6dOkiSbrvvvu0fv16SdKGDRt03333SZK6dOmibdu2ybKsq7t1AACU\nEYW+Qv/++++vauX5+fm6//77deTIET388MOqU6eOqlSpIrv996sNDAxUUlKSJCkpKUk1a9b8fSi7\nXT4+PkpNTZWfn98l1+/rW0l2e9n9RTf+/j7uHgFOKtPbytvr8ucpQbxLwbzeZfnx9F9lep+6BKf/\nHnpReHh4aMWKFUpPT9eQIUN08ODBa7r+1NSsa7q+0sTf30cnT2a4eww4oaxvq0qZ2e4ewWne3l7K\nLAXzZpXhx5NUtvepwp7IFBr0H3/8USEhIRcsv9LvoVepUkVt27ZVQkKC0tPTlZeXJ7vdrsTERAUE\nBEiSAgICdOLECQUGBiovL08ZGRny9fV1av0AAJR1hQa9bt26io2NLdKKT506JbvdripVqujcuXPa\nunWrnnzySbVt21arV69W9+7dtXz5coWFhUmSwsLCtHz5crVs2VKrV6/W7bffftFP1gMAgAsVGnRP\nT88ifwc9OTlZY8aMUX5+vizLUteuXXXXXXepYcOGeuaZZzRz5kzdeuut6tWrlyTpgQce0MiRIxUe\nHq6qVatqxowZRbpeAADKokKDXr58+SKv+JZbblFcXNwFy+vUqXPR7697eXlp1qxZRb4+AADKskK/\ntvbRRx8V1xwAAOAqOPXX1gAAQMlG0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAAD\nEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDA\nAAQdAACVHaXPAAAR0ElEQVQDEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQd\nAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABB\nBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMABBBwDAAAQdAAADEHQAAAxA\n0AEAMABBBwDAAAQdAAADEHQAAAxA0AEAMIDLgn7ixAlFRUWpW7du6t69u959911JUlpamvr376/O\nnTurf//+On36tCTJsixNmTJF4eHhioiI0P79+101GgAAxnFZ0D08PDRmzBh9+umnWrRokebPn6+f\nf/5ZsbGxCgkJ0Zo1axQSEqLY2FhJ0ubNm3X48GGtWbNGkydP1sSJE101GgAAxnFZ0GvUqKHGjRtL\nkipXrqz69esrKSlJ69evV2RkpCQpMjJS69atkyTHcpvNphYtWig9PV3JycmuGg8AAKMUy3vov/76\nq77//ns1b95cKSkpqlGjhiTJ399fKSkpkqSkpCQFBgY6LhMYGKikpKTiGA8AgFLP7uoryMzM1NCh\nQzVu3DhVrly5wGk2m002m63I6/b1rSS73eNqRyy1/P193D0CnFSmt5W3l7snuCLepWBe77L8ePqv\nMr1PXYJLg56bm6uhQ4cqIiJCnTt3liRVr15dycnJqlGjhpKTk+Xn5ydJCggIUGJiouOyiYmJCggI\nKHT9qalZrhu+hPP399HJkxnuHgNOKOvbqlJmtrtHcJq3t5cyS8G8WWX48SSV7X2qsCcyLjvkblmW\nxo8fr/r166t///6O5WFhYYqLi5MkxcXFqWPHjgWWW5alhIQE+fj4OA7NAwCAwrnsFfpXX32lFStW\n6Oabb9a9994rSRo+fLgGDBigYcOGacmSJapVq5ZmzpwpSQoNDVV8fLzCw8NVsWJFxcTEuGo0AACM\n47KgBwUF6YcffrjoaX98J/3PbDabXnjhBVeNAwCA0fhNcQAAGICgAwBgAIIOAIABCDoAAAYg6AAA\nGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoA\nAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIO\nAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICg\nAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg\n6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAZwWdDHjh2rkJAQ9ejRw7Es\nLS1N/fv3V+fOndW/f3+dPn1akmRZlqZMmaLw8HBFRERo//79rhoLAAAjuSzo999/v+bNm1dgWWxs\nrEJCQrRmzRqFhIQoNjZWkrR582YdPnxYa9as0eTJkzVx4kRXjQUAgJFcFvTg4GBVrVq1wLL169cr\nMjJSkhQZGal169YVWG6z2dSiRQulp6crOTnZVaMBAGAce3FeWUpKimrUqCFJ8vf3V0pKiiQpKSlJ\ngYGBjvMFBgYqKSnJcd5L8fWtJLvdw3UDl3D+/j7uHgFOKtPbytvL3RNcEe9SMK93WX48/VeZ3qcu\noViD/mc2m002m+2q1pGamnWNpil9/P19dPJkhrvHgBPK+raqlJnt7hGc5u3tpcxSMG9WGX48SWV7\nnyrsiUyxfsq9evXqjkPpycnJ8vPzkyQFBAQoMTHRcb7ExEQFBAQU52gAAJRqxRr0sLAwxcXFSZLi\n4uLUsWPHAssty1JCQoJ8fHwue7gdAAD8j8sOuQ8fPlw7d+5UamqqOnTooKeffloDBgzQsGHDtGTJ\nEtWqVUszZ86UJIWGhio+Pl7h4eGqWLGiYmJiXDUWAABGslmWZbl7iKIqq++hSGX7PaTSpqxvq0ov\nl54n6KXmPfRR49w9gluV5X2qxLyHDgAAXIOgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIAB\nCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBg\nAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAA\nGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoA\nAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIOAIABCDoAAAYg6AAAGICgAwBgAIIO\nAIABCDoAAAYg6AAAGICgAwBgAIIOAIAB7O4e4M82b96sqVOn6vz58+rVq5cGDBhQrNdf6eWYYr2+\nq+LtpUqZ2e6ewilZo8a5ewQAMF6JeYWen5+v6OhozZs3T6tWrdInn3yin3/+2d1jAQBQKpSYV+j7\n9u3TjTfeqDp16kiSunfvrvXr16thw4Zungwl0csve7p7BKd5e0uZmaVj3lGjctw9AtyotOxX7FMX\nV2JeoSclJSkwMNDx74CAACUlJblxIgAASo8S8wq9KPz9fa7tCl+Zdm3X52Le7h7ASa6Y85VXXLBS\nl/Jy9wBOcsGc7FfXnKtmLF37VRnepy6hxLxCDwgIUGJiouPfSUlJCggIcONEAACUHiUm6E2bNtXh\nw4d19OhR5eTkaNWqVQoLC3P3WAAAlAol5pC73W7XhAkT9MQTTyg/P189e/bUTTfd5O6xAAAoFWyW\nZVnuHgIAAFydEnPIHQAAFB1BBwDAAAS9FOrTp0+RL7ts2TJFR0dfw2nMk56erg8//LDAspdeeknd\nu3fXSy+9dNnLJyUlaejQoUW+/tmzZ+utt94q8uVxeUXZD+bMmXPZ84wZM0aff/55Uccy2pXsV1e6\nD1xs3RcTFRWlb775xun1ljYEvRRauHChu0cwWnp6uhYsWFBg2UcffaSPP/5Yo0ePvuzlAwICNGvW\nLFeNBzeZO3euu0co1a52v7rSdZdFJeZT7ib69ddf9eSTT6p169bau3evAgIC9MYbb6hChQqKiopS\no0aNtGvXLuXn5ysmJkbNmjUrcPmffvpJY8eOVW5urs6fP6/Zs2erbt26atmypfbu3asdO3botdde\nk6+vr3788Uc1btxY06dPl81mU3x8vKZNm6ZKlSqpVatWOnr06AU/kE6dOqUXXnhBx48flySNGzdO\nrVu3Lrb7p6R69dVXdeTIEd17771q166dDh06pKysLN1///0aOHCgunXr5jjvzp07NXXqVEmSzWbT\nBx98oLS0NA0aNEiffPKJli1bpg0bNujs2bM6evSoOnXqpFGjRkmSFi9erHnz5snHx0e33HKLPD09\nNWHChAKzHDlyRJMmTVJqaqoqVKigyZMnq0GDBsV3Z5RQV7tvSdKJEycUFRWlpKQk3XPPPfrHP/4h\nSRo8eLASExOVnZ2tvn37qnfv3po+fbrOnTune++9Vw0bNtSrr76quLg4vfXWW7LZbGrUqJFe+e9v\nZdm9e7feeecdnTx5UiNHjlTXrl2L9b4pqa5kv5KkAwcOqHfv3kpNTdUTTzyhBx98UJmZmRo8eLDS\n09OVl5enf/7zn+rUqdMF6x49erRiY2O1cuVK2Ww2dejQQSNGjJAkff7555o0aZIyMjI0depUBQUF\nuePucA0LLnP06FHr1ltvtb777jvLsixr6NChVlxcnGVZlvXoo49a48ePtyzLsnbu3Gl17979gstH\nR0dbK1assCzLsrKzs62zZ89almVZLVq0sCzLsrZv3261atXKOnHihJWfn289+OCD1q5du6xz585Z\nHTp0sI4cOWJZlmU988wz1oABAyzLsqylS5dakyZNsizLsoYPH27t2rXLsizLOnbsmNW1a1eX3A+l\nzdGjRy/YHn/c5381cOBAa/fu3ZZlWdaZM2es3NzcApdfunSpFRYWZqWnp1vnzp2z7rzzTuv48eNW\nYmKiddddd1mpqalWTk6O9dBDDzm2y6xZs6x58+ZZlmVZffv2tQ4dOmRZlmUlJCRYUVFRrrjJpc7V\n7ltLly61/va3v1mnTp2yzp49a3Xv3t3at2+fZVmWlZqaalmW5Vh+6tQpy7IKPgZ+/PFHq3PnzlZK\nSkqBy4wePdp6+umnrfz8fOunn36yOnXq5IqbXypdyX41a9YsKyIiwjp79qyVkpJidejQwUpMTLRy\nc3OtjIwMy7IsKyUlxerUqZN1/vz5C9a9adMmq3fv3lZWVpZlWf/bPo8++qg1bdo0x3kee+yxa30z\n3YpX6C5Wu3Zt3XrrrZKkxo0b69ixY47TunfvLkkKDg7WmTNnlJ6eripVqjhOb9GihebMmaPExER1\n7txZdevWvWD9zZo1c/wO/FtuuUXHjh2Tt7e36tSpU+AP3Xz00UcXXHbr1q0F/qLdmTNnlJmZKW/v\n0vDLL0uGVq1a6cUXX1RERIQ6d+580fsuJCREPj6//5riBg0a6NixY0pLS1NwcLCqVasmSeratasO\nHz5c4HKZmZnau3ev/vnPfzqW5eTwx1P+cDX7liS1a9dOvr6+kqTw8HB99dVXatq0qd5//32tXbtW\n0u+v4v/zn/84zveH7du3q2vXrvLz85Mkx3aUpE6dOqlcuXJq2LChfvvtt2t8q8uOjh07qkKFCqpQ\noYLatm2rb775RqGhofrXv/6lXbt2qVy5ckpKSrrofbxt2zbdf//9qlixoqSC2yc8PFzShY8ZExB0\nF/P0/N9fBPLw8FB29v/+hrnNZitw3r/+OyIiQs2bN9emTZs0YMAATZo0SSEhIYWuPz8/3+nZzp8/\nr48++kheXqXldyKXPAMGDFBoaKji4+P10EMPad68eRfcn0XdRpZlqUqVKlqxYsU1ndkUV7NvXeo8\nO3bs0NatW7Vo0SJVrFhRUVFRBdZ7pXOh6C62zVauXKlTp05p2bJlKl++vMLCwoq8fcqVK3dFPy9L\nAz4U50affvqppN/fc/Px8XG8ivvD0aNHVadOHfXt21cdO3bUDz/84NR669Wrp6NHj+rXX38tcD1/\n1b59e73//vuOf3///fdFuRnG8fb2VmZmplPnPXLkiBo1aqQBAwaoadOmOnTokFOXa9q0qXbt2qXT\np08rLy9Pa9asueA8lStXVu3atfXZZ59J+j3wBw4ccP6GlGGX27ckacuWLUpLS9O5c+e0bt06tWrV\nShkZGapataoqVqyoX375RQkJCY7z2+125ebmSpJuv/12ff7550pNTZUkpaWlFcOtKt2uZL+SpPXr\n1ys7O1upqanauXOnmjZtqoyMDFWvXl3ly5fX9u3bHa+w/7rudu3aadmyZTp79qyksrN9eIXuRl5e\nXoqMjFReXp5iYmIuOP2zzz7TihUrZLfbdd1112ngwIFOrbdChQp64YUX9MQTT6hSpUpq0qTJRc83\nfvx4RUdHKyIiQvn5+QoKCuIrbZJ8fX3VqlUr9ejRQ3fccUehn8B99913tWPHDtlsNt10003q0KGD\nkpOTL3sdAQEBGjhwoHr16qWqVauqfv36F43OK6+8ookTJ+rNN99UXl6eunXrpltuueWqbl9ZcLl9\nS/r97aqnn37a8aG4pk2bqlGjRlq4cKHuvvtu1atXTy1atHCc/8EHH9Q999yj2267Ta+++qoGDRqk\nqKgolStXTrfddptefPHF4rp5pdKV7FeS1KhRI/Xt21epqakaPHiwAgICFBERoaeeekoRERFq0qSJ\n6tevf8l1HzhwQD179lT58uUVGhqq4cOHF8fNdCt+9aubREVFadSoUWratKlL1v/He+GWZWnSpEmq\nW7eu+vXr55LrQtH8sY3y8vL0j3/8Qz179nS8v4eic/W+BZRUvEI31OLFi7V8+XLl5ubq1ltvVe/e\nvd09Ev7itdde09atW5Wdna327durU6dO7h4JQCnGK3QAAAzAh+IAADAAQQcAwAAEHQAAAxB0AAAM\nQNABADAAQQcAwAD/DzOv+htPD7E0AAAAAElFTkSuQmCC\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "objects = ('np single', 'tf single', 'np batch', 'tf batch')\n", + "y_pos = np.arange(len(objects))\n", + "performance = [small_np.average, small_tf.average, small_np_batch.average, small_tf_batch.average]\n", + "performance = [i*1000 for i in performance]\n", + "fig, ax = plt.subplots(1, figsize=(8,8))\n", + "ax.bar(y_pos, performance, align='center', alpha=0.5, color=['red', 'blue'])\n", + "plt.xticks(y_pos, objects)\n", + "plt.ylabel('Time (ms)')\n", + "plt.title('Times to perform DTCWT on small images')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analysis of Large Images" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DTCWT on a single large image (512x512)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:11.373219Z", + "start_time": "2017-08-07T11:31:10.506792Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "# Create the input\n", + "h, w = 512, 512\n", + "in_ = np.random.randn(1,h,w)\n", + "\n", + "# Set up the transforms\n", + "nlevels = 3\n", + "tf.reset_default_graph()\n", + "fwd = dtcwt.Transform2d() # Numpy Transform\n", + "fwd_tf = dtcwt.tf.Transform2d() # Tensorflow Transform\n", + "\n", + "in_placeholder = tf.placeholder(tf.float32, [None, h, w])\n", + "out_tf = fwd_tf.forward(in_placeholder, nlevels=nlevels)\n", + "out_fft = tf.fft2d(tf.cast(in_placeholder, tf.complex64))\n", + "\n", + "sess = tf.Session()\n", + "sess.run(tf.global_variables_initializer())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Numpy Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:16.031492Z", + "start_time": "2017-08-07T11:31:11.375442Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "57.3 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "large_np = %timeit -o for i in in_: fwd.forward(i, nlevels=nlevels)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TF implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:18.058548Z", + "start_time": "2017-08-07T11:31:16.033601Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.39 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "large_tf = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Comparison) Using an FFT in Tensorflow" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:23.107977Z", + "start_time": "2017-08-07T11:31:18.061652Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6.19 ms ± 325 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%timeit sess.run(out_fft, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DTCWT on a batch of large images (100x512x512)\n", + "Batches are something tensorflow naturally handles. This should widen the gap as we only have to copy data to the GPU once for multiple images" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:31:24.473872Z", + "start_time": "2017-08-07T11:31:23.112698Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "in_ = np.random.randn(100,512,512)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Numpy Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:08.472588Z", + "start_time": "2017-08-07T11:31:24.477445Z" + }, + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.44 s ± 189 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "large_np_batch = %timeit -o for i in in_: fwd.forward(i, nlevels=nlevels).lowpass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TF implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:09.632071Z", + "start_time": "2017-08-07T11:32:08.474983Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "124 ms ± 4.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "large_tf_batch = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Comparison) Using an FFT in Tensorflow" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:10.702518Z", + "start_time": "2017-08-07T11:32:09.634609Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "113 ms ± 2.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit sess.run(out_fft, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DTCWT on a batch of large images with a convolution afterwards\n", + "This again should widen the gap, as having already calculated something on the GPU, we don't need to transfer the data there again" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:12.112465Z", + "start_time": "2017-08-07T11:32:10.706380Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "h, w = 512, 512\n", + "in_ = np.random.randn(100, h, w)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Numpy Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:12.156396Z", + "start_time": "2017-08-07T11:32:12.114544Z" + }, + "collapsed": true + }, + "outputs": [], + "source": [ + "fwd = dtcwt.Transform2d()\n", + "tf.reset_default_graph()\n", + "sess = tf.Session()\n", + "highs = tf.placeholder(tf.float32, [None, h>>3, w>>3, 6])\n", + "weights = tf.get_variable('weights', shape=(5,5,6,64))\n", + "step = tf.nn.conv2d(highs, weights, strides=[1,1,1,1], padding='SAME')\n", + "sess.run(tf.global_variables_initializer())" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:57.965489Z", + "start_time": "2017-08-07T11:32:12.159253Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.7 s ± 139 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "large_np_conv = %timeit -o sess.run(step, {highs: [abs(fwd.forward(i, nlevels=3).highpasses[2]) for i in in_]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TF Implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:32:58.860531Z", + "start_time": "2017-08-07T11:32:57.968404Z" + }, + "collapsed": true, + "scrolled": false + }, + "outputs": [], + "source": [ + "tf.reset_default_graph()\n", + "in_placeholder = tf.placeholder(tf.float32, [None, h, w])\n", + "fwd_tf = dtcwt.tf.Transform2d() \n", + "out_tf = fwd_tf.forward(in_placeholder, nlevels=3)\n", + "p = tf.abs(out_tf.highpasses_ops[2])\n", + "weights = tf.get_variable('weights', shape=(5,5,6,64))\n", + "out = tf.nn.conv2d(p, weights, strides=[1,1,1,1], padding='SAME')\n", + "sess = tf.Session()\n", + "sess.run(tf.global_variables_initializer())" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T11:33:10.477651Z", + "start_time": "2017-08-07T11:32:58.863436Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "143 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "large_tf_conv = %timeit -o sess.run(out, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running DTCWT with TF backend on a CPU\n", + "Perhaps there is still a speed-up when using a CPU and tensorflow?" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T12:03:40.293069Z", + "start_time": "2017-08-07T12:03:28.964177Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12.8 ms ± 389 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "# Create the input\n", + "h, w = 512, 512\n", + "in_ = np.random.randn(1,h,w)\n", + "\n", + "# Set up the transforms\n", + "nlevels = 3\n", + "tf.reset_default_graph()\n", + "with tf.device(\"/cpu:0\"):\n", + " fwd_tf = dtcwt.tf.Transform2d() # Tensorflow Transform\n", + " in_placeholder = tf.placeholder(tf.float32, [None, h, w])\n", + " out_tf = fwd_tf.forward(in_placeholder, nlevels=nlevels)\n", + "\n", + "sess = tf.Session()\n", + "sess.run(tf.global_variables_initializer())\n", + "large_tf_cpu = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T12:03:47.646900Z", + "start_time": "2017-08-07T12:03:42.065636Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "516 ms ± 17.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "in_ = np.random.randn(100,h,w)\n", + "large_tf_batch_cpu = %timeit -o sess.run(out_tf.lowpass_op, {in_placeholder: in_})" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T12:04:18.946481Z", + "start_time": "2017-08-07T12:04:13.202643Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "610 ms ± 27.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "tf.reset_default_graph()\n", + "with tf.device(\"/cpu:0\"):\n", + " in_placeholder = tf.placeholder(tf.float32, [None, h, w])\n", + " fwd_tf = dtcwt.tf.Transform2d() \n", + " out_tf = fwd_tf.forward(in_placeholder, nlevels=3)\n", + " p = tf.abs(out_tf.highpasses_ops[2])\n", + " weights = tf.get_variable('weights', shape=(5,5,6,64))\n", + " out = tf.nn.conv2d(p, weights, strides=[1,1,1,1], padding='SAME')\n", + "sess = tf.Session()\n", + "sess.run(tf.global_variables_initializer())\n", + "large_tf_conv_cpu = %timeit -o sess.run(out, {in_placeholder: in_})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Large Image Conclusion" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-07T12:05:41.444267Z", + "start_time": "2017-08-07T12:05:40.930019Z" + }, + "scrolled": false + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAtoAAALSCAYAAADjvXZeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs/X2cV3WB//8/B0ZQkUsXBi9YNy/Xi5RREFCSjyiYIIoB\nq3lVZLmSlWSRtpaaGlurqantGurH7LNpXiRYN+3jdSKVVymr9dH8mrkpODOGCAgKDJ7fH67vnwjS\nqLwG0Pv9L+e83+d9Xu/XnDM+5nDmvOuqqqoCAACsVR3W9QAAAOCDSGgDAEABQhsAAAoQ2gAAUIDQ\nBgCAAoQ2AAAUILThfbjsssty+umnr+thrFeeeeaZHHbYYWlsbMyPf/zjdT2cXHLJJfnqV7+aJJk7\nd24aGxuzYsWKJMlf//rXHH300WlsbMx3vvOdVFWVr3/96xk4cGDGjx+/LofNOnDGGWfkBz/4wboe\nxnty00035ZOf/OS6HkaSDXseYW2rX9cDgPVZY2Nj7b9fffXVdOrUKR07dkySfOtb38qJJ564roZW\nc+yxx+bQQw/NhAkT1vVQkiRXXHFFBg0alJtvvnldD2UVW265ZR599NHa19ddd1169uyZRx55JHV1\ndXn44Yfz61//Ovfee2823XTTdh/f8OHDc+6552afffZZ7eMPPPBAPvWpT2WTTTZJknTt2jWNjY05\n/vjjs/vuu2fu3LkZPXp07flLlizJJptskrq6uiTJ5ZdfngEDBuSxxx7LJZdckkcffTQdOnTI3//9\n3+eTn/xkxo0bl4MOOignn3xyRo0alST53e9+l6OOOioXXnjhSss++9nP5vjjj8+VV16ZJGltbU1r\na2s23njjJG/M9S233FJmotaCm266KTfccEOuvfba2rKzzz57HY7og8M8wv+f0IY1eGuU/a0I+rBr\nbW1NfX39KrH3Xl6jvcydOzfbbbddLUTnzJmTrbba6j1FdnuNvU+fPpk5c2aqqkpzc3Ouu+66HH30\n0Zk2bVqGDBmy0j6700475eabb84222xTW/boo4/mM5/5TCZNmpTvfve76dmzZ/7whz/k8ssvz7hx\n4zJw4MA89NBDtah++OGHs+22266yrLGxMV/4whfyhS98Icnqw3Vdae/9aENnvqAcl47A+/DWyxKe\nf/757LTTTvnZz36WYcOGZeDAgbn22mvz2GOPZcyYMRkwYMAqZ3puvPHGHHzwwRk4cGCOP/74zJkz\nJ0lSVVWmTp2aIUOGZM8998yYMWPy1FNPrbL9Cy+8MA8//HDOPvvsNDY21l7/kUceybhx47LXXntl\n3LhxeeSRR97xPQwfPjw//OEPM2rUqAwcODBf//rXs3Tp0trj99xzTw477LAMGDAgRx55ZJ588smV\n1p02bVrGjBmT/v3757jjjssDDzxQG8+f//znLFq0KF/72tcyePDg7L///vn3f//3vP7660neiLMj\njzwyU6dOzaBBg3LJJZestGzAgAE54IAD8sgjj+Smm27KsGHDMmTIkEyfPv0d389zzz2XY445Jo2N\njZk4cWLmz59fe+zN71Fra2tOO+20zJgxI1deeWUaGxvz05/+NN/4xjcye/bsNDY25uKLL37X77+1\ntTXNzc354he/mMGDB2f48OErXT5zySWX5OSTT87Xvva1NDY2ZvTo0Xn88ceTJFOmTMncuXNz4okn\nprGxMZdffvk7vsckqaurS9++fXPyySdnwoQJOe+889b4/Df927/9W8aOHZsTTjghvXr1Sl1dXXbb\nbbd8//vfT5IMGDAgDz/8cO35Dz/8cD73uc+tsmzAgAFt2t7b3XXXXRk9enQGDBiQY489Nn/6059q\nj63NfbG1tTXTpk3LgQcemMbGxowaNSp33HFHkuRPf/pTzjzzzNr3+s33ctppp+XCCy+sveb111+f\nESNGZO+9986JJ56Y5ubm2mM77bRTrr322owcOTIDBgzIt771rbz5Qcv//d//nWOOOSZ77bVXBg0a\nlMmTJ7/n+bjyyiszZsyY7LXXXpk8efJK87Em5557boYNG5Y999wzn/jEJ1b6/l1yySX50pe+lK9+\n9avZc889M3369Lz22ms59dRTM3DgwBx88MG5/PLLs99++9XWWdN+/XZvnccHHngg++23Xy6//PIM\nGTIkQ4cOzZ133pl77703Bx10UPbee+9cdtlltXUfe+yxHHHEERkwYECGDh2as88+O8uWLas9PmvW\nrBx00EHZa6+9ctZZZ+WYY47JDTfcUHv8/f5MhbWuAtpk//33r37961+vtOziiy+uvvKVr1RVVVXP\nPfdcteOOO1bf/OY3q9dee6267777qt12262aNGlS9de//rVqamqqBg8eXD3wwANVVVXVHXfcUR14\n4IHV008/XS1fvrz6wQ9+UB1xxBFVVVXVzJkzq8MPP7xasGBB9frrr1dPP/101dzcvNpxHXPMMdX1\n119f+3r+/PnVgAEDqunTp1fLly+vfvGLX1QDBgyoXnrppXd8X6NHj67mzp1bzZ8/vzriiCOqCy64\noKqqqvrDH/5QDR48uJo9e3bV2tpa3XTTTdX+++9fLV26tLbuoYceWs2dO7d69dVXVzueKVOmVCee\neGK1aNGi6rnnnqtGjhxZe/xnP/tZtfPOO1c//vGPq+XLl1evvvpqbdmNN95Ytba2VhdccEE1bNiw\n6qyzzqqWLl1a3XfffVX//v2rV155ZbXv55/+6Z+qqVOnVkuXLq0efPDBqn///qt8j5YvX15VVVWd\neuqptff65niOPPLI2tfv9v2vWLGiOvzww6tLLrmkWrp0afWXv/ylGj58eDVz5sza/rLbbrtVv/rV\nr6rW1tbq/PPPryZMmLDS9+Lt+9hb3X///dXHPvaxVZb/5je/qXbaaadq8eLFKy3fcccdq2effbb2\n9ZIlS6p//Md/rH7729++4zaef/75aqeddqrmz59frVixoho8eHD16quvVvvtt19t2Z577lk9+OCD\nK6339rlbnWeeeabaY489qlmzZlXLli2rpk2bVh144IErzefa3BdvvfXWqqmpqVqxYkV1yy23VHvs\nsUftOFrdeN+6P/zmN7+p9t577+r3v/99tXTp0urss8+ujjrqqJXm9oQTTqgWLFhQzZkzpxo0aFB1\n7733VlVVVV/+8perf//3f69WrFhRvfbaa9VDDz30nudj3LhxVVNTUzV//vzq4x//eHXNNdes9rXe\n/n5mzJhRvfTSS9Xy5curK6+8stpnn32q1157raqqN/bDXXbZpbrjjjuqFStWVK+++mp13nnnVUcf\nfXT18ssvVy+88EJ1yCGH1Pa1v7Vfv91b5/H++++vdt555+qSSy6pli1bVl133XXVoEGDqlNOOaVa\ntGhR9dRTT1Uf/ehHq7/85S9VVVXV448/Xj366KPV8uXLq+eee676+Mc/Xl111VVVVVXVvHnzqsbG\nxuq2226rli9fXv3oRz+qdtlll9rPk7X1MxXWJme0YS076aST0rlz5wwdOjSbbrppDjnkkGy++eZp\naGjIgAED8v/+3/9Lkvz0pz/NCSeckO222y719fU58cQT88QTT2TOnDmpr6/P4sWL88wzz6Sqqmy3\n3Xbp06dPm7b/q1/9Kttss03Gjh2b+vr6HHLIIdl2221zzz33vOM6Rx99dLbYYov06NEjkyZNql1b\ne9111+WII47IHnvskY4dO+bwww/PRhttlNmzZ9fWPfbYY7PFFlvUrs19qxUrVuTWW2/NV77ylWy2\n2WbZeuutM3HixPz85z+vPadPnz459thjU19fX3uNrbfeOuPGjUvHjh0zatSovPDCCznppJPSqVOn\nDB06NJ06dcpf/vKXVbY3d+7cPP744zn55JPTqVOnDBw4MMOHD2/TvK3Ou33/jz/+eF566aV84Qtf\nSKdOndKvX7/80z/9U2699dba8/faa68MGzYsHTt2zGGHHbbSWdn3qk+fPqmqKosWLVrj8xYuXJjX\nX389vXv3fsfnbLXVVtlyyy3z8MMP58knn8w222yTjTfeOHvuuWdt2fLly7PHHnu863HeeuutGTZs\nWPbdd99stNFGOf744/Paa6+tdLnL2twXDz744DQ0NKRDhw4ZNWpUttlmmzz22GNtGusvfvGLjBs3\nLrvuums6deqUU045JbNnz87zzz9fe87nPve5dOvWLVtuuWUGDRpU+16+eQlVS0tLOnfu/I5n/9sy\nH8cee2waGhrSo0eP7L///nniiSfaNP7DDjssPXv2TH19fT7zmc9k2bJl+fOf/1x7vH///jnwwAPT\noUOHbLzxxvnlL3+Zf/7nf0737t3Tt2/fHHfccbXntmW/XpP6+vpMmjQpG220UUaNGpX58+fnuOOO\ny2abbZYddtgh22+/ff74xz8mSXbbbbf0798/9fX12XrrrXPEEUfkoYceSpLMnDkzO+ywQ0aOHJn6\n+vocd9xx+bu/+7vadkr9TIX3w0VZsJZtvvnmtf/u3LnzKl8vWbIkyRtROHXq1Hz3u9+tPV79z3W3\nQ4YMydFHH52zzz47c+bMyciRI3Pqqadms802+5vbb2lpyZZbbrnSsi233HKlf/Z+uy222GKl57a0\ntNTGOGPGjPznf/5n7fHly5fXHn/7um83f/78LF++fKXxvH0sffv2XWW9t87Zm9H01v+hdu7cOYsX\nL15lvZaWlnTr1m2la6y33HLLvPDCC+84xjV5t+9/zpw5aWlpWSmsVqxYsdLXb30fG2+8cZYuXfq+\nr5FtaWlJXV1dunbtusbndevWLR06dMiLL76Y7bbb7h2f9+blI1tssUVt7HvttVdt2e67755OnTq9\np3G+dV/o0KFDtthii5X2h7W5L86YMSNXXXVV7fKBJUuWrHQp0d8a66677lr7ukuXLunRo0eam5uz\n9dZbJ8lKv7BssskmtX1yypQp+f73v5/x48ene/fumThx4mrvYtOW+Xj7Nt76ftfkyiuvzI033ljb\nN1555ZWV3vvbj7uWlpaV5u+tj7dlv16THj161P6I/M3j+e0/F9+cuz//+c/5zne+k9///vd59dVX\ns2LFitr3oaWlZaVxvXn51JtK/UyF90NowzqyxRZb5MQTT8yhhx662sePO+64HHfccZk3b14mT56c\nK664Yo3Xer6pT58+mTt37krLXnjhhXzsYx97x3XeGqJz586tnel5c4yTJk16x3Xf/EPC1enZs2c2\n2mijzJ07N9tvv31tWw0NDW1a/93q3bt3Fi5cmCVLltRie+7cue95G+/2/W+xxRbZeuutc/vtt7+n\n7b1Xd9xxR3bZZZe/+Uecm2yySfr375/bb789gwcPfsfnDRw4MD/96U+z1VZb5ROf+ESSN+J7+vTp\n2Wqrrd7z9dl9+vRZ6brYqqpW2R/W1r44Z86cfOMb38iPfvSjNDY21v4FYXXPfaexvhnoyRuR/vLL\nL6801nfSu3fvnHvuuUneuJ594sSJGThw4Ep/lPrmNv7WfLwXDz/8cK644or86Ec/yg477JAOHTpk\n4MCBtWvIk1Xff+/evdPU1FQ7TpuammqPted+fdZZZ2WXXXbJ9773vWy22Wb50Y9+lNtuu602xrf+\nElJV1SrjLPEzFd4Pl47AOnLkkUdm2rRp+f/+v/8vSbJo0aL88pe/TPLGHwT913/9V5YvX55NNtkk\nnTp1SocOqz9c/+7v/i7PPfdc7ethw4bl2WefzS9+8Yu0trbm1ltvzdNPP53/9b/+1zuO5ZprrklT\nU1NefvnlXHbZZbW7S0yYMCE//elP81//9V+pqipLlizJr371q7zyyitteo8dO3bMxz/+8Vx44YV5\n5ZVXMmfOnFx11VXv+D/C92urrbbKbrvtlksuuSTLli3Lww8/vMZLZv6Wd/v+d99993Tp0iXTpk3L\na6+9lhUrVuSpp55q8+UKb/9ersmbZ+ouvfTS3HDDDTnllFPatN6UKVMyffr0XHHFFbUznE8++WS+\n/OUv154zYMCAPPHEE3nooYey5557Jkl23HHHPP/883nggQcycODANm3r7Q4++ODce++9+e1vf5vl\ny5fnf//v/51OnTqtdBvNtbUvvvrqq6mrq0uvXr2SJD/72c9qx1ryxhnV5ubmlf7Q7q0OOeSQ3HTT\nTXniiSeybNmyXHDBBdl9991rZ7PX5Je//GUtALt37566urrVHr9tmY/3YvHixenYsWN69eqV1tbW\nXHrppX/zmD344IPzwx/+MAsWLEhzc/NK/3Lwfvfrdzv2Ll26pEuXLvnTn/600l1shg0blj/+8Y+5\n884709ramp/85Cf561//Wnt8bf1MhbXJGW1YR0aMGJHFixfnlFNOyZw5c9K1a9fss88+Ofjgg7N4\n8eJMnTo1zz//fO265OOPP361r3PcccfltNNOy7XXXpvDDjss3/jGN3LZZZdl6tSpOeuss7LNNtvk\nsssuqwXH6hxyyCH5zGc+k5aWlhxwwAG1s4Yf/ehHc8455+Tss8/Of//3f9eu1X03ZzS/+c1v5pxz\nzsmBBx6Yzp07Z8KECRk3bty7m6x34Xvf+15OPfXUDBo0KP3798/YsWOzcOHC9/Ra7/b9d+zYMZdd\ndlm++93v5oADDsiyZcvykY98pM1nzU444YSce+65Oe+88zJp0qTVfs9bWlrS2NiYqqqy2WabZc89\n98z/+T//J/3792/TNvbcc89cffXVufjii/Mf//Ef6dixY7bZZpscffTRted85CMfSa9evdKzZ890\n69YtyRuXNey+++75zW9+855DcNttt815552Xc845J83Nzdl5551z2WWXrXQZytraF7fffvt85jOf\nyZFHHpm6urqMHTu29ktDkgwePDjbb799hg4dmrq6ujzwwAMrrb/PPvvk5JNPzhe/+MUsXLgwjY2N\nK92RZE0ef/zxTJ06Na+88ko233zznH766enXr997mo/3YujQofnYxz6Wgw46KJtuumk+9alPrfES\nr+SNvy0588wzc8ABB6R3794ZM2ZMbrrppiTvf79+N0499dR885vfzJVXXpmdd945o0aNyv33358k\n6dWrV77//e/n29/+dk499dSMGTMmu+22WzbaaKMka+9nKqxNddVb/y0J+NBxf3DWF/bF9cc111yT\nW2+9daUz2+ub119/Pfvtt1/OP//8NV4KBeuSfzcBgA+5lpaW/O53v8vrr7+eZ555JldddVUOPPDA\ndT2sVdx3331ZuHBhli1bVrv/dlv/NQfWBZeOAMCH3PLly3PmmWfm+eefT9euXTN69OgcddRR63pY\nq5g9e3a++tWvZtmyZdl+++3zgx/8YLW3FoX1hUtHAACgAJeOAABAAR/IS0defHHNn472QdGz56aZ\nP3/Juh7Gese8rMqcrMqcrMqcrMqcrMqcrJ55WdWHZU56937nDwtzRnsDVl/fcV0PYb1kXlZlTlZl\nTlZlTlZlTlZlTlbPvKzKnAhtAAAoQmgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0A\nAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFC\nGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCA\nAurX9QAAgPdn03+b2n4b69I5my5e2m6bW/K1f2m3bcHa5ow2AAAUILQBAKAAoQ0AAAUIbQAAKEBo\nAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQ\ngNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQB\nAKAAoQ0AAAXUr+sBACTJpv82tX032KVzNl28tN02t+Rr/9Ju2wJg/eCMNgAAFCC0AQCgAKENAAAF\nCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsA\nAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKE\nNgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAA\nBQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKKC+5IsPHz48\nXbp0SYcOHdKxY8fcdNNNefnll/PlL385c+bMyVZbbZWLLroo3bt3T1VV+fa3v5177703G2+8cb7z\nne9k1113TZJMnz49//Ef/5EkmTRpUg4//PCSwwYAgPet+Bntq6++OjfffHNuuummJMm0adMyZMiQ\n3H777RkyZEimTZuWJJk5c2aeffbZ3H777TnnnHNy1llnJUlefvnlXHrppbn++utzww035NJLL82C\nBQtKDxsAAN6Xdr905K677srYsWOTJGPHjs2dd9650vK6urr0798/CxcuTEtLS2bNmpV99903PXr0\nSPfu3bPvvvvmvvvua+9hAwDAu1I8tI8//vh84hOfyHXXXZckmTdvXvr06ZMk6d27d+bNm5ckaW5u\nTt++fWvr9e3bN83Nzassb2hoSHNzc+lhAwDA+1L0Gu1rr702DQ0NmTdvXiZOnJhtt912pcfr6upS\nV1e31rfbs+emqa/vuNZfd33Uu3fXdT2E9ZJ5WdV6PyddOrf/Jttxm13W9/n/H+v9frIObBBz0s7H\nj2Nn9TaIfaWdfdjnpGhoNzQ0JEk233zzjBgxIo899lg233zztLS0pE+fPmlpaUmvXr1qz21qaqqt\n29TUlIaGhjQ0NOTBBx+sLW9ubs7ee++9xu3On7+kwLtZ//Tu3TUvvrhoXQ9jvWNeVrUhzMmmi5e2\n6/a6dOmcxe24zSXr+fwnG8Z+0t42lDlpz+PHsbN6G8q+0p4+LHOypl8mil06smTJkrzyyiu1//71\nr3+dHXbYIcOHD8+MGTOSJDNmzMgBBxyQJLXlVVVl9uzZ6dq1a/r06ZOhQ4dm1qxZWbBgQRYsWJBZ\ns2Zl6NChpYYNAABrRbEz2vPmzctJJ52UJFmxYkUOOeSQ7LfffvnoRz+ayZMn58Ybb8yWW26Ziy66\nKEkybNiw3HvvvRkxYkQ22WSTTJ06NUnSo0ePfP7zn8/48eOTJCeddFJ69OhRatgAALBWFAvtfv36\n5ec///kqy3v27Jmrr756leV1dXU588wzV/ta48ePr4U2AABsCHwyJAAAFCC0AQCgAKENAAAFCG0A\nAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ\n2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAA\nFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQht\nAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAK\nENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYA\nABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUI\nbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAA\nChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2\nAAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAF\nCG0AACigeGivWLEiY8eOzT//8z8nSZ577rlMmDAhI0aMyOTJk7Ns2bIkybJlyzJ58uSMGDEiEyZM\nyPPPP197jR/+8IcZMWJEDjrooNx3332lhwwAAO9b8dD+8Y9/nO2226729fnnn59Pf/rTueOOO9Kt\nW7fceOONSZIbbrgh3bp1yx133JFPf/rTOf/885MkTz/9dG655ZbccsstueKKK/Ktb30rK1asKD1s\nAAB4X4qGdlNTU371q19l/PjxSZKqqnL//ffnoIMOSpIcfvjhueuuu5Ikd999dw4//PAkyUEHHZTf\n/va3qaoqd911V0aPHp1OnTqlX79+2WabbfLYY4+VHDYAALxvRUN76tSpmTJlSjp0eGMz8+fPT7du\n3VJfX58k6du3b5qbm5Mkzc3N2WKLLZIk9fX16dq1a+bPn5/m5ub07du39poNDQ21dQAAYH1VX+qF\n77nnnvTq1Su77bZbHnjggVKbWa2ePTdNfX3Hdt3mutK7d9d1PYT1knlZ1Xo/J106t/8m23GbXdb3\n+f8f6/1+sg5sEHPSzsePY2f1Noh9pZ192OekWGg/8sgjufvuuzNz5swsXbo0r7zySr797W9n4cKF\naW1tTX19fZqamtLQ0JDkjTPVL7zwQvr27ZvW1tYsWrQoPXv2TENDQ5qammqv29zcXFvnncyfv6TU\n21qv9O7dNS++uGhdD2O9Y15WtSHMyaaLl7br9rp06ZzF7bjNJev5/Ccbxn7S3jaUOWnP48exs3ob\nyr7Snj4sc7KmXyaKXTryla98JTNnzszdd9+dCy64IIMHD873vve9DBo0KLfddluSZPr06Rk+fHiS\nZPjw4Zk+fXqS5LbbbsvgwYNTV1eX4cOH55ZbbsmyZcvy3HPP5dlnn83uu+9eatgAALBWtPt9tKdM\nmZKrrroqI0aMyMsvv5wJEyYkScaPH5+XX345I0aMyFVXXZWvfvWrSZIddtghBx98cEaNGpXPfvaz\nOeOMM9Kx44fjshAAADZcxS4deatBgwZl0KBBSZJ+/frVbun3Vp07d87FF1+82vUnTZqUSZMmFR0j\nAACsTT4b9NcLAAAgAElEQVQZEgAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgD\nAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA\n0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEA\noAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBo\nAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQ\ngNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQB\nAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChA\naAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABdS39YmvvfZaXnzxxXTu3Dl9+vQp\nOSYAANjgrTG0X3/99cyYMSM33HBDnnzyyWy22WZZtmxZ6uvrc+CBB+bTn/50PvKRj7TXWAEAYIOx\nxtA+8sgj09jYmK9//evZdddd07FjxyTJvHnzct999+WMM87IkUcemdGjR7fLYAEAYEOxxtC+7LLL\n0qtXr1WWb7755hk7dmzGjh2bl156qdjgAABgQ7XGP4ZcXWTPmzcvs2fPXuNzAADgw65Ndx056qij\nsmjRoixcuDBjx47N6aefnu9+97ulxwYAABusNoX2kiVL0rVr19xzzz0ZM2ZMfvGLX2TWrFmlxwYA\nABusNoX2smXLkiQPPPBA9t1333To0KH2h5EAAMCq2hTae++9d0aNGpXf/e532XvvvbNw4cJ06OCz\nbgAA4J206QNrzjzzzDz55JPp169fNtpooyxatCjnnntu6bEBAMAGq02hXVdXl3/4h39IU1NTmpqa\nkiSdOnUqOjAAANiQtSm0f/zjH+fCCy9M9+7da5eM1NXV5a677io6OAAA2FC1KbSvvvrq/N//+3/T\n0NBQejwAAPCB0Ka/aOzbt6/IBgCAd6FNZ7S/+MUv5vTTT8+wYcPSuXPn2vJhw4YVGxgAAGzI2hTa\n99xzT+655548++yzK12jLbQBAGD12hTad9xxR+6+++5svPHGpccDAAAfCG26Rrtfv36pr29TkwMA\nAGnjGe1tttkmn/rUp3LggQeudP/so48+utjAAABgQ9am0F6+fHn+/u//Pk899VTp8QAAwAdCm0L7\nX//1X0uPAwAAPlDWeI3273//+zWuvGzZsvzpT39aqwMCAIAPgjWG9rRp0/LZz342M2bMyJ///Ocs\nWrQof/3rX/PQQw/lggsuyIQJE9LS0rLadZcuXZrx48fn0EMPzejRo3PxxRcnSZ577rlMmDAhI0aM\nyOTJk7Ns2bIkb0T75MmTM2LEiEyYMCHPP/987bV++MMfZsSIETnooINy3333ra33DgAAxazx0pGL\nL744jz32WK677rr84Ac/SFNTUzbZZJPsuOOOOfDAA/OTn/wkm2222WrX7dSpU66++up06dIly5cv\nz1FHHZX99tsvV111VT796U9n9OjROeOMM3LjjTfmqKOOyg033JBu3brljjvuyC233JLzzz8/F110\nUZ5++unccsstueWWW9Lc3JyJEyfmtttuS8eOHYtMCAAArA1/8xrt3XffPbvvvvu7fuG6urp06dIl\nSdLa2prW1tbU1dXl/vvvz/e+970kyeGHH55LL700Rx11VO6+++584QtfSJIcdNBBOfvss1NVVe66\n666MHj06nTp1Sr9+/bLNNtvkscceS2Nj47seEwAAtJc23Uf7vVqxYkUOO+yw7LPPPtlnn33Sr1+/\ndOvWrXZP7r59+6a5uTlJ0tzcnC222CJJUl9fn65du2b+/Plpbm5O3759a6/Z0NBQWwcAANZXRT+F\npmPHjrn55puzcOHCnHTSSXnmmWdKbq6mZ89NU1//4bi0pHfvrut6COsl87Kq9X5OunRu/0224za7\nrO/z/z/W+/1kHdgg5qSdjx/HzuptEPtKO/uwz0m7fNxjt27dMmjQoMyePTsLFy5Ma2tr6uvr09TU\nlIaGhiRvnKl+4YUX0rdv37S2tmbRokXp2bNnGhoa0tTUVHut5ubm2jrvZP78JUXfz/qid++uefHF\nRet6GOsd87KqDWFONl28tF2316VL5yxux20uWc/nP9kw9pP2tqHMSXseP46d1dtQ9pX29GGZkzX9\nMlHs0pGXXnopCxcuTJK89tpr+c1vfpPtttsugwYNym233ZYkmT59eoYPH54kGT58eKZPn54kue22\n2zJ48ODU1dVl+PDhueWWW7Js2bI899xzefbZZ9/TNeMAANCe2nRGe968efnXf/3XvPDCC/nJT36S\nJ598Mo8++mg++clPvuM6LS0tOe2007JixYpUVZWPf/zj2X///bP99tvny1/+ci666KLsvPPOmTBh\nQpJk/PjxmTJlSkaMGJHu3bvnwgsvTJLssMMOOfjggzNq1Kh07NgxZ5xxhjuOAACw3mtTaH/jG9/I\nfvvtl2uuuSZJsu2222bKlClrDO1//Md/zIwZM1ZZ3q9fv9x4442rLO/cuXPtXttvN2nSpEyaNKkt\nQwUAgPVCmy4daW5uzic/+cnameROnTqlQ4eiNywBAIANWptq+c3b8b1p4cKFqaqqyIAAAOCDoE2X\njowYMSJnnHFGFi9enJtuuinXXHNNxo0bV3psAACwwWpTaH/uc5/Lz3/+8yxcuDD33ntvjj322Bx2\n2GGlxwYAABusNt9H+9BDD82hhx5aciwAAPCB0ebb+/3nf/5n/vKXv6S1tbW2/Pvf/36xgQEAwIas\nTaH9+c9/PrvsskuGDBniHtYAANAGbQrtV199NWeeeWbpsQAAwAdGm27vt8cee+SPf/xj6bEAAMAH\nRpvOaB955JE55phj0rdv33Tu3Lm2fHWf8AgAALQxtKdMmZITTzwxu+yyi2u0AQCgDdoU2p07d87x\nxx9feiwAAPCB0aZrtD/2sY9l5syZpccCAAAfGG06o3399ddn2rRp6dKlSzp16pSqqlJXV5ff/va3\npccHAAAbpDaF9s9+9rPS4wAAgA+UNoX2VlttVXocAADwgbLG0J4yZUrOO++8jBs3LnV1das87vZ+\nAACwemsM7U996lNJklNPPbVdBgMAAB8Uawzta665JlOnTs3ee+/dXuMBAIAPhDXe3u+JJ55or3EA\nAMAHSpvuow0AALw7a7x05KmnnsqQIUNWWe4+2gAAsGZrDO1/+Id/yLRp09prLAAA8IGxxtDu1KmT\ne2gDAMB7sMZrtDfaaKP2GgcAAHygrDG0r7/++vYaBwAAfKC46wgAABQgtAEAoAChDQAABQhtAAAo\nQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoA\nAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQg\ntAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAA\nKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDa\nAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAU\nILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0A\nAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoIBiof3CCy/k2GOPzahRozJ69OhcffXV\nSZKXX345EydOzMiRIzNx4sQsWLAgSVJVVc4999yMGDEiY8aMyR/+8Ifaa02fPj0jR47MyJEjM336\n9FJDBgCAtaZYaHfs2DGnnXZabr311lx33XW55ppr8vTTT2fatGkZMmRIbr/99gwZMiTTpk1Lksyc\nOTPPPvtsbr/99pxzzjk566yzkrwR5pdeemmuv/763HDDDbn00ktrcQ4AAOurYqHdp0+f7LrrrkmS\nzTbbLNtuu22am5tz1113ZezYsUmSsWPH5s4770yS2vK6urr0798/CxcuTEtLS2bNmpV99903PXr0\nSPfu3bPvvvvmvvvuKzVsAABYK9rlGu3nn38+TzzxRPbYY4/Mmzcvffr0SZL07t078+bNS5I0Nzen\nb9++tXX69u2b5ubmVZY3NDSkubm5PYYNAADvWX3pDSxevDhf+tKX8i//8i/ZbLPNVnqsrq4udXV1\na32bPXtumvr6jmv9dddHvXt3XddDWC+Zl1Wt93PSpXP7b7Idt9llfZ///7He7yfrwAYxJ+18/Dh2\nVm+D2Ffa2Yd9ToqG9vLly/OlL30pY8aMyciRI5Mkm2++eVpaWtKnT5+0tLSkV69eSd44U93U1FRb\nt6mpKQ0NDWloaMiDDz5YW97c3Jy99957jdudP39JgXez/undu2tefHHRuh7Gese8rGpDmJNNFy9t\n1+116dI5i9txm0vW8/lPNoz9pL1tKHPSnsePY2f1NpR9pT19WOZkTb9MFLt0pKqqnH766dl2220z\nceLE2vLhw4dnxowZSZIZM2bkgAMOWGl5VVWZPXt2unbtmj59+mTo0KGZNWtWFixYkAULFmTWrFkZ\nOnRoqWEDAMBaUeyM9u9+97vcfPPN2XHHHXPYYYclSU455ZSccMIJmTx5cm688cZsueWWueiii5Ik\nw4YNy7333psRI0Zkk002ydSpU5MkPXr0yOc///mMHz8+SXLSSSelR48epYYNAABrRbHQHjBgQP74\nxz+u9rE376n9VnV1dTnzzDNX+/zx48fXQhsAADYEPhkSAAAKENoAAFCA0AYAgAKENgAAFCC0AQCg\nAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgD\nAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA\n0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEA\noAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBo\nAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQ\ngNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQB\nAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChA\naAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAA\nUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0\nAQCggGKh/fWvfz1DhgzJIYccUlv28ssvZ+LEiRk5cmQmTpyYBQsWJEmqqsq5556bESNGZMyYMfnD\nH/5QW2f69OkZOXJkRo4cmenTp5caLgAArFXFQvsTn/hErrjiipWWTZs2LUOGDMntt9+eIUOGZNq0\naUmSmTNn5tlnn83tt9+ec845J2eddVaSN8L80ksvzfXXX58bbrghl156aS3OAQBgfVYstAcOHJju\n3buvtOyuu+7K2LFjkyRjx47NnXfeudLyurq69O/fPwsXLkxLS0tmzZqVfffdNz169Ej37t2z7777\n5r777is1ZAAAWGvq23Nj8+bNS58+fZIkvXv3zrx585Ikzc3N6du3b+15ffv2TXNz8yrLGxoa0tzc\n/De307Pnpqmv77iWR79+6t2767oewnrJvKxqvZ+TLp3bf5PtuM0u6/v8/4/1fj9ZBzaIOWnn48ex\ns3obxL7Szj7sc9Kuof1WdXV1qaurK/La8+cvKfK665vevbvmxRcXrethrHfMy6o2hDnZdPHSdt1e\nly6ds7gdt7lkPZ//ZMPYT9rbhjIn7Xn8OHZWb0PZV9rTh2VO1vTLRLvedWTzzTdPS0tLkqSlpSW9\nevVK8saZ6qamptrzmpqa0tDQsMry5ubmNDQ0tOeQAQDgPWnX0B4+fHhmzJiRJJkxY0YOOOCAlZZX\nVZXZs2ena9eu6dOnT4YOHZpZs2ZlwYIFWbBgQWbNmpWhQ4e255ABAOA9KXbpyCmnnJIHH3ww8+fP\nz3777ZcvfvGLOeGEEzJ58uTceOON2XLLLXPRRRclSYYNG5Z77703I0aMyCabbJKpU6cmSXr06JHP\nf/7zGT9+fJLkpJNOSo8ePUoNGQAA1ppioX3BBResdvnVV1+9yrK6urqceeaZq33++PHja6ENAAAb\nCp8MCQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBo\nAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQ\ngNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsAAAoQ2gAAUIDQBgCAAoQ2AAAUILQB\nAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKqF/XAwAAoLx/e3Bqu26vS5fOWbx4abts62t7/0u7bOfd\nckYbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAACvDJkABsUNrz\n0+3a85PtkvX30+2A98YZbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKENgAAFCC0AQCgAKENAAAF\nCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAABQgtAEAoAChDQAABQhtAAAoQGgDAEABQhsA\nAAoQ2gAAUIDQBgCAAoQ2AAAUILQBAKAAoQ0AAAUIbQAAKEBoAwBAAUIbAAAKENoAAFCA0AYAgAKE\nNgAAFCC0AQCgAKENAAAFCG0AAChAaAMAQAFCGwAAChDaAABQgNAGAIAChDYAAP+/9u49KKrzjOP4\nV8iCqJRIiibVTmPqpWpwrIOx6ngJFyEsC15QGwWj0TSWGJPReIlp0XFirFSdMXY61anTTjuOja0X\npEFq1ca0VTHSZjBYm9rES2yiI6DIdWF5+wfjCbgIIrsLa36fv2T3nOe878N5nee8nHNe8QIV2iIi\nIiIiXqBCW0RERETECx7q6AaIiIiIeFpWVpBPj9e9O1RU+OaYy5c7fXIcaT/NaIuIiIiIeIFmtEVE\nOqkHeUYONCsnIg8+zWiLiIiIiHiBCm0RERERES9QoS0iIiIi4gUqtEVEREREvECFtoiIiIiIF6jQ\nFhERERHxAr3ez8O6Zb3lu4N1D6ZbRY3PDle5fJXPjiUiIiLi7zSjLSIiIiLiBSq0RURERES8QIW2\niIiIiIgX+E2h/f777xMfH09cXBzbt2/v6OaIiIiIiLTILx6GdLlcrF27ll/96lf07t2b1NRUoqOj\n6d+/f0c3TeS+ZJ3y4UOzQPfuwVT46MHZ5U/poVkRERHwk0K7sLCQb33rW3zzm98EwG63c+TIERXa\nfiIrK8inx+veHSoqfHPM5cudPjmOiIiI+J8uxhjT0Y1oTV5eHn/9619Zt24dAPv376ewsJDMzMwO\nbpmIiIiISPP85h5tERERERF/4heFdu/evfniiy+sn69evUrv3r07sEUiIiIiIi3zi0I7MjKSCxcu\ncPnyZZxOJ++++y7R0dEd3SwRERERkbvyi4chH3roITIzM1mwYAEul4tp06YxYMCAjm6WiIiIiMhd\n+cXDkCIiIiIi/sYvbh0REREREfE3KrRFRERERLxAhfZXwJYtWzh+/HhHN4OysjJ27tzZ5LMNGzZg\nt9vZsGGDT9rwxhtvcP78eZ8c614oJ963d+9e1q5d26Z9fvGLX7S6zcqVK8nLy7vfZnlMW86hrVu3\nsmPHjnbFbk56ejpnzpy557jeppx4zoM8fnSeuFNOPE+F9lfAK6+8wpgxYzq6GZSVlbFr164mn+3e\nvZsDBw6wYsUKn7Rh3bp1nWpFUeWkc9q2bVtHN+GeefMcai62P1BOOpa/jB+dJ+6UE89Toe1Bn332\nGc888ww/+tGPsNvtPP/881RXVwMNV2hvvvkmKSkpJCUlUVhY6LZ/TU0Nr7/+Og6Hg8mTJ3Py5Emg\nYUZh0aJFzJ8/n0mTJpGVlWXt87e//Y2ZM2cyZcoUFi9eTEVFhVvcxjMH0dHRbNq0iZSUFKZOnUpR\nURHz588nNjbWGgD19fWsWbOGhIQE5s2bxwsvvOCRmYdNmzZx6dIlUlJS2LBhAwsXLqSyspKpU6eS\nm5vbZNsbN26QkZGBw+FgxowZnDt3Dmi4gn799ddJT08nJiaG3/zmN9Y+2dnZpKamkpKSQmZmJi6X\ny60Nja+Uv/vd71pX6nPnzqWwsNCKe+TIEQCqqqp45ZVXSExM5KWXXmL69OkevdJWTlrX3nEF8Pnn\nn5Oens6kSZP42c9+Zn2ekZHB1KlTsdvtvPPOOwBs3LiR6upqUlJSWLp0KdCwGq3D4SA5OZlly5ZZ\n+58+fZrvf//7xMTEdNjsXFvOIYBz584xc+ZMJk2axO7duwGoqKjgueeeY8qUKTgcDg4fPtxsbIDt\n27dbudi4caMVNy8vj9TUVOLj4zl9+rQPen53ysmXNH7uTueJO+XEC4x4zOXLl83gwYPN2bNnjTHG\nLF682Ozfv98YY0xaWpp54403jDHGnDp1ytjtdrf9d+zYYVauXGmMMeb8+fNmwoQJprq62uzZs8dE\nR0ebsrIyU11dbSZOnGj+97//meLiYjNr1ixTUVFhjDFm27ZtZuvWrW5xV6xYYQ4ePGiMMebpp582\nO3fuNMYYs27dOpOUlGRu3bpliouLzejRo40xxhw8eNAsWLDAuFwuc+3aNRMVFWXt39783Nnv4cOH\nN7vt2rVrrb4cP37cJCcnG2OMefvtt83MmTNNTU2NKS4uNk899ZRxOp3m/Pnz5sUXXzROp9MYY8zq\n1avNvn373OKmpaWZwsJCY4wxAwcONO+9954xxpiMjAwzb94843Q6zb/+9S/reL/85S/Nj3/8Y2OM\nMf/+97/N4MGDrf09QTlpXXvH1Z49e8zYsWNNSUmJqaqqMna73WpvaWmpMcZYn5eUlBhjmv4OPv74\nYzNp0iRTXFzcZJ8VK1aYl19+2bhcLvOf//zHxMbGeqP7rWrLOfT2228bh8NhqqqqTHFxsRk/frz5\n4osvTG1trbl165Yxxpji4mITGxtr6uvr3WK/9957ZubMmaaystIY82Uu0tLSzPr1661tnnvuOU93\ns02Uky9p/NydzhN3yonn+cV7tP1J3759GTx4MABDhw7lypUr1nd2ux2AkSNHUl5eTllZGV/72tes\n7wsKCkhLSwPg29/+Nt/4xjf49NNPARg9ejShoaHWd1euXOHWrVucP3+eZ599FoDa2lqGDx/eahtj\nYmIAGDhwIJWVlfTo0QOAoKAgysrKKCgoICEhgYCAACIiIhg1alS7cnI/CgoK2Lp1K9DQ9xs3blBe\nXg7AhAkTCAoKIjw8nPDwcIqLizlx4gQfffQRqampAFRXV/PII4+0eAybzcb48eOBhlwEBQVhs9kY\nOHCg9XsrKChgzpw51jaDBg3ySn/vxVc5J+0ZVwBjxoyhZ8+eAMTFxVFQUEBkZCS//e1v+fOf/ww0\nzNpdvHjR2u62kydPkpCQQHh4OAAPP/yw9V1sbCwBAQH079+f69eve7jX3hETE0PXrl3p2rUro0aN\n4syZM0yYMIHNmzfzwQcfEBAQwNWrV5vtz4kTJ5g6dSohISFA01zExcUB7r8ff/Cg50TjxzMe9PPk\nfignrVOh7WFBQUHWvwMDA6mpqbF+7tKlS5Nt7/y5LXFdLhfGGMaOHcvmzZvb1EabzQZAQEBAk7gB\nAQHU1dW1KVZHuDMXdXV1GGOYMmWK9afKe2Gz2azfQeNcBAQENHuLRWf2oOekveOquW3y8/M5fvw4\n77zzDiEhIaSnpzeJ29Z2+Yvm8pOTk0NJSQl79+7FZrMRHR1937no6HPlfjzoOdH48YwH/Ty5H8pJ\n63SPtg/dvr/p9OnThIaGWjPUt0VFRZGTkwPAp59+yueff84TTzxx13jDhw/nH//4BxcvXgSgsrLS\nmgFvjxEjRnDo0CHq6+u5fv06p06dandMgO7duzd7D3lzoqKiOHDgAAD5+fn07NnTmnlvzujRo/nT\nn/5EcXEx0HA/syeugkeMGMHBgwcBOH/+PB9//HG7YzamnLRfa+MK4O9//zs3btygurqaw4cPM2LE\nCG7dukVYWBghISH897//5cMPP7S2f+ihh6itrQXge9/7Hnl5eZSWlgINeexM2nIOARw5coSamhpK\nS0s5deoUkZGR3Lp1i0ceeQSbzcbJkyet8+TO2GPGjGHv3r1UVVUBnS8Xtykn9+6rPH50nrhTTjxP\nM9o+FBwczOTJk6mrq+Ott95y+37WrFmsWbMGh8NBYGAg69evb/GKPzw8nPXr17NkyRKcTicAr776\nKv369WtXO+Pj4zlx4gSJiYk89thjDBkypNn/fNuqZ8+ejBgxgqSkJMaNG9fiE8yLFi1i1apVOBwO\nQhnyD5sAAAo4SURBVEJC+MlPftJi7P79+/Pqq6/y/PPPU19fj81mIzMzkz59+rSrzbNmzWLlypUk\nJibyxBNP0L9/f4/k4jblpP1aG1cAw4YN4+WXX+bq1askJycTGRnJoEGD+N3vfsczzzxDv379mtx2\nNWPGDJKTkxkyZAibNm1i4cKFpKenExAQwJAhQ1rNvS+15RwCGDRoEHPmzKG0tJSMjAx69+6Nw+Hg\nhz/8IQ6HgyeffNK6wG8u9rlz55g2bRo2m40JEyawZMkSX3SzTZSTe/dVHj86T9wpJ56nJdh9JD09\nneXLlxMZGdnRTbknFRUVdO/endLSUqZPn86uXbuIiIjo6Gb5nMvloq6ujuDgYC5dusTcuXPJy8vz\nuz95elJnyom/jSuRzkTjR8T7NKMtzVq4cCFlZWXU1taSkZHxlSyyoeFVdnPmzLHueV69evVXusgG\n5UREROReaUZbRERERMQL9DCkdHrV1dWkpaV59MnjkpIS5s+f77F4HcHTeXE6ncyePdsv3jwj907j\nx53Gjoj4igpt6fT27NlDXFwcgYGBHosZHh5Or169KCgo8FhMX/N0XoKCghg9enSzq3+J/9L4caex\n86X8/HxWrlzZ0c3odJQXd8rJ/VGhLZ1eTk4OMTExLS4NHx0dTVZWFg6Hg9TUVOuVh42Xn4eGJcZv\ni4mJsV6n6I9u5wWaX8b2bssrb926lR07dlhxkpKS+Oyzz4CGBST8OSfiTuPHncZOyy5evMjcuXNJ\nTk5mypQpXLp0CWMMGzZsICkpCYfDYV1U5Ofnk56ezuLFi0lISGDp0qUYY3j//fdZvHixFTM/P58X\nX3yxo7rkEcqLO+WkdXoYUjo1p9PJ5cuX6du3L3l5eVy5coXc3FyKi4tJTExk2rRp1rahoaHk5OSw\nf/9+3nrrLbZt29Zi7MjISLZs2eLtLnhF47wcO3aMo0ePsnv3bkJCQpq8i7S6uprs7Gw++OADVq1a\nxR//+McW4w4YMIAzZ854u/niIxo/7jR2Wvfaa6/xgx/8gLi4OGpqaqivr+fQoUOcO3eO7OxsSktL\nSU1NJSoqCoCzZ8/y7rvv0qtXL5599lkKCgoYM2YMmZmZVFZW0q1bN3Jzc0lMTOzgnrWP8uJOOWmd\nCm3p1EpLS613NLe2NHxSUhLQsKTw+vXrW40dHh7OtWvXPN9oH2icl5aWsW1ueeWWBAYGYrPZKC8v\nb3ExHPEPGj/uNHYaTJ8+HafTSWVlJTdv3iQlJQXAel/27SWwg4ODgYbzx263ExgYyNe//nVGjhzJ\nmTNn6NGjB8OGDePRRx8F4Dvf+Q5XrlwhKiqKcePG8Ze//IX4+HiOHTvGsmXLOqazbaC8uFNO2keF\ntnRqXbt2tRbjuR+BgYHU19cDUF9fb61WBlBTU2P9x+Bv7jUvzS2d3DgngNvSuE6n02/zIk1p/LjT\n2Gnw+9//Hmj4M/2+ffusRWTKy8vbHOvOJd5vP2SamJjIzp07CQsL48knn/SLCxDlxZ1y0j66R1s6\ntbCwMFwuFzU1Na0uDX97WfDc3FzrXtI+ffpQVFQEwNGjR5sUChcuXGDAgAE+6olnNc5LS8vYNre8\ncp8+fTh79iwARUVF1j2m0DDb17NnT2w2mw97I96i8eNOY6dlPXr04NFHH+Xw4cNAw8VDVVUVUVFR\nHDx4EJfLRUlJCadPn2bYsGEtxnrqqac4e/Ysu3fv9vtbAZQXd8rJvdGMtnR6Y8eOpaCgoNWl4W/e\nvInD4SAoKIjNmzcDDUsBZ2RkkJyczLhx4+jWrZu1fX5+PhMnTvR1dzzmdl7Gjx9/12Vsm1teOT4+\nnuzsbOx2O8OGDePxxx+3Yvp7TsSdxo87jZ2WZWVlkZmZyZYtW7DZbGzZsoW4uDj++c9/kpKSQpcu\nXVi2bBkRERF88sknd40TGBjIxIkT2bdvHxs2bPBhD7xDeXGnnNwDI9LJffTRR+a1114zxhhTXl5u\njDGmpKTExMTEmGvXrhljjHn66adNcXFxm+LOmjXL3Lhxw7ON9aHGeWlOWlqaKSwsbFPMl156yXzy\nySftbZp0Iho/7jR2RMRXNKMtnd7QoUMZNWoULpfLY0vDl5SUMG/ePMLCwjzcWt9pnBdPvA/Y6XQS\nGxtLv379PNA66Sw0ftxp7IiIr2gJdhERERERL9DDkCIiIiIiXqBCW0REROQO1dXVpKWlWa+g84SS\nkhLmz5/vsXgdwdN5cTqdzJ49m7q6Oo/E62xUaIuIiIjcYc+ePcTFxXnkPv7bwsPD6dWrFwUFBR6L\n6WuezktQUBCjR4+2Xqn5oFGhLSIiInKHnJwcYmJiqK+vZ82aNSQkJDBv3jxeeOEF8vLyAIiOjiYr\nKwuHw0FqaioXL14EYOXKldY2gPVueoCYmBhycnJ82xkPup0XgO3bt+NwOEhOTmbjxo0ApKen8+ab\nb5KSkkJSUhKFhYUAbN26lR07dlhxkpKSrHfRx8bG+nVOWqJCW0RERKQRp9PJ5cuX6du3L4cOHeLK\nlSvk5uaSlZXFhx9+2GTb0NBQcnJySEtLs9653pLIyEi/ndFunJdjx45x9OhRdu/ezYEDB1iwYIG1\nXXV1NdnZ2axevZpVq1a1GnfAgAGcOXPGm03vMCq0RURERBopLS21FnQqKCggISGBgIAAIiIiGDVq\nVJNtk5KSALDb7W5FeHPCw8O5du2a5xvtA43zcuLECaZOnUpISAgADz/8sLWd3W4HYOTIkZSXl1NW\nVtZi3MDAQGw2230t697ZqdAWERERaaRr1644nc773j8wMJD6+noA6uvrqa2ttb6rqakhODi43W3s\nCPealy5durj93Dgn0JCHxpxOp9/mpSUqtEVEREQaCQsLw+VyUVNTw4gRIzh06BD19fVcv36dU6dO\nNdn24MGDAOTm5lr3Yvfp04eioiIAjh492qTQvnDhAgMGDPBRTzyrcV7GjBnD3r17qaqqAuDGjRvW\ndrcfbDx9+jShoaGEhobSp08fzp49C0BRUZF1fzY0zJT37NkTm83mw974hlaGFBEREbnD2LFjKSgo\nID4+nhMnTpCYmMhjjz3GkCFDrNsnAG7evInD4SAoKIjNmzcDMGPGDDIyMkhOTmbcuHF069bN2j4/\nP5+JEyf6ujseczsv48eP59y5c0ybNg2bzcaECRNYsmQJAMHBwUyePJm6ujrrvvX4+Hiys7Ox2+0M\nGzaMxx9/3Irp7zlpiVaGFBEREblDUVERv/71r/npT39KRUUF3bt3p7S0lOnTp7Nr1y4iIiKIjo7m\nD3/4A+Hh4fccd/bs2fz85z8nLCzMi633nsZ5aU56ejrLly8nMjLynmMuWrSIpUuX0q9fP081s9PQ\njLaIiIjIHYYOHcqoUaNwuVwsXLiQsrIyamtrycjIICIi4r5ilpSUMG/ePL8tsqFpXjzxLm2n00ls\nbOwDWWSDZrRFRERERLxCD0OKiIiIiHiBCm0RERERES9QoS0iIiIi4gUqtEVEREREvECFtoiIiIiI\nF/wfAOvqbvm2/GkAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "objects = ('np one img', 'tf one img\\n(gpu)', 'tf one img\\n(cpu)', 'np batch', 'tf batch\\n(gpu)', \n", + " 'tf batch\\n(cpu)', 'np batch\\n+conv', 'tf batch\\n+conv\\n(gpu)', 'tf batch\\n+conv\\n(cpu)')\n", + "y_pos = np.arange(len(objects))\n", + "performance = [large_np.average, large_tf.average, large_tf_cpu.average, \n", + " large_np_batch.average, large_tf_batch.average, large_tf_batch_cpu.average,\n", + " large_np_conv.average, large_tf_conv.average, large_tf_conv_cpu.average]\n", + "performance = [i*1000 for i in performance]\n", + "fig, ax = plt.subplots(1, figsize=(12,12))\n", + "ax.bar(y_pos, performance, align='center', alpha=0.5, color=['red', 'blue', 'green'])\n", + "plt.xticks(y_pos, objects)\n", + "plt.ylabel('Time (ms)')\n", + "plt.title('Times to perform different DTCWT operations on large images')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparison to Convolutions\n", + "One important distinguising feature that would be nice would be to see a speed-up in using the dtcwt over using convolutions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## First we can compare execution time on a CPU" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-10T23:08:12.470902Z", + "start_time": "2017-08-10T23:08:12.467758Z" + } + }, + "outputs": [], + "source": [ + "session_conf = tf.ConfigProto(\n", + " intra_op_parallelism_threads=1,\n", + " inter_op_parallelism_threads=1,\n", + " device_count={'CPU': 1})" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-10T23:09:27.466591Z", + "start_time": "2017-08-10T23:09:20.670606Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.2268359661102295\n" + ] + } + ], + "source": [ + "# Create the input\n", + "h, w = 512, 512\n", + "in_ = np.random.randn(100,h,w,3)\n", + "\n", + "# Set up the transforms\n", + "nlevels = 3\n", + "tf.reset_default_graph()\n", + "with tf.device(\"/cpu:0\"):\n", + " fwd_tf = dtcwt.tf.Transform2d() # Tensorflow Transform\n", + " in_placeholder = tf.placeholder(tf.float32, [None, h, w, 3])\n", + " Yl, Yh = fwd_tf.forward_channels(in_placeholder, nlevels=nlevels)\n", + "\n", + "sess = tf.Session(config=session_conf)\n", + "sess.run(tf.global_variables_initializer())\n", + "\n", + "start = time.time()\n", + "#dtcwt_cpu = %timeit -o sess.run(Yl, {in_placeholder: in_})\n", + "sess.run(Yl, {in_placeholder: in_})\n", + "print(time.time()-start)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "ExecuteTime": { + "end_time": "2017-08-10T23:07:47.429889Z", + "start_time": "2017-08-10T23:06:54.087493Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6.17 s ± 150 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "# Hard to say what an equivalent is? Let us compare to a single layer with 5x5x3x64\n", + "h, w = 512, 512\n", + "in_ = np.random.randn(100,h,w,3)\n", + "\n", + "# Set up the transforms\n", + "nlevels = 3\n", + "tf.reset_default_graph()\n", + "with tf.device(\"/cpu:0\"):\n", + " in_placeholder = tf.placeholder(tf.float32, [None, h, w, 3])\n", + " weights = tf.get_variable('weights', shape=(10,10,3,64))\n", + " out = tf.nn.conv2d(in_placeholder, weights, strides=[1,1,1,1], padding='SAME')\n", + "\n", + "sess = tf.Session(config=session_conf)\n", + "sess.run(tf.global_variables_initializer())\n", + "dtcwt_cpu = %timeit -o sess.run(out, {in_placeholder: in_})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + }, + "toc": { + "colors": { + "hover_highlight": "#DAA520", + "navigate_num": "#000000", + "navigate_text": "#333333", + "running_highlight": "#FF0000", + "selected_highlight": "#FFD700", + "sidebar_border": "#EEEEEE", + "wrapper_background": "#FFFFFF" + }, + "moveMenuLeft": true, + "nav_menu": { + "height": "12px", + "width": "252px" + }, + "navigate_menu": true, + "number_sections": true, + "sideBar": true, + "threshold": 4, + "toc_cell": false, + "toc_section_display": "block", + "toc_window_display": false, + "widenNotebook": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 9bc9625db74d68695cc1c613ffee3d73ee9e2ace Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 23 Aug 2017 12:17:55 +0100 Subject: [PATCH 39/52] Refactor forward_channels Instead of using map fn, put the channels into the batch dimensions, and do the forward transform on a batch of single channel inputs. --- dtcwt/tf/transform2d.py | 240 +++++++++++++++++++++++------------- tests/test_tfTransform2d.py | 4 +- 2 files changed, 155 insertions(+), 89 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 2bdb9ad..7fa0323 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -191,7 +191,7 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False, return p_tf - def forward_channels(self, X, nlevels=3, include_scale=False, + def forward_channels(self, X, nlevels=3, # include_scale=True, data_format="nhwc", undecimated=False, max_dec_scale=1): ''' @@ -211,7 +211,7 @@ def forward_channels(self, X, nlevels=3, include_scale=False, format is "nhwc", then the data is in the form [batch, h, w, c]. :returns: tuple - A tuple of (Yl, Yh) or (Yl, Yh, Yscale) if include_scale was true. + A tuple of (Yl, Yh, Yscale). The order of output axes will match the input axes (i.e. the position of the channel dimension). I.e. (note that the spatial sizes will change) @@ -233,6 +233,9 @@ def forward_channels(self, X, nlevels=3, include_scale=False, .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 ''' + # include_scale used to be a parameter, but removed for ease + include_scale = True + data_format = data_format.lower() if data_format != "nchw" and data_format != "nhwc": raise ValueError('The data format must be either "ncwh" or ' + @@ -248,69 +251,131 @@ def forward_channels(self, X, nlevels=3, include_scale=False, It must be of shape [batch, channels, height, width] (batch can be None).'''.format(X_shape)) - original_size = X.get_shape().as_list()[1:-1] - size = '{}x{}'.format(original_size[0], original_size[1]) - name = 'dtcwt_fwd_{}'.format(size) - with tf.name_scope(name): - # Put the channel axis first - if data_format == "nhwc": - X = tf.transpose(X, perm=[3, 0, 1, 2]) + with tf.variable_scope('ch_to_batch'): + s = X.get_shape().as_list()[1:] + size = '{}x{}'.format(s[0], s[1]) + name = 'dtcwt_fwd_{}'.format(size) + # Move all of the channels into the batch dimension for the + # input. This may involve transposing, depending on the data + # format + if data_format == 'nhwc': + nch = s[2] + X = tf.transpose(X, perm=[0, 3, 1, 2]) + X = tf.reshape(X, [-1, s[0], s[1]]) else: - X = tf.transpose(X, perm=[1, 0, 2, 3]) - - f = lambda x: self._forward_ops(x, nlevels, include_scale, - return_tuple=True, - undecimated=undecimated, - max_dec_scale=max_dec_scale) - - # Calculate the dtcwt for each of the channels independently - # This will return tensors of shape: - # Yl: A tensor of shape [c, batch, h', w'] - # Yh: list of length nlevels, each of shape - # [c, batch, h'', w'', 6] - # Yscale: list of length nlevels, each of shape - # [c, batch, h''', w'''] + nch = s[0] + X = tf.reshape(X, [-1, s[1], s[2]]) + + with tf.variable_scope(name): + Yl, Yh, Yscale = self._forward_ops( + X, nlevels, include_scale, return_tuple=True, + undecimated=undecimated, max_dec_scale=max_dec_scale) + + # Put the channels back into their correct positions + with tf.variable_scope('batch_to_ch'): + # Reshape Yl + s = Yl.get_shape().as_list()[1:] + Yl = tf.reshape(Yl, [-1, nch, s[0], s[1]], name='Yl_reshape') + if data_format == 'nhwc': + Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') + + # Yh = tuple( + # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) + # for i, x in enumerate(Yh)]) + # Yscale = tuple( + # [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) + # for i, x in enumerate(Yscale)]) + # Reshape Yh + with tf.variable_scope('Yh'): + Yh_new = [None,] * nlevels + for i in range(nlevels): + s = Yh[i].get_shape().as_list()[1:] + Yh_new[i] = tf.reshape( + Yh[i], [-1, nch, s[0], s[1], s[2]], + name='scale{}_reshape'.format(i)) + if data_format == 'nhwc': + Yh_new[i] = tf.transpose( + Yh_new[i], [0, 2, 3, 1, 4], + name='scale{}_ch_to_end'.format(i)) + Yh = tuple(Yh_new) + + # Reshape Yscale if include_scale: - # (lowpass, highpasses, scales) - shape = (tf.float32, - tuple(tf.complex64 for k in range(nlevels)), - tuple(tf.float32 for k in range(nlevels))) - Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) - # Transpose the tensors to put the channel after the batch - if data_format == "nhwc": - perm_r = [1, 2, 3, 0] - perm_c = [1, 2, 3, 0, 4] - else: - perm_r = [1, 0, 2, 3] - perm_c = [1, 0, 2, 3, 4] - Yl = tf.transpose(Yl, perm=perm_r, name='Yl') - Yh = tuple( - [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) - for i, x in enumerate(Yh)]) - Yscale = tuple( - [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) - for i, x in enumerate(Yscale)]) - + with tf.variable_scope('Yscale'): + Yscale_new = [None,] * nlevels + for i in range(nlevels): + s = Yscale[i].get_shape().as_list()[1:] + Yscale_new[i] = tf.reshape( + Yscale[i], [-1, nch, s[0], s[1]], + name='scale{}_reshape'.format(i)) + if data_format == 'nhwc': + Yscale_new[i] = tf.transpose( + Yscale_new[i], [0, 2, 3, 1], + name='scale{}_ch_to_end'.format(i)) + Yscale = tuple(Yscale_new) return Yl, Yh, Yscale - else: - shape = (tf.float32, - tuple(tf.complex64 for k in range(nlevels))) - Yl, Yh = tf.map_fn(f, X, dtype=shape) - # Transpose the tensors to put the channel after the batch - if data_format == "nhwc": - perm_r = [1, 2, 3, 0] - perm_c = [1, 2, 3, 0, 4] - else: - perm_r = [1, 0, 2, 3] - perm_c = [1, 0, 2, 3, 4] - Yl = tf.transpose(Yl, perm=perm_r, name='Yl') - Yh = tuple( - [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) - for i, x in enumerate(Yh)]) - return Yl, Yh + # # Put the channel axis first + # if data_format == "nhwc": + # X = tf.transpose(X, perm=[3, 0, 1, 2]) + # else: + # X = tf.transpose(X, perm=[1, 0, 2, 3]) + + # f = lambda x: self._forward_ops(x, nlevels, include_scale, + # return_tuple=True, + # undecimated=undecimated, + # max_dec_scale=max_dec_scale) + + # # Calculate the dtcwt for each of the channels independently + # # This will return tensors of shape: + # # Yl: A tensor of shape [c, batch, h', w'] + # # Yh: list of length nlevels, each of shape + # # [c, batch, h'', w'', 6] + # # Yscale: list of length nlevels, each of shape + # # [c, batch, h''', w'''] + # if include_scale: + # # (lowpass, highpasses, scales) + # shape = (tf.float32, + # tuple(tf.complex64 for k in range(nlevels)), + # tuple(tf.float32 for k in range(nlevels))) + # Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) + # # Transpose the tensors to put the channel after the batch + # if data_format == "nhwc": + # perm_r = [1, 2, 3, 0] + # perm_c = [1, 2, 3, 0, 4] + # else: + # perm_r = [1, 0, 2, 3] + # perm_c = [1, 0, 2, 3, 4] + # Yl = tf.transpose(Yl, perm=perm_r, name='Yl') + # Yh = tuple( + # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) + # for i, x in enumerate(Yh)]) + # Yscale = tuple( + # [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) + # for i, x in enumerate(Yscale)]) + + # return Yl, Yh, Yscale + + # else: + # shape = (tf.float32, + # tuple(tf.complex64 for k in range(nlevels))) + # Yl, Yh = tf.map_fn(f, X, dtype=shape) + # # Transpose the tensors to put the channel after the batch + # if data_format == "nhwc": + # perm_r = [1, 2, 3, 0] + # perm_c = [1, 2, 3, 0, 4] + # else: + # perm_r = [1, 0, 2, 3] + # perm_c = [1, 0, 2, 3, 4] + # Yl = tf.transpose(Yl, perm=perm_r, name='Yl') + # Yh = tuple( + # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) + # for i, x in enumerate(Yh)]) + + # return Yl, Yh + def inverse(self, pyramid, gain_mask=None): ''' Perform an inverse transform on an image. @@ -453,37 +518,38 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): # Move all of the channels into the batch dimension for the lowpass # input. This may involve transposing, depending on the data format - s = Yl.get_shape().as_list() - num_channels = s[channel_ax] - nlevels = len(Yh) - if data_format == "nhwc": - size = '{}x{}_up_{}'.format(s[1], s[2], nlevels) - Yl_new = tf.transpose(Yl, [0, 3, 1, 2]) - Yl_new = tf.reshape(Yl_new, [-1, s[1], s[2]]) - else: - size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) - Yl_new = tf.reshape(Yl, [-1, s[2], s[3]]) - - # Move all of the channels into the batch dimension for the highpass - # input. This may involve transposing, depending on the data format - Yh_new = [] - for scale in Yh: - s = scale.get_shape().as_list() - if s[channel_ax] != num_channels: - raise ValueError( - '''The number of channels has to be consistent for all - inputs across the channel axis {}. You fed in Yl: {} - and Yh: {}'''.format(channel_ax, Yl, Yh)) + with tf.variable_scope('ch_to_batch'): + s = Yl.get_shape().as_list() + num_channels = s[channel_ax] + nlevels = len(Yh) if data_format == "nhwc": - scale = tf.transpose(scale, [0, 3, 1, 2, 4]) - Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) + size = '{}x{}_up_{}'.format(s[1], s[2], nlevels) + Yl_new = tf.transpose(Yl, [0, 3, 1, 2]) + Yl_new = tf.reshape(Yl_new, [-1, s[1], s[2]]) else: - Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) + size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) + Yl_new = tf.reshape(Yl, [-1, s[2], s[3]]) + + # Move all of the channels into the batch dimension for the highpass + # input. This may involve transposing, depending on the data format + Yh_new = [] + for scale in Yh: + s = scale.get_shape().as_list() + if s[channel_ax] != num_channels: + raise ValueError( + '''The number of channels has to be consistent for all + inputs across the channel axis {}. You fed in Yl: {} + and Yh: {}'''.format(channel_ax, Yl, Yh)) + if data_format == "nhwc": + scale = tf.transpose(scale, [0, 3, 1, 2, 4]) + Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) + else: + Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) - pyramid = Pyramid_tf(None, Yl_new, Yh_new) + pyramid = Pyramid_tf(None, Yl_new, Yh_new) name = 'dtcwt_inv_{}_{}channels'.format(size, num_channels) - with tf.name_scope(name): + with tf.variable_scope(name): P = self._inverse_ops(pyramid, gain_mask) s = P.X.get_shape().as_list() X = tf.reshape(P.X, [-1, num_channels, s[1], s[2]]) @@ -701,9 +767,9 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, return Pyramid_tf(X_in, Yl, tuple(Yh), tuple(Yscale)) else: if return_tuple: - return Yl, tuple(Yh) + return Yl, tuple(Yh), None else: - return Pyramid_tf(X_in, Yl, tuple(Yh)) + return Pyramid_tf(X_in, Yl, tuple(Yh), None) def _inverse_ops(self, pyramid, gain_mask=None): """Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 7a3806d..ed058d9 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -288,7 +288,7 @@ def test_results_match_endtoend(test_input, biort, qshift): @skip_if_no_tf @pytest.mark.parametrize("data_format", [ ("nhwc"), - ("nchw") + ("nchw"), ]) def test_forward_channels(data_format): batch = 5 @@ -307,7 +307,7 @@ def test_forward_channels(data_format): f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') start = time.time() Yl, Yh, Yscale = f_tf.forward_channels( - in_p, nlevels=nlevels, include_scale=True, data_format=data_format) + in_p, nlevels=nlevels, data_format=data_format) Yl, Yh, Yscale = sess.run([Yl, Yh, Yscale], {in_p: ims}) print("That took {:.2f}s".format(time.time() - start)) From b6792475e928dd58edf3ed4e5332e602e5753882 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 23 Aug 2017 12:27:05 +0100 Subject: [PATCH 40/52] Update Pyramid_tf initializer no longer needs tf installed to create a class instance. --- dtcwt/tf/__init__.py | 2 +- dtcwt/tf/common.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index 686e11a..941ca1f 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -6,7 +6,7 @@ """ from .common import Pyramid_tf -from .transform2d import Transform2d, dtwavexfm2, dtwaveifm2 +from .transform2d import Transform2d __all__ = [ 'Pyramid', diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index 4e3035b..e45b821 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -42,12 +42,17 @@ class Pyramid_tf(object): *Yh* to the tensorflow session. Assumes that the object was returned from the Trasnform2d().inverse() method. """ - def __init__(self, X, lowpass, highpasses, scales=None, - graph=tf.get_default_graph()): + def __init__(self, X, lowpass, highpasses, scales=None, graph=None): self.X = X self.lowpass_op = lowpass self.highpasses_ops = highpasses self.scales_ops = scales + if graph is None: + try: + # This could fail if we don't have tensorflow + graph = tf.get_default_graph() + except NameError: + pass self.graph = graph def _get_lowpass(self, data, sess=None): From 57642951051bce7d3cb7c880e394a10877ddd4b4 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 23 Aug 2017 14:08:14 +0100 Subject: [PATCH 41/52] Add tf to tox and travis tests --- .travis.yml | 2 ++ tests/test_tfTransform2d.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31937b4..5370278 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ env: - TOX_ENV=py27 - TOX_ENV=py3-opencl - TOX_ENV=py27-opencl + - TOX_ENV=py27-tf + - TOX_ENV=py3-tf - TOX_ENV=docs install: - pip install --upgrade pip diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index ed058d9..f5f9ed0 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -174,9 +174,9 @@ def test_multiple_inputs(): y3 = pyramid_ops.eval_fwd([mandrill, mandrill, mandrill]) assert y3.lowpass.shape == (3,) + y.lowpass.shape for hi3, hi in zip(y3.highpasses, y.highpasses): - assert hi3.shape == (3, *hi.shape) + assert hi3.shape == (3,) + hi.shape for s3, s in zip(y3.scales, y.scales): - assert s3.shape == (3, *s.shape) + assert s3.shape == (3,) + s.shape @skip_if_no_tf From 1e8d80e8834fd2d67dec6b60d26cb179f76cde0a Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 23 Aug 2017 15:43:20 +0100 Subject: [PATCH 42/52] Fix errors in tf test suite --- tests/test_tfTransform2d.py | 13 ++++++------ tests/test_tfinputshapes.py | 40 ++++++++++++++----------------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index f5f9ed0..3b2751f 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -22,9 +22,10 @@ def setup(): # Import the tensorflow modules tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') + dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') Transform2d = getattr(dtcwt_tf, 'Transform2d') - dtwavexfm2 = getattr(dtcwt_tf, 'dtwavexfm2') - dtwaveifm2 = getattr(dtcwt_tf, 'dtwaveifm2') + dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') + dtwaveifm2 = getattr(dtcwt_tf_xfm2, 'dtwaveifm2') mandrill = datasets.mandrill() in_p = tf.placeholder(tf.float32, [None, 512, 512]) @@ -358,14 +359,14 @@ def test_inverse_channels(data_format): ims = np.random.randn(batch, 100, 100, c) in_p = tf.placeholder(tf.float32, [None, 100, 100, c]) f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') - Yl, Yh = f_tf.forward_channels( - in_p, nlevels=nlevels, include_scale=False, data_format=data_format) + Yl, Yh, _ = f_tf.forward_channels( + in_p, nlevels=nlevels, data_format=data_format) else: ims = np.random.randn(batch, c, 100, 100) in_p = tf.placeholder(tf.float32, [None, c, 100, 100]) f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') - Yl, Yh = f_tf.forward_channels( - in_p, nlevels=nlevels, include_scale=False, data_format=data_format) + Yl, Yh, _ = f_tf.forward_channels( + in_p, nlevels=nlevels, data_format=data_format) # Call the inverse_channels function start = time.time() diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index 43f79d4..f0a30ac 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -12,9 +12,10 @@ def test_setup(): global tf, Transform2d, dtwavexfm2, dtwaveifm2 tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') + dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') Transform2d = getattr(dtcwt_tf, 'Transform2d') - dtwavexfm2 = getattr(dtcwt_tf, 'dtwavexfm2') - dtwaveifm2 = getattr(dtcwt_tf, 'dtwaveifm2') + dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') + dtwaveifm2 = getattr(dtcwt_tf_xfm2, 'dtwaveifm2') # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" @@ -95,11 +96,7 @@ def test_2d_input_tuple(nlevels, include_scale): in_ = tf.placeholder(tf.float32, [512, 512]) t = Transform2d() # Calling forward with a 2d input will throw a warning - if include_scale: - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, - return_tuple=True) - else: - Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level @@ -155,11 +152,8 @@ def test_batch_input(nlevels, include_scale, batch_size): def test_batch_input_tuple(nlevels, include_scale, batch_size): in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) t = Transform2d() - if include_scale: - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, - return_tuple=True) - else: - Yl, Yh = t.forward(in_, nlevels, include_scale, return_tuple=True) + + Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level @@ -178,19 +172,16 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): @skip_if_no_tf -@pytest.mark.parametrize("nlevels, include_scale, channels", [ - (2,False,5), - (2,True,2), - (4,False,10), - (3,True,6) +@pytest.mark.parametrize("nlevels, channels", [ + (2,5), + (2,2), + (4,10), + (3,6) ]) def test_multichannel(nlevels, include_scale, channels): in_ = tf.placeholder(tf.float32, [None, 512, 512, channels]) t = Transform2d() - if include_scale: - Yl, Yh, Yscale = t.forward_channels(in_, nlevels, include_scale) - else: - Yl, Yh = t.forward_channels(in_, nlevels, include_scale) + Yl, Yh, Yscale = t.forward_channels(in_, nlevels) # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level @@ -203,7 +194,6 @@ def test_multichannel(nlevels, include_scale, channels): assert (Yh[i].get_shape().as_list() == [None, extent, extent, channels, 6]) assert Yh[i].dtype == tf.complex64 - if include_scale: - assert Yscale[i].get_shape().as_list() == [ - None, 2*extent, 2*extent, channels] - assert Yscale[i].dtype == tf.float32 + assert Yscale[i].get_shape().as_list() == [ + None, 2*extent, 2*extent, channels] + assert Yscale[i].dtype == tf.float32 From 07aab9934adda589756ae9ebfd636e916f77efcf Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Wed, 23 Aug 2017 17:53:57 +0100 Subject: [PATCH 43/52] Add fix to install libOpenCL.so --- .travis.yml | 2 +- tests/test_tfinputshapes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5370278..659f136 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_install: - sudo apt-get install -y software-properties-common - sudo add-apt-repository -y "deb http://us.archive.ubuntu.com/ubuntu/ trusty universe multiverse restricted" - sudo apt-get update -qq - - sudo apt-get install -y opencl-headers fglrx + - sudo apt-get install -y opencl-headers fglrx ocl-icd-opencl-dev env: - TOX_ENV=py3 - TOX_ENV=py27 diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index f0a30ac..b78b525 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -178,7 +178,7 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): (4,10), (3,6) ]) -def test_multichannel(nlevels, include_scale, channels): +def test_multichannel(nlevels, channels): in_ = tf.placeholder(tf.float32, [None, 512, 512, channels]) t = Transform2d() Yl, Yh, Yscale = t.forward_channels(in_, nlevels) From 0b1d065f9c470ec96bcfce6a3401fbb4ed6c22f9 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Thu, 7 Sep 2017 15:35:27 +0100 Subject: [PATCH 44/52] Remove commented out code in transform2d after tests --- dtcwt/tf/transform2d.py | 67 +---------------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 7fa0323..787cd87 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -191,7 +191,7 @@ def forward(self, X, nlevels=3, include_scale=False, return_tuple=False, return p_tf - def forward_channels(self, X, nlevels=3, # include_scale=True, + def forward_channels(self, X, nlevels=3, # include_scale=True, data_format="nhwc", undecimated=False, max_dec_scale=1): ''' @@ -279,12 +279,6 @@ def forward_channels(self, X, nlevels=3, # include_scale=True, if data_format == 'nhwc': Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') - # Yh = tuple( - # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) - # for i, x in enumerate(Yh)]) - # Yscale = tuple( - # [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) - # for i, x in enumerate(Yscale)]) # Reshape Yh with tf.variable_scope('Yh'): Yh_new = [None,] * nlevels @@ -317,65 +311,6 @@ def forward_channels(self, X, nlevels=3, # include_scale=True, else: return Yl, Yh - # # Put the channel axis first - # if data_format == "nhwc": - # X = tf.transpose(X, perm=[3, 0, 1, 2]) - # else: - # X = tf.transpose(X, perm=[1, 0, 2, 3]) - - # f = lambda x: self._forward_ops(x, nlevels, include_scale, - # return_tuple=True, - # undecimated=undecimated, - # max_dec_scale=max_dec_scale) - - # # Calculate the dtcwt for each of the channels independently - # # This will return tensors of shape: - # # Yl: A tensor of shape [c, batch, h', w'] - # # Yh: list of length nlevels, each of shape - # # [c, batch, h'', w'', 6] - # # Yscale: list of length nlevels, each of shape - # # [c, batch, h''', w'''] - # if include_scale: - # # (lowpass, highpasses, scales) - # shape = (tf.float32, - # tuple(tf.complex64 for k in range(nlevels)), - # tuple(tf.float32 for k in range(nlevels))) - # Yl, Yh, Yscale = tf.map_fn(f, X, dtype=shape) - # # Transpose the tensors to put the channel after the batch - # if data_format == "nhwc": - # perm_r = [1, 2, 3, 0] - # perm_c = [1, 2, 3, 0, 4] - # else: - # perm_r = [1, 0, 2, 3] - # perm_c = [1, 0, 2, 3, 4] - # Yl = tf.transpose(Yl, perm=perm_r, name='Yl') - # Yh = tuple( - # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) - # for i, x in enumerate(Yh)]) - # Yscale = tuple( - # [tf.transpose(x, perm=perm_r, name='Yscale_'+str(i+1)) - # for i, x in enumerate(Yscale)]) - - # return Yl, Yh, Yscale - - # else: - # shape = (tf.float32, - # tuple(tf.complex64 for k in range(nlevels))) - # Yl, Yh = tf.map_fn(f, X, dtype=shape) - # # Transpose the tensors to put the channel after the batch - # if data_format == "nhwc": - # perm_r = [1, 2, 3, 0] - # perm_c = [1, 2, 3, 0, 4] - # else: - # perm_r = [1, 0, 2, 3] - # perm_c = [1, 0, 2, 3, 4] - # Yl = tf.transpose(Yl, perm=perm_r, name='Yl') - # Yh = tuple( - # [tf.transpose(x, perm=perm_c, name='Yh_'+str(i+1)) - # for i, x in enumerate(Yh)]) - - # return Yl, Yh - def inverse(self, pyramid, gain_mask=None): ''' Perform an inverse transform on an image. From 9191b615e1812c45fc6186ac54a0b89048638043 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Sat, 9 Sep 2017 15:02:35 +0100 Subject: [PATCH 45/52] Update for merge Fixed the Tensorflow backend's handling of Pyramids - made it align with the numpy and opencl versions. Added the unpack function the utils. --- dtcwt/numpy/transform2d.py | 16 +- dtcwt/tf/__init__.py | 2 +- dtcwt/tf/common.py | 211 ++--------- dtcwt/tf/transform2d.py | 705 ++++++++++++++++++------------------ dtcwt/utils.py | 81 +++-- tests/test_tfTransform2d.py | 229 ++++++++++-- tests/util.py | 3 +- 7 files changed, 648 insertions(+), 599 deletions(-) diff --git a/dtcwt/numpy/transform2d.py b/dtcwt/numpy/transform2d.py index a3da89f..683e4cc 100644 --- a/dtcwt/numpy/transform2d.py +++ b/dtcwt/numpy/transform2d.py @@ -74,8 +74,10 @@ def forward(self, X, nlevels=3, include_scale=False): original_size = X.shape if len(X.shape) >= 3: - raise ValueError('The entered image is {0}, please enter each image slice separately.'. - format('x'.join(list(str(s) for s in X.shape)))) + raise ValueError('The entered image is {0}, which is invalid '. + format('x'.join(list(str(s) for s in X.shape))) + + 'for the 2D transform in a numpy backend. ' + + 'Please enter each image slice separately.') # The next few lines of code check to see if the image is odd in size, if so an extra ... # row/column will be added to the bottom/right of the image @@ -150,9 +152,9 @@ def forward(self, X, nlevels=3, include_scale=False): Yh[level][:,:,0:6:5] = q2c(coldfilt(Hi,h0b,h0a).T) # Horizontal Yh[level][:,:,2:4:1] = q2c(coldfilt(Lo,h1b,h1a).T) # Vertical if len(self.qshift) >= 12: - Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal + Yh[level][:,:,1:5:3] = q2c(coldfilt(Ba,h2b,h2a).T) # Diagonal else: - Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal + Yh[level][:,:,1:5:3] = q2c(coldfilt(Hi,h1b,h1a).T) # Diagonal if include_scale: Yscale[level] = LoLo @@ -267,7 +269,7 @@ def inverse(self, pyramid, gain_mask=None): if np.any(np.array(Z.shape) != S[:2]): raise ValueError('Sizes of highpasses are not valid for DTWAVEIFM2') - + current_level = current_level - 1 if current_level == 1: @@ -300,7 +302,7 @@ def q2c(y): """ Convert from quads in y to complex numbers in z. """ - + j2 = (np.sqrt(0.5) * np.array([1, 1j])).astype(appropriate_complex_type_for(y)) # Arrange pixels from the corners of the quads into @@ -310,7 +312,7 @@ def q2c(y): # | | # c----d - # Combine (a,b) and (d,c) to form two complex subimages. + # Combine (a,b) and (d,c) to form two complex subimages. p = y[0::2, 0::2]*j2[0] + y[0::2, 1::2]*j2[1] # p = (a + jb) / sqrt(2) q = y[1::2, 1::2]*j2[0] - y[1::2, 0::2]*j2[1] # q = (d - jc) / sqrt(2) diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index 941ca1f..000480d 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -5,7 +5,7 @@ """ -from .common import Pyramid_tf +from .common import Pyramid from .transform2d import Transform2d __all__ = [ diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index e45b821..ab3319d 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -from dtcwt.numpy import Pyramid as Pyramid_np - try: import tensorflow as tf except ImportError: @@ -9,193 +7,64 @@ pass -class Pyramid_tf(object): +class Pyramid(object): """A tensorflow representation of a transform domain signal. + Backends are free to implement any class which respects this interface for storing transform-domain signals, so long as the attributes have the correct names and are tensorflow tensors (or placeholders). The inverse transform may accept a backend-specific version of this class but should always accept any class which corresponds to this interface. - .. py:attribute:: X - A placeholder which the user can use when they want to evaluate the - forward dtcwt. .. py:attribute:: lowpass_op A tensorflow tensor that can be evaluated in a session to return the coarsest scale lowpass signal for the input, X. + .. py:attribute:: highpasses_op A tuple of tensorflow tensors, where each element is the complex subband coefficients for corresponding scales finest to coarsest. + .. py:attribute:: scales *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. - .. py:method:: apply_reshaping(fn) - A helper method to apply a tensor reshaping to all of the elements in - the pyramid. - .. py:method:: eval_fwd(X) - A helper method to evaluate the forward transform, feeding *X* as input - to the tensorflow session. Assumes that the object was returned from - the Transform2d().forward() method. - .. py:method:: eval_inv(Yl, Yh) - A helper method to evaluate the inverse transform, feeding *Yl* and - *Yh* to the tensorflow session. Assumes that the object was returned - from the Trasnform2d().inverse() method. """ - def __init__(self, X, lowpass, highpasses, scales=None, graph=None): - self.X = X + def __init__(self, lowpass, highpasses, scales=None, numpy=False): self.lowpass_op = lowpass self.highpasses_ops = highpasses self.scales_ops = scales - if graph is None: - try: - # This could fail if we don't have tensorflow - graph = tf.get_default_graph() - except NameError: - pass - self.graph = graph - - def _get_lowpass(self, data, sess=None): - if self.lowpass_op is None: - return None - - close_after = False - if sess is None: - sess = tf.Session(graph=self.graph) - close_after = True - - try: - y = sess.run(self.lowpass_op, {self.X: data}) - except ValueError: - y = sess.run(self.lowpass_op, {self.X: [data]})[0] - - if close_after: - sess.close() - - return y - - def _get_highpasses(self, data, sess=None): - if self.highpasses_ops is None: - return None - - # Only close sessions if we had to create them - close_after = False - if sess is None: - sess = tf.Session(graph=self.graph) - close_after = True - - try: - y = tuple( - [sess.run(layer_hp, {self.X: data}) - for layer_hp in self.highpasses_ops]) - except ValueError: - y = tuple( - [sess.run(layer_hp, {self.X: [data]})[0] - for layer_hp in self.highpasses_ops]) - - if close_after: - sess.close() - return y - - def _get_scales(self, data, sess=None): - if self.scales_ops is None: - return None - - close_after = False - if sess is None: - sess = tf.Session(graph=self.graph) - close_after = True - - try: - y = tuple( - sess.run(layer_scale, {self.X: data}) - for layer_scale in self.scales_ops) - except ValueError: - y = tuple( - sess.run(layer_scale, {self.X: [data]})[0] - for layer_scale in self.scales_ops) - - if close_after: - sess.close() - return y - - def _get_X(self, Yl, Yh, sess=None): - if self.X is None: - return None - - close_after = False - if sess is None: - sess = tf.Session(graph=self.graph) - close_after = True - - try: - # Use dictionary comprehension to feed in our Yl and our - # multiple layers of Yh - data = [Yl,] + list(Yh) - placeholders = [self.lowpass_op, ] + list(self.highpasses_ops) - X = sess.run(self.X, {i: d for i,d in zip(placeholders,data)}) - except ValueError: - data = [Yl,] + list(Yh) - placeholders = [self.lowpass_op, ] + list(self.highpasses_ops) - X = sess.run(self.X, {i: [d] for i,d in zip(placeholders,data)})[0] - - if close_after: - sess.close() - - return X - - def apply_reshaping(self, fn): - """ - A helper function to apply a tensor transformation on all of the - elements in the pyramid. E.g. reshape all of them in the same way. - - :param fn: function to apply to each of the lowpass_op, highpasses_ops - and scale_ops tensors - """ - self.lowpass_op = fn(self.lowpass_op) - self.highpasses_ops = tuple( - [fn(h_scale) for h_scale in self.highpasses_ops]) - if self.scales_ops is not None: - self.scales_ops = tuple( - [fn(s_scale) for s_scale in self.scales_ops]) - - def eval_fwd(self, X, sess=None): - """ - A helper function to evaluate the forward transform on a given array of - input data. - - :param X: A numpy array of shape [, height, width], where height - and width match the size of the placeholder fed to the forward - transform. - :param sess: Tensorflow session to use. If none is provided a temporary - session will be used. - - :returns: A :py:class:`dtcwt.Pyramid` of the data. The variables in - this pyramid will typically be only 2-dimensional (when calling the - numpy forward transform), but these will be 3 dimensional. - """ - lo = self._get_lowpass(X, sess) - hi = self._get_highpasses(X, sess) - scales = self._get_scales(X, sess) - return Pyramid_np(lo, hi, scales) - - def eval_inv(self, Yl, Yh, sess=None): - """ - A helper function to evaluate the inverse transform on given wavelet - coefficients. - - :param Yl: A numpy array of shape [, h/(2**scale), w/(2**scale)], - where (h,w) was the size of the input image. - :param Yh: A tuple or list of the highpass coefficients. Each entry in - the tuple or list represents the scale the coefficients belong to. - The size of the coefficients must match the outputs of the forward - transform. I.e. Yh[0] should have shape [, 6, h/2, w/2], - where the input image had shape (h, w). should be the same - across all scales, and should match the size of the Yl first - dimension. - :param sess: Tensorflow session to use. If none is provided a temporary - session will be used. - - :returns: A numpy array of the inverted data. - """ - return self._get_X(Yl, Yh, sess) + self.numpy = numpy + + @property + def lowpass(self): + if not hasattr(self, '_lowpass'): + if self.lowpass_op is None: + self._lowpass = None + else: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + self._lowpass = sess.run(self.lowpass_op) + return self._lowpass + + @property + def highpasses(self): + if not hasattr(self, '_highpasses'): + if self.highpasses_ops is None: + self._highpasses = None + else: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + self._highpasses = \ + tuple(sess.run(x) for x in self.highpasses_ops) + return self._highpasses + + @property + def scales(self): + if not hasattr(self, '_scales'): + if self.scales_ops is None: + self._scales = None + else: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + self._scales = tuple(sess.run(x) for x in self.scales_ops) + return self._scales diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 787cd87..b28b589 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -2,25 +2,44 @@ import numpy as np import logging +import warnings from six.moves import xrange +from dtcwt.coeffs import biort as _biort, qshift as _qshift from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT from dtcwt.utils import asfarray -from dtcwt.numpy import Transform2d as Transform2dNumPy +from dtcwt.tf import Pyramid from dtcwt.numpy import Pyramid as Pyramid_np -from dtcwt.tf import Pyramid_tf -from dtcwt.tf.lowlevel import * +from dtcwt.tf.lowlevel import coldfilt, rowdfilt, rowfilter, colfilter, colifilt try: import tensorflow as tf + from tensorflow.python.framework import dtypes + tf_dtypes = frozenset( + [dtypes.float32, dtypes.float64, dtypes.int8, dtypes.int16, + dtypes.int32, dtypes.int64, dtypes.uint8, dtypes.qint8, dtypes.qint32, + dtypes.quint8, dtypes.complex64, dtypes.complex128, + dtypes.float32_ref, dtypes.float64_ref, dtypes.int8_ref, + dtypes.int16_ref, dtypes.int32_ref, dtypes.int64_ref, dtypes.uint8_ref, + dtypes.qint8_ref, dtypes.qint32_ref, dtypes.quint8_ref, + dtypes.complex64_ref, dtypes.complex128_ref] + ) except ImportError: # The lack of tensorflow will be caught by the low-level routines. pass +np_dtypes = frozenset( + [np.dtype('float16'), np.dtype('float32'), np.dtype('float64'), + np.dtype('int8'), np.dtype('int16'), np.dtype('int32'), + np.dtype('int64'), np.dtype('uint8'), np.dtype('uint16'), + np.dtype('uint32'), np.dtype('complex64'), np.dtype('complex128')] +) -def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include_scale=False): + +def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, + include_scale=False): t = Transform2d(biort=biort, qshift=qshift) r = t.forward(X, nlevels=nlevels, include_scale=include_scale) if include_scale: @@ -29,28 +48,35 @@ def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, include return r.lowpass, r.highpasses -def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, gain_mask=None): +def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, + gain_mask=None): t = Transform2d(biort=biort, qshift=qshift) r = t.inverse(Pyramid_np(Yl, Yh), gain_mask=gain_mask) return r -class Transform2d(Transform2dNumPy): +class Transform2d(object): """ An implementation of the 2D DT-CWT via Tensorflow. - *biort* and *qshift* are the wavelets which parameterise the transform. - If *biort* or *qshift* are strings, they are used as an argument to the - :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` functions. - Otherwise, they are interpreted as tuples of vectors giving filter - coefficients. In the *biort* case, this should be (h0o, g0o, h1o, g1o). In - the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). + :param biort: The biorthogonal wavelet family to use. + :param qshift: The quarter shift wavelet family to use. + + .. note:: + + *biort* and *qshift* are the wavelets which parameterise the transform. + If *biort* or *qshift* are strings, they are used as an argument to the + :py:func:`dtcwt.coeffs.biort` or :py:func:`dtcwt.coeffs.qshift` + functions. Otherwise, they are interpreted as tuples of vectors giving + filter coefficients. In the *biort* case, this should be (h0o, g0o, h1o, + g1o). In the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, + h1b, g1a, g1b). Creating an object of this class loads the necessary filters onto the tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with - a placeholder will create a forward transform for an input of the - placeholder's size. You can evaluate the resulting ops several times - feeding different images into the placeholder *assuming* they have the same + an image (or placeholder) will create a forward transform for an input of + the image's size. You can evaluate the resulting ops several times feeding + different images into the placeholder *assuming* they have the same resolution. For a different resolution image, call the :py:func:`Transform2d.forward` function again. @@ -61,14 +87,23 @@ class Transform2d(Transform2dNumPy): """ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): - super(Transform2d, self).__init__(biort=biort, qshift=qshift) + try: + self.biort = _biort(biort) + except TypeError: + self.biort = biort + + # Load quarter sample shift wavelets + try: + self.qshift = _qshift(qshift) + except TypeError: + self.qshift = qshift # Use our own graph when the user calls forward with numpy arrays self.np_graph = tf.Graph() self.forward_graphs = {} self.inverse_graphs = {} def _find_forward_graph(self, shape): - ''' See if we can reuse an old graph for the forward transform ''' + """ See if we can reuse an old graph for the forward transform """ find_key = '{}x{}'.format(shape[0], shape[1]) for key, val in self.forward_graphs.items(): if find_key == key: @@ -76,12 +111,12 @@ def _find_forward_graph(self, shape): return None def _add_forward_graph(self, p_ops, shape): - ''' Keep record of the pyramid so we can use it later if need be ''' + """ Keep record of the pyramid so we can use it later if need be """ find_key = '{}x{}'.format(shape[0], shape[1]) self.forward_graphs[find_key] = p_ops def _find_inverse_graph(self, Lo_shape, nlevels): - ''' See if we can reuse an old graph for the inverse transform ''' + """ See if we can reuse an old graph for the inverse transform """ find_key = '{}x{}'.format(Lo_shape[0], Lo_shape[1]) for key, val in self.forward_graphs.items(): if find_key == key: @@ -89,248 +124,235 @@ def _find_inverse_graph(self, Lo_shape, nlevels): return None def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): - ''' Keep record of the pyramid so we can use it later if need be ''' + """ Keep record of the pyramid so we can use it later if need be """ find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) self.inverse_graphs[find_key] = p_ops - def forward(self, X, nlevels=3, include_scale=False, return_tuple=False, - undecimated=False, max_dec_scale=1): - ''' + def forward(self, X, nlevels=3, include_scale=False): + """ Perform a forward transform on an image. Can provide the forward transform with either an np array (naive usage), or a tensorflow variable or placeholder (designed usage). - :param X: Input image which you wish to transform. Can be a numpy - array, tensorflow Variable or tensorflow placeholder. See comments - below. - :param nlevels: Number of levels of the dtcwt transform to calculate. - :param include_scale: Whether or not to return the lowpass results at - each scale of the transform, or only at the highest scale (as is - custom for multiresolution analysis) - :param return_tuple: If true, returns a tuple of lowpass, highpasses - and scales (if include_scale is True), rather than a Pyramid object. + :param ndarray X: Input image which you wish to transform. Can be a + numpy array, tensorflow Variable or tensorflow placeholder. See + comments below. + :param int nlevels: Number of levels of the dtcwt transform to + calculate. + :param bool include_scale: Whether or not to return the lowpass results + at each scale of the transform, or only at the highest scale (as is + custom for multi-resolution analysis) - :returns: A :py:class:`Pyramid_tf` object or a :py:class:`Pyramid` - object, depending on the type of input data provided. + :returns: A :py:class:`Pyramid` like object - Data Types for the :py:param:`X`: - If a numpy array is provided, the forward function will create a graph - of the right size to match the input (or check if it has previously - created one), and then feed the input into the graph and evaluate it. - This operation will return a :py:class:`Pyramid` object similar to - how running the numpy version would. + .. note:: - If a tensorflow variable or placeholder is provided, the forward - function will create a graph of the right size, and return - a Pyramid_ops() object. + If a numpy array is provided, the forward function will create a + tensorflow variable to hold the input image, and then create the + graph of the right size to match the input, and then feed the + input into the graph and evaluate it. This operation will + return a :py:class:`Pyramid` object similar to how running + the numpy version would. .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 - ''' + """ # Check if a numpy array was provided - if not tf.is_numeric_tensor(X): - X = np.atleast_2d(asfarray(X)) - if len(X.shape) >= 3: - raise ValueError('''The entered variable has incorrect dimensions {}. - If X is a numpy array (or any non tensorflow object), it - must be of shape [height, width]. For colour images, please - enter each channel separately. If you wish to enter a batch - of images, please instead provide either a tf.Placeholder - or a tf.Variable input of size [batch, height, width]. - '''.format(X.shape)) - - # Check if the ops already exist for an input of the given size - p_ops = self._find_forward_graph(X.shape) - - # If not, create a graph - if p_ops is None: - ph = tf.placeholder(tf.float32, [None, X.shape[0], X.shape[1]]) - size = '{}x{}'.format(X.shape[0], X.shape[1]) - name = 'dtcwt_fwd_{}'.format(size) - with self.np_graph.name_scope(name): - p_ops = self._forward_ops( - ph, nlevels, include_scale, False, undecimated, - max_dec_scale) - - self._add_forward_graph(p_ops, X.shape) - - # Evaluate the graph with the given input - with self.np_graph.as_default(): - return p_ops.eval_fwd(X) + numpy = False + try: + dtype = X.dtype + except AttributeError: + X = asfarray(X) + dtype = X.dtype + + if dtype in np_dtypes: + numpy = True + X = np.atleast_2d(X) + X = tf.Variable(X, dtype=tf.float32, trainable=False) + + if X.dtype not in tf_dtypes: + raise ValueError('I cannot handle the variable you have ' + + 'provided of type ' + str(X.dtype) + '. ' + + 'Inputs should be a numpy or tf array') + + X_shape = tuple(X.get_shape().as_list()) + extended = False + if len(X_shape) == 2: + # Need to make it a batch for tensorflow + X = tf.expand_dims(X, axis=0) + extended = True + elif len(X_shape) == 3: + if X_shape[2] == 3 and X_shape[1] != 3: + warnings.warn('It looks like you may have entered an RGB ' + + 'image of shape ' + str(X_shape) + '. The ' + + 'tf backend can handle batches of images, ' + + 'but needs the batch to be the zeroth ' + + 'dimension.') + elif len(X_shape) > 3: + raise ValueError( + 'The entered variable has too many ' + + 'dimensions - ' + str(X_shape) + '. For batches of ' + + 'images with multiple channels (i.e. 4 dimensions), ' + + 'please either enter each channel separately, or use ' + + 'the forward_channels method.') + + X_shape = tuple(X.get_shape().as_list()) + original_size = X_shape[1:] + size = '{}x{}'.format(original_size[0], original_size[1]) + name = 'dtcwt_fwd_{}'.format(size) + with tf.name_scope(name): + Yl, Yh, Yscale = self._forward_ops(X, nlevels) + + if extended: + Yl = Yl[0] + Yh = tuple(x[0] for x in Yh) + Yscale = tuple(x[0] for x in Yscale) - # A tensorflow object was provided + if include_scale: + return Pyramid(Yl, Yh, Yscale, numpy) else: - X_shape = X.get_shape().as_list() - if len(X_shape) > 3: - raise ValueError( - '''The entered variable has incorrect dimensions {}. - If X is a tf placeholder or variable, it must be of shape - [batch, height, width] (batch can be None) or - [height, width]. For colour images, please enter each - channel separately.'''.format(X_shape)) - - # If a batch wasn't provided, add a none dimension and remove it - # later - if len(X_shape) == 2: - logging.warn('Fed with a 2d shape input. For efficient ' + - 'calculation feed batches of inputs. Input was ' + - 'reshaped to have a 1 in the first dimension.') - X = tf.expand_dims(X, axis=0) - - original_size = X.get_shape().as_list()[1:] - size = '{}x{}'.format(original_size[0], original_size[1]) - name = 'dtcwt_fwd_{}'.format(size) - with tf.name_scope(name): - p_tf = self._forward_ops( - X, nlevels, include_scale, return_tuple, undecimated, - max_dec_scale) - - return p_tf + return Pyramid(Yl, Yh, None, numpy) - def forward_channels(self, X, nlevels=3, # include_scale=True, - data_format="nhwc", undecimated=False, - max_dec_scale=1): - ''' - Perform a forward transform on an image with multiple channels. + def forward_channels(self, X, nlevels=3, include_scale=False, + data_format="nhwc"): + """ Perform a forward transform on an image with multiple channels. - Must provide with a tensorflow variable or placeholder (unlike the more - general :py:method:`Transform2d.forward`). + Will perform the DTCWT independently on each channel. :param X: Input image which you wish to transform. - :param nlevels: Number of levels of the dtcwt transform to calculate. - :param include_scale: Whether or not to return the lowpass results at - each sclae of the transform, or only at the highest scale (as is + :param int nlevels: Number of levels of the dtcwt transform to + calculate. + :param bool include_scale: Whether or not to return the lowpass results + at each scale of the transform, or only at the highest scale (as is custom for multiresolution analysis) - :param data_format: An optional string of the form "nchw" or "nhwc", + :param str data_format: An optional string of the form "nchw" or "nhwc", specifying the data format of the input. If format is "nchw" (the default), then data is in the form [batch, channels, h, w]. If the format is "nhwc", then the data is in the form [batch, h, w, c]. - :returns: tuple - A tuple of (Yl, Yh, Yscale). - The order of output axes will match the input axes (i.e. the - position of the channel dimension). I.e. (note that the spatial - sizes will change) - Yl: [batch, c, h, w] OR [batch, h, w, c] - Yh: [batch, c, h, w, 6] OR [batch, h, w, c, 6] - Yscale: [batch, c, h, w, 6] OR [batch, h, w, c, 6] - - Yl corresponds to the lowpass of the image, and has shape - [batch, channels, height, width] of type tf.float32. - Yh corresponds to the highpasses for the image, and is a list of length - nlevels, with each entry having shape - [batch, channels, height', width', 6] of type tf.complex64. - Yscale corresponds to the lowpass outputs at each scale of the - transform, and is a list of length nlevels, with each entry having - shape [batch, channels, height', width'] of type tf.float32. + :returns: Yl - the lowpass output and the final scale. + :returns: Yh - the highpass outputs. + :returns: Yscale - the lowpass output at intermediate scales. .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 - ''' - # include_scale used to be a parameter, but removed for ease - include_scale = True - + """ data_format = data_format.lower() if data_format != "nchw" and data_format != "nhwc": raise ValueError('The data format must be either "ncwh" or ' + '"nhwc", not {}'.format(data_format)) - if not tf.is_numeric_tensor(X): - raise ValueError('The provided input must be a tensorflow ' + - 'variable or placeholder') - else: - X_shape = X.get_shape().as_list() - if len(X_shape) != 4: - raise ValueError( - '''The entered variable has incorrect dimensions {}. - It must be of shape [batch, channels, height, width] - (batch can be None).'''.format(X_shape)) - - with tf.variable_scope('ch_to_batch'): - s = X.get_shape().as_list()[1:] - size = '{}x{}'.format(s[0], s[1]) - name = 'dtcwt_fwd_{}'.format(size) - # Move all of the channels into the batch dimension for the - # input. This may involve transposing, depending on the data - # format - if data_format == 'nhwc': - nch = s[2] - X = tf.transpose(X, perm=[0, 3, 1, 2]) - X = tf.reshape(X, [-1, s[0], s[1]]) - else: - nch = s[0] - X = tf.reshape(X, [-1, s[1], s[2]]) - - with tf.variable_scope(name): - Yl, Yh, Yscale = self._forward_ops( - X, nlevels, include_scale, return_tuple=True, - undecimated=undecimated, max_dec_scale=max_dec_scale) - - # Put the channels back into their correct positions - with tf.variable_scope('batch_to_ch'): - # Reshape Yl - s = Yl.get_shape().as_list()[1:] - Yl = tf.reshape(Yl, [-1, nch, s[0], s[1]], name='Yl_reshape') - if data_format == 'nhwc': - Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') - - # Reshape Yh - with tf.variable_scope('Yh'): - Yh_new = [None,] * nlevels + + try: + dtype = X.dtype + except AttributeError: + X = asfarray(X) + dtype = X.dtype + + numpy = False + if dtype in np_dtypes: + numpy = True + X = np.atleast_2d(X) + X = tf.Variable(X, dtype=tf.float32, trainable=False) + + if X.dtype not in tf_dtypes: + raise ValueError('I cannot handle the variable you have ' + + 'provided of type ' + str(X.dtype) + '. ' + + 'Inputs should be a numpy or tf array.') + + X_shape = X.get_shape().as_list() + if len(X_shape) != 4: + raise ValueError( + 'The entered variable has incorrect shape - ' + + str(X_shape) + '. It must have 4 dimensions. For 2 or 3 ' + + 'dimensioned input, use the forward method.') + + # Move all of the channels into the batch dimension for the + # input. This may involve transposing, depending on the data + # format + with tf.variable_scope('ch_to_batch'): + s = X.get_shape().as_list()[1:] + size = '{}x{}'.format(s[0], s[1]) + name = 'dtcwt_fwd_{}'.format(size) + if data_format == 'nhwc': + nch = s[2] + X = tf.transpose(X, perm=[0, 3, 1, 2]) + X = tf.reshape(X, [-1, s[0], s[1]]) + else: + nch = s[0] + X = tf.reshape(X, [-1, s[1], s[2]]) + + # Do the dtcwt, now with a 3 dimensional input + with tf.variable_scope(name): + Yl, Yh, Yscale = self._forward_ops(X, nlevels) + + # Put the channels back into their correct positions + with tf.variable_scope('batch_to_ch'): + # Reshape Yl + s = Yl.get_shape().as_list()[1:] + Yl = tf.reshape(Yl, [-1, nch, s[0], s[1]], name='Yl_reshape') + if data_format == 'nhwc': + Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') + + # Reshape Yh + with tf.variable_scope('Yh'): + Yh_new = [None,] * nlevels + for i in range(nlevels): + s = Yh[i].get_shape().as_list()[1:] + Yh_new[i] = tf.reshape( + Yh[i], [-1, nch, s[0], s[1], s[2]], + name='scale{}_reshape'.format(i)) + if data_format == 'nhwc': + Yh_new[i] = tf.transpose( + Yh_new[i], [0, 2, 3, 1, 4], + name='scale{}_ch_to_end'.format(i)) + Yh = tuple(Yh_new) + + # Reshape Yscale + if include_scale: + with tf.variable_scope('Yscale'): + Yscale_new = [None,] * nlevels for i in range(nlevels): - s = Yh[i].get_shape().as_list()[1:] - Yh_new[i] = tf.reshape( - Yh[i], [-1, nch, s[0], s[1], s[2]], + s = Yscale[i].get_shape().as_list()[1:] + Yscale_new[i] = tf.reshape( + Yscale[i], [-1, nch, s[0], s[1]], name='scale{}_reshape'.format(i)) if data_format == 'nhwc': - Yh_new[i] = tf.transpose( - Yh_new[i], [0, 2, 3, 1, 4], + Yscale_new[i] = tf.transpose( + Yscale_new[i], [0, 2, 3, 1], name='scale{}_ch_to_end'.format(i)) - Yh = tuple(Yh_new) - - # Reshape Yscale - if include_scale: - with tf.variable_scope('Yscale'): - Yscale_new = [None,] * nlevels - for i in range(nlevels): - s = Yscale[i].get_shape().as_list()[1:] - Yscale_new[i] = tf.reshape( - Yscale[i], [-1, nch, s[0], s[1]], - name='scale{}_reshape'.format(i)) - if data_format == 'nhwc': - Yscale_new[i] = tf.transpose( - Yscale_new[i], [0, 2, 3, 1], - name='scale{}_ch_to_end'.format(i)) - Yscale = tuple(Yscale_new) - return Yl, Yh, Yscale - else: - return Yl, Yh + Yscale = tuple(Yscale_new) + + if include_scale: + return Pyramid(Yl, Yh, Yscale, numpy) + else: + return Pyramid(Yl, Yh, None, numpy) def inverse(self, pyramid, gain_mask=None): - ''' - Perform an inverse transform on an image. + """ Perform an inverse transform on an image. Can provide the inverse transform with either an np array (naive usage), or a tensorflow variable or placeholder (designed usage). - :param pyramid: A :py:class:`dtcwt.Pyramid` or - `:py:class:`dtcwt.tf.Pyramid_tf` like class holding the transform - domain representation to invert - :param gain_mask: Gain to be applied to each subband. Should have shape - [6, nlevels]. + :param pyramid: A :py:class:`dtcwt.tf.Pyramid` like class holding + the transform domain representation to invert + :param gain_mask: Gain to be applied to each sub-band. Should have shape + (6, nlevels) or be None. :returns: Either a tf.Variable or a numpy array compatible with the reconstruction. - A tf.Variable is returned if the pyramid input was a Pyramid_tf class. - If it wasn't, then, we return a numpy array (note that this is - inefficient, as in both cases we have to construct the graph - in the - second case, we then execute it and discard it). + .. note:: + + A tf.Variable is returned if the pyramid input was a Pyramid class. + If it wasn't, then, we return a numpy array (note that this is + inefficient, as in both cases we have to construct the graph - in + the second case, we then execute it and discard it). The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is @@ -341,63 +363,71 @@ def inverse(self, pyramid, gain_mask=None): .. codeauthor:: Rich Wareham , Aug 2013 .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 - ''' - - # Check if a numpy array was provided - if isinstance(pyramid, Pyramid_np) or \ - hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): - - Yl, Yh = pyramid.lowpass, pyramid.highpasses - - # Check if the ops already exist for an input of the given size - nlevels = len(Yh) - p_ops = self._find_inverse_graph(Yl.shape, nlevels) - - # If not, create a graph - if p_ops is None: - Lo_ph = tf.placeholder( - tf.float32, [None, Yl.shape[0], Yl.shape[1]]) - Hi_ph = tuple( - tf.placeholder(tf.complex64, (None,) + level.shape) - for level in Yh) - p_in = Pyramid_tf(None, Lo_ph, Hi_ph) - size = '{}x{}_up_{}'.format(Yl.shape[0], Yl.shape[1], nlevels) - name = 'dtcwt_inv_{}'.format(size) - - with self.np_graph.name_scope(name): - p_ops = self._inverse_ops(p_in, gain_mask) - - # keep record of the pyramid so we can use it later if need be - self.forward_graphs[size] = p_ops - - # Evaluate the graph with the given input - with self.np_graph.as_default(): - return p_ops.eval_inv(Yl, Yh) + """ # A tensorflow object was provided - elif isinstance(pyramid, Pyramid_tf): - s = pyramid.lowpass_op.get_shape().as_list()[1:] - nlevels = len(pyramid.highpasses_ops) - size = '{}x{}_up_{}'.format(s[0], s[1], nlevels) - name = 'dtcwt_inv_{}'.format(size) - with tf.name_scope(name): - return self._inverse_ops(pyramid, gain_mask) + numpy = False + if isinstance(pyramid, Pyramid): + Yl = pyramid.lowpass_op + Yh = pyramid.highpasses_ops + numpy = pyramid.numpy + + # Check if a numpy pyramid was provided + elif isinstance(pyramid, Pyramid_np) or \ + hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): + numpy = True + Yl, Yh = pyramid.lowpass, pyramid.highpasses + Yl = tf.Variable(Yl, trainable=False, dtype=tf.float32) + Yh = tuple( + tf.Variable(level, trainable=False, dtype=tf.complex64) + for level in Yh) else: raise ValueError( 'Unknown pyramid provided to inverse transform') - def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): - ''' + # Need to make sure it has at least 3 dimensions for tensorflow + extended = False + Yl_shape = tuple(Yl.get_shape().as_list()) + if len(Yl_shape) == 2: + Yl = tf.expand_dims(Yl, axis=0) + Yh = tuple(tf.expand_dims(x, axis=0) for x in Yh) + extended = True + elif len(Yl_shape) == 4: + raise ValueError( + 'The entered variables have too many ' + + 'dimensions - ' + str(Yl_shape) + '. For batches of ' + + 'images with multiple channels (i.e. 4 dimensions), ' + + 'please either enter each channel separately, or use ' + + 'the inverse_channels method.') + + # Do the inverse transform + s = Yl.get_shape().as_list()[1:] + nlevels = len(Yh) + size = '{}x{}_up_{}'.format(s[0], s[1], nlevels) + name = 'dtcwt_inv_{}'.format(size) + with tf.name_scope(name): + X = self._inverse_ops(Yl, Yh, gain_mask) + + # Return data in a shape the user was expecting + if extended: + X = X[0] + + if numpy: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + X = sess.run(X) + + return X + + def inverse_channels(self, pyramid, gain_mask=None, data_format="nhwc"): + """ Perform an inverse transform on an image with multiple channels. Must provide with a tensorflow variable or placeholder (unlike the more general :py:method:`Transform2d.inverse`). - :param Yl: Lowpass data. - :param Yh: A list-like structure of length nlevels. At each level, the - tensor should be of size [batch, h', w', c, 6] and of tf.complex64 - type. The sizes must match up with what would be created from the - forward transform. + :param pyramid: A :py:class:`dtcwt.tf.Pyramid` like class holding + the transform domain representation to invert :param gain_mask: Gain to be applied to each subband. Should have shape [6, nlevels]. :param data_format: An optional string of the form "nchw" or "nhwc", @@ -418,7 +448,7 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): .. codeauthor:: Rich Wareham , Aug 2013 .. codeauthor:: Nick Kingsbury, Cambridge University, Sept 2001 .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 - ''' + """ # Input checking data_format = data_format.lower() if data_format != "nchw" and data_format != "nhwc": @@ -429,27 +459,32 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): else: channel_ax = 1 - if not tf.is_numeric_tensor(Yl): - raise ValueError('The provided lowpass input must be a ' + - 'tensorflow variable or placeholder') + # A tensorflow object was provided + numpy = False + if isinstance(pyramid, Pyramid): + Yl = pyramid.lowpass_op + Yh = pyramid.highpasses_ops + numpy = pyramid.numpy + + # Check if a numpy pyramid was provided + elif isinstance(pyramid, Pyramid_np) or \ + hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): + numpy = True + Yl, Yh = pyramid.lowpass, pyramid.highpasses + Yl = tf.Variable(Yl, trainable=False, dtype=tf.float32) + Yh = tuple( + tf.Variable(level, trainable=False, dtype=tf.complex64) + for level in Yh) + else: + raise ValueError( + 'Unknown pyramid provided to inverse transform') + + # Check the shape was 4D Yl_shape = Yl.get_shape().as_list() if len(Yl_shape) != 4: raise ValueError( - '''The entered lowpass variable has incorrect dimensions {}. - for data_format of {}.'''.format(Yl_shape, data_format)) - - for scale in Yh: - if not tf.is_numeric_tensor(scale): - raise ValueError('The provided highpass inputs must be a ' + - 'tensorflow variable or placeholder') - if scale.dtype != tf.complex64: - raise ValueError('The provided highpass inputs must be ' + - 'complex numbers of 32 point precision.') - Yh_shape = scale.get_shape().as_list() - if len(Yh_shape) != 5 or Yh_shape[-1] != 6: - raise ValueError( - '''The entered highpass variable has incorrect dimensions {} - for data_format of {}.'''.format(Yh_shape, data_format)) + """The entered lowpass variable has incorrect dimensions {}. + for data_format of {}.""".format(Yl_shape, data_format)) # Move all of the channels into the batch dimension for the lowpass # input. This may involve transposing, depending on the data format @@ -472,46 +507,41 @@ def inverse_channels(self, Yl, Yh, gain_mask=None, data_format="nhwc"): s = scale.get_shape().as_list() if s[channel_ax] != num_channels: raise ValueError( - '''The number of channels has to be consistent for all + """The number of channels has to be consistent for all inputs across the channel axis {}. You fed in Yl: {} - and Yh: {}'''.format(channel_ax, Yl, Yh)) + and Yh: {}""".format(channel_ax, Yl, Yh)) if data_format == "nhwc": scale = tf.transpose(scale, [0, 3, 1, 2, 4]) Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) else: Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) - pyramid = Pyramid_tf(None, Yl_new, Yh_new) - name = 'dtcwt_inv_{}_{}channels'.format(size, num_channels) with tf.variable_scope(name): - P = self._inverse_ops(pyramid, gain_mask) - s = P.X.get_shape().as_list() - X = tf.reshape(P.X, [-1, num_channels, s[1], s[2]]) + X = self._inverse_ops(Yl_new, Yh_new, gain_mask) + + with tf.variable_scope('batch_to_ch'): + s = X.get_shape().as_list() + X = tf.reshape(X, [-1, num_channels, s[1], s[2]]) if data_format == "nhwc": X = tf.transpose(X, [0, 2, 3, 1], name='X') - return X - def _forward_ops(self, X, nlevels=3, include_scale=False, - return_tuple=False, undecimated=False, - max_dec_scale=1): - """ - Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. + if numpy: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + X = sess.run(X) + + return X + + def _forward_ops(self, X, nlevels=3): + """ Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. :param X: 3D real array of size [batch, h, w] :param nlevels: Number of levels of wavelet decomposition - :param include_scale: True if you want to receive the lowpass - coefficients at intermediate layers. - :param return_tuple: If true, instead of returning - a :py:class`dtcwt.Pyramid_tf` object, return a tuple of - (lowpass, highpasses, scales) - :param undecimated: If true, will stop decimating the transform - :param max_undec_scale: The maximum undecimated scale. Will be used if - undecimated is set to True. Beyond this scale, stop decimating - (useful if you want to partially decimate) - - :returns: A :py:class:`dtcwt.Pyramid_tf` compatible - object representing the transform-domain signal + :param extended: True if a singleton dimension was added at the + beginning of the input. Signal to remove afterwards. + + :returns: A tuple of Yl, Yh, Yscale """ # If biort has 6 elements instead of 4, then it's a modified @@ -535,22 +565,16 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, raise ValueError('Qshift wavelet must have 12 or 8 components.') # Check the shape and form of the input - if not tf.is_numeric_tensor(X): - raise ValueError( - '''Please provide the forward function with a tensorflow - placeholder or variable of size [batch, width, height] (batch - can be None if you do not wish to specify it).''') + if X.dtype not in tf_dtypes: + raise ValueError('X needs to be a tf variable or placeholder') original_size = X.get_shape().as_list()[1:] if len(original_size) >= 3: raise ValueError( - '''The entered variable has too many dimensions {}. If + """The entered variable has too many dimensions {}. If the final dimension are colour channels, please enter each - channel separately.'''.format(original_size)) - - # Save the input placeholder/variable - X_in = X + channel separately.""".format(original_size)) # ############################ Resize ################################# # The next few lines of code check to see if the image is odd in size, @@ -575,23 +599,13 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, extended_size = X.get_shape().as_list()[1:3] if nlevels == 0: - if include_scale: - if return_tuple: - return X_in, (), () - else: - return Pyramid_tf(X_in, X, (), ()) - else: - if return_tuple: - return X_in, () - else: - return Pyramid_tf(X_in, X, ()) + return X, (), () # ########################### Initialise ############################### Yh = [None, ] * nlevels - if include_scale: - # This is only required if the user specifies a third output - # component. - Yscale = [None, ] * nlevels + # This is only required if the user specifies a third output + # component. + Yscale = [None, ] * nlevels # ############################ Level 1 ################################# # Uses the biorthogonal filters @@ -623,8 +637,7 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], axis=3) - if include_scale: - Yscale[0] = LoLo + Yscale[0] = LoLo # ############################ Level 2+ ################################ # Uses the qshift filters @@ -667,8 +680,7 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, [horiz[0], diag[0], vertic[0], vertic[1], diag[1], horiz[1]], axis=3) - if include_scale: - Yscale[level] = LoLo + Yscale[level] = LoLo Yl = LoLo @@ -677,8 +689,8 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, 'x'.join(list(str(s) for s in extended_size)), 'x'.join(list(str(s) for s in original_size)))) logging.warn( - '''The bottom row and rightmost column have been duplicated, - prior to decomposition.''') + """The bottom row and rightmost column have been duplicated, + prior to decomposition.""") if initial_row_extend == 1 and initial_col_extend == 0: logging.warn('The image entered is now a {0} NOT a {1}.'.format( @@ -692,34 +704,22 @@ def _forward_ops(self, X, nlevels=3, include_scale=False, 'x'.join(list(str(s) for s in extended_size)), 'x'.join(list(str(s) for s in original_size)))) logging.warn( - '''The rightmost column has been duplicated, prior to - decomposition.''') + """The rightmost column has been duplicated, prior to + decomposition.""") - if include_scale: - if return_tuple: - return Yl, tuple(Yh), tuple(Yscale) - else: - return Pyramid_tf(X_in, Yl, tuple(Yh), tuple(Yscale)) - else: - if return_tuple: - return Yl, tuple(Yh), None - else: - return Pyramid_tf(X_in, Yl, tuple(Yh), None) + return Yl, tuple(Yh), tuple(Yscale) - def _inverse_ops(self, pyramid, gain_mask=None): + def _inverse_ops(self, Yl, Yh, gain_mask=None): """Perform an *n*-level dual-tree complex wavelet (DTCWT) 2D reconstruction. - :param pyramid: A :py:class:`dtcwt.tf.Pyramid_tf`-like class holding the - transform domain representation to invert. + :param Yl: The lowpass output from a forward transform. Should be a + tensorflow variable. + :param Yh: The tuple of highpass outputs from a forward transform. + Should be tensorflow variables. :param gain_mask: Gain to be applied to each subband. - :param undecimated: If true, will stop decimating the transform - :param max_undec_scale: The maximum undecimated scale. Will be used if - undecimated is set to True. Beyond this scale, stop decimating - (useful if you want to partially decimate) - :returns: A :py:class:`dtcwt.tf.Pyramid_tf` class which can be - evaluated to get the inverted signal, X. + :returns: A tf.Variable holding the output The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is @@ -732,9 +732,6 @@ def _inverse_ops(self, pyramid, gain_mask=None): .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 """ - Yl = pyramid.lowpass_op - Yh = pyramid.highpasses_ops - a = len(Yh) # No of levels. if gain_mask is None: @@ -851,7 +848,7 @@ def _inverse_ops(self, pyramid, gain_mask=None): # Do odd top-level filters on rows. Z = rowfilter(y1, g0o) + rowfilter(y2, g1o) - return Pyramid_tf(Z, Yl, Yh) + return Z def q2c(y): diff --git a/dtcwt/utils.py b/dtcwt/utils.py index b6c9406..f28baa5 100644 --- a/dtcwt/utils.py +++ b/dtcwt/utils.py @@ -5,11 +5,42 @@ import functools import numpy as np -try: - import tensorflow as tf - _HAVE_TF = True -except ImportError: - _HAVE_TF = False + +def unpack(pyramid, backend='numpy'): + """ Unpacks a pyramid give back the constituent parts. + + :param pyramid: The Pyramid of DTCWT transforms you wish to unpack + :param str backend: A string from 'numpy', 'opencl', or 'tf' indicating + which attributes you want to unpack from the pyramid. + + :returns: returns a generator which can be unpacked into the Yl, Yh and + Yscale components of the pyramid. The generator will only return 2 + values if the pyramid was created with the include_scale parameter set + to false. + + .. note:: + + You can still unpack a tf or opencl pyramid as if it were created by a + numpy. In this case it will return a numpy array, rather than the + backend specific array type. + """ + backend = backend.lower() + if backend == 'numpy': + yield pyramid.lowpass + yield pyramid.highpasses + if pyramid.scales is not None: + yield pyramid.scales + elif backend == 'opencl': + yield pyramid.cl_lowpass + yield pyramid.cl_highpasses + if pyramid.cl_scales is not None: + yield pyramid.cl_scales + elif backend == 'tf': + yield pyramid.lowpass_op + yield pyramid.highpasses_ops + if pyramid.scales_ops is not None: + yield pyramid.scales_ops + def drawedge(theta,r,w,N): """Generate an image of size N * N pels, of an edge going from 0 to 1 @@ -17,46 +48,46 @@ def drawedge(theta,r,w,N): r is a two-element vector, it is a coordinate in ij coords through which the step should pass. The shape of the intensity step is half a raised cosine w pels wide (w>=1). - - T. E . Gale's enhancement to drawedge() for MATLAB, transliterated + + T. E . Gale's enhancement to drawedge() for MATLAB, transliterated to Python by S. C. Forshaw, Nov. 2013. """ - + # convert theta from degrees to radians - thetar = np.array(theta * np.pi / 180) - + thetar = np.array(theta * np.pi / 180) + # Calculate image centre from given width - imCentre = (np.array([N,N]).T - 1) / 2 + 1 - + imCentre = (np.array([N,N]).T - 1) / 2 + 1 + # Calculate values to subtract from the plane - r = np.array([np.cos(thetar), np.sin(thetar)])*(-1) * (r - imCentre) + r = np.array([np.cos(thetar), np.sin(thetar)])*(-1) * (r - imCentre) # check width of raised cosine section w = np.maximum(1,w) - - + + ramp = np.arange(0,N) - (N+1)/2 hgrad = np.sin(thetar)*(-1) * np.ones([N,1]) vgrad = np.cos(thetar)*(-1) * np.ones([1,N]) plane = ((hgrad * ramp) - r[0]) + ((ramp * vgrad).T - r[1]) x = 0.5 + 0.5 * np.sin(np.minimum(np.maximum(plane*(np.pi/w), np.pi/(-2)), np.pi/2)) - + return x def drawcirc(r,w,du,dv,N): - - """Generate an image of size N*N pels, containing a circle + + """Generate an image of size N*N pels, containing a circle radius r pels and centred at du,dv relative - to the centre of the image. The edge of the circle is a cosine shaped + to the centre of the image. The edge of the circle is a cosine shaped edge of width w (from 10 to 90% points). - + Python implementation by S. C. Forshaw, November 2013.""" - + # check value of w to avoid dividing by zero w = np.maximum(w,1) - + #x plane x = np.ones([N,1]) * ((np.arange(0,N,1, dtype='float') - (N+1) / 2 - dv) / r) - + # y vector y = (((np.arange(0,N,1, dtype='float') - (N+1) / 2 - du) / r) * np.ones([1,N])).T @@ -81,7 +112,7 @@ def appropriate_complex_type_for(X): """ X = asfarray(X) - + if np.issubsctype(X.dtype, np.complex64) or np.issubsctype(X.dtype, np.complex128): return X.dtype elif np.issubsctype(X.dtype, np.float32): @@ -94,7 +125,7 @@ def appropriate_complex_type_for(X): def as_column_vector(v): """Return *v* as a column vector with shape (N,1). - + """ v = np.atleast_2d(v) if v.shape[0] == 1: diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 3b2751f..ab491d5 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -7,6 +7,7 @@ from importlib import import_module from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.coeffs import biort, qshift +from dtcwt.utils import unpack import tests.datasets as datasets from scipy import stats from .util import skip_if_no_tf @@ -18,19 +19,20 @@ @skip_if_no_tf def setup(): global mandrill, in_p, pyramid_ops - global tf, Transform2d, dtwavexfm2, dtwaveifm2 + global tf, Transform2d, dtwavexfm2, dtwaveifm2, Pyramid_tf + global np_dtypes, tf_dtypes # Import the tensorflow modules tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') Transform2d = getattr(dtcwt_tf, 'Transform2d') + Pyramid_tf = getattr(dtcwt_tf, 'Pyramid') dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') dtwaveifm2 = getattr(dtcwt_tf_xfm2, 'dtwaveifm2') + np_dtypes = getattr(dtcwt_tf_xfm2, 'np_dtypes') + tf_dtypes = getattr(dtcwt_tf_xfm2, 'tf_dtypes') mandrill = datasets.mandrill() - in_p = tf.placeholder(tf.float32, [None, 512, 512]) - f = Transform2d() - pyramid_ops = f.forward(in_p, include_scale=True) # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" @@ -164,20 +166,164 @@ def test_float32_input(): @skip_if_no_tf -def test_eval_fwd(): - # Test it runs without error - pyramid_ops.eval_fwd(mandrill) +def test_numpy_in(): + X = np.random.randn(100,100) + f = Transform2d() + p = f.forward(X) + f1 = Transform2d_np() + p1 = f1.forward(X) + np.testing.assert_array_almost_equal( + p.lowpass, p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + + X = np.random.randn(100,100) + p = f.forward(X, include_scale=True) + p1 = f1.forward(X, include_scale=True) + np.testing.assert_array_almost_equal( + p.lowpass, p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_numpy_in_batch(): + X = np.random.randn(5,100,100) + f = Transform2d() + p = f.forward(X, include_scale=True) + f1 = Transform2d_np() + for i in range(5): + p1 = f1.forward(X[i], include_scale=True) + np.testing.assert_array_almost_equal( + p.lowpass[i], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_numpy_batch_ch(): + X = np.random.randn(5,100,100,4) + f = Transform2d() + p = f.forward_channels(X, include_scale=True) + f1 = Transform2d_np() + for i in range(5): + for j in range(4): + p1 = f1.forward(X[i,:,:,j], include_scale=True) + + np.testing.assert_array_almost_equal( + p.lowpass[i,:,:,j], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i,:,:,j], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i,:,:,j], y, decimal=PRECISION_DECIMAL) + + +# Test end to end with numpy inputs +@skip_if_no_tf +def test_2d_input(): + f = Transform2d() + X = np.random.randn(100,100) + p = f.forward(X) + x = f.inverse(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_3d_input(): + f = Transform2d() + X = np.random.randn(5,100,100) + p = f.forward(X) + x = f.inverse(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) @skip_if_no_tf -def test_multiple_inputs(): - y = pyramid_ops.eval_fwd(mandrill) - y3 = pyramid_ops.eval_fwd([mandrill, mandrill, mandrill]) - assert y3.lowpass.shape == (3,) + y.lowpass.shape - for hi3, hi in zip(y3.highpasses, y.highpasses): - assert hi3.shape == (3,) + hi.shape - for s3, s in zip(y3.scales, y.scales): - assert s3.shape == (3,) + s.shape +def test_4d_input(): + f = Transform2d() + X = np.random.randn(5,100,100,4) + p = f.forward_channels(X) + x = f.inverse_channels(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) + + +# Test end to end with tf inputs +@skip_if_no_tf +def test_2d_input_tf(): + xfm = Transform2d() + X = np.random.randn(100,100) + X_p = tf.placeholder(tf.float32, [100,100]) + p = xfm.forward(X_p) + x = xfm.inverse(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:X}), decimal=PRECISION_DECIMAL) + + X_p = tf.placeholder(tf.float32, [None, 100,100]) + p = xfm.forward(X_p) + x = xfm.inverse(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:[X]})[0], decimal=PRECISION_DECIMAL) + + +# Test end to end with tf inputs +@skip_if_no_tf +def test_3d_input_tf(): + xfm = Transform2d() + X = np.random.randn(5,100,100) + X_p = tf.placeholder(tf.float32, [None,100,100]) + p = xfm.forward(X_p) + x = xfm.inverse(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:X}), decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_4d_input_tf(): + xfm = Transform2d() + X = np.random.randn(5,100,100,5) + X_p = tf.placeholder(tf.float32, [None,100,100,5]) + p = xfm.forward_channels(X_p) + x = xfm.inverse_channels(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:X}), decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_return_type(): + xfm = Transform2d() + X = np.random.randn(100,100) + p = xfm.forward(X) + x = xfm.inverse(p) + assert x.dtype in np_dtypes + X = tf.placeholder(tf.float32, [100,100]) + p = xfm.forward(X) + x = xfm.inverse(p) + assert x.dtype in tf_dtypes + xfm = Transform2d() + X = np.random.randn(5,100,100,4) + p = xfm.forward_channels(X) + x = xfm.inverse_channels(p) + assert x.dtype in np_dtypes + xfm = Transform2d() + X = tf.placeholder(tf.float32, [None, 100,100,4]) + p = xfm.forward_channels(X) + x = xfm.inverse_channels(p) + assert x.dtype in tf_dtypes @skip_if_no_tf @@ -196,9 +342,8 @@ def test_results_match(test_input, biort, qshift): f_np = Transform2d_np(biort=biort,qshift=qshift) p_np = f_np.forward(im, include_scale=True) - in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort,qshift=qshift) - p_tf = f_tf.forward(in_p, include_scale=True).eval_fwd(im) + p_tf = f_tf.forward(im, include_scale=True) np.testing.assert_array_almost_equal( p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) @@ -226,13 +371,11 @@ def test_results_match_inverse(test_input,biort,qshift): # Use a zero input and the fwd transform to get the shape of # the pyramid easily - in_ = tf.zeros([1, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort, qshift=qshift) - p_tf = f_tf.forward(in_, nlevels=4, include_scale=True) + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) # Create ops for the inverse transform - pi_tf = f_tf.inverse(p_tf) - X_tf = pi_tf.eval_inv(p_np.lowpass, p_np.highpasses) + X_tf = f_tf.inverse(p_tf) np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) @@ -278,9 +421,9 @@ def test_results_match_endtoend(test_input, biort, qshift): in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(in_p, nlevels=4, include_scale=True) - pi_tf = f_tf.inverse(p_tf) + X = f_tf.inverse(p_tf) with tf.Session() as sess: - X_tf = sess.run(pi_tf.X, feed_dict={in_p: [im]})[0] + X_tf = sess.run(X, feed_dict={in_p: [im]})[0] np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) @@ -307,8 +450,10 @@ def test_forward_channels(data_format): # Transform a set of images with forward_channels f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') start = time.time() - Yl, Yh, Yscale = f_tf.forward_channels( - in_p, nlevels=nlevels, data_format=data_format) + Yl, Yh, Yscale = unpack( + f_tf.forward_channels(in_p, nlevels=nlevels, data_format=data_format, + include_scale=True), 'tf') + Yl, Yh, Yscale = sess.run([Yl, Yh, Yscale], {in_p: ims}) print("That took {:.2f}s".format(time.time() - start)) @@ -322,24 +467,24 @@ def test_forward_channels(data_format): p_tf.scales_ops], {in_p2: ims[:,:,:,i]}) np.testing.assert_array_almost_equal( - Yl[:,:,:,i], Yl1, decimal=4) + Yl[:,:,:,i], Yl1, decimal=PRECISION_DECIMAL) for j in range(nlevels): np.testing.assert_array_almost_equal( - Yh[j][:,:,:,i,:], Yh1[j], decimal=4) + Yh[j][:,:,:,i,:], Yh1[j], decimal=PRECISION_DECIMAL) np.testing.assert_array_almost_equal( - Yscale[j][:,:,:,i], Yscale1[j], decimal=4) + Yscale[j][:,:,:,i], Yscale1[j], decimal=PRECISION_DECIMAL) else: Yl1, Yh1, Yscale1 = sess.run([p_tf.lowpass_op, p_tf.highpasses_ops, p_tf.scales_ops], {in_p2: ims[:,i]}) np.testing.assert_array_almost_equal( - Yl[:,i], Yl1, decimal=4) + Yl[:,i], Yl1, decimal=PRECISION_DECIMAL) for j in range(nlevels): np.testing.assert_array_almost_equal( - Yh[j][:,i], Yh1[j], decimal=4) + Yh[j][:,i], Yh1[j], decimal=PRECISION_DECIMAL) np.testing.assert_array_almost_equal( - Yscale[j][:,i], Yscale1[j], decimal=4) + Yscale[j][:,i], Yscale1[j], decimal=PRECISION_DECIMAL) sess.close() @@ -359,25 +504,26 @@ def test_inverse_channels(data_format): ims = np.random.randn(batch, 100, 100, c) in_p = tf.placeholder(tf.float32, [None, 100, 100, c]) f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') - Yl, Yh, _ = f_tf.forward_channels( - in_p, nlevels=nlevels, data_format=data_format) + Yl, Yh = unpack( + f_tf.forward_channels(in_p, nlevels=nlevels, + data_format=data_format), 'tf') else: ims = np.random.randn(batch, c, 100, 100) in_p = tf.placeholder(tf.float32, [None, c, 100, 100]) f_tf = Transform2d(biort='near_sym_b_bp', qshift='qshift_b_bp') - Yl, Yh, _ = f_tf.forward_channels( - in_p, nlevels=nlevels, data_format=data_format) + Yl, Yh = unpack(f_tf.forward_channels( + in_p, nlevels=nlevels, data_format=data_format), 'tf') # Call the inverse_channels function start = time.time() - X = f_tf.inverse_channels(Yl, Yh, data_format=data_format) + X = f_tf.inverse_channels(Pyramid_tf(Yl, Yh), data_format=data_format) X, Yl, Yh = sess.run([X, Yl, Yh], {in_p: ims}) print("That took {:.2f}s".format(time.time() - start)) # Now do it channel by channel in_p2 = tf.zeros((batch, 100, 100), tf.float32) p_tf = f_tf.forward(in_p2, nlevels=nlevels, include_scale=False) - p_tf = f_tf.inverse(p_tf) + X_t = f_tf.inverse(p_tf) for i in range(c): Yh1 = [] if data_format == "nhwc": @@ -390,12 +536,15 @@ def test_inverse_channels(data_format): Yh1.append(Yh[j][:,i]) # Use the eval_inv function to feed the data into the right variables - X1 = p_tf.eval_inv(Yl1, Yh1, sess) + sess.run(tf.global_variables_initializer()) + X1 = sess.run(X_t, {p_tf.lowpass_op: Yl1, p_tf.highpasses_ops: Yh1}) if data_format == "nhwc": - np.testing.assert_array_almost_equal(X[:,:,:,i], X1, decimal=4) + np.testing.assert_array_almost_equal( + X[:,:,:,i], X1, decimal=PRECISION_DECIMAL) else: - np.testing.assert_array_almost_equal(X[:,i], X1, decimal=4) + np.testing.assert_array_almost_equal( + X[:,i], X1, decimal=PRECISION_DECIMAL) sess.close() diff --git a/tests/util.py b/tests/util.py index c377c8e..ce37aed 100644 --- a/tests/util.py +++ b/tests/util.py @@ -3,7 +3,8 @@ import pytest from dtcwt.opencl.lowlevel import _HAVE_CL as HAVE_CL -from dtcwt.utils import _HAVE_TF as HAVE_TF +# from dtcwt.utils import _HAVE_TF as HAVE_TF +from dtcwt.tf.lowlevel import _HAVE_TF as HAVE_TF from six.moves import xrange From b3a2ffbc00d429776caa5817941a10b4e1da95e2 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Sun, 10 Sep 2017 00:47:43 +0100 Subject: [PATCH 46/52] Moved handling of 3 dimensions to the forward_channels method This now makes the forward method act exactly as the forward method from the numpy backend. Handling 3 or 4d inputs is now handled solely by the forward_channels method. --- dtcwt/tf/transform2d.py | 346 ++++++++++++++++++++++-------------- tests/test_tfTransform2d.py | 148 ++++++++++----- 2 files changed, 312 insertions(+), 182 deletions(-) diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index b28b589..1631aa1 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -180,23 +180,14 @@ def forward(self, X, nlevels=3, include_scale=False): 'Inputs should be a numpy or tf array') X_shape = tuple(X.get_shape().as_list()) - extended = False if len(X_shape) == 2: # Need to make it a batch for tensorflow X = tf.expand_dims(X, axis=0) - extended = True - elif len(X_shape) == 3: - if X_shape[2] == 3 and X_shape[1] != 3: - warnings.warn('It looks like you may have entered an RGB ' + - 'image of shape ' + str(X_shape) + '. The ' + - 'tf backend can handle batches of images, ' + - 'but needs the batch to be the zeroth ' + - 'dimension.') - elif len(X_shape) > 3: + elif len(X_shape) >= 3: raise ValueError( 'The entered variable has too many ' + 'dimensions - ' + str(X_shape) + '. For batches of ' + - 'images with multiple channels (i.e. 4 dimensions), ' + + 'images with multiple channels (i.e. 3 or 4 dimensions), ' + 'please either enter each channel separately, or use ' + 'the forward_channels method.') @@ -207,18 +198,17 @@ def forward(self, X, nlevels=3, include_scale=False): with tf.name_scope(name): Yl, Yh, Yscale = self._forward_ops(X, nlevels) - if extended: - Yl = Yl[0] - Yh = tuple(x[0] for x in Yh) - Yscale = tuple(x[0] for x in Yscale) + Yl = Yl[0] + Yh = tuple(x[0] for x in Yh) + Yscale = tuple(x[0] for x in Yscale) if include_scale: return Pyramid(Yl, Yh, Yscale, numpy) else: return Pyramid(Yl, Yh, None, numpy) - def forward_channels(self, X, nlevels=3, include_scale=False, - data_format="nhwc"): + def forward_channels(self, X, data_format, nlevels=3, + include_scale=False): """ Perform a forward transform on an image with multiple channels. Will perform the DTCWT independently on each channel. @@ -229,13 +219,31 @@ def forward_channels(self, X, nlevels=3, include_scale=False, :param bool include_scale: Whether or not to return the lowpass results at each scale of the transform, or only at the highest scale (as is custom for multiresolution analysis) - :param str data_format: An optional string of the form "nchw" or "nhwc", - specifying the data format of the input. If format is "nchw" (the - default), then data is in the form [batch, channels, h, w]. If the - format is "nhwc", then the data is in the form [batch, h, w, c]. + :param str data_format: An optional string of the form: + "nhw" (or "chw"), "hwn" (or "hwc"), "nchw" or "nhwc". Note that for + these strings, 'n' is used to indicate where the batch dimension is, + 'c' is used to indicate where the image channels are, 'h' is used to + indicate where the row dimension is, and 'c' is used to indicate + where the columns are. If the data_format is:: + + * "nhw" - the input will be interpreted as a batch of 2D images, + with the batch dimension as the first. + * "chw" - will function exactly the same as "nhw" but it offered + to indicate the input is a 2D image with channels. + * "hwn" - the input will be interpreted as a batch of 2D images + with the batch dimension as the last. + * "hwc" - will function exatly the same as "hwc" but is offered + to indicate the input is a 2D image with channels. + * "nchw" - the input is a batch of images with channel dimension + as the second dimension. Batch dimension is first. + * "nhwc" - the input is a batch of images with channel dimension + as the last dimension. Batch dimension is first. :returns: Yl - the lowpass output and the final scale. - :returns: Yh - the highpass outputs. + :returns: Yh - the highpass outputs. Regardless of the data_format of + the input, the Yh output will have 1 dimension more, holding the 6 + orientations of the dtcwt coefficients. This will always be the last + dimension. :returns: Yscale - the lowpass output at intermediate scales. .. codeauthor:: Fergal Cotter , Feb 2017 @@ -244,9 +252,12 @@ def forward_channels(self, X, nlevels=3, include_scale=False, .. codeauthor:: Cian Shaffrey, Cambridge University, Sept 2001 """ data_format = data_format.lower() - if data_format != "nchw" and data_format != "nhwc": - raise ValueError('The data format must be either "ncwh" or ' + - '"nhwc", not {}'.format(data_format)) + formats_3d = ("nhw", "chw", "hwn", "hwc") + formats_4d = ("nchw", "nhwc") + formats = formats_3d + formats_4d + if data_format not in formats: + raise ValueError('The data format must be one of: {}'. + format(formats)) try: dtype = X.dtype @@ -266,72 +277,99 @@ def forward_channels(self, X, nlevels=3, include_scale=False, 'Inputs should be a numpy or tf array.') X_shape = X.get_shape().as_list() - if len(X_shape) != 4: + if not ((len(X_shape) == 3 and data_format in formats_3d) or + (len(X_shape) == 4 and data_format in formats_4d)): raise ValueError( 'The entered variable has incorrect shape - ' + - str(X_shape) + '. It must have 4 dimensions. For 2 or 3 ' + - 'dimensioned input, use the forward method.') - - # Move all of the channels into the batch dimension for the - # input. This may involve transposing, depending on the data - # format - with tf.variable_scope('ch_to_batch'): - s = X.get_shape().as_list()[1:] + str(X_shape) + ' for the specified data_format ' + + data_format + '.') + + # Reshape the inputs to all be 3d inputs of shape (batch, h, w) + if data_format in formats_4d: + # Move all of the channels into the batch dimension for the + # input. This may involve transposing, depending on the data + # format + with tf.variable_scope('ch_to_batch'): + s = X.get_shape().as_list()[1:] + size = '{}x{}'.format(s[0], s[1]) + name = 'dtcwt_fwd_{}'.format(size) + if data_format == 'nhwc': + nch = s[2] + X = tf.transpose(X, perm=[0, 3, 1, 2]) + X = tf.reshape(X, [-1, s[0], s[1]]) + else: + nch = s[0] + X = tf.reshape(X, [-1, s[1], s[2]]) + elif data_format == "hwn" or data_format == "hwc": + s = X.get_shape().as_list()[:2] + size = '{}x{}'.format(s[0], s[1]) + name = 'dtcwt_fwd_{}'.format(size) + with tf.variable_scope('ch_to_start'): + X = tf.transpose(X, perm=[2,0,1]) + else: + s = X.get_shape().as_list()[1:3] size = '{}x{}'.format(s[0], s[1]) name = 'dtcwt_fwd_{}'.format(size) - if data_format == 'nhwc': - nch = s[2] - X = tf.transpose(X, perm=[0, 3, 1, 2]) - X = tf.reshape(X, [-1, s[0], s[1]]) - else: - nch = s[0] - X = tf.reshape(X, [-1, s[1], s[2]]) # Do the dtcwt, now with a 3 dimensional input with tf.variable_scope(name): Yl, Yh, Yscale = self._forward_ops(X, nlevels) - # Put the channels back into their correct positions - with tf.variable_scope('batch_to_ch'): - # Reshape Yl - s = Yl.get_shape().as_list()[1:] - Yl = tf.reshape(Yl, [-1, nch, s[0], s[1]], name='Yl_reshape') - if data_format == 'nhwc': - Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') - - # Reshape Yh - with tf.variable_scope('Yh'): - Yh_new = [None,] * nlevels - for i in range(nlevels): - s = Yh[i].get_shape().as_list()[1:] - Yh_new[i] = tf.reshape( - Yh[i], [-1, nch, s[0], s[1], s[2]], - name='scale{}_reshape'.format(i)) - if data_format == 'nhwc': - Yh_new[i] = tf.transpose( - Yh_new[i], [0, 2, 3, 1, 4], - name='scale{}_ch_to_end'.format(i)) - Yh = tuple(Yh_new) - - # Reshape Yscale - if include_scale: - with tf.variable_scope('Yscale'): - Yscale_new = [None,] * nlevels + # Reshape it all again to match the input + if data_format in formats_4d: + # Put the channels back into their correct positions + with tf.variable_scope('batch_to_ch'): + # Reshape Yl + s = Yl.get_shape().as_list()[1:] + Yl = tf.reshape(Yl, [-1, nch, s[0], s[1]], name='Yl_reshape') + if data_format == 'nhwc': + Yl = tf.transpose(Yl, [0, 2, 3, 1], name='Yl_ch_to_end') + + # Reshape Yh + with tf.variable_scope('Yh'): + Yh_new = [None,] * nlevels for i in range(nlevels): - s = Yscale[i].get_shape().as_list()[1:] - Yscale_new[i] = tf.reshape( - Yscale[i], [-1, nch, s[0], s[1]], + s = Yh[i].get_shape().as_list()[1:] + Yh_new[i] = tf.reshape( + Yh[i], [-1, nch, s[0], s[1], s[2]], name='scale{}_reshape'.format(i)) if data_format == 'nhwc': - Yscale_new[i] = tf.transpose( - Yscale_new[i], [0, 2, 3, 1], + Yh_new[i] = tf.transpose( + Yh_new[i], [0, 2, 3, 1, 4], name='scale{}_ch_to_end'.format(i)) - Yscale = tuple(Yscale_new) - - if include_scale: - return Pyramid(Yl, Yh, Yscale, numpy) - else: - return Pyramid(Yl, Yh, None, numpy) + Yh = tuple(Yh_new) + + # Reshape Yscale + if include_scale: + with tf.variable_scope('Yscale'): + Yscale_new = [None,] * nlevels + for i in range(nlevels): + s = Yscale[i].get_shape().as_list()[1:] + Yscale_new[i] = tf.reshape( + Yscale[i], [-1, nch, s[0], s[1]], + name='scale{}_reshape'.format(i)) + if data_format == 'nhwc': + Yscale_new[i] = tf.transpose( + Yscale_new[i], [0, 2, 3, 1], + name='scale{}_ch_to_end'.format(i)) + Yscale = tuple(Yscale_new) + + elif data_format == "hwn" or data_format == "hwc": + with tf.variable_scope('ch_to_end'): + Yl = tf.transpose(Yl, perm=[1,2,0], name='Yl') + Yh = tuple( + tf.transpose(x, [1, 2, 0, 3], name='Yh{}'.format(i)) + for i,x in enumerate(Yh)) + if include_scale: + Yscale = tuple( + tf.transpose(x, [1, 2, 0], name='Yscale{}'.format(i)) + for i,x in enumerate(Yscale)) + + # Return the pyramid + if include_scale: + return Pyramid(Yl, Yh, Yscale, numpy) + else: + return Pyramid(Yl, Yh, None, numpy) def inverse(self, pyramid, gain_mask=None): """ Perform an inverse transform on an image. @@ -386,17 +424,15 @@ def inverse(self, pyramid, gain_mask=None): 'Unknown pyramid provided to inverse transform') # Need to make sure it has at least 3 dimensions for tensorflow - extended = False Yl_shape = tuple(Yl.get_shape().as_list()) if len(Yl_shape) == 2: Yl = tf.expand_dims(Yl, axis=0) Yh = tuple(tf.expand_dims(x, axis=0) for x in Yh) - extended = True - elif len(Yl_shape) == 4: + elif len(Yl_shape) >= 3: raise ValueError( 'The entered variables have too many ' + 'dimensions - ' + str(Yl_shape) + '. For batches of ' + - 'images with multiple channels (i.e. 4 dimensions), ' + + 'images with multiple channels (i.e. 3 or 4 dimensions), ' + 'please either enter each channel separately, or use ' + 'the inverse_channels method.') @@ -408,9 +444,8 @@ def inverse(self, pyramid, gain_mask=None): with tf.name_scope(name): X = self._inverse_ops(Yl, Yh, gain_mask) - # Return data in a shape the user was expecting - if extended: - X = X[0] + # Chop off the first dimension + X = X[0] if numpy: with tf.Session() as sess: @@ -419,7 +454,7 @@ def inverse(self, pyramid, gain_mask=None): return X - def inverse_channels(self, pyramid, gain_mask=None, data_format="nhwc"): + def inverse_channels(self, pyramid, data_format, gain_mask=None): """ Perform an inverse transform on an image with multiple channels. @@ -428,14 +463,28 @@ def inverse_channels(self, pyramid, gain_mask=None, data_format="nhwc"): :param pyramid: A :py:class:`dtcwt.tf.Pyramid` like class holding the transform domain representation to invert + :param str data_format: An optional string of the form: + "nhw" (or "chw"), "hwn" (or "hwc"), "nchw" or "nhwc". Note that for + these strings, 'n' is used to indicate where the batch dimension is, + 'c' is used to indicate where the image channels are, 'h' is used to + indicate where the row dimension is, and 'c' is used to indicate + where the columns are. If the data_format is:: + + * "nhw" - the input will be interpreted as a batch of 2D images, + with the batch dimension as the first. + * "chw" - will function exactly the same as "nhw" but it offered + to indicate the input is a 2D image with channels. + * "hwn" - the input will be interpreted as a batch of 2D images + with the batch dimension as the last. + * "hwc" - will function exatly the same as "hwc" but is offered + to indicate the input is a 2D image with channels. + * "nchw" - the input is a batch of images with channel dimension + as the second dimension. Batch dimension is first. + * "nhwc" - the input is a batch of images with channel dimension + as the last dimension. Batch dimension is first. + :param gain_mask: Gain to be applied to each subband. Should have shape [6, nlevels]. - :param data_format: An optional string of the form "nchw" or "nhwc", - specifying the data format of the input. If format is "nchw" (the - default), then data are in the form [batch, channels, h, w] for Yl - and [batch, channels, h, w, 6] for Yh. If the format is "nhwc", then - the data are in the form [batch, h, w, c] for Yl and - [batch, h, w, c, 6] for Yh. :returns: A tf.Variable, X, compatible with the reconstruction. @@ -451,13 +500,12 @@ def inverse_channels(self, pyramid, gain_mask=None, data_format="nhwc"): """ # Input checking data_format = data_format.lower() - if data_format != "nchw" and data_format != "nhwc": - raise ValueError('The data format must be either "ncwh" or ' + - '"nhwc", not {}'.format(data_format)) - if data_format == "nhwc": - channel_ax = 3 - else: - channel_ax = 1 + formats_3d = ("nhw", "chw", "hwn", "hwc") + formats_4d = ("nchw", "nhwc") + formats = formats_3d + formats_4d + if data_format not in formats: + raise ValueError('The data format must be one of: {}'. + format(formats)) # A tensorflow object was provided numpy = False @@ -479,53 +527,85 @@ def inverse_channels(self, pyramid, gain_mask=None, data_format="nhwc"): raise ValueError( 'Unknown pyramid provided to inverse transform') - # Check the shape was 4D + # Check the shape was correct Yl_shape = Yl.get_shape().as_list() - if len(Yl_shape) != 4: + if not ((len(Yl_shape) == 3 and data_format in formats_3d) or + (len(Yl_shape) == 4 and data_format in formats_4d)): raise ValueError( - """The entered lowpass variable has incorrect dimensions {}. - for data_format of {}.""".format(Yl_shape, data_format)) + 'The entered variable has incorrect shape - ' + + str(Yl_shape) + ' for the specified data_format ' + + data_format + '.') - # Move all of the channels into the batch dimension for the lowpass - # input. This may involve transposing, depending on the data format - with tf.variable_scope('ch_to_batch'): - s = Yl.get_shape().as_list() - num_channels = s[channel_ax] - nlevels = len(Yh) + # Reshape the inputs to all be 3d inputs of shape (batch, h, w) + if data_format in formats_4d: if data_format == "nhwc": - size = '{}x{}_up_{}'.format(s[1], s[2], nlevels) - Yl_new = tf.transpose(Yl, [0, 3, 1, 2]) - Yl_new = tf.reshape(Yl_new, [-1, s[1], s[2]]) + channel_ax = 3 else: - size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) - Yl_new = tf.reshape(Yl, [-1, s[2], s[3]]) - - # Move all of the channels into the batch dimension for the highpass + channel_ax = 1 + # Move all of the channels into the batch dimension for the lowpass # input. This may involve transposing, depending on the data format - Yh_new = [] - for scale in Yh: - s = scale.get_shape().as_list() - if s[channel_ax] != num_channels: - raise ValueError( - """The number of channels has to be consistent for all - inputs across the channel axis {}. You fed in Yl: {} - and Yh: {}""".format(channel_ax, Yl, Yh)) + with tf.variable_scope('ch_to_batch'): + s = Yl.get_shape().as_list() + num_channels = s[channel_ax] + nlevels = len(Yh) if data_format == "nhwc": - scale = tf.transpose(scale, [0, 3, 1, 2, 4]) - Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) + size = '{}x{}_up_{}'.format(s[1], s[2], nlevels) + Yl = tf.transpose(Yl, [0, 3, 1, 2]) + Yl = tf.reshape(Yl, [-1, s[1], s[2]]) else: - Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) + size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) + Yl = tf.reshape(Yl, [-1, s[2], s[3]]) + + # Move all of the channels into the batch dimension for the highpass + # input. This may involve transposing, depending on the data format + Yh_new = [] + for scale in Yh: + s = scale.get_shape().as_list() + if s[channel_ax] != num_channels: + raise ValueError( + """The number of channels has to be consistent for all + inputs across the channel axis {}. You fed in Yl: {} + and Yh: {}""".format(channel_ax, Yl, Yh)) + if data_format == "nhwc": + scale = tf.transpose(scale, [0, 3, 1, 2, 4]) + Yh_new.append(tf.reshape(scale, [-1, s[1], s[2], s[4]])) + else: + Yh_new.append(tf.reshape(scale, [-1, s[2], s[3], s[4]])) + Yh = Yh_new + + elif data_format == "hwn" or data_format == "hwc": + s = Yl.get_shape().as_list() + num_channels = s[2] + size = '{}x{}'.format(s[0], s[1]) + with tf.variable_scope('ch_to_start'): + Yl = tf.transpose(Yl, perm=[2,0,1], name='Yl') + Yh = tuple( + tf.transpose(x, [2, 0, 1, 3], name='Yh{}'.format(i)) + for i,x in enumerate(Yh)) + + else: + s = Yl.get_shape().as_list() + size = '{}x{}'.format(s[1], s[2]) + num_channels = s[0] + # Do the inverse dtcwt, now with the same shape input name = 'dtcwt_inv_{}_{}channels'.format(size, num_channels) with tf.variable_scope(name): - X = self._inverse_ops(Yl_new, Yh_new, gain_mask) + X = self._inverse_ops(Yl, Yh, gain_mask) - with tf.variable_scope('batch_to_ch'): - s = X.get_shape().as_list() - X = tf.reshape(X, [-1, num_channels, s[1], s[2]]) - if data_format == "nhwc": - X = tf.transpose(X, [0, 2, 3, 1], name='X') + # Reshape the output to match the input shape. + if data_format in formats_4d: + with tf.variable_scope('batch_to_ch'): + s = X.get_shape().as_list() + X = tf.reshape(X, [-1, num_channels, s[1], s[2]]) + if data_format == "nhwc": + X = tf.transpose(X, [0, 2, 3, 1], name='X') + else: + if data_format == "hwn" or data_format == "hwc": + with tf.variable_scope('ch_to_end'): + X = tf.transpose(X, [1, 2, 0], name="X") + # If the user expects numpy back, evaluate the data. if numpy: with tf.Session() as sess: sess.run(tf.global_variables_initializer()) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index ab491d5..c6d5555 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -189,41 +189,76 @@ def test_numpy_in(): @skip_if_no_tf -def test_numpy_in_batch(): - X = np.random.randn(5,100,100) +@pytest.mark.parametrize("data_format", [ + ("nhw"), ("chw"), ("hwn"), ("hwc") +]) +def test_numpy_in_batch(data_format): + if data_format == "nhw" or data_format == "chw": + X = np.random.randn(5,100,100) + else: + X = np.random.randn(100,100,5) + f = Transform2d() - p = f.forward(X, include_scale=True) + p = f.forward_channels(X, data_format=data_format, include_scale=True) f1 = Transform2d_np() for i in range(5): - p1 = f1.forward(X[i], include_scale=True) - np.testing.assert_array_almost_equal( - p.lowpass[i], p1.lowpass, decimal=PRECISION_DECIMAL) - for x,y in zip(p.highpasses, p1.highpasses): + if data_format == "nhw" or data_format == "chw": + p1 = f1.forward(X[i], include_scale=True) np.testing.assert_array_almost_equal( - x[i], y, decimal=PRECISION_DECIMAL) - for x,y in zip(p.scales, p1.scales): + p.lowpass[i], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + else: + p1 = f1.forward(X[:,:,i], include_scale=True) np.testing.assert_array_almost_equal( - x[i], y, decimal=PRECISION_DECIMAL) + p.lowpass[:,:,i], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[:,:,i], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[:,:,i], y, decimal=PRECISION_DECIMAL) @skip_if_no_tf -def test_numpy_batch_ch(): - X = np.random.randn(5,100,100,4) +@pytest.mark.parametrize("data_format", [ + ("nhwc"), ("nchw") +]) +def test_numpy_batch_ch(data_format): + if data_format == "nhwc": + X = np.random.randn(5,100,100,4) + else: + X = np.random.randn(5,4,100,100) + f = Transform2d() - p = f.forward_channels(X, include_scale=True) + p = f.forward_channels(X, data_format=data_format, include_scale=True) f1 = Transform2d_np() for i in range(5): for j in range(4): - p1 = f1.forward(X[i,:,:,j], include_scale=True) - - np.testing.assert_array_almost_equal( - p.lowpass[i,:,:,j], p1.lowpass, decimal=PRECISION_DECIMAL) - for x,y in zip(p.highpasses, p1.highpasses): + if data_format == "nhwc": + p1 = f1.forward(X[i,:,:,j], include_scale=True) np.testing.assert_array_almost_equal( - x[i,:,:,j], y, decimal=PRECISION_DECIMAL) - for x,y in zip(p.scales, p1.scales): + p.lowpass[i,:,:,j], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i,:,:,j], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i,:,:,j], y, decimal=PRECISION_DECIMAL) + else: + p1 = f1.forward(X[i,j], include_scale=True) np.testing.assert_array_almost_equal( - x[i,:,:,j], y, decimal=PRECISION_DECIMAL) + p.lowpass[i,j], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i,j], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i,j], y, decimal=PRECISION_DECIMAL) # Test end to end with numpy inputs @@ -237,26 +272,39 @@ def test_2d_input(): @skip_if_no_tf -def test_3d_input(): +@pytest.mark.parametrize("data_format", [ + ("nhw"), ("hwn") +]) +def test_3d_input(data_format): f = Transform2d() - X = np.random.randn(5,100,100) - p = f.forward(X) - x = f.inverse(p) + if data_format == "nhw": + X = np.random.randn(5,100,100) + else: + X = np.random.randn(100,100,5) + + p = f.forward_channels(X,data_format=data_format) + x = f.inverse_channels(p,data_format=data_format) np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) @skip_if_no_tf -def test_4d_input(): +@pytest.mark.parametrize("data_format", [ + ("nhwc"), ("nchw") +]) +def test_4d_input(data_format): f = Transform2d() - X = np.random.randn(5,100,100,4) - p = f.forward_channels(X) - x = f.inverse_channels(p) + if data_format == "nhwc": + X = np.random.randn(5,100,100,4) + else: + X = np.random.randn(5,4,100,100) + p = f.forward_channels(X,data_format=data_format) + x = f.inverse_channels(p,data_format=data_format) np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) # Test end to end with tf inputs @skip_if_no_tf -def test_2d_input_tf(): +def test_2d_input_ph(): xfm = Transform2d() X = np.random.randn(100,100) X_p = tf.placeholder(tf.float32, [100,100]) @@ -268,8 +316,8 @@ def test_2d_input_tf(): X, sess.run(x, {X_p:X}), decimal=PRECISION_DECIMAL) X_p = tf.placeholder(tf.float32, [None, 100,100]) - p = xfm.forward(X_p) - x = xfm.inverse(p) + p = xfm.forward_channels(X_p,data_format="nhw") + x = xfm.inverse_channels(p,data_format="nhw") with tf.Session() as sess: sess.run(tf.global_variables_initializer()) np.testing.assert_array_almost_equal( @@ -278,12 +326,12 @@ def test_2d_input_tf(): # Test end to end with tf inputs @skip_if_no_tf -def test_3d_input_tf(): +def test_3d_input_ph(): xfm = Transform2d() X = np.random.randn(5,100,100) X_p = tf.placeholder(tf.float32, [None,100,100]) - p = xfm.forward(X_p) - x = xfm.inverse(p) + p = xfm.forward_channels(X_p,data_format="nhw") + x = xfm.inverse_channels(p,data_format="nhw") with tf.Session() as sess: sess.run(tf.global_variables_initializer()) np.testing.assert_array_almost_equal( @@ -291,12 +339,12 @@ def test_3d_input_tf(): @skip_if_no_tf -def test_4d_input_tf(): +def test_4d_input_ph(): xfm = Transform2d() - X = np.random.randn(5,100,100,5) - X_p = tf.placeholder(tf.float32, [None,100,100,5]) - p = xfm.forward_channels(X_p) - x = xfm.inverse_channels(p) + X = np.random.randn(5,100,100,4) + X_p = tf.placeholder(tf.float32, [None,100,100,4]) + p = xfm.forward_channels(X_p,data_format="nhwc") + x = xfm.inverse_channels(p,data_format="nhwc") with tf.Session() as sess: sess.run(tf.global_variables_initializer()) np.testing.assert_array_almost_equal( @@ -316,13 +364,13 @@ def test_return_type(): assert x.dtype in tf_dtypes xfm = Transform2d() X = np.random.randn(5,100,100,4) - p = xfm.forward_channels(X) - x = xfm.inverse_channels(p) + p = xfm.forward_channels(X,data_format="nhwc") + x = xfm.inverse_channels(p,data_format="nhwc") assert x.dtype in np_dtypes xfm = Transform2d() X = tf.placeholder(tf.float32, [None, 100,100,4]) - p = xfm.forward_channels(X) - x = xfm.inverse_channels(p) + p = xfm.forward_channels(X,data_format="nhwc") + x = xfm.inverse_channels(p,data_format="nhwc") assert x.dtype in tf_dtypes @@ -418,12 +466,12 @@ def test_results_match_endtoend(test_input, biort, qshift): p_np = f_np.forward(im, nlevels=4, include_scale=True) X_np = f_np.inverse(p_np) - in_p = tf.placeholder(tf.float32, [None, im.shape[0], im.shape[1]]) + in_p = tf.placeholder(tf.float32, [im.shape[0], im.shape[1]]) f_tf = Transform2d(biort=biort, qshift=qshift) p_tf = f_tf.forward(in_p, nlevels=4, include_scale=True) X = f_tf.inverse(p_tf) with tf.Session() as sess: - X_tf = sess.run(X, feed_dict={in_p: [im]})[0] + X_tf = sess.run(X, feed_dict={in_p: im}) np.testing.assert_array_almost_equal( X_np, X_tf, decimal=PRECISION_DECIMAL) @@ -459,7 +507,8 @@ def test_forward_channels(data_format): # Now do it channel by channel in_p2 = tf.placeholder(tf.float32, [None, 100, 100]) - p_tf = f_tf.forward(in_p2, nlevels=nlevels, include_scale=True) + p_tf = f_tf.forward_channels(in_p2, data_format="nhw", nlevels=nlevels, + include_scale=True) for i in range(c): if data_format == "nhwc": Yl1, Yh1, Yscale1 = sess.run([p_tf.lowpass_op, @@ -522,8 +571,9 @@ def test_inverse_channels(data_format): # Now do it channel by channel in_p2 = tf.zeros((batch, 100, 100), tf.float32) - p_tf = f_tf.forward(in_p2, nlevels=nlevels, include_scale=False) - X_t = f_tf.inverse(p_tf) + p_tf = f_tf.forward_channels(in_p2, nlevels=nlevels, data_format="nhw", + include_scale=False) + X_t = f_tf.inverse_channels(p_tf,data_format="nhw") for i in range(c): Yh1 = [] if data_format == "nhwc": From d7b365f960e9b654654ea18ada9842efe9afbeb3 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 13:32:04 +0100 Subject: [PATCH 47/52] Add support for 1D transform in tensorflow --- dtcwt/__init__.py | 4 +- dtcwt/tf/__init__.py | 2 + dtcwt/tf/common.py | 17 +- dtcwt/tf/transform1d.py | 550 ++++++++++++++++++++++++++++++++++++ tests/test_tfTransform1d.py | 375 ++++++++++++++++++++++++ 5 files changed, 940 insertions(+), 8 deletions(-) create mode 100644 dtcwt/tf/transform1d.py create mode 100644 tests/test_tfTransform1d.py diff --git a/dtcwt/__init__.py b/dtcwt/__init__.py index c1f2674..58b8050 100644 --- a/dtcwt/__init__.py +++ b/dtcwt/__init__.py @@ -40,10 +40,10 @@ 'Pyramid': dtcwt.opencl.Pyramid, }, 'tf': { - 'Transform1d': dtcwt.numpy.Transform1d, + 'Transform1d': dtcwt.tf.Transform1d, 'Transform2d': dtcwt.tf.Transform2d, 'Transform3d': dtcwt.numpy.Transform3d, - 'Pyramid': dtcwt.numpy.Pyramid, + 'Pyramid': dtcwt.tf.Pyramid, }, } diff --git a/dtcwt/tf/__init__.py b/dtcwt/tf/__init__.py index 000480d..0ce195e 100644 --- a/dtcwt/tf/__init__.py +++ b/dtcwt/tf/__init__.py @@ -6,9 +6,11 @@ """ from .common import Pyramid +from .transform1d import Transform1d from .transform2d import Transform2d __all__ = [ 'Pyramid', + 'Transform1d', 'Transform2d', ] diff --git a/dtcwt/tf/common.py b/dtcwt/tf/common.py index ab3319d..409656c 100644 --- a/dtcwt/tf/common.py +++ b/dtcwt/tf/common.py @@ -10,21 +10,26 @@ class Pyramid(object): """A tensorflow representation of a transform domain signal. - Backends are free to implement any class which respects this interface for - storing transform-domain signals, so long as the attributes have the - correct names and are tensorflow tensors (or placeholders). - The inverse transform may accept a backend-specific version of this class - but should always accept any class which corresponds to this interface. + An interface-compatible version of + :py:class:`dtcwt.Pyramid` where the initialiser + arguments are assumed to be :py:class:`tf.Variable` instances. + + The attributes defined in :py:class:`dtcwt.Pyramid` + are implemented via properties. The original tf arrays may be accessed + via the ``..._op(s)`` attributes. .. py:attribute:: lowpass_op + A tensorflow tensor that can be evaluated in a session to return the coarsest scale lowpass signal for the input, X. .. py:attribute:: highpasses_op + A tuple of tensorflow tensors, where each element is the complex subband coefficients for corresponding scales finest to coarsest. - .. py:attribute:: scales + .. py:attribute:: scales_ops + *(optional)* A tuple where each element is a tensorflow tensor containing the lowpass signal for corresponding scales finest to coarsest. This is not required for the inverse and may be *None*. diff --git a/dtcwt/tf/transform1d.py b/dtcwt/tf/transform1d.py new file mode 100644 index 0000000..d9336b5 --- /dev/null +++ b/dtcwt/tf/transform1d.py @@ -0,0 +1,550 @@ +from __future__ import absolute_import + +import numpy as np + +from six.moves import xrange + +from dtcwt.coeffs import biort as _biort, qshift as _qshift +from dtcwt.defaults import DEFAULT_BIORT, DEFAULT_QSHIFT +from dtcwt.numpy.common import Pyramid as Pyramid_np +from dtcwt.utils import asfarray +from dtcwt.tf import Pyramid +from dtcwt.tf.lowlevel import coldfilt, colfilter, colifilt + +try: + import tensorflow as tf + from tensorflow.python.framework import dtypes + tf_dtypes = frozenset( + [dtypes.float32, dtypes.float64, dtypes.int8, dtypes.int16, + dtypes.int32, dtypes.int64, dtypes.uint8, dtypes.qint8, dtypes.qint32, + dtypes.quint8, dtypes.complex64, dtypes.complex128, + dtypes.float32_ref, dtypes.float64_ref, dtypes.int8_ref, + dtypes.int16_ref, dtypes.int32_ref, dtypes.int64_ref, dtypes.uint8_ref, + dtypes.qint8_ref, dtypes.qint32_ref, dtypes.quint8_ref, + dtypes.complex64_ref, dtypes.complex128_ref] + ) +except ImportError: + # The lack of tensorflow will be caught by the low-level routines. + pass + +np_dtypes = frozenset( + [np.dtype('float16'), np.dtype('float32'), np.dtype('float64'), + np.dtype('int8'), np.dtype('int16'), np.dtype('int32'), + np.dtype('int64'), np.dtype('uint8'), np.dtype('uint16'), + np.dtype('uint32'), np.dtype('complex64'), np.dtype('complex128')] +) + + +class Transform1d(object): + """ + An implementation of the 1D DT-CWT in Tensorflow. + + :param biort: Level 1 wavelets to use. See :py:func:`dtcwt.coeffs.biort`. + :param qshift: Level >= 2 wavelets to use. See + :py:func:`dtcwt.coeffs.qshift`. + + .. note:: + + Calling the methods in this class with different inputs will slightly + vary the results. If you call the + :py:meth:`~dtcwt.tf.Transform1d.forward` or + :py:meth:`~dtcwt.tf.Transform1d.forward_channels` methods with a numpy + array, they load this array into a :py:class:`tf.Variable` and create + the graph. Subsequent calls to :py:attr:`dtcwt.tf.Pyramid.lowpass` or + other attributes in the pyramid will create a session and evaluate these + parameters. If the above methods are called with a tensorflow variable + or placeholder, these will be used to create the graph. As such, to + evaluate the results, you will need to look at the + :py:attr:`dtcwt.tf.Pyramid.lowpass_op` attribute (calling the `lowpass` + attribute will try to evaluate the graph with no initialized variables + and likely result in a runtime error). + + The behaviour is similar for the + :py:meth:`~dtcwt.tf.Transform1d.inverse` and + :py:meth:`~dtcwt.tf.Transform1d.inverse_channels` methods, except these + return an array, rather than a Pyramid style class. If a + :py:class:`dtcwt.tf.Pyramid` was created by calling the forward methods + with a numpy array, providing this pyramid to the inverse methods will + return a numpy array. If however a :py:class:`dtcwt.tf.Pyramid` was + created by calling the forward methods with a tensorflow variable, the + result from calling the inverse methods will also be a tensorflow + variable. + """ + def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): + self.biort = biort + self.qshift = qshift + + def forward(self, X, nlevels=3, include_scale=False): + """Perform a *n*-level DTCWT decompostion on a 1D column vector *X* (or on + the columns of a matrix *X*). + + Can provide the forward transform with either an np array (naive usage), + or a tensorflow variable or placeholder (designed usage). To transform + batches of vectors, use the :py:meth:`forward_channels` method. + + :param X: 1D real array or 2D real array whose columns are to be + transformed. + :param nlevels: Number of levels of wavelet decomposition + + :returns: A :py:class:`dtcwt.tf.Pyramid` object representing the + transform result. + + If *biort* or *qshift* are strings, they are used as an argument to the + :py:func:`biort` or :py:func:`qshift` functions. Otherwise, they are + interpreted as tuples of vectors giving filter coefficients. In the + *biort* case, this should be (h0o, g0o, h1o, g1o). In the *qshift* case, + this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). + + .. codeauthor:: Fergal Cotter , Sep 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + # Check if a numpy array was provided + numpy = False + try: + dtype = X.dtype + except AttributeError: + X = asfarray(X) + dtype = X.dtype + + if dtype in np_dtypes: + numpy = True + # Need this because colfilter and friends assumes input is 2d + if len(X.shape) == 1: + X = np.atleast_2d(X).T + X = tf.Variable(X, dtype=tf.float32, trainable=False) + elif dtype in tf_dtypes: + if len(X.get_shape().as_list()) == 1: + X = tf.expand_dims(X, axis=-1) + else: + raise ValueError('I cannot handle the variable you have ' + + 'provided of type ' + str(X.dtype) + '. ' + + 'Inputs should be a numpy or tf array') + + X_shape = tuple(X.get_shape().as_list()) + size = '{}'.format(X_shape[0]) + name = 'dtcwt_fwd_{}'.format(size) + if len(X_shape) == 2: + # Need to make it a batch for tensorflow + X = tf.expand_dims(X, axis=0) + elif len(X_shape) >= 3: + raise ValueError( + 'The entered variable has too many ' + + 'dimensions - ' + str(X_shape) + '.') + + # Do the forward transform + with tf.variable_scope(name): + Yl, Yh, Yscale = self._forward_ops(X, nlevels) + + Yl = Yl[0] + Yh = tuple(x[0] for x in Yh) + Yscale = tuple(x[0] for x in Yscale) + + if include_scale: + return Pyramid(Yl, Yh, Yscale, numpy) + else: + return Pyramid(Yl, Yh, None, numpy) + + def forward_channels(self, X, nlevels=3, include_scale=False): + """Perform a *n*-level DTCWT decompostion on a 3D array *X*. + + Can provide the forward transform with either an np array (naive usage), + or a tensorflow variable or placeholder (designed usage). + + :param X: 3D real array. Batch of matrices whose columns are to be + transformed (i.e. the second dimension). + :param nlevels: Number of levels of wavelet decomposition + + :returns: A :py:class:`dtcwt.tf.Pyramid` object representing the + transform result. + + If *biort* or *qshift* are strings, they are used as an argument to the + :py:func:`biort` or :py:func:`qshift` functions. Otherwise, they are + interpreted as tuples of vectors giving filter coefficients. In the + *biort* case, this should be (h0o, g0o, h1o, g1o). In the *qshift* case, + this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). + + .. codeauthor:: Fergal Cotter , Sep 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + # Check if a numpy array was provided + numpy = False + try: + dtype = X.dtype + except AttributeError: + X = asfarray(X) + dtype = X.dtype + + if dtype in np_dtypes: + numpy = True + if len(X.shape) != 3: + raise ValueError( + 'Incorrect input shape for the forward_channels ' + + 'method ' + str(X.shape) + '. For Inputs of 1 or 2 ' + + 'dimensions, use the forward method.') + # Need this because colfilter and friends assumes input is 2d + X = tf.Variable(X, dtype=tf.float32, trainable=False) + elif dtype in tf_dtypes: + X_shape = X.get_shape().as_list() + if len(X.get_shape().as_list()) != 3: + raise ValueError( + 'Incorrect input shape for the forward_channels ' + + 'method ' + str(X_shape) + '. For Inputs of 1 or 2 ' + + 'dimensions, use the forward method.') + else: + raise ValueError('I cannot handle the variable you have ' + + 'provided of type ' + str(X.dtype) + '. ' + + 'Inputs should be a numpy or tf array') + + X_shape = tuple(X.get_shape().as_list()) + size = '{}'.format(X_shape[1]) + name = 'dtcwt_fwd_{}'.format(size) + + # Do the forward transform + with tf.variable_scope(name): + Yl, Yh, Yscale = self._forward_ops(X, nlevels) + + if include_scale: + return Pyramid(Yl, Yh, Yscale, numpy) + else: + return Pyramid(Yl, Yh, None, numpy) + + def inverse(self, pyramid, gain_mask=None): + """Perform an *n*-level dual-tree complex wavelet (DTCWT) 1D + reconstruction. + + :param pyramid: A :py:class:`dtcwt.Pyramid`-like object containing + the transformed signal. + :param gain_mask: Gain to be applied to each subband. + + :returns: Reconstructed real array. Will be a tf Variable if the Pyramid + was made with tf inputs, otherwise a numpy array. + + + The *l*-th element of *gain_mask* is gain for wavelet subband at level + l. If gain_mask[l] == 0, no computation is performed for band *l*. + Default *gain_mask* is all ones. Note that *l* is 0-indexed. + + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + # A tensorflow object was provided + numpy = False + if isinstance(pyramid, Pyramid): + Yl = pyramid.lowpass_op + Yh = pyramid.highpasses_ops + numpy = pyramid.numpy + + # Check if a numpy pyramid was provided + elif isinstance(pyramid, Pyramid_np) or \ + hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): + numpy = True + Yl, Yh = pyramid.lowpass, pyramid.highpasses + Yl = tf.Variable(Yl, trainable=False, dtype=tf.float32) + Yh = tuple( + tf.Variable(level, trainable=False, dtype=tf.complex64) + for level in Yh) + else: + raise ValueError( + 'Unknown pyramid provided to inverse transform') + + # Need to make sure it has at least 3 dimensions for tensorflow + Yl_shape = tuple(Yl.get_shape().as_list()) + if len(Yl_shape) == 2: + Yl = tf.expand_dims(Yl, axis=0) + Yh = tuple(tf.expand_dims(x, axis=0) for x in Yh) + elif len(Yl_shape) >= 3: + raise ValueError( + 'The entered variables have too many ' + + 'dimensions - ' + str(Yl_shape) + '. For batches of ' + + 'images with multiple channels (i.e. 3 or 4 dimensions), ' + + 'please either enter each channel separately, or use ' + + 'the inverse_channels method.') + + # Do the inverse transform + s = Yl.get_shape().as_list()[1] + nlevels = len(Yh) + size = '{}_up_{}'.format(s, nlevels) + name = 'dtcwt_inv_{}'.format(size) + with tf.variable_scope(name): + X = self._inverse_ops(Yl, Yh, gain_mask) + + # Chop off the first dimension + X = X[0] + + # Return a 1d vector or a column vector + if X.get_shape().as_list()[1] == 1: + X = X[:,0] + + if numpy: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + X = sess.run(X) + + return X + + def inverse_channels(self, pyramid, gain_mask=None): + """Perform an *n*-level dual-tree complex wavelet (DTCWT) 1D + reconstruction on a 3D array of signals. The inverse is done on the + second dimension of these. + + This is designed to work after calling the + :py:meth:`~dtcwt.tf.Transform1d.forward_channels` method. + + :param pyramid: A :py:class:`dtcwt.Pyramid`-like object containing + the transformed signal. The lowpass signal in the pyramid should be + a 3D array to use this method. + :param gain_mask: Gain to be applied to each subband. + + :returns: Reconstructed array. Will be a tf Variable if the Pyramid was + made with tf inputs, otherwise a numpy array. + + The *l*-th element of *gain_mask* is gain for wavelet subband at level + l. If gain_mask[l] == 0, no computation is performed for band *l*. + Default *gain_mask* is all ones. Note that *l* is 0-indexed. + + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + # A tensorflow object was provided + numpy = False + if isinstance(pyramid, Pyramid): + Yl = pyramid.lowpass_op + Yl_shape = Yl.get_shape().as_list() + if len(Yl_shape) != 3: + raise ValueError( + 'Incorrect input shape for the forward_channels ' + + 'method ' + str(Yl_shape) + '. For Inputs of 1 or 2 ' + + 'dimensions, use the forward method.') + Yh = pyramid.highpasses_ops + numpy = pyramid.numpy + + # Check if a numpy pyramid was provided + elif isinstance(pyramid, Pyramid_np) or \ + hasattr(pyramid, 'lowpass') and hasattr(pyramid, 'highpasses'): + numpy = True + Yl, Yh = pyramid.lowpass, pyramid.highpasses + if len(Yl.shape) != 3: + raise ValueError( + 'Incorrect input shape for the forward_channels ' + + 'method ' + str(Yl.shape) + '. For Inputs of 1 or 2 ' + + 'dimensions, use the forward method.') + + Yl = tf.Variable(Yl, trainable=False, dtype=tf.float32) + Yh = tuple( + tf.Variable(level, trainable=False, dtype=tf.complex64) + for level in Yh) + else: + raise ValueError( + 'Unknown pyramid provided to inverse transform') + + # Do the inverse transform + s = Yl.get_shape().as_list()[1] + nlevels = len(Yh) + size = '{}_up_{}'.format(s, nlevels) + name = 'dtcwt_inv_{}'.format(size) + with tf.variable_scope(name): + X = self._inverse_ops(Yl, Yh, gain_mask) + + if numpy: + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + X = sess.run(X) + + return X + + def _forward_ops(self, X, nlevels=3): + """ Perform a *n*-level DTCWT-2D decompostion on a 2D matrix *X*. + + For column inputs, we still need the input shape to be 3D, but with 1 as + the last dimension. + + :param X: 3D real array of size [batch, h, w] + :param nlevels: Number of levels of wavelet decomposition + :param extended: True if a singleton dimension was added at the + beginning of the input. Signal to remove afterwards. + + :returns: A tuple of Yl, Yh, Yscale + """ + biort = self.biort + qshift = self.qshift + + # Try to load coefficients if biort is a string parameter + try: + h0o, g0o, h1o, g1o = _biort(biort) + except TypeError: + h0o, g0o, h1o, g1o = biort + + # Try to load coefficients if qshift is a string parameter + try: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift) + except TypeError: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = qshift + + # Check the shape and form of the input + if X.dtype not in tf_dtypes: + raise ValueError('X needs to be a tf variable or placeholder') + + original_size = X.get_shape().as_list()[1:] + + # ############################ Resize ################################# + # The next few lines of code check to see if the image is odd in size, + # if so an extra ... row/column will be added to the bottom/right of the + # image + # initial_row_extend = 0 + # initial_col_extend = 0 + # If the row count of X is not divisible by 2 then we need to + # extend X by adding a row at the bottom + if original_size[0] % 2 != 0: + # X = tf.pad(X, [[0, 0], [0, 1], [0, 0]], 'SYMMETRIC') + raise ValueError('Size of input X must be a multiple of 2') + + # extended_size = X.get_shape().as_list()[1:] + + if nlevels == 0: + return X, (), () + + # ########################### Initialise ############################### + Yh = [None, ] * nlevels + # This is only required if the user specifies a third output + # component. + Yscale = [None, ] * nlevels + + # ############################ Level 1 ################################# + # Uses the biorthogonal filters + if nlevels >= 1: + # Do odd top-level filters on cols. + Hi = colfilter(X, h1o) + Lo = colfilter(X, h0o) + + # Convert Hi to complex form by taking alternate rows + Yh[0] = tf.cast(Hi[:,::2,:], tf.complex64) + \ + 1j*tf.cast(Hi[:,1::2,:], tf.complex64) + Yscale[0] = Lo + + # ############################ Level 2+ ################################ + # Uses the qshift filters + for level in xrange(1, nlevels): + # If the row count of Lo is not divisible by 4 (it will be + # divisible by 2), add 2 extra rows to make it so + if Lo.get_shape().as_list()[1] % 4 != 0: + Lo = tf.pad(Lo, [[0, 0], [1, 1], [0, 0]], 'SYMMETRIC') + + # Do even Qshift filters on cols. + Hi = coldfilt(Lo, h1b, h1a) + Lo = coldfilt(Lo, h0b, h0a) + + # Convert Hi to complex form by taking alternate rows + Yh[level] = tf.cast(Hi[:,::2,:], tf.complex64) + \ + 1j * tf.cast(Hi[:,1::2,:], tf.complex64) + Yscale[level] = Lo + + Yl = Lo + + return Yl, tuple(Yh), tuple(Yscale) + + def _inverse_ops(self, Yl, Yh, gain_mask=None): + """Perform an *n*-level dual-tree complex wavelet (DTCWT) 1D + reconstruction. + + :param Yl: The lowpass output from a forward transform. Should be a + tensorflow variable. + :param Yh: The tuple of highpass outputs from a forward transform. + Should be tensorflow variables. + :param gain_mask: Gain to be applied to each subband. + + :returns: A tf.Variable holding the output + + The *l*-th element of *gain_mask* is gain for wavelet subband at level + l. If gain_mask[l] == 0, no computation is performed for band *l*. + Default *gain_mask* is all ones. Note that *l* is 0-indexed. + + .. codeauthor:: Fergal Cotter , Sep 2017 + .. codeauthor:: Rich Wareham , Aug 2013 + .. codeauthor:: Nick Kingsbury, Cambridge University, May 2002 + .. codeauthor:: Cian Shaffrey, Cambridge University, May 2002 + + """ + # Which wavelets are to be used? + biort = self.biort + qshift = self.qshift + a = len(Yh) # No of levels. + + if gain_mask is None: + gain_mask = np.ones(a) # Default gain_mask. + gain_mask = np.array(gain_mask) + + # Try to load coefficients if biort is a string parameter + try: + h0o, g0o, h1o, g1o = _biort(biort) + except TypeError: + h0o, g0o, h1o, g1o = biort + + # Try to load coefficients if qshift is a string parameter + try: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift) + except TypeError: + h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = qshift + + level = a-1 # No of levels = no of rows in L. + if level < 0: + # if there are no levels in the input, just return the Yl value + return Yl + + # Reconstruct levels 2 and above in reverse order. + Lo = Yl + while level >= 1: + Hi = c2q1d(Yh[level]*gain_mask[level]) + Lo = colifilt(Lo, g0b, g0a) + colifilt(Hi, g1b, g1a) + + # If Lo is not the same length as the next Therefore we have to clip + # Lo so it is the same height as the next Yh. Yh => t1 was extended. + Lo_shape = Lo.get_shape().as_list() + next_shape = Yh[level-1].get_shape().as_list() + if Lo_shape[1] != 2 * next_shape[1]: + Lo = Lo[:,1:-1] + Lo_shape = Lo.get_shape().as_list() + + # Check the row shapes across the entire matrix + if (np.any(np.asanyarray(Lo_shape[1:]) != + np.asanyarray(next_shape[1:] * np.array((2,1))))): + raise ValueError('Yh sizes are not valid for DTWAVEIFM') + + level -= 1 + + # Reconstruct level 1. + if level == 0: + Hi = c2q1d(Yh[level]*gain_mask[level]) + Z = colfilter(Lo,g0o) + colfilter(Hi,g1o) + + return Z + + +# ============================================================================= +# ********** INTERNAL FUNCTION ********** +# ============================================================================= +def c2q1d(x): + """ An internal function to convert a 1D Complex vector back to a real + array, which is twice the height of x. + """ + # Input has shape [batch, r, c, 2] + r, c = x.get_shape().as_list()[1:3] + x1 = tf.real(x) + x2 = tf.imag(x) + # Stack 2 inputs of shape [batch, r, c] to [batch, r, 2, c] + y = tf.stack([x1, x2], axis=-2) + # Reshaping interleaves the results + y = tf.reshape(y, [-1, 2 * r, c]) + + return y + +# vim:sw=4:sts=4:et diff --git a/tests/test_tfTransform1d.py b/tests/test_tfTransform1d.py new file mode 100644 index 0000000..a3a129c --- /dev/null +++ b/tests/test_tfTransform1d.py @@ -0,0 +1,375 @@ +import os +import pytest + +from pytest import raises + +import numpy as np +from importlib import import_module +from dtcwt.numpy import Transform1d as Transform1d_np +from dtcwt.coeffs import biort, qshift +import tests.datasets as datasets +from scipy import stats +from .util import skip_if_no_tf +from dtcwt.compat import dtwavexfm, dtwaveifm +import dtcwt + +PRECISION_DECIMAL = 5 +TOLERANCE = 1e-6 + + +@skip_if_no_tf +def setup(): + global mandrill, in_p, pyramid_ops + global tf, Transform1d, dtwavexfm2, dtwaveifm2, Pyramid_tf + global np_dtypes, tf_dtypes + # Import the tensorflow modules + tf = import_module('tensorflow') + dtcwt_tf = import_module('dtcwt.tf') + dtcwt_tf_xfm1 = import_module('dtcwt.tf.transform1d') + Transform1d = getattr(dtcwt_tf, 'Transform1d') + Pyramid_tf = getattr(dtcwt_tf, 'Pyramid') + np_dtypes = getattr(dtcwt_tf_xfm1, 'np_dtypes') + tf_dtypes = getattr(dtcwt_tf_xfm1, 'tf_dtypes') + + mandrill = datasets.mandrill() + # Make sure we run tests on cpu rather than gpus + os.environ["CUDA_VISIBLE_DEVICES"] = "" + dtcwt.push_backend('tf') + + +@skip_if_no_tf +def test_simple(): + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec, 3) + assert len(Yh) == 3 + + +@skip_if_no_tf +def test_simple_with_no_levels(): + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec, 0) + assert len(Yh) == 0 + + +@skip_if_no_tf +def test_simple_with_scale(): + vec = np.random.rand(630) + Yl, Yh, Yscale = dtwavexfm(vec, 3, include_scale=True) + assert len(Yh) == 3 + assert len(Yscale) == 3 + + +@skip_if_no_tf +def test_simple_with_scale_and_no_levels(): + vec = np.random.rand(630) + Yl, Yh, Yscale = dtwavexfm(vec, 0, include_scale=True) + assert len(Yh) == 0 + assert len(Yscale) == 0 + + +@skip_if_no_tf +def test_perfect_recon(): + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec) + vec_recon = dtwaveifm(Yl, Yh) + assert np.max(np.abs(vec_recon - vec)) < TOLERANCE + + +@skip_if_no_tf +def test_simple_custom_filter(): + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec, 4, biort('legall'), qshift('qshift_06')) + vec_recon = dtwaveifm(Yl, Yh, biort('legall'), qshift('qshift_06')) + assert np.max(np.abs(vec_recon - vec)) < TOLERANCE + + +@skip_if_no_tf +def test_single_level(): + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec, 1) + + +@skip_if_no_tf +def test_non_multiple_of_two(): + vec = np.random.rand(631) + with raises(ValueError): + Yl, Yh = dtwavexfm(vec, 1) + + +@skip_if_no_tf +def test_2d(): + Yl, Yh = dtwavexfm(np.random.rand(10,10)) + + +@skip_if_no_tf +def test_integer_input(): + # Check that an integer input is correctly coerced into a floating point + # array + Yl, Yh = dtwavexfm([1,2,3,4]) + assert np.any(Yl != 0) + + +@skip_if_no_tf +def test_integer_perfect_recon(): + # Check that an integer input is correctly coerced into a floating point + # array and reconstructed + A = np.array([1,2,3,4], dtype=np.int32) + Yl, Yh = dtwavexfm(A) + B = dtwaveifm(Yl, Yh) + assert np.max(np.abs(A-B)) < 1e-12 + + +@skip_if_no_tf +def test_float32_input(): + # Check that an float32 input is correctly output as float32 + Yl, Yh = dtwavexfm(np.array([1,2,3,4]).astype(np.float32)) + assert np.issubsctype(Yl.dtype, np.float32) + assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) + + +@skip_if_no_tf +def test_reconstruct(): + # Reconstruction up to tolerance + vec = np.random.rand(630) + Yl, Yh = dtwavexfm(vec) + vec_recon = dtwaveifm(Yl, Yh) + assert np.all(np.abs(vec_recon - vec) < TOLERANCE) + + +@skip_if_no_tf +def test_reconstruct_2d(): + # Reconstruction up to tolerance + vec = np.random.rand(630, 20) + Yl, Yh = dtwavexfm(vec) + vec_recon = dtwaveifm(Yl, Yh) + assert np.all(np.abs(vec_recon - vec) < TOLERANCE) + + +@skip_if_no_tf +def test_float32_input_inv(): + # Check that an float32 input is correctly output as float32 + Yl, Yh = dtwavexfm(np.array([1, 2, 3, 4]).astype(np.float32)) + assert np.issubsctype(Yl.dtype, np.float32) + assert np.all(list(np.issubsctype(x.dtype, np.complex64) for x in Yh)) + + recon = dtwaveifm(Yl, Yh) + assert np.issubsctype(recon.dtype, np.float32) + + +@skip_if_no_tf +def test_numpy_in(): + X = np.random.randn(100,100) + f = Transform1d() + p = f.forward(X) + f1 = Transform1d_np() + p1 = f1.forward(X) + np.testing.assert_array_almost_equal( + p.lowpass, p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + + X = np.random.randn(100,100) + p = f.forward(X, include_scale=True) + p1 = f1.forward(X, include_scale=True) + np.testing.assert_array_almost_equal( + p.lowpass, p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal(x,y,decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_numpy_in_batch(): + X = np.random.randn(5,100,100) + + f = Transform1d() + p = f.forward_channels(X, include_scale=True) + f1 = Transform1d_np() + for i in range(5): + p1 = f1.forward(X[i], include_scale=True) + np.testing.assert_array_almost_equal( + p.lowpass[i], p1.lowpass, decimal=PRECISION_DECIMAL) + for x,y in zip(p.highpasses, p1.highpasses): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + for x,y in zip(p.scales, p1.scales): + np.testing.assert_array_almost_equal( + x[i], y, decimal=PRECISION_DECIMAL) + + + +# Test end to end with numpy inputs +@skip_if_no_tf +def test_1d_input(): + f = Transform1d() + X = np.random.randn(100,) + p = f.forward(X) + x = f.inverse(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_2d_input(): + f = Transform1d() + X = np.random.randn(100,100) + + p = f.forward(X) + x = f.inverse(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_3d_input(): + f = Transform1d() + X = np.random.randn(5,100,100) + + p = f.forward_channels(X) + x = f.inverse_channels(p) + np.testing.assert_array_almost_equal(X,x,decimal=PRECISION_DECIMAL) + + +# Test end to end with tf inputs +@skip_if_no_tf +def test_2d_input_ph(): + xfm = Transform1d() + X = np.random.randn(100,) + X_p = tf.placeholder(tf.float32, [100,]) + p = xfm.forward(X_p) + x = xfm.inverse(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:X}), decimal=PRECISION_DECIMAL) + + X = np.random.randn(100,1) + X_p = tf.placeholder(tf.float32, [None, 100,1]) + p = xfm.forward_channels(X_p) + x = xfm.inverse_channels(p) + with tf.Session() as sess: + sess.run(tf.global_variables_initializer()) + np.testing.assert_array_almost_equal( + X, sess.run(x, {X_p:[X]})[0], decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +def test_return_type(): + xfm = Transform1d() + X = np.random.randn(100,100) + p = xfm.forward(X) + x = xfm.inverse(p) + assert x.dtype in np_dtypes + X = tf.placeholder(tf.float32, [100,100]) + p = xfm.forward(X) + x = xfm.inverse(p) + assert x.dtype in tf_dtypes + X = np.random.randn(5,100,100) + p = xfm.forward_channels(X) + x = xfm.inverse_channels(p) + assert x.dtype in np_dtypes + X = tf.placeholder(tf.float32, [None, 100,100]) + p = xfm.forward_channels(X) + x = xfm.inverse_channels(p) + assert x.dtype in tf_dtypes + + +@skip_if_no_tf +@pytest.mark.parametrize("test_input,biort,qshift", [ + (datasets.mandrill(),'antonini','qshift_a'), + (datasets.mandrill()[100:400,40:450],'legall','qshift_a'), + (datasets.mandrill(),'near_sym_a','qshift_c'), + (datasets.mandrill()[100:374,30:322],'near_sym_b','qshift_d'), +]) +def test_results_match(test_input, biort, qshift): + """ + Compare forward transform with numpy forward transform for mandrill image + """ + im = test_input + f_np = Transform1d_np(biort=biort,qshift=qshift) + p_np = f_np.forward(im, include_scale=True) + + f_tf = Transform1d(biort=biort,qshift=qshift) + p_tf = f_tf.forward(im, include_scale=True) + + np.testing.assert_array_almost_equal( + p_np.lowpass, p_tf.lowpass, decimal=PRECISION_DECIMAL) + [np.testing.assert_array_almost_equal( + h_np, h_tf, decimal=PRECISION_DECIMAL) for h_np, h_tf in + zip(p_np.highpasses, p_tf.highpasses)] + [np.testing.assert_array_almost_equal( + s_np, s_tf, decimal=PRECISION_DECIMAL) for s_np, s_tf in + zip(p_np.scales, p_tf.scales)] + + +@skip_if_no_tf +@pytest.mark.parametrize("test_input,biort,qshift", [ + (datasets.mandrill(),'antonini','qshift_c'), + (datasets.mandrill()[99:411,44:460],'near_sym_a','qshift_a'), + (datasets.mandrill(),'legall','qshift_c'), + (datasets.mandrill()[100:378,20:322],'near_sym_b','qshift_06'), +]) +def test_results_match_inverse(test_input,biort,qshift): + im = test_input + f_np = Transform1d_np(biort=biort, qshift=qshift) + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np) + + # Use a zero input and the fwd transform to get the shape of + # the pyramid easily + f_tf = Transform1d(biort=biort, qshift=qshift) + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) + + # Create ops for the inverse transform + X_tf = f_tf.inverse(p_tf) + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +@pytest.mark.parametrize("biort,qshift,gain_mask", [ + ('antonini','qshift_c',stats.bernoulli(0.8).rvs(size=(4))), + ('near_sym_a','qshift_a',stats.bernoulli(0.8).rvs(size=(4))), + ('legall','qshift_c',stats.bernoulli(0.8).rvs(size=(4))), + ('near_sym_b','qshift_06',stats.bernoulli(0.8).rvs(size=(4))), +]) +def test_results_match_invmask(biort,qshift,gain_mask): + im = mandrill + + f_np = Transform1d_np(biort=biort, qshift=qshift) + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np, gain_mask) + + f_tf = Transform1d(biort=biort, qshift=qshift) + p_tf = f_tf.forward(im, nlevels=4, include_scale=True) + X_tf = f_tf.inverse(p_tf, gain_mask) + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + + +@skip_if_no_tf +@pytest.mark.parametrize("test_input, biort, qshift", [ + (datasets.mandrill(), 'antonini', 'qshift_06'), + (datasets.mandrill()[99:411, 44:460], 'near_sym_b', 'qshift_a'), + (datasets.mandrill(), 'near_sym_b', 'qshift_c'), + (datasets.mandrill()[100:378, 20:322], 'near_sym_a', 'qshift_a'), +]) +def test_results_match_endtoend(test_input, biort, qshift): + im = test_input + f_np = Transform1d_np(biort=biort, qshift=qshift) + p_np = f_np.forward(im, nlevels=4, include_scale=True) + X_np = f_np.inverse(p_np) + + in_p = tf.placeholder(tf.float32, [im.shape[0], im.shape[1]]) + f_tf = Transform1d(biort=biort, qshift=qshift) + p_tf = f_tf.forward(in_p, nlevels=4, include_scale=True) + X = f_tf.inverse(p_tf) + with tf.Session() as sess: + X_tf = sess.run(X, feed_dict={in_p: im}) + + np.testing.assert_array_almost_equal( + X_np, X_tf, decimal=PRECISION_DECIMAL) + + +# vim:sw=4:sts=4:et From 0fd2549837c78f550c16f3eeb5439c428d167eb1 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 13:32:24 +0100 Subject: [PATCH 48/52] Update docs and test suite --- docs/backends.rst | 29 +++++--- docs/reference.rst | 11 +++ dtcwt/_version.py | 2 +- dtcwt/tf/lowlevel.py | 6 +- dtcwt/tf/transform2d.py | 134 +++++++++++++++--------------------- tests/test_tfTransform2d.py | 5 +- tests/test_tfinputshapes.py | 92 +++++++++---------------- 7 files changed, 124 insertions(+), 155 deletions(-) diff --git a/docs/backends.rst b/docs/backends.rst index 95ae793..42992ef 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -1,11 +1,12 @@ Multiple Backend Support ======================== + The ``dtcwt`` library currently provides three backends for computing the wavelet transform: a `NumPy `_ based implementation, an OpenCL implementation which uses the `PyOpenCL `_ -bindings for Python, and a Tensorflow implementation which uses the `Tensorflow -`_ bindings for Python. +bindings for Python, and a Tensorflow implementation which uses the +`Tensorflow `_ bindings for Python. NumPy ''''' @@ -35,13 +36,20 @@ Tensorflow If you want to take advantage of having a GPU on your machine, some transforms and algorithms have been implemented with a Tensorflow backend. -This backend, if present will provide an identical API to the NumPy backend. -NumPy-based input may be passed in to a tensorflow backend, in which case it +This backend will provide an identical API to the NumPy backend. +I.e. NumPy-based input may be passed to a tensorflow backend in the same manner +as it was passed to the NumPy backend. In which case it will be converted to a tensorflow variable, the transform performed, and then -converted back to a NumPy variable afterwards. +converted back to a NumPy variable afterwards. This conversion between types can +be avoided if a tensorflow variable is passed to the dtcwt Transforms. + +The real speedup gained from using GPUs is obtained by parallel processing. For +this reason, when using the tensorflow backend, the Transforms can accept +batches of images. To do this, see the `forward_channels` and `inverse_channels` +methods. More information is in the :ref:`tensorflowbackend` section. -Tensorflow support depends on the `Tensorflow -`_ python package being installed in the +Tensorflow support depends on the +`Tensorflow `_ python package being installed in the current python environment, as well as the necessary CUDA + CUDNN libraries installed). Attempting to use a Tensorflow backend without the python package available will result in a runtime (but not import-time) exception. Attempting @@ -49,8 +57,9 @@ to use the Tensorflow backend without the CUDA and CUDNN libraries properly installed and linked will result in the Tensorflow backend being used, but operations will be run on the CPU rather than the GPU. -If you do not have a GPU, some speedup was still seen for using Tensorflow with -the CPU vs the plain NumPy backend. +If you do not have a GPU, some speedup can still be seen for using Tensorflow with +the CPU vs the plain NumPy backend, as tensorflow will naturally use multiple +processors. Which backend should I use? ''''''''''''''''''''''''''' @@ -97,6 +106,7 @@ switch to the OpenCL backend .. code-block:: python dtcwt.push_backend('opencl') + xfm = Transform2d() # ... Transform2d, etc now use OpenCL ... and to switch to the Tensorflow backend @@ -104,6 +114,7 @@ and to switch to the Tensorflow backend .. code-block:: python dtcwt.push_backend('tf') + xfm = Transform2d() # ... Transform2d, etc now use Tensorflow ... As is suggested by the name, changing the backend manipulates a stack behind the diff --git a/docs/reference.rst b/docs/reference.rst index feaf3ad..7ef1ebd 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -74,8 +74,19 @@ OpenCL .. automodule:: dtcwt.opencl.lowlevel :members: +.. _tensorflowbackend: + Tensorflow '''''''''' +Currently the Tensorflow backend only supports single precision operations, and +only has functionality for the Transform1d() and Transform2d() classes (i.e. +changing the backend to 'tf' will still use the numpy Transform3d() class). + +To preserve functionality, the Transform1d() and Transform2d() classes have +a `forward` method which behaves identically to the NumPy backend. However, to +get speedups with tensorflow, we want to feed our transform batches of images. +For this reason, the 1-D and 2-D transforms also have `forward_channels` and +`inverse_channels` methods. See the below documentation for how to use these. .. automodule:: dtcwt.tf :members: diff --git a/dtcwt/_version.py b/dtcwt/_version.py index e633b3c..56df51f 100644 --- a/dtcwt/_version.py +++ b/dtcwt/_version.py @@ -1,2 +1,2 @@ # IMPORTANT: before release, remove the 'devN' tag from the release name -__version__ = '0.12.0rc4' +__version__ = '0.12.0' diff --git a/dtcwt/tf/lowlevel.py b/dtcwt/tf/lowlevel.py index ea9d8f6..732b546 100644 --- a/dtcwt/tf/lowlevel.py +++ b/dtcwt/tf/lowlevel.py @@ -216,9 +216,8 @@ def coldfilt(X, ha, hb, no_decimate=False): Both filters should be even length, and h should be approx linear phase with a quarter sample (i.e. an :math:`e^{j \pi/4}`) advance from - its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`). + its mid pt (i.e. :math:`|h(m/2)| > |h(m/2 + 1)|`):: - .. code-block:: text ext top edge bottom edge ext Level 1: ! | ! | ! odd filt on . b b b b a a a a a a a a b b b b @@ -299,9 +298,8 @@ def rowdfilt(X, ha, hb, no_decimate=False): Both filters should be even length, and h should be approx linear phase with a quarter sample advance from its mid pt (i.e. :math:`|h(m/2)| > - |h(m/2 + 1)|`). + |h(m/2 + 1)|`):: - .. code-block:: text ext top edge bottom edge ext Level 1: ! | ! | ! odd filt on . b b b b a a a a a a a a b b b b diff --git a/dtcwt/tf/transform2d.py b/dtcwt/tf/transform2d.py index 1631aa1..418a31d 100644 --- a/dtcwt/tf/transform2d.py +++ b/dtcwt/tf/transform2d.py @@ -2,7 +2,6 @@ import numpy as np import logging -import warnings from six.moves import xrange @@ -38,23 +37,6 @@ ) -def dtwavexfm2(X, nlevels=3, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, - include_scale=False): - t = Transform2d(biort=biort, qshift=qshift) - r = t.forward(X, nlevels=nlevels, include_scale=include_scale) - if include_scale: - return r.lowpass, r.highpasses, r.scales - else: - return r.lowpass, r.highpasses - - -def dtwaveifm2(Yl, Yh, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT, - gain_mask=None): - t = Transform2d(biort=biort, qshift=qshift) - r = t.inverse(Pyramid_np(Yl, Yh), gain_mask=gain_mask) - return r - - class Transform2d(object): """ An implementation of the 2D DT-CWT via Tensorflow. @@ -72,13 +54,30 @@ class Transform2d(object): g1o). In the *qshift* case, this should be (h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b). - Creating an object of this class loads the necessary filters onto the - tensorflow graph. A subsequent call to :py:func:`Transform2d.forward` with - an image (or placeholder) will create a forward transform for an input of - the image's size. You can evaluate the resulting ops several times feeding - different images into the placeholder *assuming* they have the same - resolution. For a different resolution image, call the - :py:func:`Transform2d.forward` function again. + .. note:: + + Calling the methods in this class with different inputs will slightly + vary the results. If you call the + :py:meth:`~dtcwt.tf.Transform2d.forward` or + :py:meth:`~dtcwt.tf.Transform2d.forward_channels` methods with a numpy + array, they load this array into a :py:class:`tf.Variable` and create + the graph. Subsequent calls to :py:attr:`dtcwt.tf.Pyramid.lowpass` or + other attributes in the pyramid will create a session and evaluate these + parameters. If the above methods are called with a tensorflow variable + or placeholder, these will be used to create the graph. As such, to + evaluate the results, you will need to look at the + :py:attr:`dtcwt.tf.Pyramid.lowpass_op` attribute (calling the `lowpass` + attribute will try to evaluate the graph with no initialized variables + and likely result in a runtime error). + + The behaviour is similar for the inverse methods, except these return an + array, rather than a Pyramid style class. If a + :py:class:`dtcwt.tf.Pyramid` was created by calling the forward methods + with a numpy array, providing this pyramid to the inverse methods will + return a numpy array. If however a :py:class:`dtcwt.tf.Pyramid` was + created by calling the forward methods with a tensorflow variable, the + result from calling the inverse methods will also be a tensorflow + variable. .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 @@ -97,43 +96,13 @@ def __init__(self, biort=DEFAULT_BIORT, qshift=DEFAULT_QSHIFT): self.qshift = _qshift(qshift) except TypeError: self.qshift = qshift - # Use our own graph when the user calls forward with numpy arrays - self.np_graph = tf.Graph() - self.forward_graphs = {} - self.inverse_graphs = {} - - def _find_forward_graph(self, shape): - """ See if we can reuse an old graph for the forward transform """ - find_key = '{}x{}'.format(shape[0], shape[1]) - for key, val in self.forward_graphs.items(): - if find_key == key: - return val - return None - - def _add_forward_graph(self, p_ops, shape): - """ Keep record of the pyramid so we can use it later if need be """ - find_key = '{}x{}'.format(shape[0], shape[1]) - self.forward_graphs[find_key] = p_ops - - def _find_inverse_graph(self, Lo_shape, nlevels): - """ See if we can reuse an old graph for the inverse transform """ - find_key = '{}x{}'.format(Lo_shape[0], Lo_shape[1]) - for key, val in self.forward_graphs.items(): - if find_key == key: - return val - return None - - def _add_inverse_graph(self, p_ops, Lo_shape, nlevels): - """ Keep record of the pyramid so we can use it later if need be """ - find_key = '{}x{} up {}'.format(Lo_shape[0], Lo_shape[1], nlevels) - self.inverse_graphs[find_key] = p_ops def forward(self, X, nlevels=3, include_scale=False): - """ - Perform a forward transform on an image. + """ Perform a forward transform on an image. Can provide the forward transform with either an np array (naive - usage), or a tensorflow variable or placeholder (designed usage). + usage), or a tensorflow variable or placeholder (designed usage). To + transform batches of images, use the :py:meth:`forward_channels` method. :param ndarray X: Input image which you wish to transform. Can be a numpy array, tensorflow Variable or tensorflow placeholder. See @@ -144,7 +113,7 @@ def forward(self, X, nlevels=3, include_scale=False): at each scale of the transform, or only at the highest scale (as is custom for multi-resolution analysis) - :returns: A :py:class:`Pyramid` like object + :returns: A :py:class:`dtcwt.tf.Pyramid` object .. note:: @@ -195,7 +164,7 @@ def forward(self, X, nlevels=3, include_scale=False): original_size = X_shape[1:] size = '{}x{}'.format(original_size[0], original_size[1]) name = 'dtcwt_fwd_{}'.format(size) - with tf.name_scope(name): + with tf.variable_scope(name): Yl, Yh, Yscale = self._forward_ops(X, nlevels) Yl = Yl[0] @@ -224,27 +193,22 @@ def forward_channels(self, X, data_format, nlevels=3, these strings, 'n' is used to indicate where the batch dimension is, 'c' is used to indicate where the image channels are, 'h' is used to indicate where the row dimension is, and 'c' is used to indicate - where the columns are. If the data_format is:: + where the columns are. If the data_format is: - * "nhw" - the input will be interpreted as a batch of 2D images, + - "nhw" : the input will be interpreted as a batch of 2D images, with the batch dimension as the first. - * "chw" - will function exactly the same as "nhw" but it offered + - "chw" : will function exactly the same as "nhw" but is offered to indicate the input is a 2D image with channels. - * "hwn" - the input will be interpreted as a batch of 2D images + - "hwn" : the input will be interpreted as a batch of 2D images with the batch dimension as the last. - * "hwc" - will function exatly the same as "hwc" but is offered + - "hwc" : will function exatly the same as "hwc" but is offered to indicate the input is a 2D image with channels. - * "nchw" - the input is a batch of images with channel dimension + - "nchw" : the input is a batch of images with channel dimension as the second dimension. Batch dimension is first. - * "nhwc" - the input is a batch of images with channel dimension + - "nhwc" : the input is a batch of images with channel dimension as the last dimension. Batch dimension is first. - :returns: Yl - the lowpass output and the final scale. - :returns: Yh - the highpass outputs. Regardless of the data_format of - the input, the Yh output will have 1 dimension more, holding the 6 - orientations of the dtcwt coefficients. This will always be the last - dimension. - :returns: Yscale - the lowpass output at intermediate scales. + :returns: A :py:class:`dtcwt.tf.Pyramid` object .. codeauthor:: Fergal Cotter , Feb 2017 .. codeauthor:: Rich Wareham , Aug 2013 @@ -382,8 +346,9 @@ def inverse(self, pyramid, gain_mask=None): :param gain_mask: Gain to be applied to each sub-band. Should have shape (6, nlevels) or be None. - :returns: Either a tf.Variable or a numpy array compatible with the - reconstruction. + :returns: An array , X, compatible with the reconstruction. Will be a tf + Variable if the Pyramid was made with tf inputs, otherwise a numpy + array. .. note:: @@ -441,7 +406,7 @@ def inverse(self, pyramid, gain_mask=None): nlevels = len(Yh) size = '{}x{}_up_{}'.format(s[0], s[1], nlevels) name = 'dtcwt_inv_{}'.format(size) - with tf.name_scope(name): + with tf.variable_scope(name): X = self._inverse_ops(Yl, Yh, gain_mask) # Chop off the first dimension @@ -459,7 +424,12 @@ def inverse_channels(self, pyramid, data_format, gain_mask=None): Perform an inverse transform on an image with multiple channels. Must provide with a tensorflow variable or placeholder (unlike the more - general :py:method:`Transform2d.inverse`). + general :py:meth:`~dtcwt.tf.Transform2d.inverse`). + + This is designed to work after calling the + :py:meth:`~dtcwt.tf.Transform2d.forward_channels` method. You must use + the same data_format for the inverse_channels as the one used for the + forward_channels (unless you have explicitly reshaped the output). :param pyramid: A :py:class:`dtcwt.tf.Pyramid` like class holding the transform domain representation to invert @@ -486,7 +456,10 @@ def inverse_channels(self, pyramid, data_format, gain_mask=None): :param gain_mask: Gain to be applied to each subband. Should have shape [6, nlevels]. - :returns: A tf.Variable, X, compatible with the reconstruction. + :returns: An array , X, compatible with the reconstruction. Will be a tf + Variable if the Pyramid was made with tf inputs, otherwise a numpy + array. + The (*d*, *l*)-th element of *gain_mask* is gain for subband with direction *d* at level *l*. If gain_mask[d,l] == 0, no computation is @@ -556,8 +529,9 @@ def inverse_channels(self, pyramid, data_format, gain_mask=None): size = '{}x{}_up_{}'.format(s[2], s[3], nlevels) Yl = tf.reshape(Yl, [-1, s[2], s[3]]) - # Move all of the channels into the batch dimension for the highpass - # input. This may involve transposing, depending on the data format + # Move all of the channels into the batch dimension for the + # highpass input. This may involve transposing, depending on the + # data format Yh_new = [] for scale in Yh: s = scale.get_shape().as_list() diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index c6d5555..8f741ef 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -9,7 +9,6 @@ from dtcwt.coeffs import biort, qshift from dtcwt.utils import unpack import tests.datasets as datasets -from scipy import stats from .util import skip_if_no_tf import time @@ -18,13 +17,15 @@ @skip_if_no_tf def setup(): + # Import some tf only dependencies global mandrill, in_p, pyramid_ops global tf, Transform2d, dtwavexfm2, dtwaveifm2, Pyramid_tf - global np_dtypes, tf_dtypes + global np_dtypes, tf_dtypes, stats # Import the tensorflow modules tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') + stats = import_module('scipy.stats') Transform2d = getattr(dtcwt_tf, 'Transform2d') Pyramid_tf = getattr(dtcwt_tf, 'Pyramid') dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') diff --git a/tests/test_tfinputshapes.py b/tests/test_tfinputshapes.py index b78b525..54e11b0 100644 --- a/tests/test_tfinputshapes.py +++ b/tests/test_tfinputshapes.py @@ -4,18 +4,18 @@ from importlib import import_module from .util import skip_if_no_tf +from dtcwt.utils import unpack +import dtcwt +import dtcwt.compat + PRECISION_DECIMAL = 5 @skip_if_no_tf -def test_setup(): - global tf, Transform2d, dtwavexfm2, dtwaveifm2 +def setup(): + global tf tf = import_module('tensorflow') - dtcwt_tf = import_module('dtcwt.tf') - dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') - Transform2d = getattr(dtcwt_tf, 'Transform2d') - dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') - dtwaveifm2 = getattr(dtcwt_tf_xfm2, 'dtwaveifm2') + dtcwt.push_backend('tf') # Make sure we run tests on cpu rather than gpus os.environ["CUDA_VISIBLE_DEVICES"] = "" @@ -28,60 +28,27 @@ def test_setup(): (4,False), (3,True) ]) -def test_2d_input(nlevels, include_scale): +def test_scales(nlevels, include_scale): in_ = tf.placeholder(tf.float32, [512, 512]) - t = Transform2d() - # Calling forward with a 2d input will throw a warning + t = dtcwt.Transform2d() + p = t.forward(in_, nlevels, include_scale) # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level extent = 512 * 2**(-(nlevels-1)) - assert p.lowpass_op.get_shape().as_list() == [1, extent, extent] + assert p.lowpass_op.get_shape().as_list() == [extent, extent] assert p.lowpass_op.dtype == tf.float32 for i in range(nlevels): extent = 512 * 2**(-(i+1)) assert (p.highpasses_ops[i].get_shape().as_list() == - [1, extent, extent, 6]) + [extent, extent, 6]) assert (p.highpasses_ops[i].dtype == tf.complex64) if include_scale: assert (p.scales_ops[i].get_shape().as_list() == - [1, 2*extent, 2*extent]) - assert p.scales_ops[i].dtype == tf.float32 - - -@skip_if_no_tf -@pytest.mark.parametrize("nlevels, include_scale", [ - (2,False), - (2,True), - (4,False), - (3,True) -]) -def test_apply_reshaping(nlevels, include_scale): - # Test the reshaping function of the Pyramid_tf class. This should apply - # the same tf op to all of its operations. A good example would be to - # remove the batch dimension from each op. - in_ = tf.placeholder(tf.float32, [512, 512]) - t = Transform2d() - # Calling forward with a 2d input will throw a warning - p = t.forward(in_, nlevels, include_scale) - f = lambda x: tf.squeeze(x, squeeze_dims=0) - p.apply_reshaping(f) - - # At level 1, the lowpass output will be the same size as the input. At - # levels above that, it will be half the size per level - extent = 512 * 2**(-(nlevels-1)) - assert p.lowpass_op.get_shape().as_list() == [extent, extent] - assert p.lowpass_op.dtype == tf.float32 - - for i in range(nlevels): - extent = 512 * 2**(-(i+1)) - assert p.highpasses_ops[i].get_shape().as_list() == [extent, extent, 6] - assert p.highpasses_ops[i].dtype == tf.complex64 - if include_scale: - assert p.scales_ops[i].get_shape().as_list() == [2*extent, 2*extent] + [2*extent, 2*extent]) assert p.scales_ops[i].dtype == tf.float32 @@ -94,22 +61,24 @@ def test_apply_reshaping(nlevels, include_scale): ]) def test_2d_input_tuple(nlevels, include_scale): in_ = tf.placeholder(tf.float32, [512, 512]) - t = Transform2d() - # Calling forward with a 2d input will throw a warning - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + t = dtcwt.Transform2d() + if include_scale: + Yl, Yh, Yscale = unpack(t.forward(in_, nlevels, include_scale), 'tf') + else: + Yl, Yh = unpack(t.forward(in_, nlevels, include_scale), 'tf') # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level extent = 512 * 2**(-(nlevels-1)) - assert Yl.get_shape().as_list() == [1, extent, extent] + assert Yl.get_shape().as_list() == [extent, extent] assert Yl.dtype == tf.float32 for i in range(nlevels): extent = 512 * 2**(-(i+1)) - assert Yh[i].get_shape().as_list() == [1, extent, extent, 6] + assert Yh[i].get_shape().as_list() == [extent, extent, 6] assert Yh[i].dtype == tf.complex64 if include_scale: - assert Yscale[i].get_shape().as_list() == [1, 2*extent, 2*extent] + assert Yscale[i].get_shape().as_list() == [2*extent, 2*extent] assert Yscale[i].dtype == tf.float32 @@ -122,8 +91,8 @@ def test_2d_input_tuple(nlevels, include_scale): ]) def test_batch_input(nlevels, include_scale, batch_size): in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) - t = Transform2d() - p = t.forward(in_, nlevels, include_scale) + t = dtcwt.Transform2d() + p = t.forward_channels(in_, "nhw", nlevels, include_scale) # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level @@ -151,9 +120,14 @@ def test_batch_input(nlevels, include_scale, batch_size): ]) def test_batch_input_tuple(nlevels, include_scale, batch_size): in_ = tf.placeholder(tf.float32, [batch_size, 512, 512]) - t = Transform2d() + t = dtcwt.Transform2d() - Yl, Yh, Yscale = t.forward(in_, nlevels, include_scale, return_tuple=True) + if include_scale: + Yl, Yh, Yscale = unpack( + t.forward_channels(in_, "nhw", nlevels, include_scale), "tf") + else: + Yl, Yh = unpack( + t.forward_channels(in_, "nhw", nlevels, include_scale), "tf") # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level @@ -180,9 +154,9 @@ def test_batch_input_tuple(nlevels, include_scale, batch_size): ]) def test_multichannel(nlevels, channels): in_ = tf.placeholder(tf.float32, [None, 512, 512, channels]) - t = Transform2d() - Yl, Yh, Yscale = t.forward_channels(in_, nlevels) - + t = dtcwt.Transform2d() + Yl, Yh, Yscale = unpack( + t.forward_channels(in_, "nhwc", nlevels, include_scale=True), "tf") # At level 1, the lowpass output will be the same size as the input. At # levels above that, it will be half the size per level extent = 512 * 2**(-(nlevels-1)) From ce55a1862f7f8b51908bf17a1e5c69fd40f9e80a Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 13:39:06 +0100 Subject: [PATCH 49/52] Moved scipy to global import scope --- tests/test_tfTransform1d.py | 4 ++-- tests/test_tfTransform2d.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_tfTransform1d.py b/tests/test_tfTransform1d.py index a3a129c..7839121 100644 --- a/tests/test_tfTransform1d.py +++ b/tests/test_tfTransform1d.py @@ -8,8 +8,8 @@ from dtcwt.numpy import Transform1d as Transform1d_np from dtcwt.coeffs import biort, qshift import tests.datasets as datasets -from scipy import stats from .util import skip_if_no_tf +from scipy import stats from dtcwt.compat import dtwavexfm, dtwaveifm import dtcwt @@ -21,7 +21,7 @@ def setup(): global mandrill, in_p, pyramid_ops global tf, Transform1d, dtwavexfm2, dtwaveifm2, Pyramid_tf - global np_dtypes, tf_dtypes + global np_dtypes, tf_dtypes, stats # Import the tensorflow modules tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 8f741ef..9d71b78 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -8,6 +8,7 @@ from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.coeffs import biort, qshift from dtcwt.utils import unpack +from scipy import stats import tests.datasets as datasets from .util import skip_if_no_tf import time @@ -20,12 +21,11 @@ def setup(): # Import some tf only dependencies global mandrill, in_p, pyramid_ops global tf, Transform2d, dtwavexfm2, dtwaveifm2, Pyramid_tf - global np_dtypes, tf_dtypes, stats + global np_dtypes, tf_dtypes # Import the tensorflow modules tf = import_module('tensorflow') dtcwt_tf = import_module('dtcwt.tf') dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') - stats = import_module('scipy.stats') Transform2d = getattr(dtcwt_tf, 'Transform2d') Pyramid_tf = getattr(dtcwt_tf, 'Pyramid') dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') From caabee9676262f210cd5abb033d07ccee32553fb Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 13:59:43 +0100 Subject: [PATCH 50/52] Fixed bug in tf test suite --- tests/test_tfTransform2d.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 9d71b78..3abe5b9 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -5,6 +5,7 @@ import numpy as np from importlib import import_module +import dtcwt from dtcwt.numpy import Transform2d as Transform2d_np from dtcwt.coeffs import biort, qshift from dtcwt.utils import unpack @@ -19,19 +20,16 @@ @skip_if_no_tf def setup(): # Import some tf only dependencies - global mandrill, in_p, pyramid_ops - global tf, Transform2d, dtwavexfm2, dtwaveifm2, Pyramid_tf - global np_dtypes, tf_dtypes + global mandrill, Transform2d, Pyramid + global tf, np_dtypes, tf_dtypes, dtwavexfm2, dtwaveifm2 # Import the tensorflow modules tf = import_module('tensorflow') - dtcwt_tf = import_module('dtcwt.tf') - dtcwt_tf_xfm2 = import_module('dtcwt.tf.transform2d') - Transform2d = getattr(dtcwt_tf, 'Transform2d') - Pyramid_tf = getattr(dtcwt_tf, 'Pyramid') - dtwavexfm2 = getattr(dtcwt_tf_xfm2, 'dtwavexfm2') - dtwaveifm2 = getattr(dtcwt_tf_xfm2, 'dtwaveifm2') - np_dtypes = getattr(dtcwt_tf_xfm2, 'np_dtypes') - tf_dtypes = getattr(dtcwt_tf_xfm2, 'tf_dtypes') + dtcwt.push_backend('tf') + Transform2d = getattr(dtcwt, 'Transform2d') + Pyramid = getattr(dtcwt, 'Pyramid') + compat = import_module('dtcwt.compat') + dtwavexfm2 = getattr(compat, 'dtwavexfm2') + dtwaveifm2 = getattr(compat, 'dtwaveifm2') mandrill = datasets.mandrill() # Make sure we run tests on cpu rather than gpus @@ -566,7 +564,7 @@ def test_inverse_channels(data_format): # Call the inverse_channels function start = time.time() - X = f_tf.inverse_channels(Pyramid_tf(Yl, Yh), data_format=data_format) + X = f_tf.inverse_channels(Pyramid(Yl, Yh), data_format=data_format) X, Yl, Yh = sess.run([X, Yl, Yh], {in_p: ims}) print("That took {:.2f}s".format(time.time() - start)) From 901feee82f6861229aeccc988c1ffdbd792f6929 Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 16:46:45 +0100 Subject: [PATCH 51/52] Add check for return type for tf transform --- tests/test_tfTransform2d.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_tfTransform2d.py b/tests/test_tfTransform2d.py index 3abe5b9..9907e92 100644 --- a/tests/test_tfTransform2d.py +++ b/tests/test_tfTransform2d.py @@ -30,6 +30,9 @@ def setup(): compat = import_module('dtcwt.compat') dtwavexfm2 = getattr(compat, 'dtwavexfm2') dtwaveifm2 = getattr(compat, 'dtwaveifm2') + import dtcwt.tf.transform2d as transform2d + np_dtypes = getattr(transform2d, 'np_dtypes') + tf_dtypes = getattr(transform2d, 'tf_dtypes') mandrill = datasets.mandrill() # Make sure we run tests on cpu rather than gpus From c376809658d7d93ac4ff81af9ea6fd3c4270eb3b Mon Sep 17 00:00:00 2001 From: Fergal Cotter Date: Mon, 11 Sep 2017 20:35:38 +0100 Subject: [PATCH 52/52] Remove README_tf file. Update version to 0.13.0dev1 --- README_tf.md | 76 ----------------------------------------------- dtcwt/_version.py | 2 +- 2 files changed, 1 insertion(+), 77 deletions(-) delete mode 100644 README_tf.md diff --git a/README_tf.md b/README_tf.md deleted file mode 100644 index df981a5..0000000 --- a/README_tf.md +++ /dev/null @@ -1,76 +0,0 @@ -# Using GPU acceleration with tensorflow for the DTCWT - -Normally you would import the dtcwt library and set up a forward transform. -E.g. -```python -import dtcwt -t = dtcwt.Transform2d(biort='near_sym_a',qshift='qshift_b') -p = t.forward(X, nlevels) -low, highs = p.lowpass, p.highpasses -``` -To use the tensorflow acceleration, you must however import the specific -module, and use the slightly modified functions. E.g. -```python -import dtcwt.tf -t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') -p = t.forward(X, nlevels) -low, highs = p.lowpass, p.highpasses -``` -In this instance, X was a numpy array. The library will create all the ops on -the graph, and feed the numpy array into it, create a session and evaluate it. -This provides little advantage over the straightforward numpy operation, but is -there for compatability. - -For real speed-up, you want to feed batches of images into the library. An -example would be: -```python -import dtcwt.tf -t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') -imgs = tf.placeholder(tf.float32, [None, 100,100]) -p = t.forward(imgs, nlevels) -low_op, high_ops = p.lowpass_op, p.highpasses_ops -sess = tf.Session() -low = sess.run(low_op, {imgs:X}) -``` -Having to evaluate each op independently would be quite annoying, so I've made -a helpful routine for it, called eval_fwd -```python -import dtcwt.tf -t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') -imgs = tf.placeholder(tf.float32, [None, 100,100]) -p_tf = t.forward(imgs, nlevels) # returns a dtcwt.Pyramid_tf object -sess = tf.Session() -X = np.random.randn(10,100,100) -p = p_tf.eval_fwd(X) # returns a dtcwt.Pyramid object -lows, highs = p.lowpass, p.highpasses -assert lows.shape[0] == 10 -``` -In this example, the returned pyramid object, p, now has a batch of lowpass and -highpasses. - -For added help, the forward transform can also accept channels of inputs (where -the regular dtcwt only accepts single channel input) through a special module -called forward_channels. At this point, it is likely you will not be wanting to -handle a pyramid, so instead this function returns a tuple of tensors. The tuple will be -formed of: - -(lowpass, (highpass[0], highpass[1], ... highpass[nlevels-1])), - -or if the include_scale option is true, then: - -(lowpass, (highpass[0], highpass[1], ... highpass[nlevels-1])), - (scale[0], scale[1], ... scale[nlevels-1])) - -i.e. -```python -import dtcwt.tf -t = dtcwt.tf.Transform2d(biort='near_sym_a',qshift='qshift_b') -imgs = tf.placeholder(tf.float32, [None, 100,100,3]) -yl,yh,yscale = t.forward_channels(imgs, nlevels,include_scale=True) -sess = tf.Session() -X = np.random.randn(10,100,100,3) -lows = sess.run(yl, {imgs:X}) -``` - - - diff --git a/dtcwt/_version.py b/dtcwt/_version.py index 56df51f..fa470ba 100644 --- a/dtcwt/_version.py +++ b/dtcwt/_version.py @@ -1,2 +1,2 @@ # IMPORTANT: before release, remove the 'devN' tag from the release name -__version__ = '0.12.0' +__version__ = '0.13.0dev1'