Tilt-shift miniature faking

3 minute read

Tilt-Shift

\[ \frac{1}{2} \left[ \tanh\left(\frac{x-\ell_1}{d}\right) - \tanh\left(\frac{x-\ell_2}{d}\right) \right] \]

Using sliders with OpenCV

cv2.createTrackbar(slider_name, window_name, 0, slider_max, on_change)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import cv2
import numpy as np
import sys

height_slider_max = 100
decay_slider_max = 100
offset_slider_max = 100

height_slider_name = "Region size (%)"
decay_slider_name = "Decay (%)"
offset_slider_name = "Center height (%)"

filename = sys.argv[1]
original = cv2.imread(filename)
M, N = original.shape[:2]
_, mesh = np.meshgrid(np.arange(N), np.arange(M))

blur = np.ones((5, 5)) / 25
filtered = cv2.filter2D(original, -1, blur)

window_name = "tilt-shift"
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)

def alpha(x, d, l1, l2):
    return 0.5 * (np.tanh((x - l1) / d) - np.tanh((x - l2) / d))

def tiltshift(d, l1, l2):
    mask = alpha(mesh, d, l1, l2)
    mask = np.atleast_3d(mask)
    blended = mask * original + (1 - mask) * filtered
    blended = blended.astype(np.uint8)
    return blended

def on_change(val):
    height = cv2.getTrackbarPos(height_slider_name, window_name)
    decay = cv2.getTrackbarPos(decay_slider_name, window_name)
    offset = cv2.getTrackbarPos(offset_slider_name, window_name)
    d = decay * M / 100
    l1 = (offset - height / 2) * M / 100
    l2 = (offset + height / 2) * M / 100
    blended = tiltshift(d, l1, l2)
    cv2.imshow(window_name, blended)

cv2.createTrackbar(height_slider_name, window_name, 0,
                   height_slider_max, on_change)
cv2.createTrackbar(decay_slider_name, window_name, 0,
                   decay_slider_max, on_change)
cv2.createTrackbar(offset_slider_name, window_name, 0,
                   offset_slider_max, on_change)
on_change(0)
cv2.waitKey(0)

Original image (left) and with grapes in focus (right).

Video analysis

Parsing command-line arguments with argparse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("filename", help="input video file path")
parser.add_argument("-o", "--output", default="output.avi",
                    help="output file path (default: ouput.avi)")
parser.add_argument("-s", "--size", type=float, required=True,
                    help="region size in %% of image height")
parser.add_argument("-d", "--decay", type=float, required=True,
                    help="decay rate in %% of image height")    
parser.add_argument("-c", "--center", type=float, required=True,
                    help="center height in %% of image height")
parser.add_argument("-f", '--framedrop', type=int, default=1,
                    help="frame dropping rate")
args = vars(parser.parse_args())

Reading video files frame-by-frame

1
2
3
4
5
6
7
8
9
10
cap = cv2.VideoCapture(args['filename'])
_, first_frame = cap.read()
M, N = first_frame.shape[:2]
_, mesh = np.meshgrid(np.arange(N), np.arange(M))
d = args['decay'] * M / 100
l1 = (args['center'] - args['size'] / 2) * M / 100
l2 = (args['center'] + args['size'] / 2) * M / 100

def alpha(x, d, l1, l2):
    return 0.5 * (np.tanh((x - l1) / d) - np.tanh((x - l2) / d))
1
2
3
4
5
6
7
8
9
mask = alpha(mesh, d, l1, l2)
mask = np.atleast_3d(mask)
blur = np.ones((5, 5)) / 25

def tiltshift(original):
    filtered = cv2.filter2D(original, -1, blur)
    blended = mask * original + (1 - mask) * filtered
    blended = blended.astype(np.uint8)
    return blended

Writing video files with OpenCV

1
2
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
out = cv2.VideoWriter(args['output'], fourcc, 24., (N, M))
1
2
3
4
5
6
7
8
9
10
framecount = 0
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    framecount = framecount + 1
    if framecount % args['framedrop'] > 0:
        continue
    blended = tiltshift(frame)
    out.write(blended)

End result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import cv2
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("filename", help="input video file path")
parser.add_argument("-o", "--output", default="output.avi",
                    help="output file path (default: ouput.avi)")
parser.add_argument("-s", "--size", type=float, required=True,
                    help="region size in %% of image height")
parser.add_argument("-d", "--decay", type=float, required=True,
                    help="decay rate in %% of image height")    
parser.add_argument("-c", "--center", type=float, required=True,
                    help="center height in %% of image height")
parser.add_argument("-f", '--framedrop', type=int, default=1,
                    help="frame dropping rate")
args = vars(parser.parse_args())

cap = cv2.VideoCapture(args['filename'])
_, first_frame = cap.read()
M, N = first_frame.shape[:2]
_, mesh = np.meshgrid(np.arange(N), np.arange(M))
d = args['decay'] * M / 100
l1 = (args['center'] - args['size'] / 2) * M / 100
l2 = (args['center'] + args['size'] / 2) * M / 100

def alpha(x, d, l1, l2):
    return 0.5 * (np.tanh((x - l1) / d) - np.tanh((x - l2) / d))

mask = alpha(mesh, d, l1, l2)
mask = np.atleast_3d(mask)
blur = np.ones((5, 5)) / 25

def tiltshift(original):
    filtered = cv2.filter2D(original, -1, blur)
    blended = mask * original + (1 - mask) * filtered
    blended = blended.astype(np.uint8)
    return blended

fourcc = cv2.VideoWriter_fourcc(*'MJPG')
out = cv2.VideoWriter(args['output'], fourcc, 24., (N, M))

framecount = 0
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    framecount = framecount + 1
    if framecount % args['framedrop'] > 0:
        continue
    blended = tiltshift(frame)
    out.write(blended)