Преобразование изображений в ASCII art
Пролог
эта тема всплывает здесь на так время от времени, но удаляется обычно из-за плохо написанного вопрос. Я видел много таких вопросов, а потом молчание от OP (обычный низкий rep), когда запрашивается дополнительная информация. Время от времени, если вход достаточно хорош для меня, я решаю ответить ответом, и он обычно получает несколько голосов в день, пока активен, но затем через несколько недель вопрос удаляется/удаляется и все начинается с начала. Поэтому я решил написать это Q & A поэтому я могу ссылаться на такие вопросы прямо не переписав ответ снова и снова ...
другая причина-также этот META thread нацелены на меня, так что если у вас есть дополнительный вход, не стесняйтесь комментировать.
вопрос
Как конвертировать растровое изображение в ASCII art используя C++ ?
некоторые ограничения:
- изображения серой шкалы
- используя моноширинный шрифт
- сохраняя его простым (не используя слишком продвинутые вещи для программистов начального уровня)
вот соответствующая страница Вики ASCII art (спасибо @RogerRowland)
1 ответ:
есть больше подходов для преобразования изображения в ASCII art, которые в основном основаны на использовании моноширинный шрифт для простоты я придерживаюсь только к основам:
пиксель / интенсивность области на основе (затенение)
этот подход обрабатывает каждый пиксель области пикселей как одну точку. Идея состоит в том, чтобы вычислить среднюю интенсивность серой шкалы этой точки, а затем заменить ее символом с достаточно близкой интенсивностью к вычисленной. Для этого нам нужен некоторый список полезных символов каждый с предварительно вычисленной интенсивностью пусть назовем его character
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[]
можно обрабатывать как линейно распределенные, вызывающие небольшое гамма-искажение, обычно невидимое в результате, если вы не знаете, что искать.преобразование на основе интенсивности отлично подходит также для серых изображений (не только черно-белых). Если вы выберете точку как один пиксель, результат будет большим (1 пиксель -> один символ), поэтому для больших изображений вместо этого выбирается область (умноженная на размер шрифта), чтобы сохранить соотношение сторон и не увеличивать слишком много.
как делать:
- таким образом, равномерно разделите изображение на (серые)пиксели или (прямоугольные) области точкаs
- вычислить интенсивность каждого пикселя / области
- замените его символом из карты символов с ближайшей интенсивностью
как символ
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 (увеличить, чтобы увидеть символы):
конечно для забора зоны вы теряете малые детали. Это изображение того же размера, что и первый пример выборки с помощью зоны:
слегка NSFW интенсивность расширенный пример изображения
как вы можете видеть, что это больше подходит для больших изображений
подгонка символов (гибрид между затенением и твердым ASCII-искусством)
этот подход пытается заменить область (не более одной пиксельной точки) символом с аналогичной интенсивностью и формой. Это приводит к лучшим результатам даже при использовании более крупных шрифтов по сравнению с предыдущим подходом с другой стороны, этот подход немного медленнее, конечно. Есть больше способов сделать это, но основная идея состоит в том, чтобы вычислить разницу (расстояние) между областью изображения (
dot
) и оказанные характер. Вы можете начать с наивной суммы разницы abs между пикселями, но это приведет к не очень хорошим результатам, потому что даже сдвиг 1 пикселя сделает расстояние большим, вместо этого вы можете использовать корреляцию или разные метрики. Общий алгоритм почти такой же, как и предыдущий подход:
- так равномерно разделите изображение на (серые) прямоугольные области точка ' s
- в идеале с тем же соотношением сторон, что и rendered символы шрифта (это сохранит соотношение сторон, не забывайте, что символы обычно перекрываются немного по оси x)
- вычислить интенсивность каждой области (
dot
)- замените его символом из символа
map
С ближайшей интенсивность/формакак вычислить расстояние между символом и точкой? это самая сложная часть этого подхода. Экспериментируя, я разрабатываю этот компромисс между скоростью, качеством и простотой:
разделить область символов на зоны
- вычислить отдельную интенсивность для левой, правой, верхней, нижней и центральной зоны каждого символа из вашего алфавита преобразования (
map
)- нормализовать все интенсивности, чтобы они не зависели от размера области
i=(i*256)/(xs*ys)
обрабатывать исходное изображение в прямоугольных областях
- (с тем же соотношением сторон, что и целевой шрифт)
- для каждой области вычислить интенсивность таким же образом, как в пуле 1
- найти ближайший матч с интенсивностью в пересчете алфавит
- выход установлен символ
это результат для размера шрифта = 7px
как вы можете видеть, вывод визуально приятен даже при использовании большего размера шрифта (предыдущий пример подхода был с размером шрифта 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] сравнения:
наконец, вот сравнение между двумя подходами на тот же вход:
зеленые точки отмечены изображения сделаны с подходом #2 и красные с #1 на
6
размер шрифта пикселя. Как вы можете видеть на изображении лампочки, чувствительный к форме подход намного лучше (даже если #1 выполняется на 2x увеличенном исходном изображении).[Edit2] классное приложение
во время чтения сегодня новые вопросы, которые я получил представление о прохладном приложении, которое захватывает выбранную область рабочего стола и непрерывно кормить его ASCIIart конвертер и просмотр результата. После часа кодирования это сделано, и я так доволен результатом, что я просто должен добавить его здесь.
ОК приложение состоит всего из 2 окон. Первое главное окно-это в основном мое старое окно конвертера без выбора изображения и предварительного просмотра (все вещи выше в нем). Он имеет только предварительный просмотр ASCII и настройки преобразования. Второе окно-это пустая форма с прозрачной внутренней частью для выбора области захвата (без какой-либо функциональности).
теперь по таймеру я просто хватаю выбранную область по форме выбора, передаю ее в преобразование и просматриваю ASCIIart.
таким образом, вы заключаете область, которую хотите преобразовать в окне выбора, и просматриваете результат в главном окне. Это может быть игра,зритель,... Похоже это:
так что теперь я могу смотреть даже видео в ASCIIart для удовольствия. Некоторые действительно хороши :).
[Edit3]
если вы хотите попробовать реализовать эту GLSL взгляните на это: