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

<p>(я назначу награду в 500 репутации по этому вопросу, как только он будет удовлетворен - если только вопрос не будет закрыт.)</p><p><strong>проблема в одном предложении</strong></p><p>чтение кадров из <code>videocapture</code> продвигает видео намного дальше, чем предполагалось.</p><p><strong>объяснение</strong></p><p>мне нужно читать и анализировать кадры из видео со скоростью 100 кадров в секунду (согласно <code>cv2</code> и vlc media player) между определенными временными интервалами. в следующем минимальном примере я пытаюсь прочитать все кадры за первые десять секунд трехминутного видео.</p><p>я создаю объект <code>cv2.videocapture</code>, из которого я читаю кадры, пока не будет достигнута желаемая позиция в миллисекундах. в моем реальном коде каждый кадр анализируется, но этот факт не имеет значения, чтобы продемонстрировать ошибку.</p><p>проверка текущего кадра и миллисекундной позиции <code>videocapture</code> после чтения фреймов дает правильные значения, поэтому <code>videocapture</code> <strong>думает</strong>, что он находится в правильном положении, но это не так. сохранение изображения последнего считываемого кадра показывает, что моя итерация значительно превышает время назначения <strong>более чем на две минуты</strong>.</p><p>что еще более странно, так это то, что если я вручную установлю миллисекундную позицию захвата с <code>videocapture.set</code> равной 10 секундам (то же значение <code>videocapture.get</code> возвращается после чтения кадров) и сохраню изображение, видео будет (почти) в правильной позиции!</p><p><strong>демонстрационный видеофайл</strong></p><p>если вы хотите запустить mcve, вам нужен видеофайл demo.avi . вы можете скачать его <a href="https://workupload.com/file/q4elvbc"><strong>here</strong></a> .</p><p><strong>mcve</strong></p><p>этот mcve тщательно обработан и прокомментирован. пожалуйста, оставьте комментарий под вопросом, если что-то остается неясным.</p><p>если вы используете opencv 3, вы должны заменить все экземпляры <code>cv2.cv.cv\_</code> на <code>cv2.</code>. (проблема возникает в обеих версиях для меня.)</p><pre><code>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) </code></pre><p><strong>mcve выход</strong></p><p>операторы <code>print</code> производят следующий вывод.</p><blockquote> <p>версия cv2 = 2.4.9.1<br> начальные атрибуты: fps = 100.0, pos\_msec = 0.0, pos\_frames = 0.0<br> атрибуты после чтения: pos\_msec = 10010.0, pos\_frames = 1001.0<br> атрибуты после установки msec pos вручную: pos\_msec = 10010.0, pos\_frames = 1001.0<br></p> </blockquote><p>как видите, все свойства имеют ожидаемые значения.</p><p><code>imwrite</code> сохраняет следующие изображения.</p><p>first\_frame.png <a href="https://i.stack.imgur.com/ed7fl.png"><img src="https://i.stack.imgur.com/ed7fl.png" alt="first\_frame.png"> </a> </p><p>after\_iteration.png <a href="https://i.stack.imgur.com/x2zg3.png"><img src="https://i.stack.imgur.com/x2zg3.png" alt="after\_iteration.png"> </a> </p><p>after\_setting.png <a href="https://i.stack.imgur.com/hirxh.png"><img src="https://i.stack.imgur.com/hirxh.png" alt="after\_setting.png"> </a> </p><p>вы можете увидеть проблему на втором рисунке. задание 9:26:15 (часы реального времени на картинке) пропущено более чем на две минуты. установка целевого времени вручную (третье изображение) устанавливает видео (почти) в правильное положение. </p><p>что я делаю не так и как мне это исправить?</p><p><strong>пробовал до сих пор</strong></p><p>cv2 2.4.9.1 @ ubuntu 16.04<br> cv2 2.4.13 @ scientific linux 7.3 (три компьютера)<br> cv2 3.1.0 @ scientific linux 7.3 (три компьютера)<br></p><p>создание захвата с </p><pre><code>cap = cv2.videocapture('demo.avi', apipreference=cv2.cap\_ffmpeg) </code></pre><p>а также</p><pre><code>cap = cv2.videocapture('demo.avi', apipreference=cv2.cap\_gstreamer) </code></pre><p>в opencv 3 (версия 2, похоже, не имеет аргумента <code>apipreference</code>) . использование <code>cv2.cap\_gstreamer</code> занимает очень много времени (около 2-3 минут для запуска mcve), но оба api-предпочтения выдают одинаковые неверные изображения.</p><p>при непосредственном использовании <code>ffmpeg</code> для считывания кадров (спасибо <a href="http://zulko.github.io/blog/2013/09/27/read-and-write-video-frames-in-python-using-ffmpeg/">this</a> tutorial)) создаются правильные выходные изображения.</p><pre><code>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() </code></pre><p>это дает правильный результат! (правильная временная метка 9:26:15)</p><p>frame\_1001\_ffmpeg\_only.png: <a href="https://i.stack.imgur.com/vvjqa.png"><img src="https://i.stack.imgur.com/vvjqa.png" alt="frame\_1001\_ffmpeg\_only.png"> </a> </p><p><strong>дополнительная информация</strong></p><p>в комментариях меня спросили о моем файле <code>cvconfig.h</code>. я, кажется, только этот файл для cv2 версии 3.1.0 под <code>/opt/opencv/3.1.0/include/opencv2/cvconfig.h</code>. </p><p> <a href="https://pastebin.com/dk9affan"><strong>here</strong></a> - вставка этого файла.</p><p>в случае, если это поможет, я смог извлечь следующую видео информацию с помощью <code>videocapture.get</code>.</p><blockquote> <p>яркость 0.0<br> контраст 0.0<br> convert\_rgb 0.0<br> экспозиция 0.0<br> формат 0.0<br> fourcc 1684633187.0<br> кадр/с 100,0<br> frame\_count 18000.0<br> frame\_height 593.0<br> frame\_width 792.0<br> получить 0.0<br> оттенок 0.0<br> режим 0.0<br> openni\_baseline 0.0<br> openni\_focal\_length 0.0<br> openni\_frame\_max\_depth 0.0<br> openni\_output\_mode 0.0<br> openni\_registration 0.0<br> pos\_avi\_ratio 0.01<br> pos\_frames 0.0<br> pos\_msec 0.0<br> выпрямление 0,0<br> насыщенность 0.0<br></p> </blockquote>

<p>данные вашего видеофайла содержат всего 1313 неповторяющихся кадров (т.е. от 7 до 8 кадров в секунду):</p><pre><code>$ 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 # ^^^^^^^^^ ... </code></pre><blockquote> <p>кстати, преобразованное таким образом видео (<code>demo.mp4</code>) лишено проблемы обсуждалось, то есть opencv обрабатывает его правильно.</p> </blockquote><p>в этом случае дубликаты кадров физически не присутствуют в файле avi, вместо этого каждый дубликат кадров представлен инструкцией для повторения предыдущего кадра. это можно проверить следующим образом:</p><pre><code>$ 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 ... </code></pre><p>в приведенном выше журнале кадры с фактическими данными представлены строками, начинающимися с «<code>[avi @ 0xhhhhhhhhhhh]</code>». сообщения «<code>video: delay=xxxxx a-v=yyyyy</code>» указывают, что последний кадр должен отображаться на <code>xxxxx</code> больше секунд.</p><p><code>cv2.videocapture()</code> пропускает такие дублированные кадры, читая только те кадры, которые содержат реальные данные. вот соответствующий (хотя и слегка отредактированный) <a href="https://github.com/opencv/opencv/blob/2.4/modules/highgui/src/cap\_ffmpeg\_impl.hpp#l856">код из ветки 2.4 opencv</a> (обратите внимание, кстати, что под ffmpeg используется, что я проверил, запустив python под gdb и установив точку останова на <code>cvcapture\_ffmpeg::grabframe</code>):</p><pre class="lang-cpp prettyprint-override"><code>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; } } ... } </code></pre>

<p>в двух словах: я воспроизвел вашу проблему на машине с ubuntu 12.04 с opencv 2.4.13, заметил, что кодек, используемый в вашем видео (fourcc cvid), кажется довольно старым (согласно этому <a href="http://www.codeccodecs.com/fourcc-cvid-t255.html">post</a> из 2011), и после преобразования видео в кодек mjpg (он же m-jpeg или motion jpeg) ваш mcve заработал. конечно, леон (или другие) могут опубликовать исправление для opencv, которое может быть лучшим решением для вашего случая.</p><p>я изначально пробовал конвертацию используя</p><pre><code>ffmpeg -i demo.avi -vcodec mjpeg -an demo\_mjpg.avi </code></pre><p>а также</p><pre><code>avconv -i demo.avi -vcodec mjpeg -an demo\_mjpg.avi </code></pre><p>(оба также на коробке 16.04). интересно, что оба выпускали «битые» ролики. например, при переходе на 1000-й кадр с помощью avidemux нет часов реального времени! кроме того, конвертированные видео были только около 1/6 от исходного размера, что странно, так как m-jpeg - очень простое сжатие. (каждый кадр сжимается в формате jpeg независимо.)</p><p>использование avidemux для преобразования <code>demo.avi</code> в m-jpeg привело к созданию видео, над которым работал mcve. (я использовал графический интерфейс avidemux для преобразования.) размер преобразованного видео примерно в 3 раза больше исходного размера. конечно, также возможно сделать оригинальную запись, используя кодек, который лучше поддерживается в linux. если вам нужно перейти к определенным кадрам в видео в вашем приложении, m-jpeg может быть лучшим вариантом. в противном случае, h.264 сжимается намного лучше. по моему опыту, оба они хорошо поддерживаются, и только те коды, которые я видел, реализованы непосредственно на веб-камерах (h.264 только на высококлассных).</p>

<p>как вы сказали : </p><blockquote> <p>при непосредственном использовании ffmpeg для чтения кадров (благодарность этому уроку) получаются правильные выходные изображения.</p> </blockquote><p>это нормально, потому что вы определяете <code>framesize = resolution[0]*resolution[1]*3</code> </p><p>затем используйте его снова, когда прочитаете: <code>pipe.stdout.read(framesize)</code></p><p>так что, по моему мнению, вы должны обновить каждый: </p><pre><code>\_, frame = cap.read() </code></pre><p>в</p><pre><code>\_, frame = cap.read(framesize) </code></pre><p>при условии, что разрешение идентично, окончательная версия кода будет:</p><pre><code>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) </code></pre>