ffmpeg-python icon indicating copy to clipboard operation
ffmpeg-python copied to clipboard

Selecting every Nth frame

Open ccapizzano opened this issue 2 years ago • 5 comments

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 the not(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!

ccapizzano avatar Oct 18 '23 01:10 ccapizzano

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

horsto avatar Jan 22 '24 22:01 horsto

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

mattia-lecci avatar Jan 23 '24 07:01 mattia-lecci

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

horsto avatar Jan 23 '24 10:01 horsto

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:[email protected]: 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:[email protected]: boxborderw=8, drawtext=fontfile=/Windows/Fonts/courbd.ttf: text=' + sub_dir + ': x=w-tw:y=0: fontcolor=white: fontsize=40: box=1:[email protected]: 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)

ccapizzano avatar Jan 26 '24 14:01 ccapizzano

aha, great that it worked!

horsto avatar Jan 26 '24 14:01 horsto