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

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

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

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

объяснение

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

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

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

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

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

в случае, если вы хотите запустить MCVE, вам нужна демонстрация.видеофайл 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's 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 версия = 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, похоже, не имеет

3 ответов


данные вашего видеофайла содержат всего 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 (обратите внимание, кстати, что под 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;
        }
    }
    ...
}

в двух словах: я воспроизвел вашу проблему на машине Ubuntu 12.04 с OpenCV 2.4.13, заметил, что кодек, используемый в вашем видео (FOURCC CVID), кажется довольно старым (согласно этому в должности С 2011), и после преобразования видео в кодек MJPG (он же 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 для преобразования.) Размер преобразованного видео составляет около 3x исходного размера. Конечно, можно также сделать оригинальную запись с помощью кодека, который лучше поддерживается в Linux. Если вам нужно перейти к определенным кадрам в видео в вашем приложении, M-JPEG может быть лучшим вариантом. В противном случае H. 264 сжимается намного лучше. Оба хорошо поддерживаются в моем опыте и единственные коды, которые я видел, реализованы непосредственно на веб-камерах (только H. 264 на высококлассных).


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

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

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

затем повторно использовать его при чтении : pipe.stdout.read(framesize)

поэтому, на мой взгляд, вы должны обновить каждый:

_, frame = cap.read()

to

_, 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's 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)