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

حيث

و

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

الكتل التي تم الحصول عليها بعد التقسيم ممثلة كمتجه بطول

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

:
تتم إعادة كتابة المتجهات الطبيعية في شكل أعمدة ، وبالتالي تشكل مصفوفة

الحجم

.
المصفوفة الناتجة

بحاجة إلى أن تكون ممثلة كمنتج من المصفوفات

و

جميع العناصر التي ليست سلبية:
قالب

المعروف مقدما: يتم بناؤه على غرار المصفوفة

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

الشخصيات ثم المصفوفة

سيكون لها حجم

.
يبقى فقط لاختيار المصفوفة

وذلك لتقليل قيمة وظيفة معينة تميز الفرق بين

و العمل

. يتم استخدام التبعية التالية كدالة كهذه:
يجمع هذا التعبير بشكل أساسي بين عدة وظائف موضوعية: متى

يتم تحويله إلى مربع المسافة الإقليدية (المسافة الإقليدية المربعة) ، عندما

يقترب من مسافة الاختلاف Kullback- ليبلر ، وفي

- على مسافة Itakura- سايتو (Itakura-Saito Divergence).
اختيار المصفوفة المباشرة

أنتجت على النحو التالي:

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

يتوافق مع درجة التشابه

شخصية من مجموعة مع

القسم الرابع من الصورة.
لذلك ، لتحديد أي شخصية يجب استبدالها

القسم ، يكفي العثور على الحد الأقصى للقيمة في

ال ال العمود من المصفوفة

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

وإذا كانت القيمة القصوى التي تم العثور عليها أقل من هذه العتبة ، فسيتم استبدال قسم الصورة بمسافة. يمكن أن يكون لاستخدام مسافة تأثير إيجابي على ظهور الصورة الناتجة مقارنة باستخدام رمز بدرجة منخفضة من التشابه.
تطبيق
يتم تنفيذ الخوارزمية في 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);
باستخدام القيم المحسوبة ، نقسم الصورة الأصلية إلى كتل بالحجم المطلوب. لكل كتلة ، نكتب قيم كثافة ألوان البكسل في عمود المصفوفة المقابل

(إذا لزم الأمر ، نقوم بتوسيع الصورة الأصلية عن طريق إضافة قيم صفر تقابل البيكسلات البيضاء في المصفوفة) ، وبعد ذلك نقوم بتطبيع جميع الأعمدة:
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) املأ المصفوفة

قيم عشوائية من 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) الخطوة الأخيرة هي اختيار رمز مناسب لكل قسم صورة عن طريق إيجاد الحد الأقصى للقيم في أعمدة المصفوفة

:
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 حرفًا.
استنتاج
في الخوارزمية المدروسة ، يمكن تمييز العيوب التالية:
- معالجة الصور الطويلة: اعتمادًا على حجم الصورة وعدد التكرارات ، يمكن أن تستغرق معالجتها من عدة عشرات من الثواني إلى عدة عشرات من الدقائق.
- سوء جودة معالجة الصور التفصيلية. على سبيل المثال ، تؤدي محاولة تحويل صورة لوجه بشري إلى النتيجة التالية:
في الوقت نفسه ، يمكن أن يؤدي تقليل عدد الأجزاء عن طريق زيادة سطوع الصورة وتباينها إلى تحسين مظهر الصورة الناتجة بشكل كبير:
بشكل عام ، على الرغم من العيوب المذكورة أعلاه ، يمكننا أن نستنتج أن الخوارزمية تعطي نتائج مرضية.