Création de la classe V2Char
Ensuite, notre attention se porte sur la classe V2Char
, qui hérite de la classe CharFrame
.
Tout d'abord, réfléchissons à notre classe. L'un de ses attributs est charVideo
, qui est une liste utilisée pour stocker toutes les données de l'animation de caractères.
Ensuite, nous avons deux méthodes principales : l'une est la méthode genCharVideo()
, qui convertit un fichier vidéo en une animation de caractères, et l'autre est la méthode play()
, qui joue l'animation de caractères.
De plus, étant donné que la conversion d'une vidéo en animation de caractères est un processus long, nous pouvons exporter les données de l'animation de caractères depuis charVideo
pour faciliter la lecture future. Cela signifie que nous avons besoin de méthodes d'exportation et de chargement, à savoir la méthode export()
et la méthode load()
.
La classe a également besoin d'une méthode d'initialisation qui prend le chemin du fichier à lire comme paramètre. Si le fichier est un fichier txt exporté, elle appellera la méthode load()
pour charger les données dans l'attribut charVideo
. Sinon, il sera traité comme un fichier vidéo et appellera la méthode genCharVideo()
pour convertir la vidéo en une animation de caractères et la stocker dans l'attribut charVideo
.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
Voici ensuite la méthode genCharVideo()
:
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
La méthode cv2.VideoCapture()
est utilisée pour lire le fichier vidéo, et l'objet retourné est assigné à cap
.
En utilisant la méthode cap.get()
, nous pouvons obtenir les propriétés de la vidéo, par exemple cap.get(3)
et cap.get(4)
qui retournent la largeur et la hauteur de la vidéo, et cap.get(5)
qui retourne le taux de trames de la vidéo. cap.get(7)
retourne le nombre total de trames de la vidéo.
timeInterval
stocke l'intervalle de temps de lecture, utilisé pour que le taux de trames de l'animation de caractères soit le même que celui de la vidéo originale.
pyprind.prog_bar()
est un générateur qui affiche une barre de progression dans le terminal lors de l'itération.
cap.read()
lit la trame suivante de la vidéo. Elle retourne un tuple avec deux éléments. Le premier élément est une valeur booléenne indiquant si la trame a été lue correctement, et le deuxième élément est un numpy.ndarray
contenant les données de la trame.
cv2.cvtColor()
est utilisé pour convertir l'espace de couleur de l'image. Le premier paramètre est l'objet image, et le deuxième paramètre indique le type de conversion. Il y a plus de 150 conversions d'espaces de couleur dans OpenCV. Ici, nous utilisons la conversion de couleur en niveaux de gris cv2.COLOR_BGR2GRAY
.
os.get_terminal_size()
retourne le nombre de colonnes (largeur) et le nombre de lignes (hauteur) du terminal actuel. Nous avons défini le paramètre fill
sur True
et n'avons pas défini le paramètre wrap
, donc il prend la valeur par défaut False
. Dans le terminal, si les caractères imprimés dépassent la largeur d'une ligne, le terminal effectuera automatiquement un retour à la ligne.
Enfin, n'oubliez pas de libérer la ressource avec cap.release()
.
Voici ensuite la méthode play()
. Comme mentionné précédemment, afin d'empêcher le terminal d'être rempli de caractères inutiles après avoir joué l'animation de caractères, nous pouvons utiliser des codes d'échappement de positionnement du curseur.
Nous pouvons faire cela : après avoir affiché chaque trame, déplacer le curseur au début de la lecture, et la prochaine trame sera affichée à partir de cette position et écrasera automatiquement le contenu précédent. Répétez ce processus jusqu'à la fin de la lecture, puis effacez la dernière trame affichée, de sorte que le terminal ne sera pas rempli d'art de caractères.
Voici une série de codes d'échappement de positionnement du curseur (certains terminaux peuvent ne pas prendre en charge certains codes d'échappement), tirés de The Linux Command Line :
Code d'échappement |
Action |
\033[l;cH |
Déplace le curseur à la ligne l , colonne c . |
\033[nA |
Déplace le curseur vers le haut de n lignes. |
\033[nB |
Déplace le curseur vers le bas de n lignes. |
\033[nC |
Déplace le curseur vers l'avant de n caractères. |
\033[nD |
Déplace le curseur vers l'arrière de n caractères. |
\033[2J |
Efface l'écran et déplace le curseur à (0, 0). |
\033[K |
Efface depuis la position du curseur jusqu'à la fin de la ligne actuelle. |
\033[s |
Enregistre la position actuelle du curseur. |
\033[u |
Restaure la position précédemment enregistrée du curseur. |
Il y a un autre problème : comment arrêter la lecture en cours? Bien sûr, vous pouvez appuyer sur Ctrl + C
, mais de cette façon, le programme ne peut effectuer aucun nettoyage et le terminal sera rempli d'un tas de caractères inutiles.
Nous l'avons conçu de cette manière : lorsque l'animation de caractères commence à jouer, elle lance un thread de type démon (daemon thread) qui attend une entrée utilisateur. Dès qu'il reçoit une entrée, il arrête de jouer l'animation de caractères.
Il y a deux choses à noter ici :
- N'utilisez pas la méthode
input()
pour recevoir une entrée de caractères.
- Ne pouvez pas utiliser des threads normaux.
Pour le premier point, si vous voulez arrêter la lecture en appuyant sur n'importe quel caractère, vous ne devriez pas utiliser input()
, sinon vous devrez soit appuyer sur Entrée pour arrêter la lecture, soit appuyer sur un autre caractère puis sur Entrée pour arrêter la lecture. En résumé, utiliser input()
n'est pas pratique. La meilleure solution est d'utiliser quelque chose de similaire à la méthode getchar()
en langage C. Cependant, Python ne fournit pas de méthode similaire, et une solution de rechange sera fournie dans le code plus tard.
Pour le deuxième point, nous devons comprendre que si tout thread dérivé est toujours en cours d'exécution, le thread principal ne se terminera pas à moins que le thread dérivé soit défini comme un thread de type démon. Donc, si nous utilisons un thread normal et que l'utilisateur ne l'arrête pas en cours de route, il continuera à s'exécuter jusqu'à la fin de l'animation, puis il s'exécutera indéfiniment jusqu'à ce que l'utilisateur entre n'importe quel caractère. Si nous pouvions tuer manuellement ce thread dérivé lorsque la lecture est terminée, ce ne serait pas un problème. Cependant, Python ne fournit pas de méthode pour tuer des threads. Par conséquent, nous ne pouvons que définir ce thread dérivé comme un thread de type démon. Lorsque le thread principal se termine, le programme n'aura plus qu'un thread de type démon en cours d'exécution, et le thread de type démon sera tué automatiquement lorsque le programme se terminera.
Le code complet de la classe V2Char
est le suivant :
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)