Gambar untuk konversi seni ASCII

102

Prolog

Subjek ini muncul di sini di Stack Overflow dari waktu ke waktu, tetapi biasanya dihapus karena pertanyaan yang ditulis dengan buruk. Saya melihat banyak pertanyaan seperti itu dan kemudian diam dari OP (biasanya repetisi rendah) ketika informasi tambahan diminta. Dari waktu ke waktu, jika masukannya cukup baik bagi saya, saya memutuskan untuk menjawab dengan jawaban dan biasanya mendapat beberapa suara positif per hari saat aktif, tetapi kemudian setelah beberapa minggu, pertanyaan itu dihapus / dihapus dan semua dimulai dari awal. Jadi saya memutuskan untuk menulis T&J ini agar saya dapat merujuk pertanyaan semacam itu secara langsung tanpa menulis ulang jawabannya berulang kali…

Alasan lain juga meta thread ini ditargetkan ke saya jadi jika Anda mendapat masukan tambahan, silakan berkomentar.

Pertanyaan

Bagaimana cara mengonversi gambar bitmap ke seni ASCII menggunakan C ++ ?

Beberapa kendala:

  • gambar skala abu-abu
  • menggunakan font spasi tunggal
  • menjaganya tetap sederhana (tidak menggunakan hal-hal yang terlalu canggih untuk pemrogram tingkat pemula)

Ini adalah halaman Wikipedia terkait seni ASCII (terima kasih kepada @RogerRowland).

Di sini labirin serupa dengan Q&A konversi Seni ASCII .

Spektre
sumber
Dengan menggunakan halaman wiki ini sebagai referensi, dapatkah Anda menjelaskan jenis seni ASCII yang Anda maksud? Bagi saya kedengarannya seperti "Konversi gambar ke teks" yang merupakan pencarian "sederhana" dari piksel grayscale ke karakter teks yang sesuai, jadi saya ingin tahu apakah maksud Anda berbeda. Kedengarannya seperti Anda akan menjawabnya sendiri juga .....
Roger Rowland
@RogerRowland sederhana (hanya berbasis intensitas skala abu-abu) dan lebih maju dengan mempertimbangkan juga bentuk karakter (tetapi masih cukup sederhana)
Spektre
1
Meskipun pekerjaan Anda bagus, saya pasti akan menghargai pilihan sampel yang sedikit lebih SFW.
kmote
@TimCastelijns Jika Anda membaca prolog maka Anda dapat melihat ini bukan pertama kalinya jenis jawaban diminta (dan sebagian besar pemilih dari awal di mana akrab dengan beberapa pertanyaan sebelumnya terkait sehingga sisanya hanya memilih yang sesuai), Karena ini Q&A bukan hanya T Saya tidak membuang terlalu banyak waktu dengan bagian Q (yang merupakan kesalahan di pihak saya, saya akui) telah menambahkan beberapa batasan pada pertanyaan jika Anda mendapatkan yang lebih baik, silakan edit.
Spektre

Jawaban:

152

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:

  1. 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 mapdiurutkan maka kita dapat menghitung karakter secara langsung dari intensitas (tidak perlu pencarian)

     character = map[intensity_of(dot)/constant];
  2. 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:

  1. Bagi gambar secara merata menjadi titik piksel (skala abu-abu) atau titik area (persegi panjang) s
  2. Hitung intensitas setiap piksel / area
  3. Gantilah dengan karakter dari peta karakter dengan intensitas terdekat

Sebagai karakter, mapAnda 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

  • map[(255-i)*10/256];

Jika i==0kemudian piksel / area berwarna hitam, jika i==127piksel / area berwarna abu-abu, dan jika i==255piksel / 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
  • AnsiStringadalah 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):

Contoh tingkat lanjut intensitas

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:

  1. 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)

  2. Hitung intensitas setiap area ( dot)

  3. Gantilah dengan karakter dari karakter mapdengan 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:

  1. Bagilah area karakter ke zona

    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).
  2. 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

Contoh pas karakter

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_txtdi 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 AnsiStringdengan tipe string apa pun yang Anda miliki, dan juga Graphics::TBitmapdengan 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 3x3gantinya.

Perbandingan

Terakhir, berikut perbandingan antara dua pendekatan pada input yang sama:

Perbandingan

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:

Contoh grabber ASCIIart

Jadi sekarang saya bahkan dapat menonton video di ASCIIart untuk bersenang-senang. Beberapa sangat bagus :).

Tangan

Jika Anda ingin mencoba menerapkan ini di GLSL , lihat ini:

Spektre
sumber
30
Anda melakukan pekerjaan luar biasa di sini! Terima kasih! Dan saya menyukai sensor ASCII!
Ander Biguri
1
Saran untuk perbaikan: kerjakan turunan arah, bukan hanya intensitas.
Yakk - Adam Nevraumont
1
@Yakk peduli untuk rumit?
tariksbl
2
@tarik cocok tidak hanya pada intensitas, tetapi juga pada turunannya: atau, band pass meningkatkan tepi. Pada dasarnya intensitas bukanlah satu-satunya hal yang dilihat orang: mereka melihat gradian dan tepi.
Yakk - Adam Nevraumont
1
@Yakk, subdivisi zona melakukan hal semacam itu secara tidak langsung. Mungkin lebih baik lagi akan melakukan karakter pegangan sebagai 3x3zona dan membandingkan DCT tetapi itu akan banyak menurunkan kinerja menurut saya.
Spektre