Создание класса V2Char
Теперь наше внимание уделим классу V2Char
, который наследуется от класса CharFrame
.
Сначала подумаем о нашем классе. Одним из его атрибутов является charVideo
, который представляет собой список, используемый для хранения всех данных анимации из символов.
Затем у нас есть два основных метода: один - это метод genCharVideo()
, который преобразует видеофайл в анимацию из символов, а другой - метод play()
, который воспроизводит анимацию из символов.
Кроме того, так как преобразование видео в анимацию из символов является длительным процессом, мы можем экспортировать данные анимации из символов из charVideo
для удобства дальнейшего воспроизведения. Это означает, что нам нужны методы экспорта и загрузки, а именно метод export()
и метод load()
.
Класс также нуждается в методе инициализации, который принимает в качестве параметра путь к файлу, который нужно прочитать. Если файл - это экспортированный текстовый файл, он вызовет метод load()
для загрузки данных в атрибут charVideo
. В противном случае он будет рассматриваться как видеофайл, и будет вызван метод genCharVideo()
для преобразования видео в анимацию из символов и сохранения ее в атрибуте charVideo
.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
Далее идет метод 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
Метод cv2.VideoCapture()
используется для чтения видеофайла, и возвращаемый объект присваивается переменной cap
.
Используя метод cap.get()
, мы можем получить свойства видео, например, cap.get(3)
и cap.get(4)
возвращают ширину и высоту видео, а cap.get(5)
возвращает частоту кадров видео. cap.get(7)
возвращает общее количество кадров в видео.
timeInterval
хранит интервал времени воспроизведения, используемый для того, чтобы частота кадров анимации из символов была такой же, как у исходного видео.
pyprind.prog_bar()
- это генератор, который выводит индикатор прогресса в терминале при итерации.
cap.read()
читает следующий кадр видео. Он возвращает кортеж из двух элементов. Первый элемент - это логическое значение, указывающее, был ли кадр прочитан правильно, а второй элемент - это объект numpy.ndarray
, содержащий данные кадра.
cv2.cvtColor()
используется для преобразования цветового пространства изображения. Первый параметр - это объект изображения, а второй параметр указывает тип преобразования. В OpenCV есть более 150 преобразований цветовых пространств. Здесь мы используем преобразование из цветного в черно-белое cv2.COLOR_BGR2GRAY
.
os.get_terminal_size()
возвращает текущее количество столбцов (ширину) и строк (высоту) терминала. Мы установили параметр fill
в True
и не установили параметр wrap
, поэтому он по умолчанию равен False
. В терминале, если напечатанные символы превышают ширину одной строки, терминал автоматически перенесет отображение.
Наконец, не забудьте освободить ресурсы с помощью cap.release()
.
Далее идет метод play()
. Как упоминалось ранее, чтобы предотвратить заполнение терминала бесполезными символами после воспроизведения анимации из символов, мы можем использовать управляющие последовательности для позиционирования курсора.
Мы можем сделать следующее: после вывода каждого кадра переместить курсор в начало воспроизведения, и следующий кадр будет выведен с этой позиции и автоматически перезапишет предыдущее содержимое. Повторять этот процесс до завершения воспроизведения, затем очистить последний выведенный кадр, чтобы терминал не был заполнен ASCII-артом.
Вот ряд управляющих последовательностей для позиционирования курсора (некоторые терминалы могут не поддерживать некоторые управляющие последовательности), взятых из книги The Linux Command Line:
Управляющая последовательность |
Действие |
\033[l;cH |
Переместить курсор на строку l , столбец c . |
\033[nA |
Переместить курсор вверх на n строк. |
\033[nB |
Переместить курсор вниз на n строк. |
\033[nC |
Переместить курсор вперед на n символов. |
\033[nD |
Переместить курсор назад на n символов. |
\033[2J |
Очистить экран и переместить курсор в позицию (0, 0). |
\033[K |
Очистить от текущей позиции курсора до конца текущей строки. |
\033[s |
Сохранить текущую позицию курсора. |
\033[u |
Восстановить предыдущую сохраненную позицию курсора. |
Есть еще одна проблема: как остановить воспроизведение посередине? Конечно, вы можете нажать Ctrl + C
, но в таком случае программа не сможет выполнить никаких операций по очистке, и терминал будет заполнен кучей бесполезных символов.
Мы спроектировали это так: когда начинается воспроизведение анимации из символов, запускается демонический поток, который ожидает ввода пользователя. Как только он получает ввод, он останавливает воспроизведение анимации из символов.
Вот два момента, на которые нужно обратить внимание:
- Не используйте метод
input()
для приема ввода символов.
- Нельзя использовать обычные потоки.
Что касается первого пункта, если вы хотите остановить воспроизведение, нажав любой символ, то не следует использовать input()
, иначе вам придется нажимать Enter, чтобы остановить воспроизведение, или нажать другой символ, а затем Enter, чтобы остановить воспроизведение. В любом случае, использование input()
неудобно. Лучшее решение - использовать что-то вроде метода getchar()
в языке C. Однако Python не предоставляет аналогичного метода, и альтернативное решение будет предложено в коде позже.
Что касается второго пункта, нужно понимать, что если любой порожденный поток все еще работает, основной поток не завершится, если только порожденный поток не установлен как демонический. Поэтому, если мы используем обычный поток и пользователь не остановит его посередине, он будет продолжать работать до завершения анимации, а затем будет работать вечно, пока пользователь не введет какой-либо символ. Если бы мы могли вручную убить этот порожденный поток при завершении воспроизведения, это не было бы проблемой. Однако Python не предоставляет метода для убийства потоков. Поэтому мы можем только установить этот порожденный поток как демонический. Когда основной поток завершается, в программе будет работать только демонический поток, и демонический поток будет автоматически убит при выходе из программы.
Полный код класса V2Char
выглядит следующим образом:
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)