Неизвестный вывод calcOpticalFlowFarneback OpenCV

Мне было интересно, что говорит матрица оптического потока, которую возвращает функция calcOpticalFlowFarneback OpenCV. Если я вычислю эту строку Python:

flow = cv2.calcOpticalFlowFarneback(cv2.UMat(prvs),cv2.UMat(next), None, 0.5, 3, 15, 3, 5, 1.2, 0)

Я получу матрицу того же размера, что и кадры prvs и next, содержащую для каждой позиции вектор из двух элементов (x, y). Мой вопрос... Этот вектор является вектором от prvs до next или от next до prvs?

Спасибо.


person Community    schedule 09.12.2017    source источник
comment
с prvs по next конечно! Вы также можете убедиться в этом, просто взглянув на значения. Пиксель в prvs[y, x] переместится на flow[y, x] пикселя, чтобы добраться до next[y', x']. Другими словами, next[flow[y, x]] = prvs[y, x]. (Это просто пример, здесь вам нужно позаботиться о порядке индексации).   -  person alkasm    schedule 10.12.2017
comment
Тогда я не понимаю, почему это дает мне плохие результаты. Я пытаюсь использовать этот поток для выполнения интерполяции движения и интерполяции промежуточных кадров. Если бы я хотел переместить точку из кадра prvs точно в промежуточную точку между prvs и next, что мне следует учитывать? prvs[x,y] + flow[x,y] или prvs[x,y] - flow[x,y]? @Александр Рейнольдс   -  person    schedule 10.12.2017
comment
И почему вы используете [y,x] вместо [x,y]? Элементы вектора потока изменились? Сначала компонент y, а затем компонент x? @Александр Рейнольдс   -  person    schedule 10.12.2017
comment
[y, x], потому что это изображения, которые являются массивами и, как таковые, индексируются с помощью (row, col), т.е. (y, x). prvs[y, x] — это пиксель, flow[y, x] — это вектор, их не следует добавлять. Технически это должно быть next[ [y, x] + flow[y, x][::-1] ] = prvs[y, x], [::-1] там, потому что я думаю, что flow[y, x] даст вам координаты в порядке (x, y), поэтому [::-1] меняет их на [y, x] для индексации. Я могу попробовать и дать вам лучший ответ позже, когда я дома.   -  person alkasm    schedule 10.12.2017


Ответы (1)


Общая цель метода оптического потока состоит в том, чтобы найти компонент скорости каждого пикселя (если он плотный) или каждой характерной точки (если он разреженный) между двумя изображениями (или видеокадрами, как правило). Идея состоит в том, что пиксели в кадре N-1 перемещаются в новые позиции в кадре N, а разница в местоположении этих пикселей подобна вектору скорости. Это означает, что пиксель в позиции (x, y) в предыдущем кадре будет находиться в позиции (x+v_x, y+v_y) в следующем кадре.

Для значений пикселей это означает, что для данной позиции (x, y) значение пикселя в prev_frame(x, y) совпадает со значением пикселя в curr_frame(x+v_x, y+v_y). Или, точнее, с точки зрения фактических индексов массива:

prev_frame[y, x] == curr_frame[y + flow[y, x, 1], x + flow[y, x, 0]]

Обратите внимание на обратный порядок (x, y) здесь. Массивы индексируются в порядке (строка, столбец), что означает, что сначала идет компонент y, а затем компонент x. Обратите особое внимание на то, что flow[y, x] — это вектор, где первый элемент — это координата x, а второй — координата y — поэтому я добавил y + flow[y, x, 1] и x + flow[y, x, 0]. Вы увидите то же самое в документации для calcOpticalFlowFarneback() :

Функция находит оптический поток для каждого предыдущего пикселя с помощью алгоритма Фарнебака, так что

prev(y,x) ~ next(y + flow(y,x)[1], x + flow(y,x)[0])

Алгоритмы плотного оптического потока предполагают, что пиксели будут находиться недалеко от того места, где они начинались, поэтому они обычно используются в видео, где не происходит значительных изменений в каждом кадре. Если в каждом кадре есть огромная разница, вы, вероятно, не получите правильную оценку. Конечно, цель модели разрешения пирамиды состоит в том, чтобы помочь с большими скачками, но вам нужно позаботиться о выборе надлежащих масштабов разрешения.

Вот полноценный пример. Я начну с этого короткого таймлапса, который я снял в Ванкувере в начале этого года. Я создам функцию, которая приписывает направление потока для каждого пикселя с цветом и величину потока с яркостью этого цвета. Это означает, что более яркие пиксели будут соответствовать более высоким потокам, а цвет соответствует направлению. Это то, что они делают в последнем примере на Учебное пособие по оптическому потоку OpenCV.

import cv2
import numpy as np

def flow_to_color(flow, hsv):
    mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
    hsv[..., 0] = ang*180/np.pi/2
    hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
    return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

cap = cv2.VideoCapture('vancouver.mp4')

fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('optflow.mp4', fourcc, fps, (w, h))

optflow_params = [0.5, 3, 15, 3, 5, 1.2, 0]

frame_exists, prev_frame = cap.read()
prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(prev_frame)
hsv[..., 1] = 255

while(cap.isOpened()):
    frame_exists, curr_frame = cap.read()
    if frame_exists:
        curr = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
        flow = cv2.calcOpticalFlowFarneback(prev, curr, None, *optflow_params)
        rgb = flow_to_color(flow, hsv)
        out.write(rgb)
        prev = curr
    else:
        break

cap.release()
out.release()
print('done')

И результирующее видео.

Однако то, что вы хотите сделать, это интерполировать между кадрами. Это немного сбивает с толку, потому что лучше всего это сделать с помощью cv2.remap(), но эта функция работает в противоположном направлении, которое нам нужно. Оптический поток сообщает нам, куда пиксель уходит, но remap() хочет знать, откуда пиксель появился. Так что на самом деле нам нужно поменять порядок вычисления оптического потока на remap. См. мой ответ здесь для подробного объяснения функции remap().

Итак, здесь я создал функцию interpolate_frames(), которая будет интерполировать любое количество кадров, которые вы хотите получить из потока. Это работает точно так же, как мы обсуждали в комментариях, но обратите внимание на перевернутый порядок curr и prev внутри calcOpticalFlowFarneback().

Покадровое видео выше — плохой кандидат, так как межкадровое движение очень велико. Вместо этого я буду использовать короткий клип из другого видео, снятого в том же месте, что и вход.

import cv2
import numpy as np


def interpolate_frames(frame, coords, flow, n_frames):
    frames = [frame]
    for f in range(1, n_frames):
        pixel_map = coords + (f/n_frames) * flow
        inter_frame = cv2.remap(frame, pixel_map, None, cv2.INTER_LINEAR)
        frames.append(inter_frame)
    return frames


cap = cv2.VideoCapture('vancouver.mp4')

fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('optflow-inter1a.mp4', fourcc, fps, (w, h))

optflow_params = [0.5, 3, 15, 3, 5, 1.2, 0]

frame_exists, prev_frame = cap.read()
prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
y_coords, x_coords = np.mgrid[0:h, 0:w]
coords = np.float32(np.dstack([x_coords, y_coords]))

while(cap.isOpened()):
    frame_exists, curr_frame = cap.read()
    if frame_exists:
        curr = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
        flow = cv2.calcOpticalFlowFarneback(curr, prev, None, *optflow_params)
        inter_frames = interpolate_frames(prev_frame, coords, flow, 4)
        for frame in inter_frames:
            out.write(frame)
        prev_frame = curr_frame
        prev = curr
    else:
        break

cap.release()
out.release()

И вот результат. В оригинале на каждый кадр приходится 4 кадра, поэтому он замедлен в 4 раза. Конечно, будут появляться черные краевые пиксели, поэтому при этом вы, вероятно, захотите либо выполнить интерполяцию границ ваших кадров (вы можете использовать cv2.copyMakeBorder()), чтобы повторить похожие краевые пиксели, и/или обрезать окончательный результат. немного, чтобы избавиться от этого. Обратите внимание, что большинство алгоритмов стабилизации видео обрезают изображение по тем же причинам. Это одна из причин, почему, когда вы переключаете камеру телефона на видео, вы заметите большее фокусное расстояние (оно выглядит немного увеличенным).

person alkasm    schedule 10.12.2017
comment
Затем, имея вектор потока, предыдущий кадр и следующий кадр, чтобы найти значение пикселя в интерполированном кадре, я должен использовать это. Например, если я хочу найти координату x в предыдущем кадре: x_prev = x - 0.5 * flow[x,y][0] И координату x в следующем кадре: x_next = x + 0.5 * flow[x,y][0] Я прав? - person ; 10.12.2017
comment
Ага! Вот как вы можете генерировать межкадровую интерполяцию. Опять же, будьте осторожны с порядком здесь, должно быть flow[y, x]. Однако! И это важный момент: вектор потока будет давать межпиксельные измерения (т.е. flow[y, x][0] может быть 3.105). Вам, конечно, нужно округлить или усечь до целочисленных индексов. Но что произойдет, если два значения будут сопоставлены с одной и той же точкой после округления? Точно так же, что произойдет, если есть некоторые пиксели, которые не отображаются? Это произойдет во многих местах. Вы должны использовать cv2.remap для интерполяции за вас, и он позаботится об этом. - person alkasm; 10.12.2017
comment
@kelirkenan также ознакомьтесь с моим ответом здесь, показывающим, как использовать cv2.remap(). Документы могут быть немного запутанными, и это поможет вам быстро освоиться. Кроме того, в этом ответе показано, как немедленно применить cv2.remap() из результата оптического потока. - person alkasm; 10.12.2017
comment
Идеальный! Спасибо. - person ; 10.12.2017
comment
@kelirkenan На самом деле я пошел дальше и обновил, показав, как интерполировать с помощью remap(). Я не делал этого сам, поэтому мне было просто любопытно, как это работает. Посмотрите, результаты очень крутые! - person alkasm; 10.12.2017
comment
Это отличный ответ, но мне любопытно, можно ли его использовать со спорадическими случайными данными из массива numpy. У меня есть значения в большом 2D-массиве, которые варьируются от 0 до 60. Я разделил массив на 255 для нормализации, а затем передал в функцию cv2.calcOpticalFlowFarneback(curr, prev, None, *optflow_params). Максимальное значение потока кажется очень маленьким 1.492787e-11, и я не вижу значений в переназначенном массиве. Я также пытался настроить параметры optflow_params. Любые идеи? - person wuffwuff; 01.03.2020