Einführung
In diesem Projekt werden wir OpenCV verwenden, um Bilder und Videos zu verarbeiten und ASCII-Art-Animationen zu erstellen.
- OpenCV-Kompilierung
- Bild- und Videoverarbeitung mit OpenCV
- Prinzipien der Umwandlung von Bildern in ASCII-Art
- Daemon-Threads
- Cursor-Positionierung und Escape-Codierung
👀 Vorschau

🎯 Aufgaben
In diesem Projekt lernst du:
- Wie man Bilder und Videos mit OpenCV in ASCII-Art-Animationen umwandelt.
- Wie man ASCII-Art-Animationen im Terminal mit Cursor-Positionierung und Escape-Codierung abspielt.
- Wie man ASCII-Art-Animationsdaten exportiert und lädt.
🏆 Errungenschaften
Nach Abschluss dieses Projekts kannst du:
- OpenCV verwenden, um Bilder und Videos zu verarbeiten.
- Bilder in ASCII-Art umwandeln.
- ASCII-Art-Animationen abspielen.
- ASCII-Art-Animationsdaten exportieren und laden.
Datei erstellen
Jeder sollte verstehen, dass ein Video eigentlich eine Reihe von Bildern ist. Daher besteht das grundlegende Prinzip der Umwandlung eines Videos in eine ASCII-Animation darin, Bilder in ASCII-Art umzuwandeln.
Hier ist eine einfache Erklärung des Prinzips hinter der Umwandlung von Bildern in ASCII-Art: Zunächst wird das Bild in ein Graustufenbild umgewandelt, bei dem jeder Pixel nur Helligkeitsinformationen enthält (repräsentiert durch die Werte 0 - 255). Dann erstellen wir eine begrenzte Zeichensatz, bei dem jedes Zeichen einem Bereich von Helligkeitswerten entspricht. Wir können dann jeden Pixel mit dem entsprechenden Zeichen basierend auf dieser Korrespondenz und der Helligkeitsinformation des Pixels darstellen und so ASCII-Art erstellen.
Damit eine ASCII-Animation sinnvoll ist, muss sie abspielbar sein. Der primitivste und einfachste Weg, dies zu tun, besteht darin, die ASCII-Animations-Text-Datei in einem Texteditor zu öffnen und wiederholt die PageDown-Taste zu drücken. Dieser Ansatz ist jedoch wirklich zu einfach und primitiv und überhaupt nicht elegant.
Stattdessen können wir die ASCII-Animation im Terminal abspielen, indem wir jeweils einen Frame ausgeben. Dieser Ansatz hat jedoch einen großen Nachteil: Während der Wiedergabe wird der Bildlaufbalken auf der rechten Seite des Terminals immer kleiner (falls vorhanden); nach der Wiedergabe, wenn Sie im Terminal nach oben scrollen, sehen Sie nur die zuvor ausgegebene ASCII-Art, und die gesamte Befehlshistorie vor der Wiedergabe wird herausgeschoben. Eine Lösung für dieses Problem wird später in diesem Projekt vorgestellt.
Erstellen Sie eine CLIPlayVideo.py-Datei im Verzeichnis ~/project.
cd ~/project
touch CLIPlayVideo.py
Installieren Sie dann das opencv-python-Modul:
sudo pip install opencv-python
Erstellen der CharFrame-Klasse
Um es nicht zu vergessen, importieren Sie die erforderlichen Pakete:
import sys
import os
import time
import threading
import termios
import tty
import cv2
import pyprind
Das letzte Modul, pyprind, bietet eine Fortschrittsleiste zur Anzeige. Da die Umwandlung eines Videos in eine Zeichenanimation ein zeitaufwändiger Prozess ist, benötigen wir eine Fortschrittsleiste, um den Fortschritt und die ungefähre verbleibende Zeit anzuzeigen und so einen intuitiveren Einblick in den Status des Programms zu erhalten. Installieren Sie es wie folgt:
sudo pip3 install pyprind
In diesem Projekt haben wir neben der Umwandlung von Videodateien in Zeichenanimationen und deren Wiedergabe auch die Funktion hinzugefügt, Bilddateien in Zeichenkunst umzuwandeln. Daher haben wir in unserem Programmdesign drei Klassen: CharFrame, I2Char und V2Char, wobei die letzten beiden Klassen von der ersten Klasse erben.
Die CharFrame-Klasse:
class CharFrame:
ascii_char = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
## Map pixels to characters
def pixelToChar(self, luminance):
return self.ascii_char[int(luminance/256*len(self.ascii_char))]
## Convert a regular frame to an ASCII character frame
def convert(self, img, limitSize=-1, fill=False, wrap=False):
if limitSize!= -1 and (img.shape[0] > limitSize[1] or img.shape[1] > limitSize[0]):
img = cv2.resize(img, limitSize, interpolation=cv2.INTER_AREA)
ascii_frame = ''
blank = ''
if fill:
blank += ' '*(limitSize[0]-img.shape[1])
if wrap:
blank += '\n'
for i in range(img.shape[0]):
for j in range(img.shape[1]):
ascii_frame += self.pixelToChar(img[i,j])
ascii_frame += blank
return ascii_frame
Das Attribut ascii_char kann je nach Video, das Sie umwandeln möchten, angepasst werden.
Die Methode pixelToChar() nimmt einen einzelnen Parameter, luminance, der die Helligkeitsinformation des Pixels erhält. Es sollte beachtet werden, dass der Ausdruck in der return-Anweisung der Methode den Wert 256 verwendet. Obwohl der Helligkeitsbereich des Pixels 0~255 beträgt, kann das Ändern von 256 auf 255 in diesem Ausdruck eine IndexError-Ausnahme verursachen.
Die Methode convert() hat einen Positions- und drei optionale Parameter. Der Parameter img erhält ein Objekt vom Typ numpy.ndarray, das das Objekt ist, das OpenCV zurückgibt, wenn es ein Bild öffnet. Ebenso werden die Frames aus dem Video, die später mit OpenCV abgerufen werden, auch von diesem Typ sein. Der Parameter limitSize akzeptiert ein Tupel, das die maximale Breite und Höhe des Bildes darstellt. Der Parameter fill gibt an, ob die Breite des Bildes mit Leerzeichen auf die maximale Breite aufgefüllt werden soll, und der Parameter wrap gibt an, ob am Ende jeder Zeile ein Zeilenumbruch hinzugefügt werden soll.
img.shape gibt ein Tupel zurück, das die Anzahl der Zeilen (Höhe), Spalten (Breite) und Farbkanäle des Bildes enthält. Wenn das Bild in Graustufen vorliegt, wird die Anzahl der Farbkanäle nicht enthalten.
Die Funktion cv2.resize() wird verwendet, um das Bild zu vergrößern oder zu verkleinern. Der erste Parameter ist das numpy.ndarray-Objekt, und der zweite ist die gewünschte Breite und Höhe des vergrößerten oder verkleinerten Bildes. Der Parameter interpolation gibt die Interpolationsmethode an. Es sind mehrere Interpolationsmethoden verfügbar, wie auf der offiziellen OpenCV-Website erklärt:
Bevorzugte Interpolationsmethoden sind
cv2.INTER_AREAzum Verkleinern undcv2.INTER_CUBIC(langsam) &cv2.INTER_LINEARzum Vergrößern. Standardmäßig wird die Interpolationsmethodecv2.INTER_LINEARfür alle Größenänderungen verwendet.
img[i,j] gibt eine Liste der BGR-Werte des Pixels in der i-ten Zeile und j-ten Spalte des Bildes zurück oder den Helligkeitswert des entsprechenden Pixels für Graustufenbilder.
Erstellen der I2Char-Klasse
Hier ist die I2Char-Klasse, die von CharFrame erbt:
class I2Char(CharFrame):
result = None
def __init__(self, path, limitSize=-1, fill=False, wrap=False):
self.genCharImage(path, limitSize, fill, wrap)
def genCharImage(self, path, limitSize=-1, fill=False, wrap=False):
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
if img is None:
return
self.result = self.convert(img, limitSize, fill, wrap)
def show(self, stream = 2):
if self.result is None:
return
if stream == 1 and os.isatty(sys.stdout.fileno()):
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()):
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'):
self.streamOut = stream.write
self.streamFlush = stream.flush
self.streamOut(self.result)
self.streamFlush()
self.streamOut('\n')
cv2.imread() liest ein Bild und gibt ein numpy.ndarray-Objekt zurück. Der erste Parameter ist der Pfad zum zu öffnenden Bild, und der zweite Parameter gibt die Art der Bildöffnung an und kann die folgenden drei Werte haben:
cv2.IMREAD_COLOR: Lädt ein Farbbild und ignoriert den Alpha-Kanal.cv2.IMREAD_GRAYSCALE: Lädt das Bild im Graustufenmodus.cv2.IMREAD_UNCHANGED: Lädt das Bild einschließlich des Alpha-Kanals.
Die Methode show() akzeptiert einen Parameter, der angibt, welchen Ausgabestream verwendet werden soll. 1 repräsentiert die Standardausgabe sys.stdout, und 2 repräsentiert die Standardfehlerausgabe sys.stderr. sys.stdout.fileno() und sys.stderr.fileno() geben jeweils den Dateideskriptor für die Standardausgabe und die Standardfehlerausgabe zurück. os.isatty(fd) gibt einen booleschen Wert zurück, der angibt, ob der Dateideskriptor fd geöffnet ist und mit einem TTY-Gerät verbunden ist.
Erstellen der V2Char-Klasse
Nun liegt unser Fokus auf der V2Char-Klasse, die von der CharFrame-Klasse erbt.
Zunächst sollten wir über unsere Klasse nachdenken. Eines ihrer Attribute ist charVideo, eine Liste, die alle Daten für die Zeichenanimation speichert.
Dann haben wir zwei Hauptmethoden: Die eine ist die genCharVideo()-Methode, die eine Videodatei in eine Zeichenanimation umwandelt, und die andere ist die play()-Methode, die die Zeichenanimation abspielt.
Außerdem, da die Umwandlung von Video in Zeichenanimation ein zeitaufwändiger Prozess ist, können wir die Zeichenanimationsdaten aus charVideo exportieren, um die spätere Wiedergabe zu erleichtern. Das bedeutet, dass wir Export- und Lademethoden benötigen, nämlich die export()-Methode und die load()-Methode.
Die Klasse benötigt auch eine Initialisierungsmethode, die den Pfad zur Datei, die gelesen werden soll, als Parameter nimmt. Wenn es sich um eine exportierte txt-Datei handelt, ruft sie die load()-Methode auf, um die Daten in das charVideo-Attribut zu laden. Andernfalls wird sie als Videodatei behandelt und ruft die genCharVideo()-Methode auf, um das Video in eine Zeichenanimation umzuwandeln und sie im charVideo-Attribut zu speichern.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
Als nächstes kommt die genCharVideo()-Methode:
def genCharVideo(self, filepath):
self.charVideo = []
cap = cv2.VideoCapture(filepath) ## Use the `cv2.VideoCapture()` method to read the video file
self.timeInterval = round(1/cap.get(5), 3) ## Get the frame rate of the video
nf = int(cap.get(7)) ## Get the total number of frames in the video
print('Generate char video, please wait...')
for i in pyprind.prog_bar(range(nf)):
rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
frame = self.convert(rawFrame, os.get_terminal_size(), fill=True) ## Convert the raw frame into a character frame
self.charVideo.append(frame)
cap.release() ## Release the resource
Die cv2.VideoCapture()-Methode wird verwendet, um die Videodatei zu lesen, und das zurückgegebene Objekt wird cap zugewiesen.
Mit der cap.get()-Methode können wir die Eigenschaften des Videos abrufen. Beispielsweise geben cap.get(3) und cap.get(4) die Breite und Höhe des Videos zurück, und cap.get(5) gibt die Bildrate des Videos zurück. cap.get(7) gibt die Gesamtzahl der Frames im Video zurück.
timeInterval speichert das Wiedergabezeitintervall, um die Bildrate der Zeichenanimation mit der des Originalvideos übereinzustimmen.
pyprind.prog_bar() ist ein Generator, der beim Iterieren eine Fortschrittsleiste im Terminal ausgibt.
cap.read() liest den nächsten Frame des Videos. Es gibt ein Tupel mit zwei Elementen zurück. Das erste Element ist ein boolescher Wert, der angibt, ob der Frame korrekt gelesen wurde, und das zweite Element ist ein numpy.ndarray, das die Daten des Frames enthält.
cv2.cvtColor() wird verwendet, um den Farbraum des Bildes zu konvertieren. Der erste Parameter ist das Bildobjekt, und der zweite Parameter gibt den Konvertierungstyp an. Es gibt über 150 Farbraumkonvertierungen in OpenCV. Hier verwenden wir die Farb-zu-Graustufen-Konvertierung cv2.COLOR_BGR2GRAY.
os.get_terminal_size() gibt die aktuelle Spaltenanzahl (Breite) und Zeilenanzahl (Höhe) des Terminals zurück. Wir setzen den fill-Parameter auf True und haben den wrap-Parameter nicht gesetzt, sodass er standardmäßig auf False steht. Im Terminal wird, wenn die gedruckten Zeichen die Breite einer Zeile überschreiten, das Terminal automatisch umbrechen.
Vergessen Sie schließlich nicht, die Ressource mit cap.release() freizugeben.
Als nächstes kommt die play()-Methode. Wie bereits erwähnt, können wir um zu verhindern, dass der Terminal nach der Wiedergabe der Zeichenanimation mit nutzlosen Zeichen gefüllt wird, Cursor-Positionierungs-Escape-Codes verwenden.
Wir können das so machen: Nachdem jeder Frame ausgegeben wurde, bewegen wir den Cursor an den Anfang der Wiedergabe, und der nächste Frame wird von dieser Position aus ausgegeben und überschreibt automatisch den vorherigen Inhalt. Wiederholen Sie diesen Vorgang, bis die Wiedergabe abgeschlossen ist, und löschen Sie dann den zuletzt ausgegebenen Frame, damit der Terminal nicht mit Zeichenkunst gefüllt wird.
Hier ist eine Reihe von Cursor-Positionierungs-Escape-Codes (einige Terminals unterstützen möglicherweise nicht alle Escape-Codes), entnommen aus The Linux Command Line:
| Escape Code | Aktion |
|---|---|
| \033[l;cH | Bewege den Cursor zur Zeile l, Spalte c. |
| \033[nA | Bewege den Cursor n Zeilen nach oben. |
| \033[nB | Bewege den Cursor n Zeilen nach unten. |
| \033[nC | Bewege den Cursor n Zeichen nach vorne. |
| \033[nD | Bewege den Cursor n Zeichen nach hinten. |
| \033[2J | Lösche den Bildschirm und bewege den Cursor an die Position (0, 0). |
| \033[K | Lösche vom Cursor-Position bis zum Ende der aktuellen Zeile. |
| \033[s | Speichere die aktuelle Cursor-Position. |
| \033[u | Stelle die zuvor gespeicherte Cursor-Position wieder her. |
Es gibt noch ein weiteres Problem: Wie kann man die Wiedergabe 中途 abbrechen? Natürlich können Sie Ctrl + C drücken, aber auf diese Weise kann das Programm keine Aufräumarbeiten durchführen, und der Terminal wird mit einer Reihe von nutzlosen Zeichen gefüllt.
Wir haben es so gestaltet: Wenn die Zeichenanimation beginnt, startet es einen Daemon-Thread, der auf Benutzereingaben wartet. Sobald er eine Eingabe erhält, stoppt er die Wiedergabe der Zeichenanimation.
Hier sind zwei Dinge zu beachten:
- Verwenden Sie nicht die
input()-Methode, um Zeichen-Eingaben zu empfangen. - Verwenden Sie keine normalen Threads.
Bezüglich des ersten Punktes: Wenn Sie die Wiedergabe durch Drücken eines beliebigen Zeichens stoppen möchten, sollten Sie nicht input() verwenden. Andernfalls müssen Sie entweder Enter drücken, um die Wiedergabe zu stoppen, oder ein anderes Zeichen drücken und dann Enter, um die Wiedergabe zu stoppen. Kurz gesagt, die Verwendung von input() ist unkomfortabel. Die beste Lösung ist die Verwendung einer Methode ähnlich wie die getchar()-Methode in der Programmiersprache C. Python bietet jedoch keine ähnliche Methode, und eine alternative Lösung wird später im Code vorgestellt.
Bezüglich des zweiten Punktes: Wir müssen verstehen, dass der Hauptthread nicht beendet wird, wenn irgendein abgeleiteter Thread noch läuft, es sei denn, der abgeleitete Thread ist als Daemon-Thread festgelegt. Wenn wir also einen normalen Thread verwenden und der Benutzer ihn nicht 中途 stoppt, wird er weiterlaufen, bis die Animation beendet ist, und dann wird er ewig laufen, bis der Benutzer ein beliebiges Zeichen eingibt. Wenn wir diesen abgeleiteten Thread manuell beenden könnten, wenn die Wiedergabe abgeschlossen ist, wäre das kein Problem. Python bietet jedoch keine Methode, um Threads zu beenden. Daher können wir diesen abgeleiteten Thread nur als Daemon-Thread festlegen. Wenn der Hauptthread beendet wird, wird das Programm nur noch von einem Daemon-Thread ausgeführt, und der Daemon-Thread wird automatisch beendet, wenn das Programm beendet wird.
Der vollständige Code für die V2Char-Klasse lautet wie folgt:
class V2Char(CharFrame):
charVideo = []
timeInterval = 0.033
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
def genCharVideo(self, filepath):
self.charVideo = []
cap = cv2.VideoCapture(filepath)
self.timeInterval = round(1/cap.get(5), 3)
nf = int(cap.get(7))
print('Generate char video, please wait...')
for i in pyprind.prog_bar(range(nf)):
rawFrame = cv2.cvtColor(cap.read()[1], cv2.COLOR_BGR2GRAY)
frame = self.convert(rawFrame, os.get_terminal_size(), fill=True)
self.charVideo.append(frame)
cap.release()
def export(self, filepath):
if not self.charVideo:
return
with open(filepath,'w') as f:
for frame in self.charVideo:
f.write(frame + '\n') ## Add a newline character to separate each frame
def load(self, filepath):
self.charVideo = []
for i in open(filepath): ## Each line is a frame
self.charVideo.append(i[:-1])
def play(self, stream = 1):
## Bug:
## Cursor positioning escape codes are incompatible with Windows
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()): ## If it's a standard output and its file descriptor refers to a terminal
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()): ## If it's a standard error output and its file descriptor refers to a terminal
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'): ## If it has a write attribute
self.streamOut = stream.write
self.streamFlush = stream.flush
old_settings = None
breakflag = None
fd = sys.stdin.fileno() ## Get the file descriptor of the standard input
def getChar():
nonlocal breakflag
nonlocal old_settings
old_settings = termios.tcgetattr(fd) ## Save the attributes of the standard input
tty.setraw(sys.stdin.fileno()) ## Set the standard input to raw mode
ch = sys.stdin.read(1) ## Read a character
breakflag = True if ch else False
## Create a thread
getchar = threading.Thread(target=getChar)
getchar.daemon = True ## Set it as a daemon thread
getchar.start() ## Start the daemon thread
rows = len(self.charVideo[0])//os.get_terminal_size()[0] ## Number of rows in the output character art
for frame in self.charVideo:
if breakflag is True: ## Exit the loop if input is received
break
self.streamOut(frame)
self.streamFlush()
time.sleep(self.timeInterval)
self.streamOut('\033[{}A\r'.format(rows-1)) ## Move up `rows-1` lines to the start
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) ## Restore the standard input to its original attributes
self.streamOut('\033[{}B\033[K'.format(rows-1)) ## Move down `rows-1` lines to the last line and clear it
for i in range(rows-1): ## Clear all lines of the last frame from the second last line onwards
self.streamOut('\033[1A')
self.streamOut('\r\033[K')
info = 'User interrupt!\n' if breakflag else 'Finished!\n'
self.streamOut(info)
Testen und Ausführen
Geben Sie den folgenden Code unterhalb des vorherigen Codes ein. Dieser Code kann als Skriptdatei verwendet werden, um ein Video in ASCII-Art umzuwandeln.
if __name__ == '__main__':
import argparse
## Set command line arguments
parser = argparse.ArgumentParser()
parser.add_argument('file',
help='Video file or charvideo file')
parser.add_argument('-e', '--export', nargs='?', const='charvideo.txt',
help='Export charvideo file')
## Get arguments
args = parser.parse_args()
v2char = V2Char(args.file)
if args.export:
v2char.export(args.export)
v2char.play()
Geben Sie den folgenden Befehl ein, um die Datei ~/project/BadApple.mp4 in eine ASCII-Animation umzuwandeln, sie als Datei zu exportieren und die umgewandelte ASCII-Animation abzuspielen (der Codierungsprozess kann einige Minuten dauern, bitte seien Sie geduldig).
cd ~/project
python3 CLIPlayVideo.py BadApple.mp4 -e

Anschließend können Sie, um die Animation erneut abzuspielen, ohne sie erneut zu konvertieren, direkt die exportierte ASCII-Art-Datei lesen. Nehmen wir an, die Datei heißt charvideo.txt:
python3 CLIPlayVideo.py charvideo.txt
Zusammenfassung
In diesem Projekt wird OpenCV zur Verarbeitung von Bildern und Videos verwendet. Die hier erwähnten OpenCV-Operationen sollten nur der Anfang für alle sein. Wenn Sie tiefer einsteigen möchten, sollten Sie sich öfter die offizielle Dokumentation ansehen. Durch die Verwendung eines Daemon-Threads wird hoffentlich jedem klar, was ein Daemon-Thread ist und wie er sich von einem normalen Thread unterscheidet. Schließlich haben wir uns auch mit Cursor-Positionierung und Escape-Codes beschäftigt. Obwohl dies möglicherweise nicht sehr nützlich ist, ist es dennoch eine interessante Sache, mit der man viele Dinge machen kann. Sie können gerne mehr damit experimentieren.



