Ada lebih banyak pendekatan untuk konversi gambar ke seni ASCII yang sebagian besar didasarkan pada penggunaan font spasi tunggal . Untuk kesederhanaan, saya hanya berpegang pada dasar:
Berbasis piksel / intensitas area (bayangan)
Pendekatan ini menangani setiap piksel dari suatu area piksel sebagai satu titik. Idenya adalah untuk menghitung intensitas skala abu-abu rata-rata dari titik ini dan kemudian menggantinya dengan karakter dengan intensitas yang cukup dekat dengan yang dihitung. Untuk itu kita membutuhkan beberapa daftar karakter yang dapat digunakan, masing-masing dengan intensitas yang telah dihitung sebelumnya. Sebut saja itu karaktermap
. Untuk lebih cepat memilih karakter mana yang terbaik untuk intensitas mana, ada dua cara:
Peta karakter intensitas terdistribusi secara linier
Jadi kami hanya menggunakan karakter yang memiliki perbedaan intensitas dengan langkah yang sama. Dengan kata lain, jika diurutkan secara ascending maka:
intensity_of(map[i])=intensity_of(map[i-1])+constant;
Juga ketika karakter kita map
diurutkan maka kita dapat menghitung karakter secara langsung dari intensitas (tidak perlu pencarian)
character = map[intensity_of(dot)/constant];
Peta karakter intensitas terdistribusi sewenang-wenang
Jadi kami memiliki berbagai karakter yang dapat digunakan dan intensitasnya. Kita perlu menemukan intensitas yang paling dekat dengan intensity_of(dot)
Jadi jika kita mengurutkan map[]
, kita dapat menggunakan pencarian biner, jika tidak kita memerlukan O(n)
pencarian loop jarak minimum atau O(1)
kamus. Kadang-kadang untuk kesederhanaan, karakter map[]
dapat ditangani sebagai terdistribusi linier, menyebabkan sedikit distorsi gamma, biasanya tidak terlihat dalam hasil kecuali Anda tahu apa yang harus dicari.
Konversi berbasis intensitas juga bagus untuk gambar skala abu-abu (tidak hanya hitam dan putih). Jika Anda memilih titik sebagai piksel tunggal, hasilnya menjadi besar (satu piksel -> karakter tunggal), jadi untuk gambar yang lebih besar, sebuah area (perkalian ukuran font) dipilih sebagai gantinya untuk mempertahankan rasio aspek dan tidak memperbesar terlalu banyak.
Bagaimana cara melakukannya:
- Bagi gambar secara merata menjadi titik piksel (skala abu-abu) atau titik area (persegi panjang) s
- Hitung intensitas setiap piksel / area
- Gantilah dengan karakter dari peta karakter dengan intensitas terdekat
Sebagai karakter, map
Anda dapat menggunakan karakter apa saja, tetapi hasilnya akan lebih baik jika karakter memiliki piksel yang tersebar secara merata di sepanjang area karakter. Sebagai permulaan, Anda dapat menggunakan:
char map[10]=" .,:;ox%#@";
diurutkan menurun dan berpura-pura terdistribusi linier.
Jadi jika intensitas piksel / area sesuai i = <0-255>
maka karakter pengganti akan terbentuk
Jika i==0
kemudian piksel / area berwarna hitam, jika i==127
piksel / area berwarna abu-abu, dan jika i==255
piksel / area berwarna putih. Anda dapat bereksperimen dengan berbagai karakter di dalam map[]
...
Berikut adalah contoh kuno saya di C ++ dan 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;
Anda perlu mengganti / mengabaikan hal-hal VCL kecuali Anda menggunakan lingkungan Borland / Embarcadero .
mm_log
adalah memo tempat teks dikeluarkan
bmp
adalah bitmap masukan
AnsiString
adalah jenis string VCL yang diindeks dari 1, bukan dari 0 sebagai char*
!!!
Hasilnya: Gambar contoh intensitas NSFW sedikit
Di sebelah kiri adalah keluaran seni ASCII (ukuran font 5 piksel), dan di sebelah kanan gambar masukan diperbesar beberapa kali. Seperti yang Anda lihat, hasilnya adalah piksel yang lebih besar -> karakter. Jika Anda menggunakan area yang lebih besar daripada piksel maka zoomnya lebih kecil, tetapi tentu saja outputnya kurang menyenangkan secara visual.Pendekatan ini sangat mudah dan cepat untuk kode / proses.
Saat Anda menambahkan hal-hal yang lebih canggih seperti:
- perhitungan peta otomatis
- pemilihan ukuran piksel / area otomatis
- koreksi aspek rasio
Kemudian Anda dapat memproses gambar yang lebih kompleks dengan hasil yang lebih baik:
Berikut adalah hasil perbandingan 1: 1 (perbesar untuk melihat karakter):
Tentu saja, untuk pengambilan sampel area Anda kehilangan detail-detail kecil. Ini adalah gambar dengan ukuran yang sama seperti contoh pertama yang diambil sampelnya dengan area:
Gambar contoh tingkat lanjut intensitas NSFW sedikit
Seperti yang Anda lihat, ini lebih cocok untuk gambar yang lebih besar.
Pemasangan karakter (gabungan antara seni ASCII yang teduh dan padat)
Pendekatan ini mencoba untuk mengganti area (tidak ada lagi titik piksel tunggal) dengan karakter dengan intensitas dan bentuk yang serupa. Ini mengarah pada hasil yang lebih baik, bahkan dengan font yang lebih besar yang digunakan dibandingkan dengan pendekatan sebelumnya. Di sisi lain, pendekatan ini tentu saja sedikit lebih lambat. Ada lebih banyak cara untuk melakukan ini, tetapi ide utamanya adalah menghitung perbedaan (jarak) antara area gambar ( dot
) dan karakter yang diberikan. Anda dapat memulai dengan jumlah naif dari perbedaan absolut antar piksel, tetapi itu tidak akan memberikan hasil yang sangat baik karena bahkan pergeseran satu piksel akan membuat jarak menjadi besar. Sebagai gantinya, Anda dapat menggunakan korelasi atau metrik yang berbeda. Algoritme keseluruhan hampir sama dengan pendekatan sebelumnya:
Jadi bagi gambar secara merata ke titik area persegi panjang (skala abu-abu) 's
idealnya dengan rasio aspek yang sama seperti karakter font yang dirender (ini akan mempertahankan rasio aspek. Jangan lupa bahwa karakter biasanya sedikit tumpang tindih pada sumbu x)
Hitung intensitas setiap area ( dot
)
Gantilah dengan karakter dari karakter map
dengan intensitas / bentuk terdekat
Bagaimana kita menghitung jarak antara karakter dan titik? Itu adalah bagian tersulit dari pendekatan ini. Saat bereksperimen, saya mengembangkan kompromi antara kecepatan, kualitas, dan kesederhanaan:
Bagilah area karakter ke zona
- Hitung intensitas terpisah untuk zona kiri, kanan, atas, bawah, dan tengah setiap karakter dari alfabet konversi Anda (
map
).
- Normalisasikan semua intensitas, sehingga tidak bergantung pada luas area ,
i=(i*256)/(xs*ys)
.
Proses gambar sumber di area persegi panjang
- (dengan rasio aspek yang sama dengan font target)
- Untuk setiap area, hitung intensitas dengan cara yang sama seperti pada butir # 1
- Temukan kecocokan terdekat dari intensitas dalam alfabet konversi
- Keluarkan karakter yang dipasang
Ini adalah hasil untuk ukuran font = 7 piksel
Seperti yang Anda lihat, outputnya secara visual menyenangkan, bahkan dengan ukuran font yang lebih besar yang digunakan (contoh pendekatan sebelumnya adalah dengan ukuran font 5 piksel). Outputnya kira-kira berukuran sama dengan gambar input (tanpa zoom). Hasil yang lebih baik dicapai karena karakter lebih mendekati gambar asli, tidak hanya berdasarkan intensitas, tetapi juga bentuk keseluruhan, dan oleh karena itu Anda dapat menggunakan font yang lebih besar dan tetap mempertahankan detail (hingga satu titik tentunya).
Berikut kode lengkap untuk aplikasi konversi berbasis 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();
}
//---------------------------------------------------------------------------
Ini adalah aplikasi formulir ( Form1
) sederhana dengan satu TMemo mm_txt
di dalamnya. Ini memuat gambar, "pic.bmp"
dan kemudian sesuai dengan resolusinya, pilih pendekatan mana yang akan digunakan untuk mengubah ke teks yang disimpan "pic.txt"
dan dikirim ke memo untuk divisualisasikan.
Bagi mereka yang tidak memiliki VCL, abaikan VCL dan ganti AnsiString
dengan tipe string apa pun yang Anda miliki, dan juga Graphics::TBitmap
dengan bitmap atau kelas gambar yang Anda miliki dengan kemampuan akses piksel.
Catatan yang sangat penting adalah bahwa ini menggunakan pengaturan mm_txt->Font
, jadi pastikan Anda mengatur:
Font->Pitch = fpFixed
Font->Charset = OEM_CHARSET
Font->Name = "System"
untuk membuat ini berfungsi dengan baik, jika tidak font tidak akan ditangani sebagai spasi tunggal. Roda mouse hanya mengubah ukuran font naik / turun untuk melihat hasil pada ukuran font yang berbeda.
[Catatan]
- Lihat visualisasi Word Portraits
- Gunakan bahasa dengan akses bitmap / file dan kemampuan keluaran teks
- Saya sangat menyarankan untuk memulai dengan pendekatan pertama karena sangat mudah langsung dan sederhana, dan baru kemudian pindah ke yang kedua (yang dapat dilakukan sebagai modifikasi dari yang pertama, jadi sebagian besar kode tetap seperti apa adanya)
- Merupakan ide yang baik untuk menghitung dengan intensitas terbalik (piksel hitam adalah nilai maksimum) karena pratinjau teks standar berada pada latar belakang putih, sehingga memberikan hasil yang jauh lebih baik.
- Anda dapat bereksperimen dengan ukuran, jumlah, dan tata letak zona subdivisi atau menggunakan beberapa kisi sebagai
3x3
gantinya.
Perbandingan
Terakhir, berikut perbandingan antara dua pendekatan pada input yang sama:
Gambar bertanda titik hijau dilakukan dengan pendekatan # 2 dan yang merah dengan # 1 , semuanya dalam ukuran font enam piksel. Seperti yang dapat Anda lihat pada gambar bola lampu, pendekatan peka bentuk jauh lebih baik (meskipun # 1 dilakukan pada gambar sumber yang diperbesar 2x).
Aplikasi keren
Saat membaca pertanyaan baru hari ini, saya mendapat ide tentang aplikasi keren yang mengambil wilayah desktop yang dipilih dan terus-menerus memasukkannya ke konverter ASCIIart dan melihat hasilnya. Setelah satu jam pengkodean, selesai dan saya sangat puas dengan hasilnya sehingga saya harus menambahkannya di sini.
OK aplikasinya hanya terdiri dari dua jendela. Jendela master pertama pada dasarnya adalah jendela konverter lama saya tanpa pemilihan dan pratinjau gambar (semua hal di atas ada di dalamnya). Ini hanya memiliki pratinjau ASCII dan pengaturan konversi. Jendela kedua adalah formulir kosong dengan bagian dalam transparan untuk pemilihan area pengambilan (tidak ada fungsi apa pun).
Sekarang pada pengatur waktu, saya hanya mengambil area yang dipilih dengan formulir pilihan, meneruskannya ke konversi, dan melihat pratinjau ASCIIart .
Jadi Anda menyertakan area yang ingin Anda ubah dengan jendela pemilihan dan melihat hasilnya di jendela master. Ini bisa menjadi permainan, penampil, dll. Tampilannya seperti ini:
Jadi sekarang saya bahkan dapat menonton video di ASCIIart untuk bersenang-senang. Beberapa sangat bagus :).
Jika Anda ingin mencoba menerapkan ini di GLSL , lihat ini: