20242317 2024-2025-2 《Python程序设计》实验四报告

20242317 2024-2025-2 《Python程序设计》实验四报告

课程:《Python程序设计》
班级:2423
姓名:林楚皓
学号:20242317
实验教师:王志强
实验日期:2025年5月14日
必修/选修:公选课

前言

这几天有我的不少好朋友陆续过生日,通过观察他们生日祝福的朋友圈,发现他们的朋友圈文案喜欢用符号组成蛋糕的形状(类似于颜文字),例如这样:

这给我提供了一个实验思路。是否可以通过特定的方法将字符组成特定的文字、照片或者图画?我通过百度了解到一些关于ASCII Arts(ASCII文本图形艺术)的简单介绍。

ASCII艺术是一种通过组合ASCII字符(如字母、数字、标点符号等)创造图像或图案的艺术形式,起源于早期计算机终端的文本界面时代,以字符的形状、密度和排列模拟图像的明暗、轮廓和细节,具有像素化逻辑和视觉抽象的特点,其创作方式包括手工绘制和工具生成,曾在BBS和早期互联网社区流行,如今作为复古元素应用于现代设计、编程界面等场景,还衍生出Unicode艺术等扩展形式,是技术历史与创意表达结合的独特数字艺术。

经过进一步了解,目前ASCII Arts有两种实现方式,即基于图像灰度的ASCII图像生成方法(Tone-based Method)、基于字符本身特征的ASCII图像生成方法(Structure-based Method)。其中,基于图像灰度的方法比较普适通用,且与基于字符本身特征的方法相比相对更易实现。

说干就干!Go go go 出发咯~
(PS:报告正文按照公文格式确定字体并排版)

  一、实验内容

  (一)实验目标

  本次实验旨在实现一个功能丰富、灵活性高的图像转ASCII字符画程序,通过对图像进行灰度处理、分块计算和字符映射,将用户输入的图片转换为由ASCII文字图形艺术。程序支持多种用户自定义选项,包括灰度级别、字体比例、ASCII文本列数(生成的ASCII文本图像分辨率)、亮度反转、自定义字符映射等,以满足不同用户的需求和创意。

  (二)实验步骤

  1. 定义两个不同级别的灰度字符集,用于后续图像到字符的映射;
  2. 读取输入图像并将其转换为灰度图像;
  3. 将图像分割为若干小块,计算每块的平均灰度值;
  4. 根据每块的平均灰度值,选择对应的字符进行映射;
  5. 将映射后的字符按行列排列,生成最终的ASCII字符画;
  6. 定义命令行选项,支持用户自定义输出文件、长宽拉伸比例、列数、灰度级别、亮度反转和自定义字符映射等;
  7. 将生成的ASCII字符画保存到指定的文本文件中。

  二、实验过程及结果

  (一)实验过程

  1. 安装必要的库

  本次实验需要用到Pillow库来读取图像,还将使用numpy库中的函数来计算图像中亮度的平均值。现在需要安装Pillow和numpy库。


  2. 灰度字符集定义

  在图像处理中,灰度值是描述像素亮度的指标,范围通常从0(黑色)到255(白色)。为了将图像转换为ASCII字符画,需要定义一组字符使之能够从视觉上反映不同的灰度级别。通常,密度较高的字符(如“@”、“#”)用于表示较暗的区域,而密度较低的字符[如“.”、“ ”(空格)]用于表示较亮的区域。

# 默认灰度字符集
# 70级灰度字符集(从深到浅)
gscale1 = r"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
# 10级灰度字符集(简单版)
gscale2 = '@%#*+=-:. '

  这里定义了两个灰度字符集:

  (1)gscale1包含70个字符,提供了更精细的灰度级别表示;
  (2)gscale2包含10个字符,提供了较粗略的灰度级别表示。

  在这一步骤,用户可以通过--map参数指定自定义字符梯度(见步骤7),例如@$%^.,程序会根据用户提供的字符集进行灰度映射,增加了创作的灵活性。

  3. 图像读取与预处理

  在处理图像之前,我们需要先读取图像并将其转换为灰度图像。灰度图像只包含亮度信息,不包含颜色信息,这简化了后续的处理过程。

# 打开图片并转成灰度图
image = Image.open(fileName).convert('L')
W, H = image.size  # 获取宽高
print("原图尺寸: %d x %d" % (W, H))

  这部分代码使用PIL库的Image.open()函数打开图像文件,并使用convert('L')方法将图像转换为灰度模式,然后获取图像的宽度和高度并打印出来,方便用户了解图像的基本信息。

  4. 图像分块与灰度计算(支持亮度反转)

  为了将图像转换为ASCII字符画,需要将图像分割成若干小块,每个小块对应最终字符画中的一个字符。然后计算每个小块的平均灰度值,作为该区域亮度的代表值。并且,该函数还支持亮度反转功能,允许用户使用--invert命令行(见步骤7)将生成的ASCII字符画进行黑白反转。

def getAverageL(image, invert=False):
    """
    计算图片块的平均灰度值
    invert: 是否反转亮度值
    """
    im = np.array(image)  # 转成numpy数组
    w, h = im.shape  # 获取宽高
    avg = np.average(im.reshape(w * h))  # 计算平均值
    if invert:
        avg = 255 - avg
    return avg

  getAverageL()函数接收一个图像块作为输入,首先将其转换为numpy数组以便进行高效的数值计算,然后将二维数组重塑为一维数组,并使用np.average()函数计算所有像素值的平均值,得到该图像块的平均灰度值。当invert参数为True时,平均灰度值会被反转(255-avg)。

  5. 字符映射(支持自定义梯度)

  根据每个图像块的平均灰度值选择合适的字符来表示该区域的亮度:灰度值越高(越亮),选择的字符越稀疏;灰度值越低(越暗),选择的字符越密集。同时,支持使用自定义字符梯度进行映射。

# 使用自定义字符映射(如果有)
if custom_map:
    gradient = custom_map
else:
    gradient = gscale1 if moreLevels else gscale2

# ...

# 根据灰度值选择对应字符
gsval = gradient[int((avg * (len(gradient)-1)) / 255)]

  这里根据用户是否选择使用更精细的灰度级别(--moreLevels参数),选择不同的字符集进行映射。对于70级灰度字符集,将256个灰度级别(0~255)映射到70个字符上;对于10级灰度字符集,则映射到10个字符上;如果是自定义字符映射,则具体映射公式为int((avg * (n-1)) / 255),其中avg是平均灰度值,n是字符集的长度。

  6. 字符画生成

  将图像分割成小块并计算每个小块的对应字符后,需要将这些字符按行列排列,生成最终的ASCII字符画。

# 逐行处理图片
for j in range(rows):
    y1 = int(j * h)  # 当前行开始位置
    y2 = int((j + 1) * h)  # 当前行结束位置

    # 最后一行特殊处理
    if j == rows - 1:
        y2 = H

    aimg.append("")  # 添加新行

    # 处理当前行的每一列
    for i in range(cols):
        x1 = int(i * w)  # 当前列开始位置
        x2 = int((i + 1) * w)  # 当前列结束位置

        # 最后一列特殊处理
        if i == cols - 1:
            x2 = W

        # 裁剪出当前字符对应的图片块
        img = image.crop((x1, y1, x2, y2))
        avg = int(getAverageL(img, invert))  # 计算平均灰度(可能反转)

        # 根据灰度值选择对应字符
        gsval = gradient[int((avg * (len(gradient)-1)) / 255)]
        aimg[j] += gsval  # 把字符添加到当前行

return aimg  # 返回ASCII艺术结果

  这段代码通过双重循环遍历图像的每一行和每一列,对于每个字符位置,计算其对应的图像区域坐标,裁剪出该区域的图像块,计算其平均灰度值,并映射到对应的字符。最后将所有字符按行组合成一个列表返回。

  7. 为程序定义命令行

  为了使程序更加灵活易用,可以通过定义一些命令行参数解析功能,允许用户自定义输入文件、输出文件、缩放比例、列数和灰度级别等参数。

# 添加参数选项
parser.add_argument('--file', dest='imgFile', required=True, help="输入图片路径")
parser.add_argument('--out', dest='outFile', required=True, help="输出文件路径")
parser.add_argument('--scale', dest='scale', required=False, help="长宽拉伸比例(默认0.43)")
parser.add_argument('--cols', dest='cols', required=False, help="列数(默认80)")
parser.add_argument('--morelevels', dest='moreLevels', action='store_true', help="使用更多灰度级别")
parser.add_argument('--invert', dest='invert', action='store_true', help="反转亮度值(黑变白,白变黑)")
parser.add_argument('--map', dest='custom_map', required=False, help="自定义字符梯度,如 \"@$%^`.\"")

args = parser.parse_args()  # 解析参数

  这里使用了内置的argparse类来创建命令行参数解析器,定义了七个参数:

  (1)--file:输入图片路径(必需);
  (2)--out:输出文件路径(必需);
  (3)--scale:ASCII字符画长宽拉伸比例(可选,默认0.43,适配等宽度的Courier字体);
  (4)--cols:列数(可选,默认80);
  (5)--morelevels:是否使用更精细的灰度级别(可选,标志参数);
  (6)--invert:反转亮度值,使黑色区域变为白色,白色区域变为黑色(可选);
  (7)--map:允许用户指定自定义字符梯度,用于灰度到字符的映射(可选,后接灰度由深到浅的自定义梯度字符串,如@$%^~.)。

  8. 结果输出

  最后,将生成的ASCII字符画保存到指定的文本文件中。

# 保存结果到文件
with open(outFile, 'w') as f:
    for row in aimg:
        f.write(row + '\n')
print("转换完成!结果已保存到 %s" % outFile)

  这段代码使用Python的文件操作功能,将生成的ASCII字符画逐行写入到指定的文本文件中,并在完成后打印提示信息。

  9. 完整代码展示

import sys, random, argparse
import numpy as np
import math
from PIL import Image

# 默认灰度字符集
gscale1 = r"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
gscale2 = '@%#*+=-:. '

def getAverageL(image, invert=False):
    """
    计算图片块的平均灰度值
    invert: 是否反转亮度值
    """
    im = np.array(image)  # 转成numpy数组
    w, h = im.shape  # 获取宽高
    avg = np.average(im.reshape(w * h))  # 计算平均值
    if invert:
        avg = 255 - avg
    return avg

def covertImageToAscii(fileName, cols, scale, moreLevels, invert=False, custom_map=None):
    """
    把图片转换成ASCII字符画
    参数:文件名、列数、缩放比例、是否使用更多灰度级别、是否反转亮度、自定义字符映射
    """
    global gscale1, gscale2

    # 使用自定义字符映射(如果有)
    if custom_map:
        gradient = custom_map
    else:
        gradient = gscale1 if moreLevels else gscale2

    # 打开图片并转成灰度图
    image = Image.open(fileName).convert('L')
    W, H = image.size  # 获取宽高
    print("原图尺寸: %d x %d" % (W, H))

    # 计算每个字符块的宽高
    w = W / cols  # 字符块宽度
    h = w / scale  # 字符块高度(考虑字体长宽比)
    rows = int(H / h)  # 计算行数

    print("列数: %d, 行数: %d" % (cols, rows))
    print("每个字符块代表: %d x %d 像素" % (w, h))

    # 检查图片是否太小
    if cols > W or rows > H:
        print("图片太小了,请减少列数!")
        exit(0)

    aimg = []  # 存储ASCII艺术结果的列表

    # 逐行处理图片
    for j in range(rows):
        y1 = int(j * h)  # 当前行开始位置
        y2 = int((j + 1) * h)  # 当前行结束位置

        # 最后一行特殊处理
        if j == rows - 1:
            y2 = H

        aimg.append("")  # 添加新行

        # 处理当前行的每一列
        for i in range(cols):
            x1 = int(i * w)  # 当前列开始位置
            x2 = int((i + 1) * w)  # 当前列结束位置

            # 最后一列特殊处理
            if i == cols - 1:
                x2 = W

            # 裁剪出当前字符对应的图片块
            img = image.crop((x1, y1, x2, y2))
            avg = int(getAverageL(img, invert))  # 计算平均灰度(可能反转)

            # 根据灰度值选择对应字符
            gsval = gradient[int((avg * (len(gradient)-1)) / 255)]
            aimg[j] += gsval  # 把字符添加到当前行

    return aimg  # 返回ASCII艺术结果

def main():
    # 设置命令行参数
    descStr = "图片转ASCII艺术工具"
    parser = argparse.ArgumentParser(description=descStr)

    # 添加参数选项
    parser.add_argument('--file', dest='imgFile', required=True, help="输入图片路径")
    parser.add_argument('--out', dest='outFile', required=True, help="输出文件路径")
    parser.add_argument('--scale', dest='scale', required=False, help="缩放比例(默认0.43)")
    parser.add_argument('--cols', dest='cols', required=False, help="列数(默认80)")
    parser.add_argument('--morelevels', dest='moreLevels', action='store_true', help="使用更多灰度级别")
    parser.add_argument('--invert', dest='invert', action='store_true', help="反转亮度值(黑变白,白变黑)")
    parser.add_argument('--map', dest='custom_map', required=False, help="自定义字符梯度,如 \"@$%^`.\"")

    args = parser.parse_args()  # 解析参数

    # 设置输出文件路径
    outFile = 'out.txt'
    if args.outFile:
        outFile = args.outFile

    # 设置缩放比例(默认0.43适合等宽字体,如Courier字体)
    scale = 0.43
    if args.scale:
        scale = float(args.scale)

    # 设置列数(默认80列)
    cols = 80
    if args.cols:
        cols = int(args.cols)

    # 处理自定义字符映射
    custom_map = None
    if args.custom_map:
        custom_map = args.custom_map
        print(f"使用自定义字符梯度: {custom_map}")

    print('正在生成ASCII艺术...')
    # 转换图片为ASCII
    aimg = covertImageToAscii(args.imgFile, cols, scale, args.moreLevels, args.invert, custom_map)

    # 保存结果到文件
    with open(outFile, 'w') as f:
        for row in aimg:
            f.write(row + '\n')
    print("转换完成!结果已保存到 %s" % outFile)

if __name__ == '__main__':
    main()

  10. 将代码托管至Gitee

  实验四:Python综合实践——ASCII文字图形艺术

  (三)实验结果

  通过cmd打开命令行控制窗口,输入python ASCII进阶.py --file image.jpg --out test.txt (--scale 0.5 --cols 100 --morelevels --invert --map "@#$%^") 运行程序(括号中的命令行为可选项),程序会输出ASCII字符画并保存到指定txt文件中。以下给出几个运行结果:

  1. 学校校徽

  (1)正常灰度模式

  程序执行截图:

  生成ASCII字符画的效果如下:

  (2)黑白反转模式

  程序执行截图:

  生成ASCII字符画的效果如下:

  2. 学校南门照片

  程序执行截图(使用70级灰度值):

  生成ASCII字符画的效果如下:

  3. 文字祝福语

  (1)正常灰度模式(使用70级灰度值)

  程序执行截图:

  生成ASCII字符画的效果如下:

  (2)黑白反转模式

  程序执行截图:

  生成ASCII字符画的效果如下:

  (3)用自定义字符梯度创建ASCII图形

  此处自定义字符梯度为50的字符串$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"`'.

  程序执行截图:

  生成ASCII字符画的效果如下:

  (4)调整ASCII字符画长宽拉伸比例(使用0.7比例对生成的文本图像纵向拉伸)

  程序执行截图:

  生成ASCII字符画的效果如下:

  三、实验过程中遇到的问题和解决过程

  问题1:生成的ASCII文本图像扭曲变形。

  问题1解决方案:我发现在文本文件中默认显示的字体“微软雅黑”不是等宽的字体,所以每一行的字符数量虽然相同但视觉长度不相等,导致扭曲。所以,我将其调整为每个字符都等宽的courier字体,最终显示正常。

  问题2:当用户输入无效的命令行参数时,程序会抛出异常并终止,并不会指出哪个地方输入错误。

  问题2解决方案:使用argparse类内置的错误处理功能,增强了命令行参数的错误处理。当用户输入无效参数时,会给出错误提示信息并指导用户正确使用命令行。

  问题3:在定义默认灰度梯度时,此行代码gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "导致程序报错。

SyntaxWarning: invalid escape sequence '\|'
gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "

  问题3解决方案:我询问AI得知:在gscale1字符串中,\|被当作转义序列处理,但\|不是有效的转义序列。需要在字符串前加r表示原始字符串,或者对反斜杠进行转义。修改后的代码为gscale1 = r"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "。

  四、感悟与思考

  1. 作为团学组织中的一员,为体现“规范性”,我尝试用公文格式将报告正文进行排版(大标题用“方正小标宋简体”,一级标题用“黑体”,二级标题用“楷体”,三级标题用“仿宋”加粗,正文用“仿宋”)。在这过程中我也顺便学了许多Markdown的语法,例如:

  (1)字体设置
  <font face="字体名称">文本</font>
  (2)文本大小设置
  <font size=字号大小>文本</font>
  (3)文本颜色设置
  <font color=颜色名称>文本</font>
  (4)段首缩进两格
  &emsp;&emsp;
  (5)文字居中
  <center>文本</center>

  2. 本次实验是利用了不同字符的视觉灰度不同,而不同字符按照一定规律组合后就可以表示图像中的黑白明暗(例如,商场中的电子大屏近看就是由一个个小LED灯组成的,而远看就拼凑出完整的图像并且能让我们的视觉感知,这和本实验的原理类似)。并且,实验中发现,单纯依赖灰度值映射字符有时会导致细节丢失(如dky南门照片细节较多,生成的作品效果较差),这是因为字符的本身具有“形状”。通过引入自定义映射,我们可以选择合理的字符来弥补单纯靠灰度级别来描绘图像的不足,甚至创造出艺术性的视觉效果(例如,用*和+替代部分中间灰度字符,可使机械类图像呈现齿轮般的工业风格)。

  3. 实验中,我初次接触到如何定义命令行选项。通过检索相关资料,我了解到:argparse库是一个用于解析命令行参数的标准库,能够轻松创建用户友好的命令行接口。使用argparse定义命令行参数的步骤如下:

  (1)创建解析器:argparse.ArgumentParser(description="程序描述")
  (2)添加参数:parser.add_argument()
  (3)解析参数:args = parser.parse_args()

  本实验中,我定义了--file、--out、--scale、--cols、--morelevels、--invert和--map七个参数,便于设置不同的参数以满足不同的生成需求,让程序更具交互性和灵活性。

  五、结课总结

  (一)感想与体会

  非常开心能遇见王志强老师,也非常开心能在大学期间与Python相遇。我想用三个词评价志强老师:超耐心、超专业、超体贴!还记得第一次进行课前手势签到的我慌得手忙脚乱,死活看不懂手势路径到底从哪儿开始。但从那以后,每周三的手势签到都成了固定节目,当签到界面变成绿色时,一场Python的全新探索之旅就此开始。虽然只有短短12次课,每周只和志强老师和同学们见一次面,但周三晚上始终丰富且充实。Python是我的启蒙语言(小学初次接触编程就学Python),但只学到最基础的语法,皮毛而已,并未领略到其强大的能量所在。所幸在大学期间遇到了循循善诱的志强老师,让我明白“人生苦短,我学Python”的真正含义。学习通里的资料区如宝库一般,各种丰富的PPT、讲义、动画和视频,让我更容易对所学内容进行理解和巩固,也激发了我学习Python的兴趣。课下空闲时间我也时不时翻阅《零基础学Python》《Python编程——从入门到实践》等参考书,借助AI工具和学习通资料学习新函数和新用法。《Python程序设计》这门课内容非常干、非常实用,志强老师作为领航员,把我们引进Python的神圣殿堂,让我真正体会到Python功能之强大,也激发了我探索未知领域的渴望和动力。

  (二)意见与建议

  十二周共十二节课,虽然每节课上两个半小时左右,但总时长还是相对较短,授课内容很难做到面面俱到又细致入微。建议加长课程总学时并增加实践环节,让同学们都能对所学知识进行及时巩固。

  最后,我想对王老师和《Python程序设计》说:

一路繁花,感恩遇见,感谢有你!

~完结撒花~

  参考资料

  [1] 《零基础学Python(全彩版)》
  [2] 《Python编程——从入门到实践(第三版)》
  [3] ASCII Art——使用纯文本来显示图像 - CSDN博客
  [4] Python内置模块之argparse详细功能介绍及示例 - CSDN博客
  [5] 【Markdown语法】字体颜色大小及文字底色设置 - CSDN博客
  [6] Markdown中图片对齐方式及尺寸设置 - CSDN博客
  [7] Markdown编辑器使用 - CSDN博客

posted @ 2025-06-04 21:24  Howard夏日清风  阅读(176)  评论(0)    收藏  举报