Показываем видео в терминале

от автора

Приветствую, сегодня я опробую OpenCV, библиотеку для работы с видео, на примере простой задачи — символами ASCII вывести видеоролик в терминал.

Те, кто ей пользовались, могут сказать, что я забиваю дрелью гвозди — создана она для работы с алгоритмами компьютерного зрения.

Начнем с алгоритма, он вполне интуитивен:

  1. Загружаем видео

  2. Покадрово по нему проходимся, пока кадры не закончатся, для каждого кадра:

    1. Делаем черно-белым

    2. Скейлим его до нужных нам размеров (размеров консоли)

    3. Перебираем пиксели слева направо, сверху вниз, для каждого пикселя:

      1. Получаем его яркость

      2. Ставим в соответствие его яркости символ, который имеет схожую яркость (более яркий символ — значит содержит в себе больше пикселей)

      3. Записываем полученный символ в строку для вывода

    4. Выводим эту строку

Перейдем к делу:

Для удобства пояснение будет в виде комментариев.

Подключаем необходимые библиотеки, 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/")  

Итог:

Видео:

Оно же, но в консоли:


ссылка на оригинал статьи https://habr.com/ru/articles/768456/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *