Im Allgemeinen ist das Konvertieren eines Bildes in ASCII-Grafiken eine ziemlich zeitaufwändige Aufgabe, aber es gibt Algorithmen, die diesen Prozess automatisieren. Dieser Artikel beschreibt den von den Forschern Paul D. O'Grady und Scott T. Rickard vorgeschlagenen Ansatz in
"Automatische ASCII-Kunstkonvertierung von Binärbildern unter Verwendung nicht negativer Einschränkungen". Die von ihnen beschriebene Methode beinhaltet die Darstellung des Bildkonvertierungsprozesses als Optimierungsproblem und die Lösung dieses Problems unter Verwendung einer nicht negativen Matrixzerlegung. Nachfolgend finden Sie eine Beschreibung des betreffenden Algorithmus sowie dessen Implementierung:
Beschreibung des Algorithmus
Das Originalbild ist in Größenblöcke unterteilt

wo

und

- Breite und Höhe eines Zeichens in Pixel. Wenn die Breite \ Höhe des Bildes nicht ein Vielfaches der Breite \ Höhe des Zeichens ist, wird das Bild zugeschnitten oder durch weiße Bereiche der gewünschten Größe ergänzt.
Jeder von

Blöcke, die nach der Partition erhalten werden, werden als Vektor der Länge dargestellt

deren Werte sind die Farbintensitäten der Bildpixel (Werte von 0 bis 255, wobei das weiße Pixel dem Wert 0 und das schwarze Pixel 255 entspricht). Die resultierenden Vektoren sollten unter Verwendung der Norm normalisiert werden

::
Die normalisierten Vektoren werden in Form von Spalten umgeschrieben, wodurch eine Matrix gebildet wird

die Größe

.
Die resultierende Matrix

müssen als Produkt von Matrizen dargestellt werden

und

Alle Elemente davon sind nicht negativ:
Matrix

im Voraus bekannt: Es ist ähnlich wie die Matrix aufgebaut

Anstelle von Abschnitten des Originalbilds werden jedoch Bilder aller Symbole verwendet, die bei der Erzeugung von ASCII-Grafiken verwendet werden. Wenn das zutreffende Kit enthält

Zeichen dann die Matrix

wird eine Größe haben

.
Es bleibt nur eine Matrix zu wählen

um den Wert einer Zielfunktion zu minimieren, die den Unterschied zwischen charakterisiert

und arbeiten

. Die folgende Abhängigkeit wird als solche Funktion verwendet:
Dieser Ausdruck kombiniert im Wesentlichen mehrere objektive Funktionen: wann

es wird in das Quadrat der euklidischen Entfernung (quadratische euklidische Entfernung) umgewandelt, wenn

nähert sich der Kullback-Leibler-Divergenzstrecke und bei

- bis zur Entfernung von Itakura-Saito (Itakura-Saito-Divergenz).
Direkte Matrixauswahl

wie folgt hergestellt:

Initialisiert mit zufälligen Werten von 0 bis 1, wonach die Werte gemäß der folgenden Regel iterativ aktualisiert werden (die Anzahl der Iterationen wird im Voraus festgelegt):
Jeder Wert

entspricht dem Ähnlichkeitsgrad

Zeichen aus dem Set mit

-th Abschnitt des Bildes.
So bestimmen Sie, welches Zeichen ersetzt werden soll

Abschnitt reicht es aus, den Maximalwert in zu finden

th Spalte der Matrix

. Die Zeilennummer, in der sich dieser Wert befindet, ist die Nummer des gewünschten Zeichens im Satz. Sie können auch einen Schwellenwert eingeben.

Wenn der gefundene Maximalwert unter diesem Schwellenwert liegt, wird der Bildabschnitt durch ein Leerzeichen ersetzt. Die Verwendung eines Leerzeichens kann sich positiv auf das Erscheinungsbild des resultierenden Bildes auswirken, verglichen mit der Verwendung eines Symbols mit geringem Ähnlichkeitsgrad.
Implementierung
Der Algorithmus ist in C # implementiert. ASCII-Grafiken werden mit 95 Zeichen (von 0x20 bis 0x7E) mit einer Größe von 11x23 Pixel generiert. Die verwendete Schriftart ist Courier. Unten finden Sie den Quellcode für die Funktion zum Konvertieren des Originalbilds in ASCII-Grafiken:
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; }
Betrachten Sie jeden Schritt einzeln:
1) Wir berechnen, wie viele Zeichen in die Breite und Höhe des Bildes passen können:
int charNumHor = (int)Math.Round((double)image.Width / glyphWidth); int charNumVert = (int)Math.Round((double)image.Height / glyphHeight);
Mit den berechneten Werten teilen wir das Originalbild in Blöcke der erforderlichen Größe. Für jeden Block schreiben wir die Werte der Pixelfarbintensität in die entsprechende Matrixspalte

(Falls erforderlich, erweitern wir das Originalbild, indem wir der Matrix Nullwerte hinzufügen, die weißen Pixeln entsprechen.) Anschließend normalisieren wir alle Spalten:
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) Füllen Sie die Matrix

Zufallswerte von 0 bis 1:
Matrix<double> h = Matrix<double>.Build.Random( glyphSetSize, totalCharactersNumber, new ContinuousUniform());
Wir wenden die Aktualisierungsregel eine bestimmte Anzahl von Malen auf ihre Elemente an:
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); } } }
Die direkte Aktualisierung der Matrixelemente wird wie folgt implementiert (leider werden die mit der Division durch Null verbundenen Probleme mit einigen Krücken gelöst):
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) Der letzte Schritt besteht darin, ein geeignetes Symbol für jeden Bildabschnitt auszuwählen, indem die Maximalwerte in den Matrixspalten ermittelt werden

::
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; }
Das resultierende Bild wird in die HTML-Datei geschrieben. Den vollständigen Quellcode des Programms finden Sie
hier .
Beispiele für generierte Bilder
Nachfolgend finden Sie Beispiele für Bilder, die mit verschiedenen Parameterwerten erstellt wurden

und die Anzahl der Iterationen. Das Originalbild hatte eine Größe von 407 x 500 Pixel, das resultierende Bild hatte eine Größe von 37 x 22 Zeichen.
Fazit
In dem betrachteten Algorithmus können die folgenden Nachteile unterschieden werden:
- Lange Bildverarbeitung: Abhängig von der Größe des Bildes und der Anzahl der Iterationen kann die Verarbeitung mehrere zehn Sekunden bis einige zehn Minuten dauern.
- Verarbeitung von Detailbildern in schlechter Qualität. Ein Versuch, ein Bild eines menschlichen Gesichts zu konvertieren, führt beispielsweise zu folgendem Ergebnis:
Gleichzeitig kann die Reduzierung der Anzahl der Teile durch Erhöhen der Helligkeit und des Kontrasts des Bildes das Erscheinungsbild des resultierenden Bildes erheblich verbessern:
Im Allgemeinen können wir trotz der obigen Nachteile den Schluss ziehen, dass der Algorithmus zufriedenstellende Ergebnisse liefert.