web-dev-qa-db-fra.com

Image à ASCII art

Prologue

Ce sujet apparaît ici sur [~ # ~] donc [~ # ~] de temps en temps, mais il est généralement supprimé parce qu'il est mal écrit. question. J'ai vu beaucoup de ces questions et puis silence de la [~ # ~] op [~ # ~] (rep faible habituelle) lorsque des informations supplémentaires sont demandées. De temps en temps, si l'entrée est suffisante pour moi, je décide de répondre avec une réponse et obtient généralement quelques votes positifs par jour lorsqu'il est actif, mais après quelques semaines, la question est supprimée/supprimée et commence au début. . J'ai donc décidé d'écrire ceci Q & A afin que je puisse faire référence à de telles questions directement sans réécrire la réponse encore et encore…

Une autre raison est également ceci META thread me cible, donc si vous avez des informations supplémentaires, n'hésitez pas à commenter.

Question

Comment convertir une image bitmap en art ASCII en utilisant C++ ?

Quelques contraintes:

  • images en niveaux de gris
  • en utilisant des polices mono-espacées
  • garder les choses simples (ne pas utiliser des outils trop avancés pour les programmeurs débutants)

Voici une page de wiki associée art ASCII (merci à @RogerRowland)

98
Spektre

Il y a plus d'approches pour l'image à ASCII art qui sont principalement basées sur l'utilisation de polices mono-espacées] par souci de simplicité, je m'en tiens uniquement à l'essentiel:

basé sur l'intensité de pixel/zone (Shading)

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

  1. carte de caractères d'intensité distribuée linéairement

    Nous n'utilisons donc que des caractères qui ont une différence d'intensité avec le même pas. En d'autres termes quand triés par ordre croissant alors:

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

    De plus, lorsque notre caractère map est trié, nous pouvons le calculer directement à partir de l'intensité (aucune recherche requise)

    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 de la fonction intensity_of(dot) Si nous trions le map[], Nous pouvons utiliser la recherche binaire, sinon nous avons besoin de la fonction O(n) search min distance ou la fonction O(1) dictionnaire. Parfois, par souci de simplicité, le caractère map[] Peut être traité comme une distribution linéaire, provoquant une légère distorsion gamma généralement invisible dans le résultat, à moins que vous ne sachiez quoi rechercher.

La conversion basée sur l'intensité est excellente également pour les images en niveaux de gris (pas seulement en noir et blanc). Si vous sélectionnez le point en tant que pixel unique, le résultat devient volumineux (1 pixel -> caractère unique). Ainsi, pour les images plus grandes, une zone (multiplication de la taille de la police) est sélectionnée pour préserver le rapport de format et ne pas agrandir trop.

Comment faire:

  1. divise donc uniformément l'image en pixels (niveaux de gris) ou en zones (rectangulaires) dot
  2. calculer l'intensité de chaque pixel/zone
  3. remplacez-le par le caractère de la carte avec l'intensité la plus proche

En tant que caractère map, vous pouvez utiliser n’importe quel caractère, mais le résultat est meilleur si les pixels du personnage sont répartis de manière égale dans la zone de caractères. Pour commencer, vous pouvez utiliser:

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

triés par ordre décroissant et prétendent être répartis linéairement.

Donc, si l’intensité pixel/surface est de i = <0-255>, Le personnage de remplacement sera

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

si i==0, le pixel/la zone est noir, si i==127, le pixel/la zone est gris et si i==255, le pixel/la zone est blanc. Vous pouvez expérimenter différents caractères à l'intérieur de map[] ...

Voici un exemple ancien 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 les éléments VCL sauf si vous utilisez l'environnement Borland/Embarcadero

  • mm_log Est un mémo où le texte est sorti
  • bmp est un bitmap en entrée
  • AnsiString est une chaîne de type VCL indexée sous la forme 1 et non à partir de 0 sous la forme char* !!!

voici le résultat: Légèrement NSFW intensité)

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

Lorsque vous ajoutez des éléments plus avancés tels que:

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

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

il en résulte ici un 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 une image de la même taille que celle du premier exemple échantillonné avec des zones:

Légèrement NSFW

Comme vous pouvez le constater, cela convient mieux aux grandes images.

Ajustement des caractères (hybride entre Shading et Solid ASCII Art)]

Cette approche tente de remplacer zone (plus aucun point pixel unique) par un caractère d'intensité et de forme similaires. Cela conduit à de meilleurs résultats même avec des polices plus grandes utilisées par rapport à l'approche précédente, par contre cette approche est un peu plus lente bien sûr. Il y a plus de façons de faire cela, 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 une somme naïve de différence abs entre les pixels, mais cela ne donnera pas de très bons résultats, car même un décalage de 1 pixel rendra la distance importante, mais vous pouvez utiliser une corrélation ou des métriques différentes. L'algorithme global est presque identique à l'approche précédente:

  1. divise donc uniformément l’image en zones rectangulaires (en niveaux de gris) dot ​​
    • idéalement avec les mêmes proportions que rendues caractères (cela préservera les proportions, n'oubliez pas que les caractères chevauchent généralement un bit sur l'axe x)
  2. calculer l'intensité de chaque zone (dot)
  3. remplacez-le par le caractère de 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ère en zones

    zones

    • calcule l'intensité séparée pour la zone gauche, droite, haut, bas et centre de chaque caractère à partir 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. traiter l'image source dans les zones rectangulaires

    • (avec les mêmes proportions que la police cible)
    • pour chaque zone, calculez l’intensité de la même manière que dans la puce 1
    • trouver la correspondance la plus proche des intensités en alphabet de conversion
    • caractère de sortie ajusté

C'est le résultat pour la taille de la police = 7px

char fitting example

Comme vous pouvez le constater, la sortie est visuellement agréable même avec une taille de police plus grande (l'exemple précédent était avec une taille de police de 5 pixels). La sortie a à peu près 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 proches de l'image d'origine non seulement par l'intensité mais aussi par la forme générale. Vous pouvez donc utiliser des polices plus grandes tout en préservant les détails (jusqu'à un point grossier).

Voici le 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 une application de formulaire simple (Form1) Contenant un seul TMemo mm_txt. Il charge l'image "pic.bmp", Puis, en fonction de la résolution, choisit l'approche à utiliser pour convertir en texte qui est enregistré en "pic.txt" Et envoyé au mémo pour visualisation. Pour ceux qui ne possèdent pas de VCL, ignorez le contenu de la VCL et remplacez AnsiString par votre type de chaîne, ainsi que le Graphics::TBitmap Par une classe de bitmap ou d'image à votre disposition avec une capacité d'accès en pixels.

Très important ​​remarque est que cela utilise les paramètres de mm_txt->Font Alors assurez-vous de définir:

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

pour que cela fonctionne correctement sinon la police ne sera pas traitée en mono-espacement. La molette de la souris change simplement la taille de la police pour afficher les résultats sur différentes tailles.

[Notes]

  • voir Visualisation Word Portraits
  • utiliser un langage avec des capacités d’accès bitmap/fichier et de sortie texte
  • il est fortement recommandé de commencer par la première approche car il est très facile d'aller de l'avant et simple, puis de passer à la seconde (ce qui peut être fait en modifiant la première pour que la plus grande partie du code reste telle quelle).
  • C'est une bonne idée de calculer avec une intensité inversée (les pixels noirs sont la valeur maximale) car l'aperçu du texte standard est sur fond blanc, ce qui permet d'obtenir de bien meilleurs résultats.
  • vous pouvez expérimenter avec la taille, le nombre et la disposition des zones de subdivision ou utiliser une grille comme 3x3 à la place.

[Edit1] comparaison

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

comparison

Les images marquées d'un point vert sont réalisées avec l'approche # 2 et les images rouges avec # 1 toutes sur la taille de police de pixels 6. 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 appliqué à une image source zoomée 2x).

[Edit2] application cool

En lisant les nouvelles questions d'aujourd'hui, j'ai eu l'idée d'une application géniale qui saisit une région sélectionnée du bureau et l'alimente en continu vers le convertisseur ASCIIart ​​et affiche le résultat. Après une heure de codage, le travail est terminé et je suis tellement satisfait du résultat que je dois simplement l’ajouter ici.

OK, l'application consiste en seulement 2 fenêtres. La première fenêtre principale est en gros mon ancienne fenêtre de conversion sans la sélection d’image et l’aperçu (toutes les informations ci-dessus y figurent). Il ne contient que les paramètres de prévisualisation et de conversion ASCII). La deuxième fenêtre est un formulaire vide avec une transparence à l'intérieur pour la sélection de la zone de saisie (aucune fonctionnalité).

Maintenant, avec la minuterie, je saisis simplement la zone sélectionnée par le formulaire de sélection, la passe en conversion et prévisualise le ASCIIart.

Vous définissez donc la zone que vous souhaitez convertir par la fenêtre de sélection et affichez le résultat dans la fenêtre principale. Ça peut être un jeu, spectateur, ... Ça ressemble à ça:

ASCIIart grabber example

Alors maintenant, je peux même regarder des vidéos dans ASCIIart ​​pour le plaisir. Certains sont vraiment gentils :).

hands

[Edit3]

Si vous voulez essayer d'implémenter ceci dans GLSL regardez ceci:

143
Spektre