Есть и другие подходы к преобразованию изображений в ASCII art, которые в основном основаны на использовании монотонных шрифтов . Для простоты я придерживаюсь только основ:
На основе интенсивности пикселей / области (затенение)
Этот подход обрабатывает каждый пиксель области пикселей как одну точку. Идея состоит в том, чтобы вычислить среднюю интенсивность серой шкалы этой точки и затем заменить ее символом с интенсивностью, достаточно близкой к вычисленной. Для этого нам понадобится список используемых персонажей, каждый с заранее вычисленной интенсивностью. Назовем это персонажем map
. Чтобы быстрее выбрать, какой персонаж лучше всего подходит для какой интенсивности, есть два способа:
Карта характера с линейно распределенной интенсивностью
Поэтому мы используем только символы, у которых есть разница в интенсивности на одном и том же шаге. Другими словами, при сортировке по возрастанию:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Кроме того, когда наш персонаж map
отсортирован, мы можем вычислить его непосредственно по интенсивности (поиск не требуется).
character = map[intensity_of(dot)/constant];
Карта характера произвольно распределенной интенсивности
Итак, у нас есть набор используемых персонажей и их интенсивность. Нам нужно найти интенсивность, наиболее близкую к интенсивности. intensity_of(dot)
Итак, снова, если мы отсортировали map[]
, мы можем использовать двоичный поиск, в противном случае нам понадобится O(n)
цикл или O(1)
словарь минимального расстояния поиска . Иногда для простоты характер map[]
можно рассматривать как линейно распределенный, вызывая небольшое гамма-искажение, обычно невидимое в результате, если вы не знаете, что искать.
Преобразование на основе интенсивности также отлично подходит для изображений в оттенках серого (а не только для черно-белых). Если вы выберете точку как один пиксель, результат станет большим (один пиксель -> один символ), поэтому для больших изображений вместо этого выбирается область (умноженная на размер шрифта), чтобы сохранить соотношение сторон и не увеличивать слишком сильно.
Как это сделать:
- Равномерно разделите изображение на (полутоновые) пиксели или (прямоугольные) области, точки s
- Вычислить интенсивность каждого пикселя / области
- Замените его символом из карты персонажей с наиболее близкой интенсивностью
В качестве персонажа map
вы можете использовать любые символы, но результат улучшается, если у персонажа есть пиксели, равномерно распределенные по области символа. Для начала можно использовать:
char map[10]=" .,:;ox%#@";
отсортированы по убыванию и претендуют на линейное распределение.
Таким образом, если интенсивность пикселя / области равна, i = <0-255>
то заменяющий символ будет
Если i==0
тогда пиксель / область черный, если i==127
тогда пиксель / область серые, и если i==255
тогда пиксель / область белые. Вы можете экспериментировать с разными персонажами внутри map[]
...
Вот мой древний пример на C ++ и VCL:
AnsiString m = " .,:;ox%#@";
Graphics::TBitmap *bmp = new Graphics::TBitmap;
bmp->LoadFromFile("pic.bmp");
bmp->HandleType = bmDIB;
bmp->PixelFormat = pf24bit;
int x, y, i, c, l;
BYTE *p;
AnsiString s, endl;
endl = char(13); endl += char(10);
l = m.Length();
s ="";
for (y=0; y<bmp->Height; y++)
{
p = (BYTE*)bmp->ScanLine[y];
for (x=0; x<bmp->Width; x++)
{
i = p[x+x+x+0];
i += p[x+x+x+1];
i += p[x+x+x+2];
i = (i*l)/768;
s += m[l-i];
}
s += endl;
}
mm_log->Lines->Text = s;
mm_log->Lines->SaveToFile("pic.txt");
delete bmp;
Вам нужно заменить / игнорировать VCL, если вы не используете среду Borland / Embarcadero .
mm_log
это памятка, в которой выводится текст
bmp
это входное растровое изображение
AnsiString
это строка типа VCL, индексированная с 1, а не с 0, как char*
!!!
Это результат: Пример изображения с небольшой интенсивностью NSFW
Слева находится изображение ASCII art (размер шрифта 5 пикселей), а справа входное изображение увеличено в несколько раз. Как видите, на выходе больше пиксель -> символ. Если вы используете большие области вместо пикселей, тогда масштаб будет меньше, но, конечно, результат будет менее приятным визуально. Этот подход очень легко и быстро кодировать / обрабатывать.
Когда вы добавляете более сложные вещи, например:
- автоматизированные вычисления карт
- автоматический выбор пикселя / размера области
- корректировка соотношения сторон
Затем вы можете обрабатывать более сложные изображения с лучшими результатами:
Вот результат в соотношении 1: 1 (увеличьте масштаб, чтобы увидеть символы):
Конечно, при выборке площади вы теряете мелкие детали. Это изображение того же размера, что и в первом примере с областями:
Расширенный пример изображения с небольшой интенсивностью NSFW
Как видите, это больше подходит для изображений большего размера.
Подгонка символов (гибрид между штриховкой и сплошным рисунком ASCII)
Этот подход пытается заменить область (без точек в один пиксель) символом с аналогичной интенсивностью и формой. Это приводит к лучшим результатам даже при использовании шрифтов большего размера по сравнению с предыдущим подходом. С другой стороны, этот подход, конечно, немного медленнее. Есть и другие способы сделать это, но основная идея состоит в том, чтобы вычислить разницу (расстояние) между областью изображения ( dot
) и визуализированным символом. Вы можете начать с наивной суммы абсолютной разницы между пикселями, но это приведет к не очень хорошим результатам, потому что даже сдвиг на один пиксель сделает расстояние большим. Вместо этого вы можете использовать корреляцию или другие показатели. Общий алгоритм почти такой же, как и в предыдущем подходе:
Таким образом , равномерно разделить изображение на (полутоновые) прямоугольных областях точки «s
в идеале с тем же соотношением сторон, что и отображаемые символы шрифта (это сохранит соотношение сторон. Не забывайте, что символы обычно немного перекрываются по оси x)
Вычислите интенсивность каждой области ( dot
)
Замените его персонажем персонажа map
с наиболее близкой интенсивностью / формой
Как мы можем вычислить расстояние между символом и точкой? Это самая сложная часть этого подхода. Экспериментируя, я выработал компромисс между скоростью, качеством и простотой:
Разделите область персонажа на зоны
- Вычислите отдельную интенсивность для левой, правой, верхней, нижней и центральной зоны каждого символа из вашего алфавита преобразования (
map
).
- Нормализовать все интенсивности, чтобы они не зависели от размера области
i=(i*256)/(xs*ys)
.
Обработка исходного изображения в прямоугольных областях
- (с тем же соотношением сторон, что и целевой шрифт)
- Для каждой области вычислите интенсивность так же, как в маркере №1.
- Найдите ближайшее совпадение по интенсивности в алфавите преобразования
- Вывести подобранный символ
Это результат для размера шрифта = 7 пикселей.
Как видите, результат визуально приятен даже при использовании большего размера шрифта (предыдущий пример подхода был с размером шрифта 5 пикселей). Размер выходного изображения примерно такой же, как у входного изображения (без увеличения). Лучшие результаты достигаются, потому что символы ближе к исходному изображению не только по интенсивности, но и по общей форме, и поэтому вы можете использовать более крупные шрифты и при этом сохранять детали (до определенного момента, конечно).
Вот полный код приложения преобразования на основе VCL:
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Graphics::TBitmap *bmp=new Graphics::TBitmap;
//---------------------------------------------------------------------------
class intensity
{
public:
char c; // Character
int il, ir, iu ,id, ic; // Intensity of part: left,right,up,down,center
intensity() { c=0; reset(); }
void reset() { il=0; ir=0; iu=0; id=0; ic=0; }
void compute(DWORD **p,int xs,int ys,int xx,int yy) // p source image, (xs,ys) area size, (xx,yy) area position
{
int x0 = xs>>2, y0 = ys>>2;
int x1 = xs-x0, y1 = ys-y0;
int x, y, i;
reset();
for (y=0; y<ys; y++)
for (x=0; x<xs; x++)
{
i = (p[yy+y][xx+x] & 255);
if (x<=x0) il+=i;
if (x>=x1) ir+=i;
if (y<=x0) iu+=i;
if (y>=x1) id+=i;
if ((x>=x0) && (x<=x1) &&
(y>=y0) && (y<=y1))
ic+=i;
}
// Normalize
i = xs*ys;
il = (il << 8)/i;
ir = (ir << 8)/i;
iu = (iu << 8)/i;
id = (id << 8)/i;
ic = (ic << 8)/i;
}
};
//---------------------------------------------------------------------------
AnsiString bmp2txt_big(Graphics::TBitmap *bmp,TFont *font) // Character sized areas
{
int i, i0, d, d0;
int xs, ys, xf, yf, x, xx, y, yy;
DWORD **p = NULL,**q = NULL; // Bitmap direct pixel access
Graphics::TBitmap *tmp; // Temporary bitmap for single character
AnsiString txt = ""; // Output ASCII art text
AnsiString eol = "\r\n"; // End of line sequence
intensity map[97]; // Character map
intensity gfx;
// Input image size
xs = bmp->Width;
ys = bmp->Height;
// Output font size
xf = font->Size; if (xf<0) xf =- xf;
yf = font->Height; if (yf<0) yf =- yf;
for (;;) // Loop to simplify the dynamic allocation error handling
{
// Allocate and initialise buffers
tmp = new Graphics::TBitmap;
if (tmp==NULL)
break;
// Allow 32 bit pixel access as DWORD/int pointer
tmp->HandleType = bmDIB; bmp->HandleType = bmDIB;
tmp->PixelFormat = pf32bit; bmp->PixelFormat = pf32bit;
// Copy target font properties to tmp
tmp->Canvas->Font->Assign(font);
tmp->SetSize(xf, yf);
tmp->Canvas->Font ->Color = clBlack;
tmp->Canvas->Pen ->Color = clWhite;
tmp->Canvas->Brush->Color = clWhite;
xf = tmp->Width;
yf = tmp->Height;
// Direct pixel access to bitmaps
p = new DWORD*[ys];
if (p == NULL) break;
for (y=0; y<ys; y++)
p[y] = (DWORD*)bmp->ScanLine[y];
q = new DWORD*[yf];
if (q == NULL) break;
for (y=0; y<yf; y++)
q[y] = (DWORD*)tmp->ScanLine[y];
// Create character map
for (x=0, d=32; d<128; d++, x++)
{
map[x].c = char(DWORD(d));
// Clear tmp
tmp->Canvas->FillRect(TRect(0, 0, xf, yf));
// Render tested character to tmp
tmp->Canvas->TextOutA(0, 0, map[x].c);
// Compute intensity
map[x].compute(q, xf, yf, 0, 0);
}
map[x].c = 0;
// Loop through the image by zoomed character size step
xf -= xf/3; // Characters are usually overlapping by 1/3
xs -= xs % xf;
ys -= ys % yf;
for (y=0; y<ys; y+=yf, txt += eol)
for (x=0; x<xs; x+=xf)
{
// Compute intensity
gfx.compute(p, xf, yf, x, y);
// Find the closest match in map[]
i0 = 0; d0 = -1;
for (i=0; map[i].c; i++)
{
d = abs(map[i].il-gfx.il) +
abs(map[i].ir-gfx.ir) +
abs(map[i].iu-gfx.iu) +
abs(map[i].id-gfx.id) +
abs(map[i].ic-gfx.ic);
if ((d0<0)||(d0>d)) {
d0=d; i0=i;
}
}
// Add fitted character to output
txt += map[i0].c;
}
break;
}
// Free buffers
if (tmp) delete tmp;
if (p ) delete[] p;
return txt;
}
//---------------------------------------------------------------------------
AnsiString bmp2txt_small(Graphics::TBitmap *bmp) // pixel sized areas
{
AnsiString m = " `'.,:;i+o*%&$#@"; // Constant character map
int x, y, i, c, l;
BYTE *p;
AnsiString txt = "", eol = "\r\n";
l = m.Length();
bmp->HandleType = bmDIB;
bmp->PixelFormat = pf32bit;
for (y=0; y<bmp->Height; y++)
{
p = (BYTE*)bmp->ScanLine[y];
for (x=0; x<bmp->Width; x++)
{
i = p[(x<<2)+0];
i += p[(x<<2)+1];
i += p[(x<<2)+2];
i = (i*l)/768;
txt += m[l-i];
}
txt += eol;
}
return txt;
}
//---------------------------------------------------------------------------
void update()
{
int x0, x1, y0, y1, i, l;
x0 = bmp->Width;
y0 = bmp->Height;
if ((x0<64)||(y0<64)) Form1->mm_txt->Text = bmp2txt_small(bmp);
else Form1->mm_txt->Text = bmp2txt_big (bmp, Form1->mm_txt->Font);
Form1->mm_txt->Lines->SaveToFile("pic.txt");
for (x1 = 0, i = 1, l = Form1->mm_txt->Text.Length();i<=l;i++) if (Form1->mm_txt->Text[i] == 13) { x1 = i-1; break; }
for (y1=0, i=1, l=Form1->mm_txt->Text.Length();i <= l; i++) if (Form1->mm_txt->Text[i] == 13) y1++;
x1 *= abs(Form1->mm_txt->Font->Size);
y1 *= abs(Form1->mm_txt->Font->Height);
if (y0<y1) y0 = y1; x0 += x1 + 48;
Form1->ClientWidth = x0;
Form1->ClientHeight = y0;
Form1->Caption = AnsiString().sprintf("Picture -> Text (Font %ix%i)", abs(Form1->mm_txt->Font->Size), abs(Form1->mm_txt->Font->Height));
}
//---------------------------------------------------------------------------
void draw()
{
Form1->ptb_gfx->Canvas->Draw(0, 0, bmp);
}
//---------------------------------------------------------------------------
void load(AnsiString name)
{
bmp->LoadFromFile(name);
bmp->HandleType = bmDIB;
bmp->PixelFormat = pf32bit;
Form1->ptb_gfx->Width = bmp->Width;
Form1->ClientHeight = bmp->Height;
Form1->ClientWidth = (bmp->Width << 1) + 32;
}
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
load("pic.bmp");
update();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete bmp;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
draw();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseWheel(TObject *Sender, TShiftState Shift, int WheelDelta, TPoint &MousePos, bool &Handled)
{
int s = abs(mm_txt->Font->Size);
if (WheelDelta<0) s--;
if (WheelDelta>0) s++;
mm_txt->Font->Size = s;
update();
}
//---------------------------------------------------------------------------
Это простая форма application ( Form1
) с единственным TMemo mm_txt
в ней. Он загружает изображение, "pic.bmp"
а затем в зависимости от разрешения выбирает, какой подход использовать для преобразования в текст, который сохраняется "pic.txt"
и отправляется в заметку для визуализации.
Для тех, у кого нет VCL, игнорируйте материал VCL и заменяйте его AnsiString
любым строковым типом, который у вас есть, а также Graphics::TBitmap
любым имеющимся у вас классом растрового изображения или изображения с возможностью доступа к пикселям.
Очень важно отметить, что здесь используются настройки mm_txt->Font
, поэтому убедитесь, что вы установили:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
чтобы это работало правильно, иначе шрифт не будет обрабатываться как одинарный. Колесо мыши просто изменяет размер шрифта вверх / вниз, чтобы увидеть результаты для разных размеров шрифта.
[Ноты]
- См. Визуализацию Word Portraits
- Используйте язык с возможностью доступа к растровым изображениям / файлам и вывода текста
- Я настоятельно рекомендую начать с первого подхода, так как он очень простой, понятный и простой, и только затем переходить ко второму (что может быть сделано как модификация первого, поэтому большая часть кода в любом случае остается как есть)
- Рекомендуется выполнять вычисления с инвертированной интенсивностью (черные пиксели - максимальное значение), поскольку стандартный предварительный просмотр текста находится на белом фоне, что приводит к гораздо лучшим результатам.
- вы можете поэкспериментировать с размером, количеством и расположением зон подразделения или использовать
3x3
вместо этого какую-нибудь сетку .
Сравнение
Наконец, вот сравнение двух подходов к одному и тому же входу:
Зеленая точка , выделенные изображения сделаны с подъездным # 2 и красными с # 1 , всеми по размеру шрифта в шесть пикселей. Как вы можете видеть на изображении лампочки, подход с учетом формы намного лучше (даже если №1 сделан на исходном изображении с 2-кратным увеличением).
Классное приложение
Читая сегодняшние новые вопросы, я получил представление о классном приложении, которое захватывает выбранную область рабочего стола и непрерывно передает ее конвертеру ASCIIart и просматривает результат. После часа написания кода все готово, и я настолько доволен результатом, что мне просто нужно добавить его сюда.
ОК, приложение состоит всего из двух окон. Первое главное окно - это в основном мое старое окно конвертера без выбора изображения и предварительного просмотра (в нем есть все, что указано выше). В нем есть только предварительный просмотр и настройки преобразования ASCII. Второе окно представляет собой пустую форму с прозрачной внутри для выбора области захвата (никакой функциональности).
Теперь по таймеру я просто захватываю выделенную область формой выбора, передаю ее в преобразование и просматриваю ASCIIart .
Таким образом, вы заключаете область, которую хотите преобразовать, в окно выбора и просматриваете результат в главном окне. Это может быть игра, вьювер и т.д. Выглядит это так:
Так что теперь я могу смотреть даже видео в ASCIIart для развлечения. Некоторые действительно хороши :).
Если вы хотите попробовать реализовать это в GLSL , взгляните на это: