Дрожащий зум с opencv python

Я хочу применить эффект увеличения и уменьшения масштаба к видео с помощью opencv, но поскольку opencv не имеет встроенного масштабирования, я пытаюсь обрезать кадр до ширины, высоты, x и y интерполированного значения, а затем изменить размер кадра до исходный размер видео, т. е. 1920 x 1080.

Но когда я рендерил финальное видео, оно тряслось. Я не уверен, почему это происходит, мне нужно идеально плавное увеличение и уменьшение масштаба в определенное время.

Я создал функцию замедления, которая давала интерполированное значение для каждого кадра при увеличении и уменьшении масштаба: -

import cv2


video_path = 'inputTest.mp4'
cap = cv2.VideoCapture(video_path)

fps = int(cap.get(cv2.CAP_PROP_FPS)) 
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('output_video.mp4', fourcc, fps, (1920, 1080))

initialZoomValue = {
     'initialZoomWidth': 1920,
    'initialZoomHeight': 1080,
    'initialZoomX': 0,
    'initialZoomY': 0
}

desiredValues = {
     'zoomWidth': 1672,
     'zoomHeight': 941,
     'zoomX': 200,
     'zoomY': 0
}

def ease_out_quart(t):
    return 1 - (1 - t) ** 4

async def zoomInInterpolation(initialZoomValue, desiredZoom, start, end, index):
    t = (index - start) / (end - start)
    eased_t = ease_out_quart(t)

    interpolatedWidth = round(initialZoomValue['initialZoomWidth'] + eased_t * (desiredZoom['zoomWidth']['width'] - initialZoomValue['initialZoomWidth']), 2)
    interpolatedHeight = round(initialZoomValue['initialZoomHeight'] + eased_t * (desiredZoom['zoomHeight'] - initialZoomValue['initialZoomHeight']), 2)
    interpolatedX = round(initialZoomValue['initialZoomX'] + eased_t * (desiredZoom['zoomX'] - initialZoomValue['initialZoomX']), 2)
    interpolatedY = round(initialZoomValue['initialZoomY'] + eased_t * (desiredZoom['zoomY'] - initialZoomValue['initialZoomY']), 2)
    
    return {'interpolatedWidth': int(interpolatedWidth), 'interpolatedHeight': int(interpolatedHeight), 'interpolatedX': int(interpolatedX), 'interpolatedY': int(interpolatedY)}

def generate_frame():
        while cap.isOpened():
            code, frame = cap.read()
            if code:
                yield frame
            else:
                print("bailsdfing")
                break


for i, frame in enumerate(generate_frame()):
   if i >= 1 and i <= 60:
        interpolatedValues = zoomInInterpolation(initialZoomValue, desiredValues, 1, 60, i)
        crop = frame[interpolatedValues['interpolatedY']:(interpolatedValues['interpolatedHeight'] + interpolatedValues['interpolatedY']), interpolatedValues['interpolatedX']:(interpolatedValues['interpolatedWidth'] + interpolatedValues['interpolatedX'])]
        zoomedFrame = cv2.resize(crop,(1920, 1080), interpolation = cv2.INTER_CUBIC) 

        out.write(zoomedFrame)

# Release the video capture and close windows
cap.release()
cv2.destroyAllWindows()

Но последнее видео, которое я получаю, трясет :-

Финальное видео

Я хочу, чтобы видео идеально увеличивалось и уменьшалось, не хочу никакой тряски.


Вот график интерполированных значений: -

Это график, если я не округляю число слишком рано и возвращаю только целое значение: -

Поскольку OpenCV принимает для обрезки только целые числа, невозможно вернуть значения из функции интерполяции в десятичных дробях.

Я не вижу никакой "трясучести" в выложенном вами видео. Нам нужен минимально воспроизводимый пример , чтобы запустить ваш код и увидеть результаты, иначе никто не поймет, о чем вы говорите, и вы вряд ли получите какую-либо помощь.

stateMachine 16.08.2024 09:42

@stateMachine Я внес некоторые изменения и сделал код воспроизводимым

Zaid 16.08.2024 10:03

Вы округляете интерполированные значения до двух цифр после десятичной точки, но затем преобразуете их в целое число, просто отсекая десятичные знаки. Учтите это int(round(999.994,2)) == 999 но int(round(999.996,2)) == 1000. Это означает, что относительная разница между последовательными шагами не будет постоянной. Отобразите результаты интерполяции на графике, чтобы лучше увидеть, что там происходит.

Dan Mašek 16.08.2024 12:28

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

Christoph Rackwitz 16.08.2024 12:41

@ChristophRackwitz Спасибо за комментарий, я, честно говоря, не понимаю, что вы сказали, это звучит слишком сложно, не могли бы вы, если возможно, показать небольшой пример?

Zaid 16.08.2024 13:00

@DanMašek Вы правы, я разместил график в своем исходном вопросе и увидел, что значения не меняются плавно, есть некоторые изменения.

Zaid 16.08.2024 13:14

@DanMašek Когда я удалил округление и преобразование в целочисленный код, он возвращал десятичное значение, и мы не могли обрезать кадр со значениями в десятичном формате.

Zaid 16.08.2024 13:24

Да, именно поэтому необходим другой подход, как предлагает Кристоф.

Dan Mašek 16.08.2024 13:35

@DanMašek Новый подход нужен только для интерполированных возвращаемых значений или для всей логики увеличения и уменьшения масштаба?

Zaid 16.08.2024 13:37

@ChristophRackwitz Любая помощь, пожалуйста

Zaid 16.08.2024 16:06

ответ скоро появится... видео: imgur.com/a/mDfrpre

Christoph Rackwitz 16.08.2024 19:00
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
12
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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

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

Составное преобразование рассчитывается из трех примитивных преобразований:

  1. переместить определенную точку («якорь») изображения в начало координат
  2. масштабировать вокруг начала координат (якоря)
  3. переместиться туда, где оно должно быть в видеокадре

В коде я строю это преобразование в такой последовательности. Вы также можете записать это в одном выражении как T = translate2(*+zoom_center) @ scale2(s=z) @ translate2(*-anchor). Да, операции, выраженные этими матрицами, применяются справа налево.

import numpy as np
import cv2 as cv
from tqdm import tqdm # remove that if you don't like it

# Those two functions generate simple translation and scaling matrices:

def translate2(tx=0, ty=0):
    T = np.eye(3)
    T[0:2, 2] = [tx, ty]
    return T

def scale2(s=1, sx=1, sy=1):
    T = np.diag([s*sx, s*sy, 1])
    return T

# you know this one already

def ease_out_quart(alpha):
    return 1 - (1 - alpha) ** 4

# some constants to describe the zoom

im = cv.imread(cv.samples.findFile("starry_night.jpg"))
(imheight, imwidth) = im.shape[:2]

(output_width, output_height) = (1280, 720)

fps = 60
duration = 5.0 # secs

# "anchor": somewhere in the image
anchor = np.array([ (imwidth-1) * 0.75, (imheight-1) * 0.75 ])
# position: somewhere in the frame
zoom_center = np.array([ (output_width-1) * 0.75, (output_height-1) * 0.75 ])

zoom_t_start, zoom_t_end = 1.0, 4.0
zoom_z_start, zoom_z_end = 1.0, 10.0

# calculates the matrix:

def calculate_transform(timestamp):
    alpha = (timestamp - zoom_t_start) / (zoom_t_end - zoom_t_start)
    alpha = np.clip(alpha, 0, 1)
    alpha = ease_out_quart(alpha)
    z = zoom_z_start + alpha * (zoom_z_end - zoom_z_start)

    T = translate2(*-anchor)
    T = scale2(s=z) @ T
    T = translate2(*+zoom_center) @ T

    return T

# applies the matrix:

def animation_callback(timestamp, canvas):
    T = calculate_transform(timestamp)
    cv.warpPerspective(
        src=im,
        M=T,
        dsize=(output_width, output_height),
        dst=canvas, # drawing over the same buffer repeatedly
        flags=cv.INTER_LANCZOS4, # or INTER_LINEAR, INTER_NEAREST, ...
    )

# generate the video

writer = cv.VideoWriter(
    filename = "output.avi",  # AVI container: OpenCV built-in
    fourcc=cv.VideoWriter_fourcc(*"MJPG"), # MJPEG codec: OpenCV built-in
    fps=fps,
    frameSize=(output_width, output_height),
    isColor=True
)
assert writer.isOpened()

canvas = np.zeros((output_height, output_width, 3), dtype=np.uint8)

timestamps = np.arange(0, duration * fps) / fps

try:
    for timestamp in tqdm(timestamps):
        animation_callback(timestamp, canvas)

        writer.write(canvas)

        cv.imshow("frame", canvas)

        key = cv.waitKey(1)
        if key in (13, 27): break

finally:
    cv.destroyWindow("frame")
    writer.release()
    print("done")

Вот несколько видео результатов.

https://imgur.com/a/mDfrpre

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

Ради интереса я также сделал одно видео с несколькими сегментами анимации (пауза, вход, пауза, выход, пауза). Для этого просто нужна некоторая логика calculate_transform, чтобы определить, в каком временном интервале вы находитесь.

zoom_keyframes = [ # (time, zoom)
    (0.0, 15.0),
    (1.0, 15.0),
    (2.0, 16.0),
    (3.0, 16.0),
    (4.0, 15.0),
    (5.0, 15.0),
]

def calculate_transform(timestamp):
    i0 = i1 = 0
    for i, (tq, _) in enumerate(zoom_keyframes):
        if tq <= timestamp:
            i0 = i
        if timestamp <= tq:
            i1 = i
            break

    if i1 == i0: i1 = i0 + 1

    # print(f"i0 {i0}, i1 {i1}")

    zoom_ta, zoom_za = zoom_keyframes[i0]
    zoom_tb, zoom_zb = zoom_keyframes[i1]

    alpha = (timestamp - zoom_ta) / (zoom_tb - zoom_ta)
    alpha = np.clip(alpha, 0, 1)
    alpha = ease_out_quart(alpha)
    z = zoom_za + alpha * (zoom_zb - zoom_za)

    T = translate2(*-anchor)
    T = scale2(s=z) @ T
    T = translate2(*+zoom_center) @ T

    return T

Большое спасибо, Крис, за ответ, я очень ценю твою помощь и время, которое ты уделил решению. Просто небольшой вопрос: могу ли я увеличивать масштаб определенных точек вместо увеличения центра? например, координаты x и y: - 10, 40?

Zaid 16.08.2024 19:50

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

Christoph Rackwitz 16.08.2024 19:54

если вам нужно сделать это с источником видео вместо неподвижного изображения, я бы рекомендовал не OpenCV VideoWriter, а PyAV. Это даст вам «временные метки представления» исходных видеокадров.

Christoph Rackwitz 16.08.2024 20:00

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

Zaid 16.08.2024 20:36

Другие вопросы по теме