Эта тема всплывает здесь на ТАК время от времени, но удаляется обычно из-за плохо написанного вопроса. Я видел много таких вопросов, а затем молчание от OP (обычно с низким повторением), когда запрашивается дополнительная информация. Время от времени, если входные данные достаточно хороши для меня, я решаю ответить ответом, и он обычно получает несколько голосов в день, когда активен, но затем через несколько недель вопрос удаляется / удаляется, и все начинается с самого начала. , Поэтому я решил написать это Q& поэтому я могу ссылаться на такие вопросы напрямую, не переписывая ответ снова и снова…
Другая причина также в этом МЕТА нить нацеленный на меня, так что если у вас есть дополнительные материалы, не стесняйтесь комментировать.
Как преобразовать растровое изображение в ASCII искусство с помощью C ++ ?
Некоторые ограничения:
Вот связанная страница вики ASCII искусство (спасибо @RogerRowland)
Существует больше подходов к преобразованию изображений в ASCII, которые в основном основаны на использовании моноширинные шрифты для простоты я придерживаюсь только основ:
Пиксель / площадь интенсивности (затенение)
Этот подход обрабатывает каждый пиксель области пикселей как одну точку. Идея состоит в том, чтобы вычислить среднюю интенсивность серой шкалы этой точки, а затем заменить ее символом, достаточно близким к вычисленному. Для этого нам понадобится некоторый список используемых символов, каждый из которых имеет предварительно вычисленную интенсивность, назовем ее символом. 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 пиксель -> один символ), поэтому для больших изображений вместо этого выбирается область (кратная размеру шрифта), чтобы сохранить соотношение сторон и не увеличить слишком много.
Как это сделать:
Как персонаж 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 пиксель приведет к увеличению расстояния, вместо этого вы можете использовать корреляцию или другие метрики. Общий алгоритм почти такой же, как и в предыдущем подходе:
dot
)map
с ближайшей интенсивностью / формойКак рассчитать расстояние между символом и точкой? Это самая сложная часть этого подхода. Экспериментируя, я нахожу компромисс между скоростью, качеством и простотой:
Разделить область персонажа на зоны
map
)i=(i*256)/(xs*ys)
обработать исходное изображение в прямоугольных областях
Это результат для размера шрифта = 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 ради забавы. Некоторые из них действительно хороши :).
[Edit3]
Если вы хотите попробовать реализовать это в GLSL Взгляни на это:
Других решений пока нет …