Conversion de l'Image à L'art ASCII

Prologue

Ce sujet apparaît ici sur DONC de temps à autre, mais il est plus souvent à cause d'un mal question écrite. J'ai vu beaucoup de telles questions et puis le silence de la OP (habituellement basse rep) quand des informations supplémentaires est demandée. De temps en temps, si l'entrée est assez bonne pour moi, je décide de répondre avec une réponse et il obtient généralement quelques up-votes par jour tout en actif, mais après quelques semaines la question est supprimée et tout commence dès le début. J'ai donc décidé d'écrire ce Q&R pour pouvoir référencer de telles questions directement sans réécrire la réponse encore et encore ...

une autre raison est également cette META thread ciblée sur moi donc si vous avez des entrées supplémentaires n'hésitez pas à commenter.

Question

comment convertir l'image bitmap en ASCII art utilisant C++ ?

Certaines contraintes:

  • échelle de gris des images
  • utilisant des polices à interligne unique
  • garder les choses simples (pas trop à l'aide d'un niveau assez avancé pour niveau débutant programmeurs)

Voici une page Wiki apparentée ASCII art (merci à @RogerRowland)

92
demandé sur Community 2015-10-07 11:16:47

1 réponses

il y a plus d'approches pour la conversion d'image vers l'art ASCII qui sont principalement basées sur l'utilisation de polices à interligne unique pour la simplicité je m'en tiens aux bases:

pixel / zone intensité basée (ombrage)

cette approche traite chaque pixel de la zone des pixels comme un point simple. L'idée est de calculer l'intensité moyenne de l'échelle de gris de ce point, puis de le remplacer par le caractère de proximité assez d'intensité pour le calcul. Pour cela, nous avons besoin d'une liste de caractères utilisables avec une intensité prédéfinie, appelons-la le caractère map . Pour choisir plus rapidement quel caractère est le meilleur pour quelle intensité il y a deux façons:

  1. carte des caractéristiques d'intensité linéairement répartie

    donc nous n'utilisons que des caractères qui ont une différence d'intensité avec la même étape. En d'autres termes lorsque trié Ascendant alors:

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

    aussi quand notre caractère map est trié alors nous pouvons calculer le caractère directement à partir de l'intensité (pas de recherche nécessaire)

    character=map[intensity_of(dot)/constant];
    
  2. carte de caractères d'intensité distribuée arbitraire

    nous avons donc un tableau de caractères utilisables et leurs intensités. Nous devons trouver l'intensité la plus proche du intensity_of(dot) donc encore si nous avons trié le map[] nous pouvons utiliser la recherche binaire sinon nous avons besoin de O(n) boucle de recherche min distance ou O(1) dictionnaire. Parfois, pour des raisons de simplicité, le caractère map[] peut être manipulé de façon linéaire, provoquant une légère distorsion gamma généralement invisible dans le résultat, à moins que vous ne sachiez quoi chercher.

la conversion basée sur L'intensité est grande aussi pour les images à échelle de gris (pas seulement noir et blanc). Si vous sélectionnez le point que un seul pixel le résultat devient grand (1 pixel -> un seul caractère) donc pour les images plus grandes une zone (multiplication de la taille de la police) est sélectionnée à la place pour préserver le rapport d'aspect et ne pas trop agrandir.

Comment le faire:

  1. divisez l'image de façon uniforme en pixels (à l'échelle de gris)ou en zones (rectangulaires) point 's
  2. calcule l'intensité de chaque pixel / zone
  3. remplacer par caractère tiré de la carte de caractères avec l'intensité la plus proche

comme caractère map vous pouvez utiliser n'importe quel caractère mais le résultat devient meilleur si le caractère a des pixels dispersés uniformément le long de la zone de caractère. Pour commencer, vous pouvez utiliser:

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

descendantes triées et prétendant être distribuées linéairement.

donc si l'intensité du pixel / zone est i = <0-255> alors le caractère de remplacement sera

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

si i==0 , alors le pixel/zone est noir, si i==127 , alors le pixel/la zone grise, et si i==255 , alors le pixel/est blanc. Vous pouvez expérimenter avec différents caractères dans map[] ...

voici un ancien exemple de mine en C++ et 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;

vous devez remplacer/ignorer VCL stuff sauf si vous utilisez Borland / Amarcadero environment

  • mm_log est un mémo où le texte est sorti
  • bmp est entrée bitmap
  • AnsiString est une chaîne de type Vcl dont la forme indexée 1 n'est pas de 0 comme char* !!!

c'est le résultat: légèrement intensité NSFW exemple d'image

sur la gauche est sortie ASCII art (taille de police 5px), et sur l'image d'entrée droite zoomé quelques fois. Comme vous pouvez le voir la sortie est plus grand pixel -> caractère. si vous utilisez des zones plus grandes au lieu de pixels, alors le zoom est plus petit, mais bien sûr la sortie est moins visuellement agréable. cette approche est très facile et rapide à coder/traiter.

quand vous ajoutez des choses plus avancées comme:

  • automatisé des calculs carte
  • sélection automatique de la taille des pixels/zones
  • corrections du rapport d'aspect

alors vous pouvez traiter des images plus complexes avec de meilleurs résultats:

résultat ici en rapport 1:1 (zoom pour voir les caractères):

intensity advanced example

bien sûr pour l'échantillonnage de zone vous perdez les petits détails. C'est l'image de la même taille que le premier exemple échantillonné avec des zones:

intensité NSFW légère image d'exemple avancée

comme vous pouvez le voir c'est plus adapté pour les plus grandes images

Caractère de montage (hybride entre ombres et Solide ASCII Art)

cette approche tente de remplacer la zone (pas plus de points d'un seul pixel) par un caractère d'intensité similaire et forme. Cela conduit à de meilleurs résultats, même avec des polices plus grandes utilisées en comparaison avec l'approche précédente d'autre part cette approche est un peu plus lente bien sûr. Il y a d'autres façons de le faire, mais l'idée principale est de calculer la différence (distance) entre la zone de l'image ( dot ) et le caractère Rendu. Vous pouvez commencer avec la somme naïve de différence abs entre les pixels mais cela conduira à des résultats pas très bons parce que même un décalage de 1 pixel fera la distance Grande, à la place vous pouvez utiliser corrélation ou autre mesure. L'algorithme général est presque le même que l'approche précédente:

  1. divisez l'image de façon si égale à (échelle de gris) zones rectangulaires point 's
    • idéalement avec le même rapport d'aspect que Rendu caractères de police (il préservera le rapport d'aspect, ne pas oublier que les caractères se chevauchent habituellement un peu dans l'axe des x)
  2. calcule l'intensité de chaque zone ( dot )
  3. le remplacer par le caractère du caractère map avec l'intensité/la forme la plus proche

comment calculer la distance entre le caractère et le point? C'est la partie la plus difficile de cette approche. En expérimentant, je développe ce compromis entre vitesse, qualité et simplicité:

  1. diviser la zone de caractères en zones

    zones

    • calculez l'intensité séparée pour la Zone Gauche, Droite, Haut, Bas et centre de chaque caractère de votre alphabet de conversion ( map )
    • normaliser toutes les intensités afin qu'elles soient indépendantes de la taille de la zone i=(i*256)/(xs*ys)
  2. le processus de la source de l'image dans le rectangle zones

    • (avec le même format que la police cible)
    • pour chaque zone, calculer l'intensité de la même manière qu'au point 1
    • trouver la correspondance la plus proche des intensités dans l'alphabet de conversion
    • caractère de sortie ajusté

ce résultat est pour taille de la police = 7PX

char fitting example

comme vous pouvez le voir la sortie est visuellement agréable, même avec une taille de police plus grande utilisé (l'exemple d'approche précédente était avec la taille de police 5px). La sortie est à peu près de la même taille que l'image d'entrée (pas de zoom). Les meilleurs résultats sont obtenus parce que les caractères sont plus près de l'image originale non seulement par l'intensité mais aussi par la forme globale et donc vous pouvez utiliser plus grand les polices et la conservation des détails (jusqu'à un point de grossier).

ici, code complet pour l'application de conversion basée sur 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) // charcter 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;     // temp 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 init buffers
        tmp=new Graphics::TBitmap; if (tmp==NULL) break;
            // allow 32bit 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 image by zoomed character size step
        xf-=xf/3; // characters are usually overlaping 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 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();
    }
//---------------------------------------------------------------------------

c'est la forme simple app ( Form1 ) avec simple TMemo mm_txt dans elle. Il charge l'image "pic.bmp" , puis selon la résolution choisir l'approche à utiliser pour convertir en texte qui est sauvé à "pic.txt" et envoyé à Mémo pour visualiser. Pour ceux sans VCL ignorer la substance VCL et remplacer AnsiString avec n'importe quel type de chaîne que vous avez, et aussi le Graphics::TBitmap avec n'importe quelle classe de bitmap ou d'image que vous avez à disposition avec la capacité d'accès de pixel.

très important note est que cela utilise les paramètres de mm_txt->Font donc assurez-vous de définir:

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

à faire cela fonctionne correctement, sinon la police ne sera pas traitée comme une police à interligne unique. La roue de la souris change simplement la taille de la police pour voir les résultats sur différentes tailles de police

[Notes]

  • voir Portraits de mots de visualisation
  • utiliser la langue avec bitmap/accès au fichier et le texte des capacités de sortie
  • recommande fortement de commencer par le premier approche car il est très facile de détroit vers l'avant et simple, et seulement ensuite passer à la seconde (ce qui peut être fait comme une modification de la première, donc la plupart du code reste est de toute façon)
  • c'est une bonne idée de calculer avec une intensité inversée (les pixels noirs sont la valeur max) parce que la prévisualisation de texte standard est sur fond blanc conduisant donc à de bien meilleurs résultats.
  • vous pouvez expérimenter avec la taille, le compte et la disposition des zones de subdivision ou utiliser une grille comme 3x3 à la place.

[Édite1] comparaison

voici Enfin une comparaison entre les deux approches sur la même entrée:

comparison

Le point vert marqué les images sont faites avec l'approche #2 et de rouge avec #1 tout sur 6 pixel taille de la police. Comme vous pouvez le voir sur l'image de l'ampoule, l'approche sensible à la forme est bien meilleure (même si le #1 est fait sur l'image Source zoomée 2x).

[Edit2] cool app

en lisant les nouvelles questions d'aujourd'hui j'ai eu une idée d'une application cool qui saisit la région choisie du bureau et l'alimente en continu à asciiart Converteur et voir le résultat. Après une heure de le codage est fait et je suis tellement satisfait du résultat que je dois tout simplement l'ajouter ici.

OK L'application se compose de seulement 2 fenêtres. La première fenêtre principale est essentiellement ma vieille fenêtre de conversion sans la sélection d'image et l'aperçu (tous les trucs ci-dessus sont dedans). Il a juste l'aperçu ASCII et les paramètres de conversion. La deuxième fenêtre est vide avec l'intérieur transparent pour la sélection de la zone de saisie (aucune fonctionnalité que ce soit).

maintenant sur timer je viens de saisir la zone sélectionnée par le formulaire de sélection, passer à la conversion et prévisualiser le ASCIIart .

ainsi vous enfermez la zone que vous voulez convertir par la fenêtre de sélection et de voir le résultat dans la fenêtre principale. Il peut être un jeu,visualiseur... Il ressemble à ceci:

ASCIIart grabber example

donc maintenant je peux regarder même des vidéos dans ASCIIart pour le plaisir. Certains sont vraiment sympa :).

hands

[Edit 3]

si vous voulez essayer de mettre en œuvre ceci dans GLSL jetez un oeil à ceci:

134
répondu Spektre 2018-07-12 15:43:55