Videos mit beweglichen Objekten

Spiral like a Sun

Die Idee hinter diesem Kapitel unserer Python-Kurse ist es, einen Film aus einem oder mehreren Bildern zu erstellen, in dem ein oder mehrere Objekte auf statischen Bildern herumbewegt werden. Wir werden feste Objekte oder durchscheinende Wasserzeichenobjekte über Bilder bewegen. Wir haben ein Bild mit einer Wasserzeichenstruktur für unser Kapitel über Dekoratoren und Dekoration erstellt. Sie können es ganz oben auf der Seite sehen: Ein "kaufmännisches Und" (in der Python-Gemeinschaft besser bekannt als das Dekorator-Zeichen. Sie können auch einen Kurs finden, wie man Bilder mit Wasserzeichen wie dieses erstellt mit Python, Numpy, Scipy und Matplotlib in dem Kapitel Bildverarbeitungstechniken. Darin wird der gesamte Prozess der Erstellung unseres Dekorators und des Bildes mit dem Zeichen, d.h. der mit Wasserzeichen versehenen Bilder, erklärt. Es ist also eine gute Idee, diese Kapitel zu lesen, bevor man weitermacht.

Technische Anmerkung: opencv muss für die folgenden Python-Code-Beispiele installiert sein.

Interessante Objekte mit Matplotlib erstellen

Wir beginnen mit der Erstellung einiger interessanter Objekte, die wir in den Videos, die wir erstellen wollen, als bewegliche Objekte verwenden können. Wir beginnen mit der Erstellung eines Verzeichnisses, in dem die Objekte gespeichert werden:

In [1]:
import os
import shutil
target_dir = 'images4video'
watermarks_dir = f"{target_dir}/watermarks_dir"
if os.path.exists(watermarks_dir):
    # delete the existing directory
    shutil.rmtree(watermarks_dir)
# Create target_dir, because it doesn't exist so far
os.makedirs(watermarks_dir)

Wir verwenden im folgenden Code den Parameter bbox_inches="tight" in savefig. Wenn dieser Parameter auf "tight" gesetzt ist, versucht matplotlib, alle zusätzlichen Leerzeichen oder Padding an den Rändern der Abbildung zu entfernen, so dass nur der eigentliche Inhalt der Abbildung gespeichert wird. Dies kann nützlich sein, wenn man eine Abbildung mit minimalen Rändern oder Auffüllungen speichern will.

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

x = np.linspace(-2, 2, 1000)
y1 = np.sqrt(1-(abs(x)-1)**2)
y2 = -3 * np.sqrt(1-(abs(x)/2)**0.5)

fig, ax = plt.subplots()

ax.fill_between(x, y1, color='red')
ax.fill_between(x, y2, color='red')
ax.axis(xmin=-2.3, xmax=2.3)
ax.axis('off')
#ax.xlim([-2.5, 2.5])
txt2include = "python-kurs.eu"
ax.text(0, -0.4,
         txt2include, 
         fontsize=24, 
         fontweight='bold',
         color='orange', 
         horizontalalignment='center')

plt.savefig(f"{watermarks_dir}/heart.png", bbox_inches="tight")

Wir erzeugen nun ein Bild mit einer schwarz-weißen Spirale:

In [3]:
import numpy as np
import matplotlib.pyplot as plt

theta = np.radians(np.linspace(0, 360*5, 1000))
r = theta**2
x_2 = r*np.cos(theta)
y_2 = r*np.sin(theta)
plt.figure(figsize=[5, 5])
plt.gcf().set_size_inches(10, 10)
plt.plot(x_2, y_2, linewidth=50.5, color='black')
plt.axis('off')
plt.savefig(f"{watermarks_dir}/spiral.png", bbox_inches="tight")
plt.show()

Wie sieht es mit Farben aus? Eine andere Spirale, aber jetzt in Farben:

In [4]:
import matplotlib.pyplot as plt
import numpy as np

theta = np.arange(0, 8*np.pi, 0.1)
a, b = 1, 0.5

for dt in np.arange(0, 2*np.pi, np.pi/2.0):
    x = a*np.cos(theta + dt)*np.exp(b*theta)
    y = a*np.sin(theta + dt)*np.exp(b*theta)
    dt = dt + np.pi/4.0
    x2 = a*np.cos(theta + dt)*np.exp(b*theta)
    y2 = a*np.sin(theta + dt)*np.exp(b*theta)
    xf = np.concatenate((x, x2[::-1]))
    yf = np.concatenate((y, y2[::-1]))
    p1 = plt.fill(xf, yf)

plt.axis('equal')
plt.axis('off')
plt.tight_layout()
plt.savefig(f'{watermarks_dir}/coloured_spiral.png', bbox_inches="tight")

Größe eines Bildes

Die folgende Funktion gibt die Größe eines Bildes als Tupel (Breite, Höhe) zurück. opencv muss installiert sein, damit Sie das cv2-Modul verwenden können:

In [5]:
import cv2

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

get_frame_size(f"{watermarks_dir}/spiral.png")
Out[5]:
(794, 790)

Zufällige Kachel (tile) im Bild

Jetzt definieren wir eine Funktion random_tile, die die Koordinaten (obere linke Ecke und untere rechte Ecke) einer Kachel im Bild img zurückgibt. Die Größe dieser Kachel ist gleich der Größe des Objektes bzw. Wasserzeichenbildes, das der Funktion übergeben wird:

In [6]:
import random 

def random_tile(img, watermark_image):
    """ returns the top left and bottom right corner
    of a random tile in an image. The size correspondents to
    the size of the watermake image """
    height, width, colours = img.shape
    w_height, w_width, colours = watermark_image.shape
    tile_upper_left_row = int(round(random.randint(0, height - w_height), 0))
    tile_upper_left_column = int(round(random.randint(0, width - w_width), 0))
    top_left = tile_upper_left_row, tile_upper_left_column
    bottom_right_row = int(round(tile_upper_left_row + w_height, 0))
    bottom_right_column = int(round(tile_upper_left_column + w_width, 0))
    bottom_right = bottom_right_row, bottom_right_column
    return top_left, bottom_right

Wir werden cv2.imread verwenden, um ein Bild einzulesen, das wir in unseren Beispielen verwenden wollen. Wir könnten auch cv2.imshow verwenden, aber das öffnet ein externes Fenster im Jupyter-Notebook, das wir zum Erstellen dieser Website verwenden. Das ist der Grund, warum wir plt.imshow verwenden, um das Bild zu betrachten. Wir müssen die Farben spiegeln, bevor wir plt.imshow verwenden können, da wir mit cv2.imread ein BGR-Bild (blau, grün, rot) erhalten, während plt.imread RGB-Bilder liefert.

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

image = cv2.imread("images4video/paths/pic_004.jpg")
rgb_img = image[:, :, ::-1]  # BGR ---> RGB
#plt.axis('off')
plt.imshow(rgb_img)
Out[7]:
<matplotlib.image.AxesImage at 0x7fcaa45ed750>
In [8]:
spiral = cv2.imread(f"{watermarks_dir}/spiral.png")
rgb_spiral = spiral[:, :, ::-1]  # BGR ---> RGB
plt.imshow(rgb_spiral)
Out[8]:
<matplotlib.image.AxesImage at 0x7fcaa4455210>

Wir testen unsere Funktion random_tile mit diesen beiden Bildern:

In [9]:
top_left, bottom_right = random_tile(image, spiral)
print(f"{top_left=}, {bottom_right=}")
print(f"{spiral.shape=}, {image.shape=}")
top_left=(577, 2569), bottom_right=(1367, 3363)
spiral.shape=(790, 794, 3), image.shape=(1800, 4000, 3)

Es ist zu erkennen, dass die Abmessungen der zufälligen Kachel dem Wasserzeichenbild entsprechen:

In [10]:
height_of_tile = bottom_right[0] - top_left[0]
width_of_tile = bottom_right[1] - top_left[1]
print( (height_of_tile, width_of_tile) == spiral.shape[:2])
True

Schauen wir uns ein weiteres Bild an. Diesmal geht es in den Hafen von Konstanz am Bodensee:

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

image2 = cv2.imread(f"{target_dir}/bodensee_area/konstanz.jpg")
rgb_img2 = image2[:, :, ::-1]  # BGR ---> RGB
plt.imshow(rgb_img2)
Out[11]:
<matplotlib.image.AxesImage at 0x7fcaa44af150>

Wasserzeichen mit zusätzlichem Bild

Mit Hilfe der Funktion watermark_tile können wir ein Wasserzeichen auf den Bereich der Kachel setzen, der durch die Parameter oben_links, unten_rechts. Das Wasserzeichen soll schwarz und weiß sein. Wenn das Wasserzeichen schwarz ist, werden die entsprechenden Pixel von imag2 genommen, wenn es weiß ist, werden die Pixel von imag1 genommen.

In [12]:
def watermark_tile(imag1, imag2, watermark_imag, top_left, bottom_right):
    """ The pixels of imag2 are used when the watermark_imag is black """
    r1, r2 = top_left[0], bottom_right[0]
    c1, c2 = top_left[1], bottom_right[1]
    tile = imag1[r1:r2, c1:c2] 
    tile[:] = np.where(watermark_imag>(1, 1, 1), imag1[r1:r2, c1:c2], imag2[r1:r2, c1:c2])

Wir testen diese Funktion nun mit den Bildern 'image', 'image2' und 'spiral' als Wasserzeichenbild:

In [13]:
watermark_tile(image, image2, spiral,  top_left, bottom_right)
In [14]:
rgb_img = image[:, :, ::-1]  # BGR ---> RGB
plt.imshow(rgb_img)
Out[14]:
<matplotlib.image.AxesImage at 0x7fcaa4340050>

Wasserzeichen auf Kachel

Nun wollen wir ein Wasserzeichen direkt auf das Bild "imag1" setzen. Wir verwenden ein farbiges Bild als Wasserzeichen.

In [15]:
def object_on_tile(imag1, object_image, top_left, bottom_right):
    """ The object is put over the image """
    r1, r2 = top_left[0], bottom_right[0]
    c1, c2 = top_left[1], bottom_right[1]
    #print(f'{object_image=}')
    #tile = imag1[r1:r2, c1:c2] 
    imag1[r1:r2, c1:c2] = np.where(object_image>(250, 250, 250), 
                                   imag1[r1:r2, c1:c2], 
                                   object_image)
In [16]:
watermark_image = cv2.imread(f"{watermarks_dir}/coloured_spiral.png")
rgb_watermark_image = watermark_image[:, :, ::-1]  # BGR ---> RGB
plt.imshow(rgb_watermark_image)
Out[16]:
<matplotlib.image.AxesImage at 0x7fcaa4354ad0>
In [17]:
watermark_image.shape
Out[17]:
(470, 630, 3)
In [18]:
image = cv2.imread(f"{target_dir}/bodensee_area/boat.jpg")
plt.imshow(image[:, :, ::-1])
Out[18]:
<matplotlib.image.AxesImage at 0x7fcaa43b4ad0>
In [19]:
top_left, bottom_right = random_tile(image, watermark_image)
In [20]:
object_on_tile(image, watermark_image,  top_left, bottom_right)
In [21]:
rgb_img = image[:, :, ::-1]  # BGR ---> RGB
plt.imshow(rgb_img)
Out[21]:
<matplotlib.image.AxesImage at 0x7fcaa42a7150>
In [22]:
image.shape
Out[22]:
(1800, 4000, 3)
In [23]:
watermark_image = plt.imread(f"{watermarks_dir}/spiral.png")
watermark_image = watermark_image[:, :, :3]
#imag = cv2.imread("images/20210617_201910.jpg") 
plt.imshow(watermark_image)
#cv2.imshow(image)
Out[23]:
<matplotlib.image.AxesImage at 0x7fcaa4150bd0>

Kachel nahe beieinandeer

Wir schreiben nun eine Funktion new_close_tile, die zufällig eine neue Kachel in der Nähe einer bestehenden Kachel in der Umgebung radius findet.

In [24]:
import random 

def new_close_tile(top_left, bottom_right, 
                   img_width, img_height, 
                   radius):
    """ finds randomly a new tile close to the given tile
    in a distance radius """
    row1, row2 = top_left[0], bottom_right[0]
    col1, col2 = top_left[1], bottom_right[1]
    x = random.randint(-radius-1, radius+1) 
    y = random.randint(-radius-1, radius+1)
    tile_height = row2 - row1
    tile_width = col2 - col1
    row1_new, row2_new = row1 + y, row2 + y
    col1_new, col2_new = col1 + x, col2 + x
    while (row2_new > img_height) or row1_new < 0 or col1_new < 0 or col2_new > img_width:
        x = random.randint(-radius-1, radius+1) 
        y = random.randint(-radius-1, radius+1)
        row1_new, row2_new = row1 + y, row2 + y
        col1_new, col2_new = col1 + x, col2 + x
    return ((row1_new, col1_new), (row2_new, col2_new))

Wir werden nun die Funktion new_close_tile testen:

In [25]:
# find a tile:
top_left, bottom_right = random_tile(image, watermark_image)
print(f"{top_left=} {bottom_right=}")
img_height, img_width, colours = image.shape
for i in range(5):
    top_left, bottom_right = new_close_tile(top_left, bottom_right, 
                                            img_width, img_height, 
                                            radius=4)
    print(f"New tile positions: {top_left=} {bottom_right=}")
top_left=(310, 710) bottom_right=(1100, 1504)
New tile positions: top_left=(310, 709) bottom_right=(1100, 1503)
New tile positions: top_left=(312, 712) bottom_right=(1102, 1506)
New tile positions: top_left=(313, 712) bottom_right=(1103, 1506)
New tile positions: top_left=(310, 716) bottom_right=(1100, 1510)
New tile positions: top_left=(315, 720) bottom_right=(1105, 1514)

Bilder mit beweglichen Wasserzeichen

In [26]:
n = 40
watermark_pics = f'{target_dir}/watermark_pics'
if os.path.exists(watermark_pics):
    shutil.rmtree(watermark_pics)
os.makedirs(watermark_pics)
watermark_image = cv2.imread(f"{watermarks_dir}/coloured_spiral.png")
watermark_image = watermark_image[:, :, :3]
image_orig = cv2.imread(f"{target_dir}/paths/pic_004.jpg") 
height, width, colours = image_orig.shape
top_left, bottom_right = random_tile(image, watermark_image)
print(top_left, bottom_right)
for i in range(n, 0, -1):
    image = image_orig.copy()
    top_left, bottom_right = new_close_tile(top_left, bottom_right, width, height, 20)
    object_on_tile(image, watermark_image, top_left, bottom_right)
    cv2.imwrite(f"{watermark_pics}/pic_{i:03d}.png", image)
(318, 3152) (788, 3782)
In [27]:
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')):
    """ The function creates a video from all the images with
    the suffix (default is 'png' and the prefix (default is 'pic_'
    in the folder 'folder'. If 'length_of_video_in_seconds' is set
    to None, it will be the number of images in seconds. If a positive
    value is given this will be the length of the video in seconds.
    The function assumes that the the shape of the first image is
    the one for all the images. If not a warning will be printed
    and the size will be adapted accordingly! """
    
    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:
        # each image will be shown for one second
        length_of_video_in_seconds = len(images)
    
    # calculate number of frames per seconds:
    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()

Wir haben 40 Bilder erzeugt und wir erzeugen nun ein Video mit einer Länge von 40 Sekunden:

In [28]:
import glob
video_from_images(watermark_pics, 
                  f'{target_dir}/colour_spiral.avi', 
                  'png',
                  length_of_video_in_seconds = 40)

Für das nächste Video benutzen wir eine schwarz-weiße Spirale. Wir laden sie zuerst:

In [29]:
watermark_image = cv2.imread(f"{watermarks_dir}/spiral.png")
plt.imshow(watermark_image[:,:,::-1])
Out[29]:
<matplotlib.image.AxesImage at 0x7fcaa41a8dd0>
In [30]:
n = 40
watermark_pics = f'{target_dir}/watermark_pics2'
if os.path.exists(watermark_pics):
    shutil.rmtree(watermark_pics)
os.makedirs(watermark_pics)

image_orig = cv2.imread(f"{target_dir}/paths/pic_002.jpg") 
imag_tinted = image_orig // 2
for i in range(n, 0, -1):
    image = image_orig.copy()
    top_left, bottom_right = random_tile(image, watermark_image)
    watermark_tile(image, imag_tinted, watermark_image,  top_left, bottom_right)
    cv2.imwrite(f"{watermark_pics}/pic_{i:03d}.png", image)
In [31]:
import glob
video_from_images(watermark_pics, 
                  f'{target_dir}/spiral.avi', 
                  'png',
                  length_of_video_in_seconds=40)

Die Videos, die wir in diesem Tutorial erstellt haben, kann man sich hier anhören:

Ich habe diese Technik verwendet, um Videos für Musikkompositionen von mir zu erstellen. Die Videos sind bei YouTube zu finden:

Musikstück für Klavier, Saxophon, Oboe und Schlagzeug Die Tonspur habe ich mit dem Linux-Programm kdenlive hinzugefügt.

Alle meine Videos, einschließlich eines Python-Videos, findet man unter Bernd Klein's Videos