تحويل الصور بالأبيض والأسود إلى رسومات ASCII باستخدام تحلل المصفوفة غير السلبي


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

وصف الخوارزمية


الصورة الأصلية مقسمة إلى كتل من الحجم مرات \ N حيث M و N - عرض وارتفاع حرف واحد بالبكسل. إذا لم يكن عرض / ارتفاع الصورة من مضاعفات عرض / ارتفاع الحرف ، فسيتم اقتصاص الصورة أو استكمالها بمساحات بيضاء بالحجم المطلوب.


كل من K الكتل التي تم الحصول عليها بعد التقسيم ممثلة كمتجه بطول R = M * N قيمها هي كثافة ألوان بكسلات الصورة (القيم من 0 إلى 255 ، حيث يتوافق البيكسل الأبيض مع القيمة 0 والبكسل الأسود يناظر 255). يجب تطبيع المتجهات الناتجة باستخدام القاعدة l_2 :

v_i = \ frac {v_i} {\ sqrt {\ sum ^ R_ {k = 1} {v ^ 2_k}}}


تتم إعادة كتابة المتجهات الطبيعية في شكل أعمدة ، وبالتالي تشكل مصفوفة V الحجم ص \ مرات ك .

المصفوفة الناتجة V بحاجة إلى أن تكون ممثلة كمنتج من المصفوفات W و H جميع العناصر التي ليست سلبية:

V_ {R \ times K} = W_ {R \ times L} H_ {L \ times K}

قالب W المعروف مقدما: يتم بناؤه على غرار المصفوفة V ولكن بدلاً من أقسام الصورة الأصلية ، يتم استخدام صور لجميع الرموز المستخدمة في إنشاء رسومات ASCII. إذا كانت المجموعة المطبقة تشمل L الشخصيات ثم المصفوفة W سيكون لها حجم R \ الأوقات L .
يبقى فقط لاختيار المصفوفة H وذلك لتقليل قيمة وظيفة معينة تميز الفرق بين V و العمل WH . يتم استخدام التبعية التالية كدالة كهذه:

D (V، W، H، \ beta) = \ sum_ {ik} \ bigg ({v_ {ik} \ frac {v_ {ik} ^ {\ beta-1} - [WH] ^ {\ beta-1} _ {ik}} {\ beta (\ beta-1)}} + [WH] ^ {\ beta-1} _ {ik} \ frac {[WH] _ {ik} -v_ {ik}} {\ beta } \ bigg)

يجمع هذا التعبير بشكل أساسي بين عدة وظائف موضوعية: متى \ beta = 2 يتم تحويله إلى مربع المسافة الإقليدية (المسافة الإقليدية المربعة) ، عندما \ beta \ rightarrow 1 يقترب من مسافة الاختلاف Kullback- ليبلر ، وفي \ beta \ rightarrow 0 - على مسافة Itakura- سايتو (Itakura-Saito Divergence).

اختيار المصفوفة المباشرة H أنتجت على النحو التالي: H تتم التهيئة باستخدام قيم عشوائية من 0 إلى 1 ، وبعد ذلك يتم تحديث قيمها بشكل متكرر وفقًا للقاعدة التالية (يتم تعيين عدد التكرارات مسبقًا):

h_ {jk} = h_ {jk} \ frac {\ sum ^ R_ {i = 1} {w_ {ij} \ frac {v_ {ik}} {[WH] ^ {2- \ beta} _ {ik}} }} {\ sum ^ R_ {i = 1} {w_ {ij} [WH] ^ {\ beta-1} _ {ik}}}

كل قيمة h_ {ij} يتوافق مع درجة التشابه أنا شخصية من مجموعة مع ي القسم الرابع من الصورة.


لذلك ، لتحديد أي شخصية يجب استبدالها ي القسم ، يكفي العثور على الحد الأقصى للقيمة في ي ال ال العمود من المصفوفة H . سيكون رقم السطر الذي توجد به هذه القيمة هو رقم الحرف المطلوب في المجموعة. يمكنك أيضًا إدخال بعض قيمة العتبة. \ إبسيلون وإذا كانت القيمة القصوى التي تم العثور عليها أقل من هذه العتبة ، فسيتم استبدال قسم الصورة بمسافة. يمكن أن يكون لاستخدام مسافة تأثير إيجابي على ظهور الصورة الناتجة مقارنة باستخدام رمز بدرجة منخفضة من التشابه.

تطبيق


يتم تنفيذ الخوارزمية في C #. يتم إنشاء رسومات ASCII باستخدام 95 حرفًا (من 0x20 إلى 0x7E) بحجم 11 × 23 بكسل ؛ الخط المستخدم هو Courier. فيما يلي الشفرة المصدرية للدالة لتحويل الصورة الأصلية إلى رسومات ASCII:

public static char[,] ConvertImage( Bitmap image, double beta, double threshold, ushort iterationsCount, ushort threadsNumber, Action<int> ProgressUpdated) { int charNumHor = (int)Math.Round((double)image.Width / glyphWidth); int charNumVert = (int)Math.Round((double)image.Height / glyphHeight); int totalCharactersNumber = charNumVert * charNumHor; int glyphSetSize = wNorm.ColumnCount; Matrix<double> v = SplitImage(image, charNumVert, charNumHor); Matrix<double> h = Matrix<double>.Build.Random( glyphSetSize, totalCharactersNumber, new ContinuousUniform()); int progress = 0; ushort step = (ushort)(iterationsCount / 10); for (ushort i = 0; i < iterationsCount; i++) { UpdateH(v, wNorm, h, beta, threadsNumber); if((i + 1) % step == 0) { progress += 10; if(progress < 100) { ProgressUpdated(progress); } } } var result = GetAsciiRepresentation(h, charNumVert, charNumHor, threshold); ProgressUpdated(100); return result; } 

النظر في كل خطوة على حدة:

1) نحسب عدد الأحرف التي يمكن احتواؤها في عرض الصورة وارتفاعها:

 int charNumHor = (int)Math.Round((double)image.Width / glyphWidth); int charNumVert = (int)Math.Round((double)image.Height / glyphHeight); 

باستخدام القيم المحسوبة ، نقسم الصورة الأصلية إلى كتل بالحجم المطلوب. لكل كتلة ، نكتب قيم كثافة ألوان البكسل في عمود المصفوفة المقابل V (إذا لزم الأمر ، نقوم بتوسيع الصورة الأصلية عن طريق إضافة قيم صفر تقابل البيكسلات البيضاء في المصفوفة) ، وبعد ذلك نقوم بتطبيع جميع الأعمدة:

 private static Matrix<double> SplitImage( Bitmap image, int charNumVert, int charNumHor) { Matrix<double> result = Matrix<double>.Build.Dense( glyphHeight * glyphWidth, charNumHor * charNumVert); for (int y = 0; y < charNumVert; y++) { for (int x = 0; x < charNumHor; x++) { for (int j = 0; j < glyphHeight; j++) { for (int i = 0; i < glyphWidth; i++) { byte color = 0; if ((x * glyphWidth + i < image.Width) && (y * glyphHeight + j < image.Height)) { color = (byte)(255 - image.GetPixel( x * glyphWidth + i, y * glyphHeight + j).R); } result[glyphWidth * j + i, charNumHor * y + x] = color; } } } } result = result.NormalizeColumns(2.0); return result; } 

2) املأ المصفوفة H قيم عشوائية من 0 إلى 1:

 Matrix<double> h = Matrix<double>.Build.Random( glyphSetSize, totalCharactersNumber, new ContinuousUniform()); 

نطبق قاعدة التحديث عددًا محددًا من المرات على عناصره:

 for (ushort i = 0; i < iterationsCount; i++) { UpdateH(v, wNorm, h, beta, threadsNumber); if((i + 1) % step == 0) { progress += 10; if(progress < 100) { ProgressUpdated(progress); } } } 

يتم تنفيذ التحديث المباشر لعناصر المصفوفة على النحو التالي (لسوء الحظ ، يتم حل المشكلات المرتبطة بالتقسيم على الصفر باستخدام بعض العكازات):

 private static void UpdateH( Matrix<double> v, Matrix<double> w, Matrix<double> h, double beta, ushort threadsNumber) { const double epsilon = 1e-6; Matrix<double> vApprox = w.Multiply(h); Parallel.For( 0, h.RowCount, new ParallelOptions() { MaxDegreeOfParallelism = threadsNumber }, j => { for (int k = 0; k < h.ColumnCount; k++) { double numerator = 0.0; double denominator = 0.0; for (int i = 0; i < w.RowCount; i++) { if (Math.Abs(vApprox[i, k]) > epsilon) { numerator += w[i, j] * v[i, k] / Math.Pow(vApprox[i, k], 2.0 - beta); denominator += w[i, j] * Math.Pow(vApprox[i, k], beta - 1.0); } else { numerator += w[i, j] * v[i, k]; if (beta - 1.0 > 0.0) { denominator += w[i, j] * Math.Pow(vApprox[i, k], beta - 1.0); } else { denominator += w[i, j]; } } } if (Math.Abs(denominator) > epsilon) { h[j, k] = h[j, k] * numerator / denominator; } else { h[j, k] = h[j, k] * numerator; } } }); } 

3) الخطوة الأخيرة هي اختيار رمز مناسب لكل قسم صورة عن طريق إيجاد الحد الأقصى للقيم في أعمدة المصفوفة H :

 private static char[,] GetAsciiRepresentation( Matrix<double> h, int charNumVert, int charNumHor, double threshold) { char[,] result = new char[charNumVert, charNumHor]; for (int j = 0; j < h.ColumnCount; j++) { double max = 0.0; int maxIndex = 0; for (int i = 0; i < h.RowCount; i++) { if (max < h[i, j]) { max = h[i, j]; maxIndex = i; } } result[j / charNumHor, j % charNumHor] = (max >= threshold) ? (char)(firstGlyphCode + maxIndex) : ' '; } return result; } 

تتم كتابة الصورة الناتجة إلى ملف html. يمكن العثور على شفرة المصدر الكاملة للبرنامج هنا .

أمثلة على الصور المولدة


فيما يلي أمثلة من الصور التي تم إنشاؤها في قيم المعلمات المختلفة \ بيتا وعدد التكرارات. كان حجم الصورة الأصلية 407 × 500 بكسل ، على التوالي ، وكان حجم الصورة الناتجة 37 × 22 حرفًا.


استنتاج


في الخوارزمية المدروسة ، يمكن تمييز العيوب التالية:

  1. معالجة الصور الطويلة: اعتمادًا على حجم الصورة وعدد التكرارات ، يمكن أن تستغرق معالجتها من عدة عشرات من الثواني إلى عدة عشرات من الدقائق.
  2. سوء جودة معالجة الصور التفصيلية. على سبيل المثال ، تؤدي محاولة تحويل صورة لوجه بشري إلى النتيجة التالية:


في الوقت نفسه ، يمكن أن يؤدي تقليل عدد الأجزاء عن طريق زيادة سطوع الصورة وتباينها إلى تحسين مظهر الصورة الناتجة بشكل كبير:


بشكل عام ، على الرغم من العيوب المذكورة أعلاه ، يمكننا أن نستنتج أن الخوارزمية تعطي نتائج مرضية.

Source: https://habr.com/ru/post/ar463193/


All Articles