it-swarm.com.ru

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

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

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

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

Объяснение

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

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

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

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

Демонстрационный видеофайл

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

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

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

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

HERE - вставка этого файла.

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

яркость 0.0
контраст 0.0
convert_rgb 0.0
экспозиция 0.0
формат 0.0
fourcc 1684633187.0
кадр/с 100,0
frame_count 18000.0
frame_height 593.0
frame_width 792.0
получить 0.0
оттенок 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

9
timgeb

Данные вашего видеофайла содержат всего 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
#                                                                   ^^^^^^^^^
...

Кстати, преобразованное таким образом видео (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;
        }
    }
    ...
}
4
Leon

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

1
Ulrich Stern

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

При непосредственном использовании 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'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)
0
A STEFANI