Create the V2Char class
Then our focus is on the V2Char
class, which inherits from the CharFrame
class.
First, let's think about our class. One of its attributes is charVideo
, which is a list used to store all the data for the character animation.
Then we have two main methods: one is the genCharVideo()
method, which converts a video file into a character animation, and the other is the play()
method, which plays the character animation.
Additionally, since converting from video to character animation is a time-consuming process, we can export the character animation data from charVideo
to facilitate future playback. This means we need export and load methods, namely the export()
method and the load()
method.
The class also needs an initialization method that takes the file path to be read as a parameter. If the file is a exported txt file, it will call the load()
method to load the data into the charVideo
attribute. Otherwise, it will be treated as a video file and will call the genCharVideo()
method to convert the video into a character animation and store it in the charVideo
attribute.
class V2Char(CharFrame):
def __init__(self, path):
if path.endswith('txt'):
self.load(path)
else:
self.genCharVideo(path)
Next is the genCharVideo()
method:
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
The cv2.VideoCapture()
method is used to read the video file, and the returned object is assigned to cap
.
Using the cap.get()
method, we can obtain the video's properties, such as cap.get(3)
and cap.get(4)
which return the width and height of the video, and cap.get(5)
which returns the frame rate of the video. cap.get(7)
returns the total number of frames in the video.
timeInterval
stores the playback time interval, used to make the frame rate of the character animation same as the original video.
pyprind.prog_bar()
is a generator that outputs a progress bar in the terminal when iterating.
cap.read()
reads the next frame of the video. It returns a tuple with two elements. The first element is a boolean value indicating whether the frame was read correctly, and the second element is a numpy.ndarray
containing the frame's data.
cv2.cvtColor()
is used to convert the color space of the image. The first parameter is the image object, and the second parameter indicates the conversion type. There are over 150 color space conversions in OpenCV. Here, we use the color to gray conversion cv2.COLOR_BGR2GRAY
.
os.get_terminal_size()
returns the current terminal's column count (width) and row count (height). We set the fill
parameter to True
and did not set the wrap
parameter, so it defaults to False
. In the terminal, if the printed characters exceed the width of one line, the terminal will automatically wrap the display.
Finally, don't forget to release the resource with cap.release()
.
Next is the play()
method. As mentioned earlier, in order to prevent the terminal from being filled with useless characters after playing the character animation, we can use cursor positioning escape codes.
We can do this: after outputting each frame, move the cursor to the beginning of the playback, and the next frame will be output from this position and automatically overwrite the previous content. Repeat this process until the playback is complete, then clear the last frame that was output, so that the terminal will not be filled with character art.
Here's a series of cursor positioning escape codes (some terminals may not support some escape codes), taken from The Linux Command Line:
Escape Code |
Action |
\033[l;cH |
Move the cursor to line l , column c . |
\033[nA |
Move the cursor up n lines. |
\033[nB |
Move the cursor down n lines. |
\033[nC |
Move the cursor forward n characters. |
\033[nD |
Move the cursor backward n characters. |
\033[2J |
Clear the screen and move the cursor to (0, 0). |
\033[K |
Clear from the cursor position to the end of the current line. |
\033[s |
Save the current cursor position. |
\033[u |
Restore the previous saved cursor position. |
There is another problem, how to stop the playback halfway? Of course, you can press Ctrl + C
, but in this way, the program cannot do any cleanup work and the terminal will be filled with a bunch of useless characters.
We designed it in this way: when the character animation starts playing, it starts a daemon thread that waits for user input. Once it receives input, it stops playing the character animation.
There are two things to note here:
- Do not use the
input()
method to receive character input.
- Cannot use normal threads.
For the first point, if you want to stop the playback by pressing any character, then you should not use input()
, otherwise you either have to press Enter to stop playback, or press another character and then press Enter to stop playback. In short, using input()
is not comfortable. The best solution is to use something similar to the getchar()
method in the C language. However, Python does not provide a similar method, and an alternative solution will be provided in the code later.
For the second point, we need to understand that if any derived thread is still running, the main thread will not exit unless the derived thread is set as a daemon thread. So if we use a normal thread and the user does not stop it halfway, it will continue to run until the animation is finished, and then it will run forever until the user enters any character. If we can manually kill this derived thread when the playback is complete, it is not a problem. However, Python does not provide a method to kill threads. Therefore, we can only set this derived thread as a daemon thread. When the main thread exits, the program will only have a daemon thread running, and the daemon thread will be killed automatically when the program exits.
The complete code for the V2Char
class is as follows:
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)