code: def binary_search_batch(original_image, perturbed_images, decision_function,
                          shape, constraint, theta):
    
  
    # Compute distance between each of perturbed image and original image.
    dists_post_update = np.array([
        compute_distance(
            original_image,
            perturbed_image,
            constraint
        )
        for perturbed_image in perturbed_images])
  
    # Choose upper thresholds in binary searchs based on constraint.
    if constraint == 'linf':
      highs = dists_post_update
      # Stopping criteria.
      thresholds = np.minimum(dists_post_update * theta, theta)
    else:
      highs = np.ones(len(perturbed_images))
      thresholds = theta
  
    lows = np.zeros(len(perturbed_images))
  
    while np.max((highs - lows) / thresholds) > 1:
      # projection to mids.
      mids = (highs + lows) / 2.0
      mid_images = project(original_image, perturbed_images,
                           mids, shape, constraint)
  
      # Update highs and lows based on model decisions.
      decisions = decision_function(mid_images)
      lows = np.where(decisions == 0, mids, lows)
      highs = np.where(decisions == 1, mids, highs)
  
    out_images = project(original_image, perturbed_images,
                         highs, shape, constraint)
  
    # Compute distance of the output image to select the best choice.
    # (only used when stepsize_search is grid_search.)
    dists = np.array([
        compute_distance(
            original_image,
            out_image,
            constraint
        )
        for out_image in out_images])
    idx = np.argmin(dists)
  
    dist = dists_post_update[idx]
    out_image = out_images[idx]
    return out_image, dist
docs: Binary search to approach the boundary.