code: # -*- coding: utf-8 -*-
  
  # author:    Adrian Rosebrock
  # website:   http://www.pyimagesearch.com
  
  # import the necessary packages
  import numpy as np
  import cv2
  import sys
  
  # import any special Python 2.7 packages
  if sys.version_info.major == 2:
      from urllib import urlopen
  
  # import any special Python 3 packages
  elif sys.version_info.major == 3:
      from urllib.request import urlopen
  
  def translate(image, x, y):
      # define the translation matrix and perform the translation
      M = np.float32([[1, 0, x], [0, 1, y]])
      shifted = cv2.warpAffine(image, M, (image.shape[1], image.shape[0]))
  
      # return the translated image
      return shifted
  
  def rotate(image, angle, center=None, scale=1.0):
      # grab the dimensions of the image
      (h, w) = image.shape[:2]
  
      # if the center is None, initialize it as the center of
      # the image
      if center is None:
          center = (w // 2, h // 2)
  
      # perform the rotation
      M = cv2.getRotationMatrix2D(center, angle, scale)
      rotated = cv2.warpAffine(image, M, (w, h))
  
      # return the rotated image
      return rotated
  
  def rotate_bound(image, angle):
      # grab the dimensions of the image and then determine the
      # center
      (h, w) = image.shape[:2]
      (cX, cY) = (w / 2, h / 2)
  
      # grab the rotation matrix (applying the negative of the
      # angle to rotate clockwise), then grab the sine and cosine
      # (i.e., the rotation components of the matrix)
      M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
      cos = np.abs(M[0, 0])
      sin = np.abs(M[0, 1])
  
      # compute the new bounding dimensions of the image
      nW = int((h * sin) + (w * cos))
      nH = int((h * cos) + (w * sin))
  
      # adjust the rotation matrix to take into account translation
      M[0, 2] += (nW / 2) - cX
      M[1, 2] += (nH / 2) - cY
  
      # perform the actual rotation and return the image
      return cv2.warpAffine(image, M, (nW, nH))
  
  def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
      # initialize the dimensions of the image to be resized and
      # grab the image size
      dim = None
      (h, w) = image.shape[:2]
  
      # if both the width and height are None, then return the
      # original image
      if width is None and height is None:
          return image
  
      # check to see if the width is None
      if width is None:
          # calculate the ratio of the height and construct the
          # dimensions
          r = height / float(h)
          dim = (int(w * r), height)
  
      # otherwise, the height is None
      else:
          # calculate the ratio of the width and construct the
          # dimensions
          r = width / float(w)
          dim = (width, int(h * r))
  
      # resize the image
      resized = cv2.resize(image, dim, interpolation=inter)
  
      # return the resized image
      return resized
  
  def skeletonize(image, size, structuring=cv2.MORPH_RECT):
      # determine the area (i.e. total number of pixels in the image),
      # initialize the output skeletonized image, and construct the
      # morphological structuring element
      area = image.shape[0] * image.shape[1]
      skeleton = np.zeros(image.shape, dtype="uint8")
      elem = cv2.getStructuringElement(structuring, size)
  
      # keep looping until the erosions remove all pixels from the
      # image
      while True:
          # erode and dilate the image using the structuring element
          eroded = cv2.erode(image, elem)
          temp = cv2.dilate(eroded, elem)
  
          # subtract the temporary image from the original, eroded
          # image, then take the bitwise 'or' between the skeleton
          # and the temporary image
          temp = cv2.subtract(image, temp)
          skeleton = cv2.bitwise_or(skeleton, temp)
          image = eroded.copy()
  
          # if there are no more 'white' pixels in the image, then
          # break from the loop
          if area == area - cv2.countNonZero(image):
              break
  
      # return the skeletonized image
      return skeleton
  
  def opencv2matplotlib(image):
      # OpenCV represents images in BGR order; however, Matplotlib
      # expects the image in RGB order, so simply convert from BGR
      # to RGB and return
      return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  
  def url_to_image(url, readFlag=cv2.IMREAD_COLOR):
      # download the image, convert it to a NumPy array, and then read
      # it into OpenCV format
      resp = urlopen(url)
      image = np.asarray(bytearray(resp.read()), dtype="uint8")
      image = cv2.imdecode(image, readFlag)
  
      # return the image
      return image
  
  def auto_canny(image, sigma=0.33):
      # compute the median of the single channel pixel intensities
      v = np.median(image)
  
      # apply automatic Canny edge detection using the computed median
      lower = int(max(0, (1.0 - sigma) * v))
      upper = int(min(255, (1.0 + sigma) * v))
      edged = cv2.Canny(image, lower, upper)
  
      # return the edged image
      return edged
  
  def grab_contours(cnts):
      # if the length the contours tuple returned by cv2.findContours
      # is '2' then we are using either OpenCV v2.4, v4-beta, or
      # v4-official
      if len(cnts) == 2:
          cnts = cnts[0]
  
      # if the length of the contours tuple is '3' then we are using
      # either OpenCV v3, v4-pre, or v4-alpha
      elif len(cnts) == 3:
          cnts = cnts[1]
  
      # otherwise OpenCV has changed their cv2.findContours return
      # signature yet again and I have no idea WTH is going on
      else:
          raise Exception(("Contours tuple must have length 2 or 3, "
              "otherwise OpenCV changed their cv2.findContours return "
              "signature yet again. Refer to OpenCV's documentation "
              "in that case"))
  
      # return the actual contours array
      return cnts
  
  def is_cv2(or_better=False):
      # grab the OpenCV major version number
      major = get_opencv_major_version()
  
      # check to see if we are using *at least* OpenCV 2
      if or_better:
          return major >= 2
  
      # otherwise we want to check for *strictly* OpenCV 2
      return major == 2
  
  def is_cv3(or_better=False):
      # grab the OpenCV major version number
      major = get_opencv_major_version()
  
      # check to see if we are using *at least* OpenCV 3
      if or_better:
          return major >= 3
  
      # otherwise we want to check for *strictly* OpenCV 3
      return major == 3
  
  def is_cv4(or_better=False):
      # grab the OpenCV major version number
      major = get_opencv_major_version()
  
      # check to see if we are using *at least* OpenCV 4
      if or_better:
          return major >= 4
  
      # otherwise we want to check for *strictly* OpenCV 4
      return major == 4
  
  def get_opencv_major_version(lib=None):
      # if the supplied library is None, import OpenCV
      if lib is None:
          import cv2 as lib
  
      # return the major version number
      return int(lib.__version__.split(".")[0])
  
  def check_opencv_version(major, lib=None):
      # this function may be removed in a future release as we now
      # use the get_opencv_major_function to obtain the current OpenCV
      # version and then perform the actual version check *within* the
      # respective function
      import warnings
      message = """
          The check_opencv_version function is deprecated and may be
          removed in a future release. Use at your own risk.
      """
      warnings.warn(message, DeprecationWarning, stacklevel=2)
      
      # if the supplied library is None, import OpenCV
      if lib is None:
          import cv2 as lib
          
      # return whether or not the current OpenCV version matches the
      # major version number
      return lib.__version__.startswith(major)
  
  def build_montages(image_list, image_shape, montage_shape):
      """
      ---------------------------------------------------------------------------------------------
      author: Kyle Hounslow
      ---------------------------------------------------------------------------------------------
      Converts a list of single images into a list of 'montage' images of specified rows and columns.
      A new montage image is started once rows and columns of montage image is filled.
      Empty space of incomplete montage images are filled with black pixels
      ---------------------------------------------------------------------------------------------
      :param image_list: python list of input images
      :param image_shape: tuple, size each image will be resized to for display (width, height)
      :param montage_shape: tuple, shape of image montage (width, height)
      :return: list of montage images in numpy array format
      ---------------------------------------------------------------------------------------------
  
      example usage:
  
      # load single image
      img = cv2.imread('lena.jpg')
      # duplicate image 25 times
      num_imgs = 25
      img_list = []
      for i in xrange(num_imgs):
          img_list.append(img)
      # convert image list into a montage of 256x256 images tiled in a 5x5 montage
      montages = make_montages_of_images(img_list, (256, 256), (5, 5))
      # iterate through montages and display
      for montage in montages:
          cv2.imshow('montage image', montage)
          cv2.waitKey(0)
  
      ----------------------------------------------------------------------------------------------
      """
      if len(image_shape) != 2:
          raise Exception('image shape must be list or tuple of length 2 (rows, cols)')
      if len(montage_shape) != 2:
          raise Exception('montage shape must be list or tuple of length 2 (rows, cols)')
      image_montages = []
      # start with black canvas to draw images onto
      montage_image = np.zeros(shape=(image_shape[1] * (montage_shape[1]), image_shape[0] * montage_shape[0], 3),
                            dtype=np.uint8)
      cursor_pos = [0, 0]
      start_new_img = False
      for img in image_list:
          if type(img).__module__ != np.__name__:
              raise Exception('input of type {} is not a valid numpy array'.format(type(img)))
          start_new_img = False
          img = cv2.resize(img, image_shape)
          # draw image to black canvas
          montage_image[cursor_pos[1]:cursor_pos[1] + image_shape[1], cursor_pos[0]:cursor_pos[0] + image_shape[0]] = img
          cursor_pos[0] += image_shape[0]  # increment cursor x position
          if cursor_pos[0] >= montage_shape[0] * image_shape[0]:
              cursor_pos[1] += image_shape[1]  # increment cursor y position
              cursor_pos[0] = 0
              if cursor_pos[1] >= montage_shape[1] * image_shape[1]:
                  cursor_pos = [0, 0]
                  image_montages.append(montage_image)
                  # reset black canvas
                  montage_image = np.zeros(shape=(image_shape[1] * (montage_shape[1]), image_shape[0] * montage_shape[0], 3),
                                        dtype=np.uint8)
                  start_new_img = True
      if start_new_img is False:
          image_montages.append(montage_image)  # add unfinished montage
      return image_montages
  
  
  def adjust_brightness_contrast(image, brightness=0., contrast=0.):
      """
      Adjust the brightness and/or contrast of an image
  
      :param image: OpenCV BGR image
      :param contrast: Float, contrast adjustment with 0 meaning no change
      :param brightness: Float, brightness adjustment with 0 meaning no change
      """
      beta = 0
      # See the OpenCV docs for more info on the `beta` parameter to addWeighted
      # https://docs.opencv.org/3.4.2/d2/de8/group__core__array.html#gafafb2513349db3bcff51f54ee5592a19
      return cv2.addWeighted(image,
                             1 + float(contrast) / 100.,
                             image,
                             beta,
                             float(brightness))
apis: numpy.zeros numpy.float32 numpy.median numpy.abs