Esistono più approcci per la conversione da immagine a arte ASCII che si basano principalmente sull'uso di caratteri a spaziatura singola . Per semplicità, mi attengo solo alle basi:
Basato su intensità di pixel / area (ombreggiatura)
Questo approccio gestisce ogni pixel di un'area di pixel come un singolo punto. L'idea è di calcolare l'intensità media della scala di grigi di questo punto e quindi sostituirla con un carattere con un'intensità abbastanza vicina a quella calcolata. Per questo abbiamo bisogno di un elenco di caratteri utilizzabili, ciascuno con un'intensità precalcolata. Chiamiamolo un personaggio map
. Per scegliere più rapidamente quale personaggio è il migliore per quale intensità, ci sono due modi:
Mappa dei caratteri di intensità distribuita linearmente
Quindi usiamo solo caratteri che hanno una differenza di intensità con lo stesso passo. In altre parole, se ordinato in ordine crescente:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Inoltre, quando il nostro personaggio map
è ordinato, possiamo calcolare il carattere direttamente dall'intensità (nessuna ricerca necessaria)
character = map[intensity_of(dot)/constant];
Mappa dei caratteri a intensità distribuita arbitraria
Quindi abbiamo una serie di caratteri utilizzabili e le loro intensità. Dobbiamo trovare di nuovo l'intensità più vicina a intensity_of(dot)
So, se ordiniamo il map[]
, possiamo usare la ricerca binaria, altrimenti abbiamo bisogno di un O(n)
loop o di un O(1)
dizionario di distanza minima di ricerca . A volte per semplicità, il personaggio map[]
può essere gestito come distribuito linearmente, provocando una leggera distorsione gamma, solitamente invisibile nel risultato a meno che non si sappia cosa cercare.
La conversione basata sull'intensità è ottima anche per le immagini in scala di grigi (non solo in bianco e nero). Se selezioni il punto come un singolo pixel, il risultato diventa grande (un pixel -> singolo carattere), quindi per immagini più grandi viene selezionata invece un'area (moltiplicare la dimensione del carattere) per preservare le proporzioni e non ingrandire troppo.
Come farlo:
- Uniformemente dividere l'immagine in pixel (scala di grigi) o (rettangolari) aree puntino s
- Calcola l'intensità di ogni pixel / area
- Sostituiscilo con il carattere della mappa dei caratteri con l'intensità più vicina
Come personaggio map
puoi usare qualsiasi carattere, ma il risultato migliora se il personaggio ha pixel distribuiti uniformemente lungo l'area del carattere. Per cominciare puoi usare:
char map[10]=" .,:;ox%#@";
ordinati discendenti e fingono di essere distribuiti linearmente.
Quindi, se l'intensità del pixel / area è i = <0-255>
il carattere sostitutivo sarà
Se i==0
il pixel / area è nero, se i==127
il pixel / area è grigio e se i==255
il pixel / area è bianco. Puoi sperimentare con diversi personaggi all'interno map[]
...
Ecco un mio antico esempio in C ++ e 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;
È necessario sostituire / ignorare le cose VCL a meno che non si utilizzi l' ambiente Borland / Embarcadero .
mm_log
è il promemoria in cui viene emesso il testo
bmp
è la bitmap di input
AnsiString
è una stringa di tipo VCL indicizzata da 1, non da 0 come char*
!!!
Questo è il risultato: immagine di esempio di intensità leggermente NSFW
A sinistra c'è l'output di arte ASCII (dimensione del carattere 5 pixel) e l'immagine in ingresso a destra è stata ingrandita alcune volte. Come puoi vedere, l'output è pixel più grande -> carattere. Se usi aree più grandi invece di pixel, lo zoom è più piccolo, ma ovviamente l'output è visivamente meno piacevole. Questo approccio è molto facile e veloce da codificare / elaborare.
Quando aggiungi cose più avanzate come:
- calcoli di mappe automatizzati
- selezione automatica delle dimensioni di pixel / area
- correzioni delle proporzioni
Quindi puoi elaborare immagini più complesse con risultati migliori:
Ecco il risultato in un rapporto 1: 1 (ingrandisci per vedere i caratteri):
Naturalmente, per il campionamento dell'area si perdono i piccoli dettagli. Questa è un'immagine delle stesse dimensioni del primo esempio campionato con aree:
Immagine di esempio avanzata di intensità leggermente NSFW
Come puoi vedere, questo è più adatto per immagini più grandi.
Adattamento dei caratteri (ibrido tra ombreggiatura e arte ASCII solida)
Questo approccio cerca di sostituire l'area (non più punti di un singolo pixel) con caratteri con intensità e forma simili. Ciò porta a risultati migliori, anche con caratteri più grandi utilizzati rispetto all'approccio precedente. D'altra parte, questo approccio è ovviamente un po 'più lento. Ci sono più modi per farlo, ma l'idea principale è calcolare la differenza (distanza) tra l'area dell'immagine ( dot
) e il carattere renderizzato. Puoi iniziare con la somma ingenua della differenza assoluta tra i pixel, ma ciò porterà a risultati non molto buoni perché anche uno spostamento di un pixel aumenterà la distanza. Invece puoi usare la correlazione o metriche diverse. L'algoritmo generale è quasi lo stesso dell'approccio precedente:
Così in modo uniforme dividere l'immagine (in scala di grigi) aree rettangolari dot s'
idealmente con le stesse proporzioni dei caratteri dei font renderizzati (manterrà le proporzioni. Non dimenticare che i caratteri di solito si sovrappongono leggermente sull'asse x)
Calcola l'intensità di ogni area ( dot
)
Sostituiscilo con un carattere del personaggio map
con l'intensità / forma più vicina
Come possiamo calcolare la distanza tra un carattere e un punto? Questa è la parte più difficile di questo approccio. Durante la sperimentazione, sviluppo questo compromesso tra velocità, qualità e semplicità:
Dividi l'area del personaggio in zone
- Calcola un'intensità separata per la zona sinistra, destra, su, giù e centrale di ogni carattere dall'alfabeto di conversione (
map
).
- Normalizzare tutte le intensità, in modo che siano indipendenti dalla dimensione dell'area,
i=(i*256)/(xs*ys)
.
Elabora l'immagine sorgente in aree rettangolari
- (con le stesse proporzioni del carattere di destinazione)
- Per ogni area, calcola l'intensità nello stesso modo del punto 1
- Trova la corrispondenza più vicina dalle intensità nell'alfabeto di conversione
- Emetti il carattere adattato
Questo è il risultato per la dimensione del carattere = 7 pixel
Come puoi vedere, l'output è visivamente piacevole, anche con una dimensione del carattere più grande utilizzata (l'esempio di approccio precedente era con una dimensione del carattere di 5 pixel). L'output ha all'incirca le stesse dimensioni dell'immagine in ingresso (senza zoom). I risultati migliori si ottengono perché i caratteri sono più vicini all'immagine originale, non solo per intensità, ma anche per forma complessiva, e quindi puoi usare caratteri più grandi e conservare comunque i dettagli (fino a un certo punto ovviamente).
Ecco il codice completo per l'applicazione di conversione basata su 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();
}
//---------------------------------------------------------------------------
È semplice un modulo application ( Form1
) con un singolo TMemo mm_txt
in esso. Carica un'immagine, "pic.bmp"
quindi, in base alla risoluzione, sceglie quale approccio utilizzare per convertire in testo che viene salvato "pic.txt"
e inviato al promemoria per la visualizzazione.
Per quelli senza VCL, ignora le cose VCL e sostituiscile AnsiString
con qualsiasi tipo di stringa che hai, e anche Graphics::TBitmap
con qualsiasi bitmap o classe di immagine che hai a disposizione con capacità di accesso ai pixel.
Una nota molto importante è che questo utilizza le impostazioni di mm_txt->Font
, quindi assicurati di impostare:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
per farlo funzionare correttamente, altrimenti il carattere non verrà gestito come mono-spaziatura. La rotellina del mouse cambia solo la dimensione del carattere su / giù per vedere i risultati su diverse dimensioni dei caratteri.
[Appunti]
- Vedere la visualizzazione dei ritratti di parole
- Utilizzare una lingua con accesso a bitmap / file e funzionalità di output di testo
- Consiglio vivamente di iniziare con il primo approccio in quanto è molto facile, diretto e semplice, e solo dopo passare al secondo (che può essere fatto come modifica del primo, quindi la maggior parte del codice rimane comunque così com'è)
- È una buona idea calcolare con intensità invertita (pixel neri è il valore massimo) perché l'anteprima del testo standard è su uno sfondo bianco, quindi porta a risultati molto migliori.
- puoi sperimentare con le dimensioni, il conteggio e il layout delle zone di suddivisione o utilizzare una griglia simile
3x3
.
Confronto
Infine ecco un confronto tra i due approcci sullo stesso input:
Le immagini contrassegnate dal punto verde sono realizzate con l'approccio n. 2 e quelle rosse con il n. 1 , tutte su una dimensione del carattere di sei pixel. Come puoi vedere sull'immagine della lampadina, l'approccio sensibile alla forma è molto migliore (anche se il n. 1 viene eseguito su un'immagine sorgente ingrandita 2x).
Bella applicazione
Durante la lettura delle nuove domande di oggi, ho avuto l'idea di una fantastica applicazione che cattura una regione selezionata del desktop e la invia continuamente al convertitore ASCIIart e visualizza il risultato. Dopo un'ora di codifica, è fatto e sono così soddisfatto del risultato che devo semplicemente aggiungerlo qui.
OK l'applicazione è composta da due sole finestre. La prima finestra principale è fondamentalmente la mia vecchia finestra di conversione senza la selezione e l'anteprima dell'immagine (tutte le cose sopra sono dentro). Ha solo l'anteprima ASCII e le impostazioni di conversione. La seconda finestra è un modulo vuoto con interno trasparente per la selezione dell'area di cattura (nessuna funzionalità di sorta).
Ora su un timer, prendo semplicemente l'area selezionata dal modulo di selezione, la passo alla conversione e visualizzo l'anteprima di ASCIIart .
Quindi racchiudi un'area che desideri convertire dalla finestra di selezione e visualizzi il risultato nella finestra principale. Può essere un gioco, un visualizzatore e così via. Assomiglia a questo:
Così ora posso guardare anche i video in ASCIIart per divertimento. Alcuni sono davvero carini :).
Se vuoi provare a implementarlo in GLSL , dai un'occhiata a questo: