Hay más enfoques para la conversión de imágenes a arte ASCII que se basan principalmente en el uso de fuentes monoespaciadas . Por simplicidad, me limito solo a lo básico:
Basado en intensidad de píxel / área (sombreado)
Este enfoque maneja cada píxel de un área de píxeles como un solo punto. La idea es calcular la intensidad media de la escala de grises de este punto y luego reemplazarlo con un carácter con una intensidad lo suficientemente cercana a la calculada. Para eso necesitamos una lista de caracteres utilizables, cada uno con una intensidad precalculada. Llamémoslo personaje map
. Para elegir más rápidamente qué personaje es el mejor para qué intensidad, hay dos formas:
Mapa de caracteres de intensidad distribuida linealmente
Entonces usamos solo caracteres que tienen una diferencia de intensidad con el mismo paso. En otras palabras, cuando se ordena de forma ascendente, entonces:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Además, cuando nuestro personaje map
está ordenado, podemos calcular el personaje directamente desde la intensidad (no se necesita búsqueda)
character = map[intensity_of(dot)/constant];
Mapa de caracteres de intensidad distribuida arbitrariamente
Entonces tenemos una variedad de caracteres utilizables y sus intensidades. Necesitamos encontrar la intensidad más cercana al intensity_of(dot)
Entonces, nuevamente, si ordenamos map[]
, podemos usar la búsqueda binaria, de lo contrario, necesitamos un O(n)
ciclo de búsqueda de distancia mínima o un O(1)
diccionario. A veces, por simplicidad, el carácter map[]
se puede manejar como distribuido linealmente, lo que provoca una ligera distorsión gamma, que generalmente no se ve en el resultado a menos que sepa qué buscar.
La conversión basada en intensidad también es excelente para imágenes en escala de grises (no solo en blanco y negro). Si selecciona el punto como un solo píxel, el resultado se vuelve grande (un píxel -> un solo carácter), por lo que para imágenes más grandes se selecciona un área (multiplicar el tamaño de fuente) en su lugar para preservar la relación de aspecto y no agrandar demasiado.
Cómo hacerlo:
- Divida uniformemente la imagen en píxeles (escala de grises) o áreas (rectangulares) punto s
- Calcule la intensidad de cada píxel / área
- Reemplácelo por carácter del mapa de caracteres con la intensidad más cercana
Como personaje map
, puede usar cualquier carácter, pero el resultado mejora si el personaje tiene píxeles distribuidos uniformemente a lo largo del área del personaje. Para empezar puedes usar:
char map[10]=" .,:;ox%#@";
ordenados descendente y pretenden estar distribuidos linealmente.
Entonces, si la intensidad del píxel / área es i = <0-255>
, el carácter de reemplazo será
Si i==0
entonces el píxel / área es negro, si i==127
entonces el píxel / área es gris, y si i==255
entonces el píxel / área es blanco. Puedes experimentar con diferentes personajes dentro map[]
...
Aquí hay un ejemplo antiguo mío en C ++ y 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;
Debe reemplazar / ignorar las cosas de VCL a menos que use el entorno Borland / Embarcadero .
mm_log
es la nota donde se genera el texto
bmp
es el mapa de bits de entrada
AnsiString
es una cadena de tipo VCL indexada desde 1, no desde 0 como char*
!!!
Este es el resultado: Imagen de ejemplo de intensidad ligeramente NSFW
A la izquierda está la salida de arte ASCII (tamaño de fuente 5 píxeles) y a la derecha la imagen de entrada se amplió varias veces. Como puede ver, la salida es un píxel más grande -> carácter. Si usa áreas más grandes en lugar de píxeles, entonces el zoom es más pequeño, pero, por supuesto, la salida es menos agradable a la vista. Este enfoque es muy fácil y rápido de codificar / procesar.
Cuando agrega cosas más avanzadas como:
- cálculos de mapas automatizados
- selección automática de tamaño de área / píxel
- correcciones de relación de aspecto
Entonces puede procesar imágenes más complejas con mejores resultados:
Aquí está el resultado en una proporción 1: 1 (haga zoom para ver los personajes):
Por supuesto, para el muestreo de áreas se pierden los pequeños detalles. Esta es una imagen del mismo tamaño que el primer ejemplo muestreado con áreas:
Imagen de ejemplo avanzada de intensidad levemente NSFW
Como puede ver, esto es más adecuado para imágenes más grandes.
Ajuste de caracteres (híbrido entre sombreado y arte ASCII sólido)
Este enfoque intenta reemplazar el área (no más puntos de un solo píxel) con caracteres con intensidad y forma similares. Esto conduce a mejores resultados, incluso con fuentes más grandes en comparación con el enfoque anterior. Por otro lado, este enfoque es un poco más lento, por supuesto. Hay más formas de hacer esto, pero la idea principal es calcular la diferencia (distancia) entre el área de la imagen ( dot
) y el carácter renderizado. Puede comenzar con una suma ingenua de la diferencia absoluta entre píxeles, pero eso dará lugar a resultados no muy buenos porque incluso un cambio de un píxel aumentará la distancia. En su lugar, puede utilizar correlación o métricas diferentes. El algoritmo general es casi el mismo que el enfoque anterior:
Así dividir uniformemente la imagen para (escala de grises) áreas rectangulares dot 's
idealmente con la misma relación de aspecto que los caracteres de fuente renderizados (preservará la relación de aspecto. No olvide que los caracteres generalmente se superponen un poco en el eje x)
Calcule la intensidad de cada área ( dot
)
Reemplácelo por un carácter del personaje map
con la intensidad / forma más cercana
¿Cómo podemos calcular la distancia entre un carácter y un punto? Esa es la parte más difícil de este enfoque. Mientras experimento, desarrollo este compromiso entre velocidad, calidad y sencillez:
Dividir el área del personaje en zonas
- Calcule una intensidad separada para la zona izquierda, derecha, arriba, abajo y central de cada carácter de su alfabeto de conversión (
map
).
- Normalice todas las intensidades para que sean independientes del tamaño del área
i=(i*256)/(xs*ys)
.
Procesar la imagen de origen en áreas rectangulares
- (con la misma relación de aspecto que la fuente de destino)
- Para cada área, calcule la intensidad de la misma manera que en la viñeta n. ° 1
- Encuentre la coincidencia más cercana de las intensidades en el alfabeto de conversión
- Salida del carácter ajustado
Este es el resultado para el tamaño de fuente = 7 píxeles
Como puede ver, el resultado es visualmente agradable, incluso con un tamaño de fuente más grande (el ejemplo de enfoque anterior fue con un tamaño de fuente de 5 píxeles). La salida es aproximadamente del mismo tamaño que la imagen de entrada (sin zoom). Los mejores resultados se logran porque los caracteres están más cerca de la imagen original, no solo por la intensidad, sino también por la forma general, y por lo tanto puede usar fuentes más grandes y aún preservar los detalles (hasta cierto punto, por supuesto).
Aquí está el código completo para la aplicación de conversión basada en 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();
}
//---------------------------------------------------------------------------
Es una aplicación de formulario simple ( Form1
) con un solo TMemo mm_txt
en él. Carga una imagen, "pic.bmp"
y luego, de acuerdo con la resolución, elige qué enfoque usar para convertir a texto que se guarda "pic.txt"
y envía a una nota para visualizar.
Para aquellos sin VCL, ignore el material VCL y reemplácelo AnsiString
con cualquier tipo de cadena que tenga, y también Graphics::TBitmap
con cualquier mapa de bits o clase de imagen que tenga a disposición con capacidad de acceso a píxeles.
Una nota muy importante es que utiliza la configuración de mm_txt->Font
, así que asegúrese de configurar:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
para que esto funcione correctamente, de lo contrario, la fuente no se manejará como monoespaciada. La rueda del mouse simplemente cambia el tamaño de la fuente hacia arriba o hacia abajo para ver resultados en diferentes tamaños de fuente.
[Notas]
- Ver visualización de Word Portraits
- Utilice un idioma con mapa de bits / acceso a archivos y capacidades de salida de texto
- Recomiendo encarecidamente comenzar con el primer enfoque, ya que es muy fácil, sencillo y simple, y solo luego pasar al segundo (que se puede hacer como modificación del primero, por lo que la mayor parte del código permanece como está de todos modos)
- Es una buena idea calcular con intensidad invertida (píxeles negros es el valor máximo) porque la vista previa del texto estándar está sobre un fondo blanco, lo que conduce a resultados mucho mejores.
- puede experimentar con el tamaño, el recuento y el diseño de las zonas de subdivisión o usar alguna cuadrícula como en su
3x3
lugar.
Comparación
Finalmente, aquí hay una comparación entre los dos enfoques en la misma entrada:
Las imágenes marcadas con puntos verdes se hacen con el enfoque n . ° 2 y las rojas con el n . ° 1 , todas en un tamaño de fuente de seis píxeles. Como puede ver en la imagen de la bombilla, el enfoque sensible a la forma es mucho mejor (incluso si el # 1 se realiza en una imagen de origen con zoom 2x).
Aplicación genial
Mientras leía las nuevas preguntas de hoy, tuve una idea de una aplicación genial que captura una región seleccionada del escritorio y la alimenta continuamente al convertidor ASCIIart y ve el resultado. Después de una hora de codificación, está hecho y estoy tan satisfecho con el resultado que simplemente debo agregarlo aquí.
De acuerdo, la aplicación consta de solo dos ventanas. La primera ventana maestra es básicamente mi antigua ventana de conversión sin la selección de imágenes y la vista previa (todo lo anterior está en ella). Solo tiene la configuración de conversión y vista previa ASCII. La segunda ventana es un formulario vacío con un interior transparente para la selección del área de agarre (sin funcionalidad alguna).
Ahora, con un temporizador, solo tomo el área seleccionada por el formulario de selección, la paso a la conversión y obtengo una vista previa del ASCIIart .
Por lo tanto, encierra un área que desea convertir en la ventana de selección y ve el resultado en la ventana maestra. Puede ser un juego, un visor, etc. Se ve así:
Así que ahora puedo ver incluso videos en ASCIIart por diversión. Algunos son realmente agradables :).
Si desea intentar implementar esto en GLSL , eche un vistazo a esto: