Creación de la clase V2Char
A continuación, nuestro foco se centra en la clase V2Char
, que hereda de la clase CharFrame
.
Primero, pensemos en nuestra clase. Uno de sus atributos es charVideo
, que es una lista utilizada para almacenar todos los datos de la animación de caracteres.
Luego, tenemos dos métodos principales: uno es el método genCharVideo()
, que convierte un archivo de video en una animación de caracteres, y el otro es el método play()
, que reproduce la animación de caracteres.
Además, dado que la conversión de video a animación de caracteres es un proceso que consume tiempo, podemos exportar los datos de la animación de caracteres de charVideo
para facilitar la reproducción futura. Esto significa que necesitamos métodos de exportación y carga, es decir, el método export()
y el método load()
.
La clase también necesita un método de inicialización que tome como parámetro la ruta del archivo a leer. Si el archivo es un archivo txt exportado, llamará al método load()
para cargar los datos en el atributo charVideo
. De lo contrario, se tratará como un archivo de video y llamará al método genCharVideo()
para convertir el video en una animación de caracteres y almacenarla en el atributo charVideo
.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
A continuación, está el método 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
El método cv2.VideoCapture()
se utiliza para leer el archivo de video, y el objeto devuelto se asigna a cap
.
Utilizando el método cap.get()
, podemos obtener las propiedades del video, como cap.get(3)
y cap.get(4)
que devuelven el ancho y la altura del video, y cap.get(5)
que devuelve la tasa de fotogramas del video. cap.get(7)
devuelve el número total de fotogramas del video.
timeInterval
almacena el intervalo de tiempo de reproducción, utilizado para hacer que la tasa de fotogramas de la animación de caracteres sea la misma que la del video original.
pyprind.prog_bar()
es un generador que muestra una barra de progreso en la terminal al iterar.
cap.read()
lee el siguiente fotograma del video. Devuelve una tupla con dos elementos. El primer elemento es un valor booleano que indica si el fotograma se leyó correctamente, y el segundo elemento es un numpy.ndarray
que contiene los datos del fotograma.
cv2.cvtColor()
se utiliza para convertir el espacio de color de la imagen. El primer parámetro es el objeto de la imagen, y el segundo parámetro indica el tipo de conversión. Hay más de 150 conversiones de espacio de color en OpenCV. Aquí, utilizamos la conversión de color a escala de grises cv2.COLOR_BGR2GRAY
.
os.get_terminal_size()
devuelve el número de columnas (ancho) y el número de filas (altura) del terminal actual. Establecemos el parámetro fill
en True
y no establecimos el parámetro wrap
, por lo que su valor predeterminado es False
. En el terminal, si los caracteres impresos exceden el ancho de una línea, el terminal automáticamente ajustará la visualización.
Finalmente, no olvides liberar el recurso con cap.release()
.
A continuación, está el método play()
. Como se mencionó anteriormente, para evitar que el terminal se llene de caracteres inútiles después de reproducir la animación de caracteres, podemos utilizar códigos de escape de posicionamiento del cursor.
Podemos hacer lo siguiente: después de mostrar cada fotograma, mover el cursor al inicio de la reproducción, y el siguiente fotograma se mostrará desde esta posición y sobrescribirá automáticamente el contenido anterior. Repite este proceso hasta que la reproducción se complete, luego borra el último fotograma mostrado, para que el terminal no se llene de arte de caracteres.
A continuación, se muestra una serie de códigos de escape de posicionamiento del cursor (algunos terminales pueden no admitir algunos códigos de escape), tomados de The Linux Command Line:
Código de Escape |
Acción |
\033[l;cH |
Mueve el cursor a la línea l , columna c . |
\033[nA |
Mueve el cursor hacia arriba n líneas. |
\033[nB |
Mueve el cursor hacia abajo n líneas. |
\033[nC |
Mueve el cursor hacia adelante n caracteres. |
\033[nD |
Mueve el cursor hacia atrás n caracteres. |
\033[2J |
Borra la pantalla y mueve el cursor a (0, 0). |
\033[K |
Borra desde la posición del cursor hasta el final de la línea actual. |
\033[s |
Guarda la posición actual del cursor. |
\033[u |
Restaura la posición anterior guardada del cursor. |
Hay otro problema, ¿cómo detener la reproducción a mitad del camino? Por supuesto, puedes presionar Ctrl + C
, pero de esta manera, el programa no puede realizar ningún trabajo de limpieza y el terminal se llenará de una serie de caracteres inútiles.
Lo diseñamos de la siguiente manera: cuando comienza la reproducción de la animación de caracteres, se inicia un hilo demonio que espera la entrada del usuario. Una vez que recibe una entrada, detiene la reproducción de la animación de caracteres.
Hay dos cosas a tener en cuenta aquí:
- No uses el método
input()
para recibir entrada de caracteres.
- No se pueden usar hilos normales.
En cuanto al primer punto, si deseas detener la reproducción presionando cualquier carácter, entonces no debes usar input()
, de lo contrario, tendrás que presionar Enter para detener la reproducción, o presionar otro carácter y luego presionar Enter para detener la reproducción. En resumen, usar input()
no es cómodo. La mejor solución es usar algo similar al método getchar()
en el lenguaje C. Sin embargo, Python no proporciona un método similar, y se proporcionará una solución alternativa en el código más adelante.
En cuanto al segundo punto, debemos entender que si cualquier hilo derivado todavía está en ejecución, el hilo principal no saldrá a menos que el hilo derivado se establezca como un hilo demonio. Entonces, si usamos un hilo normal y el usuario no lo detiene a mitad del camino, seguirá ejecutándose hasta que la animación termine, y luego se ejecutará para siempre hasta que el usuario ingrese cualquier carácter. Si pudiéramos matar manualmente este hilo derivado cuando se complete la reproducción, no sería un problema. Sin embargo, Python no proporciona un método para matar hilos. Por lo tanto, solo podemos establecer este hilo derivado como un hilo demonio. Cuando el hilo principal sale, el programa solo tendrá un hilo demonio en ejecución, y el hilo demonio se eliminará automáticamente cuando el programa salga.
El código completo de la clase V2Char
es el siguiente:
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)