Criar a classe V2Char
Então, nosso foco está na classe V2Char, que herda da classe CharFrame.
Primeiro, vamos pensar sobre nossa classe. Um de seus atributos é charVideo, que é uma lista usada para armazenar todos os dados para a animação de caracteres.
Então, temos dois métodos principais: um é o método genCharVideo(), que converte um arquivo de vídeo em uma animação de caracteres, e o outro é o método play(), que reproduz a animação de caracteres.
Além disso, como a conversão de vídeo para animação de caracteres é um processo demorado, podemos exportar os dados da animação de caracteres de charVideo para facilitar a reprodução futura. Isso significa que precisamos de métodos de exportação e carregamento, ou seja, o método export() e o método load().
A classe também precisa de um método de inicialização que recebe o caminho do arquivo a ser lido como um parâmetro. Se o arquivo for um arquivo txt exportado, ele chamará o método load() para carregar os dados no atributo charVideo. Caso contrário, ele será tratado como um arquivo de vídeo e chamará o método genCharVideo() para converter o vídeo em uma animação de caracteres e armazená-lo no atributo charVideo.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
Em seguida, vem o método genCharVideo():
def genCharVideo(self, filepath):
self.charVideo = []
cap = cv2.VideoCapture(filepath) ## Use o método `cv2.VideoCapture()` para ler o arquivo de vídeo
self.timeInterval = round(1/cap.get(5), 3) ## Obtenha a taxa de quadros do vídeo
nf = int(cap.get(7)) ## Obtenha o número total de quadros no vídeo
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) ## Converta o quadro bruto em um quadro de caracteres
self.charVideo.append(frame)
cap.release() ## Libere o recurso
O método cv2.VideoCapture() é usado para ler o arquivo de vídeo, e o objeto retornado é atribuído a cap.
Usando o método cap.get(), podemos obter as propriedades do vídeo, como cap.get(3) e cap.get(4) que retornam a largura e a altura do vídeo, e cap.get(5) que retorna a taxa de quadros do vídeo. cap.get(7) retorna o número total de quadros no vídeo.
timeInterval armazena o intervalo de tempo de reprodução, usado para tornar a taxa de quadros da animação de caracteres igual à do vídeo original.
pyprind.prog_bar() é um gerador que gera uma barra de progresso no terminal durante a iteração.
cap.read() lê o próximo quadro do vídeo. Ele retorna uma tupla com dois elementos. O primeiro elemento é um valor booleano indicando se o quadro foi lido corretamente, e o segundo elemento é um numpy.ndarray contendo os dados do quadro.
cv2.cvtColor() é usado para converter o espaço de cores da imagem. O primeiro parâmetro é o objeto da imagem, e o segundo parâmetro indica o tipo de conversão. Existem mais de 150 conversões de espaço de cores no OpenCV. Aqui, usamos a conversão de cor para cinza cv2.COLOR_BGR2GRAY.
os.get_terminal_size() retorna a contagem de colunas (largura) e a contagem de linhas (altura) do terminal atual. Definimos o parâmetro fill como True e não definimos o parâmetro wrap, então ele assume o padrão False. No terminal, se os caracteres impressos excederem a largura de uma linha, o terminal irá automaticamente quebrar a exibição.
Finalmente, não se esqueça de liberar o recurso com cap.release().
Em seguida, vem o método play(). Como mencionado anteriormente, para evitar que o terminal seja preenchido com caracteres inúteis após a reprodução da animação de caracteres, podemos usar códigos de escape de posicionamento do cursor.
Podemos fazer isso: após a saída de cada quadro, mover o cursor para o início da reprodução, e o próximo quadro será exibido a partir desta posição e substituirá automaticamente o conteúdo anterior. Repita este processo até que a reprodução seja concluída, em seguida, limpe o último quadro que foi exibido, para que o terminal não seja preenchido com arte de caracteres.
Aqui está uma série de códigos de escape de posicionamento do cursor (alguns terminais podem não suportar alguns códigos de escape), retirados de The Linux Command Line:
| Código de Escape |
Ação |
| \033[l;cH |
Move o cursor para a linha l, coluna c. |
| \033[nA |
Move o cursor para cima n linhas. |
| \033[nB |
Move o cursor para baixo n linhas. |
| \033[nC |
Move o cursor para frente n caracteres. |
| \033[nD |
Move o cursor para trás n caracteres. |
| \033[2J |
Limpa a tela e move o cursor para (0, 0). |
| \033[K |
Limpa da posição do cursor até o final da linha atual. |
| \033[s |
Salva a posição atual do cursor. |
| \033[u |
Restaura a posição do cursor salva anteriormente. |
Há outro problema, como parar a reprodução no meio do caminho? Claro, você pode pressionar Ctrl + C, mas dessa forma, o programa não pode fazer nenhum trabalho de limpeza e o terminal será preenchido com um monte de caracteres inúteis.
Nós o projetamos desta forma: quando a animação de caracteres começa a ser reproduzida, ela inicia uma thread daemon que espera pela entrada do usuário. Assim que recebe a entrada, ela para de reproduzir a animação de caracteres.
Há duas coisas a serem observadas aqui:
- Não use o método
input() para receber a entrada de caracteres.
- Não pode usar threads normais.
Para o primeiro ponto, se você deseja parar a reprodução pressionando qualquer caractere, então você não deve usar input(), caso contrário, você terá que pressionar Enter para parar a reprodução, ou pressionar outro caractere e, em seguida, pressionar Enter para parar a reprodução. Em suma, usar input() não é confortável. A melhor solução é usar algo semelhante ao método getchar() na linguagem C. No entanto, o Python não fornece um método semelhante, e uma solução alternativa será fornecida no código posterior.
Para o segundo ponto, precisamos entender que, se alguma thread derivada ainda estiver em execução, a thread principal não sairá, a menos que a thread derivada seja definida como uma thread daemon. Portanto, se usarmos uma thread normal e o usuário não a interromper no meio do caminho, ela continuará a ser executada até que a animação termine, e então ela será executada para sempre até que o usuário insira qualquer caractere. Se pudermos matar manualmente esta thread derivada quando a reprodução for concluída, não haverá problema. No entanto, o Python não fornece um método para matar threads. Portanto, só podemos definir esta thread derivada como uma thread daemon. Quando a thread principal sair, o programa terá apenas uma thread daemon em execução, e a thread daemon será morta automaticamente quando o programa sair.
O código completo para a classe V2Char é o seguinte:
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') ## Adicione um caractere de nova linha para separar cada quadro
def load(self, filepath):
self.charVideo = []
for i in open(filepath): ## Cada linha é um quadro
self.charVideo.append(i[:-1])
def play(self, stream = 1):
## Bug:
## Códigos de escape de posicionamento do cursor são incompatíveis com o Windows
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()): ## Se for uma saída padrão e seu descritor de arquivo se referir a um terminal
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()): ## Se for uma saída de erro padrão e seu descritor de arquivo se referir a um terminal
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'): ## Se tiver um atributo de escrita
self.streamOut = stream.write
self.streamFlush = stream.flush
old_settings = None
breakflag = None
fd = sys.stdin.fileno() ## Obtenha o descritor de arquivo da entrada padrão
def getChar():
nonlocal breakflag
nonlocal old_settings
old_settings = termios.tcgetattr(fd) ## Salve os atributos da entrada padrão
tty.setraw(sys.stdin.fileno()) ## Defina a entrada padrão para o modo raw
ch = sys.stdin.read(1) ## Leia um caractere
breakflag = True if ch else False
## Crie uma thread
getchar = threading.Thread(target=getChar)
getchar.daemon = True ## Defina como uma thread daemon
getchar.start() ## Inicie a thread daemon
rows = len(self.charVideo[0])//os.get_terminal_size()[0] ## Número de linhas na arte de caracteres de saída
for frame in self.charVideo:
if breakflag is True: ## Saia do loop se a entrada for recebida
break
self.streamOut(frame)
self.streamFlush()
time.sleep(self.timeInterval)
self.streamOut('\033[{}A\r'.format(rows-1)) ## Mova para cima `rows-1` linhas para o início
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) ## Restaure a entrada padrão para seus atributos originais
self.streamOut('\033[{}B\033[K'.format(rows-1)) ## Mova para baixo `rows-1` linhas para a última linha e limpe-a
for i in range(rows-1): ## Limpe todas as linhas do último quadro a partir da penúltima linha
self.streamOut('\033[1A')
self.streamOut('\r\033[K')
info = 'Interrupção do usuário!\n' if breakflag else 'Concluído!\n'
self.streamOut(info)