Існує більше підходів для перетворення зображень на перетворення мистецтв ASCII, які в основному засновані на використанні моновіддалених шрифтів . Для простоти я дотримуюся лише основ:
На основі інтенсивності пікселів / площі (затінення)
Цей підхід обробляє кожен піксель площі пікселів як одну крапку. Ідея полягає у тому, щоб обчислити середню інтенсивність сірої шкали цієї крапки, а потім замінити її символом з досить близькою інтенсивністю до обчисленої. Для цього нам потрібен перелік корисних символів, кожен з попередньо обчисленою інтенсивністю. Назвемо це персонажем map
. Щоб швидше вибрати, який персонаж найкращий, з якою інтенсивністю, є два способи:
Лінійно розподілена карта символів інтенсивності
Тому ми використовуємо лише символи, які мають різницю інтенсивності з одним і тим же кроком. Іншими словами, при сортуванні за зростанням тоді:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Також, коли наш персонаж map
відсортований, ми можемо обчислити персонажа безпосередньо за інтенсивністю (не потрібен пошук)
character = map[intensity_of(dot)/constant];
Довільна карта символів розподіленої інтенсивності
Таким чином, ми маємо безліч корисних персонажів та їх інтенсивності. Нам потрібно знайти інтенсивність, найближчу до intensity_of(dot)
So, знову ж таки, якщо ми відсортували 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 (розмір шрифту 5 пікселів), а праворуч вхідне зображення кілька разів збільшується . Як бачите, вихід має більший піксель -> символ. Якщо ви використовуєте більші площі замість пікселів, то масштаб менший, але, звичайно, вихід менше візуально приємний. Цей підхід дуже легко та швидко кодувати / обробляти.
Коли ви додаєте більш досконалі речі, такі як:
- автоматизовані обчислення карт
- автоматичний вибір розміру пікселя / площі
- корекції співвідношення сторін
Тоді ви можете обробити більш складні зображення з кращими результатами:
Ось результат у співвідношенні 1: 1 (масштабування, щоб побачити символи):
Звичайно, для відбору проб ви втрачаєте дрібні деталі. Це зображення такого ж розміру, як перший приклад, відібраний з областями:
Невеликий показник інтенсивності NSFW - розширений приклад зображення
Як бачите, це більше підходить для великих зображень.
Підгонка символів (гібрид між затіненням і суцільним ASCII art)
Цей підхід намагається замінити область (не більше одиничних піксельних точок) символом з подібною інтенсивністю та формою. Це призводить до кращих результатів, навіть якщо використовуються більші шрифти порівняно з попереднім підходом. З іншого боку, такий підхід, звичайно, трохи повільніше. Існує більше способів зробити це, але головна ідея - обчислити різницю (відстань) між областю зображення ( 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();
}
//---------------------------------------------------------------------------
Це простий додаток форми ( 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
- Використовуйте мову з можливістю доступу до растрових зображень / файлів та виводу тексту
- Я настійно рекомендую почати з першого підходу, оскільки це дуже просто і просто, і тільки потім переходити до другого (що можна зробити як модифікацію першого, тому більша частина коду залишається такою, як і в будь-якому випадку)
- Це гарна ідея для обчислення з інвертованою інтенсивністю (чорні пікселі - це максимальне значення), оскільки стандартний попередній перегляд тексту знаходиться на білому тлі, що призводить до набагато кращих результатів.
- ви можете експериментувати з розміром, кількістю та компонуванням зон підрозділу або використовувати якусь сітку на зразок
3x3
.
Порівняння
Нарешті, ось порівняння двох підходів на одному вході:
Зображення, позначені зеленою крапкою, виконуються з підходом №2, а червоні - з №1 , всі розміром шрифту в шість пікселів. Як ви бачите на зображенні лампочки, підхід, залежний від форми, значно кращий (навіть якщо №1 виконано на 2-кратному масштабному вихідному зображенні).
Класне застосування
Під час читання нових запитань сьогодні я отримав уявлення про класну програму, яка захоплює вибрану область робочого столу і постійно подає її до конвертора ASCIIart і переглядає результат. Через годину кодування це зроблено, і я настільки задоволений результатом, що мені просто доведеться додати його сюди.
OK додаток складається з двох вікон. Перше головне вікно - це в основному моє старе вікно перетворювача без вибору зображення та попереднього перегляду (у ньому є все те, що вище). Він має лише параметри попереднього перегляду та перетворення ASCII. Друге вікно - це порожня форма з прозорою внутрішньою стороною для вибору області захоплення (функціональності взагалі немає).
Тепер на таймері я просто захоплюю вибрану область за формою вибору, передаю її конверсії та попередній перегляд ASCIIart .
Таким чином, ви додаєте область, яку ви хочете перетворити, у вікні вибору та переглядаєте результат у головному вікні. Це може бути гра, глядач тощо. Це виглядає приблизно так:
Тож тепер я можу дивитись навіть відео в ASCIIart для задоволення. Деякі справді приємні :).
Якщо ви хочете спробувати реалізувати це в GLSL , погляньте на це: