В данной статье будет рассмотрен прогресс от ЧБ картинки в консоли до 24 bit изображения в такой последовательности

  1. ЧБ

  2. 48 цветов

  3. 216 цветов

  4. 24bit

моя плюшевая игрушка на фоне монитора, изображение в консоли
моя плюшевая игрушка на фоне монитора, изображение в консоли
Исходное изображение для конвертации
Исходное изображение для конвертации

Текстовые изображения

Необходимо создать таблицу символов по возрастающей яркости

block_table: list[str] = [" ", "▂", "▃", "▄",
                          "▅", "▆", "▇", "█"]

Для работы с изображениями импортируйте PIL.Image

Подготовьте изображение для конвертации:

  • Переведите изображение в RGB

  • Измените размер изображения по желанию

  • Сделайте копию изображения в Grayscale

from PIL.Image import Image

size: tuple[int, int] = (..., ...)
image: Image = Image.open("filepath.extension").resize(size)
image = image.convert("RGB")
image_grayscale: Image = image.convert("L")

Напишем простую функцию перевода яркости пикселя в символ нашей таблицы

Через функцию
def bright_to_symbol(bright: int) -> str:
  return block_table[round(bright / 255 * (len(block_table) - 1))]
Через лямбду
bright_to_symbol: typing.Callable[[int], str] = \
	lambda bright: block_table[
  	round(bright / 255 * (len(block_table) - 1))]

Проходимся по grayscale копии и конвертируем яркость в символы, выводя всё в консоль

for i in range(image.height):
  for j in range(image.width):
    print(bright_to_symbol(
      	 image_grayscale.getpixel((j, i))),
         end = "")
	print()
  
Скриншот из майнкрафта с шейдерами
Скриншот из майнкрафта с шейдерами

48 цветов

8 и 16 цветов рассматривать вообще бессмысленно

Для достижения 48 цветов из 16 имеющихся в стандартных терминалах нужно использовать стили BRIGHT и DIM, чтобы к каждому цвету прибавить 2 варианта с данными стилями

Создаём палитру такого плана

colors: typing.Dict[str, typing.List[str]] = \
    {"GREEN": [colorama.Style.DIM + colorama.Fore.GREEN,
               colorama.Fore.GREEN,
               colorama.Style.BRIGHT + colorama.Fore.GREEN,
               colorama.Style.DIM + colorama.Fore.LIGHTGREEN_EX,
               colorama.Fore.LIGHTGREEN_EX, colorama.Style.BRIGHT +
               colorama.Fore.LIGHTGREEN_EX
               ],
		... : [...]
    }

Прописываем цветовые границы

def color_it48(color: typing.Tuple[int, int, int]) -> str:
    """48 colors"""
    if all([col > 240 for col in color]) \
            and color[0] * 3 - 10 < sum(color) < color[0] * 3 + 10:
        return colors["WHITE"][
            round((len(colors["WHITE"]) - 1)
                  / 255 * (color[1] + color[2]) / 2)]
    if all([col < 30 for col in color]) \
            and color[0] * 3 - 10 < sum(color) < color[0] * 3 + 10:
        return colors["BLACK"][
            round((len(colors["BLACK"]) - 1)
                  / 255 * (color[1] + color[2]) / 2)]
    if max(color) == color[1] and color[1] > color[0] + color[2] - 20:
        return colors["GREEN"][
            round((len(colors["GREEN"]) - 1)
                  / 255 * color[1])]
    if max(color) == color[0] and color[0] > sum(color[1:3]) - 20:
        return colors["RED"][
            round((len(colors["RED"]) - 1)
                  / 255 * color[0])]
    if max(color) == color[2] and color[2] > sum(color[0:2]) - 20:
        return colors["BLUE"][
            round((len(colors["BLUE"]) - 1)
                  / 255 * color[0])]
    if color[1] + color[2] > color[0] * 2 + 40:
        return colors["CYAN"][
            round((len(colors["CYAN"]) - 1)
                  / 255 * (color[1] + color[2]) / 2)]
    if color[0] + color[2] > color[1] * 2 + 40:
        return colors["MAGENTA"][
            round((len(colors["MAGENTA"]) - 1)
                  / 255 * (color[2] + color[1]) / 2)]
    if sum(color[0:2]) > color[2] * 2 + 40:
        return colors["YELLOW"][
            round((len(colors["YELLOW"]) - 1)
                  / 255 * (color[1] + color[0]) / 2)]
    return ""
  

Добавляем цвета в символьный вариант

for i in range(0, image.height):
  for j in range(0, image.width):
    print(color_it48(image.getpixel((j, i))) +
          bright_to_symbol(image_grayscale.getpixel((j, i))),
          end='')
  print()
48 цветов, выглядит страшно
48 цветов, выглядит страшно

216 цветов

Для данной расцветки ваш терминал должен поддерживать xterm-256colors

https://robotmoon.com/256-colors/
Если рассмотреть RGB значения цветов, то можно найти простую последовательность, сохраняем

pal: typing.List[int] = [0, 95, 135, 175, 215, 255]

Для приведения RGB цветов к xterm-256colors номеру цвета напишем функцию, которая определит к каким значениям ближе всего цвет
Например (100, 100, 100) -> [1, 1, 1] т.е (95, 95, 95)

def get_pal(color: typing.Tuple[int, int, int]) -> typing.List[int]:
    """Get nearest value of pal to color's rgb"""
    col_data: typing.List[int] = []
    for col in color:
        added: bool = False
        for i in enumerate(pal[1:]):
            added = False
            if (col - pal[i[0]]) / (i[1] - pal[i[0]]) < 0.5:
                col_data.append(i[0])
                added = True
                break
        if not added:
            col_data.append(len(pal) - 1)
    return col_data
  

Теперь нужно перевести эти индексы в номер xterm цвета, не забываем что первые 16 цветов заняты и не относятся к последовательности

def color_it216(color: typing.Tuple[int, int, int]) -> str:
    """216 colors"""
    color_data: typing.List[int] = get_pal(color)
    color_num: int = sum([6 ** (len(color_data) - index - 1) * data
                          for index, data in enumerate(color_data)])
    return f"\033[38;05;" \
           f"{16 + color_num }m"
Выглядит куда приятнее
Выглядит куда приятнее

24bit

На удивление самая простая часть, поддерживается огромное кол-во терминалов
https://gist.github.com/XVilka/8346728

def color_it_full(color: typing.Tuple[int, int, int]) -> str:
    """Full rgb"""
    return f"\033[38;02;{color[0]};{color[1]};{color[2]}m"

В принципе и всё:D

Вот это красота
Вот это красота

Заключение

Исходники (там также есть показ гифок в консоли): https://github.com/LedinecMing/console_images

Цветные круги всех вариантов

Комментарии (17)


  1. andreymal
    14.11.2021 02:55
    +5

    Тема SIXEL не раскрыта


    1. Dimsml
      14.11.2021 13:40

      Ещё был ReGIS для векторной графики.


    1. vtb_k
      14.11.2021 15:25

      А также iTerm Image Protocol реализованный в wezterm эмуляторе.


      wezterm imgcat


    1. kt97679
      14.11.2021 22:59

      Про sixel я некоторое время назад писал: habr.com/ru/post/543594


  1. mwizard
    14.11.2021 04:00
    +2

    Даже первую картинку можно было бы сделать куда приятнее, если бы использовать дизеринг. К тому же такая прямолинейная таблица яркости символов не оставляет пространства для маневров - куда интереснее было бы использовать всю палитру допустимых знаков ASCII, и осуществлять дизеринг с учетом того, как глиф заполняет знакоместо.


    1. LedinecMing Автор
      14.11.2021 14:59

      По ссылке на гитхаб вместе с таблицей из статьи есть таблица из 23 симвлолов, да она выглядит лучше, но она слишком тусклая

      Дизеринг я посмотрю и попробую реализовать


  1. AlB80
    14.11.2021 05:24
    +1

    Яркость всегда контролируется символом. Как следствие полоски и HDR эффект на 24битной версии. Даже 48 цветная могла бы получше выглядеть, если учитывать яркость в цвете.

    Деление целого на целое "bright / 255" меня напрягает тем, что работает у автора.


    1. SemmZemm
      14.11.2021 11:03
      +2

      Python 3, наверное, там по умолчанию деление целого на целое не целочисленное. Целочисленное деление //


    1. LedinecMing Автор
      14.11.2021 14:58

      48 цветная графика очевидно плохо реализована, я попробую сегодня изменить способ определения цвета в ней


  1. orekh
    14.11.2021 14:56

    Я возможно проглядел, но, где исходное изображение?


    1. LedinecMing Автор
      14.11.2021 14:56

      Да, совсем забыл добавить


    1. LedinecMing Автор
      14.11.2021 15:23
      +1

      По неким причинам загрузка новых изображений в редакторе у меня вызывает ошибку
      Причина была в слишком большом файле, добавил исходную картинку в начало статьи


  1. marlishink
    16.11.2021 17:13

    Отличная статья, спасибо большое, жаль мало таких статей на Хабре.


    1. LedinecMing Автор
      16.11.2021 17:13

      Звучит как ирония:)


  1. x2v0
    17.11.2021 17:51

    https://github.com/LedinecMing/console_images/blob/main/console_images/__init__.py

    Не понял?

    if__name__=="__main__":

    Наверное, надо это добавить в __main__.py?


    1. LedinecMing Автор
      17.11.2021 20:07

      Точно, сейчас исправлю)


  1. COKPOWEHEU
    20.11.2021 15:48

    Не хватает сравнения с libaa / libcaca, они разные символы используют, не только квадраты.