web-dev-qa-db-fra.com

OpenCV: la lecture d'images de VideoCapture fait avancer la vidéo vers un emplacement bizarrement incorrect

(Je mettrai une prime de 500 points de réputation sur cette question dès qu'elle sera éligible - à moins que la question ne soit close.)

_ {Problème en une phrase} _

La lecture d'images à partir d'un VideoCapture fait avancer la vidéo beaucoup plus loin que prévu.

Explication

Je dois lire et analyser des images à partir d'une vidéo à 100 ips (selon cv2 et VLC Media Player) entre certains intervalles de temps. Dans l'exemple minimal qui suit, j'essaie de lire toutes les images pendant les dix premières secondes d'une vidéo de trois minutes.

Je crée un objet cv2.VideoCapture à partir duquel je lis des cadres jusqu'à atteindre la position souhaitée, en millisecondes. Dans mon code actuel, chaque image est analysée, mais ce fait n'est pas pertinent pour mettre en évidence l'erreur.

Vérifier la position actuelle et la position en millisecondes de VideoCapture après la lecture des trames donne des valeurs correctes. Ainsi, VideoCapturepense il est à la bonne position - mais ce n'est pas le cas. L'enregistrement d'une image du dernier cadre lu révèle que mon itération dépasse largement le temps de destination de plus de deux minutes.

Ce qui est encore plus étrange, c’est que si je règle manuellement la position en millisecondes de la capture avec VideoCapture.set à 10 secondes (la même valeur que VideoCapture.get revient après avoir lu les images) et que l’image est enregistrée, la vidéo est à (presque) la bonne position!

_ {Fichier vidéo de démonstration} _

Si vous souhaitez exécuter le programme MCVE, vous avez besoin du fichier vidéo demo.avi . Vous pouvez le télécharger ICI .

MCVE

Ce MCVE est soigneusement conçu et commenté. S'il vous plaît laissez un commentaire sous la question si quelque chose reste incertaine.

Si vous utilisez OpenCV 3, vous devez remplacer toutes les instances de cv2.cv.CV_ par cv2.. (Le problème se produit dans les deux versions pour moi.)

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)

Sortie MCVE

Les instructions print produisent la sortie suivante.

version cv2 = 2.4.9.1
attributs initiaux: fps = 100.0, pos_msec = 0.0, pos_frames = 0.0
attributs après lecture: pos_msec = 10010.0, pos_frames = 1001.0
attributs après la définition manuelle de msec pos: pos_msec = 10010.0, pos_frames = 1001.0

Comme vous pouvez le constater, toutes les propriétés ont les valeurs attendues.

imwrite enregistre les images suivantes.

first_frame.png  first_frame.png

after_iteration.png  after_iteration.png

after_setting.png  after_setting.png

Vous pouvez voir le problème dans la deuxième image. La cible de 9:26:15 (horloge en temps réel sur l'image) manque plus de deux minutes. Le réglage manuel de l'heure cible (troisième image) règle la vidéo sur (presque) la position correcte. 

Qu'est-ce que je fais mal et comment puis-je résoudre le problème?

_ {Essayé jusqu'à présent} _

cv2 2.4.9.1 @ Ubuntu 16.04
cv2 2.4.13 @ Scientific Linux 7.3 (trois ordinateurs)
cv2 3.1.0 @ Scientific Linux 7.3 (trois ordinateurs)

Création de la capture avec 

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

et

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

sous OpenCV 3 (la version 2 ne semble pas avoir l'argument apiPreference) . L'utilisation de cv2.CAP_GSTREAMER prend extrêmement longtemps (environ 2 à 3 minutes pour exécuter MCVE), mais les deux préférences de l'API produisent les mêmes images incorrectes.

Lorsque vous utilisez ffmpeg directement pour lire des cadres (à créditer this tutorial), les images de sortie correctes sont générées.

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()

Cela produit le résultat correct! (Horodatage correct 9:26:15)

frame_1001_ffmpeg_only.png:  frame_1001_ffmpeg_only.png

Information additionnelle

Dans les commentaires, on m'a demandé mon fichier cvconfig.h. Je ne semble avoir ce fichier pour CV2 version 3.1.0 sous /opt/opencv/3.1.0/include/opencv2/cvconfig.h

ICI est un collage de ce fichier.

Au cas où cela aiderait, j'ai pu extraire les informations vidéo suivantes avec VideoCapture.get.

luminosité 0.0
contraste 0.0
convert_rgb 0.0
exposition 0.0
format 0.0
fourcc 1684633187.0
100.0 FPS
nombre de cadres 18000.0
frame_height 593.0
frame_width 792.0
gagner 0.0
teinte 0.0
mode 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
rectification 0.0
saturation 0.0

9
timgeb

Les données de votre fichier vidéo ne contiennent que 1313 images non dupliquées (c'est-à-dire entre 7 et 8 images par seconde):

$ 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        # !!!

La conversion du fichier avi avec ffmpeg signale 16697 images en double (pour une raison quelconque, 10 images supplémentaires sont ajoutées et 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, donc la vidéo convertie (demo.mp4) est dépourvue du problème être discuté, c’est-à-dire que OpenCV le traite correctement.

Dans ce cas, les images en double ne sont pas physiquement présentes dans le fichier avi, mais chaque image en double est représentée par une instruction permettant de répéter l'image précédente. Ceci peut être vérifié comme suit:

$ 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
...

Dans le journal ci-dessus, les images avec les données réelles sont représentées par les lignes commençant par "[avi @ 0xHHHHHHHHHHH]". Les messages "video: delay=xxxxx A-V=yyyyy" indiquent que la dernière image doit être affichée pendant xxxxx secondes supplémentaires.

cv2.VideoCapture() ignore ces images en double, en ne lisant que les images contenant des données réelles. Voici le code correspondant correspondant (quoique légèrement modifié) de la branche 2.4 de opencv (remarque, BTW, qui est utilisé sous ffmpeg, que j'ai vérifié en exécutant python sous gdb et en définissant un point d'arrêt sur 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

En un mot: je reproduis votre problème sur une machine Ubuntu 12.04 avec OpenCV 2.4.13, remarque que le codec utilisé dans votre vidéo (FourCC CVID) semble être assez ancien (selon ce post post de 2011), et Après avoir converti la vidéo en codec MJPG (alias M-JPEG ou Motion JPEG), votre MCVE a fonctionné. Bien sûr, Leon (ou d’autres) peut publier un correctif pour OpenCV, ce qui peut être la meilleure solution pour votre cas.

J'ai d'abord essayé la conversion en utilisant

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

et

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

(les deux aussi sur une boîte 16.04). Fait intéressant, les deux ont produit des vidéos "cassées". Par exemple, lorsque vous passez à l'image 1000 en utilisant Avidemux, il n'y a pas d'horloge en temps réel! En outre, les vidéos converties ne représentent qu'environ 1/6 de la taille d'origine, ce qui est étrange, car M-JPEG est une compression très simple. (Chaque image est compressée JPEG indépendamment.)

Utiliser Avidemux pour convertir demo.avi en M-JPEG a généré une vidéo sur laquelle le MCVE fonctionnait. (J'ai utilisé l'interface graphique d'Avidemux pour la conversion.) La taille de la vidéo convertie est d'environ 3 fois la taille d'origine. Bien sûr, il est également possible de faire l’enregistrement original en utilisant un codec mieux supporté sous Linux. Si vous devez accéder à des images spécifiques de la vidéo dans votre application, M-JPEG peut être la meilleure option. Sinon, H.264 compresse beaucoup mieux. D'après mon expérience, les deux logiciels sont bien pris en charge et les seuls codes que j'ai vus sont appliqués directement sur les webcams (H.264 uniquement sur les modèles haut de gamme).

1
Ulrich Stern

Comme tu dis : 

Lorsque vous utilisez ffmpeg directement pour lire des cadres (à noter dans ce tutoriel), les images de sortie correctes sont produites.

Est-ce normal parce que vous définissez un framesize = resolution[0]*resolution[1]*3 

puis le réutiliser quand on lit: pipe.stdout.read(framesize)

Donc, à mon avis, vous devez mettre à jour chacun: 

_, frame = cap.read()

à

_, frame = cap.read(framesize)

En supposant que la résolution soit identique, la version finale du code sera:

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