Конвертируем картинку в ANSI

  • Tutorial
Не знаю, насколько это будет кому-то интересно, но на днях решил поиграться и сделать следующее:

Дано: Картинка (например, BMP) 640 на 400, шрифт 8 на 16

Надо: Перевести ее в ANSI псевдографику в стандартном режиме 80 на 25 символов, символы и фон могут иметь любой цвет (true color).

image

Благодаря очень хорошим комментариям я обновил версию: ссылка
С ней получаются примерно такие картинки: image


Здесь стоит оговориться, что «тру» EGA текстовый режим с 16 цветами нам не подойдет, так как получится слишком некрасиво из-за низкого разрешения и большого размера символов. Второй аргумент: задача не стоИт максимально приблизить картинку ее символьному представлению, так как тогда можно было бы уменьшать шрифт, увеличивать разрешение и т.д. Задача — сделать именно такой фильтр, который описан выше.

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

Для реализации нам понадобится какая-нибудь библиотека для работы с графикой, умеющая getpixel и putpixel, то есть считать цвет пикселя с экрана и нарисовать пиксель заданным цветом на экран. Я пользуюсь старенькой, но проверенной библиотекой Allegro (www.allegro.cc), которая легка в обращении, а также позволяет работать в разных ОС (даже в MSDOS!).

Но хватит слов, пора к делу.

const int size_x=640;
const int size_y=400;
const int ascii_x=80;
const int ascii_y=25;


Устанавливаем константы, отвечающие за размер картинки и размер ее текстового представления.

for(int cx=0;cx<ascii_x;cx++)
	for(int cy=0;cy<ascii_y;cy++){
		int fullr=0;
		int fullg=0;
		int fullb=0;
		int pixels=0;

//find background color
		for(int i=0;i<size_x/ascii_x;i++)
		for(int j=0;j<size_y/ascii_y;j++){
			int col=getpixel(pic,i+cx*size_x/ascii_x,j+cy*size_y/ascii_y);
			int r=getr(col);
			int g=getg(col);
			int b=getb(col);
			fullr+=r;fullg+=g;fullb+=b;
			pixels++;
		}
		fullr/=pixels;fullg/=pixels;fullb/=pixels;


Начинаем основной цикл, в котором последовательно находим каждый символ из 80 на 25 матрицы. Следующий цикл — по каждому пикселю той области картинки, которая соответствует на экране данному символу из этой матрицы. Как легко сообразить, область эта составляет 640/80=8 на 400/25=16 пикселей. Здесь полезно вспомнить, что это как раз равно размеру нашего шрифта.

В этом втором цикле наша задача — найти средний цвет, который мы установим в качестве цвета фона нашего ANSI символа. Сделать это очень просто: раскладываем цвет на R, G, B компоненты и вычисляем среднее по каждой из них.

//find ascii and its color
		int rms=100000000;

		for(int curch=1;curch<256;curch++){ //loop over ASCII table
			int charr=0;
			int charg=0;
			int charb=0;

//find char color
			pixels=0; //number of pixels for char
			for(i=0;i<size_x/ascii_x;i++)
			for(int j=0;j<size_y/ascii_y;j++){
				int col=getpixel(pic,i+cx*size_x/ascii_x,j+cy*size_y/ascii_y);
				int r=getr(col);
				int g=getg(col);
				int b=getb(col);

				int colch=getpixel(font,i+(curch%32)*8,j+(curch/32)*16);
				int rc=getr(colch);
				int gc=getg(colch);
				int bc=getb(colch);
				if(rc!=0&&gc!=0&&bc!=0){ //get actual char pixels only
					charr+=r;
					charg+=g;
					charb+=b;
					pixels++;
				}
			}
			if(pixels!=0){
				charr/=pixels;charg/=pixels;charb/=pixels;
			}


Дальше уже более интересно. Нам приходится вставить еще один цикл — по каждому символу из ASCII таблицы (всего их 256). Я беру их из черно-белой картинки с шрифтом, то есть теоретически, там могут быть любые изображения 8 на 16 в качестве символа. Стоит отметить, что в этом месте можно ограничиться только какой-то одной областью символов (например, кириллицей), что сильно повлияет на результат.

В приведенном выше куске кода мы находим цвет символа. Для этого мы считаем сумму как выше, но с небольшим изменением: пиксели берутся по маске, которой является текущий символ из шрифта. Таким образом, второй «средний цвет» соответствует только пикселям, которыми будет нарисован сам символ.

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

//find rms
			int currms=0;
			for(i=0;i<size_x/ascii_x;i++)
			for(int j=0;j<size_y/ascii_y;j++){
				int col=getpixel(pic,i+cx*size_x/ascii_x,j+cy*size_y/ascii_y);
				int r=getr(col);
				int g=getg(col);
				int b=getb(col);
				int rr=0,gg=0,bb=0;

				int colch=getpixel(font,i+(curch%32)*8,j+(curch/32)*16);
				int rc=getr(colch);
				int gc=getg(colch);
				int bc=getb(colch);
				if(rc!=0&&gc!=0&&bc!=0){ //char pixel
					rr=charr;
					gg=charg;
					bb=charb;
				}
				else{ //back pixel
					rr=fullr;
					gg=fullg;
					bb=fullb;
				}
				currms+=sqrt((r-rr)*(r-rr)+(g-gg)*(g-gg)+(b-bb)*(b-bb));
			}

			if(currms<rms){ //find minimal rms
				findr=charr;
				findg=charg;
				findb=charb;

				findch=curch;
				rms=currms;
				if(DEBUG)printf("!!!%d %d\n",currms, curch);
			}
			else
				if(DEBUG)printf("%d %d\n",currms, curch);

		} //find char


Ну и, наконец, пришли к самой соли. В этом куске кода мы еще раз проходимся по пикселям заданной области картинки и считаем RMS (среднеквадратичное отклонение) всех трех компонент цвета от цвета фона или символа (в зависимости от того, находится ли данный пиксель на фоне или на символе). Суммарный RMS и будет являться критерием того, насколько данный символ хорошо соответствует данной области картинки.

Далее следует вывод на экран (в двух режимах — с фоном и без (пример)), но на нем мы останавливаться не будем, так как там всё самоочевидно.

image

Программу и исходный код можно скачать здесь: dimouse.ru/data/ansiconv.rar (130 Kb).

Как насчет нарисовать это дело в текстовом режиме? Как вы можете увидеть, программа создает файл с результатами (кроме нового bmp) — ascii. Там для каждого символа хранится его значение в ASCII таблице, его цвет и цвет его фона. Я для текстового режима пользуюсь pdcurses.sourceforge.net (пишите в комментариях, если знаете что-то лучше!), но эта библиотека, хоть и является враппером над SDL (во всяком случае, виндовая версия — точно), не умеет показывать цвета больше 16: тяжелое наследие древности. Как вы, наверное, помните, PDCurses берет свое начало от знаменитой Interactive Fiction игры Curses. Но один товарищ написал усовершенствование этой библиотеки и выложил здесь: www.projectpluto.com/win32a.htm

К сожалению, в ней нельзя использовать bitmap шрифт, только системные (зато можно выбрать любой), что несколько портит впечатление. Но задачу также можно считать выполненной.

Программу для чтения ascii можно скачать здесь: dimouse.ru/data/ascii.rar (70 Kb).
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 25

    +2
      +1
      Да, классная вещь:) А так-то вообще даже целый конкурс проводится: en.wikipedia.org/wiki/Text_Mode_Demo_Contest
      Причем фильтр там работает в реалтайме, у меня-то неоптимизировано, одна картинка обрабатывается секунд 10.
        0
        Дык! А вот эта дема так ваще!!! Какая здоровская синхронизация камеры с музыкой и объектами… Немного фликера многовато, но с другой стороны придает особый шарм. Дема, которая западает в памяти. Виндоюзерам советаю все-таки качнуть EXE (около 100 Кб), а не смотреть унылое ютуб-видео.

      +4
      Советую посмотреть исходный код libaa и libcaca
        +2
        Народ больше фанатеет от местной символики.

        А есть ли опция использования без фонового цвета? Тогда результаты пошли бы текстом в комментарии.
          0
          Можно без фона, но придется перекомпилировать (#define ASCII вместо 0 — 1 в самом начале). Но это дело работает плохо, так как какая-то область может выделяться, но подберется именно фоном (с пустым символом) — тогда в результате, при выводе только символа, будет на этом месте пусто. См. пример здесь dimouse.ru/images/s.png по сравнению с картинкой в начале.
          0
          В том и дело, что цвет нужно регулировать только плотностью буквы. А вообще, если использовать псевдографические символы, то вопрос решится. Только непонятно, что за кодировка у ASCII-файла:
          яяяяяя яяяяяя яяяяяя яяяяяя ЯЭжиьь ыАЙЧЭЋ ¬ёЬююэ яяя яяяяяя яяяяяя яяяяяя яяяяяя
          
          — это фрагмент первой строчки, в которой я поставил пробелы на месте невидимых символов. Если вместо «яяяяяяя» будет белый псевдосимвол и т.д., но можно будет отображать картинку в HTML при условии, что у фонта эти псевдосимволы будут. И учесть, что псевдографических символов — 16 и они будут иметь один цвет.
            0
            ascii-файл — это тупо дамп в таком виде: байт — номер символа ascii (я честно говоря, сам не помню какая там кодировка, скорее всего вообще досовская, см. шрифт в файле pdcfont.bmp), 3 байта — RGB компоненты цвета фона, 3 байта — RGB компоненты цвета символа.

            Насчет HTML — я попробую сделать дамп в html.
              0
              Значится отчитываюсь:
              html сохраняет: dimouse.ru/data/ansiconv2.rar

              Но возникают те же проблемы, что я вкратце упоминал выше при использовании pdcurses:
              Поскольку в браузере используются true type шрифты, то вывод не соответствует тому, как происходит подбор. Основная проблема в том, что сам символ и область под фон не равны — область под фон дается больше. И символы, конечно не 8 на 16. И вообще полный отстой получается.
              В лучшем случае можно получить что-то вроде этого:
              image

              Не забудьте, что кодировка используется досовская (CP-866) и ее надо выставить в браузере!
              Как альтернатива — можно использовать только нижнюю часть ASCII (до 128), но тогда будет некрасиво (только буквы).
              Еще я в этой версии не использую символы до 32 (всякие рожицы и т.д.). Их вроде в html тоже можно выводить, но нужно как-то так: амперсант, решетка, номер символа. Я пока поленился добавить.
                0
                >я в этой версии не использую символы до 32 (всякие рожицы и т.д.). Их вроде в html тоже можно выводить

                А нет, нельзя их выводить в html
                  0
                  У меня легко получилось «Habrastorage» вывести в HTML (результат) с помощью 2-й версии кодировщика. Но вопросы остались те же: с ним нельзя вывести картинки только с цветом шрифта, существенно используется цвет бекграунда. Поэтому для местных фанатов не подойдёт.

                  Хабр позволяет задавать цвет шрифта, но не цвет фона. Поэтому можно представить пользу от кодировщика в таком формате (в порядке бреда): юзеру с кармой меньше 0 не разрешают выводить картинки. Тогда он кодирует их в ASCII и выкладывает, обходя недостаток публикатора.

                  Хотя, возможно, что ему не дают никакими тегами пользоваться. Тогда публикация в ASCII могла бы пригодиться для любителей постить жесты типа «лицорука» в тексте (ASCII). Но тогда нужен формат Unicode. Псевдографика в Arial и Verdana поддерживается (я вот про эти: unicode-table.com/ru/sections/block-elements/, не все из них, а 16, из тут разбросали по кодам). Если преследовать эту практическую (но совершенно ненужную) цель, то каждый символ должен идти примерно в такой кодировке:


                  Исходный код:
                  <font color=#eeeeee>&#9608;</font><font color=#dddddd>&#9608;</font><font color=#cccccc>&#9608;</font><font color=#6699ff>&#9600;</font><font color=#6699ff>&#9605;</font><font color=#669999>&#9631;</font><font color=#6699cc>&#9626;</font><font color=#6699ff>&#9622;</font><font color=#aaccff>&#9616;</font><font color=#aaaaff>&#9605;</font><font color=#6699ff>&#9600;</font><font color=#6699ff>&#9605;</font><font color=#669999>&#9631;</font><font color=#cccccc>&#9608;</font><font color=#dddddd>&#9608;</font><font color=#eeeeee>&#9608;</font>
                  Полезность её всё же есть по сравнению с чистым DOSом — представление кроссбраузерное и кроссплатформенное, позтому подтолкнёт кого-то на следующую мысль, особенно, если будет на Гитхабе и генерировать тексты из канваса на JS — у имеющейся программы на С++ я всё равно не заметил скорости, отрисовка картинки проходит за секунд 5-8.
                    0
                    > У меня легко получилось «Habrastorage» вывести в HTML (результат)
                    Скорее всего вывод представлен в неправильной кодировке. Сравните вывод в браузере и в bmp. Но, согласен, вывод в дос-кодировке в html — это извращение жуткое:)

                    Я сделаю версию, ограничившись только этими символами ASCII (их где-то 50 на самом деле) + без фона (попробую поиграться) + добавлю вывод в html в нормальной кодировке. Интересно, что получится.
                      0
                      dimouse.ru/data/ansiconv3.rar — новая версия.

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

                      image

                      P.S. В процессе пришлось немного погуглить для перевода DOS-кодировки в юникод. Возможно, кому-то тоже пригодится: www.lingua-systems.com/knowledge/unicode-mappings/cp866-to-unicode.html
                        0
                        Ой, прошу прощения за невнимательность — этот вариант был тоже с фоном. Обновил архив и добавил туда откомпилированную версию без фона.
                          0
                          Спасибо. Стало почти то, что можно вставлять в форумы: ). Правда, при умолчательных настройках Хрома (Times New Roman) пришлось добавить:
                          <style>td{line-height: 1.1}</style>
                          
                          , чтобы не было белых полос между строками.

                          А в данный коммент тоже напрямую не вставишь, потому что таблицы поддерживаются только в статьях. И вообще, таблицы — тяжелее для отображения, чем строки, поэтому я бы предпочёл вариант со сплошным текстом. Но теперь эту страницу с таблицей можно легко конвертировать скриптом или регекспом и оптимизировать.

                          Хроника оптимизации (одними регекспами) такая: исходный ascii.html 80 на 24 в таблицах — 100.5 К (ни в один комментарий не влезет);
                          * приведение к хабровскому виду (font color) — 68K (многовато для комментария — не так давно даже статьи неявно ограничивали примерно на 42-45 К текста, сейчас не проверял);
                          * замена белых блоков на символы &#9617; (не нашёл пробельного среди псевдографики) — 44 К;
                          * убирание точек с запятой (хак HTML) — 42К;
                          * убрать по 3 символа справа и слева — 41 К;
                          Точечный фон выглядит плохо, поэтому дальше:
                          * Возврат к белым символам — 63К
                          * Оптимизация — объединение одинаковых последовательностей цветов — 42 К
                          (регексп
                          <font color=#ffffff>(.*?)</font><font color=#ffffff>(.*?)</font> на <font color=#ffffff>$1$2</font>
                          
                          несколько раз). Остальные места не слишком пригодны для ручной оптимизации — цвета почти похожи, но отличаются на единицы и не группируются.

                          Псевдокартинку на 42К текста убрал под спойлер, чтобы она не тормозила кому-либо рендеринг.
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░█░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░░░░░░░░█░░░░░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░█░█░█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
                          ░░░░░░░░░░░░░░░░░░░█░█░░░░░░░░░░░░░░░░░░░░░░░░░░░░
                          █░█░█░░█░░░░░░░░░░░░░░░░░░░░░░░░░░
                          █░░░░░░░░░░░░░░░░░░░░█░
                          █░
                          █░
                          █░░░


                          █░
                          █░░█░
                          ░░░░░░░█░░░░░░░░█░░░░░
                          ░░░░░░░░░░░░█░░░░█░█░░░░░░░░░░█░░░░░░░░░░░

                          Судя по картинке, видны направления дальнейшей оптимизации:
                          1) сводить не к миллиону цветов, а к 256-1К, чтобы затем оптимизировать по рядам одинаковых цветов;
                          2) ввести 16 символов псевдографики, чтобы увеличить детальность картинки вдвое без дополнительных затрат символов.
                            0
                            > 1) сводить не к миллиону цветов, а к 256-1К, чтобы затем оптимизировать по рядам одинаковых цветов;
                            Это можно сделать постфактум, как я приводил пример ниже.

                            > 2) ввести 16 символов псевдографики, чтобы увеличить детальность картинки вдвое без дополнительных затрат символов.
                            Сейчас используется 48 символов псевдографики с B0 до E0. Странно, что у вас в выводе ни одного такого не получилось, только полные квадраты. Можно посмотреть исходную картинку?
                0
                Сделайте это в виде web сервиса!
                  +2
                  Понятно, что если программа компилируется и даже делает то что нужно, то сложно желать чего-то большего, но…

                  С точки зрения качества исходного кода и его оформления в посте всё просто ужасно.

                  1. Оформлять части одного и того же цикла разными кусками кода — это жесть. Читается это плохо, особенно сложно уследить за зависимостями между частями цикла.
                  2. Несогласованные отступы, брезгование верхним регистром, фигурными скобками для внешних циклов и пробелами между составными частями выражений, короткие и не однозначно трактуемые имена переменных — такое ощущение, что вы на коленке это делали, не шибко запариваясь о результате. Понятно, что вам этот код как родной и сейчас вы в нём ориентируетесь, но через месяц вы и сами забудете, чем, например, r отличается от rr, а colch от curch.
                  3. Наконец, что мешало выложить его на тот же GitHub, чтобы при желании код можно было увидеть в браузере, а не запариваться со скачиванием архива и извлечением из него всей сути?

                  Я при виде такого кода вспоминаю себя самого в старших классах школы, когда я только учился программировать.

                  PS: Заглянул в ваш профиль, увидел запись «старший научный сотрудник Курчатовского Института» и вспомнил вот этот комментарий про физиков и программирование.
                    +1
                    Виноват, в профиле написано просто «научный сотрудник НИЦ „Курчатовский институт“». Простите мне мою невнимательность и примите пожелания того, чтобы она стала пророческой =).
                      0
                      Спасибо:)
                      +1
                      Спасибо за комментарий!

                      Да, я согласен, что упомянутые выше проблемы с культурой кода у меня имеют место быть. Как туториал такое, конечно, не годится — плохой пример. Могу сказать только, что культуру кода мне никто не прививал, никогда не было начальника, который бы следил за этим, у нас там всё довольно либерально — работает и ладно. И лень, конечно. Если бы работал где-то в IT, то, думаю, меня бы приструнили. А так — лень. И вообще консервативность, хоть я и не из поколения «программистов на фортране» (хотя знаю довольно молодых товарищей, которые продолжают это делать, в Протвино, например). Надеюсь, что я смогу перебороть лень когда-нибудь и буду писать красивый и приятный код и не лениться.

                      P.S. Комментарий про физиков-программистов повеселил. Конечно, не всё так плохо, но общая тенденция схвачена верно. А почему так — я написал выше.
                      P.P.S. Не старший, пока просто нс. Защищусь наконец — буду старшим.
                      0
                      Я кстати не понял насчет EGA. Что там не получится? Какие символы большие и какое разрешение низкое? EGA в текстмоде = VGA в текстмоде. А при умелых ручках еще аппаратно можно такие штуки вытворять, что VGA можно не на всех видеокартах.
                        0
                        Ну так про 16 цветов и про 16 пар вы согласны? Символы я использую 8 на 16, можно использовать 8 на 8, тогда возможностей будет больше. Точно так же можно использовать разрешения 80 на 30 (что не сильно больше, конечно) или SVGA
                        132×25 16 colors VESA-compatible Super VGA
                        132×43 16 colors VESA-compatible Super VGA
                        132×50 16 colors VESA-compatible Super VGA
                        132×60 16 colors VESA-compatible Super VGA
                        Но по религиозным соображениям я не хочу. Ну а вообще исходники есть — можно округлять цвета или подгонять под EGA палитру, или же уже картинку результат в подходящем граф. редакторе подогнать под палитру EGA.
                          0
                          Чтобы не быть голословным, вот быстрый конверт картинки из титульного сообщения в EGA палитру:

                          image

                          Как видно, получается полная ерунда. И надо еще принять во внимание, что в текстмоде это показываться, наверное, не будет, так как цветовых пар фон-символ должно быть тоже 16 (наверное, это обходится, конечно, но в pdcurses так). Для более контрастных изображений, должно быть, получится лучше.
                            +5
                            Похоже на кадр из фильма для взрослых.

                      Only users with full accounts can post comments. Log in, please.