Existem mais abordagens para a conversão de imagem para arte ASCII que são baseadas principalmente no uso de fontes mono-espaçadas . Para simplificar, eu me concentro apenas no básico:
Com base na intensidade de pixel / área (sombreamento)
Essa abordagem trata cada pixel de uma área de pixels como um único ponto. A ideia é calcular a intensidade média da escala de cinza desse ponto e, em seguida, substituí-lo por um caractere com intensidade próxima o suficiente da calculada. Para isso, precisamos de uma lista de caracteres utilizáveis, cada um com uma intensidade pré-calculada. Vamos chamá-lo de personagem map
. Para escolher mais rapidamente qual personagem é o melhor para qual intensidade, existem duas maneiras:
Mapa de caráter de intensidade linearmente distribuído
Portanto, usamos apenas personagens que têm uma 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 nosso personagem map
é classificado, podemos computar o personagem diretamente a partir da intensidade (sem necessidade de pesquisa)
character = map[intensity_of(dot)/constant];
Mapa de caráter de intensidade distribuída arbitrariamente
Portanto, temos uma variedade de caracteres utilizáveis e suas intensidades. Precisamos encontrar a intensidade mais próxima do intensity_of(dot)
Então, novamente, se classificarmos o map[]
, podemos usar a pesquisa binária, caso contrário, precisamos de um O(n)
loop ou O(1)
dicionário de distância mínima de pesquisa. Às vezes, para simplificar, o caractere map[]
pode ser tratado como uma distribuição linear, 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 é ótima para imagens em escala de cinza (não apenas em preto e branco). Se você selecionar o ponto como um único pixel, o resultado ficará grande (um pixel -> caractere único); portanto, para imagens maiores, uma área (multiplicação do tamanho da fonte) é selecionada para preservar a proporção do aspecto e não aumentar muito.
Como fazer isso:
- Divida uniformemente a imagem em pixels (escala de cinza) ou áreas (retangulares) ponto s
- Calcule a intensidade de cada pixel / área
- Substitua-o por caractere do mapa de caracteres com a intensidade mais próxima
Como personagem, map
você pode usar qualquer caractere, mas o resultado fica melhor se o personagem tiver pixels dispersos uniformemente ao longo da área do personagem. Para começar, você pode usar:
char map[10]=" .,:;ox%#@";
classificados em ordem decrescente e fingem ser linearmente distribuídos.
Portanto, se a intensidade do pixel / área for i = <0-255>
, o caractere de substituição será
Se i==0
o pixel / área for preto, se i==127
o pixel / área for cinza e se i==255
o pixel / área for branco. Você pode experimentar diferentes personagens dentro de map[]
...
Aqui está um exemplo antigo 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; 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;
Você precisa substituir / ignorar as coisas da VCL, a menos que use o ambiente Borland / Embarcadero .
mm_log
é o memorando onde o texto é enviado
bmp
é o bitmap de entrada
AnsiString
é uma string do tipo VCL indexada de 1, não de 0 como char*
!!!
Este é o resultado: Exemplo de imagem de intensidade ligeiramente NSFW
À esquerda está a saída de arte ASCII (tamanho da fonte 5 pixels) e à direita a imagem de entrada ampliada algumas vezes. Como você pode ver, a saída é pixel -> caractere maior. Se você usar áreas maiores em vez de pixels, o zoom será menor, mas é claro que a saída será menos agradável visualmente. Essa abordagem é muito fácil e rápida de codificar / processar.
Quando você adiciona coisas mais avançadas como:
- cálculos de mapa automatizados
- seleção automática de tamanho de pixel / área
- correções de proporção de aspecto
Então você pode processar imagens mais complexas com melhores resultados:
Aqui está o resultado em uma proporção de 1: 1 (zoom para ver os caracteres):
Claro, para amostragem de área você perde os pequenos detalhes. Esta é uma imagem do mesmo tamanho que o primeiro exemplo com amostra de á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 sombreado e arte ASCII sólida)
Essa abordagem tenta substituir a área (não há mais pontos de pixel único) por caracteres com intensidade e forma semelhantes. Isso leva a melhores resultados, mesmo com fontes maiores usadas em comparação com a abordagem anterior. Por outro lado, essa abordagem é um pouco mais lenta, é claro. Existem mais maneiras de fazer isso, mas a ideia principal é calcular a diferença (distância) entre a área da imagem ( dot
) e o caractere renderizado. Você pode começar com uma soma ingênua da diferença absoluta entre os pixels, mas isso levará a resultados não muito bons, porque mesmo uma mudança de um pixel tornará a distância grande. Em vez disso, você pode usar correlação ou métricas diferentes. O algoritmo geral é quase o mesmo da abordagem anterior:
Portanto, divida uniformemente a imagem em áreas retangulares (em escala de cinza) ponto 's
de preferência com a mesma proporção dos caracteres de fonte renderizados (isso preservará a proporção. Não se esqueça de que os caracteres geralmente se sobrepõem um pouco no eixo x)
Calcule a intensidade de cada área ( dot
)
Substitua-o por um personagem do personagem map
com a intensidade / forma mais próxima
Como podemos calcular a distância entre um caractere e um ponto? Essa é a parte mais difícil dessa abordagem. Enquanto experimento, desenvolvo este meio-termo entre velocidade, qualidade e simplicidade:
Divida a área do personagem em zonas
- Calcule uma intensidade separada para a zona esquerda, direita, para cima, para baixo e central de cada caractere de seu alfabeto de conversão (
map
).
- Normalize todas as intensidades, de forma que sejam independentes do tamanho da área
i=(i*256)/(xs*ys)
.
Processa a imagem de origem em áreas retangulares
- (com a mesma proporção da fonte de destino)
- Para cada área, calcule a intensidade da mesma maneira que no item 1
- Encontre a correspondência mais próxima de intensidades no alfabeto de conversão
- Produza o caractere ajustado
Este é o resultado para o tamanho da fonte = 7 pixels
Como você pode ver, a saída é visualmente agradável, mesmo com um tamanho de fonte maior usado (o exemplo de abordagem anterior era com um tamanho de fonte de 5 pixels). A saída tem aproximadamente o mesmo tamanho da imagem de entrada (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 fontes maiores e ainda preservar os detalhes (até certo ponto, é claro).
Aqui está o código completo para o aplicativo de conversão baseado em 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) // Character 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; // Temporary 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 initialise buffers
tmp = new Graphics::TBitmap;
if (tmp==NULL)
break;
// Allow 32 bit 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 the image by zoomed character size step
xf -= xf/3; // Characters are usually overlapping 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 the 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();
}
//---------------------------------------------------------------------------
É um aplicativo de formulário simples ( Form1
) com um único TMemo mm_txt
nele. Ele carrega uma imagem, "pic.bmp"
e, de acordo com a resolução, escolhe qual abordagem usar para converter para o texto que será salvo no"pic.txt"
e enviado para memo para visualização.
Para aqueles sem VCL, ignore as coisas do VCL e substitua AnsiString
por qualquer tipo de string que você tenha, e também Graphics::TBitmap
por qualquer bitmap ou classe de imagem que você tenha à disposição com capacidade de acesso a pixels.
Uma observação muito importante é que ele usa as configurações de mm_txt->Font
, portanto, certifique-se de definir:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
para fazer isso funcionar corretamente, caso contrário, a fonte não será tratada como mono-espaçada. A roda do mouse apenas muda o tamanho da fonte para cima / baixo para ver os resultados em diferentes tamanhos de fonte.
[Notas]
- Vejo visualização de Word Portraits
- Use uma linguagem com acesso a bitmap / arquivo e recursos de saída de texto
- Eu recomendo fortemente começar com a primeira abordagem, pois é muito fácil, direto e simples, e só então passar para a segunda (que pode ser feita como modificação da primeira, então a maior parte do código permanece como está)
- É uma boa ideia calcular com intensidade invertida (pixels pretos é o valor máximo) porque a visualização do texto padrão está em um fundo branco, levando a resultados muito melhores.
- você pode experimentar o tamanho, a contagem e o layout das zonas de subdivisão ou usar uma grade semelhante
3x3
.
Comparação
Finalmente, aqui está uma comparação entre as duas abordagens na mesma entrada:
As imagens marcadas com pontos verdes são feitas com a abordagem # 2 e as vermelhas com # 1 , todas em um tamanho de fonte de seis pixels. Como você pode ver na imagem da lâmpada, a abordagem sensível à forma é muito melhor (mesmo se o nº 1 for feito em uma imagem de origem com zoom 2x).
Aplicativo legal
Ao ler as novas perguntas de hoje, tive uma ideia de um aplicativo legal que pega uma região selecionada da área de trabalho e a alimenta continuamente no ASCIIart conversor e visualiza o resultado. Depois de uma hora de codificação, está pronto e estou tão satisfeito com o resultado que simplesmente preciso adicioná-lo aqui.
OK, o aplicativo consiste em apenas duas janelas. A primeira janela mestre é basicamente minha janela do conversor antigo, sem a seleção e visualização da imagem (todo o material acima está nela). Ele tem apenas as configurações de visualização e conversão ASCII. A segunda janela é um formulário vazio com o interior transparente para a seleção da área de captura (sem qualquer funcionalidade).
Agora, em um cronômetro, eu apenas pego a área selecionada pelo formulário de seleção, passo para a conversão e visualizo o ASCIIart .
Assim, você inclui uma área que deseja converter pela janela de seleção e visualiza o resultado na janela principal. Pode ser um jogo, visualizador, etc. Tem a seguinte aparência:
Agora posso assistir até vídeos em ASCIIart para me divertir. Alguns são muito legais :).
Se você quiser tentar implementar isso em GLSL , dê uma olhada nisso: