Conversión de imagen a arte ASCII

102

Prólogo

Este tema aparece aquí en Stack Overflow de vez en cuando, pero generalmente se elimina porque es una pregunta mal escrita. Vi muchas preguntas de este tipo y luego silencio del OP (repetición baja habitual) cuando se solicita información adicional. De vez en cuando, si la entrada es lo suficientemente buena para mí, decido responder con una respuesta y, por lo general, recibe algunos votos positivos por día mientras está activa, pero luego de unas semanas la pregunta se quita / borra y todo comienza desde el comenzando. Así que decidí escribir estas preguntas y respuestas para poder hacer referencia a esas preguntas directamente sin volver a escribir la respuesta una y otra vez ...

Otra razón es también este meta hilo dirigido a mí, así que si recibiste información adicional, no dudes en comentar.

Pregunta

¿Cómo puedo convertir una imagen de mapa de bits a arte ASCII usando C ++ ?

Algunas limitaciones:

  • imagenes de escala de grises
  • utilizando fuentes monoespaciadas
  • mantenerlo simple (sin usar cosas demasiado avanzadas para programadores de nivel principiante)

Aquí hay una página de Wikipedia relacionada con arte ASCII (gracias a @RogerRowland).

Aquí laberinto similar a la conversión de arte ASCII Q&A.

Spektre
fuente
Usando esta página wiki como referencia, ¿puede aclarar a qué tipo de arte ASCII se refiere? Me suena a "Conversión de imagen a texto", que es una búsqueda "simple" de píxeles en escala de grises al carácter de texto correspondiente, así que me pregunto si te refieres a algo diferente. Aunque parece que vas a responder tú mismo de todos modos .....
Roger Rowland
Relacionado: stackoverflow.com/q/26347985/2564301
usr2564301
@RogerRowland tanto simple (solo basado en la intensidad de escala de grises) como más avanzado, teniendo en cuenta también la forma de los personajes (pero aún lo suficientemente simple)
Spektre
1
Si bien su trabajo es excelente, ciertamente agradecería una selección de muestras que sean un poco más SFW.
kmote
@TimCastelijns Si lee el prólogo, puede ver que esta no es la primera vez que se solicita este tipo de respuesta (y la mayoría de los votantes desde el principio estaban familiarizados con algunas preguntas anteriores relacionadas, por lo que el resto simplemente votó en consecuencia), ya que esta es una sesión de preguntas y respuestas no solo Q No perdí demasiado tiempo con la parte Q (lo cual es un error de mi lado, lo admito), he agregado algunas restricciones a la pregunta si tienes una mejor, siéntete libre de editar.
Spektre

Respuestas:

152

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:

  1. 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 mapestá ordenado, podemos calcular el personaje directamente desde la intensidad (no se necesita búsqueda)

     character = map[intensity_of(dot)/constant];
  2. 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:

  1. Divida uniformemente la imagen en píxeles (escala de grises) o áreas (rectangulares) punto s
  2. Calcule la intensidad de cada píxel / área
  3. 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á

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

Si i==0entonces el píxel / área es negro, si i==127entonces el píxel / área es gris, y si i==255entonces 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
  • AnsiStringes 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):

Ejemplo avanzado de intensidad

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:

  1. 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)

  2. Calcule la intensidad de cada área ( dot)

  3. Reemplácelo por un carácter del personaje mapcon 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:

  1. Dividir el área del personaje en zonas

    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).
  2. 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

Ejemplo de ajuste de personaje

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_txten é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 AnsiStringcon cualquier tipo de cadena que tenga, y también Graphics::TBitmapcon 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 3x3lugar.

Comparación

Finalmente, aquí hay una comparación entre los dos enfoques en la misma entrada:

Comparación

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í:

Ejemplo de capturador ASCIIart

Así que ahora puedo ver incluso videos en ASCIIart por diversión. Algunos son realmente agradables :).

Manos

Si desea intentar implementar esto en GLSL , eche un vistazo a esto:

Spektre
fuente
30
¡Hiciste un trabajo increíble aquí! ¡Gracias! ¡Y me encanta la censura ASCII!
Ander Biguri
1
Una sugerencia de mejora: elabora derivadas direccionales, no solo intensidad.
Yakk - Adam Nevraumont
1
@Yakk se preocupa por elaborar?
tariksbl
2
@tarik coincide no solo en intensidad, sino en derivadas: o, el pase de banda mejora los bordes. Básicamente, la intensidad no es lo único que la gente ve: ven degradados y bordes.
Yakk - Adam Nevraumont
1
@Yakk la subdivisión de las zonas hace algo así indirectamente. Podría ser incluso mejor manejar los personajes como 3x3zonas y comparar los DCT , pero creo que eso disminuiría mucho el rendimiento.
Spektre