Conversione da immagine ad arte ASCII


102

Prologo

Questo argomento viene visualizzato di tanto in tanto qui su Stack Overflow, ma di solito viene rimosso perché è una domanda scritta male. Ho visto molte di queste domande e poi il silenzio dall'OP (solita bassa ripetizione) quando sono richieste informazioni aggiuntive. Di tanto in tanto se l'input è abbastanza buono per me decido di rispondere con una risposta e di solito ottiene alcuni voti positivi al giorno mentre è attivo, ma poi dopo alcune settimane la domanda viene rimossa / eliminata e tutto inizia dal inizio. Così ho deciso di scrivere questo Q & A in modo da poter fare riferimento a queste domande direttamente senza dover riscrivere la risposta più e più volte ...

Un altro motivo è anche questo meta thread mirato a me, quindi se hai ulteriori input, sentiti libero di commentare.

Domanda

Come posso convertire un'immagine bitmap in arte ASCII usando C ++ ?

Alcuni vincoli:

  • immagini in scala di grigi
  • utilizzando caratteri a spaziatura singola
  • mantenerlo semplice (non usare cose troppo avanzate per programmatori di livello principiante)

Ecco una pagina di Wikipedia relativa all'arte ASCII (grazie a @RogerRowland).

Qui labirinto simile alla conversione di ASCII Art Q&A.


Usando questa pagina wiki come riferimento, puoi chiarire a quale tipo di arte ASCII ti riferisci? Mi suona come "Conversione da immagine a testo" che è una "semplice" ricerca dai pixel in scala di grigi al carattere di testo corrispondente, quindi mi chiedo se intendi qualcosa di diverso. Sembra che tu risponda comunque tu stesso .....
Roger Rowland


@RogerRowland sia semplice (basata solo sull'intensità della scala di grigi) che più avanzato tenendo conto anche della forma dei personaggi (ma comunque abbastanza semplice)
Spektre

1
Anche se il tuo lavoro è fantastico, apprezzerei sicuramente una selezione di campioni un po 'più SFW.
kmote

@TimCastelijns Se leggete il prologo allora si può vedere questa non è la prima volta che è stato richiesto questo tipo di risposta (e la maggior parte degli elettori di partenza dove familiarità con alcune domande precedenti legati in modo che il resto appena votato di conseguenza), Poiché si tratta di Q & A non solo D Non ho perso troppo tempo con la parte Q (che è colpa mia, lo ammetto) ho aggiunto poche restrizioni alla domanda se ne hai una migliore, sentiti libero di modificare.
Spektre

Risposte:


152

Esistono più approcci per la conversione da immagine a arte ASCII che si basano principalmente sull'uso di caratteri a spaziatura singola . Per semplicità, mi attengo solo alle basi:

Basato su intensità di pixel / area (ombreggiatura)

Questo approccio gestisce ogni pixel di un'area di pixel come un singolo punto. L'idea è di calcolare l'intensità media della scala di grigi di questo punto e quindi sostituirla con un carattere con un'intensità abbastanza vicina a quella calcolata. Per questo abbiamo bisogno di un elenco di caratteri utilizzabili, ciascuno con un'intensità precalcolata. Chiamiamolo un personaggio map. Per scegliere più rapidamente quale personaggio è il migliore per quale intensità, ci sono due modi:

  1. Mappa dei caratteri di intensità distribuita linearmente

    Quindi usiamo solo caratteri che hanno una differenza di intensità con lo stesso passo. In altre parole, se ordinato in ordine crescente:

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

    Inoltre, quando il nostro personaggio mapè ordinato, possiamo calcolare il carattere direttamente dall'intensità (nessuna ricerca necessaria)

     character = map[intensity_of(dot)/constant];
  2. Mappa dei caratteri a intensità distribuita arbitraria

    Quindi abbiamo una serie di caratteri utilizzabili e le loro intensità. Dobbiamo trovare di nuovo l'intensità più vicina a intensity_of(dot)So, se ordiniamo il map[], possiamo usare la ricerca binaria, altrimenti abbiamo bisogno di un O(n)loop o di un O(1)dizionario di distanza minima di ricerca . A volte per semplicità, il personaggio map[]può essere gestito come distribuito linearmente, provocando una leggera distorsione gamma, solitamente invisibile nel risultato a meno che non si sappia cosa cercare.

La conversione basata sull'intensità è ottima anche per le immagini in scala di grigi (non solo in bianco e nero). Se selezioni il punto come un singolo pixel, il risultato diventa grande (un pixel -> singolo carattere), quindi per immagini più grandi viene selezionata invece un'area (moltiplicare la dimensione del carattere) per preservare le proporzioni e non ingrandire troppo.

Come farlo:

  1. Uniformemente dividere l'immagine in pixel (scala di grigi) o (rettangolari) aree puntino s
  2. Calcola l'intensità di ogni pixel / area
  3. Sostituiscilo con il carattere della mappa dei caratteri con l'intensità più vicina

Come personaggio mappuoi usare qualsiasi carattere, ma il risultato migliora se il personaggio ha pixel distribuiti uniformemente lungo l'area del carattere. Per cominciare puoi usare:

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

ordinati discendenti e fingono di essere distribuiti linearmente.

Quindi, se l'intensità del pixel / area è i = <0-255>il carattere sostitutivo sarà

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

Se i==0il pixel / area è nero, se i==127il pixel / area è grigio e se i==255il pixel / area è bianco. Puoi sperimentare con diversi personaggi all'interno map[]...

Ecco un mio antico esempio in 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;

È necessario sostituire / ignorare le cose VCL a meno che non si utilizzi l' ambiente Borland / Embarcadero .

  • mm_log è il promemoria in cui viene emesso il testo
  • bmp è la bitmap di input
  • AnsiStringè una stringa di tipo VCL indicizzata da 1, non da 0 come char*!!!

Questo è il risultato: immagine di esempio di intensità leggermente NSFW

A sinistra c'è l'output di arte ASCII (dimensione del carattere 5 pixel) e l'immagine in ingresso a destra è stata ingrandita alcune volte. Come puoi vedere, l'output è pixel più grande -> carattere. Se usi aree più grandi invece di pixel, lo zoom è più piccolo, ma ovviamente l'output è visivamente meno piacevole. Questo approccio è molto facile e veloce da codificare / elaborare.

Quando aggiungi cose più avanzate come:

  • calcoli di mappe automatizzati
  • selezione automatica delle dimensioni di pixel / area
  • correzioni delle proporzioni

Quindi puoi elaborare immagini più complesse con risultati migliori:

Ecco il risultato in un rapporto 1: 1 (ingrandisci per vedere i caratteri):

Esempio di intensità avanzata

Naturalmente, per il campionamento dell'area si perdono i piccoli dettagli. Questa è un'immagine delle stesse dimensioni del primo esempio campionato con aree:

Immagine di esempio avanzata di intensità leggermente NSFW

Come puoi vedere, questo è più adatto per immagini più grandi.

Adattamento dei caratteri (ibrido tra ombreggiatura e arte ASCII solida)

Questo approccio cerca di sostituire l'area (non più punti di un singolo pixel) con caratteri con intensità e forma simili. Ciò porta a risultati migliori, anche con caratteri più grandi utilizzati rispetto all'approccio precedente. D'altra parte, questo approccio è ovviamente un po 'più lento. Ci sono più modi per farlo, ma l'idea principale è calcolare la differenza (distanza) tra l'area dell'immagine ( dot) e il carattere renderizzato. Puoi iniziare con la somma ingenua della differenza assoluta tra i pixel, ma ciò porterà a risultati non molto buoni perché anche uno spostamento di un pixel aumenterà la distanza. Invece puoi usare la correlazione o metriche diverse. L'algoritmo generale è quasi lo stesso dell'approccio precedente:

  1. Così in modo uniforme dividere l'immagine (in scala di grigi) aree rettangolari dot s'

    idealmente con le stesse proporzioni dei caratteri dei font renderizzati (manterrà le proporzioni. Non dimenticare che i caratteri di solito si sovrappongono leggermente sull'asse x)

  2. Calcola l'intensità di ogni area ( dot)

  3. Sostituiscilo con un carattere del personaggio mapcon l'intensità / forma più vicina

Come possiamo calcolare la distanza tra un carattere e un punto? Questa è la parte più difficile di questo approccio. Durante la sperimentazione, sviluppo questo compromesso tra velocità, qualità e semplicità:

  1. Dividi l'area del personaggio in zone

    Zone

    • Calcola un'intensità separata per la zona sinistra, destra, su, giù e centrale di ogni carattere dall'alfabeto di conversione ( map).
    • Normalizzare tutte le intensità, in modo che siano indipendenti dalla dimensione dell'area, i=(i*256)/(xs*ys).
  2. Elabora l'immagine sorgente in aree rettangolari

    • (con le stesse proporzioni del carattere di destinazione)
    • Per ogni area, calcola l'intensità nello stesso modo del punto 1
    • Trova la corrispondenza più vicina dalle intensità nell'alfabeto di conversione
    • Emetti il ​​carattere adattato

Questo è il risultato per la dimensione del carattere = 7 pixel

Esempio di adattamento del personaggio

Come puoi vedere, l'output è visivamente piacevole, anche con una dimensione del carattere più grande utilizzata (l'esempio di approccio precedente era con una dimensione del carattere di 5 pixel). L'output ha all'incirca le stesse dimensioni dell'immagine in ingresso (senza zoom). I risultati migliori si ottengono perché i caratteri sono più vicini all'immagine originale, non solo per intensità, ma anche per forma complessiva, e quindi puoi usare caratteri più grandi e conservare comunque i dettagli (fino a un certo punto ovviamente).

Ecco il codice completo per l'applicazione di conversione basata su 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();
}

//---------------------------------------------------------------------------

È semplice un modulo application ( Form1) con un singolo TMemo mm_txtin esso. Carica un'immagine, "pic.bmp"quindi, in base alla risoluzione, sceglie quale approccio utilizzare per convertire in testo che viene salvato "pic.txt"e inviato al promemoria per la visualizzazione.

Per quelli senza VCL, ignora le cose VCL e sostituiscile AnsiStringcon qualsiasi tipo di stringa che hai, e anche Graphics::TBitmapcon qualsiasi bitmap o classe di immagine che hai a disposizione con capacità di accesso ai pixel.

Una nota molto importante è che questo utilizza le impostazioni di mm_txt->Font, quindi assicurati di impostare:

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

per farlo funzionare correttamente, altrimenti il ​​carattere non verrà gestito come mono-spaziatura. La rotellina del mouse cambia solo la dimensione del carattere su / giù per vedere i risultati su diverse dimensioni dei caratteri.

[Appunti]

  • Vedere la visualizzazione dei ritratti di parole
  • Utilizzare una lingua con accesso a bitmap / file e funzionalità di output di testo
  • Consiglio vivamente di iniziare con il primo approccio in quanto è molto facile, diretto e semplice, e solo dopo passare al secondo (che può essere fatto come modifica del primo, quindi la maggior parte del codice rimane comunque così com'è)
  • È una buona idea calcolare con intensità invertita (pixel neri è il valore massimo) perché l'anteprima del testo standard è su uno sfondo bianco, quindi porta a risultati molto migliori.
  • puoi sperimentare con le dimensioni, il conteggio e il layout delle zone di suddivisione o utilizzare una griglia simile 3x3.

Confronto

Infine ecco un confronto tra i due approcci sullo stesso input:

Confronto

Le immagini contrassegnate dal punto verde sono realizzate con l'approccio n. 2 e quelle rosse con il n. 1 , tutte su una dimensione del carattere di sei pixel. Come puoi vedere sull'immagine della lampadina, l'approccio sensibile alla forma è molto migliore (anche se il n. 1 viene eseguito su un'immagine sorgente ingrandita 2x).

Bella applicazione

Durante la lettura delle nuove domande di oggi, ho avuto l'idea di una fantastica applicazione che cattura una regione selezionata del desktop e la invia continuamente al convertitore ASCIIart e visualizza il risultato. Dopo un'ora di codifica, è fatto e sono così soddisfatto del risultato che devo semplicemente aggiungerlo qui.

OK l'applicazione è composta da due sole finestre. La prima finestra principale è fondamentalmente la mia vecchia finestra di conversione senza la selezione e l'anteprima dell'immagine (tutte le cose sopra sono dentro). Ha solo l'anteprima ASCII e le impostazioni di conversione. La seconda finestra è un modulo vuoto con interno trasparente per la selezione dell'area di cattura (nessuna funzionalità di sorta).

Ora su un timer, prendo semplicemente l'area selezionata dal modulo di selezione, la passo alla conversione e visualizzo l'anteprima di ASCIIart .

Quindi racchiudi un'area che desideri convertire dalla finestra di selezione e visualizzi il risultato nella finestra principale. Può essere un gioco, un visualizzatore e così via. Assomiglia a questo:

Esempio di grabber ASCIIart

Così ora posso guardare anche i video in ASCIIart per divertimento. Alcuni sono davvero carini :).

Mani

Se vuoi provare a implementarlo in GLSL , dai un'occhiata a questo:


30
Hai fatto un lavoro incredibile qui! Grazie! E adoro la censura ASCII!
Ander Biguri

1
Un suggerimento per migliorare: elabora derivati ​​direzionali, non solo intensità.
Yakk - Adam Nevraumont

1
@ Yakk cura di elaborare?
tariksbl

2
@tarik o corrisponde non solo all'intensità, ma ai derivati: oppure, il passaggio banda migliora i bordi. Fondamentalmente l'intensità non è l'unica cosa che le persone vedono: vedono sfumature e bordi.
Yakk - Adam Nevraumont

1
@Yakk la suddivisione delle zone fa qualcosa del genere indirettamente. Potrebbe essere ancora meglio gestire i personaggi come 3x3zone e confrontare i DCT , ma credo che ciò ridurrebbe molto le prestazioni.
Spektre
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.