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)