Saída gerada pelo conversor |
Na época em que os consoles de texto eram a mais comum, se não a única opção de interface disponível para os usuários de computador, as pessoas se valiam de muita criatividade (e um bocado de trabalho) para criar imagens usando somente os caracteres disponíveis na tabela ASCII. Muita coisa boa foi e ainda é criada com essa idéia, incluindo jogos e animações.
Depois de assistir a este vídeo, decidi implementar um pequeno conversor ASCII em pascal. O resultado da empreitada pode ser visto na imagem do rei do rock ao lado, que foi criada usando a versão do conversor criada para este artigo.
O Algoritmo
Para este conversor vamos usar uma abordagem de conversão pixel a pixel onde cada pixel na imagem vai ser representado por um caractere ASCII. Para tanto, vamos criar uma "paleta" ou uma tabela de equivalência para cada tom RGB presente na imagem de origem. Além disto, para manter as coisas simples, vamos gerar somente representações em tons de cinza.Assim temos:
- Converter a imagem de origem para tons de cinza (0..255)
- Criar uma tabela de equivalência de cor RGB -> ASCII
- Representar cada pixel com o caractere de tom mais próximo
Simples e rápido. Vamos ao código!
Convertendo Imagens para Tons de Cinza
Esta é a parte mais fácil.
Para converter um pixel colorido para um tom de cinza, basta calcular a média dos canais RGB:
c = (r + g + b) / 3
Assumindo que nossa imagem utiliza 32bit por pixel, vamos armazenar o tom calculado no quarto byte, que normalmente é utilizado para guardar o canal alpha. Assim temos:
procedure TForm1.ConvertSourceImage; var x, y : integer; pColor : PRGBQuad; begin FreeAndNil(fGrayAlphaImage); fGrayAlphaImage := TBitmap.Create; fGrayAlphaImage.PixelFormat := pf32bit; fGrayAlphaImage.Width := imgOrigem.Picture.Width; fGrayAlphaImage.Height := imgOrigem.Picture.Height; fGrayAlphaImage.Canvas.Draw(0,0, imgOrigem.Picture.Graphic); for y := 0 to fGrayAlphaImage.Height-1 do begin pColor := fGrayAlphaImage.ScanLine[y]; for x:=0 to fGrayAlphaImage.Width-1 do begin pColor^.rgbReserved := ( pColor^.rgbRed + pColor^.rgbGreen + pColor^.rgbBlue) div 3; inc(pColor); end; end; end;
Tons de cinza calculados, vamos ao próximo passo, nossos tons (ou shades) ASCII.
Equivalências de intensidade de luz
Tabela de shades |
Para criar uma equivalência entre os dois, vamos tratar o caractere como uma imagem em preto e branco e calcular o percentual de pixels pretos em relação à quantidade de pixels da imagem. Depois de fazer isto para todos os caracteres escolhidos, teremos uma tabela de equivalência que pode ser visualmente representada pela imagem ao lado.
O código para esta função ficou assim:
procedure TForm1.InitShades; var bmp : TBitmap; i, x, y, pixelCount : integer; k: Double; p : PRGBQuad; rct : TRect; begin FillChar(fIntensityByAnsiChar, length(fIntensityByAnsiChar), $FF); FillChar(fAnsiCharByIntensity, length(fAnsiCharByIntensity), AnsiChar(' ')); bmp := TBitmap.Create; bmp.Canvas.Font.Size := 10; bmp.Canvas.Font.Color := clBlack; bmp.Canvas.Font.Name := cbxFont.Text; bmp.PixelFormat := pf32bit; charW := bmp.Canvas.TextWidth('A'); charH := bmp.Canvas.TextHeight('A'); bmp.Height := charH; bmp.Width := charW; pixelCount := charW * charH; bmp.Canvas.Brush.Color := clWhite; for i := 0 to high(fIntensityByAnsiChar) do begin k := 0; bmp.Canvas.FillRect(rect(0,0, charW, charH)); bmp.Canvas.TextOut(0,0, WideChar(i)); for y := 0 to bmp.Height-1 do begin p := PRGBQuad(bmp.ScanLine[y]); for x := 0 to bmp.Width-1 do begin if ((p^.rgbRed + p^.rgbGreen + p^.rgbBlue) div 3) = 255 then k := k + 1; Inc(p); end; end; fIntensityByAnsiChar[i] := trunc(( k / pixelCount ) * 255); fAnsiCharByIntensity[fIntensityByAnsiChar[i]] := AnsiChar(i); end; for i:=1 to High(fAnsiCharByIntensity) do if (fAnsiCharByIntensity[i] = AnsiChar(' ')) and (i <> 32) then fAnsiCharByIntensity[i] := fAnsiCharByIntensity[i-1]; FreeAndNil(bmp); end;
Nossos shades estão prontos!
Temos agora um array que contém a intensidade de cor para todos os nossos caracteres (fIntensityByAnsiChar) e um outro que contém o caractere equivalente a uma determinada intensidade (fAnsiCharByIntensity).
Só nos falta agora gerar a nossa imagem e conferir o resultado.
Gerando nosso ASCII
Com nossa tabela de shades devidamente calculada, gerar o ASCII a partir de uma imagem é simplesmente uma questão de varrer os pixels da imagem de origem e pesquisar o caractere equivalente:procedure TForm1.CreateASCIIImage; var x, y : integer; pColor : PRGBQuad; begin memOut.Font.Name := cbxFont.Text; memOut.Font.Size := 8; SetLength(fASCIIImage, fGrayAlphaImage.Width, fGrayAlphaImage.Height); for x:=0 to fGrayAlphaImage.Width-1 do for y := 0 to fGrayAlphaImage.Height-1 do fASCIIImage[x,y]:= AnsiChar(32); for y := 0 to fGrayAlphaImage.Height - 1 do begin pColor := fGrayAlphaImage.ScanLine[y]; for x := 0 to fGrayAlphaImage.Width - 1 do begin if pColor^.rgbReserved <> 0 then if ckbInvert.Checked then fASCIIImage[x,y]:= fAnsiCharByIntensity[255-pColor^.rgbReserved] else fASCIIImage[x,y]:= fAnsiCharByIntensity[pColor^.rgbReserved]; Inc(pColor); end; end; end;
Bingo! Convertemos a imagem.
Para visualizá-la, vamos utilizar um
TMemo
:procedure TForm1.ShowASCIIImage; var s : AnsiString; x, y : integer; begin memOut.Lines.BeginUpdate; memOut.Clear; for y := 0 to fGrayAlphaImage.Height - 1 do begin s := ''; for x :=0 to fGrayAlphaImage.Width-1 do s := s + AnsiChar(fASCIIImage[x,y]); memOut.Lines.Add(s); end; memOut.Lines.EndUpdate; end;
Considerações
O algoritmo que implementamos neste artigo é muito simples e muito rápido, mas tem suas limitações.A mais importante delas é o fato de que ele não leva em consideração o aspecto (relação entre altura e largura) do pixel nem da fonte utilizada e como consequência a imagem final pode apresentar distorções em suas dimensões.