Преобразование изображений в ASCII art


Пролог

эта тема всплывает здесь на так время от времени, но удаляется обычно из-за плохо написанного вопрос. Я видел много таких вопросов, а потом молчание от OP (обычный низкий rep), когда запрашивается дополнительная информация. Время от времени, если вход достаточно хорош для меня, я решаю ответить ответом, и он обычно получает несколько голосов в день, пока активен, но затем через несколько недель вопрос удаляется/удаляется и все начинается с начала. Поэтому я решил написать это Q & A поэтому я могу ссылаться на такие вопросы прямо не переписав ответ снова и снова ...

другая причина-также этот META thread нацелены на меня, так что если у вас есть дополнительный вход, не стесняйтесь комментировать.

вопрос

Как конвертировать растровое изображение в ASCII art используя C++ ?

некоторые ограничения:

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

вот соответствующая страница Вики ASCII art (спасибо @RogerRowland)

1 92

1 ответ:

есть больше подходов для преобразования изображения в ASCII art, которые в основном основаны на использовании моноширинный шрифт для простоты я придерживаюсь только к основам:

пиксель / интенсивность области на основе (затенение)

этот подход обрабатывает каждый пиксель области пикселей как одну точку. Идея состоит в том, чтобы вычислить среднюю интенсивность серой шкалы этой точки, а затем заменить ее символом с достаточно близкой интенсивностью к вычисленной. Для этого нам нужен некоторый список полезных символов каждый с предварительно вычисленной интенсивностью пусть назовем его character map. Чтобы быстрее выбрать, какой символ является лучшим для какой интенсивности есть два способа:

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

    поэтому мы используем только символы, которые имеют разницу интенсивности с тем же шагом. Другими словами, при сортировке по возрастанию:

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

    также, когда наш персонаж map сортируется, то мы можем вычислить символ непосредственно из интенсивности (не требуется поиск)

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

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

преобразование на основе интенсивности отлично подходит также для серых изображений (не только черно-белых). Если вы выберете точку как один пиксель, результат будет большим (1 пиксель -> один символ), поэтому для больших изображений вместо этого выбирается область (умноженная на размер шрифта), чтобы сохранить соотношение сторон и не увеличивать слишком много.

как делать:

  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 art output (размер шрифта 5px), а справа входное изображение увеличено в несколько раз. Как вы можете видеть, выход больше пиксель - > символ. если вы используете большие области вместо пикселей, то масштаб меньше, но, конечно, выход менее визуально приятен. этот подход очень прост и быстр для кодирования/обработки.

когда вы добавляете более продвинутые вещи, как:

  • автоматизированные карте вычисления
  • автоматический выбор размера пикселя / области
  • коррекция соотношения сторон

затем вы можете обрабатывать более сложные изображения с лучшими результатами:

здесь результат в соотношении 1:1 (увеличить, чтобы увидеть символы):

intensity advanced example

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

слегка NSFW интенсивность расширенный пример изображения

как вы можете видеть, что это больше подходит для больших изображений

подгонка символов (гибрид между затенением и твердым ASCII-искусством)

этот подход пытается заменить область (не более одной пиксельной точки) символом с аналогичной интенсивностью и формой. Это приводит к лучшим результатам даже при использовании более крупных шрифтов по сравнению с предыдущим подходом с другой стороны, этот подход немного медленнее, конечно. Есть больше способов сделать это, но основная идея состоит в том, чтобы вычислить разницу (расстояние) между областью изображения (dot) и оказанные характер. Вы можете начать с наивной суммы разницы abs между пикселями, но это приведет к не очень хорошим результатам, потому что даже сдвиг 1 пикселя сделает расстояние большим, вместо этого вы можете использовать корреляцию или разные метрики. Общий алгоритм почти такой же, как и предыдущий подход:

  1. так равномерно разделите изображение на (серые) прямоугольные области точка ' s
    • в идеале с тем же соотношением сторон, что и rendered символы шрифта (это сохранит соотношение сторон, не забывайте, что символы обычно перекрываются немного по оси x)
  2. вычислить интенсивность каждой области (dot)
  3. замените его символом из символа map С ближайшей интенсивность/форма

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

  1. разделить область символов на зоны

    zones

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

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

это результат для размера шрифта = 7px

char fitting example

как вы можете видеть, вывод визуально приятен даже при использовании большего размера шрифта (предыдущий пример подхода был с размером шрифта 5px). Выход имеет примерно тот же размер, что и входное изображение (без увеличения). Лучшие результаты достигаются потому, что герои ближе к исходному изображению не только интенсивности, но и по общей форме и поэтому вы можете использовать более крупные шрифты и сохраняя детали (вплоть до крупных).

здесь полный код для приложения преобразования на основе 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) // charcter 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;     // temp 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 init buffers
        tmp=new Graphics::TBitmap; if (tmp==NULL) break;
            // allow 32bit 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 image by zoomed character size step
        xf-=xf/3; // characters are usually overlaping 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 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" и отправлено в memo для визуализации. Для тех, кто без VCL игнорировать VCL вещи и заменить AnsiString С любым типом строки у вас есть, а также Graphics::TBitmap С любым растровым или графическим классом, который у вас есть в распоряжении, с возможностью доступа к пикселям.

очень важно обратите внимание, что это использует параметры mm_txt->Font поэтому убедитесь, что вы устанавливаете:

  • Font->Pitch=fpFixed
  • Font->Charset=OEM_CHARSET
  • Font->Name="System"

чтобы сделать эту работу правильно в противном случае шрифт не будет обрабатываться как моно-интервал. Мышь колесо просто изменяет размер шрифта вверх / вниз, чтобы увидеть результаты на разных размерах шрифта

[Примечания]

  • посмотреть визуализация портретов слов
  • используйте язык с доступом к растровым изображениям / файлам и возможностями вывода текста
  • настоятельно рекомендуем начать с первого подхода, так как это очень легко пролив вперед и просто, и только потом переходить ко второму (что можно сделать как модификацию первого так самого код остается как есть в любом случае)
  • это хорошая идея, чтобы вычислить с инвертированной интенсивностью (черные пиксели-это максимальное значение), потому что стандартный просмотр текста на белом фоне, следовательно, приводит к гораздо лучшим результатам.
  • вы можете экспериментировать с размером, количеством и расположением зон подразделения или использовать некоторую сетку, например 3x3 вместо.

[Edit1] сравнения:

наконец, вот сравнение между двумя подходами на тот же вход:

comparison

зеленые точки отмечены изображения сделаны с подходом #2 и красные с #1 на 6 размер шрифта пикселя. Как вы можете видеть на изображении лампочки, чувствительный к форме подход намного лучше (даже если #1 выполняется на 2x увеличенном исходном изображении).

[Edit2] классное приложение

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

ОК приложение состоит всего из 2 окон. Первое главное окно-это в основном мое старое окно конвертера без выбора изображения и предварительного просмотра (все вещи выше в нем). Он имеет только предварительный просмотр ASCII и настройки преобразования. Второе окно-это пустая форма с прозрачной внутренней частью для выбора области захвата (без какой-либо функциональности).

теперь по таймеру я просто хватаю выбранную область по форме выбора, передаю ее в преобразование и просматриваю ASCIIart.

таким образом, вы заключаете область, которую хотите преобразовать в окне выбора, и просматриваете результат в главном окне. Это может быть игра,зритель,... Похоже это:

ASCIIart grabber example

так что теперь я могу смотреть даже видео в ASCIIart для удовольствия. Некоторые действительно хороши :).

hands

[Edit3]

если вы хотите попробовать реализовать эту GLSL взгляните на это: