Искусство ASCII, техника создания визуальных изображений с использованием символов стандарта ASCII, на протяжении десятилетий была частью компьютерного мира. Это увлекательный способ представления изображений без необходимости использования традиционной графики. Для новичков в программировании создание программы для создания изображений ASCII может послужить полезным введением.

В этом руководстве мы рассмотрим подход C# для преобразования стандартных изображений и создания из них изображений ASCII. У вас не только будет полный исходный код для работающего приложения C#, способного генерировать изображения ASCII, но я также объясню, почему такие простые программы могут иметь решающее значение для облегчения вашего обучения.

Прежде чем я предоставлю код для создания изображений ASCII…

Я понимаю, что многие из вас, пришедших сюда, просто хотят сразу перейти к коду. Я понимаю! Но именно поэтому я хочу заранее сделать важное сообщение, особенно для более младших разработчиков.

Часто начинающие программисты застревают на ранних этапах обучения, потому что не знают, как распределить свое время. Они пытаются читать книги, статьи и сообщения в блогах (как этот!), чтобы изучить теорию, или просматривают видео и пытаются найти лучший BootCamp, чтобы иметь больше шансов на успех. Я регулярно напоминаю своей аудитории, что считаю, что создание вещей и написание кода — это один из лучших способов обучения.

Я хочу, чтобы вы помнили об этом, когда мы вместе будем работать с этим кодом! В конце статьи я предлагаю некоторые варианты и улучшения, которые вы, возможно, захотите рассмотреть. Я включил этот список не только потому, что считаю его крутым, но и для того, чтобы дать вам волю творчеству! Подумайте о разных вещах, на которых вы хотите сосредоточиться как разработчик, и посмотрите, сможете ли вы включить их в свой генератор изображений ASCII!

Возможность использовать такие простые программы снимает стресс с вопроса «что правильно построить» и позволяет вам сосредоточиться на обучении и исследовании. Смотрите видео по ходу дела!

Пример кода для создания изображения ASCII

Хорошо, вы выдержали это через мое вступление. Спасибо! Давайте посмотрим на код (который, кстати, полностью доступен на GitHub):

string imagePath = "your file path here";
using var inputStream = new FileStream(
    imagePath, 
    FileMode.Open, 
    FileAccess.Read,
    FileShare.Read);

var generator = new Generator();

using var sourceImage = Image.Load(inputStream);
using var imageRgba32 = sourceImage.CloneAs<Rgba32>();
using var image = new ImageSharpImageSource(imageRgba32);

var asciiArt = generator.GenerateAsciiArtFromImage(image);

Console.WriteLine(asciiArt.Art);

Это точка входа в нашу программу на C#. Здесь мы задаем путь к нашему изображению и создаем поток для его чтения. Мы также создаем экземпляр нашего основного класса Generator, который будет обрабатывать преобразование ASCII, а также класса ImageSharpImageSource, который будет хранить данные изображения. Волшебство происходит внутри метода GenerateAsciiArtFromImage, который мы вскоре рассмотрим.

Библиотека ImageSharp используется для загрузки изображения и последующего его клонирования в формат (Rgba32), который позволяет нам работать с отдельными цветами пикселей. Класс ImageSharpImageSource действует как мост между библиотекой ImageSharp и нашей логикой генерации ASCII. Когда мы посмотрим на код этого класса, мы сможем увидеть метод индексатора, который позволяет нам получать данные пикселей для координат X и Y.

Давайте посмотрим на реализацию источника изображения дальше:

internal interface IImageSource : IDisposable
{
    int Width { get; }

    int Height { get; }

    float AspectRatio { get; }

    Rgb GetPixel(int x, int y);
}

internal sealed class ImageSharpImageSource : IImageSource
{
    private readonly Image<Rgba32> _image;

    public ImageSharpImageSource(Image<Rgba32> image)
    {
        _image = image;
    }

    public int Width => _image.Width;

    public int Height => _image.Height;

    public float AspectRatio => _image.Width / (float)_image.Height;

    public Rgb GetPixel(int x, int y)
    {
        var pixel = _image[x, y];
        return new(
            pixel.R,
            pixel.G,
            pixel.B);
    }

    public void Dispose() => _image.Dispose();
}

В приведенном выше коде мы видим, что реализуем интерфейс IImageSource. Это было сделано потому, что вы действительно можете реализовать ту же функциональность с пространством имен System.Drawing и классом Bitmap, но это будет работать только в Windows. Код _image[x, y] позволяет нам получить информацию о пикселях изображения!

Последний класс важности — это сам генератор. Разберем код подробнее в следующих разделах:

internal sealed class Generator
{
    public AsciiArt GenerateAsciiArtFromImage(
        IImageSource image)
    {
        var asciiChars = "@%#*+=-:,. ";

        var aspect = image.Width / (double)image.Height;
        var outputWidth = image.Width / 16;
        var widthStep = image.Width / outputWidth;
        var outputHeight = (int)(outputWidth / aspect);
        var heightStep = image.Height / outputHeight;

        StringBuilder asciiBuilder = new(outputWidth * outputHeight);
        for (var h = 0; h < image.Height; h += heightStep)
        {
            for (var w = 0; w < image.Width; w += widthStep)
            {
                var pixelColor = image.GetPixel(w, h);
                var grayValue = (int)(pixelColor.Red * 0.3 + pixelColor.Green * 0.59 + pixelColor.Blue * 0.11);
                var asciiChar = asciiChars[grayValue * (asciiChars.Length - 1) / 255];
                asciiBuilder.Append(asciiChar);
                asciiBuilder.Append(asciiChar);
            }

            asciiBuilder.AppendLine();
        }

        AsciiArt art = new(
            asciiBuilder.ToString(),
            outputWidth,
            outputHeight);
        return art;
    }
}

Разбивка обработки изображений

Когда мы говорим об изображениях на компьютерах, мы, по сути, обсуждаем матрицу пикселей. Каждый пиксель имеет цвет, и этот цвет обычно представлен тремя основными компонентами: красным, зеленым и синим (RGB). В этом миксе вы также можете увидеть четвертый компонент — «альфа» (или прозрачность), обозначенный буквой A (RGBA). Сочетание этих компонентов различной интенсивности дает нам широкий спектр цветов, которые мы видим на цифровых изображениях.

Искусство ASCII не имеет дело с цветами в традиционном смысле. Вместо этого он представляет изображения с использованием символов, которые имеют разный визуальный вес или плотность. Здесь в игру вступает концепция оттенков серого. Изображение в оттенках серого — это изображение, в котором компоненты RGB каждого пикселя имеют одинаковое значение, что приводит к различным оттенкам серого. Значение преобразования изображения в оттенки серого для изображений ASCII заключается в упрощении представления. Уменьшая яркость изображения до его яркости, мы можем затем сопоставить различные оттенки серого с конкретными символами ASCII и создать изображение ASCII на основе изображения.

В нашем коде интерфейс IImageSource служит абстракцией для источника изображения. Он предоставляет свойства для получения ширины, высоты и соотношения сторон изображения, а также метод для получения цвета определенного пикселя. Класс ImageSharpImageSource представляет собой реализацию этого интерфейса с использованием библиотеки ImageSharp. Как мы видели, он оборачивает изображение ImageSharp и предоставляет нашей программе необходимые данные для создания изображений ASCII.

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

Класс генератора: ключ к созданию изображений ASCII

В классе Generator происходит волшебство. Он отвечает за преобразование нашего изображения в произведение искусства ASCII. Давайте углубимся в его основной метод: GenerateAsciiArtFromImage.

var asciiChars = "@%#*+=-:,. ";

Эта строка определяет нашу палитру символов ASCII. Эти символы выбираются на основе их визуальной плотности: @ — самая плотная, а пробел ( ) — наименее плотная. Вы можете настроить этот список, чтобы он имел различный внешний вид, а также добавить больше или удалить некоторые символы, чтобы изменить степень детализации используемой штриховки.

var aspect = image.Width / (double)image.Height;
        var outputWidth = image.Width / 16;
        var widthStep = image.Width / outputWidth;
        var outputHeight = (int)(outputWidth / aspect);
        var heightStep = image.Height / outputHeight;

Этот код на самом деле неполный, но это хорошая возможность подумать об улучшениях. Целью этого блока является работа над получением правильного выходного разрешения изображения и рассмотрение того, как его необходимо масштабировать. Было бы идеально, если бы это можно было настроить, чтобы не было никаких магических чисел!

Одна важная деталь, которую мы используем при циклическом обходе каждого пикселя изображения, заключается в том, что мы начинаем с верхнего левого угла, а затем проходим всю строку, прежде чем перейти к следующей строке. Это связано с тем, что гораздо проще вывести строку на консоль, чем печатать столбец за столбцом. Перебирая пиксели изображения, нам нужно определить, какой символ ASCII лучше всего представляет цвет конкретного пикселя. Для этого мы сначала преобразуем цвет пикселя в значение оттенков серого:

var pixelColor = image.GetPixel(w, h);
var grayValue = (int)(pixelColor.Red * 0.3 + pixelColor.Green * 0.59 + pixelColor.Blue * 0.11);

Эту формулу можно изменить, чтобы получить значение в оттенках серого, но текущие магические числа здесь подчеркивают зеленый компонент из-за чувствительности человеческого глаза к нему. Имея значение оттенков серого, мы сопоставляем его с одним из наших символов ASCII:

var asciiChar = asciiChars[grayValue * (asciiChars.Length - 1) / 255];

Такое сопоставление гарантирует, что более темные пиксели будут представлены более плотными символами ASCII, а более светлые пиксели — менее плотными символами. Полученный символ затем добавляется к нашему художественному представлению ASCII.

Переход на кроссплатформенность: ImageSharp для создания изображений ASCII

Если вам понравилась эта статья, вы можете просмотреть полную публикацию, чтобы узнать, как заставить это работать в Linux И как вы можете расширить этот генератор изображений ASCII! Большое спасибо за вашу поддержку и рассмотрите возможность подписаться на мою еженедельную рассылку новостей по электронной почте, чтобы каждые выходные получать подобный контент на свой почтовый ящик!