ASCII Art на C#

На страницах КГ уже не раз проскакивала тема ASCII Art. Поэтому на этот раз обойдемся без лишних прелюдий. Сегодня предлагаю вам написать программу, которая преобразовывала бы изображение в последовательность символов. Наша программа будет уметь отображать результат на экране монитора, а также выводить рисунок на печать.

Библиотека

По привычке зашел на хорошо знакомый мне ресурс CodeProject. На всякий случай поучаствовал в очередном голосовании, и совершенно случайно мой взгляд упал на статью с заголовком "ASCII Art with C#". Мне стало определенно интересно, хоть я уже примерно знал, о чем пойдет речь. Но от этого мой интерес еще больше подогревался: мне давно хотелось знать, каким образом авторам подобных программ удается передать оттенки с помощью различных символов, но руки до этого как-то не доходили. Собственно говоря, появление этой статьи заинтересовало не только меня. Ее автор Даниел Фишер даже попал в блог MSDN. Естественно, я посчитал несправедливым оставлять нашего "неанглоязычного" читателя без возможности попробовать этот фрукт на вкус. Что ж, давайте проверим уже имеющийся код в действии.

Оформим проект в виде библиотеки классов. У нас будет один класс Converter и метод (возможно, статический) ConvertImage, который имеет следующую сигнатуру:

public static string ConvertImage(string filePath)

Как вы уже догадались, filePath — это путь к файлу с изображением. Для начала изображение необходимо загрузить и проинициализировать необходимые переменные. В этом нам помогут методы класса System.Drawing.Imaging, а точнее, один из них — FromFile. Для работы ему требуется путь к файлу — тот самый filePath. Далее вся работа будет вестись с экземпляром класса System.Drawing.Bitmap. По сути, это не более чем надстройка над классом Imaging, предоставляющая обширные возможности по работе с растровым изображением. Нам из всего этого невиданного многообразия понадобится всего лишь один метод — GetPixel. Но это я уже немного забежал вперед. Короче говоря, на данном этапе все, что нам нужно, — инициализировать прямоугольную область. Она нам пригодится для дальнейшего использования. Все вышесказанное наконец реализуем в коде:

Image img = Image.FromFile(filePath);
Bitmap bmp = new Bitmap(img, new Size(img.Width, img.Height));
img.Dispose();
Rectangle bounds = new Rectangle(0, 0, bmp.Width, bmp.Height);

Выбранное изображение поступает на растерзание алгоритму не без предварительной обработки. Дело в том, что производить расчеты с черно-белым изображением гораздо проще, нежели с цветным. В черно-белом изображении присутствуют только оттенки серого, в цветном же имеет место вся гамма формата RGBA (red-green-blue-alpha) — от 0,0,0,0 до 255,255,255,255. Поэтому перво-наперво необходимо привести изображение к черно-белому. Основополагающей для последующих преобразований является структура ColorMatrix. Для начала ненадолго остановимся на понятии матрицы как таковом. Графически матрица представлена на рисунке
— ничем не примечательный массив для хранения чисел, находящий, однако, широкое применение. При программировании трехмерных сцен наиболее часто применяются матрицы 4х4. Существуют матрицы и с иной размерностью, например, 1х4, которые используются для задания вектора
. Оказывается, цветовая модель RGBA тоже является своего рода вектором. Для преобразований векторов используются те же матрицы. При этом осуществляется операция умножения вектора на матрицу. Чтобы получить координаты результирующего вектора, необходимо найти сумму произведений каждой координаты исходного вектора на значения в соответствующей колонке
. Разумно было бы предположить, что матрица ColorMatrix для преобразования цветовых составляющих также представляет собой матрицу 4х4 — все-таки RGBA. Но на этот раз не все так просто. Матрицы 4х4 хватило бы только для того, чтобы осуществить линейные преобразования над вектором (например, вращение). Установка же цвета является нелинейной операцией. Пятый элемент вектора позволяет объединить эти два вида операций в одну — аффинные преобразования. При операциях с цветом за единицу берется максимальное значение 255. Меньшие значения удовлетворяют промежутку [0.0; 1.0). Например, 135 ковертируется в 0.6, т.к. исходя из прямопропорциональной зависимости имеем: 255 / 135 = 0.6.

Ну вот, разобравшись с понятием матрицы, можно приступать к реализации алгоритма, приводящего изображение к черно-белому. Как видите, сейчас схема действий программы абсолютно прозрачна. Формируем соответствующую матрицу, а затем применяем ее к имеющемуся изображению.

ColorMatrix matrix = new ColorMatrix();

for(int i = 0; i <= 2; i++)
   for(int j = 0; j <= 2; j++)
      matrix[i, j] = 1/3f;
   }
}

ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(matrix);

Graphics gphGrey = Graphics.FromImage(bmp);
gphGrey.DrawImage(bmp, bounds, 0, 0, bmp.Width, bmp.Height, GraphicsUnit.Pixel, attributes);

gphGrey.Dispose();

Но то, что мы делали раньше, — это, так сказать, побочный эффект. Сейчас мы напишем сердце нашей библиотечки. Заменять каждый пиксель изображения соответствующим символом неинтересно — ведь символ по размерам больше пикселя, а, следовательно, изображение увеличится в несколько раз. Поэтому будем заменять покусочно. Каждый кусок будет 5 пикселей в ширину и 10 пикселей в высоту.

Теперь проходимся по всем пикселям изображения, и на каждом участке размером 5х10 высчитываем общую яркость. И на этом основании подставляем уже конкретный символ из заданного набора. Вот этот нехитрый код (я для краткости оставил только несколько секций выборки):

for(int h = 0; h <(bmp.height / 10); h++) {
int startY = (h * 10);

   for(int w = 0; w <(bmp.width / 5); w++) {
      int startX = (w * 5);

      int brightness = 0;

      for(int y = 0; y <10; y++) {
         for(int x=0; x <10; x++) {
            int cY = y + startY;
int cX = x + startX;
                        
try {
               Color c = bmp.GetPixel(cX, cY);
               int b = (int)(c.GetBrightness() * 10);
               brightness = (brightness + b);
            } catch {
               brightness = (brightness + 10);
            }
         }
      }

      int sb = (brightness / 10);

      if(sb <25) {
asciiOutput.Append("#");
} else if(sb <30) {
asciiOutput.Append("@");
      } else if(sb <40) {
         asciiOutput.Append("Ш");
} ... else if (sb <80) {
         asciiOutput.Append("ґ");
} else {
         asciiOutput.Append(" ");
}

asciiOutput.Append("\n");
   }

   bmp.Dispose();
   return asciiOutput.ToString();
}

После 40 с шагом 5 идут следующие символы: '$', '&', '¤', '~', '·', 'Ё'.

Демонстрация
Демонстрационное приложение довольно простое. Нам необходимо всего лишь сделать ссылку на библиотеку Converter.dll, а затем в коде вызвать статический метод ConvertImage и скормить ему какое-нибудь изображение. Естественно, необходимо предусмотреть контрол, в который будет осуществляться вывод получившейся картинки. Т.к. метод ConvertImage возвращает переменную типа string, то вполне разумно сделать ставку на RichTextBox. Правда, для того чтобы картинка хорошо смотрелась, необходимо выбрать шрифт, все символы которого идентичны по ширине. Отличным кандидатом на эту роль может стать шрифт Courier New. Кегль выбирайте исходя из разрешения вашего монитора — возможно, при большом размере шрифта картинка не будет умещаться на экран. В принципе, за минимальную функциональность сойдет. Но я пошел несколько дальше — прикрутил возможность распечатки полученной картинки. Сейчас я вам расскажу, как это сделать. Итак, для работы нам потребуется экземпляр класса System.Windows.Forms.PrintDialog и класса System.Drawing.Prining.PrintDocument. Нам также будет необходима строковая переменная, которую мы будем использовать для построчного считывания картинки. Предположим, вы завели на форме кнопку для вывода документа на печать. Тогда в событии Click этой кнопки записываем:

this.printDialog.Document = this.printDocument;
strReader = new StringReader(this.textBox.Text);
if(printDialog.ShowDialog() == DialogResult.OK) {
this.printDocument.Print();
}

Это, правда, не самое важное. Подписавшись на событие PrintPage объекта printDocument, в теле класса записываем:

private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
   float linesPerPage = 0.0f;
   float yPos = 0.0f;
   float leftMargin = e.MarginBounds.Left;
   float topMargin = e.MarginBounds.Top;
   string line = "";
   int count = 0;
   Font printFont = this.textBox.Font;
   Brush br = new SolidBrush(Color.Black);
   linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics);

   while(count       line = strReader.ReadLine();
               
      if(line == null)
         return;

      yPos = topMargin + count * printFont.GetHeight(e.Graphics);
      e.Graphics.DrawString(line, printFont, Brushes.Black, leftMargin, yPos, new StringFormat());
      ++count;
   }

   if(line != null)
      e.HasMorePages = true;
   else
      e.HasMorePages = false;

   br.Dispose();
}

Вот и все. Теперь мы с вами не только приобщились к ASCII Art, но и знаем весь процесс изнутри.



Использованную при подготовке материала статью можно найти по адресу:
http://www.codeproject.com/aspnet/ascii_art_with_c_.asp

2004, Алексей Нестеров


Компьютерная газета. Статья была опубликована в номере 01 за 2005 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета