V2Char クラスの作成
次に焦点を当てるのは、CharFrame クラスを継承した V2Char クラスです。
まず、このクラスについて考えてみましょう。その属性の 1 つは charVideo で、これは文字アニメーションのすべてのデータを格納するためのリストです。
次に、2 つの主要なメソッドがあります。1 つは genCharVideo() メソッドで、動画ファイルを文字アニメーションに変換します。もう 1 つは 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) ## 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() は動画の次のフレームを読み込みます。これは 2 つの要素を持つタプルを返します。最初の要素はフレームが正しく読み込まれたかどうかを示すブール値で、2 番目の要素はフレームのデータを含む numpy.ndarray です。
cv2.cvtColor() は画像の色空間を変換するために使用されます。最初のパラメータは画像オブジェクトで、2 番目のパラメータは変換の種類を示します。OpenCV には 150 以上の色空間変換があります。ここでは、色からグレーへの変換 cv2.COLOR_BGR2GRAY を使用しています。
os.get_terminal_size() は現在のターミナルの列数(幅)と行数(高さ)を返します。fill パラメータを True に設定し、wrap パラメータを設定していないので、デフォルトで False になります。ターミナルでは、印刷された文字が 1 行の幅を超えると、ターミナルが自動的に折り返し表示します。
最後に、cap.release() でリソースを解放するのを忘れないでください。
次は play() メソッドです。前述のように、文字アニメーションの再生後にターミナルが無駄な文字で埋まらないようにするために、カーソル位置決めのエスケープコードを使用することができます。
このようにすることができます。各フレームを出力した後、カーソルを再生の開始位置に移動し、次のフレームはこの位置から出力され、自動的に前の内容を上書きします。このプロセスを再生が完了するまで繰り返し、最後に出力されたフレームをクリアすると、ターミナルが文字アートで埋まることはありません。
以下は一連のカーソル位置決めのエスケープコードです(一部のターミナルは一部のエスケープコードをサポートしていない場合があります)。これらは 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 |
前に保存したカーソル位置を復元します。 |
もう 1 つの問題は、途中で再生を停止するにはどうすればよいかです。もちろん、Ctrl + C を押すことができますが、この方法では、プログラムはクリーンアップ作業を行うことができず、ターミナルはたくさんの無駄な文字で埋まってしまいます。
私たちはこのように設計しました。文字アニメーションの再生が開始されると、ユーザー入力を待つデーモンスレッドが開始されます。入力を受け取ると、文字アニメーションの再生が停止します。
ここで注意すべきことが 2 点あります。
input() メソッドを使用して文字入力を受け取らないでください。
- 通常のスレッドを使用することはできません。
1 点目について、任意の文字を押して再生を停止したい場合は、input() を使用しないでください。そうしないと、再生を停止するには Enter を押すか、別の文字を押してから Enter を押す必要があります。要するに、input() を使用するのは快適ではありません。最善の解決策は、C 言語の getchar() メソッドのようなものを使用することです。しかし、Python は同様のメソッドを提供していません。後でコードで代替策を提供します。
2 点目について、派生スレッドがデーモンスレッドとして設定されていない限り、派生スレッドが実行中であれば、メインスレッドは終了しないことを理解する必要があります。したがって、通常のスレッドを使用し、ユーザーが途中で停止しない場合、アニメーションが終了するまで実行し続け、その後、ユーザーが任意の文字を入力するまで永久に実行されます。再生が完了したときにこの派生スレッドを手動で殺すことができれば問題ありません。しかし、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)