Зображення в ASCII перетворення мистецтв


102

Пролог

Ця тема час від часу з’являється на переповнюванні стека, але вона видаляється, як правило, через неякісне запитання. Я бачив багато таких питань, а потім мовчав від ОП (звичайно низький реп.), Коли вимагається додаткова інформація. Час від часу, якщо вхід для мене достатньо хороший, я вирішу відповісти відповіддю, і він, як правило, отримує кілька голосів на день, коли він активний, але потім через кілька тижнів питання видаляється / видаляється і все починається з початок. Тож я вирішив написати це запитання, щоб я міг посилатися на подібні питання безпосередньо, не переписуючи відповіді знову і знову ...

Ще одна причина - це мета-нитка, націлена на мене, тому якщо у вас є додатковий внесок, сміливо коментуйте.

Питання

Як я можу перетворити растрове зображення в ASCII мистецтво за допомогою C ++ ?

Деякі обмеження:

  • зображення сірого масштабу
  • використовуючи монорозміщені шрифти
  • простота (не використання занадто сучасних матеріалів для початківців програмістів рівня)

Ось відповідна сторінка Вікіпедії мистецтва ASCII (завдяки @RogerRowland).

Тут схожий на лабіринт ASCII Art Conversion Q&A.


Використовуючи цю сторінку вікі в якості посилання, чи можете ви уточнити, на який тип мистецтва ASCII ви посилаєтесь? Мені це звучить як "Перетворення зображення в текст", що є "простим" переглядом від пікселів у відтінках сірого до відповідного символу тексту, тому мені цікаво, чи маєте ви на увазі щось інше. Здається, ви все одно самі відповісте на це .....
Роджер Роуленд


@RogerRowland як прості (тільки на основі інтенсивності сірого масштабу), так і більш досконалі, враховуючи також форму символів (але все ще досить просту)
Spektre

1
Хоча ваша робота велика, я, безумовно, вдячний за вибірку зразків, які трохи більше SFW.
kmote

@TimCastelijns Якщо ви читаєте пролог, то можете побачити, що це не перший раз, коли просили такого типу відповіді (і більшість виборців починали з тих пір, коли знайомі з кількома попередніми питаннями, тому решта просто проголосували відповідно), оскільки це питання не тільки Q Я не витрачав занадто багато часу на Q- частину (що, зі свого боку, я визнаю), додав кілька обмежень до питання, якщо у вас є кращі, не соромтеся редагувати.
Спектр

Відповіді:


152

Існує більше підходів для перетворення зображень на перетворення мистецтв ASCII, які в основному засновані на використанні моновіддалених шрифтів . Для простоти я дотримуюся лише основ:

На основі інтенсивності пікселів / площі (затінення)

Цей підхід обробляє кожен піксель площі пікселів як одну крапку. Ідея полягає у тому, щоб обчислити середню інтенсивність сірої шкали цієї крапки, а потім замінити її символом з досить близькою інтенсивністю до обчисленої. Для цього нам потрібен перелік корисних символів, кожен з попередньо обчисленою інтенсивністю. Назвемо це персонажем map. Щоб швидше вибрати, який персонаж найкращий, з якою інтенсивністю, є два способи:

  1. Лінійно розподілена карта символів інтенсивності

    Тому ми використовуємо лише символи, які мають різницю інтенсивності з одним і тим же кроком. Іншими словами, при сортуванні за зростанням тоді:

     intensity_of(map[i])=intensity_of(map[i-1])+constant;

    Також, коли наш персонаж mapвідсортований, ми можемо обчислити персонажа безпосередньо за інтенсивністю (не потрібен пошук)

     character = map[intensity_of(dot)/constant];
  2. Довільна карта символів розподіленої інтенсивності

    Таким чином, ми маємо безліч корисних персонажів та їх інтенсивності. Нам потрібно знайти інтенсивність, найближчу до intensity_of(dot)So, знову ж таки, якщо ми відсортували map[], ми можемо використовувати двійковий пошук, інакше нам потрібна O(n)петля пошуку чи мінімального відстані пошуку O(1). Іноді для простоти персонаж map[]може оброблятися як лінійно розподілений, викликаючи незначне спотворення гами, як правило, невидимий в результаті, якщо ви не знаєте, що шукати.

Перетворення на основі інтенсивності чудово підходить і для зображень сірого масштабу (не тільки чорно-білих). Якщо ви виділите крапку в якості одного пікселя, результат виходить великим (один піксель -> один символ), тому для більш великих зображень замість цього вибирається область (множення розміру шрифту), щоб зберегти співвідношення сторін і не збільшувати занадто багато.

Як це зробити:

  1. Рівномірно розділіть зображення на (сірі) пікселі або (прямокутні) області точки s
  2. Обчисліть інтенсивність кожного пікселя / області
  3. Замініть його символом з карти символів з найближчою інтенсивністю

Як персонаж mapможна використовувати будь-які символи, але результат стає кращим, якщо у нього пікселі рівномірно розподілені по області символів. Для початку ви можете використовувати:

  • char map[10]=" .,:;ox%#@";

відсортовано за спаданням і прикидаються лінійним розподілом.

Отже, якщо інтенсивність пікселя / області є i = <0-255>символом заміни

  • map[(255-i)*10/256];

Якщо 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) та виведеним символом. Можна почати з наївної суми абсолютної різниці між пікселями, але це призведе до не дуже хороших результатів, оскільки навіть зсув на один піксель зробить відстань великою. Натомість ви можете використовувати кореляцію чи різні показники. Загальний алгоритм майже такий же, як і попередній підхід:

  1. Таким чином , рівномірно розділити зображення на (напівтонові) прямокутних областях точки «s

    в ідеалі з тим же співвідношенням сторін, що і відображені символи шрифту (це збереже співвідношення сторін. Не забувайте, що символи зазвичай трохи накладаються на вісь x)

  2. Обчисліть інтенсивність кожної області ( dot)

  3. Замініть його символом у персонажа mapз найближчою інтенсивністю / формою

Як ми можемо обчислити відстань між символом і крапкою? Це найважча частина цього підходу. Експериментуючи, я розвиваю цей компроміс між швидкістю, якістю та простотою:

  1. Розділіть зону символів на зони

    Зони

    • Обчисліть окрему інтенсивність для лівої, правої, вгору, вниз та центральної зони кожного символу з алфавіту перетворення ( map).
    • Нормалізувати все інтенсивності, так що вони не залежать від розміру площі, i=(i*256)/(xs*ys).
  2. Обробіть вихідне зображення в прямокутних областях

    • (з тим же співвідношенням сторін, що і цільовий шрифт)
    • Для кожної області обчислюйте інтенсивність так само, як у кулі №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

Тож тепер я можу дивитись навіть відео в ASCIIart для задоволення. Деякі справді приємні :).

Руки

Якщо ви хочете спробувати реалізувати це в GLSL , погляньте на це:


30
Ви тут зробили неймовірну роботу! Дякую! І я люблю цензуру ASCII!
Андер Бігурі

1
Пропозиція щодо вдосконалення: розробляйте похідні похідні, а не лише інтенсивність.
Якк - Адам Невраумон

1
@Yakk хочете допрацювати?
tariksbl

2
@tarik або відповідають не тільки по інтенсивності, але і по похідних: або, межі покращення пропускання смуги. В основному інтенсивність - це не єдине, що бачать люди: вони бачать градієнти та краї.
Якк - Адам Невраумон

1
@Yakk підрозділ зон робить подібне щось опосередковано. Можливо, ще краще було б зробити символи обробки як 3x3зони та порівняти DCT , але я думаю, що це значно знизить продуктивність.
Спектр
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.