it-swarm.com.de

OpenCV: Durch das Lesen von Frames aus VideoCapture wird das Video an bizarr falsche Position verschoben

(Ich werde auf diese Frage eine Prämie von 500 Reputationen aufbringen, sobald sie berechtigt ist - es sei denn, die Frage wurde geschlossen.)

Problem in einem Satz

Durch das Lesen von Frames aus einer VideoCapture wird das Video viel weiter als vorgesehen vorgerückt.

Erklärung

Ich muss Frames von einem Video mit 100 Bildern pro Sekunde (gemäß cv2 und VLC media player) zwischen bestimmten Zeitintervallen lesen und analysieren. Im folgenden minimalen Beispiel versuche ich, alle Bilder für die ersten zehn Sekunden eines dreiminütigen Videos zu lesen.

Ich erstelle ein cv2.VideoCapture-Objekt, von dem aus ich Frames lese, bis die gewünschte Position in Millisekunden erreicht ist. In meinem eigentlichen Code wird jeder Frame analysiert, diese Tatsache ist jedoch irrelevant, um den Fehler darzustellen.

Wenn Sie nach dem Lesen der Frames die aktuelle Position und die Millisekundenposition der VideoCapture überprüfen, erhalten Sie korrekte Werte. Die VideoCapturedenkt, dass sie sich an der richtigen Position befindet - dies ist jedoch nicht der Fall. Wenn Sie ein Bild des letzten gelesenen Frames speichern, wird deutlich, dass meine Iteration die Zielzeit um mehr als zwei Minuten überschreitet.

Noch bizarrer ist, dass, wenn ich die Millisekundenposition des Captures mit VideoCapture.set auf 10 Sekunden eingestellt habe (derselbe Wert VideoCapture.get nach dem Lesen der Frames zurückgegeben wird) und ein Bild gespeichert wird, das Video an (fast) der richtigen Position ist!

Demo-Videodatei

Wenn Sie den MCVE ausführen möchten, benötigen Sie die Videodatei demo.avi . Sie können sie HERE herunterladen.

MCVE

Dieser MCVE wurde sorgfältig ausgearbeitet und kommentiert. Bitte hinterlassen Sie einen Kommentar zu der Frage, wenn etwas unklar bleibt.

Wenn Sie OpenCV 3 verwenden, müssen Sie alle Instanzen von cv2.cv.CV_ durch cv2. ersetzen. (Das Problem tritt bei mir in beiden Versionen auf.)

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-Ausgang

Die Anweisungen print erzeugen die folgende Ausgabe.

cv2-version = 2.4.9.1
Anfangsattribute: fps = 100.0, pos_msec = 0.0, pos_frames = 0.0
Attribute nach dem Lesen: pos_msec = 10010.0, pos_frames = 1001.0
Attribute nach dem manuellen Setzen von msec pos: pos_msec = 10010.0, pos_frames = 1001.0

Wie Sie sehen, haben alle Eigenschaften die erwarteten Werte.

imwrite speichert die folgenden Bilder.

first_frame.png first_frame.png

after_iteration.png after_iteration.png

after_setting.png after_setting.png

Sie können das Problem im zweiten Bild sehen. Das Ziel von 9:26:15 (Echtzeituhr im Bild) wird um mehr als zwei Minuten verfehlt. Durch manuelles Einstellen der Zielzeit (drittes Bild) wird das Video auf (fast) die richtige Position eingestellt. 

Was mache ich falsch und wie kann ich das beheben?

Bisher ausprobiert

cv2 2.4.9.1 @ Ubuntu 16.04
cv2 2.4.13 @ Scientific Linux 7.3 (drei Computer)
cv2 3.1.0 @ Scientific Linux 7.3 (drei Computer)

Das Capture erstellen mit 

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

und

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

in OpenCV 3 (Version 2 scheint das Argument apiPreference nicht zu haben) . Die Verwendung von cv2.CAP_GSTREAMER dauert extrem lange (ca. 2-3 Minuten, um den MCVE auszuführen), aber beide API-Voreinstellungen erzeugen dieselben falschen Bilder.

Wenn Sie ffmpeg direkt zum Lesen von Frames verwenden (Gutschrift für this Tutorial), werden die korrekten Ausgabebilder erzeugt.

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

Das ergibt das richtige Ergebnis! (Richtiger Zeitstempel 9:26:15)

frame_1001_ffmpeg_only.png: frame_1001_ffmpeg_only.png

Zusätzliche Information

In den Kommentaren wurde ich nach meiner cvconfig.h-Datei gefragt. Ich scheine diese Datei nur für cv2 Version 3.1.0 unter /opt/opencv/3.1.0/include/opencv2/cvconfig.h zu haben. 

HERE ist eine Einfügung dieser Datei.

Falls es hilft, konnte ich die folgenden Videoinformationen mit VideoCapture.get extrahieren.

helligkeit 0,0
dagegen 0,0
convert_rgb 0.0
Belichtung 0,0
Format 0.0
fourcc 1684633187.0
fps 100,0
frame_count 18000.0
Rahmenhöhe 593.0
Rahmenbreite 792.0
Gewinn von 0,0
Farbton 0,0
Modus 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
Rektifikation 0,0
Sättigung 0,0

9
timgeb

Ihre Videodateidaten enthalten nur 1313 nicht duplizierte Frames (d. H. Zwischen 7 und 8 Frames pro Sekunde):

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

Konvertieren der avi-Datei mit ffmpeg-Berichten 16697 doppelten Frames (aus irgendeinem Grund werden 10 zusätzliche Frames hinzugefügt und 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
#                                                                   ^^^^^^^^^
...

Übrigens, das konvertierte Video (demo.mp4) hat das Problem, dass besprochen wird, dass OpenCV es richtig verarbeitet.

In diesem Fall sind die doppelten Frames in der avi-Datei nicht physisch vorhanden. Stattdessen wird jeder doppelte Frame durch eine Anweisung zum Wiederholen des vorherigen Frames dargestellt. Dies kann wie folgt überprüft werden:

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

Im obigen Protokoll werden Rahmen mit tatsächlichen Daten durch die Zeilen dargestellt, die mit "[avi @ 0xHHHHHHHHHHH]" beginnen. Die "video: delay=xxxxx A-V=yyyyy" -Meldungen zeigen an, dass das letzte Bild xxxxx für weitere Sekunden angezeigt werden muss.

cv2.VideoCapture() überspringt solche doppelten Frames und liest nur Frames, die echte Daten enthalten. Hier ist der entsprechende (wenn auch leicht bearbeitete) Code aus dem 2.4-Zweig von opencv (Hinweis, BTW, dass unter ffmpeg verwendet wird, was ich durch Ausführen von Python unter gdb und Setzen eines Haltepunkts auf CvCapture_FFMPEG::grabFrame verifiziert hat):

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

Kurz gesagt: Ich habe Ihr Problem auf einem Ubuntu 12.04-Computer mit OpenCV 2.4.13 reproduziert. Dabei ist mir aufgefallen, dass der in Ihrem Video verwendete Codec (FourCC CVID) recht alt ist (laut diesem post von 2011) und Nach dem Konvertieren des Videos in ein Codec-MJPG (auch als M-JPEG oder Motion JPEG bezeichnet) ist Ihr MCVE erfolgreich. Natürlich können Leon (oder andere) einen Fix für OpenCV bereitstellen, was möglicherweise die bessere Lösung für Ihren Fall ist.

Ich habe die Konvertierung zunächst mit versucht

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

und

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

(beide auch auf einer 16.04 Box). Interessanterweise produzierten beide "kaputte" Videos. Wenn zum Beispiel mit Avidemux auf Frame 1000 gesprungen wird, gibt es keine Echtzeituhr! Außerdem waren die konvertierten Videos nur etwa 1/6 der Originalgröße, was merkwürdig ist, da M-JPEG eine sehr einfache Komprimierung ist. (Jeder Frame ist unabhängig JPEG-komprimiert.)

Durch die Konvertierung von demo.avi in M-JPEG mit Avidemux wurde ein Video erstellt, an dem der MCVE gearbeitet hat. (Ich habe die Avidemux-Benutzeroberfläche für die Konvertierung verwendet.) Die Größe des konvertierten Videos beträgt etwa das 3-Fache der Originalgröße. Natürlich ist es auch möglich, die Originalaufnahme mit einem Codec auszuführen, der unter Linux besser unterstützt wird. Wenn Sie im Video Ihrer Anwendung zu bestimmten Frames springen möchten, ist M-JPEG möglicherweise die beste Option. Ansonsten komprimiert H.264 wesentlich besser. Beide sind nach meiner Erfahrung gut unterstützt und die einzigen Codes, die ich gesehen habe, wurden direkt auf Webcams implementiert (H.264 nur bei High-End-Codes).

1
Ulrich Stern

Wie du gesagt hast : 

Wenn Sie ffmpeg direkt zum Lesen von Frames verwenden (Gutschrift für dieses Lernprogramm), werden die richtigen Ausgabebilder erzeugt.

Ist es normal, weil Sie einen framesize = resolution[0]*resolution[1]*3 definieren 

dann wiederverwenden, wenn gelesen wird: pipe.stdout.read(framesize)

Meiner Meinung nach müssen Sie also jedes Mal aktualisieren: 

_, frame = cap.read()

zu

_, frame = cap.read(framesize)

Unter der Annahme, dass die Auflösung identisch ist, lautet die endgültige Codeversion:

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