OpenCV: кадры чтения из VideoCapture продвигают видео в странно неправильное местоположение

(Я поставлю 500 рецензий на этот вопрос, как только он будет допущен - если вопрос не будет закрыт.)

Проблема в одном предложении

Чтение кадров с VideoCapture продвигает видео гораздо дальше, чем предполагалось.

Объяснение

Мне нужно читать и анализировать кадры со скоростью 100 кадров в секунду (в соответствии с видеоформатом cv2 и VLC media player) между определенными интервалами времени. В следующем минимальном примере я пытаюсь прочитать все кадры за первые десять секунд трехминутного видео.

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

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

Что еще более странно, если я вручную установил миллисекундную позицию захвата с VideoCapture.set до 10 секунд (то же самое значение VideoCapture.get вернется после прочтения кадров) и сохранит изображение, видео находится в (почти ) правильное положение!

Демо-видео файл

Если вы хотите запустить MCVE, вам нужен видеофайл demo.avi. Вы можете скачать его ЗДЕСЬ.

MCVE

Этот MCVE тщательно обработан и прокомментирован. Пожалуйста, оставьте комментарий в вопросе, если что-то останется неясным.

Если вы используете OpenCV 3, вам нужно заменить все экземпляры cv2.cv.CV_ на cv2.. (Проблема возникает в обеих версиях для меня.)

import cv2

# set up capture and print properties
print 'cv2 version = {}'.format(cv2.__version__)
cap = cv2.VideoCapture('demo.avi')
fps = cap.get(cv2.cv.CV_CAP_PROP_FPS)
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('initial attributes: fps = {}, pos_msec = {}, pos_frames = {}'
      .format(fps, pos_msec, pos_frames))

# get first frame and save as picture
_, frame = cap.read()
cv2.imwrite('first_frame.png', frame)

# advance 10 seconds, that 100*10 = 1000 frames at 100 fps
for _ in range(1000):
    _, frame = cap.read()
    # in the actual code, the frame is now analyzed

# save a picture of the current frame
cv2.imwrite('after_iteration.png', frame)

# print properties after iteration
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('attributes after iteration: pos_msec = {}, pos_frames = {}'
      .format(pos_msec, pos_frames))

# assert that the capture (thinks it) is where it is supposed to be
# (assertions succeed)
assert pos_frames == 1000 + 1 # (+1: iteration started with second frame)
assert pos_msec == 10000 + 10

# manually set the capture to msec position 10010
# note that this should change absolutely nothing in theory
cap.set(cv2.cv.CV_CAP_PROP_POS_MSEC, 10010)

# print properties  again to be extra sure
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('attributes after setting msec pos manually: pos_msec = {}, pos_frames = {}'
      .format(pos_msec, pos_frames))

# save a picture of the next frame, should show the same clock as
# previously taken image - but does not
_, frame = cap.read()
cv2.imwrite('after_setting.png', frame)

Выход MCVE

Операторы print производят следующий вывод.

cv2 version = 2.4.9.1
начальные атрибуты: fps = 100.0, pos_msec = 0.0, pos_frames = 0.0
атрибуты после чтения: pos_msec = 10010.0, pos_frames = 1001.0
атрибуты после установки msec pos вручную: pos_msec = 10010.0, pos_frames = 1001.0

Как вы можете видеть, все свойства имеют ожидаемые значения.

imwrite сохраняет следующие снимки.

first_frame.png first_frame.png

after_iteration.png after_iteration.png

after_setting.png after_setting.png

Во второй картинке вы можете увидеть эту проблему. Цель 9:26:15 (часы реального времени на снимке) пропущена более двух минут. Установка целевого времени вручную (третье изображение) устанавливает видео в (почти) правильное положение.

Что я делаю неправильно и как его исправить?

Пробовал до сих пор

cv2 2.4.9.1 @Ubuntu 16.04
cv2 2.4.13 @Scientific Linux 7.3 (три компьютера)
cv2 3.1.0 @Scientific Linux 7.3 (три компьютера)

Создание захвата с помощью

cap = cv2.VideoCapture('demo.avi', apiPreference=cv2.CAP_FFMPEG)

и

cap = cv2.VideoCapture('demo.avi', apiPreference=cv2.CAP_GSTREAMER)

в OpenCV 3 (версия 2 не имеет аргумента apiPreference). Использование cv2.CAP_GSTREAMER занимает очень много времени (около 2-3 минут для запуска MCVE), но оба api-предпочтения создают одинаковые неправильные изображения.

При использовании ffmpeg непосредственно для чтения фреймов (кредит этого учебника) создаются правильные выходные изображения.

import numpy as np
import subprocess as sp
import pylab

# video properties
path = './demo.avi'
resolution = (593, 792)
framesize = resolution[0]*resolution[1]*3

# set up pipe
FFMPEG_BIN = "ffmpeg"
command = [FFMPEG_BIN,
           '-i', path,
           '-f', 'image2pipe',
           '-pix_fmt', 'rgb24',
           '-vcodec', 'rawvideo', '-']
pipe = sp.Popen(command, stdout = sp.PIPE, bufsize=10**8)

# read first frame and save as image
raw_image = pipe.stdout.read(framesize)
image = np.fromstring(raw_image, dtype='uint8')
image = image.reshape(resolution[0], resolution[1], 3)
pylab.imshow(image)
pylab.savefig('first_frame_ffmpeg_only.png')
pipe.stdout.flush()

# forward 1000 frames
for _ in range(1000):
    raw_image = pipe.stdout.read(framesize)
    pipe.stdout.flush()

# save frame 1001
image = np.fromstring(raw_image, dtype='uint8')
image = image.reshape(resolution[0], resolution[1], 3)
pylab.imshow(image)
pylab.savefig('frame_1001_ffmpeg_only.png')

pipe.terminate()

Это дает правильный результат! (Правильная временная метка 9:26:15)

frame_1001_ffmpeg_only.png: frame_1001_ffmpeg_only.png

Дополнительная информация

В комментариях меня попросил мой файл cvconfig.h. Кажется, у меня есть этот файл для cv2 версии 3.1.0 под /opt/opencv/3.1.0/include/opencv2/cvconfig.h.

ЗДЕСЬ является вставкой этого файла.

В случае, если это помогает, мне удалось извлечь следующую видеоинформацию с помощью VideoCapture.get.

яркость 0.0
контраст 0.0
convert_rgb 0.0
экспозиция 0.0
формат 0.0
fourcc 1684633187.0
fps 100.0
frame_count 18000.0
frame_height 593.0
frame_width 792.0
прибыль 0.0
hue 0.0
режим 0.0
openni_baseline 0.0
openni_focal_length 0.0
openni_frame_max_depth 0.0
openni_output_mode 0.0
openni_registration 0.0
pos_avi_ratio 0.01
pos_frames 0.0
pos_msec 0.0
ректификация 0.0
насыщенность 0.0 -

Ответ 1

Данные вашего видеофайла содержат всего 1313 недвумерных кадров (то есть от 7 до 8 кадров в секунду продолжительности):

$ ffprobe -i demo.avi -loglevel fatal -show_streams -count_frames|grep frame
has_b_frames=0
r_frame_rate=100/1
avg_frame_rate=100/1
nb_frames=18000
nb_read_frames=1313        # !!!

Преобразование avi файла с ffmpeg отчетами 16697 дубликатов кадров (по какой-то причине добавлено 10 дополнительных кадров и 16697 = 18010-1313).

$ ffmpeg -i demo.avi demo.mp4
...
frame=18010 fps=417 Lsize=3705kB time=03:00.08 bitrate=168.6kbits/s dup=16697
#                                                                   ^^^^^^^^^
...

BTW, поэтому преобразованное видео (demo.mp4) лишено проблемы, являющейся обсуждается, то есть OpenCV обрабатывает его правильно.

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

$ ffplay -loglevel trace demo.avi
...
[ffplay_crop @ 0x7f4308003380] n:16 t:2.180000 pos:1311818.000000 x:0 y:0 x+w:792 y+h:592
[avi @ 0x7f4310009280] dts:574 offset:574 1/100 smpl_siz:0 base:1000000 st:0 size:81266
video: delay=0.130 A-V=0.000094
    Last message repeated 9 times
video: delay=0.130 A-V=0.000095
video: delay=0.130 A-V=0.000094
video: delay=0.130 A-V=0.000095
[avi @ 0x7f4310009280] dts:587 offset:587 1/100 smpl_siz:0 base:1000000 st:0 size:81646
[ffplay_crop @ 0x7f4308003380] n:17 t:2.320000 pos:1393538.000000 x:0 y:0 x+w:792 y+h:592
video: delay=0.140 A-V=0.000091
    Last message repeated 4 times
video: delay=0.140 A-V=0.000092
    Last message repeated 1 times
video: delay=0.140 A-V=0.000091
    Last message repeated 6 times
...

В приведенном выше журнале кадры с фактическими данными представлены строками, начинающимися с "[avi @ 0xHHHHHHHHHHH]". Сообщения "video: delay=xxxxx A-V=yyyyy" показывают, что последний кадр должен отображаться для xxxxx больше секунд.

cv2.VideoCapture() пропускает такие повторяющиеся кадры, только чтение кадров, имеющих реальные данные. Вот соответствующий (хотя и слегка отредактированный) код из ветки 2.4 opencv (обратите внимание, BTW, что под ffmpeg используется, что я проверяется путем запуска python под gdb и установки точки останова на CvCapture_FFMPEG::grabFrame):

bool CvCapture_FFMPEG::grabFrame()
{
    ...
    int count_errs = 0;
    const int max_number_of_attempts = 1 << 9; // !!!
    ...
    // get the next frame
    while (!valid)
    {
        ...
        int ret = av_read_frame(ic, &packet);
        ...        
        // Decode video frame
        avcodec_decode_video2(video_st->codec, picture, &got_picture, &packet);
        // Did we get a video frame?
        if(got_picture)
        {
            //picture_pts = picture->best_effort_timestamp;
            if( picture_pts == AV_NOPTS_VALUE_ )
                picture_pts = packet.pts != AV_NOPTS_VALUE_ && packet.pts != 0 ? packet.pts : packet.dts;
            frame_number++;
            valid = true;
        }
        else
        {
            // So, if the next frame doesn't have picture data but is
            // merely a tiny instruction telling to repeat the previous
            // frame, then we get here, treat that situation as an error
            // and proceed unless the count of errors exceeds 1 billion!!!
            if (++count_errs > max_number_of_attempts)
                break;
        }
    }
    ...
}

Ответ 2

Вкратце: я воспроизвел вашу проблему на машине Ubuntu 12.04 с OpenCV 2.4.13, заметив, что кодек, используемый в вашем видео (CVC CVC), кажется довольно старым (согласно этому post от 2011 года), и после преобразования видео в Meco Mec (или M-JPEG или Motion JPEG) ваш MCVE работал. Конечно, Леон (или другие) может опубликовать исправление для OpenCV, которое может быть лучшим решением для вашего дела.

Сначала я попытался преобразовать, используя

ffmpeg -i demo.avi -vcodec mjpeg -an demo_mjpg.avi

и

avconv -i demo.avi -vcodec mjpeg -an demo_mjpg.avi

(оба тоже на коробке 16.04). Интересно, что оба фильма произвели "разбитые" видео. Например, при переходе к кадру 1000 с использованием Avidemux, в режиме реального времени часы! Кроме того, преобразованные видео были только около 1/6 от первоначального размера, что странно, так как M-JPEG - очень простое сжатие. (Каждый кадр сжимается JPEG независимо.)

Использование Avidemux для преобразования demo.avi в M-JPEG создало видео, на котором работал MCVE. (Я использовал графический интерфейс Avidemux для преобразования.) Размер конвертированного видео примерно в 3 раза по сравнению с исходным размером. Конечно, также возможно сделать оригинальную запись с использованием кодека, который лучше поддерживается в Linux. Если вам нужно перейти к определенным кадрам в видео в приложении, M-JPEG может быть лучшим вариантом. В противном случае H.264 сжимается намного лучше. Оба из них хорошо поддерживаются в моем опыте, и единственные коды, которые я видел, реализованы непосредственно на веб-камерах (H.264 только на high-end).

Ответ 3

Как вы сказали:

При непосредственном использовании ffmpeg для чтения кадров (в соответствии с этим руководством) создаются правильные выходные изображения.

Это нормально, потому что вы определяете framesize = resolution[0]*resolution[1]*3

то повторно используйте его, когда читаете: pipe.stdout.read(framesize)

Итак, по-моему, вам нужно обновить каждый:

_, frame = cap.read()

к

_, frame = cap.read(framesize)

Предполагая, что разрешение идентично, окончательная версия кода будет:

import cv2

# set up capture and print properties
print 'cv2 version = {}'.format(cv2.__version__)
cap = cv2.VideoCapture('demo.avi')
fps = cap.get(cv2.cv.CV_CAP_PROP_FPS)
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('initial attributes: fps = {}, pos_msec = {}, pos_frames = {}'
      .format(fps, pos_msec, pos_frames))

resolution = (593, 792) #here resolution 
framesize = resolution[0]*resolution[1]*3 #here framesize

# get first frame and save as picture
_, frame = cap.read( framesize ) #update to get one frame
cv2.imwrite('first_frame.png', frame)

# advance 10 seconds, that 100*10 = 1000 frames at 100 fps
for _ in range(1000):
    _, frame = cap.read( framesize ) #update to get one frame
    # in the actual code, the frame is now analyzed

# save a picture of the current frame
cv2.imwrite('after_iteration.png', frame)

# print properties after iteration
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('attributes after iteration: pos_msec = {}, pos_frames = {}'
      .format(pos_msec, pos_frames))

# assert that the capture (thinks it) is where it is supposed to be
# (assertions succeed)
assert pos_frames == 1000 + 1 # (+1: iteration started with second frame)
assert pos_msec == 10000 + 10

# manually set the capture to msec position 10010
# note that this should change absolutely nothing in theory
cap.set(cv2.cv.CV_CAP_PROP_POS_MSEC, 10010)

# print properties  again to be extra sure
pos_msec = cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC)
pos_frames = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES)
print ('attributes after setting msec pos manually: pos_msec = {}, pos_frames = {}'
      .format(pos_msec, pos_frames))

# save a picture of the next frame, should show the same clock as
# previously taken image - but does not
_, frame = cap.read()
cv2.imwrite('after_setting.png', frame)