Conversão de arte em imagem para ASCII

Prólogo

Este assunto aparece aqui no SO de tempos em tempos, mas é removido geralmente por causa de uma pergunta mal escrita. Eu vi muitas dessas perguntas e, em seguida, silêncio do OP (baixo representante usual) quando informações adicionais são solicitadas. De tempos em tempos, se a input for boa o suficiente para mim, decido responder com uma resposta e normalmente recebe alguns votos por dia enquanto estiver ativo, mas depois de algumas semanas a pergunta é removida / excluída e tudo começa do começo. . Por isso, decidi escrever esta session de perguntas e respostas para poder fazer referência a essas perguntas sem rewrite a resposta vezes sem conta…

Outra razão é também este tópico META direcionado para mim, por isso, se você tiver mais informações, sinta-se à vontade para comentar.

Questão

Como converter imagem de bitmap para arte ASCII usando C ++ ?

Algumas restrições:

  • imagens em escala de cinza
  • usando fonts mono espaçadas
  • mantendo-o simples (não usando coisas muito avançadas para programadores iniciantes)

Aqui está uma página Wiki relacionada a arte ASCII (graças a @RogerRowland)

Existem mais abordagens para a conversão de imagens para arte em ASCII que são baseadas principalmente no uso de fonts mono-espaçadas para simplificar.

intensidade de pixel / área com base (sombreamento)

Essa abordagem manipula cada pixel de área de pixels como ponto único. A idéia é calcular a intensidade média da escala de cinza desse ponto e, em seguida, substituí-lo por um caractere com intensidade próxima à computada. Para isso, precisamos de uma lista de caracteres utilizáveis, cada qual com intensidade pré-computada. Vamos chamá-lo de map caracteres. Para escolher mais rapidamente qual personagem é o melhor para qual intensidade existem duas maneiras:

  1. mapa de caracteres de intensidade linearmente distribuído

    Então, usamos apenas caracteres que têm diferença de intensidade com o mesmo passo. Em outras palavras, quando classificado em ordem crescente, então:

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

    Além disso, quando o nosso map personagens é ordenado, podemos calcular o caractere diretamente a partir da intensidade (não é necessária pesquisa)

     character=map[intensity_of(dot)/constant]; 
  2. mapa de caracteres de intensidade arbitrária distribuída

    Portanto, temos vários caracteres utilizáveis ​​e suas intensidades. Precisamos encontrar intensidade mais próxima da intensity_of(dot) Então, novamente, se classificarmos o map[] , podemos usar a pesquisa binária, caso contrário, precisaremos de O(n) search min distance loop ou O(1) dictionary. Às vezes, por simplicidade, o map[] caracteres map[] pode ser tratado como distribuído linearmente, causando uma leve distorção gama geralmente invisível no resultado, a menos que você saiba o que procurar.

A conversão baseada em intensidade também é excelente para imagens em escala de cinza (não apenas em preto e branco). Se você selecionar o ponto como um único pixel, o resultado será grande (1 pixel -> caractere único) para imagens maiores, uma área (multiplicação do tamanho da fonte) será selecionada para preservar a proporção e não aumentar muito.

Como fazer isso:

  1. dividir igualmente a imagem em pixels (em escala de cinza) ou áreas (retangulares) em pontos
  2. calcular a intensidade de cada pixel / área
  3. substituí-lo por personagem do mapa de personagem com a intensidade mais próxima

Como map caracteres, você pode usar qualquer caractere, mas o resultado fica melhor se o personagem tiver pixels dispersos uniformemente ao longo da área de caracteres. Para começar, você pode usar:

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

classificados descendente e fingem ser distribuídos linearmente.

Então, se a intensidade de pixel / área for i = <0-255> , o caractere de substituição será

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

se i==0 então o pixel / área é preto, se i==127 então o pixel / área é cinza e se i==255 então o pixel / área é branco. Você pode experimentar diferentes personagens dentro do map[]

Aqui exemplo antigo do meu em C ++ e 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;yHeight;y++) { p=(BYTE*)bmp->ScanLine[y]; for (x=0;xWidth;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[li]; } s+=endl; } mm_log->Lines->Text=s; mm_log->Lines->SaveToFile("pic.txt"); delete bmp; 

você precisa replace / ignorar as coisas da VCL, a menos que você use o ambiente da Borland / Embarcadero

  • mm_log é o memorando em que o texto é enviado
  • bmp é o bitmap de input
  • AnsiString é o formulário indexado da string de tipo VCL 1 não de 0 como char* !!!

este é o resultado: Imagem de exemplo de intensidade ligeiramente NSFW

À esquerda está a saída de arte ASCII (tamanho da fonte 5px) e na imagem de input à direita Zoom algumas vezes. Como você pode ver a saída é maior pixel -> personagem. Se você usar áreas maiores em vez de pixels, então o zoom é menor, mas é claro que a saída é menos visualmente agradável. Essa abordagem é muito fácil e rápida de codificar / processar.

Quando você adiciona coisas mais avançadas, como:

  • cálculos automatizados de mapas
  • seleção automática de tamanho de pixel / área
  • correções de relação de aspecto

Então você pode processar imagens mais complexas com melhores resultados:

aqui resulta na proporção de 1: 1 (zoom para ver os caracteres):

exemplo avançado de intensidade

Claro que para amostragem de área você perde os pequenos detalhes. Esta é uma imagem do mesmo tamanho que o primeiro exemplo amostrado com áreas:

Imagem de exemplo avançado de intensidade ligeiramente NSFW

Como você pode ver isso é mais adequado para imagens maiores

Ajuste de caracteres (híbrido entre Shading e Solid ASCII Art)

Essa abordagem tenta replace a área (sem mais pontos de pixel único) por caracteres com intensidade e forma semelhantes. Isso leva a melhores resultados, mesmo com fonts maiores usadas em comparação com a abordagem anterior; por outro lado, essa abordagem é um pouco mais lenta, é claro. Existem outras maneiras de fazer isso, mas a idéia principal é calcular a diferença (distância) entre a área da imagem ( dot ) e o caractere renderizado. Você pode começar com a sum ingênua da diferença de abs entre os pixels, mas isso levará a resultados não muito bons, porque mesmo uma mudança de 1 pixel aumentará a distância, em vez disso, você poderá usar correlação ou métricas diferentes. O algoritmo geral é quase o mesmo da abordagem anterior:

  1. divida tão uniformemente a imagem em áreas retangulares (em escala de cinza)
    • idealmente com a mesma proporção de caracteres de fonte renderizada (ele preservará a proporção, não se esqueça de que os caracteres geralmente se sobrepõem a um pouco no eixo x)
  2. calcular a intensidade de cada área ( dot )
  3. substituí-lo por personagem do map personagem com a intensidade / forma mais próxima

Como calcular a distância entre o caractere e o ponto? Essa é a parte mais difícil dessa abordagem. Ao experimentar, desenvolvo esse compromisso entre velocidade, qualidade e simplicidade:

  1. Divide a área do personagem em zonas

    zonas

    • calcule a intensidade separada para a esquerda, direita, para cima, para baixo e zona central de cada caractere do seu alfabeto de conversão ( map )
    • normalize todas as intensidades para que elas sejam independentes do tamanho da área i=(i*256)/(xs*ys)
  2. processa a imagem de origem em áreas de retângulo

    • (com a mesma proporção da fonte alvo)
    • para cada área computa a intensidade da mesma maneira que no item 1
    • encontre a correspondência mais próxima das intensidades no alfabeto de conversão
    • personagem equipado de saída

Este é o resultado para o tamanho da fonte = 7px

exemplo de montagem de caracteres

Como você pode ver, a saída é visualmente agradável, mesmo com o tamanho de fonte maior usado (o exemplo de abordagem anterior era com tamanho de fonte de 5 px). A saída é aproximadamente do mesmo tamanho que a imagem de input (sem zoom). Os melhores resultados são obtidos porque os caracteres estão mais próximos da imagem original não apenas pela intensidade, mas também pela forma geral e, portanto, você pode usar fonts maiores e preservar os detalhes (até um ponto mais grosseiro).

Aqui o código completo para o aplicativo de conversão baseado em VCL:

 //--------------------------------------------------------------------------- #include  #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=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;yScanLine[y]; q =new DWORD*[yf]; if (q ==NULL) break; for (y=0;yScanLine[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;yd)) { 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;yHeight;y++) { p=(BYTE*)bmp->ScanLine[y]; for (x=0;xWidth;x++) { i =p[(x<<2)+0]; i+=p[(x<<2)+1]; i+=p[(x<<2)+2]; i=(i*l)/768; txt+=m[li]; } 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 (y0ClientWidth=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(); } //--------------------------------------------------------------------------- 

É simples aplicativo de formulário ( Form1 ) com único TMemo mm_txt nele. Ele carrega a imagem "pic.bmp" , então, de acordo com a resolução, escolha qual abordagem usar para converter em texto que é salvo em "pic.txt" e enviado para o memorando para visualizar. Para aqueles sem VCL ignorar o material VCL e replace AnsiString por qualquer tipo de string que você tenha, e também o Graphics::TBitmap com qualquer bitmap ou class de imagem que você tenha à disposição com capacidade de access a pixel.

Nota muito importante é que isso usa as configurações de mm_txt->Font portanto, certifique-se de definir:

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

para que isso funcione corretamente, caso contrário, a fonte não será tratada como mono-espaçada. A roda do mouse apenas altera o tamanho da fonte para cima / baixo para ver os resultados em diferentes tamanhos de fonte

[Notas]

  • ver visualização do Word Portraits
  • usar linguagem com access a bitmap / arquivo e resources de saída de texto
  • recomendo fortemente começar com a primeira abordagem, pois é muito fácil avançar e simples, e só então passar para a segunda (o que pode ser feito como modificação da primeira, assim como a maioria do código permanece como está)
  • É uma boa ideia computar com intensidade invertida (pixels pretos é o valor máximo) porque a textview padrão está no plano de fundo branco, o que leva a resultados muito melhores.
  • Você pode experimentar com tamanho, contagem e layout das zonas de subdivisão ou usar alguma grade como 3x3 .

Comparação [Edit1]

Finalmente, aqui está uma comparação entre as duas abordagens na mesma input:

comparação

As imagens marcadas com ponto verde são feitas com a abordagem # 2 e as vermelhas com a imagem # 1, todas no tamanho de fonte de 6 pixels. Como você pode ver na imagem da lâmpada, a abordagem sensível à forma é muito melhor (mesmo que o número 1 seja feito em uma imagem de fonte com zoom 2x).

[Edit2] app legal

Ao ler hoje novas perguntas eu tenho uma idéia de um aplicativo legal que agarra região selecionada da área de trabalho e continuamente alimentá-lo para ASCIIart conversor e ver o resultado. Depois de uma hora de codificação é feito e estou tão satisfeito com o resultado que eu simplesmente preciso adicioná-lo aqui.

OK, o aplicativo consiste em apenas 2 janelas. A primeira janela principal é basicamente a minha antiga janela do conversor sem a seleção e visualização da imagem (todas as coisas acima estão nela). Tem apenas as configurações de visualização e conversão ASCII. A segunda janela é forma vazia com interior transparente para a seleção da área de captura (sem qualquer funcionalidade).

Agora no cronômetro eu apenas pego a área selecionada pelo formulário de seleção, passo para a conversão e visualizo o ASCIIart .

Então você coloca a área que deseja converter pela janela de seleção e visualiza o resultado na janela principal. Pode ser um jogo, visualizador, … Parece assim:

Exemplo de captura ASCIIart

Então agora eu posso assistir vídeos em ASCIIart por diversão. Alguns são muito legais :).

mãos

[Edit3]

Se você quiser tentar implementar isso no GLSL, dê uma olhada nisso:

  • Converter números de ponto flutuante em dígitos decimais no GLSL?