c++ - صورة لتحويل الفن ASCII



image-processing bitmap (1)

هناك المزيد من الأساليب لتحويل الصور إلى ASCII ، والتي تعتمد في الغالب على استخدام الخطوط أحادية التباعد للبساطة التي ألصقها بالأساسيات فقط:

بكسل / منطقة كثافة القائمة (تظليل)

يعالج هذا النهج كل بكسل من مساحة البكسل كنقطة واحدة. تتمثل الفكرة في حساب متوسط ​​شدة المقياس الرمادي لهذه النقطة ثم استبدالها بحرف ذي كثافة قريبة بدرجة كافية من النقطة المحسوبة. لذلك نحن بحاجة إلى قائمة من الشخصيات القابلة للاستخدام مع كل كثافة precomputed دعونا نسميها map شخصية. للاختيار بشكل أسرع أي شخصية هي الأفضل التي توجد بها شدة طريقتان:

  1. موزعة خطيا خريطة شخصية شدة

    لذلك نحن نستخدم فقط الأحرف التي لها فرق شدة مع نفس الخطوة. بعبارة أخرى عند الفرز تصاعدياً:

    intensity_of(map[i])=intensity_of(map[i-1])+constant;

    أيضًا عندما يتم فرز map الشخصيات الخاصة بنا ، يمكننا حينئذٍ حساب الشخصية مباشرةً من الكثافة (لا يلزم البحث)

    character=map[intensity_of(dot)/constant];
  2. التعسفي الموزعة خريطة شخصية شدة

    لذلك لدينا مجموعة من الشخصيات القابلة للاستخدام وكثافتها. نحتاج إلى العثور على الكثافة الأقرب إلى intensity_of(dot) لذا مرة أخرى إذا قمنا بفرز map[] يمكننا استخدام البحث الثنائي وإلا فإننا نحتاج إلى البحث عن O(n) حلقة البحث عن مسافة دقيقة أو O(1) . أحيانًا للبساطة ، يمكن التعامل مع map[] الأحرف map[] كتوزيعها خطيًا مما يؤدي إلى حدوث تشوه طفيف في غاما غير مرئي عادة في النتيجة إلا إذا كنت تعرف ما الذي تبحث عنه.

يعد التحويل المستند إلى الكثافة مفيدًا أيضًا للصور ذات المقياس الرمادي (وليس فقط بالأبيض والأسود). إذا قمت بتحديد النقطة على هيئة بكسل واحد ، فستكون النتيجة كبيرة (1 بكسل -> حرف مفرد) ، لذلك بالنسبة للصور الكبيرة ، يتم تحديد منطقة (مضاعفة حجم الخط) بدلاً من ذلك للحفاظ على نسبة العرض إلى الارتفاع وعدم تكبيرها كثيرًا.

كيف افعلها:

  1. لذا قسّم الصورة بالتساوي إلى وحدات بكسل (بمقياس رمادي) أو مناطق (مستطيلة)
  2. حساب شدة كل بكسل / منطقة
  3. استبدلها بحرف من خريطة الشخصيات بأقرب كثافة

map للأحرف ، يمكنك استخدام أي أحرف ، لكن النتيجة ستتحسن إذا كانت الشخصية قد تم توزيع وحدات البكسل بالتساوي على طول منطقة الأحرف. بالنسبة للمبتدئين ، يمكنك استخدام:

  • char map[10]=" .,:;ox%#@";

مرتبة تنازليًا وتظاهر بأنها موزعة خطيًا.

لذلك إذا كانت شدة البكسل / المساحة هي i = <0-255> فسيكون حرف الاستبدال

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

إذا كانت i==0 فإن البيكسل / المساحة باللون الأسود ، وإذا كان i==127 فإن البيكسل / المساحة باللون الرمادي وإذا كان i==255 فإن البيكسل / المساحة باللون الأبيض. يمكنك تجربة شخصيات مختلفة داخل map[] ...

هنا مثال قديم لي في C ++ و 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;

تحتاج إلى استبدال / تجاهل عناصر VCL إلا إذا كنت تستخدم بيئة Borland / Embarcadero

  • mm_log عبارة عن مذكرة يتم فيها mm_log النص
  • bmp هو صورة نقطية الإدخال
  • AnsiString عبارة عن سلسلة من نوع VCL مفهرسة 1 وليس من 0 كـ char* !!!

هذه هي النتيجة: مثال لشدة NSFW قليلاً

على اليسار يوجد مخرج فن ASCII (حجم الخط 5px) ، وعلى الصورة المدخلة اليمنى تم التكبير عدة مرات. كما ترون الإخراج أكبر بكسل -> حرف. إذا كنت تستخدم مساحات أكبر بدلاً من وحدات البكسل ، فسيكون حجم الزوم أصغر ولكن بالطبع يكون الإخراج أقل إرضاءًا. هذا النهج سهل جدا وسريع لرمز / عملية.

عند إضافة المزيد من الأشياء المتقدمة مثل:

  • حسابات الخريطة الآلية
  • اختيار حجم بكسل / مساحة تلقائية
  • تصحيح نسبة العرض إلى الارتفاع

بعد ذلك يمكنك معالجة الصور الأكثر تعقيدًا بنتائج أفضل:

هنا ينتج نسبة 1: 1 (تكبير لرؤية الأحرف):

بالطبع لأخذ عينات المنطقة تفقد التفاصيل الصغيرة. هذه صورة بنفس حجم المثال الأول الذي تم أخذ عينات منه بمساحات:

قليلا NSFW شدة صورة المثال المتقدمة

كما ترون هذا هو أكثر ملاءمة للصور أكبر

تركيب الحروف (هجين بين تظليل و ASCII الفن الصلبة)

يحاول هذا النهج استبدال المساحة (لا مزيد من نقاط البكسل المفردة) بحرف بنفس الكثافة والشكل. هذا يؤدي إلى نتائج أفضل حتى مع الخطوط الأكبر المستخدمة مقارنة مع النهج السابق من ناحية أخرى هذا النهج أبطأ قليلاً بالطبع. هناك المزيد من الطرق للقيام بذلك ولكن الفكرة الرئيسية هي حساب الفرق (المسافة) بين مساحة الصورة ( dot ) والحرف المقدم. يمكنك أن تبدأ بمجموع الفرق الساذج بين القيمة المطلقة (البكسل) ، لكن ذلك سيؤدي إلى نتائج غير جيدة للغاية ، لأنه حتى التحول بمقدار 1 بكسل سيجعل المسافة كبيرة ، بدلاً من ذلك يمكنك استخدام الارتباط أو مقاييس مختلفة. الخوارزمية الإجمالية هي نفسها تقريبًا مثل النهج السابق:

  1. لذا قسّم الصورة بالتساوي إلى مناطق مستطيلة (رمادية)
    • من الناحية المثالية بنفس نسبة العرض إلى الارتفاع كأحرف الخطوط المقدمة (ستحافظ على نسبة العرض إلى الارتفاع ، لا تنس أن الأحرف عادةً ما تتداخل قليلاً في محور x)
  2. حساب شدة كل منطقة ( dot )
  3. استبدلها بحرف من map الحروف بأقرب كثافة / شكل

كيفية حساب المسافة بين الشخصية ونقطة؟ هذا هو الجزء الأصعب من هذا النهج. أثناء إجراء التجربة ، أقوم بتطوير هذه التسوية بين السرعة والجودة والبساطة:

  1. اقسم مساحة الحرف على المناطق

    • حساب كثافة منفصلة لليسار واليمين وأعلى ولأسفل والمنطقة المركزية لكل حرف من الأبجدية التحويلية ( map )
    • قم بتطبيع كل الشدة بحيث تكون مستقلة في حجم المساحة i=(i*256)/(xs*ys)
  2. صورة مصدر العملية في مناطق المستطيل

    • (مع نفس نسبة العرض إلى الارتفاع كالخط المستهدف)
    • لكل منطقة حساب شدة بنفس الطريقة كما في رصاصة 1
    • العثور على أقرب مباراة من شدة في تحويل الأبجدية
    • شخصية الانتاج المجهزة

هذا هو نتيجة لحجم الخط = 7px

كما ترى ، فإن المخرج مرتاح بشكل مرئي حتى مع استخدام حجم الخط الأكبر (مثال النهج السابق كان بحجم خط 5px). الإخراج هو تقريبا نفس حجم صورة الإدخال (لا يوجد تكبير). يتم تحقيق نتائج أفضل لأن الشخصيات أقرب إلى الصورة الأصلية ليس فقط من حيث الشدة ولكن أيضًا من خلال الشكل العام وبالتالي يمكنك استخدام خطوط أكبر مع الاحتفاظ بالتفاصيل (بحد أقصى نقطة).

أكمل هنا الشفرة لتطبيق التحويل المستند إلى 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) // charcter 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;     // temp 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 init buffers
        tmp=new Graphics::TBitmap; if (tmp==NULL) break;
            // allow 32bit 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 image by zoomed character size step
        xf-=xf/3; // characters are usually overlaping 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 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();
    }
//---------------------------------------------------------------------------

إنه تطبيق نموذج بسيط ( Form1 ) مع TMemo mm_txt واحد فيه. يقوم بتحميل الصورة "pic.bmp" ، ثم وفقًا للقرار ، اختر "pic.bmp" التي يجب استخدامها لتحويل النص الذي يتم حفظه إلى "pic.txt" وإرساله إلى المذكرة لتصوره. بالنسبة لأولئك الذين ليس لديهم VCL ، يتجاهلون عناصر VCL AnsiString بأي نوع من السلاسل لديك ، وأيضًا Graphics::TBitmap مع أي صورة نقطية أو فئة صورة لديك تحت تصرف إمكانية الوصول إلى البكسل.

ملاحظة مهمة للغاية هي أن هذا يستخدم إعدادات mm_txt->Font لذا تأكد من ضبط:

  • Font->Pitch=fpFixed
  • Font->Charset=OEM_CHARSET
  • Font->Name="System"

لجعل هذا العمل بشكل صحيح وإلا لن يتم التعامل مع الخط كما أحادية متباعدة. عجلة الماوس فقط يغير حجم الخط لأعلى / لأسفل لرؤية النتائج على أحجام الخطوط المختلفة

[ملاحظات]

  • رؤية صور Word التصور
  • استخدام اللغة مع وصول الصورة النقطية / الملف وإمكانيات إخراج النص
  • نوصي بشدة بالبدء في النهج الأول لأنه من السهل جدًا المضيق للأمام وبسيط ، وبعد ذلك فقط انتقل إلى الثاني (والذي يمكن إجراؤه كتعديل للنهج الأول حتى يبقى معظم الكود كما هو على أي حال)
  • إنها لفكرة جيدة حساب الكثافة المقلوبة (البيكسلات السوداء هي القيمة القصوى) لأن معاينة النص القياسية تظهر على خلفية بيضاء وبالتالي تؤدي إلى نتائج أفضل بكثير.
  • يمكنك تجربة الحجم والعدد والتخطيط لمناطق التقسيم الفرعي أو استخدام بعض الشبكات مثل 3x3 بدلاً من ذلك.

[Edit1] المقارنة

أخيرًا ، هناك مقارنة بين النهجين على نفس المدخلات:

تتم الصور التي تحمل علامة النقطة الخضراء مع النهج رقم 2 والصور الحمراء ذات الرقم 1 على حجم الخط 6 بكسل. كما ترون في صورة المصباح الكهربائي ، فإن النهج الحساس للأشكال أفضل بكثير (حتى إذا كان الرقم 1 يتم على صورة المصدر بتكبير 2x).

[Edit2] تطبيق رائع

أثناء قراءة الأسئلة الجديدة اليوم ، حصلت على فكرة عن تطبيق رائع يستحوذ على منطقة محددة من سطح المكتب ويغذيها باستمرار إلى محول ASCIIart وعرض النتيجة. بعد ساعة من الترميز ، يتم إرضاءي بالنتيجة التي يجب علي إضافتها هنا.

موافق التطبيق يتكون من 2 نوافذ فقط. النافذة الرئيسية الأولى هي في الأساس نافذة المحول القديمة الخاصة بي دون تحديد صورة ومعاينة (كل العناصر المذكورة أعلاه موجودة فيها). إنه يحتوي فقط على معاينة وإعدادات ASCII. النافذة الثانية هي شكل فارغ مع شفافية من الداخل لاختيار منطقة المسكة (لا توجد وظائف على الإطلاق).

الآن في جهاز الموقت ، استول فقط على المنطقة المحددة من خلال نموذج التحديد ، وقم بتمريرها إلى التحويل ومعاينة ASCIIart .

لذا يمكنك إحاطة المساحة التي تريد تحويلها من خلال نافذة التحديد وعرض النتيجة في النافذة الرئيسية. يمكن أن تكون لعبة ، عارض ، ... يبدو كما يلي:

حتى الآن يمكنني مشاهدة مقاطع الفيديو حتى في ASCIIart للمتعة. بعضها لطيف حقا :).

[Edit3]

إذا كنت ترغب في محاولة تنفيذ ذلك في GLSL ، فقم بإلقاء نظرة على هذا:

  • تحويل أرقام الفاصلة العائمة إلى أرقام عشرية في GLSL؟

فاتحة

ينبثق هذا الموضوع هنا على SO من وقت لآخر ، ولكن تتم إزالته عادة بسبب كونه سؤالًا سيئًا. لقد رأيت العديد من هذه الأسئلة ثم أكون صامتًا من البروتوكول الاختياري (مندوب منخفض معتاد) عند طلب معلومات إضافية. من وقت لآخر إذا كانت المدخلات جيدة بما يكفي بالنسبة لي ، فقد قررت الرد بإجابة وعادة ما تحصل على عدد قليل من الأصوات في اليوم بينما تكون نشطة ولكن بعد بضعة أسابيع يتم حذف / حذف السؤال ويبدأ كل شيء من البداية . لذلك قررت أن أكتب سؤال وجواب حتى أتمكن من الرجوع إلى هذه الأسئلة مباشرة دون إعادة كتابة الإجابة مرارًا وتكرارًا ...

سبب آخر هو أن مؤشر ترابط META هذا يستهدفني لذا إذا حصلت على مدخلات إضافية فلا تتردد في التعليق.

سؤال

كيفية تحويل صورة نقطية إلى فن ASCII باستخدام C ++ ؟

بعض القيود:

  • مقياس الصور الرمادية
  • باستخدام الخطوط أحادية متباعدة
  • جعلها بسيطة (لا تستخدم مواد متقدمة جدًا للمبرمجين على مستوى المبتدئين)

إليكم صفحة ويكي ذات صلة بـ ASCII art (بفضلRogerRowland)





ascii-art