创建V2Char类
接下来我们关注V2Char
类,它继承自CharFrame
类。
首先,思考一下我们的类。它有一个属性charVideo
,这是一个列表,用于存储字符动画的所有数据。
然后我们有两个主要方法:一个是genCharVideo()
方法,用于将视频文件转换为字符动画;另一个是play()
方法,用于播放字符动画。
此外,由于从视频转换为字符动画是一个耗时的过程,我们可以从charVideo
中导出字符动画数据,以便将来播放。这意味着我们需要导出和加载方法,即export()
方法和load()
方法。
该类还需要一个初始化方法,它以要读取的文件路径作为参数。如果文件是导出的txt文件,它将调用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) ## 使用`cv2.VideoCapture()`方法读取视频文件
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() ## 释放资源
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()
方法。如前所述,为了防止在播放字符动画后终端被无用字符填满,我们可以使用光标定位转义码。
我们可以这样做:在输出每一帧后,将光标移动到播放的开头,下一帧将从这个位置输出并自动覆盖上一帧的内容。重复这个过程直到播放完成,然后清除最后输出的帧,这样终端就不会被字符艺术填满。
以下是一系列光标定位转义码(某些终端可能不支持某些转义码),取自《Linux命令行》:
转义码 |
操作 |
\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()
不太方便。最好的解决方案是使用类似于C语言中getchar()
的方法。然而,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') ## 添加换行符以分隔每一帧
def load(self, filepath):
self.charVideo = []
for i in open(filepath): ## 每一行是一帧
self.charVideo.append(i[:-1])
def play(self, stream = 1):
## 问题:
## 光标定位转义码与Windows不兼容
if not self.charVideo:
return
if stream == 1 and os.isatty(sys.stdout.fileno()): ## 如果是标准输出且其文件描述符指向一个终端
self.streamOut = sys.stdout.write
self.streamFlush = sys.stdout.flush
elif stream == 2 and os.isatty(sys.stderr.fileno()): ## 如果是标准错误输出且其文件描述符指向一个终端
self.streamOut = sys.stderr.write
self.streamFlush = sys.stderr.flush
elif hasattr(stream, 'write'): ## 如果它有一个write属性
self.streamOut = stream.write
self.streamFlush = stream.flush
old_settings = None
breakflag = None
fd = sys.stdin.fileno() ## 获取标准输入的文件描述符
def getChar():
nonlocal breakflag
nonlocal old_settings
old_settings = termios.tcgetattr(fd) ## 保存标准输入的属性
tty.setraw(sys.stdin.fileno()) ## 将标准输入设置为原始模式
ch = sys.stdin.read(1) ## 读取一个字符
breakflag = True if ch else False
## 创建一个线程
getchar = threading.Thread(target=getChar)
getchar.daemon = True ## 将其设置为守护线程
getchar.start() ## 启动守护线程
rows = len(self.charVideo[0])//os.get_terminal_size()[0] ## 输出字符艺术中的行数
for frame in self.charVideo:
if breakflag is True: ## 如果接收到输入则退出循环
break
self.streamOut(frame)
self.streamFlush()
time.sleep(self.timeInterval)
self.streamOut('\033[{}A\r'.format(rows-1)) ## 向上移动`rows-1`行到开头
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) ## 将标准输入恢复到其原始属性
self.streamOut('\033[{}B\033[K'.format(rows-1)) ## 向下移动`rows-1`行到最后一行并清除它
for i in range(rows-1): ## 从倒数第二行开始清除最后一帧的所有行
self.streamOut('\033[1A')
self.streamOut('\r\033[K')
info = 'User interrupt!\n' if breakflag else 'Finished!\n'
self.streamOut(info)