Image Animation in Python with OpenCV¶
This Python script demonstrates an approach to creating video content for the Pausch Bridge using a single image as a source. The image is sequenced out row by row from top to bottom at a rate determined by the ‘tempo’ setting. Each row is treated as a keyframe and the video smoothly cross-fades between each row. The default setting of 60 BPM emits one row per second.
The source image must be 57 pixels wide, corresponding to the pier sections of the bridge. This is expanded to the standard 228 x 8 video mapping size which covers all the lighting elements. The default (preferred) output format is “PNG within AVI” which stores each frame losslessly without any compression artifacts. Other formats may introduce some slight color bleed due to compression artifacts at the crisp pixel block boundaries.
The script uses the Python bindings for the OpenCV library. These include the ffmpeg video codecs used to generate the output.
Installation Requirements¶
The code requires a working installation of Python 3 with OpenCV and its dependency NumPy. For suggestions on setting up your system please see Python 3 Installation.
Usage¶
This script is intended to be run from a command line. It requires an input image exactly 57 pixels wide. This is typically a lossless format like PNG or TIFF.
Run without options, it will read input from timeline.png and write output to animation.avi with a key frame tempo of 60:
python3 pb_animate_image
To see all available command-line options:
python3 pb_animate_image.py --help
A more full use of options might look as follows:
python3 pb_animate_image.py --input myshow.png --tempo 90 --verbose myshow.avi
Full Code¶
1#!/usr/bin/env python3
2# pb_animate_image.py
3
4# This script converts an image into a Pausch Bridge video file by sequencing
5# out each row from top to bottom. The source image must be 57 pixels wide.
6# Each row is treated as a key frame spaced in time at the 'tempo' rate, with
7# the video smoothly cross-fading between each row.
8
9# This script assumes the availability of the OpenCV and numpy libraries. A
10# recommended method for installing these in Python 3 follows:
11#
12# pip3 install opencv-contrib-python
13#
14# General OpenCV information: https://opencv.org/
15# General NumPy information: https://numpy.org/
16
17#================================================================
18# Import standard Python modules.
19import argparse
20
21# Import the numpy and OpenCV modules.
22import numpy as np
23import cv2 as cv
24
25#================================================================
26# Define the video properties using the canonical video format for the Pausch
27# Bridge lighting system.
28frame_rate = 30
29frame_width = 228
30frame_height = 8
31
32# Specify a format code and file format. The exact combinations of codec and
33# file formats available are different for each platform.
34codec_code = cv.VideoWriter.fourcc(*'png ') # PNG images, lossless, clean block edges
35file_extension = 'avi'
36
37#================================================================
38# Generate successive frames of the video sequence.
39
40def frame_generator(verbose, source, tempo):
41 count = 0 # count of generated frames
42 frame_time = 0.0 # time stamp for generated frame in seconds
43 keyframe_phase = 0.0 # unit phase for the cross-fade, cycles over 0 to 1
44
45 frame_interval = 1.0 / frame_rate # seconds between video frames
46 keyframe_interval = 60.0 / tempo # seconds between key frames
47 keyframe_rate = 1.0 / (frame_rate * keyframe_interval) # phase / frame
48
49 source_rows = source.shape[0]
50 source_cols = source.shape[1]
51
52 # Use the first two rows as the first keyframes.
53 row0 = source[0:1, :, :]
54 row1 = source[1:2, :, :]
55 frame0 = cv.resize(row0, None, fx=4, fy=8, interpolation=cv.INTER_NEAREST)
56 frame1 = cv.resize(row1, None, fx=4, fy=8, interpolation=cv.INTER_NEAREST)
57 next_scanline = 2
58
59 while True:
60 # Cross-fade between successive key frames at the given tempo. This will
61 # return a new frame of integer pixels.
62 frame = cv.addWeighted(frame0, (1.0 - keyframe_phase), frame1, keyframe_phase, 0.0)
63
64 # Return the frame and advance the generator state.
65 yield frame
66 count += 1
67 frame_time += frame_interval
68
69 # Advance the cross-fade phase.
70 keyframe_phase += keyframe_rate
71
72 # Once the second keyframe is reached, generate the successor and reset the fade.
73 if keyframe_phase > 1.0:
74 keyframe_phase -= 1.0
75
76 # Once all rows have been consumed, start returning a null result.
77 if next_scanline >= source_rows:
78 if verbose:
79 print(f"Generated {count} frames.")
80 while True:
81 yield None
82 else:
83 frame0 = frame1
84 row1 = source[next_scanline:next_scanline+1, :, :]
85 frame1 = cv.resize(row1, None, fx=4, fy=8, interpolation=cv.INTER_NEAREST)
86 next_scanline += 1
87
88
89#================================================================
90# Write a video file in the default format.
91
92def write_video_file(filename, verbose, *args):
93
94 # Open the writer with a path, format, frame rate, and size.
95 out = cv.VideoWriter(filename, codec_code, frame_rate, (frame_width, frame_height))
96
97 if verbose:
98 print(f"Open file {filename} for output.")
99
100 # Set up the frame generator.
101 frame_sequence = frame_generator(verbose, *args)
102
103 # Synthesize some frames and write them to the stream.
104 while True:
105 next_frame = next(frame_sequence)
106 if next_frame is not None:
107 out.write(next_frame)
108 else:
109 break
110
111 # Release everything when done.
112 out.release()
113
114#================================================================
115# Main script follows.
116if __name__ == "__main__":
117 parser = argparse.ArgumentParser(description = """Convert an image row by row to video frames, smoothly interpolating between rows. \
118The source image must be exactly 57 pixels wide.""")
119 parser.add_argument( '-v', '--verbose', action='store_true', help='Enable more detailed output.' )
120 parser.add_argument( '--tempo', type=float, default=60.0, help='Tempo of key frames (image rows) in beats per minute (default: %(default)s)')
121 parser.add_argument( '--input', type=str, default='timeline.png', help='Path of input image (default: %(default)s.')
122 parser.add_argument( 'videopath', default='animation.avi', nargs='?', help='Path of output video file (default: %(default)s).')
123
124 args = parser.parse_args()
125 source = cv.imread(args.input)
126 if args.verbose:
127 print(f"Source image size: {source.shape}")
128
129 write_video_file(args.videopath, args.verbose, source, args.tempo)