Main algorithm for Boundary Attack ++.

    Return a tensor that constructs adversarial examples for the given
    input. Generate uses tf.py_func in order to operate over tensors.

    :param sample: input image. Without the batchsize dimension.
    :param target_label: integer for targeted attack,
      None for nontargeted attack. Without the batchsize dimension.
    :param target_image: an array with the same size as sample, or None.
      Without the batchsize dimension.


    Output:
    perturbed image.

def _bapp(self, sample, target_label, target_image):
    """
    Main algorithm for Boundary Attack ++.

    Return a tensor that constructs adversarial examples for the given
    input. Generate uses tf.py_func in order to operate over tensors.

    :param sample: input image. Without the batchsize dimension.
    :param target_label: integer for targeted attack,
      None for nontargeted attack. Without the batchsize dimension.
    :param target_image: an array with the same size as sample, or None.
      Without the batchsize dimension.


    Output:
    perturbed image.

    """

    # Original label required for untargeted attack.
    if target_label is None:
      original_label = np.argmax(
          self.sess.run(self.logits, feed_dict={self.input_ph: sample[None]})
          )
    else:
      target_label = np.argmax(target_label)

    def decision_function(images):
      """
      Decision function output 1 on the desired side of the boundary,
      0 otherwise.
      """
      images = clip_image(images, self.clip_min, self.clip_max)
      prob = []
      for i in range(0, len(images), self.batch_size):
        batch = images[i:i+self.batch_size]
        prob_i = self.sess.run(self.logits, feed_dict={self.input_ph: batch})
        prob.append(prob_i)
      prob = np.concatenate(prob, axis=0)
      if target_label is None:
        return np.argmax(prob, axis=1) != original_label
      else:
        return np.argmax(prob, axis=1) == target_label

    # Initialize.
    if target_image is None:
      perturbed = initialize(decision_function, sample, self.shape,
                             self.clip_min, self.clip_max)
    else:
      perturbed = target_image

    # Project the initialization to the boundary.
    perturbed, dist_post_update = binary_search_batch(sample,
                                                      np.expand_dims(perturbed, 0),
                                                      decision_function,
                                                      self.shape,
                                                      self.constraint,
                                                      self.theta)

    dist = compute_distance(perturbed, sample, self.constraint)

    for j in np.arange(self.num_iterations):
      current_iteration = j + 1

      # Choose delta.
      delta = select_delta(dist_post_update, current_iteration,
                           self.clip_max, self.clip_min, self.d,
                           self.theta, self.constraint)

      # Choose number of evaluations.
      num_evals = int(min([self.initial_num_evals * np.sqrt(j+1),
                           self.max_num_evals]))

      # approximate gradient.
      gradf = approximate_gradient(decision_function, perturbed, num_evals,
                                   delta, self.constraint, self.shape,
                                   self.clip_min, self.clip_max)
      if self.constraint == 'linf':
        update = np.sign(gradf)
      else:
        update = gradf

      # search step size.
      if self.stepsize_search == 'geometric_progression':
        # find step size.
        epsilon = geometric_progression_for_stepsize(perturbed,
                                                     update, dist, decision_function, current_iteration)

        # Update the sample.
        perturbed = clip_image(perturbed + epsilon * update,
                               self.clip_min, self.clip_max)

        # Binary search to return to the boundary.
        perturbed, dist_post_update = binary_search_batch(sample,
                                                          perturbed[None],
                                                          decision_function,
                                                          self.shape,
                                                          self.constraint,
                                                          self.theta)

      elif self.stepsize_search == 'grid_search':
        # Grid search for stepsize.
        epsilons = np.logspace(-4, 0, num=20, endpoint=True) * dist
        epsilons_shape = [20] + len(self.shape) * [1]
        perturbeds = perturbed + epsilons.reshape(epsilons_shape) * update
        perturbeds = clip_image(perturbeds, self.clip_min, self.clip_max)
        idx_perturbed = decision_function(perturbeds)

        if np.sum(idx_perturbed) > 0:
          # Select the perturbation that yields the minimum distance # after binary search.
          perturbed, dist_post_update = binary_search_batch(sample,
                                                            perturbeds[idx_perturbed],
                                                            decision_function,
                                                            self.shape,
                                                            self.constraint,
                                                            self.theta)

      # compute new distance.
      dist = compute_distance(perturbed, sample, self.constraint)
      if self.verbose:
        print('iteration: {:d}, {:s} distance {:.4E}'.format(
            j+1, self.constraint, dist))

    perturbed = np.expand_dims(perturbed, 0)
    return perturbed