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:
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 map
est trié, nous pouvons calculer le caractère directement à partir de l'intensité (aucune recherche nécessaire)
character = map[intensity_of(dot)/constant];
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:
- Divisez uniformément l'image en pixels (en niveaux de gris) ou en zones (rectangulaires) point s
- Calculez l'intensité de chaque pixel / zone
- Remplacez-le par un caractère de la carte de caractères avec l'intensité la plus proche
En tant que personnage, map
vous 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
Si i==0
alors le pixel / zone est noir, si i==127
alors le pixel / zone est gris, et si i==255
alors 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
AnsiString
est 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):
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:
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)
Calculez l'intensité de chaque zone ( dot
)
Remplacez-le par un caractère du personnage map
avec 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é:
Diviser la zone de caractère en 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)
.
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
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 AnsiString
par n'importe quel type de chaîne que vous avez, ainsi que Graphics::TBitmap
par 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:
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:
Alors maintenant, je peux même regarder des vidéos en ASCIIart pour le plaisir. Certains sont vraiment sympas :).
Si vous souhaitez essayer d'implémenter cela dans GLSL , jetez un œil à ceci: