Seite 1 von 1

Mandelbrot Zoom-Video -- Code Optimierung

Verfasst: Montag 18. Mai 2020, 10:57
von Lukas Kretschmann
Hallo zusammen,
Ich habe einen Code geschrieben, der ein Zoom-Video in die Mandelbrotmenge erstellt.
Der Code funktioniert einwandfrei und nun geht es an das weitere Optimieren. Da nun also meine Frage:
Was kann man an folgendem Code noch verbessern? Um ihn effektiver (natürlich vor allem RAM und Zeit effektiver) zu machen.
Über Vorschläge wäre ich sehr dankbar, bitte postet dann einfach den Code mit den Veränderungen in eurem Kommentar.

Code: Alles auswählen

import matplotlib.animation as manimation
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation, rc
from itertools import cycle
import matplotlib.colors as clr
import matplotlib.animation as animation
import moviepy.editor as mp
import os
import cv2
import time

colorpoints = [(1-(1-q)**4, c) for q, c in zip(np.linspace(0, 1, 20),
                                               cycle(['#ffff88', '#000000',    #Standard: #ffff88, #000000, #ffaa00
                                                      '#ffaa00',]))]   
#http://www.am.uni-duesseldorf.de/de/Links/Tools/farbtabelle.html for RGB-codes
cmap = clr.LinearSegmentedColormap.from_list('mycmap',colorpoints, N=2048)
    
rc('animation', html='html5')

fig_size = 8

fig = plt.figure(figsize=(fig_size, fig_size), dpi = 150, tight_layout=True)
max_frames = 10
max_zoom = 100000 #max_zoom for these coordinates (0.357535415497125, 0.070571561552046) 1.7592187E13
                 #can go higher but it is possible that it zooms into a non borderregion region
                 #but 1.7592187E13 is already a huge zoom-factor 
rmin, rmax, imin, imax = -2.5, 1.5, -2, 2

Writer = animation.writers['ffmpeg']
writer = animation.FFMpegWriter(fps=2, metadata=dict(artist='Lukas Kretschamnn'), bitrate = -1, extra_args=['-pix_fmt', 'yuv420p'] )


def mandelbrot(rmin, rmax, rpoints, imin, imax, ipoints, max_iterations=1000, infinity_border=10):
    image = np.zeros((rpoints, ipoints))
    r, i = np.mgrid[rmin:rmax:(rpoints * 1j), imin:imax:(ipoints * 1j)]
    c = r + 1j * i
    z = np.zeros_like(c)
    for k in range(max_iterations):
        z = z ** 2 + c
        mask = (np.abs(z) > infinity_border) & (image == 0)
        image[mask] = k
        z[mask] = np.nan
    return -image.T


def init():
    return plt.gca()


def animate(i):
    r_center, i_center = 0.340037926617566, -0.051446751669 #Standard:  0.357535415497125, 0.070571561552046
    zoom = (i / max_frames) ** 3 * max_zoom + 1
    scalefactor = 1 / zoom
    rmin_ = (rmin - r_center) * scalefactor + r_center 
    imin_ = (imin - i_center) * scalefactor + i_center
    rmax_ = (rmax - r_center) * scalefactor + r_center
    imax_ = (imax - i_center) * scalefactor + i_center
    image = mandelbrot(rmin_, rmax_, 1000, imin_, imax_, 1000) #increase rpoints and ipoints for better resolution
    plt.axis('off', bbox_inches='tight', pad_inches = 0, tight_layout=True)
    plt.imshow(image, cmap=cmap, interpolation='none')
    print("Frame number {} created;".format(i), "next frame: {}".format(i + 1), "|", ((i + 1)/max_frames) * 100, "%") #counter starts with zero; so last frame number is max_frames - 1

def cut_frames():
    cam = cv2.VideoCapture("OUTPUT.mp4")
    try: 
       # create a directory named data 
       if not os.path.exists('data'): 
            os.makedirs('data') 
     # if directory was not created then raise an error 
    except OSError: 
        print ('Error: Creating directory of data failed') 
    # frame 
    currentframe = 0
    while(True): 
        # reading from frame 
        ret,frame = cam.read() 
        if ret: 
            # if video is still left continue creating images 
            name = './data/frame' + str(currentframe) + '.jpg'
            print ('Creating...' + name) 
            # writing the extracted images 
            cv2.imwrite(name, frame) 
            # increasing counter so that it will 
            # show how many frames are created 
            currentframe += 1
        else: 
            break
    # Release all space and windows once done 
    cam.release() 
    cv2.destroyAllWindows() 
 
print("Video Processing")
t0a = time.time()
anim = animation.FuncAnimation(fig, animate, init_func=init, frames=max_frames, interval=150)
anim.save('OUTPUT.mp4', writer=writer, dpi = 150)
t1a = time.time()
print("Took:", t1a - t0a, "Next Step: Frame slicing")
t0b = time.time()
cut_frames()
t1b = time.time()
print("Took:", t1b - t0b, " FINISHED")

Re: Mandelbrot Zoom-Video -- Code Optimierung

Verfasst: Montag 18. Mai 2020, 17:38
von __blackjack__
@Lukas Kretschmann: Ich denke Du meinst „effizienter“ und nicht „effektiver“.

Ich finde es ein wenig unübersichtlich. Das fängt schon bei den Importen an. `matplotlib.animation` wird zweimal importiert. `moviepy.editor` wird importiert, aber nicht verwendet.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Konstanten werden KOMPLETT_GROSS geschrieben. Wenn man das bei (R|I)(MIN|MAX) macht, braucht man den lokalen Namen mit gleichem Namen keinen Unterstrich anhängen.

`Writer` wird definiert aber nirgends verwendet.

`t0a`, `t1a`, `t0b`, und Ähnliche sind keine gute Namen. Namen sollen dem Leser vermitteln was der Wert dahinter bedeutet, das tun kryptische Kürzel nicht. Für solche Zeitmessungen ist `time.monotonic()` besser geeignet.

Kommentare, insbesondere längere, gehören vor den kommentierten Code und nicht hinter die Zeile und dann noch mehrere Zeilen weit eingerückt fortgeführt.

Der MP4-Dateiname sollte nicht zweimal im Quelltext stehen sondern als Konstante definiert werden.

In `cut_frames()` ist `cam` ein irreführender Name weil es ja gar keine Kamera ist sondern für eine Videodatei steht.

Die Fehlerbehandlung für den Fall, dass das "data"-Verzeischnis nicht angelegt werden konnte ist fehlerhaft. Das Programm macht danach einfach weiter als wäre nichts geschehen und versucht dann Bilder in das Verzeichnis das eben gerade *nicht* angelegt werden konnte, zu speichern.

Der Verzeichnisname "data" kommt im Code 3× vor — auch hier sollte man eine Konstante definieren. Zudem bietet sich das `pathlib`-Modul an statt mit Zeichenkettenformatierung und Funktionen aus `os` zu arbeiten.

``while`` ist keine Funktion, sollte also auch nicht so geschrieben werden als wäre es eine. An der Stelle würde sich eine ``for``-Schleife und `itertools.count()` anbieten, dann muss man den Zähler nicht vor der Schleife initialisieren und in der Schleife manuell hochzählen.

Bei den Dateinamen der Einzelbilder wäre es sinnvoll die Framenummer links mit 0en aufzufüllen, damit die Dateinamen lexikografisch sortiert in der richtigen Reihenfolge sind und nicht 10 auf 1 folgt.

Die Aufräumarbeiten in der Funktion würde ich in einen ``finally``-Block stecken.

Das `print()` in `animate()` enthält eine komische Mischung aus `format()` und Einzelargumenten. Das wäre übersichtlicher wenn man daraus *eine* Formatzeichenkette mit *einem* `format()`-Aufruf machen würde.

Code: Alles auswählen

#!/usr/bin/env python3
import os
import time
from itertools import count, cycle
from pathlib import Path

import cv2
import matplotlib
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FFMpegWriter, FuncAnimation

COLOR_MAP = clr.LinearSegmentedColormap.from_list(
    "mycmap",
    [
        (1 - (1 - q) ** 4, c)
        for q, c in zip(
            np.linspace(0, 1, 20), cycle(["#ffff88", "#000000", "#ffaa00"])
        )
    ],
    N=2048,
)

MAX_FRAMES = 10
#
# MAX_ZOOM for these coordinates (0.357535415497125, 0.070571561552046)
# 1.7592187E13 can go higher but it is possible that it zooms into a non
# borderregion region but 1.7592187E13 is already a huge zoom-factor
#
MAX_ZOOM = 100_000
RMIN, RMAX, IMIN, IMAX = -2.5, 1.5, -2, 2
OUTPUT_FILENAME = "OUTPUT.mp4"
IMAGES_PATH = Path("data")


def mandelbrot(
    rmin,
    rmax,
    rpoints,
    imin,
    imax,
    ipoints,
    max_iterations=1000,
    infinity_border=10,
):
    image = np.zeros((rpoints, ipoints))
    r, i = np.mgrid[rmin : rmax : rpoints * 1j, imin : imax : ipoints * 1j]
    c = r + 1j * i
    z = np.zeros_like(c)
    for k in range(max_iterations):
        z = z ** 2 + c
        mask = (np.abs(z) > infinity_border) & (image == 0)
        image[mask] = k
        z[mask] = np.nan
    return -image.T


def init():
    return plt.gca()


def animate(frame_number):
    #
    # Standard:  0.357535415497125, 0.070571561552046
    #
    r_center, i_center = 0.340037926617566, -0.051446751669
    zoom = (frame_number / MAX_FRAMES) ** 3 * MAX_ZOOM + 1
    scalefactor = 1 / zoom
    rmin = (RMIN - r_center) * scalefactor + r_center
    imin = (IMIN - i_center) * scalefactor + i_center
    rmax = (RMAX - r_center) * scalefactor + r_center
    imax = (IMAX - i_center) * scalefactor + i_center
    #
    # Increase rpoints and ipoints for better resolution.
    #
    image = mandelbrot(rmin, rmax, 1000, imin, imax, 1000)
    plt.axis("off", bbox_inches="tight", pad_inches=0, tight_layout=True)
    plt.imshow(image, cmap=COLOR_MAP, interpolation="none")
    #
    # Counter starts with zero; so last frame number is MAX_FRAMES - 1.
    #
    print(
        "Frame number {} created; next frame: {} | {:.2%}".format(
            frame_number, frame_number + 1, (frame_number + 1) / MAX_FRAMES
        )
    )


def cut_frames(video_filename, target_path):
    video = cv2.VideoCapture(video_filename)
    try:
        try:
            target_path.mkdir(exist_ok=True)
        except OSError:
            print("Error: Creating directory for data failed")
        else:
            for frame_number in count():
                is_ok, frame = video.read()
                if not is_ok:
                    break

                image_filename = target_path / f"frame{frame_number:04d}.jpg"
                print("Creating...", image_filename)
                cv2.imwrite(str(image_filename), frame)
    finally:
        video.release()
        cv2.destroyAllWindows()


def main():
    matplotlib.rc("animation", html="html5")
    figure_size = 8
    figure = plt.figure(
        figsize=(figure_size, figure_size), dpi=150, tight_layout=True
    )

    print("Video Processing")
    start_time = time.monotonic()
    animation = FuncAnimation(
        figure, animate, init_func=init, frames=MAX_FRAMES, interval=150
    )
    writer = FFMpegWriter(
        fps=2,
        metadata={"artist": "Lukas Kretschamnn"},
        bitrate=-1,
        extra_args=["-pix_fmt", "yuv420p"],
    )
    animation.save(OUTPUT_FILENAME, writer=writer, dpi=150)
    end_time = time.monotonic()
    print("Took:", end_time - start_time, "Next Step: Frame slicing")

    start_time = time.monotonic()
    cut_frames(OUTPUT_FILENAME, IMAGES_PATH)
    end_time = time.monotonic()
    print("Took:", end_time - start_time, " FINISHED")


if __name__ == "__main__":
    main()
Die `print()`-Ausgaben würde ich durch Logging ersetzen.

Re: Mandelbrot Zoom-Video -- Code Optimierung

Verfasst: Mittwoch 20. Mai 2020, 19:57
von Lukas Kretschmann
@__blackjack__ vielen Dank für die gute Kritik und Hilfe