Magic Image(3)——Implementation in Python3 with Either OpenCV3 or PIL

It has been 3 years since the last update on Magic Image, https://await.moe/2016/09/magic-image2-mathematical-model/, which talked about the mathematical model of creating the mix image.

And to be honest, the Python implementation actually wrote 4 months ago, but it only support OpenCV then. And today, out of personal interest, I added PIL support. Now it could run with PIL only. (But if it detects the existence of OpenCV, that would be preferred)

Basically, for the OpenCV part, it changed to corresponding Python APIs compared to C++ version, and numpy made it more readable when operating on the image matrix. Speaking of the PIL implementation, just googled and substituted with equivalent operations.

And PIL is slightly slower than OpenCV, but PIL comes with Pythonista 3 on iOS, which means I could run the script on my iPhone/iPad without figuring out how to install OpenCV library into Pythonista. And no need to write native iOS apps to do this magic image trick.

Well, here goes the code. Because 1) talk is cheap, show me the code. 2) there's nothing left to talk about IMHO :p

And this code is on my GitHub, #/hoshizora-py

#!/usr/bin/python3
# -*- coding: utf-8 -*-


from optparse import OptionParser

# support OpenCV / PIL
# OpenCV is first choice
using_cv = False
try:
    import cv2
    import numpy as np
    using_cv = True
except ImportError:
    try:
        from PIL import Image
    except ImportError:
        raise "OpenCV / PIL is required"


def parse_arg():
    """
    Parsing command line arguments
    
    Returns
    -------
    optparse.Values
        Parsed command line options
    """
    parser = OptionParser()
    parser.add_option("-f", "--front", dest="front", type=str, help="front layer")
    parser.add_option("-b", "--back", dest="back", type=str, help="back layer")
    parser.add_option("-o", "--output", dest="output", type=str, help="output image")
    parser.add_option("-i", "--increase", default=0, dest="increase", type=int, help="increase brightness of front layer")
    parser.add_option("-d", "--decrease", default=0, dest="decrease", type=int, help="decrease brightness of back layer")
    (options, args) = parser.parse_args()
    return options


def img_shape(frontlayer, backlayer):
    """
    Retrive Image Shape

    Parameters     ----------     frontlayer : numpy / PIL Image         Image for the front layer     backlayer : numpy / PIL Image         Image for the back layer
    Returns     -------     Tuple         ``f_rows``, ``f_cols``, ``b_rows``, ``b_cols``     """     global using_cv     if using_cv:         # OpenCV         f_rows, f_cols = frontlayer.shape         b_rows, b_cols = backlayer.shape     else:         # PIL         f_cols, f_rows = frontlayer.size         b_cols, b_rows = backlayer.size     return f_rows, f_cols, b_rows, b_cols def resize_layer(layer, rows : int, cols : int):     """     Actual Resize          Parameters     ----------     layer : numpy / PIL Image         Image to be resized     rows : int         Rows after resized     cols : int         Cols after resized          Returns     -------     numpy / PIL Image         Resized image     """     global using_cv     if using_cv:         return cv2.resize(layer, (rows, cols))     else:         return layer.resize((rows, cols)) def resize_layers(frontlayer, backlayer):     """     Coordinate resizing with both frontlayer and backlayer          Parameters     ----------     frontlayer : numpy / PIL Image         Image for the front layer     backlayer : numpy / PIL Image         Image for the back layer          Returns     -------     Tuple         Resized layers     """     f_rows, f_cols, b_rows, b_cols = img_shape(frontlayer, backlayer)     # basic idea     # 1. keep aspect ratio     # 2. fit to large one (both width and height)     # e.g,     #   front:  500x800     #   back:   700x400     #   output: 700x800     if f_cols > b_cols:         if f_rows > b_rows:             if (f_cols / f_rows) > (b_cols / b_rows):                 backlayer = resize_layer(backlayer, int(b_cols * f_rows / b_rows), f_rows)             else:                 backlayer = resize_layer(backlayer, f_cols, int(b_rows * f_cols / b_cols))         else:             backlayer = resize_layer(backlayer, int(b_cols * f_rows / b_rows), f_rows)     else:         if f_rows < b_rows:             if (f_cols / f_rows) > (b_cols / b_rows):                 frontlayer = resize_layer(frontlayer, int(f_cols * b_rows / f_rows), b_rows)             else:                 frontlayer = resize_layer(frontlayer, int(f_rows * b_rows / f_cols), b_cols)         else:             frontlayer = resize_layer(frontlayer, int(f_cols * b_rows / f_rows), b_rows)     return frontlayer, backlayer def load_layers(options):     """     Load images in grey based on command line options          Parameters     ----------     options : optparse.Values         Parsed command line options          Returns     -------     Tuple         ``frontlayer``, ``backlayer``     """     global using_cv     if using_cv:         # read and convert to grey image         frontlayer = cv2.imread(options.front, 0)         backlayer = cv2.imread(options.back, 0)     else:         # read and convert to grey image         frontlayer = Image.open(options.front).convert("L")         backlayer = Image.open(options.back).convert("L")     return frontlayer, backlayer def overlay_center(canvas, image):     """     Overlay original image onto canvas          Parameters     ----------     canvas : numpy / PIL Image         Corresponding canvas for the image     image : numpy / PIL Image         Either frontlayer or backlayer     """     global using_cv     if using_cv:         # get upper left point         canvas_size = canvas.shape[:2]         overlay_rows_start = (canvas_size[0] - image.shape[0]) // 2         overlay_cols_start = (canvas_size[1] - image.shape[1]) // 2         # overlay original image         canvas[overlay_rows_start:overlay_rows_start+image.shape[0], overlay_cols_start:overlay_cols_start+image.shape[1]] = image     else:         # get upper left point         canvas_size = canvas.size         overlay_rows_start = (canvas_size[1] - image.size[1]) // 2         overlay_cols_start = (canvas_size[0] - image.size[0]) // 2         # overlay original image         canvas.paste(image, (overlay_rows_start, overlay_cols_start)) def create_canvas(mix_size):     """     Create canvas          Parameters     ----------     mix_size : tuple         (rows, cols, channels)          Returns     -------     Tuple         White ``frontlayer``, black ``backlayer``     """     global using_cv     if using_cv:         front_canvas = np.ones(mix_size[:2], dtype=np.float) * 255         back_canvas = np.zeros(mix_size[:2], dtype=np.float)     else:         front_canvas = Image.new("L", (mix_size[1], mix_size[0]), 255)         back_canvas = Image.new("L", (mix_size[1], mix_size[0]))     return front_canvas, back_canvas def color_shift(options, front_canvas, back_canvas):     """     Shift color intensity          Parameters     ----------     options : optparse.Values         Parsed command line options     frontlayer : numpy / PIL Image         Image for the front layer     backlayer : numpy / PIL Image         Image for the back layer              Returns     -------     Tuple         Color intensity shifted ``frontlayer``, ``backlayer``     """     global using_cv     if using_cv:         # increase frontlayer         front_canvas += options.increase         front_canvas[front_canvas > 255] = 255         # decrease backlayer         back_canvas -= options.decrease         back_canvas[back_canvas < 0] = 0     else:         # increase frontlayer         front_canvas = front_canvas.point(lambda p: min(p + options.increase, 255))         # decrease backlayer         back_canvas = back_canvas.point(lambda p: max(p - options.decrease, 0))     return front_canvas, back_canvas def compute_alpha(front_canvas, back_canvas):     """     Compute alpha channel of output          Parameters     ----------     front_canvas : numpy / PIL Image         Image for the front layer     back_canvas : numpy / PIL Image         Image for the back layer
    Returns     -------     numpy / list         Alpha channel of output     """     global using_cv     if using_cv:         A = back_canvas + 255 - front_canvas         A[A > 255] = 255         A[A <= 0] = 1e-12         return A     else:         front_canvas_data = front_canvas.getdata()         back_canvas_data = back_canvas.getdata()         tmp = list(map(lambda p: p + 255, back_canvas_data))         A = map(lambda i: max(1e-12, min(tmp[i] - front_canvas_data[i], 255)), range(len(back_canvas_data)))         return list(A) def compute_grey(back_canvas, alpha):     """     Compute grey channel of output          Parameters     ----------     back_canvas : numpy / PIL Image         Image for the back layer     alpha : numpy / list         Alpha channel of output
    Returns     -------     numpy / list         Grey channel of output     """     global using_cv     if using_cv:         G = np.divide(back_canvas * 255, A)         return G     else:         back_canvas_data = back_canvas.getdata()         G = map(lambda i: back_canvas_data[i] * 255 / A[i], range(len(A)))         return list(G) def create_mix_canvas(mix_size, alpha, grey):     """     Create and merge alpha and grey channels into output canvas          Parameters     ----------     mix_size : tuple         (rows, cols, channels)     alpha : numpy / list         Alpha channel of output     grey : numpy / list         Grey channel of output          Returns     -------     numpy / PIL Image         Output image     """     global using_cv     if using_cv:         mix_canvas = np.zeros(mix_size, dtype=np.uint8)         mix_canvas[:, :, 0] = grey         mix_canvas[:, :, 1] = grey         mix_canvas[:, :, 2] = grey         mix_canvas[:, :, 3] = alpha     else:         # convert list data into PIL Image         grey_image = Image.new("L", (mix_size[1], mix_size[0]))         grey_image.putdata(grey)         alpha_image = Image.new("L", (mix_size[1], mix_size[0]))         alpha_image.putdata(alpha)         # merge 4 channels         mix_canvas = Image.merge("RGBA", (grey_image, grey_image, grey_image, alpha_image))     return mix_canvas def save_image(options, mix_canvas):     """     Save image          Parameters     ----------     options : optparse.Values         Parsed command line options     mix_canvas : numpy / PIL Image         Output image     """     global using_cv     if using_cv:         cv2.imwrite(options.output, mix_canvas)     else:         mix_canvas.save(options.output) if __name__ == '__main__':     options = parse_arg()     frontlayer, backlayer = load_layers(options)     frontlayer, backlayer = resize_layers(frontlayer, backlayer)     f_rows, f_cols, b_rows, b_cols = img_shape(frontlayer, backlayer)     mix_size = (max([f_rows, b_rows]), max([f_cols, b_cols]), 4)     front_canvas, back_canvas = create_canvas(mix_size)     overlay_center(front_canvas, frontlayer)     overlay_center(back_canvas, backlayer)     front_canvas, back_canvas = color_shift(options, front_canvas, back_canvas)     A = compute_alpha(front_canvas, back_canvas)     G = compute_grey(back_canvas, A)     mix_canvas = create_mix_canvas(mix_size, A, G)     save_image(options, mix_canvas)

Leave a Reply

Your email address will not be published. Required fields are marked *

one × three =