我是一名刚接触 C++ 的学生,我想编写一些代码来习惯这种语言。不久前,我编写了一个将 ASCII 艺术制作为“视频”的 Python 程序。基本上,它将视频的每一帧转换为存储在列表中的字符串,然后将它们显示在终端上。但计算速度太慢,所以我决定改用 C++。最后,我通过切换到 C++ 成功地加快了转换过程。
但是,当我尝试在终端上打印 ASCII 帧时,我注意到出现了一个奇怪的卡顿问题,而 Python 则不会出现这种问题(请参阅链接): 视频链接
这是处理打印的 C++ 代码片段。
(...)
auto start_time(chrono::high_resolution_clock::now());
int current_index(0);
int last_index(0);
while (current_index < number_of_frames - 1) {
// Computing the current index
double elapsed_time(chrono::duration_cast<chrono::milliseconds>(chrono::high_resolution_clock::now() - start_time).count() / 1000.);
double percentage(elapsed_time / audio_duration);
current_index = percentage * number_of_frames;
// Print the new frame if it's different from the last one to avoid spamming them
if (current_index != last_index) {
_write(_fileno(stdout), frames[current_index].c_str(), frames[current_index].size());
}
last_index = current_index;
}
(...)
为了与Python进行比较,这里是该代码的Python版本。它使用
time
和 sys
模块。
(...)
start_time = time.time()
current_frame = 0 # nombre de frames passées
last_frame = 0
while current_frame < len(frames) - 1:
elapsed_time = time.time() - start_time
percentage = (elapsed_time / float(audio_duration))
current_frame = int(percentage * number_of_frames)
if current_frame != last_frame:
sys.stdout.write(frames[current_frame])
last_frame = current_frame
(...)
虽然这似乎是一个非常具体的问题,但在这种情况下,Python 比 C++ 工作得更好是没有任何意义的。谷歌也没有帮助,所以我的源代码中一定有问题。
我尝试调用不同类型的函数,如
std::cout
、std::prinf
和原始 write
,甚至对帧数据进行了一些修改,例如调整其中一些数据的大小,但无济于事。我真的不知道是什么导致了这个问题。
以下是在 C++ 中重现该问题的一些步骤(伪代码/概要):
terminal_height*terminal_width
才能覆盖整个屏幕。它可以是任何东西,但我建议使用易于识别的模式,例如 ASCII 艺术。这是完整的 C++ 代码。它使用 OpenCV 和 FFmpeg。我还没有尝试优化任何东西。
#include <iostream>
#include <filesystem>
#include <Windows.h>
#include <fstream>
#include <string>
#include <cmath>
#include <vector>
#include <chrono>
#include <io.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>
#pragma comment (lib, "winmm.lib")
using namespace std;
namespace fs = filesystem;
// Prototypes
size_t number_of_files(fs::path path);
int main()
{
// Defining the grey scale ramp
string char_ramp("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\" ^ `'. ");
reverse(char_ramp.begin(), char_ramp.end());
// Ask user for input about the video
string video_name("");
int brightness(0);
cout << "Mettez le terminal en plein ecran." << endl;
cout << "Nom de la video et seuil de luminosite : ";
cin >> video_name >> brightness;
// Get terminal info
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
const unsigned int target_frame_width(csbi.srWindow.Right - csbi.srWindow.Left + 1);
const unsigned int target_frame_height(csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
// Get video and audio path
const string video_path("videos/" + video_name);
const string audio_path("tmp/audio.wav");
cout << "[1/3] Nettoyage..." << endl;
// Delete previous folders and make new ones for the new video
fs::remove_all(audio_path);
fs::remove_all("tmp/frames");
fs::create_directory("tmp/frames");
cout << "[2/3] Extraction des images et de l'audio..." << endl;
// Ffmpeg extracts frames and audio
system(("ffmpeg -loglevel warning -i " + video_path + " -vf scale=" + to_string(target_frame_width) + ":" + to_string(target_frame_height) + " tmp/frames/%0d.bmp").c_str());
system(("ffmpeg -loglevel warning -i " + video_path + " tmp/audio.wav").c_str());
cout << "[3/3] Conversion..." << endl;
const size_t number_of_frames(number_of_files("tmp/frames"));
vector<string> frames(number_of_frames);
// Frame to ASCII frame conversion
for (int frame_index(1); frame_index <= number_of_frames; ++frame_index) {
string frame_path("tmp/frames/" + to_string(frame_index) + ".bmp");
string current_frame("");
cv::Mat current_bmp(cv::imread(frame_path));
for (int i(0); i < current_bmp.rows; ++i) {
for (int j(0); j < current_bmp.cols; ++j) {
// Le vecteur des couleurs B, G, R du pixel (i, j)
cv::Vec3b bgr(current_bmp.at<cv::Vec3b>(i, j));
// On utilise la luminance
double greyscale_index(sqrt(0.299*bgr[2]*bgr[2] + 0.587*bgr[1]*bgr[1] + 0.114*bgr[0] *bgr[0]));
current_frame += char_ramp[(int)(greyscale_index / 255 * (char_ramp.size() - 1)) / brightness];
}
current_frame += "\n";
}
frames[frame_index - 1] = current_frame;
}
// Grabbing audio duration
FILE* fp;
char var[40];
double audio_duration(0);
fp = _popen(("ffprobe -loglevel warning -i " + audio_path + " -show_entries format=duration -v quiet -of csv=\"p=0\"").c_str(), "r");
while (fgets(var, sizeof(var), fp) != NULL) {
audio_duration = atof(var);
}
_pclose(fp);
PlaySound(TEXT("E:/Programmation/Cpp/SeuillageImage/tmp/audio.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP);
// Begin printing frames
auto start_time(chrono::steady_clock::now());
int current_index(0);
int last_index(0);
while (current_index < number_of_frames - 1) {
// Computing the current index
double elapsed_time(chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - start_time).count() / 1000.);
double percentage(elapsed_time / audio_duration);
current_index = percentage * number_of_frames;
// Print the new frame if it's different from the last one to avoid spamming them
if (current_index != last_index) {
_write(_fileno(stdout), frames[current_index].c_str(), frames[current_index].size());
}
last_index = current_index;
}
// Stop audio
PlaySound(NULL, NULL, 0);
return 0;
}
size_t number_of_files(fs::path path) {
return distance(fs::directory_iterator(path), fs::directory_iterator{});
}
这是该代码的 Python 版本。它使用 Python 成像库 (
PIL
) 和可选的 progress.bar
模块。它比 C++ 代码慢得多。
import argparse
import os
import shutil
import subprocess
import time
import sys
import wave
import winsound
from math import sqrt
from PIL import Image
from progress.bar import IncrementalBar
parser = argparse.ArgumentParser()
parser.add_argument("video_name", type=str, help="Nom de la vidéo")
parser.add_argument("brightness", type=int, help="Seuil de luminosité")
arguments = parser.parse_args()
# http://paulbourke.net/dataformats/asciiart/
# "Standard" character ramp for grey scale pictures from black to white
char_ramp = """$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'. """[::-1]
video_name = arguments.video_name
brightness = arguments.brightness
video_path = os.path.join(os.getcwd(), fr"videos\{video_name}")
audio_path = os.path.join(os.getcwd(), fr"tmp\audio.wav")
# ÉTAPE 1 : On supprime les fichiers déjà existants
print("[INFO] Étape 1 sur 3 : nettoyage...")
# On vérifie s'il existe déjà un dossier de stockage temporaire
if os.path.isdir("tmp"):
# On supprime le dossier contenant les images de la vidéo et on en crée un nouveau
if os.path.isdir("tmp/frames"):
shutil.rmtree("tmp/frames", ignore_errors=False)
os.mkdir("tmp/frames")
# Idem pour le fichier audio correspondant
if os.path.isfile("tmp/audio.wav"):
os.remove("tmp/audio.wav")
# Sinon, on crée un nouveau dossier de stockage temporaire
else:
os.mkdir("tmp")
os.mkdir("tmp/frames")
# ÉTAPE 2 : On extrait les images successives de la vidéo avec ffmpeg une par une pour les convertir
print("[INFO] Étape 2 sur 3 : extraction des images et de l'audio...")
# On définit la longueur et la largeur de la fenêtre d'exécution
window_size = os.get_terminal_size()
target_frame_width = window_size[0] - 1
target_frame_height = window_size[1]
# On utilise le module externe ffmpeg pour extraire les images successives de la vidéo
subprocess.call(
f"ffmpeg -loglevel warning -i {video_path} -vf scale={target_frame_width}:{target_frame_height} tmp/frames/%0d.bmp")
subprocess.call(f"ffmpeg -loglevel warning -i {video_path} tmp/audio.wav")
# ÉTAPE 3 : On convertit la vidéo en ASCII
print("[INFO] Étape 3 sur 3 : conversion de la vidéo en cours, cela peut prendre un certain temps...")
# On crée une liste dans laquelle on stocke les images et quelques autres variables utiles
frames = []
number_of_frames = len(
[frame for frame in os.listdir("tmp/frames") if os.path.isfile(os.path.join("tmp/frames", frame))])
bar = IncrementalBar("Progression", max=number_of_frames)
for frame_index in range(1, number_of_frames + 1):
frame_path = os.path.join(os.getcwd(), fr"tmp\frames\{frame_index}.bmp")
# On construit l'image ASCII à partir de niveaux de gris dans l'image bitmap redimensionnée obtenue
frame_builder = ""
with Image.open(frame_path) as frame:
# Pour chaque pixel de l'image
for y in range(frame.height):
for x in range(frame.width):
# Conversion de chaque pixel en niveau de gris
rgb = frame.getpixel((x, y))
# On utilise la luminance plutôt que la moyenne RGB
greyscale_index = sqrt(0.299 * rgb[0] ** 2 + 0.587 * rgb[1] ** 2 + 0.114 * rgb[2] ** 2)
# On ajoute le caractère ASCII à la chaine une fois identifié (mapping linéaire)
frame_builder += char_ramp[int(greyscale_index / 255 * (len(char_ramp) - 1)) // brightness]
# On revient à la ligne pour chaque ligne traitée
frame_builder += "\n"
# On ajoute l'image Ascii crée à la liste des images de la vidéo
frames.append(frame_builder)
bar.next()
bar.finish()
# On initialise les fichiers audio et vidéo pour des calculs de fréquence
audio_wave = wave.open(audio_path, "rb")
audio_sample_rate = audio_wave.getframerate()
audio_frames = audio_wave.getnframes()
audio_duration = audio_frames / audio_sample_rate
# On joue la musique et on lance le chrono
winsound.PlaySound(audio_path, winsound.SND_ASYNC)
start_time = time.time()
# On imprime les frames en fonction de leur position dans le temps par rapport à l'audio (synchronisation)
current_frame = 0 # nombre de frames passées
last_frame = 0
while current_frame < len(frames) - 1:
elapsed_time = time.time() - start_time
percentage = (elapsed_time / float(audio_duration))
current_frame = int(percentage * number_of_frames)
if current_frame != last_frame:
sys.stdout.write(frames[current_frame])
last_frame = current_frame
我希望这些额外信息可以帮助您更好地理解我的问题。
基本上,cmd.exe 对于我的程序来说太慢了。我切换到 Windows Terminal,它工作得很好。我还必须调整光标的位置,使其保持在 (0, 0) 以防止屏幕滚动。