65.9K
CodeProject 正在变化。 阅读更多。
Home

生成ASCII艺术 – C# 简单教程

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2023 年 8 月 25 日

CPOL

10分钟阅读

viewsIcon

24738

使用你自己的C#程序将图片转换为ASCII艺术

ASCII艺术,一种使用ASCII标准中的字符来创建视觉效果的技术,已经存在于计算世界数十年。这是一种无需传统图形即可表示图像的迷人方式。对于编程新手来说,构建一个生成ASCII艺术的程序可以作为一种富有洞察力的入门。

在本指南中,我们将介绍一种C#方法,用于转换标准图像并从中生成ASCII艺术。你不仅将获得一个可以生成ASCII艺术的C#应用程序的完整源代码,我还会解释为什么像这样的简单程序对于帮助你学习至关重要。

在我提供生成ASCII艺术的代码之前

我意识到你们中的许多人只是想直接看代码。我理解!但正因如此,我特别想在开始之前放一条重要信息,尤其是对于那些更初级开发者

很多时候,初级程序员在学习的早期阶段感到停滞不前,因为他们不知道如何分配时间。他们尝试阅读书籍、文章和博客(就像这篇!)来学习理论,或者观看视频并寻找最好的训练营,以获得最大的成功机会。我经常提醒我的听众,我认为构建事物并实际编写代码是学习的绝对最佳方式之一

在我们一起研究代码时,我希望你牢记这一点!在文章的最后,我将提出一些你可能想考虑的变体和增强功能。我包含此列表不仅仅是因为我认为它很酷,而是为了激发你的创造力!思考你作为开发人员想要关注的不同事物,并看看你是否可以将它们融入到你的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方法内部,我们稍后会看它。

如果你们不熟悉using声明语法,我想快速说明一下。在C# 8中,我们获得了这项语言特性,它允许我们简化using语句。以前,显式的using块使用花括号来表示作用域。有了这项新的语言特性,当变量超出作用域时,它就相当于using作用域的结束。虽然隐式并不总是那么明显,但这里可读性的一个额外好处是,我们不会有过度嵌套的代码。

ImageSharp库用于加载图像,然后将其克隆为一种格式(Rgba32),允许我们处理单个像素颜色。ImageSharpImageSource类充当ImageSharp库和我们的ASCII生成逻辑之间的桥梁。当我们查看这个类的代码时,我们将能够看到索引器方法,它允许我们获取XY坐标的像素数据。

接下来我们看看图像源的实现

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)。你还可能在这个组合中看到第四个分量,即“alpha”(或透明度),用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艺术

ImageSharp是.NET生态系统中一个强大、开源的库,提供图像处理功能。它功能多样、高效,并支持多种图像格式。要生成ASCII艺术,我们需要一种读取和操作图像的方法,而ImageSharp完美契合要求。

由于我最初是使用System.Drawing开始这个项目,我想说明我们可以互换使用ImageSharpSystem.Drawing来生成ASCII艺术。通过这样做,我可以将核心逻辑集中到一个地方,并抽象对特定于图像的代码的访问。为什么要这样做?嗯,如果我们将来决定切换到另一个图像处理库怎么办?为了保持模块化和可维护性,我们引入了一个抽象层:IImageSource接口。

ImageSharpImageSource类充当ImageSharp和我们的生成器之间的桥梁。它实现了IImageSource接口,包装了一个ImageSharp图像。

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

GetPixel方法尤其重要。它检索图像中特定像素的RGB值。此方法对我们的生成器至关重要,因为它使用这些RGB值来确定灰度值,进而确定该像素的适当ASCII字符。我们想使用的任何图像库都需要某种方式让我们访问特定像素,这似乎是一个合理的预期功能。

生成ASCII艺术:项目增强

为了生成ASCII艺术,我们打下了坚实的基础,但总有改进和创新的空间。重申一下,像这样的项目对学习来说非常棒,因为你改进的方向可以让您选择如何集中精力学习。以下是一些将此项目提升到新水平的建议:

1. 完善ASCII艺术生成

不同的图像可以从不同的ASCII字符集中受益。尝试各种字符集,看看哪些字符集能为不同类型的图像产生最佳效果。这会不会是允许用户配置的东西?

图像大小和缩放呢?我们能将输出调整到特定尺寸吗?保持纵横比呢,还是不保持?这些都可以真正增强生成器的可用性!

2. 添加颜色

虽然传统的ASCII艺术是单色的,但没有理由不能引入颜色。通过将像素颜色映射到终端或HTML颜色代码,您可以生成鲜艳、多彩的ASCII艺术。即使在Windows的C#控制台中,我们也有一些基本的颜色可供使用,所以完全重新设计现有代码之前,您可以修改代码以生成ASCII艺术,使其返回像素颜色,并让某些东西将其映射到最接近的控制台颜色!

3. 性能优化

对于较大的图像,生成过程可能有点慢。深入性能分析,看看是否有可以解决的瓶颈,也许通过优化循环或利用并行处理。你甚至可能可以使用比StringBuilder更有效的东西……即使我们回顾C#数据类型的基础知识,我们也许可以选择更高效的东西!

4. Web应用程序或GUI集成

将这个基于控制台的工具变成一个更用户友好的应用程序。想象一个Web应用程序,用户可以上传图片并立即看到它们的ASCII艺术表示。或者一个带有GUI的桌面应用程序,允许用户实时调整设置。您可以通过摆脱传统的控制台应用程序来探索很多选项!

结论

事实证明,0到255范围内的字符可以教会我们很多东西,当我们将其转化为一个可以生成ASCII艺术的程序时!虽然该项目表面上看起来很简单,但它提供了深入研究各种编程概念的机会,从图像处理到算法优化。通过一些可选的增强功能,您可以真正地让它大放异彩并学到一些很棒的新知识!

对于那些一直跟随到这里的人,我希望你们不仅获得了创建ASCII艺术的知识,还对即使是简单的项目也能提供的学习机会有了欣赏。真正的价值不仅在于最终产品,还在于解决问题、实验和迭代的过程。

我鼓励您获取这段代码,进行修改,并使其成为您自己的。尝试不同的功能,进一步优化代码,或将其集成到更大的项目中。请记住,您编写的每一行代码,以及您克服的每一个挑战,都在您作为开发者的成长中增添了价值。有时,最简单的项目反而提供了最深刻的教训!

© . All rights reserved.