Я хочу применить эффект увеличения и уменьшения масштаба к видео с помощью 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 Я внес некоторые изменения и сделал код воспроизводимым
Вы округляете интерполированные значения до двух цифр после десятичной точки, но затем преобразуете их в целое число, просто отсекая десятичные знаки. Учтите это int(round(999.994,2)) == 999
но int(round(999.996,2)) == 1000
. Это означает, что относительная разница между последовательными шагами не будет постоянной. Отобразите результаты интерполяции на графике, чтобы лучше увидеть, что там происходит.
вы обрезали на целые пиксели. это ошибка. не делай этого. используйте варпаффин. сгенерируйте подходящее преобразование из примитивных матриц перевода и масштабирования. сделайте это в тех частях, которые я описал, затем умножьте матрицу, чтобы получить составное преобразование. не пытайтесь сформулировать всю матрицу сразу. попробуйте это. дайте мне знать, как у вас дела.
@ChristophRackwitz Спасибо за комментарий, я, честно говоря, не понимаю, что вы сказали, это звучит слишком сложно, не могли бы вы, если возможно, показать небольшой пример?
@DanMašek Вы правы, я разместил график в своем исходном вопросе и увидел, что значения не меняются плавно, есть некоторые изменения.
@DanMašek Когда я удалил округление и преобразование в целочисленный код, он возвращал десятичное значение, и мы не могли обрезать кадр со значениями в десятичном формате.
Да, именно поэтому необходим другой подход, как предлагает Кристоф.
@DanMašek Новый подход нужен только для интерполированных возвращаемых значений или для всей логики увеличения и уменьшения масштаба?
@ChristophRackwitz Любая помощь, пожалуйста
Я не рассматривал метод интерполяции подробно. Вот пример , как выполнить масштабирование с помощью аффинного преобразования. Обратите внимание, что масштабирование происходит вокруг начала координат, поэтому нам нужно добавить некоторый сдвиг до/после, чтобы масштабировать изображение по центру. Также смотрите en.wikipedia.org/wiki/…
ответ скоро появится... видео: imgur.com/a/mDfrpre
Во-первых, давайте посмотрим, почему ваш подход дает сбои. Тогда я покажу вам альтернативу, которая не дрожит.
В вашем подходе вы масштабируете изображение, сначала обрезая его, а затем изменяя его размер. Такое кадрирование происходит только по целым пиксельным строкам/столбцам, а не более мелкими шагами. Вы видели это особенно хорошо в конце ролика, где изображение очень точно увеличено. Обрезанное изображение изменит ширину/высоту менее чем на один пиксель за кадр, поэтому оно меняется только каждые пару кадров. Подергивание будет усиливаться по мере увеличения масштаба, потому что тогда пиксель становится больше.
Вместо такой обрезки рассчитайте и примените матрицу преобразования для каждого кадра. Это включает в себя warpAffine()
или warpPerspective()
. Учитывая, что исходное изображение является текстурой, эти функции обрабатывают каждый целевой пиксель, используют матрицу преобразования для вычисления точки в исходном изображении, а затем сэмплируют ее в исходном изображении с некоторым режимом интерполяции.
Составное преобразование рассчитывается из трех примитивных преобразований:
В коде я строю это преобразование в такой последовательности. Вы также можете записать это в одном выражении как 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")
Вот несколько видео результатов.
Один из них — очень близкое увеличение с интерполяцией ближайшего соседа, поэтому вы можете четко видеть пиксели. Как видите, изображение не «обрезано» до целых пикселей. По краям видны кусочки пикселей.
Ради интереса я также сделал одно видео с несколькими сегментами анимации (пауза, вход, пауза, выход, пауза). Для этого просто нужна некоторая логика 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?
да, легко. в коде уже определены такие координаты. представьте себе, что изображение и видео протыкается булавкой. zoom_center
— расположение булавки в кадре, anchor
— расположение булавки на изображении. вы можете изменить или даже анимировать их.
если вам нужно сделать это с источником видео вместо неподвижного изображения, я бы рекомендовал не OpenCV VideoWriter
, а PyAV. Это даст вам «временные метки представления» исходных видеокадров.
Еще раз большое спасибо за решение, я экспериментирую с кодом, которым вы поделились, чтобы он вписался в мой текущий проект. Мне действительно нужна возможность захватывать видео вместо неподвижного изображения и иметь возможность записать его в формате mp4.
Я не вижу никакой "трясучести" в выложенном вами видео. Нам нужен минимально воспроизводимый пример , чтобы запустить ваш код и увидеть результаты, иначе никто не поймет, о чем вы говорите, и вы вряд ли получите какую-либо помощь.