class_docstr: This Op serves a dual purpose: it can implement a vanilla 2D convolution
  (as taught in any signal processing class) or implement the
  convolutional layers found in Convolutional Neural Networks.
  
  In this setting, a set of 3D images is convolved with a set of 3D kernels,
  with the particularity that their leading dimensions are of equal length.
  Vanilla 2D convolution is treated as a special case of this.
  
  The input parameter represents a mini-batch of multiple images. Its shape is:
      batch size x num. input feature maps x image height x image width
  
  The kernel parameter represents a set of 3D kernels. Its shape is:
      number of filters x num. input images x filter height x filter width
  
  The output of ConvOp is a 4D tensor, generated as follows:
      output[b,k,:,:] = \sum_i input[b,i,:,:] * filter[k,i,:,:] \forall b,k
  where b is the mini-batch index, k the filter index and * is the
  convolution operator.
  
  The constructor initializes a ConvOp with given output_mode (full/valid).
  All other parameters are optional and are only used to generate more
  optimized c code, or to enable graph optimizers to optimally replace the
  ConvOp.
  
  NOTES ON OPTIMIZATION:
  There are two types of optimization. The first is the selection of the
  fastest algo when bsize and nkern are provided with imshp and kshp.
  By default we try to select the fastest version. You can specify it
  with the unroll_batch, unroll_kern, and unroll_patch parameter.
  
  The second type of optimization is hardcoding some dimensions into the
  code when all shape are know.
  This make a significant difference for the 'full' output_mode.
  
  Sometimes, the fastest implementation on x86-64 uses
  {unroll_batch=4, unroll_kern=4, unroll_patch=False}
  with all other shape parameters being provided.
  
  For optimizing other architectures, see:
  Kazushige Goto and Robert A. Van De Geijn, Anatomy of High-Performance
  Matrix Multiplication, (mr x nr). ACM Transactions on Mathematical
  Software, May 2008.
  Figure 12: (mr x nr). For x86 use 2x4, itanium 8x8, etc.
  
  Parameters
  ----------
  output_mode : {'valid', 'full'}
      'valid' gives an output smaller then the image.
      'full' gives an output bigger then the image.
       See 'border_mode' in conv2d's doc.
  
  Optional parameters: (will generate more optimal c code)
  
  imshp : tuple of len 2 or 3: 2 for 2d image, 3 for a stack of 2d images.
      Stacksize, nb image row, nb image col.
  kshp : tuple of len 2
      Nb kernel row, nb kernel col.
  nkern : int
      The number of kernel.
  bsize : int
      The size of the minibatch.
  dx : int
      Patch stride rows.
  dy : int
      Patch stride cols
  
  Params which select the version of code used:
  
  unroll_patch : bool
      Use a version of c_code that unroll the patch loop that don't
      request all shape information to work, but if all shape information
      are present, will use it to hardcode the value in the code for
      faster code.
  unroll_batch : int
      Use a version of c_code that unroll the batch (by unroll_batch)
      and the nkern (by unroll_kern) loop. The size must by a multiple
      of bsize or nkern respectively.
  unroll_kern : int
      Use a version of c_code that unroll the batch
      (by unroll_batch) and the nkern(by unroll_kern) loop. The size
      must by a multiple of bsize or nkern respectively.
  verbose : int
      Passed to GpuConv.
  version: int or str
      Passed to GpuConv, if version='no_fft', fft
      optimization will be desactivated at the op level.
  direction_hint: {'forward', 'bprop weights', 'bprop inputs'}
      Passed to GpuConv, used by graph optimizers to aid algorithm choice.
  
  The 3 following parameters are used internally when we generate
  the gradient when dx!=1 or dy!=1.
  
  imshp_logical
      Default None. None value is equivalent to imshp value.
      When imshp_logical != imshp, it tell we need to insert 0 in
      the image before we do the convolution. For example, when dx==dy==2
      and the image is [[1, 2], [3, 4]], we should make as if the image
      was [[1, 0, 2, 0], [0, 0, 0, 0], [3, 0, 4, 0], [0, 0, 0, 0]].
      Our python code insert the zero, but the c code optimize it.
      imshp_logical != imshp when taking the grad again the weights or
      the image when the output_mode is full and `dx != 1` or `dy != 1`.
  kshp_logical
      Idem but for kshp and used for the grad again the
      weights when the output_mode is valid and `dx != 1` or `dy != 1`.
  kshp_logical_top_aligned
      Used in the same case. Default to True.
      Set to False in the grad again the weight when the
      output_mode is full.
docstr: By default if len(img2d.shape)==3, we TODO
code: def perform(self, node, inp, out):
      """
      By default if len(img2d.shape)==3, we TODO
  
      """
      img2d, filtersflipped = inp
      (z,) = out
      if not imported_scipy_signal:
          raise theano.gof.utils.MethodNotDefined(
              "c_headers",
              type(self),
              self.__class__.__name__,
              "Need the python package for scipy.signal to be installed "
              "for the python implementation. You can use the C"
              " implementation instead.",
          )
  
      # TODO: move these back out to global scope when they no longer
      #       cause an atexit error
      imshp = self.imshp
      if any(x is None for x in imshp):
          imshp = tuple(img2d.shape[1:])
      if imshp != img2d.shape[1:]:
          raise ValueError(
              "The image shape provided at build time "
              "is different from the one passed at run time",
              imshp,
              img2d.shape[1:],
          )
      kshp = self.kshp
      if any(x is None for x in kshp):
          kshp = tuple(filtersflipped.shape[2:])
      if kshp != filtersflipped.shape[2:]:
          raise ValueError(
              "The filter shape provided at build time "
              "is different from the one passed at run time",
              kshp,
              filtersflipped.shape[2:],
          )
      bsize = self.bsize
      if bsize is None:
          bsize = img2d.shape[0]
      elif bsize != img2d.shape[0]:
          raise ValueError(
              "The batch size provided at build time "
              "is different from the one passed at run time",
              bsize,
              img2d.shape[0],
          )
      nkern = self.nkern
      if nkern is None:
          nkern = filtersflipped.shape[0]
      elif nkern != filtersflipped.shape[0]:
          raise ValueError(
              "The number of filters provided at build time "
              "is different from the one passed at run time",
              nkern,
              filtersflipped.shape[0],
          )
  
      imshp_logical = self.imshp_logical
      if imshp_logical[0] is None:
          imshp_logical = (imshp[0],) + imshp_logical[1:]
      if imshp_logical[1] is None:
          imshp_logical = (imshp_logical[0], imshp[1], imshp_logical[2])
      if imshp_logical[2] is None:
          imshp_logical = imshp_logical[:2] + (imshp[2],)
      assert all(x is not None for x in imshp_logical)
  
      kshp_logical = self.kshp_logical
      if kshp_logical[0] is None:
          kshp_logical = (kshp[0], kshp_logical[1])
      if kshp_logical[1] is None:
          kshp_logical = (kshp_logical[0], kshp[1])
      assert all(x is not None for x in kshp_logical)
  
      if all(shp is not None for shp in self.fulloutshp):
          fulloutshp = tuple(self.fulloutshp)
      else:
          fulloutshp = get_conv_output_shape(
              (None,) + imshp_logical,
              (
                  None,
                  None,
              )
              + kshp_logical,
              self.out_mode,
              (1, 1),
          )[2:]
  
      if (
          z[0] is None
          or z[0].shape
          != (
              bsize,
              nkern,
          )
          + fulloutshp
      ):
          z[0] = np.zeros(
              (
                  bsize,
                  nkern,
              )
              + fulloutshp,
              dtype=img2d.dtype,
          )
      zz = z[0]
  
      stacklen = imshp[0]
  
      img2d = img2d.reshape((bsize,) + imshp)
      filtersflipped = filtersflipped.reshape((nkern, stacklen) + kshp)
  
      if self.imshp != self.imshp_logical:
          # assuming that to get from imshp to imshp logical we insert zeros in missing spots
          rstride = int(np.ceil(imshp_logical[1] / float(imshp[1])))
          cstride = int(np.ceil(imshp_logical[2] / float(imshp[2])))
          buf = np.zeros((bsize,) + imshp_logical, dtype=img2d.dtype)
          buf[:, :, ::rstride, ::cstride] = img2d
          img2d = buf
          del buf, rstride, cstride
  
      if kshp != kshp_logical:
          rstride = int(np.ceil(kshp_logical[0] / float(kshp[0])))
          cstride = int(np.ceil(kshp_logical[1] / float(kshp[1])))
          buf = np.zeros(
              (nkern, stacklen) + self.kshp_logical,
              dtype=filtersflipped.dtype,
          )
          if self.kshp_logical_top_aligned:
              roffset = coffset = 0
          else:
              roffset = (
                  kshp_logical[0] - (kshp[0] * rstride) - 1 + rstride
              ) % rstride
              coffset = (
                  kshp_logical[1] - (kshp[1] * cstride) - 1 + cstride
              ) % cstride
              assert roffset >= 0
              assert coffset >= 0
          buf[:, :, roffset::rstride, coffset::cstride] = filtersflipped
          filtersflipped = buf
          del buf, rstride, cstride
  
      val = _valfrommode(self.out_mode)
      bval = _bvalfromboundary("fill")
  
      with warnings.catch_warnings():
          warnings.simplefilter("ignore", np.ComplexWarning)
          for b in xrange(bsize):
              for n in xrange(nkern):
                  zz[b, n, ...].fill(0)
                  for im0 in xrange(stacklen):
                      # some cast generates a warning here
                      zz[b, n, ...] += _convolve2d(
                          img2d[b, im0, ...],
                          filtersflipped[n, im0, ...],
                          1,
                          val,
                          bval,
                          0,
                      )
  
      if False:
          if False and self.out_mode == "full":
              img2d2 = np.zeros(
                  (
                      bsize,
                      stacklen,
                      imshp[1] + 2 * kshp[0] - 2,
                      imshp[2] + 2 * kshp[1] - 2,
                  )
              )
              img2d2[
                  :,
                  :,
                  kshp[0] - 1 : kshp[0] - 1 + imshp[1],
                  kshp[1] - 1 : kshp[1] - 1 + imshp[2],
              ] = img2d
              img2d = img2d2
          # N_image_shape = image_data.shape
  
          for b in xrange(bsize):
              for n in xrange(nkern):
                  zz[b, n, ...].fill(0)
                  for im0 in xrange(stacklen):
                      for row in xrange(0, zz.shape[2], self.dx):
                          for col in xrange(0, zz.shape[3], self.dy):
                              zz[b, n, row, col] += (
                                  img2d[
                                      b,
                                      im0,
                                      row : row + kshp[0],
                                      col : col + kshp[1],
                                  ]
                                  * filtersflipped[n, im0, ::-1, ::-1]
                              ).sum()
  
      # We copy it to remove the Stride mismatch warning from DEBUG_MODE.
      # The copy make that we return an object with the same stride as the c version.
      # The copy don't affect the performance during our experience as in that case we
      # execute the c version which is much faster.
      if self.dx > 1 or self.dy > 1:
          zz = zz[:, :, 0 :: self.dx, 0 :: self.dy].copy()
      z[0] = zz
  
imports: from __future__ import absolute_import, print_function, division
  import logging
  import numpy
  from six.moves import xrange
  import warnings
  import theano
  from theano import OpenMPOp
  from theano.tensor import as_tensor_variable, blas, get_scalar_constant_value, patternbroadcast, NotScalarConstantError
  from theano.gof import Apply
  from theano.tensor.nnet.abstract_conv import get_conv_output_shape, get_conv_shape_1axis
label_desc: Use image node_id 12 for calling the ConvOp obj's underlying member method code with example usage: obj.perform(node, inp, out) without return types