kkroening / ffmpeg-python

Python bindings for FFmpeg - with complex filtering support
Apache License 2.0
10.04k stars 889 forks source link

Selecting every Nth frame #800

Open ccapizzano opened 1 year ago

ccapizzano commented 1 year ago

Hello,

I've been able to the the ffmpeg library to import a video and extract a sequence of JPG images (1 frame every 300 frames) using the below command.

ffmpeg -i input.mov -vf "select=not(mod(n\,300))" -vsync vfr -q:v 2 img_%03d.jpg

However, I am unable to duplicate this effort when using the ffmpeg.filter function in the ffmpeg-python module. When I run

ffmpeg.input("example.mov).filter("select","not(mod(n\,300))").output('image_%03d.jpg')

Python outputs the following error:

Traceback (most recent call last): File "<string>", line 32, in <module> File "C:\Users\CONNOR~1\MINICO~1\envs\general\Lib\site-packages\ffmpeg\_run.py", line 325, in run raise Error('ffmpeg', out, err) ffmpeg._run.Error: ffmpeg error (see stderr output for detail)

Removing the \' in thenot(mod(n\,300))` statement allows the code to execute, but it outputs every frame in the video (30 fps * 30 seconds = 900 frames).

Does anyone have any suggestions or can they provide an example where I can use the select filter in ffmpeg-python to extract images at specific intervals?

Thanks in advance!

horsto commented 9 months ago

I am running into the same issue - have you found a solution for this?

mattia-lecci commented 9 months ago

Have you checked this out? I would try using \\, or using python's raw string r"not(mod(n\,300))". I didn't test this, it's just a test for you to try :)

horsto commented 9 months ago

Thanks,! Yes, I tried that. Getting rid of the backslash altogether actually does seem to work. I am copy+pasting my current def for reference below. I am not sure I understand all the details. The vsync='vfr' is / seems to be important to dial down the frame rate / not replicate frames that have been skipped.

def trim_video(input_file, output_file, start, end, to_skip=100):
    '''
    Trim an input video file to start / end 
    and take only a subset of the original frames 

    '''
    filter_string = f"not(mod(n,{to_skip}))"
    pipe = (
        ffmpeg
        .input(input_file.as_posix())
        .trim(start=start, end=end)
        .setpts('PTS-STARTPTS')
        .filter('select', filter_string)
        .output(output_file.as_posix(), vsync='vfr')
        .global_args('-loglevel', 'error')
        .global_args('-y') 
        .run()
    )
ccapizzano commented 9 months ago

I actually solved the issue by accessing ffmpeg using Python and the subprocess.call function. Although not as streamlined, I can now loop through my videos and add appropriate timestamps, frame numbers, and titles to each image extracted at a specific interval. Using the cv2 module, I read the frame rate of each video and use it with a user-defined time duration to select out frames at specific intervals.

in_path = sub_path
out_path = video_path + "/" + sub_dir + "_%03d.jpg"
ffmpeg_path = 'C:/ffmpeg/bin/ffmpeg.exe'

fps = fps
seconds = 10
framenumber = str(fps*seconds)

cmd = ffmpeg_path + 
' -i ' + 
in_path + 
' -vf "drawtext=fontfile=/Windows/Fonts/courbd.ttf:fontsize=40:fontcolor=white:box=1:boxcolor=black@0.4: boxborderw=8:x=10:y=h-th-10:timecode=\'00\:00\:00\:00:rate=29.97\', drawtext=fontfile=/Windows/Fonts/courbd.ttf: text=\'Frame \: %{eif\:n\:d\:6}\': start_number=0: x=w-tw:y=h-th: fontcolor=white: fontsize=40: box=1:boxcolor=black@0.4: boxborderw=8, drawtext=fontfile=/Windows/Fonts/courbd.ttf: text=' + sub_dir + ': x=w-tw:y=0: fontcolor=white: fontsize=40: box=1:boxcolor=black@0.4: boxborderw=8, select=not(mod(n\,'   + framenumber + '))" -vsync vfr -q:v 2 ' + 
out_path

print(cmd)

subprocess.call(args=cmd, shell=True, stdout=sys.stdout, stderr=sys.stderr)
horsto commented 9 months ago

aha, great that it worked!