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

7dda088eda0352a07a479f863525b734

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подключаем необходимые библиотеки:

#include  
#include 
#include 
#include 
#include  // уже догадались, зачем нужны chrono и thread :D?

Загружаем видео в объект VideoCapture:

cv::VideoCapture video_capture("/path/to/video"));

Прописываем все константы:

// Размеры консоли (в символах):
constexpr int screen_width = 500; 
constexpr int screen_height = 150;

// Продолжительность кадра в мс и егор размер:
const int frame_duration = 1000 / video_capture.get(cv::CAP_PROP_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);

Теперь напишем функцию для того, чтобы получить символ, соотв. интенсивности пикселя (она у нас будет от 0 до 255):

std::string get_ASCII_from_pixel(const int pixelintensity) {
  std::string chars_by_brightness = "$@B%8&#*/|-_+:,.  ";
  // std::reverse(chars_by_brightness.begin(), chars_by_brightness.end()); // для инверсии при желании
  std::string return_string(
      1,
      chars_by_brightness[pixelintensity * chars_by_brightness.length() / 255]);
  return return_string;
}

Можно перейти к основному циклу:

// Объекты, которые будем использовать в цикле
// cv::Mat - n-мерный массив, в который будем загружать кадр

cv::Mat original_frame, grayscaled_frame, grayscaled_resized_frame;
std::string output;

  for (;;) {
    // оператор >> возвращает нам следующий кадр видео, удобно реализовано
    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) {
        output +=
            get_ASCII_from_pixel(grayscaled_resized_frame.at(x, y));
      }
      output += '\n'; // не забываем
    }
    std::system("clear"); // очищаем консоль (работает на linux)
    std::cout << output << std::flush; // выводим кадр

    // кадр будет в консоли столько времени, сколько он пробыл на видео
    std::this_thread::sleep_for(std::chrono::milliseconds(frame_duration)); 
    output.clear();
  }

Весь код:

src/main.cpp

#include 
#include 
#include 
#include 
#include 

const std::string getpathto(const char *file) {
  std::stringstream ss;
  ss << RESOURCES_PATH << file;
  return ss.str();
}

std::string get_ASCII_from_pixel(const int pixelintensity) {
  std::string chars_by_brightness = "$@B%8&#*/|-_+:,.  ";
  std::reverse(chars_by_brightness.begin(), chars_by_brightness.end());
  std::string return_string(
      1,
      chars_by_brightness[pixelintensity * chars_by_brightness.length() / 255]);
  return return_string;
}

int main() {
  std::ios_base::sync_with_stdio(false);
  const std::string video_path = getpathto("vid56.mov");
  cv::VideoCapture video_capture(video_path);

  if (!video_capture.isOpened()) {
    std::cerr << "Failed to open video file.\n";
    return -1;
  }

  constexpr int screen_width = 500;
  constexpr int screen_height = 150;
  const int frame_duration = 1000 / video_capture.get(cv::CAP_PROP_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);

  cv::Mat original_frame, grayscaled_frame, grayscaled_resized_frame;
  std::string output;

  for (;;) {
    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) {
        output +=
            get_ASCII_from_pixel(grayscaled_resized_frame.at(x, y));
      }
      output += '\n';
    }
    std::system("clear");
    std::cout << output << std::flush;
    std::this_thread::sleep_for(std::chrono::milliseconds(frame_duration));
    output.clear();
  }

  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(VTK REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(MyExecutable src/main.cpp) 
target_link_libraries(MyExecutable ${OpenCV_LIBS} ${VTK_LIBRARIES})
target_compile_definitions(MyExecutable PUBLIC RESOURCES_PATH="${CMAKE_CURRENT_SOURCE_DIR}/resources/")

Итог:

Видео:

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

© Habrahabr.ru