fast <image,time> linear interpolation

Viewed 88

I'm trying to achieve linear interpolation, where the data points are N images of shape: HxWx3 (stored in buf (NxHxWx3)), and the points to interpolate are specified in another (2D) grid (interp_values).

diagram

Non-vectorizable approach: In principle I have made interp_values a HxW grid with values 0..N-1 indicating for each i,j element from which image (in buf) to read it from, including fractional values meaning interpolation. E.g.: a value of 3.6 means blend 40% (1-0.6) of image 3 with 60% (0.6) of image 4. However with this approach it is quite impossible to vectorize the code, and performance was poor.

One vectorization approach: So I changed interp_values to be a NxHxWx3 grid with values 0..1. Each column :,i,j,c would specify blend coefficients for the N images, where only 1 or 2 elements are non-zero, e.g. for 3.6 we have: [0, 0, 0, 0.6, 0.4, 0, 0, ...]. I can convert interp_values from HxW to NxHxWx3 with:

def expand_interp_values(interp_values):
    r = np.zeros((N,) + interp_values.shape + (3,))
    for i in range(interp_values.shape[0]):
        for j in range(interp_values.shape[1]):
            v = interp_values[i, j]
            a, b, x = math.floor(v), math.ceil(v), math.fmod(v, 1)
            if int(a) == int(b):
                r[a, i, j, :] = 3 * [1]
            else:
                r[a, i, j, :] = 3 * [1 - x]
                r[b, i, j, :] = 3 * [x]
    return r

This representation is more sparse (many zeros) but now interpolation can be computed as element-wise multiplication between buf and interp_values (the multiplication part of the linear interpolation) followed by a sum(..., axis=0) (i.e. the addition part of the linear interpolation):

def linear_interp(data, interp_values):
    return np.sum(data * interp_values, axis=0)

With this approach, there is some performance improvement, however it seems with this approach the CPU will be most of the times busy computing x1*0, x2*0, ... or 0 + 0 + 0... Can this be improved any better?

Additionally, the creation of the expanded interp_values grid is not vectorized, so perhaps performance would be bad if that grid has to be updated continuously.

Complete python+opencv code:

import cv2
import numpy as np
import math

vid = cv2.VideoCapture(0)
vid.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# store last N images into a NxHxWx3 grid (circular buffer):
N = 25
buf = None
interp_values = None

DOWNSAMPLING = 6

def linear_interp(data, interp_values):
    return np.sum(data * interp_values / 256, axis=0)

def expand_interp_values(interp_values):
    r = np.zeros((N,) + interp_values.shape + (3,))
    for i in range(interp_values.shape[0]):
        for j in range(interp_values.shape[1]):
            v = interp_values[i, j]
            a, b, x = math.floor(v), math.ceil(v), math.fmod(v, 1)
            if int(a) == int(b):
                r[a, i, j, :] = 3 * [1]
            else:
                r[a, i, j, :] = 3 * [1 - x]
                r[b, i, j, :] = 3 * [x]
    return r

while True:
    ret, frame = vid.read()
    H, W, Ch = frame.shape
    frame = cv2.resize(frame, dsize=(W//DOWNSAMPLING, H//DOWNSAMPLING), interpolation=cv2.INTER_LINEAR)

    # circular buffer:
    if buf is None:
        buf = np.zeros((N,) + frame.shape, dtype=np.uint8)
    # there should be a simpler way to a FIFO-grid...
    for i in reversed(range(1, N)):
        buf[i] = buf[i - 1]
    buf[0] = frame

    if interp_values is None:
        # create a lookup pattern here:
        interp_values = np.zeros(frame.shape[:2])
        for i in range(frame.shape[0]):
            for j in range(frame.shape[1]):
                y = i / (frame.shape[0] - 1) * 2 - 1
                x = j / (frame.shape[1] - 1) * 2 - 1
                #interp_values[i, j] = (N - 1) * min(1, math.hypot(x, y))
                interp_values[i, j] = (N - 1) * (y + 1) / 2
        interp_values = expand_interp_values(interp_values)

    im = linear_interp(buf, interp_values)

    im = cv2.resize(im, dsize=(W, H), interpolation=cv2.INTER_LANCZOS4)
    cv2.imshow('image', im)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
vid.release()
cv2.destroyAllWindows()
0 Answers
Related