Videos aus Bildern erstellen

Dieses Kapitel unseres Python-Kurses beschäftigt sich mit Filmen und Bildern. Sie werden lernen wie Sie aus Bildern Filme erstellen können.

Ein Video aus mehreren Bildern

Sie haben also eine Menge Bilder und möchten daraus ein Video erstellen. Und zwar so, dass jedes Bild eine bestimmte Zeit lang gezeigt wird. Wir nehmen an, dass alle Ihre Bilder, - das können Fotos sein, die Sie in Ihrem letzten Urlaub gemacht haben oder Gemälde, die Sie zu einem bestimmten Zeitpunkt gemalt haben - in einem Verzeichnis sind. Wir gehen nun davon aus, dass alle Ihre Bilder die gleiche Größe haben, falls nicht, werden Sie auch lernen, wie Sie die Größe dieser Bilder ändern können. Für unsere Zwecke könnte es gut sein, dass die Bildnamen einem speziellen Benennungsschema folgen, d.h. sie beginnen alle mit dem gleichen Präfix (wie z.B. 'pic_') gefolgt von einer Zahl (z.B. '001', '002', '003', ...) in aufsteigender Reihenfolge. Der letzte Teil des Namens ist natürlich das Suffix, das dem eigentlichen Bildformat entsprechen muss.

Sie können alle benötigten Bilder und die erstellten Videos für den persönlichen Gebrauch herunterladen (das Copyright liegt bei Bernd Klein) unter python-courses: Material

Zunächst stellen wir eine Funktion 'seq_renaming' bereit, die alle Bilder in einem Ordner umbenennt nach dem zuvor beschriebenen Namensschema umbenennt:

In [1]:
import glob
import os

def seq_renaming(folder='.', prefix='pic_', start=0, num_length=3):
    count = start
    folder += '/'
    for fname in glob.glob(folder + '*.jpg'):
        suffix = fname[fname.rfind('.'):]
        outstr = f"{folder}/{prefix}{count:0{num_length}d}{suffix}"
        count += 1
        os.rename(fname, outstr)
        
seq_renaming('flower_images')

Um unsere neu erstellte Funktion zu testen und zu demonstrieren, wie sie funktioniert, erstellen wir ein Testverzeichnis mit leeren Dateien, die Bilder simulieren:

In [2]:
import shutil
target_dir = 'test_dir'

if os.path.exists(target_dir):
    # delete the existing directory
    shutil.rmtree(target_dir)
# Create target_dir, because it doesn't exist so far
os.makedirs(target_dir)
    
names = ['apples', 'oranges', 'bananas', 'pears']
for name in names:
    open(f'{target_dir}/{name}.jpg', 'w').write(' ')
    
print('Before renaming: ', os.listdir(target_dir))

# Let's rename the image names:
seq_renaming(target_dir)
print('After renaming: ', os.listdir(target_dir))
Before renaming:  ['apples.jpg', 'oranges.jpg', 'bananas.jpg', 'pears.jpg']
After renaming:  ['pic_000.jpg', 'pic_001.jpg', 'pic_002.jpg', 'pic_003.jpg']

Unser erstes Video wird mit Hilfe der Bilder im Ordner flower_images erstellt. Schauen wir uns eines der Bilder an.

In [2]:
import matplotlib.pyplot as plt
import random
import numpy as np
import cv2

image = plt.imread("im2video/images/20210617_201910.jpg")
plt.imshow(image)
#cv2.imshow(image)
Out[2]:
<matplotlib.image.AxesImage at 0x7fa6f5bd8210>

Das Bild ist in Radolfzell am Bodensee aufgenommen und im Hintergrund sind die Berge Hohentwiel und Hohenstoffeln zu sehen.

In [3]:
import matplotlib.pyplot as plt
import random
import numpy as np
import os
import cv2
import glob

def get_frame_size(image_path):
    """ Reads an image and calculates
    the width and length of the images,
    which will be returned """
    frame = cv2.imread(image_path)
    height, width, layers = frame.shape
    frame_size = (width, height)
    return frame_size


def video_from_images(folder='.', 
                      video_name = 'video.avi',
                      suffix='png',
                      prefix='pic_',
                      reverse=False,
                      length_of_video_in_seconds=None,
                      codec = cv2.VideoWriter_fourcc(*'DIVX')):
    """ Die Funktion erstellt ein Video aus allen Bildern mit
    dem Suffix 'suffix' (Standard ist 'png') und dem Präfix 
    'prefix'(Standard ist 'pic_') im Ordner 'folder'. 
    Wenn 'length_of_video_in_seconds' auf None gesetzt ist, 
    ist dies die Anzahl der Bilder in Sekunden. 
    Wenn ein positiver  Wert angegeben wird, handelt es sich 
    um die Länge des Videos in Sekunden. Die Funktion geht davon
    aus, dass die 'shape' des ersten Bildes gleich für alle 
    anderen Bilder ist. Ist dies nicht der Fall, so wird eine 
    Warnung ausgegeben und die Größe wird entsprechend angepasst! """
    
    images = []
    for fname in glob.glob(f'{folder}/{prefix}*{suffix}'):    
        images.append(fname)
    images.sort(reverse=reverse)
    if length_of_video_in_seconds is None:
        # jedes Bild wird für eine Sekunde angezeigt
        length_of_video_in_seconds = len(images)
    
    # Es wird die Anzahl der Frames pro Sekunde berechnet:
    frames_per_second = len(images) / length_of_video_in_seconds
    frame_size = get_frame_size(images[0])

    video = cv2.VideoWriter(video_name, 
                            codec, 
                            frames_per_second, 
                            frame_size)  
    
    for image in images:
        im = cv2.imread(image)
        height, width, layers = im.shape
        if (width, height) != frame_size:
            print(f'Warning: {image} resized from {(width, height)} to {frame_size}')
            im = cv2.resize(im, frame_size)
        video.write(im)
    cv2.destroyAllWindows()
    video.release()
    
video_from_images('im2video/flower_images', 'flowers.avi', 'jpg')
Warning: im2video/flower_images/pic_004.jpg resized from (3993, 1860) to (4000, 1800)
Warning: im2video/flower_images/pic_005.jpg resized from (4000, 2337) to (4000, 1800)

Das Ergebnis ist in flowers.avi zu sehen!

Ein Video aus einem Bild

Wir verwenden nun ein Bild als Grundlage, um eine Bildfolge zu erstellen. Wir tun dies, indem wir die Farben eines Teils (einer Tile / Kachel) des Bildes ändern. Wir können uns das Bild als ein Schachbrett mit n Zeilen und m Spalten vorstellen. Die Funktion random_tile kann verwendet werden, um die obere linke und untere rechte Ecke einer zufällig ausgewählten Kachel zu liefern. Wir übergeben das Bild an den Parameter 'img' und die Anzahl der Zeilen und Spalten werden durch height_factor, width_factor definiert:

In [4]:
import random 

def random_tile(img, height_factor, width_factor):
    """ returns the top left and bottom right corner
    of a random tile in an image. The tile sizes are
    determined by height_factor, width_factor, the division
    factors """
    height, width, colours = img.shape
    tiles = height_factor, width_factor
    tile_height, tile_width = height / height_factor, width / width_factor
    tile_upper_left_row = int(round(random.randint(0, height_factor) * tile_height, 0))
    tile_upper_left_column = int(round(random.randint(0, width_factor) * tile_width, 0))
    top_left = tile_upper_left_row, tile_upper_left_column
    bottom_right_row = int(round(tile_upper_left_row + tile_height, 0))
    bottom_right_column = int(round(tile_upper_left_column + tile_width, 0))
    bottom_right = bottom_right_row, bottom_right_column
    return top_left, bottom_right

Im folgenden Code lesen wir ein Bild ein und rufen die Funktion random_tile einige Male auf:

In [5]:
import matplotlib.pyplot as plt
import random
import numpy as np
imag = cv2.imread("im2video/images/20210617_201910.jpg") 
print(f'{imag.shape=}')
for i in range(4):
    tile = random_tile(imag, 4, 5)
    print(tile)
imag.shape=(873, 1941, 3)
((0, 1553), (218, 1941))
((655, 1941), (873, 2329))
((218, 388), (436, 776))
((436, 1553), (654, 1941))

Die Funktion 'colorize_tile' kann verwendet werden, um die Farbe einer Kachel zufällig zu ändern. Dies geschieht durch Hinzufügen eines zufälligen Farbwert zu jedem Pixel der Kachel hinzugefügt wird.

In [6]:
def colorize_tile(imag, top_left, bottom_right, max_col_val):
    color = np.random.randint(0, max_col_val, (3,))
    r1, r2 = top_left[0], bottom_right[0]
    c1, c2 = top_left[1], bottom_right[1]
    imag[r1:r2, c1:c2] = imag[r1:r2, c1:c2] + color

Wir demonstrieren die Arbeitsweise von colorize_tile im folgenden Code. Schauen wir uns zunächst die Bilder an. Wir verwenden plt.imread anstelle von cv2.imread, weil cv2.imshow ein externes Fenster im Jupyter-Notebook öffnet, das wir zur Erstellung dieser Website verwenden. Außerdem sind die Bilder von cv2.imread im BGR-Format, während plt.imread RGB-Bilder liefert. Das bedeutet, dass sie nicht kompatibel sind.

In [8]:
import matplotlib.pyplot as plt
import random
import numpy as np
imag = plt.imread("im2video/images/20230215_161843.jpg") 
plt.imshow(imag)
Out[8]:
<matplotlib.image.AxesImage at 0x7fa6f5147190>

Lassen Sie uns jetzt mit colorize_tile experimentieren:

In [9]:
imag = cv2.imread("im2video/images/20230215_161843.jpg") 
for i in range(6):
    top_left, bottom_right = random_tile(imag, 4, 5)
    colorize_tile(imag, top_left, bottom_right, max_col_val=100)

# Wir speichern das Ergebnis in 'experiments.png'
cv2.imwrite('experiments.png', imag)

# Wegen Einschränkungen des Jupyter-Noebook 
# müssen wir plt zum Lesen und Schreiben verwenden:
plt.imshow(plt.imread('experiments.png'))
Out[9]:
<matplotlib.image.AxesImage at 0x7fa6f5340f50>

Wir werden nun 100 Bilder in einem Ordner mit dem Namen "video_pics" erstellen. Das Originalbild wird unter dem Namen pic_101.png in diesem Ordner gespeichert. Das Bild mit dem Namen "pic_001.png" ist das Bild mit den meisten geänderten Kacheln:

In [10]:
pics_dir='im2video/video_pics'
n = 100
im = cv2.imread("im2video/images/20210617_201910.jpg") 
fname = f"{pics_dir}/pic_{n+1:03d}.png"
cv2.imwrite(fname, im)
for i in range(n, 0, -1):
    top_left, bottom_right = random_tile(im, 5, 6)
    colorize_tile(im, top_left, bottom_right, 100)
    fname = f"{pics_dir}/pic_{i:03d}.png"
    cv2.imwrite(fname, im)

Schauen wir uns das endgültige Bild "pic_001.png" der erstellten Bilder an:

In [11]:
plt.imshow(plt.imread(f'{pics_dir}/pic_001.png'))
Out[11]:
<matplotlib.image.AxesImage at 0x7fa6f5237190>

Wir werden nun wieder unsere Funktion video_from_images verwenden, um aus diesen neu erstellten Bildern ein Video mit dem Namen 'rand_squares.avi' zu erstellen:

In [12]:
video_from_images(pics_dir, 
                  'random_squares.avi', 
                  'png',
                  length_of_video_in_seconds = 280)

Ein Video mit Kacheln (Tiles) aus mehreren Bildern

Wir werden nun Bilder erstellen, wobei wir ein Startbild verwenden und zufällig Kacheln durch die entsprechenden Kacheln anderer Bilder ersetzen. Die Funktion 'tile_from_image' ersetzt den Inhalt einer Kachel eines Bildes1 durch den Inhalt der entsprechenden Kachel eines anderen Bildes 'image2'. Beide Bilder müssen die gleiche Form haben:

In [14]:
def tile_from_image(image1, image2, top_left, bottom_right):
    """ Nimmt eine Kachel aus Bild2 und ersetzen den 
    entsprechenden Bereich in Bild1 """
    r1, r2 = top_left[0], bottom_right[0]
    c1, c2 = top_left[1], bottom_right[1]
    image1[r1:r2, c1:c2] = image2[r1:r2, c1:c2]

Im folgenden Code verwenden wir Bilder, die aus der Bodensee-Region stammen:

In [16]:
images_dir = 'im2video/bodensee_area'
target_dir = 'video_pics_bodensee'
if not os.path.exists(target_dir):
   # target_dir erstellen, da es bisher nicht existiert
   os.makedirs(target_dir)
n_of_images = 120
import random

images = []
for fname in glob.glob(f'{images_dir}/*.jpg'):    
    images.append(fname)

imags = [cv2.imread(fname) for fname in images]
start_image = imags[0]
for i in range(n_of_images, 0, -1):
    top_left, bottom_right = random_tile(start_image, 5, 6)
    image2 = random.choice(imags)
    tile_from_image(start_image, image2, top_left, bottom_right)
    fname = f"{target_dir}/pic_{i:03d}.png"
    cv2.imwrite(fname, start_image)
In [17]:
video_from_images('video_pics_bodensee', 
                 'bodensee.avi', 
                 'png',
                 length_of_video_in_seconds = 60)

Ich habe diese Technik verwendet, um ein Video für eine Musikkomposition von mir zu erstellen. Das Video ist bei YouTube zu finden:

Die Musik ist in beiden Videos die gleiche. Ein Musikstück für Streichorchester und Solocello. Die Tonspur habe ich mit dem Linux-Programm kdenlive hinzugefügt.