Доброе утро!
Разбирая мусор в домашнем каталоге, я наткнулся на забавную утилиту, написанную несколько лет назад при изучении Qt. Занимается программа преобразованием обычных картинок в ASCII-графику. Алгоритм довольно прост, но кому-то может показаться интересным, так что в этой статье речь пойдёт о нём.
Для начала, взглянем на результат:



Как видим, в полученном изображении неплохо передаются градации яркости, видно даже тени.
Получить такую картинку достаточно просто. В процессе можно выделить два этапа: сортировку символов шрифта по заполненности чёрным цветом и замену изображения на символы, в соответствии с яркостью пикселя. В моём случае, эти этапы были разнесены в отдельные приложения.
Реализация первого этапа сводится к выполнению следующей функции:
bool ClStats::compareStats(const QPair<char, float> &a, const QPair<char, float> &b) {
return a.second < b.second;
}
void ClStats::countStats() {
QGraphicsScene *scn = new QGraphicsScene(this);
QPixmap pixmap = QPixmap(800, 800);
QImage img;
QPainter painter(&pixmap);
stats.resize(95);
painter.setBrush(QBrush(QColor(255, 255, 255)));
painter.setFont(QFont(ui->leFont->text(), 512));
for (int c = 32; c < 127; ++c) {
painter.drawRect(QRect(-1, -1, 1100, 1100));
painter.drawText(QPoint(50, 590), QString((char) c));
scn->addPixmap(pixmap);
scn->setSceneRect(pixmap.rect());
ui->gvChar->setScene(scn);
ui->gvChar->fitInView(scn->sceneRect(), Qt::KeepAspectRatio);
this->repaint();
img = pixmap.toImage();
int black = 0;
for (int i = 0; i < 800; ++i) {
for (int j = 0; j < 800; ++j) {
if (QColor(img.pixel(i, j)).green() < 128) {
++black;
}
}
}
stats[c-32] = QPair<char, float>((char) c, black/160000.0);
}
std::sort(stats.begin(), stats.end(), compareStats);
for (int i = 0; i < 95; ++i) {
ui->tblResults->setItem(i, 0, new QTableWidgetItem(QString::number(stats[i].first)));
ui->tblResults->setItem(i, 1, new QTableWidgetItem(QString(stats[i].first)));
ui->tblResults->setItem(i, 2, new QTableWidgetItem(QString::number(stats[i].second)));
}
QString st = "";
for (int i = 0; i < 95; ++i) {
st.append(stats[i].first);
}
ui->leResult->setText(st);
}
Она выполняет заполнение и сортировку массива stats, являющегося списком пар из символа и его относительной яркости путём рассчёта отношения чёрных пикселей символов к белым, при том, что символы отрисовываются одним шрифтом одного размера на одинакового размера белых холстах. После этого получаем строку, где символы расположены в соответствии с их "яркостью". Для DejaVu Sans Mono это такая последовательность:
.`-,':;_"~^i!*l/\rI()j|?ctf+][JvL<>=7}{zsxY1TunyFok2eahC3V54XPS$qdpbU0AEZK96HgwGR#8m&OD%QBNMW@
Даже если читатель сейчас банально удалится от монитора, он сможет увидеть градиентную линию.
Дальше всё тоже довольно просто. Запоминаем эту строчку (для удобства, я делаю это в обратном порядке):
stats = " `.-'_,:~\";^!*r\\/+()|<>=?lciv][tzjL7fxs}{YT1JnuCyIFo2%ewVhk3a4Z5SXP$GmAqpbdEU&K69OHg#D0R8QWNBM@";
std::reverse(stats.begin(), stats.end());
Открываем заданную картинку как QImage и заполняем массив символов, выбирая их в соответствии с яркостью текущего пикселя:
QImage img;
QVector< QVector<QChar> > result;
img.load(ui->leSource->text());
ui->lScale->setText(
QString("Width: ") +
QString::number(img.width()) +
QString("\nHeight: ") +
QString::number(img.height()));
this->repaint();
result.resize(img.height());
for (size_t i = 0; i < img.height(); ++i) {
ui->statusBar->showMessage(
QString("row: ") +
QString::number(i) +
QString(", ") +
QString::number(1.0*i/img.height()) +
QString("%"));
this->repaint();
result[i].resize(img.width());
for (size_t j = 0; j < img.width(); ++j) {
result[i][j] = stats[(int) (94 * (QColor(img.pixel(j, i)).value() / 255.0))];
}
}
Далее остаётся только сохранить результат в необходимом формате. Я делал это в одном из четырёх видов: текст, HTML, картинка «чёрный на белом», картинка «белый на чёрном». Последние два делаются стандартными методами QPixmap, в который конвертируется QImage, из первых двух приведу пример экспорта в HTML, так как они похожи, но HTML, в моём случае, сохраняет ещё и оригинальный цвет символов:
QFile fout(filename);
fout.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream res(&fout);
res << "<pre style=\"font-family: DejaVu Sans Mono, monospace; font-size: 2pt;\">\n";
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data[i].size(); ++j) {
res << "<span style=\"color: " << QColor(img.pixel(j, i)).name() << "\">" << data[i][j] << "</span>";
}
res << "\n";
}
res << "</pre>";
fout.close();
Всё довольно просто и примитивно, Qt использовался для того, чтобы дать приложению графический интерфейс и работать с графикой, но сам алгоритм ни на какие его особенности не завязан. Напоследок, приведу ещё картинок, полученных таким образом:

в полном размере

в полном размере

в полном размере