Приветствую, сегодня я опробую OpenCV, библиотеку для работы с видео, на примере простой задачи - символами ASCII вывести видеоролик в терминал.
Те, кто ей пользовались, могут сказать, что я забиваю дрелью гвозди - создана она для работы с алгоритмами компьютерного зрения.
Начнем с алгоритма, он вполне интуитивен:
Загружаем видео
Покадрово по нему проходимся, пока кадры не закончатся, для каждого кадра:
Делаем черно-белым
Скейлим его до нужных нам размеров (размеров консоли)
Перебираем пиксели слева направо, сверху вниз, для каждого пикселя:
Получаем его яркость
Ставим в соответствие его яркости символ, который имеет схожую яркость (более яркий символ - значит содержит в себе больше пикселей)
Записываем полученный символ в строку для вывода
Выводим эту строку
Перейдем к делу:
Для удобства пояснение будет в виде комментариев.
Подключаем необходимые библиотеки, ncurses будем использовать для работы с выводом в консоль (можно обойтись и без него):
#include <chrono> #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/videoio.hpp> #include <string> #include <thread> // уже догадались, зачем нужны chrono и thread :D? // Сишные библиотеки всегда подключаем после плюсовых #include <curses.h> #include <ncurses.h>
Загружаем видео в объект VideoCapture, не забывая проверить, загрузилось ли видео:
cv::VideoCapture video_capture("/path/to/video")); if (!video_capture.isOpened()) { std::cerr << "Failed to open video file.\n"; return -1; }
Прописываем все константы:
// Эмпирический коэфицент // нужный для сохранения отношения сторон видео при минимальном скейле // моей консоли const float correction_factor = 4.75; // Отрисовывать картинку будем 10 раз в секунду // иначе реальный фреймтайм будет выше того, что в видео const int targetfps = 10; const int fps = video_capture.get(cv::CAP_PROP_FPS); const int fpsdif = fps / targetfps; const int frame_duration = 1000 / fps; // Получаем размеры видео в будущих символах, сохраняя отношение сторон const int frame_width = video_capture.get(cv::CAP_PROP_FRAME_WIDTH); const int frame_height = video_capture.get(cv::CAP_PROP_FRAME_HEIGHT); const int screen_height = 120; const int screen_width = screen_height * (frame_width / frame_height) * correction_factor;
Теперь напишем функцию для того, чтобы получить символ, соотв. интенсивности пикселя (она у нас будет от 0 до 255):
const char get_ASCII_from_pixel(const int pixelintensity) { // Выбор этой строки субъективен, мне нравится так std::string chars_by_brightness = "$@B%8&#*/|(-_+;:,. "; // При желании инвертируем std::reverse(chars_by_brightness.begin(), chars_by_brightness.end()); return chars_by_brightness[static_cast<float>(pixelintensity * chars_by_brightness.length()) / 256.f]; }
Можно перейти к основному циклу:
// будем считать время отрисовки кадра, // чтобы вычесть его из фреймтайма int starttime = 0, endtime = 0, framedrawtime = 0, i = 0; // Объекты, которые будем использовать в цикле // cv::Mat - специальный n-мерный массив, // в который будем загружать кадр (элементы - пиксели) cv::Mat original_frame, grayscaled_frame, grayscaled_resized_frame; initscr(); // переводим терминал в curses режим for (;;) { // засекаем время starttime = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::system_clock::now().time_since_epoch()) .count(); // выкидываем кадры, которые отрисовывать не будем i = fpsdif; while (--i) { video_capture.grab(); } // оператор >> у cv::VideoCapture возвращает нам следующий кадр video_capture >> original_frame; // проверяем, не закончилось ли видео if (original_frame.empty()) break; // проводим манипуляции согласно алгоритму cv::cvtColor(original_frame, grayscaled_frame, cv::COLOR_BGR2GRAY); cv::resize(grayscaled_frame, grayscaled_resized_frame, cv::Size(screen_width, screen_height), 0, 0, cv::INTER_LINEAR); for (int x = 0; x < screen_height; ++x) { for (int y = 0; y < screen_width; ++y) { // перемещаем каретку curses на нужные координаты и помещаем туда символ mvaddch(x, y, get_ASCII_from_pixel(grayscaled_resized_frame.at<uchar>(x, y))); } } // показываем результат в консоли refresh(); endtime = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::system_clock::now().time_since_epoch()) .count(); // столько времени прошло уже framedrawtime = endtime - starttime; // теперь итерация длится сколько нам надо std::this_thread::sleep_for( std::chrono::milliseconds(fpsdif * frame_duration - framedrawtime)); } endwin(); // завершение curses режима
Весь код:
src/main.cpp
#include <chrono> #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/videoio.hpp> #include <string> #include <thread> #include <ncurses.h> const std::string getpathto(const char *file) { std::stringstream ss; ss << RESOURCES_PATH << file; return ss.str(); } const char get_ASCII_from_pixel(const int pixelintensity) { std::string chars_by_brightness = "$@B%8&#*/|(-_+;:,. "; std::reverse(chars_by_brightness.begin(), chars_by_brightness.end()); return chars_by_brightness[static_cast<float>(pixelintensity * chars_by_brightness.length()) / 256.f]; } int main() { cv::VideoCapture video_capture(getpathto("vid1.mp4")); if (!video_capture.isOpened()) { std::cerr << "Failed to open video file.\n"; return -1; } const float correction_factor = 4.75; const int targetfps = 10; const int fps = video_capture.get(cv::CAP_PROP_FPS); const int fpsdif = fps / targetfps; const int frame_duration = 1000 / fps; const int frame_width = video_capture.get(cv::CAP_PROP_FRAME_WIDTH); const int frame_height = video_capture.get(cv::CAP_PROP_FRAME_HEIGHT); const int screen_height = 120; const int screen_width = screen_height * (frame_width / frame_height) * correction_factor; int starttime = 0, endtime = 0, framedrawtime = 0, i = 0; cv::Mat original_frame, grayscaled_frame, grayscaled_resized_frame; initscr(); for (;;) { starttime = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::system_clock::now().time_since_epoch()) .count(); i = fpsdif; while (--i) { video_capture.grab(); } video_capture >> original_frame; if (original_frame.empty()) break; cv::cvtColor(original_frame, grayscaled_frame, cv::COLOR_BGR2GRAY); cv::resize(grayscaled_frame, grayscaled_resized_frame, cv::Size(screen_width, screen_height), 0, 0, cv::INTER_LINEAR); for (int x = 0; x < screen_height; ++x) { for (int y = 0; y < screen_width; ++y) { mvaddch(x, y, get_ASCII_from_pixel(grayscaled_resized_frame.at<uchar>(x, y))); } } refresh(); endtime = std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::system_clock::now().time_since_epoch()) .count(); framedrawtime = endtime - starttime; std::this_thread::sleep_for( std::chrono::milliseconds(fpsdif * frame_duration - framedrawtime)); } endwin(); return 0; }
CMakeLists.txt
cmake_minimum_required(VERSION 3.25) project(ASCII_video) set(OpenCV_DIR /usr/lib/opencv4/opencv2) find_package(OpenCV REQUIRED) find_package(Curses REQUIRED) # find_package(VTK REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS} ${CURSES_INCLUDE_DIR}) add_executable(MyExecutable src/main.cpp) target_link_libraries(MyExecutable ${OpenCV_LIBS} ${VTK_LIBRARIES} ${CURSES_LIBRARIES}) target_compile_definitions(MyExecutable PUBLIC RESOURCES_PATH="${CMAKE_CURRENT_SOURCE_DIR}/resources/")
Итог:
Видео:
Оно же, но в консоли:
