Conversion d'image en art ASCII


102

Prologue

Ce sujet apparaît ici sur Stack Overflow de temps en temps, mais il est généralement supprimé car il s'agit d'une question mal écrite. J'ai vu beaucoup de ces questions, puis le silence de l' OP (faible représentant habituel) lorsque des informations supplémentaires sont demandées. De temps en temps, si l'entrée est assez bonne pour moi, je décide de répondre avec une réponse et elle obtient généralement quelques votes par jour lorsqu'elle est active, mais après quelques semaines, la question est supprimée / supprimée et tout commence à partir du début. J'ai donc décidé d'écrire ce Q&A afin de pouvoir référencer directement ces questions sans réécrire la réponse encore et encore ...

Une autre raison est également ce méta-fil qui m'est destiné, donc si vous avez des informations supplémentaires, n'hésitez pas à commenter.

Question

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

Quelques contraintes:

  • images en échelle de gris
  • utilisation de polices mono-espacées
  • garder les choses simples (ne pas utiliser de trucs trop avancés pour les programmeurs de niveau débutant)

Voici une page Wikipédia liée à l' art ASCII (merci à @RogerRowland).

Voici un labyrinthe similaire à l'ASCII Art conversion Q&A.


En utilisant cette page wiki comme référence, pouvez-vous préciser à quel type d'art ASCII vous faites référence? Cela me ressemble à une "conversion d'image en texte" qui est une recherche "simple" des pixels en niveaux de gris au caractère de texte correspondant, donc je me demande si vous voulez dire quelque chose de différent. On dirait que vous allez quand même y répondre vous-même .....
Roger Rowland


@RogerRowland à la fois simple (uniquement basé sur l'intensité des niveaux de gris) et plus avancé prenant en compte également la forme des personnages (mais toujours assez simple)
Spektre

1
Bien que votre travail soit excellent, j'apprécierais certainement une sélection d'échantillons un peu plus SFW.
kmote

@TimCastelijns Si vous lisez le prologue, vous pouvez voir que ce n'est pas la première fois qu'un tel type de réponse était demandé (et la plupart des électeurs du début connaissaient peu de questions précédentes, les autres ont donc voté en conséquence), car il ne s'agit pas seulement de questions et réponses Q Je n'ai pas perdu trop de temps avec la partie Q (ce qui est une faute de mon côté je l'admets) ont ajouté quelques restrictions à la question si vous en avez une meilleure, n'hésitez pas à la modifier.
Spektre

Réponses:


152

Il existe d'autres approches pour la conversion d'image en art ASCII qui sont principalement basées sur l'utilisation de polices à espacement unique . Pour simplifier, je m'en tiens uniquement aux bases:

Basé sur l'intensité des pixels / zones (ombrage)

Cette approche traite chaque pixel d'une zone de pixels comme un point unique. L'idée est de calculer l'intensité moyenne de l'échelle de gris de ce point, puis de le remplacer par un caractère avec une 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 cela un personnage 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, une fois triés par ordre croissant, alors:

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

    De plus, lorsque notre personnage mapest trié, nous pouvons calculer le caractère directement à partir de l'intensité (aucune recherche nécessaire)

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

    Nous avons donc une gamme de caractères utilisables et leurs intensités. Nous devons trouver l'intensité la plus proche du intensity_of(dot)Donc, encore une fois, si nous avons trié le map[], nous pouvons utiliser la recherche binaire, sinon nous avons besoin d'une O(n)boucle ou d'un O(1)dictionnaire de distance minimale de recherche . Parfois, par souci de simplicité, le caractère map[]peut être traité comme étant distribué linéairement, 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 également idéale 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 grand (un pixel -> caractère unique), donc pour les images plus grandes, une zone (multiplication de la taille de la police) est sélectionnée à la place pour conserver le rapport hauteur / largeur et ne pas trop agrandir.

Comment faire:

  1. Divisez uniformément l'image en pixels (en niveaux de gris) ou en zones (rectangulaires) point s
  2. Calculez l'intensité de chaque pixel / zone
  3. Remplacez-le par un caractère de la carte de caractères avec l'intensité la plus proche

En tant que personnage, mapvous pouvez utiliser n'importe quel caractère, mais le résultat est meilleur si le personnage a des pixels répartis uniformément le long de la zone de caractère. Pour commencer, vous pouvez utiliser:

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

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

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

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

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

Voici un ancien exemple du mien 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 le mémo où le texte est sorti
  • bmp est le bitmap d'entrée
  • AnsiStringest une chaîne de type VCL indexée à partir de 1, pas à partir de 0 car char*!!!

Voici le résultat: Image d'exemple d'intensité légèrement NSFW

Sur la gauche se trouve la sortie artistique ASCII (taille de la police 5 pixels), et sur l'image d'entrée de droite agrandie plusieurs fois. Comme vous pouvez le voir, la sortie est un pixel plus grand -> 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 visuellement. Cette approche est très simple et rapide à coder / traiter.

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

  • calculs cartographiques automatisés
  • sélection automatique de la taille des pixels / zones
  • corrections de rapport hauteur / largeur

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

Voici le résultat dans un rapport 1: 1 (zoomez pour voir les caractères):

Exemple avancé d'intensité

Bien sûr, pour l'échantillonnage par zone, vous perdez les petits détails. Il s'agit d'une image de la même taille que le premier exemple échantillonné avec des zones:

Image d'exemple avancée d'intensité légèrement NSFW

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

Ajustement des caractères (hybride entre l'ombrage et l'art ASCII solide)

Cette approche tente de remplacer la zone (plus de points de pixel unique) par un caractère ayant une intensité et une forme similaires. Cela conduit à de meilleurs résultats, même avec des polices plus grandes utilisées par rapport à l'approche précédente. En revanche, cette approche est un peu plus lente bien sûr. Il existe d'autres moyens de le faire, mais l'idée principale est de calculer la différence (distance) entre la zone d'image ( dot) et le caractère rendu. Vous pouvez commencer par une somme naïve de la différence absolue entre les pixels, mais cela ne conduira pas à de très bons résultats car même un décalage d'un pixel rendra la distance grande. Au lieu de cela, vous pouvez utiliser la corrélation ou différentes mesures. L'algorithme global est presque le même que l'approche précédente:

  1. Donc diviser uniformément l'image à échelle de gris (-zones rectangulaires) dot « s

    idéalement avec le même rapport hauteur / largeur que les caractères de police rendus (cela conservera le rapport hauteur / largeur. N'oubliez pas que les caractères se chevauchent généralement un peu sur l'axe des x)

  2. Calculez l'intensité de chaque zone ( dot)

  3. Remplacez-le par un caractère du personnage mapavec l'intensité / la forme la plus proche

Comment calculer la distance entre un caractère et un 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

    • Calculez une intensité distincte pour les zones gauche, droite, haut, bas et centrale de chaque caractère à partir de votre alphabet de conversion ( map).
    • Normaliser toutes les intensités, elles sont indépendantes de la taille de la zone, i=(i*256)/(xs*ys).
  2. Traitez l'image source dans des zones rectangulaires

    • (avec le même rapport hauteur / largeur que la police cible)
    • Pour chaque zone, calculez l'intensité de la même manière que dans la puce # 1
    • Trouvez la correspondance la plus proche des intensités dans l'alphabet de conversion
    • Sortie du caractère ajusté

Voici le résultat pour la taille de la police = 7 pixels

Exemple d'ajustement de caractère

Comme vous pouvez le voir, la sortie est visuellement agréable, même avec une taille de police plus grande (l'exemple d'approche 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 intensité, mais aussi par forme générale, et vous pouvez donc utiliser des polices plus grandes tout en préservant les détails (jusqu'à un certain point bien sûr).

Voici le code complet de l'application de conversion basée sur la 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();
}

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

Il s'agit d'un simple formulaire de demande ( Form1) avec un seul TMemo mm_txt. Il charge une image, "pic.bmp"puis, en fonction de la résolution, choisit l'approche à utiliser pour convertir en texte qui est enregistré "pic.txt"et envoyé dans le mémo à visualiser.

Pour ceux qui n'ont pas de VCL, ignorez le contenu de la VCL et remplacez-le AnsiStringpar n'importe quel type de chaîne que vous avez, ainsi que Graphics::TBitmappar n'importe quelle classe de bitmap ou d'image dont vous disposez avec la capacité d'accès aux pixels.

Une note très importante 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 comme mono-interligne. La molette de la souris change simplement la taille de la police vers le haut / bas pour voir les résultats sur différentes tailles de police.

[Remarques]

  • Voir la visualisation Word Portraits
  • Utilisez un langage avec accès bitmap / fichier et capacités de sortie de texte
  • Je recommande fortement de commencer par la première approche car elle est très simple, directe et simple, puis de passer à la seconde (ce qui peut être fait en tant que modification de la première, de sorte que la plupart du code reste tel quel de toute façon)
  • C'est une bonne idée de calculer avec une intensité inversée (les pixels noirs sont la valeur maximale) car l'aperçu de texte standard est sur un fond blanc, ce qui conduit à 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 place.

Comparaison

Voici enfin une comparaison entre les deux approches sur une même entrée:

Comparaison

Les images marquées par un point vert sont réalisées avec l'approche n ° 2 et les images rouges avec le n ° 1 , le tout sur une taille de police de six pixels. 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 effectué sur une image source zoomée 2x).

Application cool

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

OK, l'application se compose de seulement deux 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 (tout ce qui précède y est). Il n'a que les paramètres de prévisualisation et de conversion ASCII. La deuxième fenêtre est un formulaire vide avec un intérieur transparent pour la sélection de la zone de saisie (aucune fonctionnalité du tout).

Maintenant, sur une minuterie, je saisis simplement la zone sélectionnée par le formulaire de sélection, la passe à la conversion et prévisualise l' ASCIIart .

Ainsi, vous entourez une zone que vous souhaitez convertir par la fenêtre de sélection et affichez le résultat dans la fenêtre principale. Cela peut être un jeu, une visionneuse, etc. Cela ressemble à ceci:

Exemple de saisie ASCIIart

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

Mains

Si vous souhaitez essayer d'implémenter cela dans GLSL , jetez un œil à ceci:


30
Vous avez fait un travail incroyable ici! Merci! Et j'adore la censure ASCII!
Ander Biguri

1
Une suggestion d'amélioration: élaborez des dérivées directionnelles, pas seulement l'intensité.
Yakk - Adam Nevraumont

1
@Yakk veut-il élaborer?
tariksbl

2
@tarik correspond non seulement à l'intensité, mais aussi aux dérivés: ou, la bande passante améliore les bords. Fondamentalement, l'intensité n'est pas la seule chose que les gens voient: ils voient les dégradés et les bords.
Yakk - Adam Nevraumont

1
@Yakk la subdivision des zones fait un peu une telle chose indirectement. Peut-être même mieux serait de gérer les caractères en tant que 3x3zones et de comparer les DCT, mais cela diminuerait beaucoup les performances, je pense.
Spektre
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.