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

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

Другая причина также в этом МЕТА нить нацеленный на меня, так что если у вас есть дополнительные материалы, не стесняйтесь комментировать.

Как преобразовать растровое изображение в ASCII искусство с помощью C ++ ?

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

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

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

92

Решение

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

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

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

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

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

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

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

интенсивность передовой пример

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

Немного NSFW интенсивность продвинутый пример изображения

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

Подгонка персонажа (гибрид между Shading и Solid ASCII Art)

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

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

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

  1. Разделить область персонажа на зоны

    зон

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

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

Это результат для размера шрифта = 7 пикселей

пример примерки

Как видите, результат визуально приятен даже при использовании большего размера шрифта (предыдущий пример подхода был с размером шрифта 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" и отправлено в заметку для визуализации. Для тех без VCL проигнорируйте материал VCL и замените AnsiString с любым типом строки, а также Graphics::TBitmap с любым классом растровых изображений или изображений, которыми вы располагаете, с возможностью доступа к пикселям.

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

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

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

[Заметки]

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

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

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

сравнение

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

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

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

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

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

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

Пример ASCIIart grabber

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

Руки

[Edit3]

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

136

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]