GIF转字符画

news2024/11/15 23:39:39

前言

上一次我们实现了静态图片转字符画:

https://blog.csdn.net/weixin_54143563/article/details/139778645

由此我们不禁思考,对于动态的gif应该怎么转换呢?

思路

在网上我学习到了一种思路:

1.创建中间的临时文件夹tmp,用来存放gif每一帧的画面。

2。上一步保存的每一帧画面拿出来进行字符画的处理,在继续保存到tmp中,用字符画覆盖原来第一步保存的画面。

3.将tmp中的字符画合成为gif。

其中第2步的转化字符画的核心函数为:

# 将图片处理成字符画
def img2ascii(img, ascii_chars, isgray, font, scale1):
    scale = scale1
    # 将图片转换为 RGB 模式
    im = Image.open(img).convert('RGB')
    # 设定处理后的字符画大小
    raw_width = int(im.width * scale)
    raw_height = int(im.height * scale)
    # 获取设定的字体的尺寸
    x0, y0, x1, y1 = font.getbbox(' ')

    font_x, font_y = x1-x0, y1
    # 确定单元的大小
    block_x = int(font_x * scale)
    block_y = int(font_y * scale)

    # 确定长宽各有几个单元
    w = int(raw_width/block_x)
    h = int(raw_height/block_y)
    # 将每个单元缩小为一个像素
    im = im.resize((w, h), Image.NEAREST)
    # txts 和 colors 分别存储对应块的 ASCII 字符和 RGB 值
    txts = []
    colors = []
    for i in range(h):
        line = ''
        lineColor = []
        for j in range(w):
            pixel = im.getpixel((j, i))
            lineColor.append((pixel[0], pixel[1], pixel[2]))
            line += get_char(ascii_chars, pixel[0], pixel[1], pixel[2])
        txts.append(line)
        colors.append(lineColor)
    # 创建新画布
    img_txt = Image.new('RGB', (raw_width, raw_height), (255, 255, 255))
    # 创建 ImageDraw 对象以写入 ASCII
    draw = ImageDraw.Draw(img_txt)
    for j in range(len(txts)):
        for i in range(len(txts[0])):
            if isgray:
                draw.text((i * block_x, j * block_y), txts[j][i], (119,136,153))
            else:
                draw.text((i * block_x, j * block_y), txts[j][i], colors[j][i])
    img_txt.save(img)

scale为缩放的比例,这里一般选择1即按照原来的尺寸,那么我们就可以先忽略这个参数了。

然后将图片转为RGB的模式,获取图像的大小。

接着获取字体尺寸的大小,字体文件选用的是Courier-New.ttf文件:

文件分享

由block_x和block_y分别表示字体单元的长和宽。

w和h确定了原图像中含有的单元数量

那么这里说的将每个单元缩小为一个像素应该如何理解呢?

因为w和h确定了原图像中含有的单元数量,现在将im按照最近邻算法缩小为w和h。

所以这个新缩小的图像每一个像素实际上表示的为一个单元。

简单理解就是将原图像划分为许多方格:

然后,我们将上面每一个方格看作一个元素,来创建一个新的图像im。

接着我们处理每一个单元即可。

get_char函数内容如下:

# 将不同的灰度值映射为 ASCII 字符
def get_char(ascii_chars, r, g, b):
    length = len(ascii_chars)
    gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
    return ascii_chars[int(gray/(256/length))]

因此该函数实际为按原图像像素值选取ASCII字符。

最后txts为一个列表,列表中每个元素代表的每一行选取的字符串。

colors存放每个单元块的颜色信息。

下面创建原图像尺寸的画布,但是依然按照方格作为像素来遍历(这样做的好处是可以简化处理步骤)。

但是在画布draw上放置字符时,坐标为(i * block_x, j * block_y),这是将方格又还原为了像素。

将拆分画面帧与处理字符画整合:

# 拆分 gif 将每一帧处理成字符画
def gif2pic(file, ascii_chars, isgray, font, scale):
    '''
    file: gif 文件
    ascii_chars: 灰度值对应的字符串
    isgray: 是否黑白
    font: ImageFont 对象
    scale: 缩放比例
    '''
    im = Image.open(file)
    im.seek(0)
    duration = im.info.get('duration')
    path = os.getcwd()
    if(not os.path.exists(path+"/tmp")):
        os.mkdir(path+"/tmp")
    os.chdir(path+"/tmp")
    # 清空 tmp 目录下内容
    for f in os.listdir(path+"/tmp"):
        os.remove(f)
    try:
        while 1:
            current = im.tell()
            name = file.split('.')[0]+'_tmp_'+str(current)+'.png'
            # 保存每一帧图片
            im.save(name)
            # 将每一帧处理为字符画
            img2ascii(name, ascii_chars, isgray, font, scale)
            # 继续处理下一帧
            im.seek(current+1)
    except:
        os.chdir(path)

    return duration

合成函数如下:

# 拆分 gif 将每一帧处理成字符画
#def gif2pic(file, ascii_chars, isgray, font, scale):
def gif2pic(file):
    '''
    file: gif 文件
    ascii_chars: 灰度值对应的字符串
    isgray: 是否黑白
    font: ImageFont 对象
    scale: 缩放比例
    '''
    file_reader=imageio.get_reader(file)
    gif_frames=[]
    for i,frame in enumerate(file_reader):
        im=Image.fromarray(frame)
        gif_frames.append(im)
    
    file_reader.close()
    
    return gif_frames
    #imageio.mimsave("test003.gif",gif_frames,format='GIF',duration=30,loop=0)

    print('end')

我们看看示例如何:

存在的问题

这么看貌似是实现了目标,但是对于我重新找到的示例:芙芙的表情包,来进行转换效果如下:

感觉有卡顿的现象,这是什么造成的呢?回看了一下生成的tmp中间文件,确确实实抽取了原gif的每一帧呀。

另外,将每一帧转化为字符画后保存,最后才将转化的字符画合成为gif还是有些多此一举,为什么不再每一帧转化为字符画后就写入gif动画中呢?这样我们只用遍历一次gif画面帧即可。为了实现这个步骤,我首先创建一个画面帧的列表,然后每次将转化的字符画添加进列表,最后使用

imageio.mimsave("test002.gif",images,mode='I',format='GIF')

合成gif,不过,执行这行代码总是出现这样的报错:

直接上网搜,别人使用的变量和我们是不一样的,还是不能解燃眉之急呀。

上面两个问题困扰了我很久。

经过多次尝试,我发现了其实上面两个问题是有一定联系的。

为什么会出现卡顿?

因为我们保存画面帧的时候,后缀的区别在与01,02,03·······这样的数字。在功夫熊猫的示例中,gif的帧数不超过10,所以没有卡顿的情况。

但是对于芙芙表情包,还有100帧的前提下,我们读取tmp的方式为:

打印一下读取的文件名:

发现了问题,按照字符大小来读取文件,那么10是小于2的因为我们首先比较第一个不同的字符,1小于2,因此我们并不是按照顺序读取的。

接下来,我们可以单纯提取文件名中的数字字符,然后转为整形,从而按照实际循序读取文件。但是这样显然有增加了许多计算。

那么我们直接在获取每一帧画面转字符画的时候就合成gif就可以避免文件名大小的问题了,这也就是上面我们遇到的第二个要解决的情况。

按理说,既然能够在tmp中保存每一帧的画面,那么直接利用这些画面合成一个新的gif也不应该有问题呀。我在仔细回看原来的代码,发现了这样一个细节:

画面转为RGB,还记得之前webp文件转JPG吗?因为JPG文件没有透明度的通道所以要先转为RGB,所以上面出现输入shape不匹配也是因为多了一个透明度的通道,那么我们只需要在获取每一帧后先转一下RGB再次进行后续的操作就没有问题了:

最终效果展示:

可见转化后也很丝滑了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1859641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web前端期末大作业--绿色自适应医疗健康医院网页设计(HTML+CSS+JavaScript+)实现

Toggle navigation Hospital 首页 关于我们医疗动态医疗资源联系我们 我们的服务 心脏监测 XXX的通道有许多变化,但大多数人以某种形式遭受了改变,通过注射幽默 康复治疗 XXX的通道有许多变化,但大多数人以某种形式遭受了改变,通…

Java技术栈总结:数据库MySQL篇

一、慢查询 1、常见情形 聚合查询 多表查询 表数据量过大查询 深度分页查询 2、定位慢查询 方案一、开源工具 调试工具:Arthas运维工具:Prometheus、Skywalking 方案二、MySQL自带慢日志 在MySQL配置文件 /etc/my.conf 中配置: # …

音频傅里叶变换(基于开源kissffs)

主要参考资料: 深入浅出的讲解傅里叶变换(真正的通俗易懂): https://zhuanlan.zhihu.com/p/19763358 推荐开源项目:KISS FFT: https://blog.csdn.net/gitblog_00031/article/details/138840117 数字硅麦数据的处理&…

【Android】实现图片和视频混合轮播(无限循环、视频自动播放)

目录 前言一、实现效果二、具体实现1. 导入依赖2. 布局3. Banner基础配置4. Banner无限循环机制5. 轮播适配器6. 视频播放处理7. 完整源码 总结 前言 我们日常的需求基本上都是图片的轮播,而在一些特殊需求,例如用于展览的的数据大屏,又想展…

使用 Swift 6 语言模式构建 Swift 包

文章目录 前言下载 Swift 6 工具链Swiftenv - macOSSwiftly - Linux在 SPM 中启用语言模式命令行包清单文件输出结论前言 我最近了解到,Swift 6 的一些重大变更(如完整的数据隔离和数据竞争安全检查)将成为 Swift 6 语言模式的一部分,该模式将在 Swift 6 编译器中作为可选…

【征服数据结构】:期末通关秘籍

【征服数据结构】:期末通关秘籍 💘 数据结构的基本概念😈 数据结构的基本概念😈 逻辑结构和存储结构的区别和联系😈 算法及其特性😈 简答题 💘 线性表(链表、单链表)&…

怎么查找企业的经营动态信息?

很多人都会查询企业的经营动态,比如很多投资者会关注企业的财务状况,市场战略,经营决策等信息;职场上也需要了解竞争对手和合作伙伴的相关经营动态,新品发布,技术专利申请等等。还有一些行业研究人员需要了…

STM32单片机WDG看门狗详解

文章目录 1. WDG简介 2. IWDG框图 3. IWDG键寄存器 4. IWDG超时时间 5. WWDG框图 6. WWDG工作特性 7. WWDG超时时间 8. IWDG和WWDG对比 9. 代码示例 1. WDG简介 WDG(Watchdog)看门狗 看门狗可以监控程序的运行状态,当程序因为设计…

钡铼技术BL101串口6路Modbus转MQTT网关加速智慧城市部署

随着物联网技术的飞速发展,如何高效地整合传统设备与现代云端系统,成为了亟待解决的关键问题。钡铼技术,作为物联网领域的硬件设备制造商,近期推出的BL101六路串口Modbus转MQTT网关,正以其独特优势,为智慧城…

LabVIEW在光学与光子学实验室中的应用

光学与光子学实验室致力于光学和光子学前沿领域的研究,涉及超快光学、非线性光学、光纤通信、光子晶体等多个方向。实验室需要高精度的实验控制和数据采集系统,以进行复杂的光学实验,并对实验数据进行实时处理和分析。 项目需求 实时控制与监…

CMDB详解及对企业的作用

CMDB即配置管理数据库(Configuration Management Database),是一种专门用于管理IT资产、配置信息和关系的数据库。CMDB以规划、监控、分析和存档企业的所有IT基础设施和应用程序为目的,成为企业IT管理和运营的重要工具。 CMDB的…

MySQL数据库(二):数据库基本操作

MySQL是一种流行的关系型数据库管理系统,广泛用于Web应用和各种数据存储需求。通过本次介绍,您将学习如何进行MySQL数据库的基本操作,包括创建数据库和表、插入和查询数据、更新和删除记录。这些基础知识将为您打下坚实的数据库操作基础。 目…

Hadoop04【集群环境搭建】

1 dfs.secondary.http.address hadoop-node01:50090 4.mapred-site.xml 首先需要将文件名称修改了。原文件名称为mapred-site.xml.template。指定MapReduce的资源调度方式为yarn。 mapreduce.framework.name yarn 5.yarn-site.xml 指定ResourceManager(yarn的老大)的地址和…

Maven编译打包时报“PKIX path building failed”异常

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 方法11.报错信息2.InstallCert.java3.生成证书文件 jssecacerts4.复制 jssecacerts 文件5. 然后重启Jenkins 或者maven即可 方法21.下载证书2. 导入证书执行keytool…

.NET使用原生方法实现文件压缩和解压

前言 在.NET中实现文件或文件目录压缩和解压可以通过多种方式来完成,包括使用原生方法(System.IO.Compression命名空间中的类)和第三方库(如:SharpZipLib、SharpCompress、K4os.Compression.LZ4等)。本文我…

排序算法(C语言版)

前言 排序作为生产环境中常见的需求之一,对整个产品有举足轻重的影响,可以说使用一个合适的排序算法是业务逻辑中比较重要的一部分。今天我们就来介绍常见的排序算法以及实现 排序 所谓排序无非就是按照特定的规则对一组数据就行顺序化。 常见的排序有…

柠檬班车载测试视频课程

这门课程将教授学员如何进行车载测试视频拍摄。学习者将学习如何选择合适的拍摄设备、构思拍摄场景、拍摄技巧和后期制作等内容。课程结合实例演练和个性化指导,帮助学员掌握车载测试视频拍摄的关键技能,提升视频制作能力。无论您是初学者还是有经验者&a…

从移动切换到电信IP:详细介绍两种方法

在当前的互联网环境中,用户可能会因为各种原因需要切换网络服务提供商,比如从移动切换到电信。这种切换不仅涉及到网络服务的变更,还可能意味着IP地址的改变。那么,移动的怎么切换成电信的IP?下面一起来了解一下吧。 方…

React:tabs或标签页自定义右击菜单内容,支持内嵌iframe关闭菜单方案

React:tabs或标签页自定义右击菜单内容,支持内嵌iframe关闭菜单方案 不管是react、vue还是原生js,原理是一样的。 注意如果内嵌iframe情况下,iframe无法使用事件监听,但是可以使用iframe的任何点击行为都会往父级wind…

Python | Leetcode Python题解之第169题多数元素

题目: 题解: class Solution:def majorityElement(self, nums: List[int]) -> int:count 0candidate Nonefor num in nums:if count 0:candidate numcount (1 if num candidate else -1)return candidate