Görüntüden ASCII sanat dönüştürmesine yönelik daha çok yaklaşım vardır ve bunlar çoğunlukla tek aralıklı yazı tiplerini kullanır . Basit olması için sadece temellere bağlıyım:
Piksel / alan yoğunluğuna dayalı (gölgeleme)
Bu yaklaşım, bir piksel alanının her pikselini tek bir nokta olarak ele alır. Buradaki fikir, bu noktanın ortalama gri tonlama yoğunluğunu hesaplamak ve ardından onu hesaplanana yeterince yakın yoğunluğa sahip bir karakterle değiştirmektir. Bunun için, her biri önceden hesaplanmış yoğunluğa sahip bazı kullanılabilir karakterlerin listesine ihtiyacımız var. Buna karakter diyelim map
. Hangi karakterin hangi yoğunluk için en iyi olduğunu daha hızlı seçmenin iki yolu vardır:
Doğrusal olarak dağıtılmış yoğunluk karakter haritası
Bu yüzden sadece aynı adımda yoğunluk farkı olan karakterleri kullanıyoruz. Başka bir deyişle, artan düzende sıralandığında:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Ayrıca karakterimiz map
sıralandığında, karakteri doğrudan yoğunluktan hesaplayabiliriz (arama gerekmez)
character = map[intensity_of(dot)/constant];
Keyfi dağıtılmış yoğunluk karakter haritası
Yani bir dizi kullanılabilir karakterimiz ve bunların yoğunlukları var. So'ya en yakın yoğunluğu bulmalıyız, intensity_of(dot)
eğer 'yi sıralarsak, map[]
ikili aramayı kullanabiliriz, aksi takdirde bir O(n)
arama minimum mesafe döngüsüne veya O(1)
sözlüğe ihtiyacımız var. Bazen basitlik için, karakter map[]
doğrusal olarak dağıtılmış olarak ele alınabilir ve neye bakacağınızı bilmediğiniz sürece genellikle sonuçta görünmeyen hafif bir gama bozulmasına neden olabilir.
Yoğunluğa dayalı dönüştürme, gri ölçekli görüntüler için de harikadır (yalnızca siyah beyaz değil). Noktayı tek bir piksel olarak seçerseniz, sonuç büyür (bir piksel -> tek karakter), bu nedenle daha büyük görüntüler için en boy oranını korumak ve çok fazla büyütmemek için bir alan (yazı tipi boyutunun çarpımı) seçilir.
Nasıl yapılır:
- Görüntüyü (gri ölçekli) piksellere veya (dikdörtgen) alanlara eşit olarak bölün nokta s
- Her pikselin / alanın yoğunluğunu hesaplayın
- En yakın yoğunluğa sahip karakter haritasındaki karakterle değiştirin
Karakter olarak map
herhangi bir karakter kullanabilirsiniz, ancak karakterin karakter alanı boyunca eşit olarak dağılmış pikselleri varsa sonuç daha iyi olur. Yeni başlayanlar için şunları kullanabilirsiniz:
char map[10]=" .,:;ox%#@";
azalan sıralanır ve doğrusal olarak dağıtılmış gibi görünür.
Öyleyse, piksel / alan yoğunluğu ise i = <0-255>
, ikame karakter olacaktır
Eğer i==0
o zaman piksel / alan siyah i==127
sonra piksel / alan gri ise ve i==255
daha sonra piksel / alan beyazdır. İçinde farklı karakterleri deneyebilirsiniz map[]
...
İşte C ++ ve VCL'deki eski bir örnek:
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;
Borland / Embarcadero ortamını kullanmadığınız sürece VCL öğelerini değiştirmeniz / yok saymanız gerekir .
mm_log
metnin çıktısının alındığı not
bmp
girdi bit eşlemi
AnsiString
1'den indekslenmiş bir VCL türü dizedir, 0'dan değil char*
!!!
Sonuç: Biraz NSFW yoğunluğu örnek resmi
Solda ASCII sanat çıktısı (yazı tipi boyutu 5 piksel) ve sağda giriş görüntüsü birkaç kez yakınlaştırılmış . Gördüğünüz gibi, çıktı daha büyük piksel -> karakterdir. Piksel yerine daha geniş alanlar kullanırsanız, yakınlaştırma daha küçük olur, ancak elbette çıktı görsel olarak daha az hoştur. Bu yaklaşım, kodlanması / işlenmesi çok kolay ve hızlıdır.
Aşağıdakiler gibi daha gelişmiş şeyler eklediğinizde:
- otomatik harita hesaplamaları
- otomatik piksel / alan boyutu seçimi
- en boy oranı düzeltmeleri
Daha sonra daha karmaşık görüntüleri daha iyi sonuçlarla işleyebilirsiniz:
Sonuç 1: 1 oranındadır (karakterleri görmek için yakınlaştırın):
Elbette, alan örneklemesi için küçük ayrıntıları kaybedersiniz. Bu, alanlarla örneklenen ilk örnekle aynı boyutta bir görüntüdür:
Biraz NSFW yoğunluğu gelişmiş örnek resim
Gördüğünüz gibi, bu daha büyük görüntüler için daha uygun.
Karakter uydurma (gölgeleme ve katı ASCII sanatı arasında karma)
Bu yaklaşım, alanı (artık tek piksel noktası yok) benzer yoğunluk ve şekle sahip karakterle değiştirmeye çalışır. Bu, önceki yaklaşımla karşılaştırıldığında daha büyük yazı tiplerinde bile daha iyi sonuçlara yol açar. Öte yandan, bu yaklaşım elbette biraz daha yavaştır. Bunu yapmanın daha fazla yolu vardır, ancak ana fikir, görüntü alanı ( dot
) ile oluşturulan karakter arasındaki farkı (mesafeyi) hesaplamaktır . Pikseller arasındaki mutlak farkın saf toplamıyla başlayabilirsiniz, ancak bu çok iyi sonuçlara yol açmayacaktır çünkü tek piksellik bir kayma bile mesafeyi büyütecektir. Bunun yerine korelasyon veya farklı ölçümler kullanabilirsiniz. Genel algoritma, önceki yaklaşımla neredeyse aynıdır:
Yani eşit görüntüyü bölmek (gri ölçekli) dikdörtgen alanlar dot 'ler
ideal olarak, oluşturulmuş yazı tipi karakterleriyle aynı en boy oranıyla (en boy oranını koruyacaktır. Karakterlerin genellikle x ekseninde bir bit üst üste geldiğini unutmayın)
Her alanın yoğunluğunu hesaplayın ( dot
)
map
En yakın yoğunluk / şekle sahip karakterden bir karakterle değiştirin
Bir karakter ve bir nokta arasındaki mesafeyi nasıl hesaplayabiliriz? Bu yaklaşımın en zor kısmı budur. Deney yaparken hız, kalite ve basitlik arasındaki bu uzlaşmayı geliştiriyorum:
Karakter alanını bölgelere böl
- Dönüşüm alfabenizden (
map
) her karakterin sol, sağ, yukarı, aşağı ve orta bölgesi için ayrı bir yoğunluk hesaplayın .
- Tüm yoğunlukları normalleştirin, böylece alan boyutundan bağımsız olurlar
i=(i*256)/(xs*ys)
.
Kaynak görüntüyü dikdörtgen alanlarda işleyin
- (hedef yazı tipiyle aynı en boy oranına sahip)
- Her alan için yoğunluğu 1 numaralı madde işaretiyle aynı şekilde hesaplayın
- Dönüşüm alfabesindeki yoğunluklardan en yakın eşleşmeyi bulun
- Yerleştirilen karakteri çıktılar
Bu, yazı tipi boyutu = 7 piksel sonucudur
Gördüğünüz gibi, daha büyük bir yazı tipi boyutu kullanıldığında bile çıktı görsel olarak hoştur (önceki yaklaşım örneği 5 piksel yazı tipi boyutundaydı). Çıktı, giriş görüntüsüyle kabaca aynı boyuttadır (yakınlaştırma yok). Daha iyi sonuçlar elde edilir, çünkü karakterler yalnızca yoğunluk açısından değil, aynı zamanda genel şekil açısından da orijinal görüntüye daha yakındır ve bu nedenle daha büyük yazı tiplerini kullanabilir ve yine de ayrıntıları koruyabilirsiniz (tabii ki bir noktaya kadar).
İşte VCL tabanlı dönüştürme uygulaması için tam kod:
//---------------------------------------------------------------------------
#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();
}
//---------------------------------------------------------------------------
İçinde Form1
tek TMemo mm_txt
bulunan bir form uygulamasıdır ( ) . Bir görüntü yükler "pic.bmp"
ve ardından çözünürlüğe göre, "pic.txt"
görselleştirmek için nota kaydedilen ve nota gönderilen metne dönüştürmek için hangi yaklaşımın kullanılacağını seçer .
VCL'si olmayanlar için, VCL öğelerini yok sayın ve AnsiString
sahip olduğunuz herhangi bir dize türüyle ve ayrıca Graphics::TBitmap
piksel erişim özelliğine sahip elinizde bulunan herhangi bir bitmap veya görüntü sınıfıyla değiştirin.
Çok önemli bir not, bunun ayarlarını kullanmasıdır mm_txt->Font
, bu nedenle şunları ayarladığınızdan emin olun:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
bunun düzgün çalışmasını sağlamak için, aksi takdirde yazı tipi tek aralıklı olarak ele alınmayacaktır. Fare tekerleği, sonuçları farklı yazı tipi boyutlarında görmek için yazı tipi boyutunu yukarı / aşağı değiştirir.
[Notlar]
- Word Portreleri görselleştirmesine bakın
- Bitmap / dosya erişimi ve metin çıktı yeteneklerine sahip bir dil kullanın
- Çok kolay anlaşılır ve basit olduğu için ilk yaklaşımla başlamanızı ve ancak o zaman ikinciye geçmenizi şiddetle tavsiye ederim (bu, ilkinin değiştirilmesi olarak yapılabilir, böylece kodun çoğu olduğu gibi kalır)
- Tersine çevrilmiş yoğunlukta (siyah pikseller maksimum değerdir) hesaplama yapmak iyi bir fikirdir çünkü standart metin önizlemesi beyaz bir arka plan üzerindedir ve bu nedenle çok daha iyi sonuçlar sağlar.
- Alt bölüm bölgelerinin boyutu, sayısı ve düzenini deneyebilir veya
3x3
bunun yerine bazı ızgaraları kullanabilirsiniz .
Karşılaştırma
Son olarak, aynı girdiye ilişkin iki yaklaşım arasında bir karşılaştırma:
Yeşil nokta işaretli resimler , tümü altı piksel yazı tipi boyutunda olmak üzere , yaklaşım # 2 ve kırmızı olanlar # 1 ile yapılır . Ampul görüntüsünde görebileceğiniz gibi, şekle duyarlı yaklaşım çok daha iyidir ( # 1 2x yakınlaştırılmış kaynak görüntüde yapılsa bile ).
Harika uygulama
Bugünün yeni sorularını okurken, masaüstünün seçilen bir bölgesini yakalayan ve onu sürekli olarak ASCIIart dönüştürücüsüne besleyen ve sonucu görüntüleyen harika bir uygulama fikrimi aldım . Bir saatlik kodlamadan sonra, bitti ve sonuçtan o kadar memnunum ki, onu buraya eklemem gerekiyor.
Tamam uygulama sadece iki pencereden oluşuyor. İlk ana pencere, temelde görüntü seçimi ve önizlemesi olmayan eski dönüştürücü penceremdir (yukarıdaki tüm şeyler içindedir). Yalnızca ASCII önizleme ve dönüştürme ayarlarına sahiptir. İkinci pencere, kapma alanı seçimi için içi şeffaf olan boş bir formdur (herhangi bir işlevsellik yoktur).
Şimdi bir zamanlayıcıda, sadece seçilen alanı seçim formundan alıyorum , dönüşüme geçiriyorum ve ASCIIart'ı önizliyorum .
Böylece dönüştürmek istediğiniz alanı seçim penceresi ile çevreliyor ve sonucu ana pencerede görüntüleyebiliyorsunuz. Bu bir oyun, izleyici vb. Olabilir. Şuna benzer:
Artık eğlenmek için ASCIIart'taki videoları bile izleyebiliyorum . Bazıları gerçekten güzel :).
Bunu GLSL'de uygulamayı denemek istiyorsanız , şuna bir göz atın: