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.
Comment convertir une image bitmap en art ASCII en utilisant C++ ?
Quelques contraintes:
Voici une page de wiki associée art ASCII (merci à @RogerRowland)
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:
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];
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:
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 sortibmp
est un bitmap en entréeAnsiString
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:
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):
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:
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:
dot
)map
avec l'intensité/la forme la plus procheComment 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é:
Diviser la zone de caractère en zones
map
)i=(i*256)/(xs*ys)
traiter l'image source dans les zones rectangulaires
C'est le résultat pour la taille de la police = 7px
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]
3x3
à la place.[Edit1] comparaison
Enfin, voici une comparaison entre les deux approches sur la même entrée:
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:
Alors maintenant, je peux même regarder des vidéos dans ASCIIart pour le plaisir. Certains sont vraiment gentils :).
[Edit3]
Si vous voulez essayer d'implémenter ceci dans GLSL regardez ceci: