Python-多媒体应用初学者指南-全-
Python 多媒体应用初学者指南(全)
原文:
zh.annas-archive.org/md5/d50538a8544612ae5ab04ee9ad16176c
译者:飞龙
前言
多媒体应用程序被广泛应用于各个领域。编写能够处理图像、视频和其他感官效果的应用程序是非常棒的。并非每个应用程序都能充分利用音频/视觉效果,但一定程度的多媒体可以使任何应用程序都非常吸引人。
本书全部关于使用 Python 进行多媒体处理。这个逐步指南为您提供了开发激动人心的多媒体应用程序的动手经验。您将构建用于处理图像、创建 2D 动画以及处理音频和视频的应用程序。
有许多多媒体库,它们都提供了 Python 绑定。这些库使得处理不同类型的媒体成为可能,例如图像、音频、视频、游戏等等。本书通过几个令人难以置信的激动人心的项目,向读者介绍了一些这些(开源)库。流行的多媒体框架和库,如 GStreamer、Pyglet、QT Phonon 和 Python Imaging 库,被用来开发各种多媒体应用程序。
本书涵盖的内容
第一章,Python 和多媒体,教您一些关于使用 Python 进行多媒体处理流行多媒体框架的知识,并展示了如何使用 PyGame 开发一个简单的交互式应用程序。
第二章,处理图像,解释了使用 Python Imaging Library 进行基本图像转换和操作技术。通过几个示例和代码片段的帮助,我们将对图像进行一些基本操作,例如将图像粘贴到另一张图像上、调整大小、旋转/翻转、裁剪等等。我们将编写工具来捕获屏幕截图并将图像文件在不同格式之间转换。本章以一个激动人心的项目结束,我们将开发一个具有图形用户界面的图像处理应用程序。
第三章,增强图像,描述了如何使用 Python Imaging Library 给图像添加特殊效果。您将学习使用图像过滤器增强数字图像的技术,例如,从图片中减少'噪声'、平滑和锐化图像、浮雕等等。本章将涵盖诸如在图像内部选择性地改变颜色等主题。我们将开发一些令人兴奋的工具,用于混合图像、添加透明效果和创建水印。
第四章,动画乐趣,向您介绍了使用 Python 和 Pyglet 多媒体应用程序开发框架开发动画的基础。我们将进行一些激动人心的项目,例如在雷雨中驾驶一辆有趣的汽车,一个带有键盘控制的“保龄球动画”等等。
第五章,处理音频教你如何掌握 GStreamer 多媒体框架的基础知识,并使用此 API 进行音频和视频处理。在本章中,我们将开发一些简单的音频处理工具用于“日常使用”。我们将开发一些工具,例如命令行音频播放器、文件格式转换器、MP3 切割器和音频录制器。
第六章,音频控制和效果描述了如何开发添加音频效果、混合音频轨道、创建自定义音乐轨道、可视化音频轨道等工具。
第七章,处理视频解释了视频处理的基本原理。本章将涵盖将视频在不同视频格式之间转换、混合或分离音频和视频轨道、将一个或多个视频帧保存为静态图像、执行基本的视频操作,如裁剪、调整大小、调整亮度等主题。
第八章,使用 QT Phonon 的基于 GUI 的媒体播放器将带你了解 QT Phonon 框架的基本组件。我们将使用 QT Phonon 开发基于图形用户界面的音频和视频播放器。
本书面向的对象
想要尝试使用 Python 处理图像、动画、音频和视频处理的 Python 开发者。
习惯用法
在本书中,你会发现几个经常出现的标题。
为了清楚地说明如何完成一个程序或任务,我们使用:
行动时间 - 标题 -
-
动作 1
-
动作 2
-
动作 3
指令通常需要一些额外的解释,以便它们有意义,因此它们后面跟着:
刚才发生了什么?
这个标题解释了你刚刚完成的任务或指令的工作原理。
你也会在书中找到一些其他的学习辅助工具,包括:
快速问答 - 标题
这些是简短的多项选择题,旨在帮助你测试自己的理解。
尝试一下英雄标题
这些设定了实际挑战,并为你提供了对所学知识的实验想法。
你还会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词如下显示:“字典self.addedEffects
跟踪所有音频。”
代码块设置如下:
1 def __init__(self):
2 self.constructPipeline()
3 self.is_playing = False
4 self.connectSignals()
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
1 def constructPipeline(self):
2 self.pipeline = gst.Pipeline()
3 self.filesrc = gst.element_factory_make( 4 "gnlfilesource")
任何命令行输入或输出都如下所示:
>>>import pygst
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“你需要调整效果菜单 UI,并在代码中进行一些其他更改,以跟踪添加的效果。”
注意
警告或重要提示会以这样的框出现。
小贴士
小技巧和窍门会像这样出现。
读者反馈
我们欢迎读者的反馈。告诉我们您对这本书的看法,您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要发送一般反馈,只需将电子邮件发送到< feedback@packtpub.com>
,并在邮件主题中提及书籍标题。
如果有您需要的书籍并且希望我们看到出版,请通过www.packtpub.com上的建议标题表单或通过电子邮件发送到<suggest@packtpub.com>
给我们留言。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有众多方法可以帮助您从购买中获得最大收益。
注意
下载本书的示例代码
您可以从www.PacktPub.com
您购买的所有 Packt 书籍的账户中下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误,可能是文本或代码中的错误,如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从www.packtpub.com/support
查看任何现有勘误。
盗版
互联网上版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过< copyright@packtpub.com>
与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果你在本书的任何方面遇到问题,可以通过< questions@packtpub.com>
联系我们,我们将尽力解决。
第一章。Python 和多媒体
自从 1989 年问世以来,Python 作为一种通用编程语言,越来越受欢迎。它是一种高级、面向对象的编程语言,拥有全面的标准库。该语言的特点,如自动内存管理和易于阅读性,吸引了广泛的开发者社区的注意。通常,与其他一些语言相比,人们可以用 Python 非常快速地开发复杂的应用程序。它被用于多个开源以及商业科学建模和可视化软件包中。它已经在动画和游戏开发工作室等行业中获得了流行,这些行业专注于多媒体应用开发。本书全部关于使用 Python 进行多媒体处理。
在本章的介绍中,我们将:
-
了解多媒体和多媒体处理
-
使用 Python 讨论几个流行的多媒体处理框架
-
使用 PyGame 开发一个简单的交互式应用程序
那么,让我们开始吧。
多媒体
我们在日常生活中使用多媒体应用程序。我们在看电影、听歌或玩视频游戏时处理的就是多媒体。多媒体应用在众多领域都有广泛应用。在广告和娱乐行业中,多媒体扮演着至关重要的角色。最常见的用途之一就是给电影添加音频和视频效果。例如,飞行或驾驶模拟器等教育软件包使用多媒体以交互式的方式教授各种主题。
那么,多媒体究竟是什么呢?一般来说,任何利用不同数字媒体来源的应用程序都被称作数字多媒体。例如,一个视频就是不同来源或内容的组合。这些内容可能包括音频轨道、视频轨道和字幕轨道。当这样的视频播放时,所有这些媒体来源都会一起呈现,以达到预期的效果。
一个多声道音频可以包含背景音乐轨道和歌词轨道。甚至可能包括各种音频效果。通过快速连续显示一系列数字图像,可以创建动画。这些都是多媒体的不同例子。
在计算机或视频游戏的情况下,应用程序增加了一个维度,即用户交互。这通常被称为交互式多媒体。在这里,用户决定了多媒体内容呈现的方式。借助键盘、鼠标、轨迹球、操纵杆等设备,用户可以交互式地控制游戏。
多媒体处理
我们讨论了一些多媒体被广泛使用的应用领域。本书的重点将放在多媒体处理上,通过使用它,将开发各种多媒体应用程序。
图像处理
使用数码相机拍照后,我们经常出于各种原因对原始数码图像进行调整。最常见的原因之一是去除图像上的瑕疵,例如去除“红眼”或如果照片是在光线不足的情况下拍摄的,则增加亮度级别等。这样做另一个原因是添加特殊效果,使图像看起来更令人愉悦。例如,将家庭照片变为黑白,并在照片周围数字添加边框,使其具有怀旧效果。下一张插图显示了增强前后的图像。有时,修改原始图像只是为了让你理解图像所呈现的重要信息。假设图片代表了一个复杂的组件组装。可以通过添加特殊效果来使图像中仅边缘被突出显示。然后,可以使用这些信息来检测,例如,组件之间的干扰。因此,我们进一步对图像进行数字处理,直到得到所需的输出图像。
使用数码相机拍照后,我们经常出于各种原因对原始数码图像进行调整。最常见的原因之一是去除图像上的瑕疵,例如去除“红眼”或如果照片是在光线不足的情况下拍摄的,则增加亮度级别等。这样做另一个原因是添加特殊效果,使图像看起来更令人愉悦。例如,将家庭照片变为黑白,并在照片周围数字添加边框,使其具有怀旧效果。下一张插图显示了增强前后的图像。有时,修改原始图像只是为了让你理解图像所呈现的重要信息。假设图片代表了一个复杂的组件组装。可以通过添加特殊效果来使图像中仅边缘被突出显示。然后,可以使用这些信息来检测,例如,组件之间的干扰。因此,我们进一步对图像进行数字处理,直到得到所需的输出图像。
在以下示例中,我们可以看到如何给图片添加边框以改变其外观:
数字图像处理可以看作是在图像数据上应用各种算法/滤波器。其中一个例子是图像平滑滤波器。图像平滑意味着减少图像中的噪声。图像数据中亮度和颜色级别的随机变化通常被称为图像噪声。平滑算法修改输入图像数据,使得在最终图像中这种噪声被减少。
另一个常见的图像处理操作是混合。正如其名所示,混合意味着将两个兼容的图像混合以创建一个新的图像。通常,使用恒定的 alpha 值对两个输入图像的数据进行插值,以产生最终的图像。下一幅插图显示了两个输入图像和混合后的结果图像。在接下来的章节中,我们将学习几种这样的数字图像处理技术。
桥梁和飞翔的鸟的图片是在不同的地点拍摄的。使用图像处理技术,这两张图片可以被融合在一起,看起来像一张单独的图片:
音频和视频处理
当你在电脑上听音乐时,你的音乐播放器在后台执行几个操作。它处理数字媒体数据,以便将其转换为输出媒体设备(如音频扬声器)所需的可播放格式。媒体数据流经多个相互连接的媒体处理组件,然后到达媒体输出设备或写入的媒体文件。这将在下一幅插图显示。
以下图像显示了媒体数据处理流程:
音频和视频处理包括许多内容。其中一些在本节中简要讨论。在这本书中,我们将学习使用 GStreamer 多媒体框架的 Python 绑定来实现的多种音频-视频处理技术。
压缩
如果你用摄像机录制视频,然后将其传输到电脑上,它将占用大量空间。为了将这些时刻保存在 VCD 或 DVD 上,你几乎总是需要压缩音频-视频数据,以便它占用更少的空间。有两种类型的音频和视频压缩;有损和无损。有损压缩非常常见。在这里,假设一些数据是不必要的,并且不会保留在压缩媒体中。例如,在有损视频压缩中,即使一些原始数据丢失,对视频整体质量的影响也较小。另一方面,在无损压缩中,压缩的音频或视频数据与原始数据完美匹配。然而,压缩比非常低。随着我们的进展,我们将编写音频-视频数据转换实用程序来压缩媒体数据。
混合
混合是一种使用多个媒体源创建复合媒体的方法。在音频混合的情况下,来自不同来源的音频数据被组合成一个或多个音频通道。例如,它可以用来添加音频效果,以便同步单独的音乐和歌词轨道。在接下来的章节中,我们将学习更多关于使用 Python 的媒体混合技术。
编辑
媒体混音可以被视为一种媒体编辑类型。媒体编辑可以大致分为线性编辑和非线性编辑。在线性编辑中,程序员不控制媒体展示的方式。而在非线性编辑中,编辑是交互式进行的。本书将涵盖媒体编辑的基础知识。例如,我们将学习如何通过组合不同音频文件的部分来创建新的音频轨道。
动画
一个动画可以被视为通过依次显示一系列图像帧来创建的运动视觉错觉。这些图像帧中的每一个都与之前显示的略有不同。下一张插图显示了“祖父的钟”的动画帧:
如您所见,时钟动画中有四个图像帧。这些帧快速依次显示,以达到所需的动画效果。每个图像将显示 0.25 秒。因此,它模拟了一秒钟的钟摆振荡。
卡通动画是动画的经典例子。自从 20 世纪初首次亮相以来,动画已经成为一个突出的娱乐行业。本书的重点将放在使用 Python 构建的 2D 卡通动画上。在第四章中,我们将学习一些构建此类动画的技术。创建一个卡通角色并将其“赋予生命”是一项繁重的工作。直到 20 世纪 70 年代末,大多数动画和效果都是没有使用计算机创建的。在当今这个时代,大部分图像创作工作都是数字生成的。最先进的技术使这个过程变得更快。例如,可以应用图像变换来显示或移动图像的一部分,从而避免在下一帧中创建整个卡通图像的需要。
内置的多媒体支持
Python 为应用程序开发提供了一些内置的多媒体模块。我们将简要介绍其中的一些模块。
winsound
winsound
模块在 Windows 平台上可用。它提供了一个接口,可以用于在应用程序中实现基本的音频播放元素。可以通过调用PlaySound(sound, flags)
来播放声音。在这里,参数 sound 用于指定音频文件的路径。如果此参数指定为None
,则停止当前正在播放的音频(如果有)。第二个参数指定要播放的文件是声音文件还是系统声音。以下代码片段展示了如何使用winsound
模块播放波形格式的音频文件。
from winsound import PlaySound, SND_FILENAME
PlaySound("C:/AudioFiles/my_music.wav", SND_FILENAME )
这个函数PlaySound
的第一个参数指定了要播放的声音文件。第二个参数SND_FILENAME
表示第一个参数是一个音频文件。如果标志设置为SND_ALIAS
,则表示第一个参数的值来自注册表中的系统声音。
audioop
此模块用于操作原始音频数据。可以对声音片段执行多个有用的操作。例如,它可以找到声音片段中所有样本的最小值和最大值。
wave
wave
模块提供了一个接口,用于读取和写入WAV
格式的音频文件。以下代码行打开了一个 wav 文件。
import wave
fil = wave.open('horn.wav', 'r')
open
方法的第一参数指定了波文件路径的位置。第二个参数'r'返回一个Wave_read
对象。这是打开音频文件的模式,'r'或'rb'为只读模式,'w'或'wb'为只写模式。
外部多媒体库和框架
有几个开源的多媒体框架可用于多媒体应用程序开发。其中大多数的 Python 绑定都很容易获得。在这里,我们将讨论一些最受欢迎的多媒体框架。在接下来的章节中,我们将使用这些库中的许多来创建一些有用的多媒体应用程序。
Python 图像库
Python 图像库为 Python 提供了图像处理功能。它支持多种图像格式。在本书的后续章节中,将详细讨论使用 PIL 的多种图像处理技术。我们将学习诸如图像格式转换以及使用 Python 图像库的各种图像操作和增强技术。
PyMedia
PyMedia 是一个流行的开源媒体库,支持多种多媒体格式的音频/视频操作。
GStreamer
此框架使多媒体操作成为可能。它是一个可以在此基础上开发多媒体应用程序的框架。它提供的丰富库集使得开发具有复杂音频/视频处理能力的应用程序变得更加容易。GStreamer 是用 C 编程语言编写的,并为包括 Python 在内的其他一些编程语言提供了绑定。许多开源项目使用 GStreamer 框架来开发自己的多媒体应用程序。GStreamer 项目网站上有详细的文档。GStreamer 应用程序开发手册是一个非常好的起点。在此组中,此框架将被广泛用于开发音频和视频应用程序。
Pyglet
对动画和游戏应用感兴趣吗?Pyglet 就在这里帮助你。Pyglet 提供了一个用于使用 Python 开发多媒体应用的 API。它是一个基于 OpenGL 的库,可以在多个平台上运行。它是用于游戏和其他图形密集型应用开发的流行多媒体框架之一。它支持游戏应用开发通常需要的多显示器配置。在本书的后续部分,我们将广泛使用这个 Pyglet 框架来创建动画。
PyGame
PyGame (www.pygame.org)是另一个非常流行的开源框架,它为游戏应用开发需求提供了一个 API。它提供了一套丰富的图形和声音库。本书中我们不会使用 PyGame。但由于它是一个突出的多媒体框架,我们将简要讨论其一些最重要的模块,并给出一个简单的示例。PyGame 网站提供了大量关于使用此框架进行动画和游戏编程的资源。
精灵
Sprite
模块包含几个类;在这些类中,Sprite
和Group
是最重要的。Sprite
是所有可见游戏对象的超类。Group
对象是 Sprite 的几个实例的容器。
显示
如其名所示,Display
模块具有处理显示的功能。它用于创建用于显示 Pygame 窗口的 Surface 实例。该模块的一些重要方法包括flip
和update
。前者用于确保所有绘制的内容都正确地显示在屏幕上。而后者用于你只想更新屏幕的一部分时。
表面
此模块用于显示图像。Surface
的实例代表一个图像。以下代码行创建了一个这样的实例。
surf = pygame.display.set_mode((800,600))
API 方法display.set_mode
用于创建此实例。窗口的宽度和高度作为此方法的参数指定。
绘制
使用Draw
模块,可以在Surface
内渲染多个基本形状。例如包括圆形、矩形、线条等。
事件
这是 PyGame 的另一个重要模块。当用户点击鼠标按钮或按下一个键等事件发生时,我们说发生了事件。事件信息用于指导程序以某种方式执行。
图像
Image
模块用于处理不同文件格式的图像。加载的图像由一个表面表示。
音乐
Pygame.mixer.music
提供了控制播放的便捷方法,如播放、倒放、停止等。
以下是一个简单的程序,它突出了动画和游戏编程的一些基本概念。它展示了如何在应用程序窗口中显示对象,然后交互式地修改它们的位置。我们将使用 PyGame 来完成这个任务。在本书的后续部分,我们将使用不同的多媒体框架 Pyglet 来创建动画。
使用 PyGame 的简单应用程序行动时间
此示例将使用我们刚刚讨论的模块。为了使此应用程序正常工作,您需要安装 PyGame。PyGame 的二进制和源代码分发可以在 PyGame 网站上找到。
-
创建一个新的 Python 源文件,并在其中写入以下代码。
1 import pygame 2 import sys 3 4 pygame.init() 5 bgcolor = (200, 200, 100) 6 surf = pygame.display.set_mode((400,400)) 7 8 circle_color = (0, 255, 255) 9 x, y = 200, 300 10 circle_rad = 50 11 12 pygame.display.set_caption("My Pygame Window") 13 14 while True: 15 for event in pygame.event.get(): 16 if event.type == pygame.QUIT: 17 sys.exit() 18 elif event.type == pygame.KEYDOWN: 19 if event.key == pygame.K_UP: 20 y -= 10 21 elif event.key == pygame.K_DOWN: 22 y += 10 23 elif event.key == pygame.K_RIGHT: 24 x += 10 25 elif event.key == pygame.K_LEFT: 26 x -= 10 27 28 circle_pos = (x, y) 29 30 surf.fill(bgcolor) 31 pygame.draw.circle(surf, circle_color , 32 circle_pos , circle_rad) 33 pygame.display.flip()
-
第一行导入
pygame
包。在第 4 行,初始化此pygame
包内的模块。使用display.set_mode
方法创建Surface
类的实例。这是 PyGame 窗口中的主窗口,图像将在其中绘制。为了确保此窗口始终显示在屏幕上,我们需要添加一个将永远运行的while
循环,直到用户关闭窗口。在这个简单的应用程序中,我们需要的所有内容都放置在while
循环内。在第 30 行设置了表示 PyGame 窗口背景颜色的对象surf
。 -
在 PyGame 表面上,通过第 31 行的代码绘制了一个圆形。
draw.circle
函数的参数是(Surface, color, position, radius)
。这将在由参数circle_pos
指定的位置创建一个圆形。将Surface
类的实例作为第一个参数传递给此方法。 -
代码块 16-26 捕获某些事件。例如,当鼠标按钮或按键被按下时,就会发生事件。在这个例子中,我们指示程序在按下箭头键时执行某些操作。当按下
RIGHT
箭头键时,圆形将在x
坐标上偏移 10 像素到上一个位置绘制。因此,每次按下RIGHT
箭头键时,圆形看起来就像向右移动。当 PyGame 窗口关闭时,将发生pygame.QUIT
事件。在这里,我们简单地通过调用sys.exit()
(如第 17 行所示)退出应用程序。 -
最后,我们需要确保在
Surface
上绘制的所有内容都可见。这是通过第 31 行的代码实现的。如果您禁用此行,屏幕上可能会出现未完全绘制的图像。 -
从终端窗口执行程序。它将显示一个包含圆形的新图形窗口。如果您按下键盘上的箭头键,圆形将沿着箭头键指示的方向移动。下一张插图显示了原始圆形位置(左侧)和当使用
UP
和RIGHT
箭头键移动时的情况。一个简单的 PyGame 应用程序,在 Surface(窗口)内绘制圆形。右侧的图像是使用箭头键调整圆形位置后拍摄的截图:
刚才发生了什么?
我们使用 PyGame 创建了一个简单的用户交互式应用。本例的目的是介绍动画和游戏编程背后的基本概念。这只是对接下来内容的预览!在本书的后续部分,我们将使用 Pyglet 框架创建一些有趣的 2D 动画。
QT Phonon
当人们想到媒体播放器时,它几乎总是与图形用户界面联系在一起。当然,可以使用命令行多媒体播放器。但具有 GUI 的媒体播放器是一个明显的赢家,因为它提供了一个易于使用、直观的用户界面来流式传输媒体和控制其播放。下一张截图展示了使用 QT Phonon 开发的音频播放器的用户界面。
使用 QT Phonon 开发的音频播放器应用:
QT 是一个开源的 GUI 框架。'Phonon' 是 QT 中的一个多媒体包,支持音频和视频播放。请注意,Phonon 主要用于简单的媒体播放器功能。对于复杂的音频/视频播放器功能,您应使用 GStreamer 等多媒体框架。Phonon 依赖于特定平台的媒体处理后端。例如,在 Windows 平台上,后端框架是 DirectShow。支持的功能可能因平台而异。
要开发媒体处理应用,需要在 Phonon 中创建一个媒体图。这个媒体图包含各种相互连接的媒体节点。每个媒体节点执行部分媒体处理。例如,一个效果节点将为媒体添加音频效果,如回声。另一个节点将负责从音频或视频设备输出媒体,等等。在第八章中,我们将使用 Phonon 框架开发音频和视频播放器应用。下一张插图展示了一个使用 QT Phonon 开发的视频播放器正在流式传输视频。我们将在这个第八章中开发这个应用。
使用 QT Phonon 的各种内置模块,创建基于 GUI 的音频和视频播放器非常容易。本例展示了视频播放器的实际应用:
其他多媒体库
在各种平台上,有多个其他多媒体库的 Python 绑定可用。以下是一些流行的库。
Snack 音频工具包
Snack 是一个音频工具包,用于创建跨平台的音频应用。它包括音频分析和输入/输出功能,并且支持音频可视化。Snack 音频工具包的官方网站是 www.speech.kth.se/snack/
。
PyAudiere
PyAudiere (pyaudiere.org/
) 是一个开源音频库。它提供了一个 API,可以轻松地在各种应用中实现音频功能。它基于 Audiere 音频库。
摘要
本章作为使用 Python 进行多媒体处理的入门介绍。
具体来说,在本章中我们涵盖了:
-
多媒体处理概述。它向我们介绍了数字图像、音频和视频处理。
-
我们了解了一些可以用于多媒体处理的免费多媒体框架。
现在我们已经知道了有哪些多媒体库和框架,我们准备去探索这些来开发令人兴奋的多媒体应用!
第二章。处理图像
在本章中,我们将学习使用 Python 图像处理库的基本图像转换和操作技术。本章以一个令人兴奋的项目结束,我们将创建一个图像处理应用程序。
在本章中,我们将:
-
学习使用Python 图像处理库(PIL)进行各种图像 I/O 操作以读取和写入图像
-
通过几个示例和代码片段的帮助,对图像进行一些基本操作,如调整大小、旋转/翻转、裁剪、粘贴等。
-
利用 PIL 编写图像处理应用程序
-
使用 QT 库作为此应用程序的前端(GUI)
那么,让我们开始吧!
安装前提条件
在我们跳入主要内容之前,安装以下包是必要的。
Python
在本书中,我们将使用 Python 版本 2.6,或者更具体地说,版本 2.6.4。它可以从以下位置下载:python.org/download/releases/
Windows 平台
对于 Windows 用户,只需下载并安装 Python 2.6.4 的平台特定二进制发行版。
其他平台
对于其他平台,例如 Linux,Python 可能已经安装在了您的机器上。如果安装的版本不是 2.6,请从源代码发行版构建和安装它。如果您在 Linux 系统上使用包管理器,请搜索 Python 2.6。您很可能在那里找到 Python 发行版。然后,例如,Ubuntu 用户可以从命令提示符安装 Python,如下所示:
$sudo apt-get python2.6
注意,为此,您必须在安装 Python 的机器上拥有管理员权限。
Python 图像处理库(PIL)
在本章中,我们将通过广泛使用 Python 图像处理库(PIL)来学习图像处理技术。正如第一章中提到的,PIL 是一个开源库。您可以从www.pythonware.com/products/pil/
下载它。请安装 PIL 版本 1.1.6 或更高版本。
Windows 平台
对于 Windows 用户,安装很简单,使用 Python 2.6 的二进制发行版 PIL 1.1.6。
其他平台
对于其他平台,从源代码安装 PIL 1.1.6。仔细阅读源代码发行版中的 README 文件,以获取特定平台的说明。以下表格中列出的库在安装 PIL 之前必须安装。对于某些平台,如 Linux,操作系统提供的库应该可以正常工作。但是,如果这些库不起作用,请安装预构建的"libraryName-devel"版本的库。例如,对于 JPEG 支持,名称将包含"jpeg-devel-",其他类似。这通常适用于基于 rpm 的发行版。对于像 Ubuntu 这样的 Linux 版本,您可以在 shell 窗口中使用以下命令。
$sudo apt-get install python-imaging.
然而,您应该确保安装的版本为 1.1.6 或更高版本。检查 PIL 文档以获取更多特定平台的安装说明。对于 Mac OSX,查看是否可以使用 fink
安装这些库。有关更多详细信息,请参阅 www.finkproject.org/
。您还可以检查网站 pythonmac.org
或 Darwin ports 网站 darwinports.com/
,以查看是否提供二进制包安装程序。如果任何库都没有提供预编译版本,则从源代码安装。
从源代码安装 PIL 的 PIL 预先条件列在下面的表格中:
库 | URL | 版本 | 安装选项(a) 或 (b) |
---|---|---|---|
libjpeg (JPEG 支持) |
www.ijg.org/files |
7 或 6a 或 6b | (a) 预编译版本。例如:jpeg-devel-7 检查是否可以执行:sudo apt-install libjpeg (在某些 Linux 发行版上有效)(b) 源代码包。例如:jpegsrc.v7.tar.gz |
zlib (PNG 支持) |
www.gzip.org/zlib/ |
1.2.3 或更高版本 | (a) 预编译版本。例如:zlib-devel-1.2.3 。(b) 从源代码安装。 |
freetype2 (OpenType/TrueType 支持) |
www.freetype.org |
2.1.3 或更高版本 | (a) 预编译版本。例如:freetype2-devel-2.1.3 。(b) 从源代码安装。 |
PyQt4
此包为 Qt 库提供 Python 绑定。我们将使用 PyQt4 为本章后面将要开发的图像处理应用程序生成 GUI。GPL 版本可在:www.riverbankcomputing.co.uk/software/pyqt/download
获取。
Windows 平台
下载并安装与 Python 2.6 相关的二进制发行版。例如,可执行文件的名称可能是 'PyQt-Py2.6-gpl-4.6.2-2.exe'。除了 Python 之外,它还包括使用 PyQt 进行 GUI 开发所需的所有内容。
其他平台
在构建 PyQt 之前,您必须安装 SIP Python 绑定生成器。有关更多详细信息,请参阅 SIP 主页:www.riverbankcomputing.com/software/sip/
。
安装 SIP 后,从源代码包下载并安装 PyQt 4.6.2 或更高版本。对于 Linux/Unix 源代码,文件名将以 PyQt-x11-gpl-
开头,而对于 Mac OS X,则以 PyQt-mac-gpl-..
开头。Linux 用户还应检查 PyQt4 是否已通过包管理器提供。
安装先决条件摘要
包 | 下载位置 | 版本 | Windows 平台 | Linux/Unix/OS X 平台 |
---|---|---|---|---|
Python | python.org/download/releases/ |
2.6.4(或任何 2.6.x 版本) | 使用二进制发行版安装 | (a) 从二进制安装;还安装额外的开发包(例如,在 rpm 系统中,包名中包含python-devel )或者(b) 从源 tarball 构建和安装。(c) MAC 用户也可以检查类似darwinports.com/ 或pythonmac.org/ 的网站。 |
PIL | www.pythonware.com/products/pil/ | 1.1.6 或更高版本 | 为 Python 2.6 安装 PIL 1.1.6(二进制版) | (a) 如有必要,安装先决条件。参考表#1 和 PIL 源代码包中的 README 文件。(b) 从源代码安装 PIL。(c) MAC 用户也可以检查类似darwinports.com/ 或pythonmac.org/ 的网站。 |
PyQt4 | www.riverbankcomputing.co.uk/software/pyqt/download |
4.6.2 或更高版本 | 使用针对 Python 2.6 的二进制安装 | (a) 首先安装 SIP 4.9 或更高版本。(b) 然后安装 PyQt4。 |
读取和写入图像
要操作现有图片,我们必须首先打开它进行编辑,并在更改后需要能够以合适的文件格式保存图片。PIL 中的Image
模块提供了在指定图像文件格式中读取和写入图像的方法。它支持广泛的文件格式。
要打开图像,请使用Image.open
方法。启动 Python 解释器并编写以下代码。您应该在系统上指定一个适当的路径作为Image.open
方法的参数。
>>>import Image
>>>inputImage = Image.open("C:\\PythonTest\\image1.jpg")
这将打开名为image1.jpg
的图像文件。如果文件无法打开,将引发IOError
,否则,它将返回Image
类的一个实例。
要保存图片,请使用Image
类的save
方法。确保您将以下字符串替换为适当的/path/to/your/image/file
。
>>>inputImage.save("C:\\PythonTest\\outputImage.jpg")
您可以使用Image
类的show
方法查看刚刚保存的图片。
>>>outputImage = Image.open("C:\\PythonTest\\outputImage.jpg")
>>>outputImage.show()
这里,它与输入图片基本相同,因为我们没有对输出图片进行任何更改。
实践时间 - 图像文件转换器
在获得这些基本信息后,让我们构建一个简单的图像文件转换器。此实用程序将批量处理图像文件,并以用户指定的文件格式保存它们。
要开始使用,请从 Packt 网站下载文件ImageFileConverter.py
,www.packtpub.com。此文件可以从命令行运行,如下所示:
python ImageConverter.py [arguments]
这里,[arguments]
包括:
-
--input_dir:
存放图像文件的目录路径。 -
--input_format:
要转换的图像文件格式。例如,jpg
。 -
--output_dir:
您希望保存转换后图片的位置。 -
--output_format:
输出图片格式。例如,jpg, png, bmp
等。
以下截图显示了在 Windows XP 上运行的图像转换实用工具的效果,即从命令行运行图像转换器。
在这里,它将批量处理 C:\PythonTest\images
目录下所有的 .jpg
图像,并将它们以 png
格式保存在 C:\PythonTest\images\OUTPUT_IMAGES
目录中。
该文件定义了 class ImageConverter
。我们将讨论这个类中最重要的方法。
-
def processArgs:
这个方法处理前面列出的所有命令行参数。它使用 Python 的内置模块getopts
来处理这些参数。建议读者查看本书代码包中ImageConverter.py
文件中的代码,以获取有关如何处理这些参数的更多详细信息。 -
def convertImage:
这是图像转换实用工具的工作马方法。1 def convertImage(self): 2 pattern = "*." + self.inputFormat 3 filetype = os.path.join(self.inputDir, pattern) 4 fileList = glob.glob(filetype) 5 inputFileList = filter(imageFileExists, fileList) 6 7 if not len(inputFileList): 8 print "\n No image files with extension %s located \ 9 in dir %s"%(self.outputFormat, self.inputDir) 10 return 11 else: 12 # Record time before beginning image conversion 13 starttime = time.clock() 14 print "\n Converting images.." 15 16 # Save image into specified file format. 17 for imagePath in inputFileList: 18 inputImage = Image.open(imagePath) 19 dir, fil = os.path.split(imagePath) 20 fil, ext = os.path.splitext(fil) 21 outPath = os.path.join(self.outputDir, 22 fil + "." + self.outputFormat) 23 inputImage.save(outPath) 24 25 endtime = time.clock() 26 print "\n Done!" 27 print "\n %d image(s) written to directory:\ 28 %s" %(len(inputFileList), self.outputDir) 29 print "\n Approximate time required for conversion: \ 30 %.4f seconds" % (endtime starttime)
现在我们来回顾一下前面的代码。
-
我们的首要任务是获取所有要保存为不同格式的图像文件的列表。这是通过使用 Python 的
glob
模块来实现的。代码片段中的第 4 行找到所有与局部变量fileType
指定的模式匹配的文件路径名称。在第 5 行,我们检查fileList
中的图像文件是否存在。这个操作可以使用 Python 内置的filter
功能在整个列表上高效地执行。 -
第 7 行到 14 行之间的代码块确保存在一个或多个图像。如果存在,它将记录开始图像转换之前的时间。
-
下一个代码块(第 17-23 行)执行图像文件转换。在第 18 行,我们使用
Image.open
打开图像文件。第 18 行创建了一个Image
对象。然后推导出适当的输出路径,最后使用Image
模块的save
方法保存输出图像。
刚才发生了什么?
在这个简单的例子中,我们学习了如何以指定的图像格式打开和保存图像文件。我们通过编写一个图像文件转换器来实现这一点,该转换器批量处理指定的图像文件。我们使用了 PIL 的 Image.open
和 Image.save
功能以及 Python 的内置模块,如 glob
和 filter
。
现在我们将讨论与图像读取和写入相关的其他关键方面。
从头创建图像
到目前为止,我们已经看到了如何打开现有的图像。如果我们想创建自己的图像怎么办?例如,如果你想创建作为图像的精美文本,我们现在要讨论的功能就派上用场了。在本书的后面部分,我们将学习如何使用包含一些文本的此类图像嵌入到另一个图像中。创建新图像的基本语法是:
foo = Image.new(mode, size, color)
其中,new
是Image
类的一个内置方法。Image.new
接受三个参数,即mode, size
和color
。mode
参数是一个字符串,它提供了有关图像波段数量和名称的信息。以下是最常见的mode
参数值:L
(灰度)和RGB
(真彩色)。size
是一个指定图像像素尺寸的tuple
,而color
是一个可选参数。如果它是一个多波段图像,可以分配一个 RGB 值(一个3-tuple
)。如果没有指定,图像将填充为黑色。
行动时间 - 创建包含一些文本的新图像
如前所述,生成只包含一些文本或常见形状的图像通常很有用。这样的图像可以粘贴到另一个图像上,并定位在所需的角度和位置。现在我们将创建一个包含以下文本的图像:“这并不是一个花哨的文本!”
-
在一个 Python 源文件中编写以下代码:
1 import Image 2 import ImageDraw 3 txt = "Not really a fancy text!" 4 size = (150, 50) 5 color = (0, 100, 0) 6 img = Image.new('RGB', size, color) 7 imgDrawer = ImageDraw.Draw(img) 8 imgDrawer.text((5, 20), txt) 9 img.show()
-
让我们逐行分析代码。前两行从 PIL 中导入必要的模块。变量
txt
是我们想要包含在图像中的文本。在第 7 行,使用Image.new
创建新的图像。这里我们指定了mode
和size
参数。可选的color
参数指定为一个包含 RGB 值的tuple
,与“深绿色”颜色相关。 -
PIL 中的
ImageDraw
模块为Image
对象提供图形支持。函数ImageDraw.Draw
接受一个图像对象作为参数以创建一个Draw
实例。在输出代码中,它被称为imgDrawer
,如第 7 行所示。这个Draw
实例允许在给定的图像中绘制各种东西。 -
在第 8 行,我们调用 Draw 实例的文本方法,并提供了位置(一个
tuple
)和文本(存储在字符串txt
中)作为参数。 -
最后,可以使用
img.show()
调用查看图像。你可以选择使用Image.save
方法保存图像。以下截图显示了结果图像。
刚才发生了什么?
我们刚刚学习了如何从头创建图像。使用Image.new
方法创建了一个空图像。然后,我们使用 PIL 中的ImageDraw
模块向该图像添加文本。
从归档中读取图像
如果图像是归档容器的一部分,例如,一个 TAR 归档,我们可以使用 PIL 中的TarIO
模块来打开它,然后调用Image.open
将这个TarIO
实例作为参数传递。
行动时间 - 从归档中读取图像
假设有一个包含图像文件image1.jpg
的归档文件images.tar
。以下代码片段显示了如何从归档中读取image1.jpg
。
>>>import TarIO
>>>import Images
>>>fil = TarIO.TarIO("images.tar", "images/image1.jpg")
>>>img = Image.open(fil)
>>>img.show()
刚才发生了什么?
我们学习了如何从归档容器中读取图像。
英雄尝试者向图像文件转换器添加新功能
修改图像转换代码,使其支持以下新功能,即:
-
以包含图像的 ZIP 文件作为输入
-
创建转换图像的 TAR 归档
基本图像操作
现在我们已经了解了如何打开和保存图像,让我们学习一些基本的图像操作技术。PIL 支持各种几何操作,例如调整图像大小、旋转角度、上下翻转或左右翻转,等等。它还便于执行裁剪、剪切和粘贴图像片段等操作。
调整大小
改变图像的尺寸是使用最频繁的图像操作之一。使用 PIL 中的 Image.resize
实现图像调整大小。以下代码行解释了它是如何实现的。
foo = img.resize(size, filter)
在这里,img
是一个图像(Image
类的实例)和调整大小操作的结果存储在 foo
(另一个 Image
类的实例)中。size
参数是一个 tuple
(宽度,高度)。请注意,size
是以像素为单位的。因此,调整图像大小意味着修改图像中的像素数量。这也被称为 图像重采样。Image.resize
方法还接受 filter
作为可选参数。filter
是在重采样给定图像时使用的插值算法。它处理重采样过程中像素的删除或添加,当调整大小操作旨在使图像变大或变小时分别处理。有四种滤波器可用。按质量递增的顺序,调整大小滤波器是 NEAREST, BILINEAR, BICUBIC
和 ANTIALIAS
。默认滤波器选项是 NEAREST
。
行动时间 - 调整大小
现在我们通过修改像素尺寸并应用各种滤波器进行重采样来调整图像大小。
-
从 Packt 网站下载文件
ImageResizeExample.bmp
。我们将使用此作为参考文件来创建缩放图像。ImageResizeExample.bmp
的原始尺寸是200 x 212
像素。 -
将以下代码写入文件或 Python 解释器中。将
inPath
和outPath
字符串替换为机器上适当的图像路径。1 import Image 2 inPath = "C:\\images\\ImageResizeExample.jpg" 3 img = Image.open(inPath) 4 width , height = (160, 160) 5 size = (width, height) 6 foo = img.resize(size) 7 foo.show() 8 outPath = "C:\\images\\foo.jpg" 9 foo.save(outPath)
-
由
inPath
指定的图像将被调整大小并保存为outPath
指定的图像。代码片段中的第 6 行执行调整大小任务,并在第 9 行最终保存新图像。你可以通过调用foo.show()
来查看调整大小后的图像外观。 -
现在我们指定
filter
参数。在以下代码的第 14 行,filterOpt
参数在resize
方法中指定。有效的filter
选项作为字典filterDict
中的值指定。filterDict
的键用作输出图像的文件名。下一幅插图将比较这四个图像。你可以清楚地注意到ANTIALIAS
图像与其他图像之间的区别(尤其是,看看这些图像中的花瓣)。当处理时间不是问题时,选择ANTIALIAS
滤波器选项,因为它可以提供最佳质量的图像。1 import Image 2 inPath = "C:\\images\\ImageResizeExample.jpg" 3 img = Image.open(inPath) 4 width , height = (160, 160) 5 size = (width, height) 6 filterDict = {'NEAREST':Image.NEAREST, 7 'BILINEAR':Image.BILINEAR, 8 'BICUBIC':Image.BICUBIC, 9 'ANTIALIAS':Image.ANTIALIAS } 10 11 for k in filterDict.keys(): 12 outPath= "C:\\images\\" + k + ".jpg" 13 filterOpt = filterDict[k] 14 foo = img.resize(size, filterOpt) 15 foo.save(outPath)
-
不同过滤器选项调整大小的图像如下所示。从左到右顺时针,Image.NEAREST、Image.BILINEAR、Image.BICUBIC 和 Image.ANTIALIAS:
-
然而,这里展示的
resize
功能不能保持结果图像的宽高比。如果与另一个维度相比,一个维度被拉伸更多或更少,图像将出现扭曲。PIL 的Image
模块提供了一个内置的另一种方法来解决这个问题。它将覆盖两个维度中较大的一个,这样就可以保持图像的宽高比。import Image inPath = "C:\\images\\ResizeImageExample.jpg" img = Image.open(inPath) width , height = (100, 50) size = (width, height) outPath = "C:\\images\\foo.jpg" img.thumbnail(size, Image.ANTIALIAS) img.save(outPath)
-
此代码将覆盖程序员指定的最大像素维度值(在本例中为宽度)并替换为保持图像宽高比的价值。在这种情况下,我们有一个像素维度为(47,50)的图像。以下插图显示了结果图像的比较。
这显示了使用 Image.thumbnail 和 Image.resize 方法输出图像的比较。
刚才发生了什么?
我们刚刚学习了如何使用 PIL 的 Image
模块通过编写几行代码来实现图像缩放。我们还学习了在图像缩放(重采样)中使用的不同类型的过滤器。最后,我们还看到了如何使用 Image.thumbnail
方法在不扭曲的情况下调整图像大小(即,保持宽高比不变)。
旋转
与图像缩放类似,围绕图像中心旋转图像是另一种常见的变换。例如,在一个合成图像中,在将其嵌入到另一个图像之前,可能需要将文本旋转一定角度。对于这种需求,PIL 的 Image
模块中提供了 rotate
和 transpose
等方法。使用 Image.rotate
旋转图像的基本语法如下:
foo = img.rotate(angle, filter)
其中,angle
以度为单位提供,filter
是可选参数,是图像重采样过滤器。有效的 filter
值可以是 NEAREST
、BILINEAR
或 BICUBIC
。您可以使用 Image.transpose
仅在 90 度、180 度和 270 度旋转角度下旋转图像。
执行动作 - 旋转
-
从 Packt 网站下载文件
Rotate.png
。或者,您可以使用任何您选择的受支持图像文件。 -
在 Python 解释器或 Python 文件中编写以下代码。像往常一样,为
inPath
和outPath
变量指定适当的路径字符串。1 import Image 2 inPath = "C:\\images\\Rotate.png" 3 img = Image.open(inPath) 4 deg = 45 5 filterOpt = Image.BICUBIC 6 outPath = "C:\\images\\Rotate_out.png" 7 foo = img.rotate(deg, filterOpt) 8 foo.save(outPath)
-
运行此代码后,旋转 45 度的输出图像被保存到
outPath
。过滤器选项Image.BICUBIC
确保了最高的质量。下一张插图显示了原始图像以及旋转 45 度和 180 度的图像。 -
使用
Image.transpose
功能还可以通过另一种方式实现特定角度的旋转。以下代码实现了 270 度的旋转。其他有效的旋转选项还有Image.ROTATE_90
和Image.ROTATE_180
。import Image inPath = "C:\\images\\Rotate.png" img = Image.open(inPath) outPath = "C:\\images\\Rotate_out.png" foo = img.transpose(Image.ROTATE_270) foo.save(outPath)
刚才发生了什么?
在前面的章节中,我们使用了Image.rotate
来实现按所需角度旋转图像。图像过滤器Image.BICUBIC
用于在旋转后获得更好的输出图像质量。我们还看到了如何使用Image.transpose
按特定角度旋转图像。
翻转
在 PIL 中,有多种方法可以水平或垂直翻转图像。一种实现方式是使用Image.transpose
方法。另一种选择是使用ImageOps
模块的功能。这个模块通过一些现成的方法使图像处理工作变得更加容易。然而,请注意,PIL 版本 1.1.6 的文档中提到ImageOps
仍然是一个实验性模块。
执行动作 - 翻转
想象一下,你正在使用一些基本形状构建一个对称的图像。为了创建这样的图像,一个可以翻转(或镜像)图像的操作将非常有用。那么,让我们看看图像翻转是如何实现的。
-
在 Python 源文件中编写以下代码。
1 import Image 2 inPath = "C:\\images\\Flip.png" 3 img = Image.open(inPath) 4 outPath = "C:\\images\\Flip_out.png" 5 foo = img.transpose(Image.FLIP_LEFT_RIGHT) 6 foo.save(outPath)
-
在此代码中,通过调用
transpose
方法水平翻转图像。要垂直翻转图像,将代码中的第 5 行替换为以下内容:foo = img.transpose(Image.FLIP_TOP_BOTTOM)
-
以下插图显示了当图像水平翻转和垂直翻转时,前面代码的输出。
-
使用
ImageOps
模块也可以达到相同的效果。要水平翻转图像,使用ImageOps.mirror
,要垂直翻转图像,使用ImageOps.flip
。import ImageOps # Flip image horizontally foo1 = ImageOps.mirror(img) # Flip image vertically foo2 = ImageOps.flip(img)
刚才发生了什么?
通过示例,我们学习了如何使用Image.transpose
和ImageOps
类中的方法水平或垂直翻转图像。此操作将在本书的后续部分用于进一步的图像处理,如准备合成图像。
截图
你如何使用 Python 捕获桌面屏幕或其一部分?PIL 中有ImageGrab
模块。这一行简单的代码就可以捕获整个屏幕。
img = ImageGrab.grab()
其中,img
是Image
类的一个实例。
然而,请注意,在 PIL 版本 1.1.6 中,ImageGrab
模块仅支持 Windows 平台的屏幕截图。
执行动作 - 定时截图
想象你正在开发一个应用程序,在某个时间间隔后,程序需要自动捕获整个屏幕或屏幕的一部分。让我们编写实现这一点的代码。
-
在 Python 源文件中编写以下代码。当代码执行时,它将在每两秒后捕获屏幕的一部分。代码将运行大约三秒。
1 import ImageGrab 2 import time 3 startTime = time.clock() 4 print "\n The start time is %s sec" % startTime 5 # Define the four corners of the bounding box. 6 # (in pixels) 7 left = 150 8 upper = 200 9 right = 900 10 lower = 700 11 bbox = (left, upper, right, lower) 12 13 while time.clock() < 3: 14 print " \n Capturing screen at time %.4f sec" \ 15 %time.clock() 16 screenShot = ImageGrab.grab(bbox) 17 name = str("%.2f"%time.clock())+ "sec.png" 18 screenShot.save("C:\\images\\output\\" + name) 19 time.sleep(2)
-
我们现在将回顾此代码的重要方面。首先,导入必要的模块。
time.clock()
跟踪花费的时间。在第 11 行,定义了一个边界框。它是一个4-tuple
,定义了一个矩形区域的边界。此tuple
中的元素以像素为单位指定。在 PIL 中,原点(0, 0)被定义为图像的左上角。下一图示是图像裁剪的边界框表示;看看如何指定左、上、右和下为矩形的对角线端点。用于图像裁剪的边界框示例。
-
while
循环运行直到time.clock()
达到三秒。在循环内部,屏幕上bbox
内的部分被捕获(见第 16 行),然后图像在第 18 行被保存。图像名称对应于它被捕获的时间。 -
time.sleep(2)
调用暂停应用程序的执行两秒。这确保了它每两秒捕获一次屏幕。循环重复直到达到给定的时间。 -
在这个例子中,它将捕获两个屏幕截图,一个是在它第一次进入循环时,另一个是在两秒的时间间隔后。在下图所示中,展示了代码捕获的两个图像。注意这些图像中的时间和控制台打印。
前面的截图是在 00:02:15 时拍摄的,如对话框所示。下一个截图是在 2 秒后,即墙上的时间 00:02:17。
刚才发生了什么?
在前面的例子中,我们编写了一个简单的应用程序,它以固定的时间间隔捕获屏幕。这帮助我们学习了如何使用ImageGrab
捕获屏幕区域。
裁剪
在前面的章节中,我们学习了如何使用ImageGrab
捕获屏幕的一部分。裁剪是对图像执行的一个非常类似的操作。它允许你修改图像内的一个区域。
执行动作 - 裁剪图像
这个简单的代码片段裁剪了一个图像并对裁剪的部分进行了某些更改。
-
从 Packt 网站下载文件
Crop.png
。此图像的大小为400 x 400
像素。你也可以使用你自己的图像文件。 -
在 Python 源文件中编写以下代码。将图像文件的路径修改为适当的路径。
import Image img = Image.open("C:\\images\\Crop.png") left = 0 upper = 0 right = 180 lower = 215 bbox = (left, upper, right, lower) img = img.crop(bbox) img.show()
-
这将裁剪出由
bbox
定义的图像区域。边界框的指定与我们之前在捕获屏幕截图部分看到的是相同的。此示例的输出如下所示。原始图像(左)及其裁剪区域(右)。
刚才发生了什么?
在上一节中,我们使用了Image.crop
功能来裁剪图像中的某个区域并保存结果图像。在下一节中,我们将应用这一功能将图像的一个区域粘贴到另一个图像上。
粘贴
在处理图像时,将复制的或剪切的图像粘贴到另一个图像上是一个常见的操作。以下是将一个图像粘贴到另一个图像上的最简单语法。
img = img.paste(image, box)
在这里,image
是Image
类的一个实例,而box
是一个矩形边界框,它定义了img
的区域,其中image
将被粘贴。box
参数可以是4-tuple
或2-tuple
。如果指定了4-tuple
边界框,粘贴的图像大小必须与区域大小相同。否则,PIL 将抛出一个错误,错误信息为ValueError: images do not match
。另一方面,2-tuple
提供了要粘贴的区域左上角的像素坐标。
现在看看以下代码行。这是一次对图像的复制操作。
img2 = img.copy(image)
复制操作可以看作是将整个图像粘贴到新图像上。当例如你想保持原始图像不变而只对图像的副本进行修改时,这个操作非常有用。
行动时间 - 粘贴:镜像笑脸!
考虑到之前章节中我们裁剪图像的例子。裁剪区域包含了一个笑脸。让我们修改原始图像,使其有一个笑脸的“反射”。
-
如果还没有的话,请从 Packt 网站下载文件
Crop.png
。 -
通过将文件路径替换为您系统上的适当路径来编写此代码。
1 import Image 2 img = Image.open("C:\\images\\Crop.png") 3 # Define the elements of a 4-tuple that represents 4 # a bounding box ( region to be cropped) 5 left = 0 6 upper = 25 7 right = 180 8 lower = 210 9 bbox = (left, upper, right, lower) 10 # Crop the smiley face from the image 11 smiley = img.crop(bbox_1) 12 # Flip the image horizontally 13 smiley = smiley.transpose(Image.FLIP_TOP_BOTTOM) 14 # Define the box as a 2-tuple. 15 bbox_2 = (0, 210) 16 # Finally paste the 'smiley' on to the image. 17 img.paste(smiley, bbox_2) 18 img.save("C:\\images\\Pasted.png") 19 img.show()
-
首先,我们打开一个图像并将其裁剪以提取包含笑脸的区域。这已经在
Error: Reference source not found'Cropping'
部分完成了。您唯一会注意到的细微差别是元组元素upper
的值。它故意保持为顶部 25 像素,以确保裁剪图像的大小可以适应原始笑脸下方的空白区域。 -
然后使用代码在第 13 行水平翻转裁剪后的图像。
-
现在我们定义一个矩形框,
bbox_2
,用于将裁剪的笑脸重新粘贴到原始图像上。应该粘贴在哪里?我们的意图是制作原始笑脸的“反射”。因此,粘贴图像右上角的坐标应该大于或等于裁剪区域的底部 y 坐标,由“lower”变量(见第 8 行)指示。边界框在第 15 行定义,作为一个表示笑脸左上坐标的2-tuple
。 -
最后,在第 17 行,执行粘贴操作,将笑脸粘贴到原始图像上。然后以不同的名称保存生成的图像。
-
下一个图例显示了粘贴操作后的原始图像和输出图像。
图例显示了粘贴操作后原始图像和结果图像的比较。
刚才发生了什么?
通过结合使用Image.crop
和Image.paste
,我们完成了裁剪一个区域、进行一些修改,然后将该区域粘贴回图像。
项目:缩略图制作器
现在我们开始一个项目。我们将应用本章学到的某些操作来创建一个简单的缩略图制作工具。这个应用程序将接受一个图像作为输入,并将创建该图像的缩放版本。虽然我们称之为缩略图制作器,但它是一个多功能的工具,实现了某些基本的图像处理功能。
在继续之前,请确保你已经安装了本章开头讨论的所有包。缩略图制作器对话框的截图如下所示。
缩略图制作器 GUI 有两个组件:
-
左侧面板是一个“控制区域”,你可以在这里指定某些图像参数,以及输入和输出路径的选项。
-
右侧有一个图形区域,你可以在这里查看生成的图像。
简而言之,这是它的工作原理:
-
应用程序接受一个图像文件作为输入。
-
它接受用户输入的图像参数,如像素尺寸、重采样滤波器和旋转角度(以度为单位)。
-
当用户在对话框中点击确定按钮时,图像将被处理并保存到用户指定的输出图像格式位置。
行动时间 - 玩转缩略图制作器应用程序
首先,我们将以最终用户身份运行缩略图制作器应用程序。这个预热练习旨在让我们更好地理解应用程序的工作原理。这将反过来帮助我们快速开发/学习相关的代码。所以,准备行动吧!
-
从 Packt 网站下载文件
ThumbnailMaker.py, ThumbnailMakeDialog.py
和Ui_ThumbnailMakerDialog.py
。将这些文件放置在某个目录中。 -
从命令提示符中,切换到这个目录位置,并输入以下命令:
python ThumbnailMakerDialog.py
- 弹出的 Thumbnail Maker 对话框在之前的截图中已经显示。接下来,我们将指定输入输出路径和各种图像参数。你可以打开任何你选择的图像文件。在这里,将使用之前某些部分中显示的花朵图像作为输入图像。要指定输入图像,点击带有三个点的按钮……它将打开一个文件对话框。以下插图显示了已指定所有参数的对话框。
- 弹出的 Thumbnail Maker 对话框在之前的截图中已经显示。接下来,我们将指定输入输出路径和各种图像参数。你可以打开任何你选择的图像文件。在这里,将使用之前某些部分中显示的花朵图像作为输入图像。要指定输入图像,点击带有三个点的按钮……它将打开一个文件对话框。以下插图显示了已指定所有参数的对话框。
-
如果勾选了保持纵横比复选框,它将内部调整图像尺寸,以确保输出图像的纵横比保持不变。当点击确定按钮时,结果图像将保存在输出位置字段指定的位置,并且保存的图像将在对话框的右侧面板中显示。以下截图显示了点击确定按钮后的对话框。
-
你现在可以尝试修改不同的参数,例如输出图像格式或旋转角度,并保存结果图像。
-
看看当保持纵横比复选框未勾选时会发生什么。结果图像的纵横比将不会保留,如果宽度和高度尺寸没有正确指定,图像可能会出现变形。
-
尝试不同的重采样过滤器;你可以注意到结果图像和早期图像之间的质量差异。
-
这个基本实用程序有一定的局限性。需要在对话框中为所有参数字段指定合理的值。如果任何参数未指定,程序将打印错误。
刚才发生了什么?
我们熟悉了 Thumbnail Maker 对话框的用户界面,并看到了它是如何处理不同尺寸和质量的图像的。这些知识将使理解 Thumbnail Maker 代码更容易。
生成 UI 代码
Thumbnail Maker GUI 是用 PyQt4 编写的(Qt4 GUI 框架的 Python 绑定)。关于如何生成 GUI 以及 GUI 元素如何连接到主功能的详细讨论超出了本书的范围。然而,我们将介绍这个 GUI 的某些主要方面,以便你开始使用。这个应用程序中的 GUI 相关代码可以“直接使用”,如果你对此感兴趣,可以继续实验!在本节中,我们将简要讨论如何使用 PyQt4 生成 UI 代码。
操作时间 - 生成 UI 代码
PyQt4 自带一个名为 QT Designer 的应用程序。它是一个基于 QT 的应用程序 GUI 设计器,提供了一种快速开发包含一些基本小部件的图形用户界面的方法。现在,让我们看看在 QT Designer 中 Thumbnail Maker 对话框的样式,然后运行一个命令从.ui
文件生成 Python 源代码。
-
从 Packt 网站下载
thumbnailMaker.ui
文件。 -
启动 PyQt4 安装中附带 QT 设计器应用程序。
-
在 QT 设计器中打开
thumbnailMaker.ui
文件。注意对话框中 UI 元素周围的红色边框。这些边框表示一个布局,其中小部件被排列。如果没有布局,当您运行应用程序时,UI 元素可能会变形,例如调整对话框的大小。使用了三种类型的QLayouts
,即Horizontal
、Vertical
和Grid
布局。 -
您可以通过从 QT 设计器的“小部件框”拖放来添加新的 UI 元素,例如
QCheckbox
或QLabel
。它默认位于左侧面板中。 -
点击“输入文件”旁边的字段。在 QT 设计器的右侧面板中,有一个属性编辑器,它显示所选小部件(在这种情况下是
QLineEdit
)的属性。这在上面的插图中有展示。属性编辑器允许我们为小部件的各种属性分配值,例如objectName
、width
和height
等。Qt 设计器在属性编辑器中显示所选小部件的详细信息。
-
QT 设计器以
.ui
扩展名保存文件。要将此转换为 Python 源代码,PyQt4 提供了一个名为pyuic4
的转换工具。在 Windows XP 上,对于标准的 Python 安装,它位于以下位置C:\Python26\Lib\site-packages\PyQt4\pyuic4.bat
。将此路径添加到您的环境变量中。或者,每次您想要将ui
文件转换为 Python 源文件时,指定整个路径。转换工具可以从命令提示符中运行,如下所示: -
此脚本将生成包含所有 GUI 元素的
Ui_ThumbnailMakerDialog.py
文件。您可以进一步审查此文件以了解 UI 元素是如何定义的。pyuic4 thumbnailMaker.ui -o Ui_ThumbnailMakerDialog.py
刚才发生了什么?
我们学习了如何从 Qt 设计器文件自动生成定义 Thumbnail Maker 对话框 UI 元素的 Python 源代码。
英雄尝试调整 Thumbnail Maker 对话框的 UI
在 QT 设计器中修改 thumbnailMaker.ui
文件,并在 Thumbnail Maker 对话框中实现以下列表中的内容。
-
将左侧面板中所有行编辑的颜色更改为浅黄色。
-
调整默认文件扩展名在 输出文件格式 组合框中显示,使第一个选项为
.png
而不是.jpeg
小贴士
双击此组合框以编辑它。
-
将新的选项
.tiff
添加到输出格式组合框中。 -
将 OK 和 Cancel 按钮对齐到右下角。
-
将旋转角度的范围设置为 0 到 360 度,而不是当前的 -180 到 +180 度。
小贴士
您需要断开布局,移动空格,并重新创建布局。
然后,通过运行
pyuic4
脚本创建Ui_ThumbnailMakerDialog.py
,然后运行 Thumbnail Maker 应用程序。
连接小部件
在前面的部分,表示 UI 的 Python 源代码是使用pyuic4
脚本自动生成的。然而,这仅仅定义了小部件并将它们放置在一个漂亮的布局中。我们需要教这些小部件在发生特定事件时应该做什么。为此,我们将使用 QT 的槽和信号。当特定的 GUI 事件发生时,会发出一个信号。例如,当用户点击OK按钮时,内部会发出一个clicked()
信号。槽是一个在特定信号发出时被调用的函数。因此,在这个例子中,它将在OK按钮被点击时调用一个指定的方法。请参阅 PyQt4 文档以获取各种小部件可用的完整信号列表。
行动时间 - 连接小部件
你会在对话框中注意到几个不同的小部件。例如,接受输入图像路径或输出目录路径的字段是QLineEdit
。指定图像格式的控件是QCombobox
。按照类似的逻辑,OK和Cancel按钮是QPushButton
。作为一个练习,你可以打开thumbnailMaker.ui
文件并点击每个元素,以查看属性编辑器中关联的 QT 类。
现在,让我们学习小部件是如何连接的。
-
打开文件
ThumbnailMakerDialog.py
。类ThumbnailMakerDialog
的_connect
方法被复制。该方法在这个类的构造函数中被调用。def _connect(self): """ Connect slots with signals. """ self.connect(self._dialog.inputFileDialogButton, SIGNAL("clicked()"), self._openFileDialog) self.connect(self._dialog.outputLocationDialogButton, SIGNAL("clicked()"), self._outputLocationPath) self.connect(self._dialog.okPushButton, SIGNAL("clicked()"), self._processImage) self.connect(self._dialog.closePushButton, SIGNAL("clicked()"), self.close) self.connect(self._dialog.aspectRatioCheckBox, SIGNAL('stateChanged(int)'), self._aspectRatioOptionChanged)
-
self._dialog
是类Ui_ThumbnailMakerDialog
的一个实例。self.connect
是 Qt 类QDialog
继承的方法。在这里,它接受以下参数(QObject,signal,callable
),其中QObject
是任何小部件类型(所有都继承自QObject
),signal
是 QT 的SIGNAL
,它告诉我们发生了什么事件,而callable
是处理此事件的任何方法。 -
例如,考虑代码片段中高亮显示的行。它们将OK按钮连接到处理图像的方法。第一个参数
self._dialog.okPushButton
指的是在类Ui_ThumbnailMakerDialog
中定义的按钮小部件。参考QPushButton
文档,你会发现它有一个可以发出的“clicked()”信号。第二个参数SIGNAL("clicked()")
告诉 Qt 我们希望在用户点击该按钮时知道。第三个参数是当这个信号发出时被调用的方法self._processImage
。 -
同样,你可以回顾这个方法中的其他连接。这些连接中的每一个都将一个小部件连接到类
ThumbnailMakerDialog
的方法。
刚才发生了什么?
我们回顾了ThumbnailMakerDialog._connect()
方法,以了解 UI 元素是如何连接到各种内部方法的。前两个部分帮助我们学习了使用 QT 进行 GUI 编程的一些初步概念。
开发图像处理代码
前几节旨在让我们作为最终用户熟悉应用程序,并了解应用程序中 GUI 元素的一些基本方面。所有必要的部分都准备好了,让我们将注意力集中在执行应用程序中所有主要图像处理的类上。
ThumbnailMaker
类处理纯图像处理代码。它定义了各种方法来实现这一点。例如,类方法如_rotateImage
、_makeThumbnail
和_resizeImage
分别用于旋转、生成缩略图和调整大小。该类接受来自ThumbnailMakerDialog
的输入。因此,这里不需要 QT 相关的 UI 代码。如果您想使用其他 GUI 框架来处理输入,可以轻松做到。只需确保实现ThumbnailMakerDialog
中定义的公共 API 方法,因为ThumbnailMaker
类使用这些方法。
行动时间 - 开发图像处理代码
因此,有了ThumbnailMakerDialog
,您可以在ThumbnailMaker
类中从头开始编写自己的代码。只需确保实现processImage
方法,因为这是ThumbnailMakerDialog
调用的唯一方法。
让我们开发一些ThumbnailMaker
类的重要方法。
-
编写
ThumbnailMaker
类的构造函数。它以dialog
作为参数。在构造函数中,我们只初始化self._dialog
,它是ThumbnailMakerDialog
类的一个实例。以下是代码。def __init__(self, dialog): """ Constructor for class ThumbnailMaker. """ # This dialog can be an instance of # ThumbnailMakerDialog class. Alternatively, if # you have some other way to process input,
# it will be that class. Just make sure to implement # the public API methods defined in # ThumbnailMakerDialog class! self._dialog = dialog
-
接下来,在
ThumbnailMaker
类中编写processImage
方法。代码如下:小贴士
注意:您可以从 Packt 网站下载
ThumbnailMaker.py
文件。编写的代码来自此文件。唯一的区别是这里删除了一些代码注释。1 def processImage(self): 2 filePath = self._dialog.getInputImagePath() 3 imageFile = Image.open(filePath) 4 5 if self._dialog.maintainAspectRatio: 6 resizedImage = self._makeThumbnail(imageFile) 7 else: 8 resizedImage = self._resizeImage(imageFile) 9 10 rotatedImage = self._rotateImage(resizedImage) 11 12 fullPath = self._dialog.getOutImagePath() 13 14 # Finally save the image. 15 rotatedImage.save(fullPath)
-
在第 2 行,它获取输入图像文件的完整路径。请注意,它依赖于
self._dialog
提供此信息。 -
然后按照常规方式打开图像文件。在第 4 行,它检查一个标志以决定是否通过保持宽高比来处理图像。相应地,调用
_makeThumbnail
或_resizeImage
方法。 -
在第 10 行,它使用
_rotateImage
方法旋转之前调整大小的图像。 -
最后,在第 15 行,处理后的图像被保存在
ThumbnailMakerDialog
类的getOutImagePath
方法获得的路径上。 -
我们现在将编写
_makeThumbnail
方法。1 def _makeThumbnail(self, imageFile): 2 foo = imageFile.copy() 3 size = self._dialog.getSize() 4 imageFilter = self._getImageFilter() 5 foo.thumbnail(size, imageFilter) 6 return foo
-
首先,创建原始图像的一个副本。我们将操作这个副本,并将方法返回以进行进一步处理。
-
然后从
self._dialog
和_getImageFilter
分别获取必要的参数,例如图像尺寸和用于重采样的过滤器。 -
最后,在第 5 行创建缩略图,然后方法返回此图像实例。
-
我们已经讨论了如何调整和旋转图像。相关的代码编写简单,建议读者将其作为练习来编写。您需要从文件
ThumbnailMakerDialog.py
中审查代码以获取适当的参数。编写剩余的例程,即_resizeImage, _rotateImage
和_getImageFilter
。 -
一旦所有方法都到位,就可以从命令行运行代码:
-
应显示我们的应用程序对话框。玩一玩,确保一切正常!
python Thumbnailmaker.py
刚才发生了什么?
在上一节中,我们完成了一个令人兴奋的项目。本章中学到的许多内容,如图像 I/O、调整大小等,都应用于该项目。我们开发了一个 GUI 应用程序,其中实现了一些基本的图像处理功能,例如创建缩略图。这个项目还帮助我们了解了使用 QT 进行 GUI 编程的各个方面。
尝试增强 ThumbnailMaker 应用程序
想要在缩略图制作器上做更多的事情吗?这里就是!随着您向此应用程序添加更多功能,您首先需要做的是至少更改弹出对话框的标题!在 QT 设计师中编辑thumbnailMaker.ui
文件,将其名称更改为类似“图像处理器”的内容,并重新创建相应的.py
文件。接下来,向此应用程序添加以下功能。
小贴士
如果您不想处理任何 UI 代码,那也行!您可以编写一个类似于ThumbnailMakerDialog
的类。以您自己的方式处理输入参数。这个ThumbnailMaker
类只需要在这个新类中实现某些公共方法,以获取各种输入参数。
-
接受用户输入的输出文件名。目前,它给出与输入文件相同的名称。
编辑
.ui
文件。您需要在添加QLineEdit
及其QLabel
之前打破布局,然后重新创建布局。 -
如果输出目录中存在先前创建的输出图像文件,则单击确定将简单地覆盖该文件。添加一个复选框,内容为“覆盖现有文件(如果有)”。如果复选框未选中,则应弹出警告对话框并退出。
对于后面部分,在
ThumbnailMakerDialog._processImage
中有一个被注释掉的代码块。只需启用该代码即可。 -
添加一个功能,可以在输出图像的左下角添加指定的文本。
-
使用此文本创建一个图像,并通过组合裁剪和粘贴来达到所需的效果。对于用户输入,您需要在
ThumbnailMakerDialog._connect
中添加一个新的QLineEdit
以接受文本输入,然后连接信号与可调用的方法。
摘要
在本章中,我们学习了大量的基本图像处理技巧。
具体来说,我们涵盖了图像输入输出操作,这些操作可以读取和写入图像,以及从头创建图像。
在众多示例和代码片段的帮助下,我们学习了几个图像处理操作。其中一些是:
-
如何调整图像大小,是否保持纵横比
-
旋转或翻转图像
-
裁剪图像,使用本章早期学到的技术对其进行操作,然后将其粘贴到原始图像上
-
创建带有文本的图像
-
我们开发了一个小应用程序,它会在固定的时间间隔内捕获您的屏幕区域
-
我们创建了一个有趣的项目,实现了本章学到的某些图像处理功能
在掌握了基本的图像处理知识后,我们就可以学习如何给图像添加一些酷炫的效果了。在下一章中,我们将看到如何增强图像。
第三章. 增强图像
在上一章中,我们学习了关于日常图像处理的大量知识。通过处理几个示例和小型项目,我们完成了通过基本图像处理来执行学习目标。在本章中,我们将进一步学习如何给图像添加特殊效果。添加到图像中的特殊效果具有多种用途。这不仅使图像看起来令人愉悦,还可能帮助你理解图像呈现的重要信息。
在本章中,我们将:
-
学习如何调整图像的亮度和对比度级别
-
添加代码以选择性地修改图像的颜色,并创建灰度图像和负片
-
使用 PIL 功能将两个图像合并在一起,并为图像添加透明效果
-
将各种图像增强过滤器应用于图像,以实现平滑、锐化、浮雕等效果
-
承接一个项目,开发一个工具,用于向图像添加水印、文本或日期戳
所以,让我们开始吧。
安装和下载先决条件
本章的安装先决条件与 第二章 中的相同,即 处理图像。有关更多详细信息,请参阅该章节。
重要的是要从 Packt 网站下载本章所需的所有图像,网址为 www.packtpub.com/
。我们将在这章的图像处理代码中使用这些图像。此外,请从 Packt 网站下载 PDF 文件,Chapter 3 Supplementary Material.pdf
。如果你阅读的是黑白印刷的纸质书,这一点非常重要。在即将到来的章节,如“调整颜色”中,我们将比较处理前后的图像。在黑白版中,你将无法看到比较图像之间的差异。例如,改变图像颜色、修改对比度等效果将不明显。PDF 文件包含所有这些图像比较。因此,在处理本章的示例时,请务必保留此文件!
调整亮度和对比度
经常需要调整图像的亮度和对比度级别。例如,你可能有一张用基本相机拍摄的照片,当时光线不足。你将如何通过数字方式纠正它?亮度调整有助于使图像变亮或变暗,而对比度调整则强调图像数据中颜色和亮度级别之间的差异。可以使用 PIL 中的 ImageEnhance
模块使图像变亮或变暗。该模块还提供了一个可以自动对比图像的类。
操作时间 - 调整亮度和对比度
让我们学习如何修改图像的亮度和对比度。首先,我们将编写代码来调整亮度。ImageEnhance
模块通过提供 Brightness
类使我们的工作变得简单。
-
下载图像
0165_3_12_Before_BRIGHTENING.png
并将其重命名为Before_BRIGHTENING.png
。 -
使用以下代码:
1 import Image 2 import ImageEnhance 3 4 brightness = 3.0 5 peak = Image.open( "C:\\images\\Before_BRIGHTENING.png ") 6 enhancer = ImageEnhance.Brightness(peak) 7 bright = enhancer.enhance(brightness) 8 bright.save( "C:\\images\\BRIGHTENED.png ") 9 bright.show()
-
在代码片段的第 6 行,我们创建了一个
Brightness
类的实例。它以Image
实例作为参数。 -
第 7 行通过使用指定的
brightness
值创建了一个新的图像bright
。介于0.0
和小于1.0
之间的值会使图像变暗,而大于1.0
的值会使它变亮。值为1.0
保持图像的亮度不变。 -
下一个插图显示了原始图像和结果图像。
亮化前后的图像比较。
-
让我们继续调整亮化图像的对比度。我们将向亮化图像的代码片段中添加以下行。
10 contrast = 1.3 11 enhancer = ImageEnhance.Contrast(bright) 12 con = enhancer.enhance(contrast) 13 con.save( "C:\\images\\CONTRAST.png ") 14 con.show()
-
因此,与调整图像亮度类似,我们通过使用
ImageEnhance.Contrast
类调整了图像的对比度。对比度值为0.0
创建一个黑色图像。值为1.0
保持当前对比度。 -
下面的插图显示了结果图像与原始图像的比较。
小贴士
注意:如安装和下载必备条件部分所述,如果你阅读的是这本书的纸质版,那么在下图中比较的图像将看起来完全相同。请下载并参考补充 PDF 文件
Chapter 3 Supplementary Material.pdf
。在这里,提供了彩色图像,这将帮助你看到差异。- 显示增加对比度的原始图像和图像。
-
在前面的代码片段中,我们需要指定一个对比度值。如果你更喜欢使用 PIL 来决定合适的对比度级别,有一种方法可以做到这一点。
ImageOps.autocontrast
功能设置一个合适的对比度级别。此功能使图像对比度归一化。现在让我们使用这个功能。 -
使用以下代码:
import ImageOps bright = Image.open( "C:\\images\\BRIGHTENED.png ") con = ImageOps.autocontrast(bright, cutoff = 0) con.show()
-
代码中高亮显示的行是自动设置对比度的位置。
autocontrast
函数计算输入图像的直方图。cutoff
参数表示从该直方图中修剪最亮和最暗像素的百分比。然后图像被重新映射。
刚才发生了什么?
通过使用ImageEnhance
模块中的类和功能,我们学习了如何增加或减少图像的亮度和对比度。我们还编写了代码,使用ImageOps
模块提供的功能来自动对比度图像。在这里学到的知识将在本章后续部分中很有用。
调整颜色
在图像上执行的其他有用操作之一是调整图像内的颜色。图像可能包含一个或多个波段,包含图像数据。image
模式包含有关图像像素数据深度和类型的信息。在本章中我们将使用最常用的模式是RGB
(真彩色,3x8 位像素数据)、RGBA
(带透明度蒙版的真彩色,4x8 位)和L
(黑白,8 位)。
在 PIL 中,你可以轻松地获取图像内波段数据的信息。要获取波段名称和数量,可以使用Image
类的getbands()
方法。在这里,img
是Image
类的一个实例。
>>> img.getbands()
('R', 'G', 'B', 'A')
动手时间 - 在图像内交换颜色!
为了理解一些基本概念,让我们编写代码来仅交换图像波段数据。
-
下载图像
0165_3_15_COLOR_TWEAK.png
并将其重命名为COLOR_TWEAK.png
。 -
输入以下代码:
1 import Image 2 3 img = Image.open( "C:\\images\\COLOR_TWEAK.png ") 4 img = img.convert('RGBA') 5 r, g, b, alpha = img.split() 6 img = Image.merge( "RGBA ", (g, r, b, alpha)) 7 img.show()
-
让我们分析一下这段代码。在第 2 行,创建了一个
Image
实例,然后我们将图像的mode
更改为RGBA
。小贴士
在这里,我们应该检查图像是否已经具有该模式,或者这种转换是否可行。你可以将这个检查作为练习来添加!
-
接下来,对
Image.split()
的调用创建了Image
类的单独实例,每个实例包含单个波段数据。因此,我们有四个Image
实例r, g, b
和alpha
,分别对应红色、绿色、蓝色波段和 alpha 通道。 -
第 6 行的代码执行主要的图像处理。
Image.merge
函数的第一个参数是mode
,第二个参数是一个包含波段信息的图像实例的元组。所有波段必须具有相同的大小。正如你所注意到的,我们在指定第二个参数时,在Image
实例r
和g
中交换了波段数据的顺序。 -
在下一幅插图中所获得的原始图像和结果图像进行了比较。现在花的颜色呈现绿色调,而花后面的草以红色调呈现。
小贴士
如同在安装和下载必备条件部分所述,如果你阅读的是这本书的纸质版,那么在下一幅插图中所比较的图像将看起来完全相同。请下载并参考补充 PDF 文件
Chapter 3 Supplementary Material.pdf
。在这里,提供了彩色图像,这将帮助你看到差异。原图(左)和颜色交换图像(右)。
刚才发生了什么?
我们成功创建了一个波段数据交换的图像。我们学习了如何使用 PIL 的Image.split()
和Image.merge()
来实现这一点。然而,这个操作是在整个图像上执行的。在下一节中,我们将学习如何将颜色变化应用于特定的颜色区域。
修改单个图像波段
在上一节中,我们看到了如何改变整个波段表示的数据。由于这种波段交换,花朵的颜色变成了绿色调,草地颜色被渲染成红色调。如果我们只想改变花朵的颜色,而保持草地颜色不变呢?为了做到这一点,我们将利用 Image.point
功能以及上一章中深入讨论的 Image.paste
操作。
然而,请注意,我们需要小心指定需要更改的颜色区域。它也可能取决于图像。有时,它将选择一些其他与指定颜色范围匹配的区域,而我们不希望这样。
行动时间 - 改变花朵的颜色
我们将使用上一节中使用的相同花朵图像。如前所述,我们的任务是改变花朵的颜色,同时保持草地颜色不变。
-
在 Python 源文件中添加此代码。
1 import Image 2 3 img = Image.open( "C:\\images\\COLOR_TWEAK.png ") 4 img = img.convert('RGBA') 5 r, g, b, alpha = img.split() 6 selection = r.point(lambda i: i > 120 and 150) 7 selection.save( "C:\\images\\COLOR_BAND_MASK.png ") 8 r.paste(g, None, selection) 9 img = Image.merge( "RGBA ", (r, g, b, alpha)) 10 img.save( "C:\\images\\COLOR_CHANGE_BAND.png ") 11 img.show()
-
1 到 5 行与之前看到的一样。在第 5 行,我们将原始图像分割,创建了四个
Image
实例,每个实例包含一个波段数据。 -
在第 6 行创建了一个新的
Image
实例 'selection'。这是一个重要的操作,是选择性修改颜色的关键!让我们看看这一行代码做了什么。如果你观察原始图像,花朵区域(嗯,大部分)被渲染成红色调。因此,我们在Image
实例r
上调用了point(function)
方法。point
方法接受一个函数和一个参数,通过这个函数映射图像。它返回一个新的Image
实例。 -
第 6 行上的这个 lambda 函数做了什么?内部,PIL 的
point
函数做的是这样的事情:lst = map(function, range(256)) * no_of_bands
- 在这个例子中,函数实际上是一个 lambda 函数。图像的
no_of_bands
为 1。因此,第 6 行用于选择红色值大于 120 的区域。lst 是一个列表,在这种情况下,前 120 个值是 False,其余值是 150。150 的值在我们执行粘贴操作时确定最终颜色起着作用。
- 在这个例子中,函数实际上是一个 lambda 函数。图像的
-
在应用
point
操作后创建的图像掩码如图所示。此图像中的白色区域代表我们刚刚通过point
操作捕获的区域。在执行下一个paste
操作时,只有白色区域将发生变化。 -
在第 8 行,我们执行了上一章中讨论的
paste
操作。在这里,使用掩码selection
将图像g
粘贴到图像r
上。结果,图像r
的波段数据被修改。 -
最后,通过使用包含新波段信息的单个
r, g, b
和alpha
图像实例,使用merge
操作创建了一个新的Image
实例。 -
在下一幅插图中的原始图像和最终处理后的图像进行了比较。新的花朵颜色看起来和原始颜色一样酷,不是吗?
小贴士
如同在 安装和下载必备条件 部分所述,如果你在阅读这本书的纸质版,以下插图中的图像将看起来完全相同。请下载并参考补充的 PDF 文件
Chapter 3 Supplementary Material.pdf
。其中提供了彩色图像,这将帮助你看到差异。
刚才发生了什么?
我们设计了一个示例,修改了选择性的颜色区域。通过处理单个图像波段数据来完成这项任务。借助 PIL 的 Image
模块中的 point
、paste
和 merge
操作,我们成功改变了提供的图像中花朵的颜色。
灰度图像
如果你想给图像添加怀旧效果,你可以做的许多事情之一就是将其转换为灰度。在 PIL 中创建灰度图像有多种方法。当模式指定为 L
时,生成的图像是灰度的。将彩色图像转换为黑白的基本语法是:
img = img.convert('L')
或者,我们可以使用 ImageOps
模块提供的功能。
img = ImageOps.grayscale(img)
如果你从头开始创建图像,语法是:
img = Image.new('L', size)
以下插图显示了使用这些技术之一创建的原始图像和转换后的灰度图像。
小贴士
请下载并参考补充的 PDF 文件 Chapter 3 Supplementary Material.pdf
。其中提供了彩色图像,这将帮助你看到以下图像之间的差异。
桥梁的原始图像和灰度图像:
制作负片
创建图像的负片非常简单。我们只需要反转每个颜色像素。因此,如果你在像素处有一个颜色 x
,那么负片在该像素处将会有 (255 x
)。ImageOps
模块使得这个过程非常简单。以下代码行创建了一个图像的负片。
img = ImageOps.invert(img)
这是此操作的结果:
原始图像(左)及其负片(右)。
混合
你是否曾希望看到自己在家庭照片中的样子,而那时你并不在场?或者,如果你只是想看到自己在珠穆朗玛峰顶上的样子,至少在照片中是这样?嗯,使用 PIL 提供的功能,如混合、合成图像处理等,这是可能的。
在本节中,我们将学习如何混合图像。正如其名所示,混合意味着将两个兼容的图像混合以创建一个新的图像。PIL 中的blend
功能使用两个具有相同size
和mode
的输入图像创建一个新的图像。内部,两个输入图像使用恒定的alpha
值进行插值。
在 PIL 文档中,它被表述为:
blended_image = in_image1 * (1.0 - alpha) + in_image2 * alpha
看这个公式,很明显alpha = 1.0
将使混合图像与'n_image2'相同,而alpha = 0.0
则返回in_image1
作为混合图像。
行动时间 - 混合两个图像
有时,将两个图像混合在一起的综合效果与以不同的方式查看相同的图像相比,会产生很大的影响。现在,是时候通过混合两张图片来发挥你的想象力了。在这个例子中,我们的结果图像显示了密歇根州的大 Mackinac 桥上飞翔的鸟类。然而,它们是从哪里来的?鸟类在桥梁的原始图像中并不存在。
-
从 Packt 网站下载以下文件:
0165_3_28_BRIDGE2.png
和0165_3_29_BIRDS2.png
。分别将这些文件重命名为BRIDGE2.png
和BIRDS2.png
。 -
在 Python 源文件中添加以下代码。
1 import Image 2 3 img1 = Image.open( "C:\\images\\BRIDGE2.png ") 4 img1 = img1.convert('RGBA') 5 6 img2 = Image.open( "C:\\images\\BIRDS2.png ") 7 img2 = img2.convert('RGBA') 8 9 img = Image.blend(img1, img2, 0.3) 10 img.show() 11 img.save( "C:\\images\\BLEND.png")
-
下一个插图显示了混合前的两个图像,在代码中用
img1
和img2
表示。混合前的桥梁和飞翔的鸟类的单独图像。
-
第 3 到 7 行打开了要混合的两个输入图像。注意,我们已经将两个图像都转换为
RGBA
模式。这不必一定是RGBA
模式。我们可以指定模式,如'RGB'或'L'。然而,要求两个图像具有相同的size
和mode
。 -
图像在第 9 行使用 PIL 中的
Image.blend
方法进行混合。blend
方法的前两个参数是两个表示要混合的图像的Image
对象。第三个参数定义了透明度因子alpha
。在这个例子中,桥梁的图像是我们想要关注的主体图像。因此,alpha
因子被定义为在创建最终图像时对飞翔的鸟类的图像应用更多的透明度。alpha
因子可以有一个介于0.0
到1.0
之间的值。请注意,在渲染输出图像时,第二个图像img2
被乘以这个alpha
值,而第一个图像被乘以1 - alpha
。这可以用以下方程表示。blended_img = img1 * (1 alpha) + img2* alpha
-
因此,如果我们选择一个例如 0.8 的
alpha
因子,这意味着鸟类将比桥梁更不透明。尝试更改alpha
因子以查看它如何改变结果图像。alpha = 0.3
的结果图像如下:混合图像显示鸟类在桥梁上飞翔。
-
由于在创建图像时应用了透明效果,图片看起来有点单调。如果你将输入图像转换为
mode L
,结果图像将看起来更好,但它将以灰度形式呈现。这将在下一幅插图中展示。当两个输入图像都具有模式 L 时,混合的灰度图像。
发生了什么?
混合是重要的图像增强功能。通过示例,我们完成了创建混合图像的任务。我们学习了使用Image.blend
方法,并应用透明度因子alpha
来完成这项任务。本章学到的技术在本章中将会非常有用。在下一节中,我们将应用混合技术来创建透明图像。
创建透明图像
在上一节中,我们学习了如何将两个图像混合在一起。在本节中,我们将更进一步,看看相同的blend
功能如何用于创建透明图像!具有模式 RGBA 的图像定义了一个alpha
通道。可以通过调整此通道数据来改变图像的透明度。Image.putalpha()
方法允许为图像的alpha
通道定义新数据。我们将看到如何执行点操作以实现相同的效果。
是时候动手创建透明效果了
让我们编写几行代码,为输入图像添加透明效果。
-
我们将使用第二章中使用的其中一个图像。下载
0165_3_25_SMILEY.png
并将其重命名为SMILEY.png
。 -
使用以下代码:
1 import Image 2 3 def addTransparency(img, factor = 0.7 ): 4 img = img.convert('RGBA') 5 img_blender = Image.new('RGBA', img.size, (0,0,0,0)) 6 img = Image.blend(img_blender, img, factor) 7 return img 8 9 img = Image.open( "C:\\images\\SMILEY.png ") 10 11 img = addTransparency(img, factor =0.7)
-
在这个例子中,
addTransparency()
函数接受img
实例作为输入,并返回一个具有所需透明度级别的新图像实例。 -
现在我们来看看这个函数是如何工作的。在第 4 行,我们首先将图像模式转换为
RGBA
。如前所述,你可以在这里添加一个条件来检查图像是否已经处于RGBA
模式。 -
接下来,我们使用
Image.new
方法创建一个新的Image
类实例,名为image_blender
。它具有与输入图像相同的尺寸和模式。第三个参数代表颜色。在这里,我们指定透明度为0
。 -
在第 6 行,两个图像
img
(输入图像)和img_blender
通过应用一个常量alpha
值混合在一起。然后函数返回这个修改后的Image
实例。 -
比较了原始图像和具有透明效果图像。这些图像是 GIMP 编辑器中打开的图像的截图。这样做是为了让你清楚地了解透明效果。这些图像中的棋盘图案代表画布。注意画布如何在透明图像中显示。
-
添加透明度到图像的另一种简单方法,使用
Image.point
功能!在 Python 源文件中输入以下代码并执行它。1 import Image 2 img = Image.open( "C:\\images\\SMILEY.png ") 3 r, g, b, alpha = img.split() 4 alpha = alpha.point(lambda i: i>0 and 178) 5 img.putalpha(alpha) 6 img.save( "C:\\images\\Transparent_SMILEY.png ")
-
在这段新代码中,我们将原始图像分割成四个新的图像实例,每个实例包含一个图像通道数据(r,
g, b
, 或alpha
)。请注意,我们这里假设图像的模式是RGBA
。如果不是,你需要将此图像转换为 RGBA!作为一个练习,你可以在代码中添加这个检查。 -
接下来,在第 4 行,调用了
Image.point
方法。lambda
函数作用于alpha
通道数据。它将值设置为178
。这大约等于我们之前设置的alpha
因子 0.7。这里计算为int(255*0.7) )
。在修改单个图像通道部分,我们详细讨论了点操作。 -
在第 5 行,我们将新的
alpha
通道数据放回img
中。使用blend
和point
功能的结果图像将在下一幅插图显示。添加透明度前后的图像。
刚才发生了什么?
我们已经完成了向图像添加透明度效果。这是一种非常有用的图像增强,我们不时需要它。我们学习了如何使用两种不同的技术创建透明图像,即使用Image.blend
功能和使用Image.point
操作。在本节中获得的知识将在本章的后面应用。
使用图像蒙版制作复合材料
到目前为止,我们已经看到了如何将两张图像混合在一起。这是通过使用Image.blend
操作完成的,其中两个输入图像通过使用一个常数alpha
透明度因子进行混合。在本节中,我们将学习另一种将两张图像组合在一起的技术。在这里,我们使用一个定义透明度蒙版的图像实例作为第三个参数,而不是一个常数alpha
因子。另一个区别是输入图像不需要有相同的mode
。例如,第一张图像可以是L
模式,第二张是RGBA
模式。创建合成图像的语法是:
outImage = Image.composite(img1, img2, mask)
这里,复合方法的参数是Image
实例。蒙版指定为alpha
。蒙版图像实例的模式可以是1, L
或RGBA
。
行动时间 - 使用图像蒙版制作复合材料
我们将混合在另一个部分中混合的相同两张图像。为了尝试一些不同的事情,在合成图像中,我们将聚焦于飞翔的鸟儿而不是桥梁。
-
我们将使用与混合部分相同的输入图像集。
1 import Image 2 3 img1 = Image.open( "C:\\images\\BRIDGE2.png ") 4 img1 = img1.convert('RGBA') 5 6 img2 = Image.open( "C:\\images\\BIRDS2.png ") 7 img2 = img2.convert('RGBA') 8 9 r, g, b, alpha = img2.split() 10 alpha = alpha.point(lambda i: i>0 and 204) 11 12 img = Image.composite(img2, img1, alpha) 13 img.show()
-
代码直到第 7 行与混合示例中展示的代码相同。请注意,两个输入图像不需要具有相同的
mode
。在第 10 行,调用了Image.point
方法。lambda
函数作用于alpha
通道数据。第 9 行和第 10 行的代码与创建透明图像部分中展示的类似。请参考该部分以获取更多详细信息。唯一的区别是像素值被设置为204
。这修改了图像实例alpha
中的通道数据。如果图像要混合,这个204
的值大约相当于alpha
因子0.7
。这意味着桥梁将会有渐变效果,飞翔的鸟儿将在合成图像中突出显示。 -
你会注意到,我们并没有将修改后的
alpha
通道数据放回img2
中。而是在第 12 行,使用蒙版作为alpha
创建了合成图像。 -
下一个插图显示了带有飞翔鸟类图像强调的合成图像。
刚才发生了什么?
我们学习了如何使用alpha
蒙版创建两个图像的组合图像。这是通过使用Image.composite
功能实现的。
项目:水印制作工具
我们现在已经学到了足够多的图像增强技术,可以开始一个简单的项目来应用这些技术。让我们创建一个简单的命令行工具,一个“水印制作工具”。虽然我们称之为“水印制作工具”,但实际上它提供了一些更有用的功能。使用此工具,您可以将日期戳添加到图像中(使用此工具增强图像的日期)。它还允许在图像中嵌入自定义文本。该工具可以使用以下语法在命令行上运行:
python WaterMarkMaker.py [options]
其中,[options]
如下:
-
--image1:
提供画布的主要图像的文件路径。 -
--waterMark:
水印图像的文件路径(如果有)。 -
--mark_pos:
要嵌入的水印图像的左上角坐标。值应使用双引号指定,例如100, 50。 -
--text:
应出现在输出图像中的文本。 -
--text_pos:
要嵌入的文本的左上角坐标。值应使用双引号指定,例如100, 50。 -
--transparency:
水印的透明度因子(如果有) -
--dateStamp:
标志(True 或False
),用于确定是否在图像中插入日期戳。如果为True
,则将插入处理此图像时的日期戳。
下面的示例展示了如何使用所有指定选项运行此工具。
python WaterMarkMaker.py --image1= "C:\foo.png "
--watermark= "C:\watermark.png "
--mark_pos= "200, 200 "
--text= "My Text "
-text_pos= "10, 10 "
--transparency=0.4
--dateStamp=True
这将创建一个带有水印和文本的输出图像文件WATERMARK.png
,文本位于图像指定的锚点位置内。
行动时间 - 水印制作工具
想想我们需要的所有方法来完成这个任务。首先想到的是处理前面提到的命令行参数的函数。接下来,我们需要编写能够将水印图像添加到主图像中的代码。让我们称这个函数为 addWaterMark()
。按照类似的思路,我们还需要添加文本和日期戳到图像的方法。我们将分别称这些方法为 addText()
和 addDateStamp()
。有了这些信息,我们将开发代码来实现这个功能。在这个项目中,我们将把这个功能封装在一个类中,但这不是必需的。我们这样做是为了使这个工具在未来使用时更具可扩展性。
-
下载文件
WaterMarkMaker.py
。这个文件包含在这个项目中需要的代码。只需保留它以供以后使用。本节将不会讨论一些方法。如果你在开发这些方法时遇到困难,你总是可以回过头来参考这个文件。 -
打开一个新的 Python 源文件,声明以下类及其方法。现在只需创建空方法即可。随着我们继续前进,我们将扩展这些方法。
import Image, ImageDraw, ImageFont import os, sys import getopt from datetime import date class WaterMarkMaker: def __init__(self): pass def addText(self): pass def addDateStamp(self): pass def _addTextWorker(self, txt, dateStamp = False): pass def addWaterMark(self): pass def addTransparency(self, img): pass def createImageObjects(self): pass def _getMarkPosition(self, canvasImage, markImage): return def processArgs(self): pass def printUsage(self): pass
-
接下来,我们将在这个类的构造函数中编写代码。
def __init__(self): # Image paths self.waterMarkPath = '' self.mainImgPath = '' # Text to be embedded self.text = '' # Transparency Factor self.t_factor = 0.5 # Anchor point for embedded text self.text_pos = (0, 0) # Anchor point for watermark. self.mark_pos = None # Date stamp self.dateStamp = False # Image objects self.waterMark = None self.mainImage = None self.processArgs() self.createImageObjects() self.addText() self.addWaterMark() if self.dateStamp: self.addDateStamp() self.mainImage.save( "C:\\images\\WATERMARK.png ") self.mainImage.show()
-
代码是自我解释的。首先,初始化所有必要的属性,然后调用相关方法来创建带有水印和/或嵌入文本的图像。让我们按照在构造函数中调用的顺序编写这些方法。
-
processArgs()
方法处理命令行参数。你可以将此方法作为练习来编写。或者,你可以使用 Packt 网站上的WaterMarkMaker.py
文件中的代码。处理参数的方法应采用以下表格中所示的任务。在参考文件中,使用getopt
模块来处理这些参数。或者,你也可以使用 Python 的optparse
模块中的OptionParser
。参数 值 参数 值 image1
self.mainImgPath
text_pos
self.text_pos
waterMark
self.waterMarkPath
transparency
self.t_factor
mark_pos
self.mark_pos
dateStamp
self.dateStamp
text
self.text
-
printUsage()
方法只是打印如何运行这个工具。你可以轻松地编写这个方法。 -
现在我们来回顾一下
addText()
和_addTextWorker()
方法。请注意,为了清晰起见,代码示例中删除了一些注释。你可以参考WaterMarkMaker.py
中的代码以获取详细的注释。def addText(self): if not self.text: return if self.mainImage is None: print "\n Main Image not defined.Returning. " return txt = self.text self._addTextWorker(txt)
-
addText()
方法简单地通过提供从命令行接收到的self.text
参数来调用_addTextWorker()
。 -
_addTextWorker()
方法执行将文本嵌入图像中的主要处理。这个方法在以下代码中使用:1 def _addTextWorker(self, txt, dateStamp = False): 2 size = self.mainImage.size 3 color = (0, 0, 0) 4 textFont = ImageFont.truetype( "arial.ttf ", 50) 5 6 # Create an ImageDraw instance to draw the text. 7 imgDrawer = ImageDraw.Draw(self.mainImage) 8 textSize = imgDrawer.textsize(txt, textFont) 9 10 if dateStamp: 11 pos_x = min(10, size[0]) 12 pos_y = size[1] - textSize[0] 13 pos = (pos_x, pos_y) 14 else: 15 # We need to add text. Use self.text_pos 16 pos = self.text_pos 17 #finally add the text 18 imgDrawer.text(pos, txt, font=textFont) 19 20 if ( textSize[0] > size[0] 21 or textSize[1] > size[1] ): 22 print ( "\n Warning, the specified text " 23 "going out of bounds. " )
-
在第二章中,我们创建了一个包含文本字符串的新图像。它读作 "Not really a fancy text "。你还记得吗?在这里,我们编写了类似的代码,并进行了一些改进。ImageDraw.Draw 函数将 self.mainImage(一个 Image 实例)作为参数传递以创建一个 Draw 实例,imgDrawer。
在第 18 行,使用给定的字体将文本嵌入到指定的位置。Draw 实例的 text() 方法接受三个参数,即位置、文本和字体。在上一章中,我们已经使用了前两个参数。第三个参数字体是 PIL 中的 ImageFont 类的实例。
在第 4 行,我们创建了这个实例,指定了字体类型(arial.ttf)和字体大小(=50)。现在给定的文本字符串被添加到主图像上!
-
我们接下来要讨论的方法是
addDateStamp()
。它最终调用了相同的方法_addTextWorker()
。然而,这个日期戳的位置固定在图像的左下角,当然我们通过使用 Python 的datetime
模块来创建我们的日期字符串。下面的代码展示了这一过程,包括之前声明的导入语句。from datetime import date def addDateStamp(self): today = date.today() time_tpl = today.timetuple() year, month, day = map(str, time_tpl) datestamp = "%s/%s/%s "%(year,month, day) self._addTextWorker(datestamp, dateStamp = True)
-
在这个方法中的代码第一行创建了一个日期实例
today
,提供了今天的日期作为3-tuple
。类似于这样的:datetime.date(2010, 1, 20)
。接下来,我们调用date
实例的timetuple
方法。这个tuple
中的前三个值分别是year
(年)、month
(月)和day
(日)。 -
代码的其余部分只是将日期戳作为文本字符串进行处理,然后调用前面讨论的主要工作方法。
-
现在我们将回顾
addWaterMark()
方法中的代码。水印通常是一个半透明的图像,它出现在主图像上。有两种不同的方法来实现创建水印。下面的代码考虑了这两种方法。1 def addWaterMark(self): 2 # There are more than one way to achieve creating a 3 # watermark. The following flag,if True, will use 4 # Image.composite to create the watermark instead of a 5 # simple Image.paste 6 using_composite = False 7 8 if self.waterMark is None: 9 return 10 # Add Transparency 11 self.waterMark = self.addTransparency(self.waterMark) 12 # Get the anchor point 13 pos_x, pos_y = self._getMarkPosition(self.mainImage, 14 self.waterMark) 15 # Create the watermark 16 if not using_composite: 17 # Paste the image using the transparent 18 # watermark image as the mask. 19 self.mainImage.paste(self.waterMark, 20 (pos_x, pos_y), 21 self.waterMark) 22 else: 23 # Alternate method to create water mark. 24 # using Image.composite create a new canvas 25 canvas = Image.new('RGBA', 26 self.mainImage.size, 27 (0,0,0,0)) 28 # Paste the watermark on the canvas 29 canvas.paste(self.waterMark, (pos_x, pos_y)) 30 # Create a composite image 31 self.mainImage = Image.composite(canvas, 32 self.mainImage, 33 canvas)
-
要添加水印,首先我们需要将图像设置为透明。这是通过调用
addTransparency()
方法实现的。此方法还将图像的mode
改为RGBA
。这个方法在这里展示。它与我们在早期部分开发的方法几乎相同,当时我们使用 PIL 的混合功能将图像设置为透明。def addTransparency(self, img): img = img.convert('RGBA') img_blender = Image.new('RGBA', img.size, (0,0,0,0)) img = Image.blend(img_blender, img, self.t_factor) return img
-
接下来,在第 13 行,我们确定主图像上的锚点,水印的左上角将出现在这里。默认情况下,我们将水印的左下角与主图像的左下角对齐。你可以查看文件 WaterMarkMaker.py 中方法 _getMarkPosition() 的代码来了解这是如何实现的。向前推进,第 16-21 行之间的代码块使用粘贴功能创建水印。这是创建带水印图像的一种方法。Image.paste 函数提供的参数是待粘贴的图像、锚点以及遮罩。遮罩被选为水印图像本身,以便考虑透明度。否则,水印图像将是不透明的。以下插图比较了带有和不带有图像遮罩指定的结果图像。
使用 Image.paste 操作创建的带有和没有蒙版的最终图像。
-
接下来,在 else 条件块(第 22 至 33 行)中,我们使用 PIL 中的 Image.composite 功能嵌入水印。这里使用的示例水印图像的尺寸是 200x200 像素,而主图像的尺寸是 800x600 像素。要使用 composite()方法,我们需要将这些图像调整为相同的大小,同时确保将水印粘贴到指定的位置。如何实现这一点?首先要做的是创建一个画布图像来保存水印。画布图像与主图像大小相同。代码块 25-29 创建了画布并在适当的位置粘贴了水印。
最后,在第 31 行,使用画布图像实例作为 alpha 蒙版创建了合成图像。
-
-
现在让我们运行这个工具!你可以使用自己的图像文件作为主图像或水印。或者,你可以使用图像
0165_3_34_KokanPeak_for_WATERMARK.png
作为主图像,0165_3_38_SMILEY_small.png
作为水印图像。这次运行的命令行参数是:python WaterMarkMaker.py --image1= "C:\images\KokanPeak_for_WATERMARK.png " --text= "Peak " --text_pos= "10, 10 " --waterMark= "C:\\images\\SMILEY_small.png " --dateStamp=True
-
带有文本、日期戳和水印的最终图像将在下一幅插图显示。
最终处理后的图像带有文本、日期戳和水印。
刚才发生了什么?
我们创建了一个非常有用的实用工具,可以将水印和/或文本字符串和/或日期戳添加到输入图像中。我们在此以及之前章节中学到的图像处理技术中也使用了这些技术。特别是,应用了图像增强功能,如混合、创建合成图像和添加透明度来完成这项任务。此外,我们还使用了常见的功能,如粘贴图像、在图像上绘制文本等。
尝试使用 Watermark Maker Tool 进行更多操作
我们的 Watermark Maker 工具需要升级。扩展此应用程序以便它支持以下功能:
-
文本或日期戳的颜色目前是硬编码的。添加一个新的命令行参数,以便可以将文本颜色指定为可选参数。
-
添加一些标准默认选项,用于指定文本、日期戳和水印图像的锚点位置。这些选项可以是
TOP_RIGHT, TOP_LEFT, BOTTOM_RIGHT
和BOTTOM_LEFT
。 -
命令行选项列表太长了。添加代码以便所有参数都可以从文本文件中读取。
-
添加支持以便它可以批量处理图像以创建所需效果。
应用图像过滤器
在上一章中,执行图像缩放操作时使用了filter
参数。这个filter
决定了输出图像的质量。然而,只有四个filter
选项可用,并且其范围仅限于缩放操作。在本节中,将介绍一些额外的图像增强滤波器。这些是预定义的滤波器,可以直接应用于任何输入图像。以下是应用滤波器的基本语法。
img = Image.open('foo.jpg')
filtered_image = img.filter(FILTER)
在这里,我们通过过滤图像img
创建了一个新的图像filtered_image
。FILTER
参数可以是 PIL 的ImageFilter
模块中预定义的滤波器之一,用于过滤图像数据。PIL 提供了几个预定义的图像增强滤波器。这些可以大致分为以下几类。通过示例,我们将在接下来的章节中学习其中的一些。
-
模糊和锐化:
BLUR, SHARPEN, SMOOTH, SMOOTH_MORE
-
边缘检测和增强:
EDGE_ENHANCE, EDGE_ENHANCE_MORE, FIND_EDGES, CONTOUR
-
扭曲/特殊效果:
EMBOSS
PIL 源代码中的ImageFilter.py
文件定义了上述过滤器类。你可以通过调整这些过滤器类中的各种参数来创建自己的自定义过滤器。
filterargs = size, scale, offset, kernel
其中,kernel
是卷积核。在这里,'卷积'是一个数学运算,通过'kernel'矩阵对图像矩阵进行操作以产生结果矩阵。
矩阵的大小由大小参数指定。它以(宽度,高度)的形式指定。在当前 PIL 版本中,这可以是(3,3)或(5,5)的大小。每个像素的结果被scale
参数除以。这是一个可选参数。如果指定了offset
值,则将其值加到除以scale
参数后的结果上。
在一些图像增强滤波器示例中,我们将创建自己的自定义过滤器。
平滑
平滑图像意味着减少图像数据中的噪声。为此,对图像数据应用某些数学近似以识别图像中的重要模式。ImageFilter
模块定义了用于平滑图像的class SMOOTH
。PIL 为图像平滑滤波器指定了以下默认过滤器参数。
filterargs = (3, 3),
13,
0,
(1, 1, 1,
1, 5, 1,
1, 1, 1)
实践时间 - 平滑图像
让我们通过一个例子来演示如何对一个图像应用平滑滤波器。
-
从 Packt 网站下载图像文件
0165_3_Before_SMOOTHING.png
并将其保存为Before_SMOOTHING.png
。 -
这是一张从冲洗好的照片扫描的低分辨率图像。正如你所见,图像中有很多盐和胡椒噪声。我们将应用平滑滤波器来减少图像数据中的一些噪声。
-
在一个 Python 文件中添加以下代码。
import ImageFilter import Image img = Image.open( "C:\\images\\Before_SMOOTH.png ") img = img.filter(ImageFilter.SMOOTH) img.save( "C:\\images\\ch3\\After_SMOOTH.png") img.show()
-
代码中高亮显示的行是平滑滤波器应用于图像的位置。结果将在下一幅插图显示。
平滑前后的图片:
-
为了进一步减少噪声,你可以使用
ImageFilter.SMOOTH_MORE
或尝试多次重新应用ImageFilter.SMOOTH
直到你得到期望的效果。import ImageFilter import Image img = Image.open( "C:\\images\\0165_3_2_Before_SMOOTH.png ") i = 0 while i < 5: img = img.filter(ImageFilter.SMOOTH) i += 1 img.save( "C:\\images\\0165_3_3_After_SMOOTH_5X.png") img.show()
-
如插图所示,噪声进一步减少,但图像看起来有点模糊。因此,必须确定适当的平滑程度。
将结果图像与单次和多次平滑滤镜的对比。
刚才发生了什么?
我们学习了如何使用ImageFilter
模块中的平滑滤镜从图像数据中减少高级噪声。
锐化
在前面的章节中,我们学习了图像平滑技术。如果你想查看图像中的更细微的细节,可以在图像上应用锐化滤镜。与图像平滑滤镜类似,PIL 提供了用于锐化的预定义滤镜,称为ImageFilter.SHARPEN
。锐化图像的基本语法如下:
img = img.filter(ImageFilter.SHARPEN)
你可以在前面章节中平滑多次的图像上尝试这个滤镜。
模糊
通常,模糊会使图像失去焦点。在 PIL 中,用于此目的的预定义滤镜是ImageFilter.BLUR
。这通常在你想淡化背景以突出前景中的某个对象时很有用。语法与其他滤镜使用的语法类似。
img = img.filter(ImageFilter.BLUR)
以下插图显示了此滤镜的效果。
应用模糊滤镜前后的图像对比:
边缘检测和增强
在本节中,我们将学习一些通用的边缘检测和增强滤镜。边缘增强滤镜提高了边缘对比度。它增加了非常接近边缘区域的对比度。这使得边缘更加突出。边缘检测算法在图像的像素数据中寻找不连续性。例如,它寻找亮度中的急剧变化以识别边缘。
行动时间 - 检测和增强边缘
让我们看看边缘检测和增强滤镜如何修改图片的数据。我们将使用的照片是一片叶子的特写。原始照片在下一张插图显示。在这张图像上应用边缘检测滤镜会产生一种效果,其中只有边缘被突出显示,而图像的其余部分被渲染为黑色。
-
从 Packt 网站下载图像
0165_3_6_Before_EDGE_ENHANCE.png
并将其保存为Before_EDGE_ENHANCE.png
。 -
在 Python 文件中添加以下代码。
1 import Image 2 import ImageFilter 3 import os 4 paths = [ "C:\images\Before_EDGE_ENHANCE.png ", 5 "C:\images\After_EDGE_ENHANCE.png ", 6 "C:\images\EDGE_DETECTION_1.png ", 7 "C:\images\EDGE_DETECTION_2.png " 8 ] 9 paths = map(os.path.normpath, paths) 10 11 ( imgPath ,outImgPath1, 12 outImgPath2, outImgPath3) = paths 13 img = Image.open(imgPath) 14 img1 = img.filter(ImageFilter.FIND_EDGES) 15 img1.save(outImgPath1) 16 17 img2 = img.filter(ImageFilter.EDGE_ENHANCE) 18 img2.save(outImgPath2) 19 20 img3 = img2.filter(ImageFilter.FIND_EDGES) 21 img3.save(outImgPath3)
-
第 14 行使用
FIND_EDGES
滤镜修改图像数据,然后将结果图像保存。 -
接下来,我们修改原始图像数据,以便使叶子中的更多叶脉变得可见。这是通过应用
ENHANCE_EDGES
滤镜(第 17 行)实现的。 -
在第 20 行,将
FIND_EDGES
滤镜应用于边缘增强的图像。下一幅插图显示了比较后的结果图像。a) 第一行:应用边缘增强滤镜前后的图像 b) 第二行:由 ImageFilter.FIND_EDGES 滤镜检测到的边缘。
刚才发生了什么?
我们通过在ImageFilter
模块中应用EDGE_ENHANCE
滤镜来创建具有增强边缘的图像。我们还学习了如何使用边缘检测滤镜在图像中检测边缘。在下一节中,我们将应用一种特殊的边缘滤波器,该滤波器可以突出显示或变暗图像中检测到的边缘。它被称为浮雕滤镜。
浮雕
在图像处理中,浮雕是一种使图像具有 3D 外观的过程。图像中的边缘看起来像是凸起在图像表面之上。这种视觉错觉是通过突出显示或变暗图像中的边缘来实现的。以下插图显示了原始和浮雕图像。注意,浮雕图像中字符的边缘要么被突出显示,要么被变暗,以达到预期的效果。
ImageFilter
模块提供了一个预定义的滤镜ImageFilter.EMBOSS
,用于为图像实现浮雕效果。此滤镜的卷积核大小为(3, 3),默认的滤镜参数为:
filterargs = (3, 3), 1, 128, (
-1, 0, 0,
0, 1, 0,
0, 0, 0
)
行动时间 - 浮雕
-
从 Packt 网站下载图像
0165_3_4_Bird_EMBOSS.png
并将其保存为Bird_EMBOSS.png
。 -
在 Python 文件中添加以下代码:
1 import os, sys 2 import Image 3 import ImageFilter 4 imgPath = "C:\images\Bird_EMBOSS.png " 5 outImgPath = "C:\images\Bird_EMBOSSED.png " 6 imgPath = os.path.normpath(imgPath) 6 outImgPath = os.path.normpath(outImgPath) 7 bird = Image.open(imgPath) 8 bird = bird.filter(ImageFilter.EMBOSS) 9 bird.save(outImgPath) 10 bird.show()
-
在第 9 行,将浮雕滤镜
ImageFilter.EMBOSS
应用于图像对象bird
。下一幅插图显示了鸟的浮雕图像。使用 ImageFilter.EMBOSS 创建的鸟的原始和浮雕图像。
刚才发生了什么?
我们对一个图像应用了浮雕滤镜,创建了一个浮雕图像。如前文所述,该滤镜通过改变各种边缘的亮度使它们看起来被突出显示或变暗。这创造了一种视觉错觉,使图像看起来像是在表面之上凸起。
添加边框
你更喜欢怎样查看家庭照片?是作为裸露的图片还是装在漂亮的相框里?在ImageOps
模块中,PIL 提供了在图片周围添加普通边框的初步功能。以下是实现此功能的语法:
img = ImageOps.expand(img, border, fill)
此代码在图像周围创建了一个边框。内部,PIL 创建了一个具有相应尺寸的图像:
new_width = ( right_border_thickness + image_width +
left_border_thickness )
new_height = ( top_border_thickness + image_height +
bottom_border_thickness )
然后,将原始图像粘贴到这个新图像上以创建边框效果。前述代码中的border
参数表示边框厚度(以像素为单位)。在这个例子中它是均匀的,设置为左、右、上、下边框均为 20 像素。fill
参数指定边框颜色。它可以是一个范围在0
到255
之间的数字,表示像素颜色,其中0
表示黑色边框,255
表示白色边框。或者,你可以指定一个表示颜色的字符串,例如red
表示红色,等等。
执行动作 - 将图片封装在相框中
让我们开发代码,为图片添加边框。
-
下载图像
0165_3_15_COLOR_TWEAK.png
并将其重命名为FLOWER.png
。 -
在 Python 源文件中添加以下代码。确保修改代码以适当地指定输入和输出路径。
1 import Image, ImageOps 2 img = Image.open( "C:\\images\\FLOWER.png ") 3 img = ImageOps.expand(img, border=20, fill='black') 4 img = ImageOps.expand(img, border=40, fill='silver') 5 img = ImageOps.expand(img, border=2, fill='black') 6 img.save( "C:\\images\\PHOTOFRAME.png ") 7 img.show()
-
在此代码片段中,创建了三个堆叠的边框。最内层的边框层以黑色渲染。这是故意选择较暗的颜色。
-
接下来,有一个中间层的边框,以较浅的颜色(在这个例子中是银色)渲染。这是通过第 4 行的代码实现的。它比最内层的边框更厚。
-
最外层的边框是通过第 5 行的代码创建的。它是一个非常薄的黑色层。
-
这三个边框层共同创造了一个相框的视觉错觉,使得边框看起来比原始图像突出。
-
下面的图像显示了将此边框添加到指定输入图像的结果,显示了在“相框”中封装前后的图像。
刚才发生了什么?
我们学习了如何为图像创建一个简单的边框。通过多次调用ImageOps.expand
,我们创建了一个多层边框,每一层的厚度和颜色都不同。通过这种方式,我们实现了图片看起来被包含在一个简单相框中的视觉错觉。
概述
本章教授了我们几个重要的图像增强技术,具体包括:
-
在大量示例的帮助下,我们学习了如何调整图像的颜色、亮度和对比度。
-
我们学习了如何混合图像,使用图像蒙版创建合成图像,以及如何添加透明度。
-
我们将学到的混合、粘贴和其他技术应用到开发一个有趣的小工具中。我们在该工具中实现了插入水印、文本或日期戳到图像的功能。
-
讨论了许多图像增强过滤器。通过使用代码片段,我们学习了如何从图像中减少高级噪声,增强边缘,添加锐化或模糊效果,浮雕图像,等等。
-
我们学习了其他一些有用的图像增强技巧,例如创建负片和为图像添加边框效果。
第四章.动画乐趣
卡通一直吸引着年轻人和老年人。动画是想象中的生物变得生动,带我们进入一个完全不同的世界。
动画是一系列快速连续显示的帧。这创造了一种视觉错觉,例如,物体似乎在移动。本章将介绍使用 Python 和 Pyglet 多媒体应用程序开发框架开发动画的基础知识。Pyglet 旨在执行 3D 操作,但在这本书中,我们将用它来开发非常简单的 2D 动画。
在本章中,我们将:
-
学习 Pyglet 框架的基础知识。这将用于开发创建或播放动画的代码。
-
学习如何播放现有的动画文件,并使用一系列图像创建动画。
-
在“保龄球动画”项目中工作,其中可以使用键盘输入来控制动画。
-
使用单个图像的不同区域开发创建动画的代码。
-
从事一个令人兴奋的项目,该项目将展示一辆汽车在雷雨中移动的动画。这个项目将涵盖本章中涵盖的许多重要内容。
那么,让我们开始吧。
安装先决条件
本节将介绍安装 Pyglet 的先决条件。
Pyglet
Pyglet 提供了一个用于使用 Python 进行多媒体应用程序开发的 API。它是一个基于 OpenGL 的库,支持多个平台。它主要用于开发游戏应用程序和其他图形丰富的应用程序。可以从www.pyglet.org/download.html
下载 Pyglet。安装 Pyglet 版本 1.1.4 或更高版本。Pyglet 的安装相当简单。
Windows 平台
对于 Windows 用户,Pyglet 的安装很简单,使用二进制分发Pyglet 1.1.4.msi
或更高版本。
注意
您应该安装 Python 2.6。对于 Python 2.4,还有一些额外的依赖项。我们不会在本书中讨论它们,因为我们使用 Python 2.6 来构建多媒体应用程序。
如果您从源代码安装 Pyglet,请参阅下一小节其他平台下的说明。
其他平台
Pyglet 网站为 Mac OS X 提供了二进制分发文件。下载并安装pyglet-1.1.4.dmg
或更高版本。
在 Linux 上,如果您的操作系统软件仓库中有 Pyglet 1.1.4 或更高版本,则安装它。否则,可以从源 tarball 安装,如下所示:
-
下载并解压缩
pyglet-1.1.4.tar.gz
或更高版本的 tarball。 -
确保在 shell 中
python
是一个可识别的命令。否则,将PYTHONPATH
环境变量设置为正确的 Python 可执行路径。 -
在 shell 窗口中,切换到提到的提取目录,然后运行以下命令:
python setup.py install
- 使用 Pyglet 源 tarball 中的 readme/install 指令文件审查后续的安装说明。
小贴士
如果您已安装setuptools
包(http://pypi.python.org/pypi/setuptools),则 Pyglet 的安装应该非常简单。然而,为此,您将需要一个 Pyglet 的运行时egg
文件。但是,Pyglet 的egg
文件在pypi.python.org
上不可用。如果您能获取到 Pyglet 的egg
文件,可以通过在 Linux 或 Mac OS X 上运行以下命令来安装它。您需要管理员权限来安装此包:
$sudo easy_install -U pyglet
安装需求概述
以下表格展示了根据版本和平台的不同,安装需求。
包名 | 下载位置 | 版本 | Windows 平台 | Linux/Unix/OS X 平台 |
---|---|---|---|---|
Python | python.org/download/releases/ |
2.6.4(或任何 2.6.x 版本) | 使用二进制分发安装 | 从二进制安装;也可以安装额外的开发包(例如,在基于 rpm 的 Linux 发行版中,包名中包含python-devel )。从源 tarball 构建和安装。 |
Pyglet | www.pyglet.org/download.html |
1.1.4 或更高版本 | 使用二进制分发(.msi 文件)安装 |
Mac: 使用磁盘映像文件(.dmg 文件)安装。Linux: 使用源 tarball 构建和安装。 |
测试安装
在继续之前,请确保 Pyglet 已正确安装。为了测试这一点,只需从命令行启动 Python 并输入以下命令:
>>>import pyglet
如果这个导入成功,我们就准备就绪了!
Pyglet 入门
Pyglet 提供了一个使用 Python 进行多媒体应用程序开发的 API。它是一个基于 OpenGL 的库,可以在多个平台上运行。它主要用于开发游戏和其他图形丰富的应用程序。我们将介绍 Pyglet 框架的一些重要方面。
重要组件
我们将简要讨论我们将使用的 Pyglet 的一些重要模块和包。请注意,这仅仅是 Pyglet 框架的一小部分。请查阅 Pyglet 文档以了解更多关于其功能的信息,因为这超出了本书的范围。
窗口
pyglet.window.Window
模块提供了用户界面。它用于创建具有 OpenGL 上下文的窗口。Window
类有 API 方法来处理各种事件,如鼠标和键盘事件。窗口可以以正常或全屏模式查看。以下是一个创建Window
实例的简单示例。您可以通过在构造函数中指定width
和height
参数来定义大小。
win = pyglet.window.Window()
使用 OpenGL 调用glClearColor
可以设置图像的背景颜色,如下所示:
pyglet.gl.glClearColor(1, 1, 1, 1)
这将设置白色背景颜色。前三个参数是红色、绿色和蓝色颜色值。而最后一个值代表 alpha。以下代码将设置灰色背景颜色。
pyglet.gl.glClearColor(0.5, 0.5, 0.5, 1)
The following illustration shows a screenshot of an empty window with a gray background color.
图像
The pyglet.image
module enables the drawing of images on the screen. The following code snippet shows a way to create an image and display it at a specified position within the Pyglet window.
img = pyglet.image.load('my_image.bmp')
x, y, z = 0, 0, 0
img.blit(x, y, z)
后续部分将介绍pyglet.image
模块支持的一些重要操作。
精灵
这是一个另一个重要的模块。它用于在之前讨论的 Pyglet 窗口中显示图像或动画帧。它是一个图像实例,允许我们在 Pyglet 窗口的任何位置定位图像。精灵也可以旋转和缩放。可以创建多个相同图像的精灵并将它们放置在窗口的不同位置和不同的方向上。
动画
Animation
模块是pyglet.image
包的一部分。正如其名所示,pyglet.image.Animation
用于从一个或多个图像帧创建动画。有不同方式来创建动画。例如,可以从一系列图像或使用AnimationFrame
对象创建。我们将在本章后面学习这些技术。可以在 Pyglet 窗口中创建并显示动画精灵。
AnimationFrame
这从给定的图像创建一个动画的单帧。可以从这样的AnimationFrame
对象创建动画。以下代码行显示了一个示例。
animation = pyglet.image.Animation(anim_frames)
anim_frames
是一个包含AnimationFrame
实例的列表。
时钟
在许多其他功能中,此模块用于安排在指定时间调用函数。例如,以下代码每秒调用moveObjects
方法十次。
pyglet.clock.schedule_interval(moveObjects, 1.0/10)
显示图像
在图像子部分中,我们学习了如何使用image.blit
加载图像。然而,图像块拷贝是一种效率较低的绘图方式。通过创建Sprite
实例,有更好的和推荐的方式来显示图像。可以为绘制相同图像创建多个Sprite
对象。例如,相同的图像可能需要在窗口的多个位置显示。每个这样的图像都应该由单独的Sprite
实例表示。以下简单的程序仅加载一个图像并在屏幕上显示代表此图像的Sprite
实例。
1 import pyglet
2
3 car_img= pyglet.image.load('images/car.png')
4 carSprite = pyglet.sprite.Sprite(car_img)
5 window = pyglet.window.Window()
6 pyglet.gl.glClearColor(1, 1, 1, 1)
7
8 @window.event
9 def on_draw():
10 window.clear()
11 carSprite.draw()
12
13 pyglet.app.run()
在第 3 行,使用pyglet.image.load
调用打开图像。在第 4 行创建与该图像对应的Sprite
实例。第 6 行的代码为窗口设置白色背景。on_draw
是一个 API 方法,当窗口需要重绘时会被调用。在这里,图像精灵被绘制到屏幕上。下一图显示了 Pyglet 窗口中的加载图像。
提示
在本章和其他章节的各个示例中,文件路径字符串是硬编码的。我们使用了正斜杠作为文件路径。尽管这在 Windows 平台上有效,但惯例是使用反斜杠。例如,images/car.png
表示为 images\car.png
。此外,您还可以使用 Python 中的 os.path.join
方法指定文件的完整路径。无论您使用什么斜杠,os.path.normpath
都会确保它修改斜杠以适应平台。以下代码片段展示了 os.path.normpath
的使用:
import os
original_path = 'C:/images/car.png'
new_path = os.path.normpath(original_path)
前面的图像展示了 Pyglet 窗口显示的静态图像。
鼠标和键盘控制
Pyglet 的 Window
模块实现了一些 API 方法,这些方法允许用户在播放动画时输入。API 方法如 on_mouse_press
和 on_key_press
用于在动画期间捕获鼠标和键盘事件。这些方法可以被覆盖以执行特定操作。
添加音效
Pyglet 的 media
模块支持音频和视频播放。以下代码加载一个媒体文件并在动画期间播放它。
1 background_sound = pyglet.media.load(
2 'C:/AudioFiles/background.mp3',
3 streaming=False)
4 background_sound.play()
在第 3 行提供的第二个可选参数在加载媒体时将媒体文件完全解码到内存中。如果媒体在动画期间需要播放多次,这很重要。API 方法 play()
开始流式传输指定的媒体文件。
使用 Pyglet 的动画
Pyglet 框架提供了一系列开发动画所需的模块。其中许多在早期章节中已简要讨论。现在让我们学习使用 Pyglet 创建 2D 动画的技术。
查看现有动画
如果您已经有一个动画,例如,.gif
文件格式,它可以直接使用 Pyglet 加载和显示。这里要使用的 API 方法是 pyglet.image.load_animation
。
查看现有动画的行动时间
这将是一个简短的练习。本节的目标是培养对使用 Pyglet 查看动画的初步理解。那么,让我们开始吧。
-
从 Packt 网站下载文件
SimpleAnimation.py
。同时下载文件SimpleAnimation.gif
并将其放置在子目录images
中。代码如下所示:1 import pyglet 2 3 animation = pyglet.image.load_animation( 4 "images/SimpleAnimation.gif") 5 6 # Create a sprite object as an instance of this animation. 7 animSprite = pyglet.sprite.Sprite(animation) 8 9 # The main pyglet window with OpenGL context 10 w = animSprite.width 11 h = animSprite.height 12 win = pyglet.window.Window(width=w, height=h) 13 14 # r,g b, color values and transparency for the background 15 r, g, b, alpha = 0.5, 0.5, 0.8, 0.5 16 17 # OpenGL method for setting the background. 18 pyglet.gl.glClearColor(r, g, b, alpha) 19 20 # Draw the sprite in the API method on_draw of 21 # pyglet.Window 22 @win.event 23 def on_draw(): 24 win.clear() 25 animSprite.draw() 26 27 pyglet.app.run()
-
代码是自我解释的。在第 3 行,API 方法 image.load_animation 使用指定的动画文件创建了一个 image.Animation 类的实例。对于这个动画,在第 7 行创建了一个 Sprite 对象。第 12 行创建的 Pyglet 窗口将用于显示动画。这个窗口的大小由 animSprite 的高度和宽度指定。使用 OpenGL 调用 glClearColor 设置窗口的背景颜色。
-
接下来,我们需要将这个动画精灵绘制到 Pyglet 窗口中。
pyglet.window
定义了一个 API 方法on_draw
,当发生事件时会被调用。在 25 行调用了动画Sprite
的draw()
方法,以在屏幕上渲染动画。第 22 行的代码很重要。装饰器@win.event
允许我们在事件发生时修改pyglet.window.Window
的on_draw
API 方法。最后,第 27 行运行了这个应用程序。小贴士
你可以使用像 GIMP 这样的免费图像编辑软件包创建自己的动画文件,例如
SimpleAnimation.gif
。这个动画文件是使用 GIMP 2.6.7 创建的,通过在每个单独的图层上绘制每个角色,然后使用 滤镜 | 动画 | 混合 将所有图层混合在一起。 -
将文件
SimpleAnimation.py
与动画文件SimpleAnimation.gif
放在同一目录下,然后按照以下方式运行程序:$python SimpleAnimation.py
- 这将在 Pyglet 窗口中显示动画。你可以使用除 SimpleAnimation.gif 之外的动画文件。只需修改此文件中相关的代码,或者添加代码以接受任何 GIF 文件作为此程序的命令行参数。下一幅插图显示了该动画在不同时间间隔的一些帧。
上一幅图像是不同时间间隔运行动画的屏幕截图。
刚才发生了什么?
我们通过一个示例演示了如何使用 Pyglet 加载并查看已创建的动画文件。这个简短的练习让我们了解了使用 Pyglet 查看动画的一些初步知识。例如,我们学习了如何创建 Pyglet 窗口并使用 pyglet.Sprite
对象加载动画。这些基础知识将在本章中用到。
使用一系列图像进行动画
API 方法 Animation.from_image_sequence
允许使用一系列连续的图像创建动画。每个图像都作为动画中的一帧显示,依次排列。可以在创建动画对象时指定每帧显示的时间,也可以在动画实例创建后设置。
动作时间 - 使用一系列图像进行动画
让我们开发一个工具,可以创建动画并在屏幕上显示。这个工具将使用给定的图像文件创建并显示动画。每个图像文件都将作为动画中的一帧显示,持续指定的时间。这将是一个有趣的动画,展示了一个带有摆锤的祖父钟。我们将使用其他东西来动画化摆锤的振荡,包括使表盘保持静止。这个动画只有三个图像帧;其中两个显示了摆锤在相反的极端位置。这些图像的顺序如图所示。
动画的时间图像帧出现在前面的图像中。
-
从 Packt 网站下载文件
ImageSequenceAnimation.py
。 -
该文件中的代码如下所示。
1 import pyglet 2 3 image_frames = ('images/clock1.png', 4 'images/clock2.png', 5 'images/clock3.png') 6 7 # Create the list of pyglet images 8 images = map(lambda img: pyglet.image.load(img), 9 image_frames) 10 11 # Each of the image frames will be displayed for 0.33 12 # seconds 13 # 0.33 seconds chosen so that the 'pendulam in the clock 14 # animation completes one oscillation in ~ 1 second ! 15 16 animation = pyglet.image.Animation.from_image_sequence( 17 images, 0.33) 18 # Create a sprite instance. 19 animSprite = pyglet.sprite.Sprite(animation) 20 21 # The main pyglet window with OpenGL context 22 w = animSprite.width 23 h = animSprite.height 24 win = pyglet.window.Window(width=w, height=h) 25 26 # Set window background color to white. 27 pyglet.gl.glClearColor(1, 1, 1, 1) 28 29 # The @win.event is a decorator that helps modify the API 30 # methods such as 31 # on_draw called when draw event occurs. 32 @win.event 33 def on_draw(): 34 win.clear() 35 animSprite.draw() 36 37 pyglet.app.run()
-
元组
image_frames
包含图像的路径。第 8 行的map
函数调用为每个图像路径创建 pyglet.image 对象,并将结果图像存储在列表中。在第 16 行,使用 API 方法Animation.from_image_sequence
创建动画。此方法将图像对象列表作为参数。另一个可选参数是每帧显示的秒数。我们将此时间设置为每张图像 0.33 秒,以便整个动画循环的总时间接近 1 秒。因此,在动画中,摆锤的一次完整振荡将在大约一秒内完成。我们已经在早期部分讨论了其余的代码。$python ImageSequenceAnimation.py
-
将图像文件放置在包含文件
ImageSequenceAnimation.py
的目录下的子目录images
中。然后使用以下命令运行程序:- 你将在窗口中看到一个带有摆动摆锤的时钟。动画将循环进行,关闭窗口将结束动画。
刚才发生了什么?
通过快速显示静态图像,我们刚刚创建了一个类似“翻页书”的卡通!我们开发了一个简单的实用工具,它接受一系列图像作为输入,并使用 Pyglet 创建动画。为了完成这个任务,我们使用了Animation.from_image_sequence
来创建动画,并重新使用了查看现有动画部分的大部分框架。
单个图像动画
想象一下你正在制作一部卡通电影,你想要动画化箭头或子弹击中目标的动作。在这种情况下,通常只有一个图像。所需的动画效果是通过执行适当的平移或旋转图像来实现的。
动作时间 - 弹跳球动画
让我们创建一个简单的“弹跳球”动画。我们将使用单个图像文件,ball.png
,可以从 Packt 网站下载。此图像的像素尺寸为 200x200,是在透明背景上创建的。以下截图显示了在 GIMP 图像编辑器中打开的此图像。球上的三个点标识了其侧面。我们将看到为什么这是必要的。想象这是一个用于保龄球的球。
在 GIMP 中打开的球图像如图所示。球的像素大小为 200x200。
-
从 Packt 网站下载文件
SingleImageAnimation.py
和ball.png
。将ball.png
文件放置在SingleImageAnimation.py
所在目录的子目录 'images' 中。 -
以下代码片段显示了代码的整体结构。
1 import pyglet 2 import time 3 4 class SingleImageAnimation(pyglet.window.Window): 5 def __init__(self, width=600, height=600): 6 pass 7 def createDrawableObjects(self): 8 pass 9 def adjustWindowSize(self): 10 pass 11 def moveObjects(self, t): 12 pass 13 def on_draw(self): 14 pass 15 win = SingleImageAnimation() 16 # Set window background color to gray. 17 pyglet.gl.glClearColor(0.5, 0.5, 0.5, 1) 18 19 pyglet.clock.schedule_interval(win.moveObjects, 1.0/20) 20 21 pyglet.app.run()
-
虽然这不是必需的,但我们将将事件处理和其他功能封装在
SingleImageAnimation
类中。要开发的应用程序很短,但通常,这是一种良好的编码实践。这也将有利于代码的任何未来扩展。在第 14 行创建了一个SingleImageAnimation
实例。这个类是从pyglet.window.Window
继承的。它封装了我们这里需要的功能。类重写了 API 方法on_draw
。当窗口需要重绘时调用on_draw
。请注意,我们不再需要在on_draw
方法上方使用如@win.event
这样的装饰器语句,因为窗口 API 方法只是通过这个继承类被重写。 -
SingleImageAnimation
类的构造函数如下:1 def __init__(self, width=None, height=None): 2 pyglet.window.Window.__init__(self, 3 width=width, 4 height=height, 5 resizable = True) 6 self.drawableObjects = [] 7 self.rising = False 8 self.ballSprite = None 9 self.createDrawableObjects() 10 self.adjustWindowSize()
-
如前所述,
SingleImageAnimation
类继承自pyglet.window.Window
。然而,其构造函数并不接受其超类支持的所有参数。这是因为我们不需要更改大多数默认参数值。如果你想进一步扩展此应用程序并需要这些参数,你可以通过将它们作为__init__
参数添加来实现。构造函数初始化一些实例变量,然后调用方法创建动画精灵和调整窗口大小。 -
createDrawableObjects
方法使用ball.png
图像创建一个精灵实例。1 def createDrawableObjects(self): 2 """ 3 Create sprite objects that will be drawn within the 4 window. 5 """ 6 ball_img= pyglet.image.load('images/ball.png') 7 ball_img.anchor_x = ball_img.width / 2 8 ball_img.anchor_y = ball_img.height / 2 9 10 self.ballSprite = pyglet.sprite.Sprite(ball_img) 11 self.ballSprite.position = ( 12 self.ballSprite.width + 100, 13 self.ballSprite.height*2 - 50) 14 self.drawableObjects.append(self.ballSprite)
-
图像实例的
anchor_x
和anchor_y
属性被设置为图像有一个锚点正好在其中心。这在稍后旋转图像时将很有用。在第 10 行,创建了精灵实例self.ballSprite
。稍后,我们将设置 Pyglet 窗口的宽度和高度为精灵宽度的两倍和高度的三倍。图像在窗口中的位置在第 11 行设置。初始位置选择如图所示。在这种情况下,只有一个 Sprite 实例。然而,为了使程序更通用,维护了一个名为self.drawableObjects
的可绘制对象列表。 -
为了继续上一步的讨论,我们现在将回顾
on_draw
方法。def on_draw(self): self.clear() for d in self.drawableObjects: d.draw()
-
如前所述,on_draw 函数是 pyglet.window.Window 类的 API 方法,当窗口需要重绘时被调用。这里重写了这个方法。self.clear()调用清除了窗口中之前绘制的所有内容。然后,在 for 循环中绘制列表 self.drawableObjects 中的所有 Sprite 对象。
- 前面的图片展示了动画中球的初始位置。
-
方法
adjustWindowSize
设置了 Pyglet 窗口的width
和height
参数。代码是自我解释的:def adjustWindowSize(self): w = self.ballSprite.width * 3 h = self.ballSprite.height * 3 self.width = w self.height = h
-
到目前为止,我们已经为动画的播放设置了所有必要的东西。现在轮到有趣的部分了。我们将改变代表图片的精灵的位置,以实现动画效果。在动画过程中,图片也会被旋转,以使其看起来像是一个自然弹跳的球。
1 def moveObjects(self, t): 2 if self.ballSprite.y - 100 < 0: 3 self.rising = True 4 elif self.ballSprite.y > self.ballSprite.height*2 - 50: 5 self.rising = False 6 7 if not self.rising: 8 self.ballSprite.y -= 5 9 self.ballSprite.rotation -= 6 10 else: 11 self.ballSprite.y += 5 12 self.ballSprite.rotation += 5
-
这个方法计划在程序中以每秒 20 次的频率被调用。
pyglet.clock.schedule_interval(win.moveObjects, 1.0/20)
- 首先,球被放置在顶部附近。动画应该逐渐下落,碰到底部后弹起。之后,它继续向上运动,在顶部附近的一个边界处碰撞,然后再次开始向下运动。从第 2 行到第 5 行的代码块检查 self.ballSprite 的当前 y 位置。如果它触碰到上限,self.rising 标志被设置为 False。同样,当触碰到下限,标志被设置为 True。这个标志随后被下一个代码片段用来增加或减少 self.ballSprite 的 y 位置。
-
高亮显示的代码行旋转
Sprite
实例。当前的旋转角度根据给定值增加或减少。这就是为什么我们将图像锚点anchor_x
和anchor_y
设置在图像中心的原因。Sprite
对象尊重这些图像锚点。如果锚点没有这样设置,球在最终动画中看起来会摇摆不定。 -
一旦所有部件都到位,可以从命令行运行程序:
$python SingleImageAnimation.py
- 这将弹出一个窗口,播放弹跳球动画。接下来的插图显示了动画中球下落时的几个中间帧。
- 这将弹出一个窗口,播放弹跳球动画。接下来的插图显示了动画中球下落时的几个中间帧。
刚才发生了什么?
我们学习了如何仅使用一张图片来创建动画。一个球的形象由一个精灵实例表示。然后这个精灵在屏幕上被平移和旋转,以实现弹跳球的动画。整个功能,包括事件处理,都被封装在类SingleImageAnimation
中。
项目:一个简单的保龄球动画
是时候进行一个小项目了。我们将重用 Single Image Animation 部分的大部分代码和一些其他内容,以创建一个动画,其中滚动的球在保龄球游戏中击中球柱。尽管本章涵盖了动画,但这个项目将让你初步了解如何将动画转变为游戏。这并不是一个真正的游戏,但它将涉及一些用户交互来控制动画。
保龄球动画的起始位置,显示了球和球图片。
动作时间 - 一个简单的保龄球动画
让我们开发这个应用程序的代码。如前所述,大部分代码来自 Single Image Animation 部分。因此,我们只讨论创建保龄球动画所需的新和修改的方法。
-
从 Packt 网站下载 Python 源文件
BowlingAnimation.py
。整体类设计与在 Single Image Animation 部分开发的设计相同。我们只讨论新的和修改的方法。你可以从该文件中查看其余的代码。 -
此外,下载本项目中使用的图像文件。这些文件是
ball.png
和pin.png
。将这些文件放置在子目录images
中。images
目录应放置在上述 Python 源文件所在的目录中。 -
类的
__init__
方法与SingleImageAnimation
类的__init__
方法相同。这里唯一的改变是初始化以下标志:self.paused = False self.pinHorizontal = False
-
后续使用 self.pinHorizontal 标志来检查球是否将球撞倒。而 self.paused 用于根据其值暂停或恢复动画。
-
createDrawable
对象方法被修改为为球图片创建精灵实例。同时,调整球和球精灵的位置以适应我们的动画需求。代码如下所示:1 def createDrawableObjects(self): 2 ball_img= pyglet.image.load('images/ball.png') 3 ball_img.anchor_x = ball_img.width / 2 4 ball_img.anchor_y = ball_img.height / 2 5 6 pin_img = pyglet.image.load('images/pin.png') 7 pin_img.anchor_x = pin_img.width / 2 8 pin_img.anchor_y = pin_img.height / 2 9 10 self.ballSprite = pyglet.sprite.Sprite(ball_img) 11 self.ballSprite.position = (0 + 100, 12 self.ballSprite.height) 13 14 self.pinSprite = pyglet.sprite.Sprite(pin_img) 15 self.pinSprite.position = ( 16 (self.ballSprite.width*2 + 100, 17 self.ballSprite.height) ) 18 19 # Add these sprites to the list of drawables 20 self.drawableObjects.append(self.ballSprite) 21 self.drawableObjects.append(self.pinSprite)
-
代码块 6-8 创建了一个代表球图片的图像实例,并将其图像锚点设置在其中心。代表球和球图片的 Sprite 实例分别在 10 行和 14 行创建。它们的设置位置使得初始位置看起来与前面的插图所示相同。最后,将这些精灵添加到在 on_draw 方法中绘制的可绘制对象列表中。
-
接下来,让我们回顾
moveObjects
方法。与之前一样,这个方法每0.05
秒被调用一次。1 def moveObjects(self, t): 2 if self.pinHorizontal: 3 self.ballSprite.x = 100 4 self.pinSprite.x -= 100 5 6 if self.ballSprite.x < self.ballSprite.width*2: 7 if self.ballSprite.x == 100: 8 time.sleep(1) 9 self.pinSprite.rotation = 0 10 self.pinHorizontal = False 11 12 self.ballSprite.x += 5 13 self.ballSprite.rotation += 5 14 15 if self.ballSprite.x >= self.ballSprite.width*2: 16 self.pinSprite.rotation = 90 17 self.pinSprite.x += 100 18 self.pinHorizontal = True
-
当球精灵的 x 位置在 100 像素到 self.ballSprite 宽度的两倍之间时,会调用从第 6 行到第 13 行的 if 块。在第 12 行,self.ballSprite 的 x 位置增加了 5 像素。此外,精灵旋转了 5 度。这两个变换的组合产生了一种效果,我们在 Pyglet 窗口中看到球从左到右水平滚动。如前所述,球心的位置在 x = self.ballSprite.width*2 + 100 和 y = self.ballSprite.height。
从第 15 行到第 18 行的 if 块是球似乎击中柱子的地方。也就是说,球精灵中心的 x 坐标大约离柱子中心 100 像素。选择 100 像素值是为了考虑到球的半径。因此,一旦球击中柱子,柱子图像在第 16 行旋转了 90 度。这产生了一种视觉效果,看起来球把柱子撞倒了。柱子的 x 坐标增加了 100 像素,这样在柱子旋转后,球和柱子图像不会重叠。你还可以在这里做一些改进。将柱子精灵的 y 位置进一步向下移动,这样柱子看起来就像躺在地面上。在这个 if 块中,我们还设置了标志 self.pinHorizontal 为 True。下次调用 moveObjects 方法时,首先检查柱子是垂直还是水平。如果柱子是水平的,代码在第 2 行到第 4 行将恢复球和柱子的原始位置。这是为下一个动画循环做准备。在第 9 行,柱子被旋转回 0 度,而在第 10 行,标志 self.pinHorizontal 被重置为 False。
-
使用我们迄今为止开发的代码,以及来自
SingleImageAnimation
类的其余代码,如果你运行程序,它将显示保龄球动画。现在让我们为此动画添加一些控制。在构造函数中初始化了一个标志self.paused
。它将在这里使用。就像on_draw
一样,on_key_press
是pyglet.window.Window
的另一个 API 方法。它在这里被重写以实现暂停和恢复控制。1 def on_key_press(self, key, modifiers): 2 if key == pyglet.window.key.P and not self.paused: 3 pyglet.clock.unschedule(self.moveObjects) 4 self.paused = True 5 elif key == pyglet.window.key.R and self.paused: 6 pyglet.clock.schedule_interval( 7 self.moveObjects, 1.0/20) 8 self.paused = False
-
关键参数是用户按下的键盘键。从第 2 行到第 4 行的 if 块在按下 P 键时暂停动画。self.moveObjects 方法每 0.05 秒被调度一次。使用 pyglet.clock.unschedule 方法取消对该方法的调度。要恢复动画,在第 6 行调用 schedule_interval 方法。self.paused 标志确保多次按键不会对动画产生任何不良影响。例如,如果你多次按下 R 键,代码将仅忽略随后的按键事件。
-
请参阅文件
BowlingAnimation.py
以查看或开发其余代码,然后从命令行运行程序,如下所示:$python BowlingAnimation.py
-
这将在窗口中播放动画。按键盘上的 P 键暂停动画。要恢复暂停的动画,按 R 键。下一张插图显示了该动画中的几个中间帧。
保龄球动画的中间帧如图所示。
-
刚才发生了什么?
我们完成了一个简单但令人兴奋的项目,其中开发了一个碗击打钉子的动画。这是通过在屏幕上移动和旋转图像精灵来实现的。我们重新使用了 SingleImageAnimation
类中的几个方法。此外,我们还学习了如何通过重写 on_key_press
API 方法来控制动画。
使用不同图像区域的动画
使用单个图像的不同区域来创建动画是可能的。这些区域中的每一个都可以被视为一个单独的动画帧。为了达到预期的动画效果,正确创建带有区域的图像非常重要。在下面的示例中,动画将使用这些区域来创建。我们还将使用该图像内每个区域默认的位置参数。因此,本节的主要任务仅仅是使用这些区域的原貌,并从中创建动画帧。我们首先看看图像的外观。下面的插图显示了这张图像。
上一张图片中显示了一个带有想象中的 '网格' 的单个图像。
覆盖在这个图像上的水平虚线表示一个想象中的图像网格如何将图像分割成不同的区域。这里我们有四行和一列。因此,在动画过程中,这些图像区域将作为单个动画帧显示。注意雨滴图像是如何绘制的。在第一行,四个雨滴被绘制在顶部。然后在下一行,这些图像相对于第一行的雨滴稍微向西南方向偏移。这种偏移在第三行和第四行进一步增加。
动作时间 - 雨滴动画
让我们通过使用单个图像的不同区域来创建下落雨滴的动画。
-
从 Packt 网站下载 Python 源文件
RainDropsAnimation.py
和图像文件droplet.png
。与之前一样,将图像文件放置在子目录images
中。images
目录应位于 Python 源文件所在的目录中。 -
类
RainDropsAnimation
的__init__
方法被展示出来。1 def __init__(self, width=None, height=None): 2 pyglet.window.Window.__init__(self, 3 width=width, 4 height=height) 5 self.drawableObjects = [] 6 self.createDrawableObjects()
-
代码是自我解释的。类
RainDropsAnimation
继承自pyglet.window.Window
。类的构造函数调用创建用于在屏幕上显示动画的Sprite
实例的方法。 -
让我们回顾一下
createDrawableObjects
方法。1 def createDrawableObjects(self): 2 num_rows = 4 3 num_columns = 1 4 droplet = 'images/droplet.png' 5 animation = self.setup_animation(droplet, 6 num_rows, 7 num_columns) 8 9 self.dropletSprite = pyglet.sprite.Sprite(animation) 10 self.dropletSprite.position = (0,0) 11 12 # Add these sprites to the list of drawables 13 self.drawableObjects.append(self.dropletSprite)
-
在第 5 行,通过调用
setup_animation
方法创建了pyglet.image.Animation
实例。在第 9 行,为这个动画对象创建了Sprite
实例。 -
setup_animation
方法是主要的工作方法,它使用图像文件内的区域来创建单个动画帧。1 def setup_animation(self, img, num_rows, num_columns): 2 base_image = pyglet.image.load(img) 3 animation_grid = pyglet.image.ImageGrid(base_image, 4 num_rows, 5 num_columns) 6 image_frames = [] 7 8 for i in range(num_rows*num_columns, 0, -1): 9 frame = animation_grid[i-1] 10 animation_frame = ( 11 pyglet.image.AnimationFrame(frame, 12 0.2)) 13 image_frames.append(animation_frame) 14 15 animation = pyglet.image.Animation(image_frames) 16 return animation
-
首先,在第二行创建了图像实例。ImageGrid 是在水滴图像上放置的一个想象中的网格。在这个图像网格中的每个'单元格'或'图像区域'都可以被视为动画中的一个单独图像帧。ImageGrid 实例是通过提供图像对象和行数和列数作为参数来构建的。在这种情况下,行数是 4,只有一列。因此,在动画中将有四个这样的图像帧,对应于 ImageGrid 中的每一行。在第十行创建了 AnimationFrame 对象。第八行的代码将 i 的值从最大区域或单元格递减到最小。第九行获取特定的图像区域,然后用于创建 pyglet.image.AnimationFrame 实例,就像我们在第十行所做的那样。第二个参数是每个帧将在屏幕上显示的时间。在这里,我们显示每个帧 0.2 秒。所有这样的动画帧都存储在 image_frames 列表中,然后使用此列表创建 pyglet.image.Animation 实例。
-
请参考文件
RainDropsAnimation.py
来查看剩余的代码,然后从命令行运行程序,如下所示:$python RainDropsAnimation.py
-
此动画显示了一个单一图像的四个图像区域,依次展示。下一幅插图显示了这四个图像。
在上一幅插图显示的四个图像帧展示了单一图像的不同区域。这四个图像帧在动画循环中重复出现。
-
刚才发生了什么?
我们使用单一图像的不同区域创建了一个动画。这些区域中的每一个都被当作一个单独的动画帧来处理。在此动画中使用的图像创建过程进行了简要讨论。在众多其他事情中,我们学习了如何创建和使用 Pyglet 类,例如ImageGrid
和AnimationFrame
。
项目:雨天驾驶!
本项目基本上是对本章到目前为止所学内容的总结。此外,它还将涵盖一些其他内容,例如为动画添加音效,在播放动画时显示或隐藏某些图像精灵等。在这个动画中,将有一个静止的云图像。我们将重新使用雨滴动画部分的代码来动画化下落的雨。将有一个图像精灵来动画化闪电效果。最后,一辆卡通车将从左到右穿过这场大雨。以下快照是捕捉所有这些组成部分图像的动画帧。
在前一幅图像中展示了动画雨天驾驶的组成部分图像。
行动时间 - 雨天驾驶!
是时候编写这个动画的代码了。
-
下载 Python 源文件
RainyDayAnimation.py
。我们将讨论此文件中的一些重要方法。你可以阅读此文件中的其余代码。 -
从 Packt 网站下载图像文件,
droplet.png, cloud.png, car.png
和lightening.png
。将这些图像文件放置在名为images
的子目录中。images
目录应放置在 Python 源文件所在的目录中。 -
类的构造函数编写如下:
1 def __init__(self, width=None, height=None): 2 pyglet.window.Window.__init__(self, 3 width=width, 4 height=height, 5 resizable=True) 6 self.drawableObjects = [] 7 self.paused = False 8 9 10 self.createDrawableObjects() 11 self.adjustWindowSize() 12 # Make sure to replace the following media path to 13 # with an appropriate path on your computer. 14 self.horn_sound = ( 15 pyglet.media.load('C:/AudioFiles/horn.wav', 16 streaming=False) )
-
代码与雨滴动画中开发的代码相同。媒体文件 horn.wav 在第 14 行被解码。streaming 标志设置为 False,以便在动画期间可以多次播放媒体。确保在第 15 行指定计算机上的适当音频文件路径。
-
让我们回顾一下
createDrawableObjects
方法:1 def createDrawableObjects(self): 2 3 num_rows = 4 4 num_columns = 1 5 droplet = 'images/droplet.png' 6 animation = self.setup_animation(droplet, 7 num_rows, 8 num_columns) 9 10 self.dropletSprite = pyglet.sprite.Sprite(animation) 11 self.dropletSprite.position = (0,200) 12 13 cloud = pyglet.image.load('images/cloud.png') 14 self.cloudSprite = pyglet.sprite.Sprite(cloud) 15 self.cloudSprite.y = 100 16 17 lightening = pyglet.image.load('images/lightening.png') 18 self.lSprite = pyglet.sprite.Sprite(lightening) 19 self.lSprite.y = 200 20 21 car = pyglet.image.load('images/car.png') 22 self.carSprite = pyglet.sprite.Sprite(car, -500, 0) 23 24 # Add these sprites to the list of drawables 25 self.drawableObjects.append(self.cloudSprite) 26 self.drawableObjects.append(self.lSprite) 27 self.drawableObjects.append(self.dropletSprite) 28 self.drawableObjects.append(self.carSprite)
-
从第 3 行到第 10 行的代码块与雨滴动画中开发的那一个相同。self.dropletSprite 图像放置在适当的位置。接下来,我们只需在 Pyglet 窗口中创建精灵来加载云、闪电和汽车图像。这些精灵被放置在窗口内的适当位置。例如,汽车的起始位置在屏幕外。它在 x = -500 和 y = 0 的位置锚定。从第 24 行到第 28 行的代码块将所有 Sprite 实例添加到 self.drawableObjects 中。在 on_draw 方法中调用这些实例的 draw() 方法。
-
为了达到预期的动画效果,我们必须在动画过程中移动各种精灵。这是通过安排一些方法在指定的时间间隔被调用来完成的。这些方法在 Pyglet 窗口重绘时更新精灵的坐标或切换其可见性。代码如下所示:
# Schedule the method RainyDayAnimation.moveObjects to be # called every 0.05 seconds. pyglet.clock.schedule_interval(win.moveObjects, 1.0/20) # Show the lightening every 1 second pyglet.clock.schedule_interval(win.show_lightening, 1.0)
-
我们已经在早期部分看到了 moveObjects 方法的例子。在这个项目中,我们安排了另一个方法,RainyDayAnimation.show_lightening,每秒被调用一次。该方法创建了一个动画效果,闪电每秒在不同的位置击中。
-
我们现在将回顾
show_lightening
方法。1 def show_lightening(self, t): 2 if self.lSprite.visible: 3 self.lSprite.visible = False 4 else: 5 if(self.lSprite.x == 100): 6 self.lSprite.x += 200 7 else: 8 self.lSprite.x = 100 9 10 self.lSprite.visible = True
-
self.lSprite 是代表闪电图像的精灵。我们的目标是创建一个动画效果,闪电闪现一下然后消失。这可以通过切换 Sprite.visible 属性来实现。当此属性设置为 False 时,闪电不会显示。当它设置为 True 时,执行 else 块 4-10。self.lSprite 的位置被改变,以便闪电在下次调用此方法时出现在不同的位置。
-
moveObjects
方法被安排每0.05
秒调用一次。1 def moveObjects(self, t): 2 if self.carSprite.x <= self.cloudSprite.width: 3 self.carSprite.x += 10 4 else: 5 self.carSprite.x = -500 6 self.horn_sound.play()
-
每次调用时,它都会将代表汽车的精灵在 x 轴正方向上移动 10 个像素。然而,如果 self.carSprite 的 x 坐标超过了其宽度,汽车将重置到原始位置。此外,在汽车的起始位置,会播放喇叭声。
-
检查
RainyDayAnimation.py
文件中的其余代码。确保将self.horn_sound
的音频文件路径替换为您的计算机上的适当文件路径。一切设置妥当后,从命令行运行程序,如下所示:$python RainyDayAnimation.py
- 这将弹出一个窗口,播放一个有趣的汽车在雷雨中巡航的动画。下一张插图显示了动画的一些中间帧。
- 在前面的图像中显示了汽车在雨天行驶的动画的中间帧。
刚才发生了什么?
本项目中开发的动画使用了四张不同的图像。我们学习了如何在动画中添加音效和更改图像精灵的可见性。一些图像被翻译或间歇性地显示,以实现所需的动画效果。单个图像的不同区域被用来动画化雨滴。总的来说,这个有趣的项目涵盖了本书中学到的许多内容。
尝试添加更多效果的英雄
-
在动画中,每当闪电出现时,都会播放雷雨声效。
-
在前面展示的代码中,闪电图像的位置在两个固定位置之间切换。使用 Python 的 random 模块获取 0 到
self.cloudSprite.width
之间的随机坐标,并将其用作self.lSprite
的 x 坐标。 -
添加键盘控制以改变汽车的速度、闪电的频率等。
概述
在本章中,我们学习了使用 Pyglet 在 Python 中创建 2D 动画的很多知识。具体来说,我们:
-
学习了 Pyglet 框架创建动画的一些基本组件。例如,讨论了
Window, Image, Animation, Sprite, AnimationFrame, ImageGrid
等模块。 -
编写代码以使用一系列图像创建动画或播放预创建的动画。
-
学习了如何修改 Pyglet 精灵的位置,添加键盘和鼠标控制以及为动画添加音效。
-
在“雨天驾驶”的卡通动画项目中工作。在这里,我们应用了本章中学到的多种技术。
第五章. 音频处理
几十年前,无声电影照亮了银幕,但后来,是音频效果让它们变得生动。在播放 CD 曲目、录制自己的声音或将歌曲转换为不同的音频格式时,我们经常处理数字音频处理。有许多库或多媒体框架可用于音频处理。本章将使用名为 GStreamer 的流行多媒体框架的 Python 绑定介绍一些常见的数字音频处理技术。
在本章中,我们将:
-
学习 GStreamer 多媒体框架背后的基本概念
-
使用 GStreamer API 进行音频处理
-
开发一些简单的音频处理工具用于“日常使用”。我们将开发能够批量转换音频文件格式、录制音频和播放音频文件的工具
那么,让我们开始吧!
安装必备条件
由于我们将使用外部多媒体框架,因此有必要安装本节中提到的软件包。
GStreamer
GStreamer 是一个流行的开源多媒体框架,支持广泛的多媒体格式的音频/视频操作。它用 C 编程语言编写,并为包括 Python 在内的其他编程语言提供了绑定。许多开源项目使用 GStreamer 框架来开发自己的多媒体应用程序。在本章中,我们将使用 GStreamer 框架进行音频处理。为了与 Python 一起使用,我们需要安装 GStreamer 及其 Python 绑定。
Windows 平台
GStreamer 的二进制发行版在项目网站www.gstreamer.net/
上未提供。从源代码安装可能需要 Windows 用户付出相当大的努力。幸运的是,GStreamer WinBuilds项目提供了预编译的二进制发行版。以下是项目网站的 URL:www.gstreamer-winbuild.ylatuya.es
GStreamer 的二进制发行版以及其 Python 绑定(Python 2.6)可在网站上的下载区域找到:www.gstreamer-winbuild.ylatuya.es/doku.php?id=download
您需要安装两个软件包。首先,安装 GStreamer,然后安装 GStreamer 的 Python 绑定。从 GStreamer WinBuilds 项目网站下载并安装 GStreamer 的 GPL 发行版。GStreamer 可执行文件名为GStreamerWinBuild-0.10.5.1.exe
。版本应为 0.10.5 或更高。默认情况下,此安装将在您的机器上创建一个文件夹C:\gstreamer。该文件夹中的bin
目录包含使用 GStreamer 时所需的运行时库。
接下来,安装 GStreamer 的 Python 绑定。二进制分发可在同一网站上找到。使用适用于 Python 2.6 的可执行文件 Pygst-0.10.15.1-Python2.6.exe
。版本应为 0.10.15 或更高。
注意
GStreamer WinBuilds 似乎是一个独立的项目。它基于 OSSBuild 开发套件。有关更多信息,请访问 code.google.com/p/ossbuild/
。可能发生的情况是,使用 Python 2.6 构建的 GStreamer 二进制文件在您阅读此书时可能不再在提到的网站上可用。因此,建议您联系 OSSBuild 的开发者社区。也许他们可以帮助您解决问题!
或者,您可以在 Windows 平台上从源代码构建 GStreamer,使用类似 Linux 的 Windows 环境,如 Cygwin(http://www.cygwin.com/)。在此环境中,您可以首先安装依赖软件包,如 Python 2.6、gcc 编译器等。从 GStreamer 网站 www.gstreamer.net/
下载 gst-python-0.10.17.2.tar.gz
软件包。然后提取此软件包,并使用 Cygwin 环境从源代码安装它。此软件包内的 INSTALL
文件将包含安装说明。
其他平台
许多 Linux 发行版都提供 GStreamer 软件包。您可以在软件包仓库中搜索适当的 gst-python
发行版(针对 Python 2.6)。如果此类软件包不可用,可以从源代码安装 gst-python
,如前文所述的 Windows 平台 部分所述。
如果您是 Mac OS X 用户,请访问 py26-gst-python.darwinports.com/
。它提供了关于如何下载和安装 Py26-gst-python 版本 0.10.17
(或更高版本)的详细说明。
注意
Mac OS X 10.5.x(Leopard)自带 Python 2.5 发行版。如果您使用的是使用此默认 Python 版本的软件包,可以在 darwinports 网站上找到使用 Python 2.5 的 GStreamer Python 绑定:gst-python.darwinports.com/
PyGobject
有一个名为 'GLib' 的免费多平台软件库。它提供诸如哈希表、链表等数据结构。它还支持线程的创建。GLib 的 '对象系统' 被称为 GObject。在这里,我们需要安装 GObject 的 Python 绑定。Python 绑定可在 PyGTK 网站上找到:www.pygtk.org/downloads.html
。
Windows 平台
二进制安装程序可在 PyGTK 网站上找到。完整的网址是:ftp.acc.umu.se/pub/GNOME/binaries/win32/pygobject/2.20/
。下载并安装 Python 2.6 的 2.20 版本。
其他平台
对于 Linux 系统,源代码 tar 包可以在 PyGTK 网站上找到。甚至可能存在于您 Linux 操作系统的软件包仓库中的二进制发行版。PyGObject 版本 2.21(源代码 tar 包)的直接链接是:ftp.gnome.org/pub/GNOME/sources/pygobject/2.21/
.
如果您是 Mac 用户并且已安装 Python 2.6,PyGObject 的发行版可在py26-gobject.darwinports.com/
找到。安装 2.14 或更高版本。
安装前提条件摘要
以下表格总结了本章所需的软件包。
软件包 | 下载位置 | 版本 | Windows 平台 | Linux/Unix/OS X 平台 |
---|---|---|---|---|
GStreamer | www.gstreamer.net/ |
0.10.5 或更高版本 | 使用 Gstreamer WinBuild 网站上的二进制发行版进行安装:www.gstreamer-winbuild.ylatuya.es/doku.php?id=download 使用GStreamerWinBuild-0.10.5.1.exe (或更高版本,如果可用)。 |
Linux:使用软件包仓库中的 GStreamer 发行版。Mac OS X:按照网站上的说明下载和安装:gstreamer.darwinports.com/ . |
GStreamer 的 Python 绑定 | www.gstreamer.net/ |
Python 2.6 的 0.10.15 或更高版本 | 使用 GStreamer WinBuild 项目提供的二进制文件。有关 Python 2.6 的详细信息,请参阅www.gstreamer-winbuild.ylatuya.es 。 |
Linux:使用软件包仓库中的 gst-python 发行版。Mac OS X:如果您使用 Python 2.6,请使用此软件包。py26-gst-python.darwinports.com/ 有关详细信息。 |
GObject 的 Python 绑定“PyGObject” | 源代码发行版:www.pygtk.org/downloads.html |
Python 2.6 的 2.14 或更高版本 | 使用pygobject-2.20.0.win32-py2.6.exe 的二进制软件包。 |
Linux:如果软件包仓库中没有 pygobject,则从源代码安装。Mac:如果您使用 Python 2.6,请使用此软件包在 darwinports 上。py26-gobject.darwinports.com/ 有关详细信息。 |
安装测试
确保 GStreamer 及其 Python 绑定已正确安装。测试这一点很简单。只需从命令行启动 Python,并输入以下内容:
>>>import pygst
如果没有错误,则表示 Python 绑定已正确安装。
接下来,输入以下内容:
>>>pygst.require("0.10")
>>>import gst
如果此导入成功,那么我们就准备好使用 GStreamer 处理音频和视频了!
如果 import gst
失败,它可能会抱怨它无法工作一些所需的 DLL/共享对象。在这种情况下,检查您的环境变量,并确保 PATH 变量包含 gstreamer/bin
目录的正确路径。以下 Python 解释器中的代码行显示了 Windows 平台上 pygst
和 gst
模块的典型位置。
>>> import pygst
>>> pygst
<module 'pygst' from 'C:\Python26\lib\site-packages\pygst.pyc'>
>>> pygst.require('0.10')
>>> import gst
>>> gst
<module 'gst' from 'C:\Python26\lib\site-packages\gst-0.10\gst\__init__.pyc'>
接下来,测试 PyGObject 是否成功安装。启动 Python 解释器并尝试导入 gobject
模块。
>>import gobject
如果这成功了,我们就一切准备就绪了!
GStreamer 入门
在本章中,我们将广泛使用 GStreamer 多媒体框架。在我们学习各种音频处理技术之前,了解 GStreamer 的基础知识是必要的。
那么 GStreamer 是什么呢?它是一个框架,可以在其上开发多媒体应用程序。它提供的丰富库集使得开发具有复杂音频/视频处理能力的应用程序变得更加容易。GStreamer 的基本组件将在接下来的小节中简要解释。
GStreamer 项目网站上有全面的文档。GStreamer 应用程序开发手册是一个非常好的起点。在本节中,我们将简要介绍 GStreamer 的一些重要方面。为了进一步阅读,建议您访问 GStreamer 项目网站:www.gstreamer.net/documentation/
gst-inspect 和 gst-launch
我们将首先学习两个重要的 GStreamer 命令。GStreamer 可以通过命令行运行,通过调用 gst-launch-0.10.exe
(Windows 上)或 gst-launch-0.10
(其他平台)。以下命令展示了 Linux 上 GStreamer 的典型执行。我们将在下一小节中看到 pipeline
的含义。
$gst-launch-0.10 pipeline_description
GStreamer 具有插件架构。它支持大量的插件。要查看 GStreamer 安装中任何插件的更多详细信息,请使用命令 gst-inspect-0.10
(Windows 上的 gst-inspect-0.10.exe
)。我们将经常使用此命令。此命令的使用在此处展示。
$gst-inspect-0.10 decodebin
在这里,decodebin
是一个插件。执行前面的命令后,它将打印有关插件 decodebin
的详细信息。
元素和管道
在 GStreamer 中,数据在管道中流动。各种元素连接在一起形成一个管道,使得前一个元素的输出成为下一个元素的输入。
管道可以逻辑上表示如下:
Element1 ! Element2 ! Element3 ! Element4 ! Element5
在这里,从 Element1
到 Element5
是通过符号 !
连接起来的元素对象。每个元素执行特定的任务。其中一个元素对象负责读取输入数据,如音频或视频。另一个元素解码第一个元素读取的文件,而另一个元素则负责将数据转换为其他格式并保存输出。如前所述,以适当的方式链接这些元素对象创建了一个管道。
管道的概念与 Unix 中使用的类似。以下是一个 Unix 管道的示例。在这里,垂直分隔符 |
定义了管道。
$ls -la | more
在这里,ls -la
列出了目录中的所有文件。然而,有时这个列表太长,无法在 shell 窗口中显示。因此,添加 | more
允许用户导航数据。
现在让我们看看从命令提示符运行 GStreamer 的现实示例。
$ gst-launch-0.10 -v filesrc location=path/to/file.ogg ! decodebin ! audioconvert ! fakesink
对于 Windows 用户,gst
命令名称将是 gst-launch-0.10.exe
。管道是通过指定不同的元素来构建的。!symbol
链接相邻的元素,从而形成整个数据流管道。对于 GStreamer 的 Python 绑定,管道元素的抽象基类是 gst.Element
,而 gst.Pipeline
类可以用来创建管道实例。在管道中,数据被发送到一个单独的线程进行处理,直到它到达末端或发送了终止信号。
插件
GStreamer 是一个基于插件的框架。有多个插件可用。插件用于封装一个或多个 GStreamer 元素的功能。因此,我们可以有一个插件,其中多个元素协同工作以生成所需的输出。然后,该插件本身可以用作 GStreamer 管道中的抽象元素。例如,decodebin
。我们将在接下来的章节中学习它。GStreamer 网站上提供了可用的插件的综合列表 gstreamer.freedesktop.org
。在这本书中,我们将使用其中的一些来开发音频/视频处理应用程序。例如,将使用 Playbin
插件进行音频播放。在几乎所有将要开发的应用程序中,都将使用 decodebin
插件。对于音频处理,将使用如 gnonlin
、audioecho
、monoscope
、interleave
等插件提供的功能。
槽位
在 GStreamer 中,bin 是一个管理添加到其中的元素对象的容器。可以使用 gst.Bin
类创建 bin 实例。它继承自 gst.Element
并可以作为表示其中的一组元素的抽象元素。GStreamer 插件 decodebin 是一个很好的 bin 示例。decodebin 包含解码器元素。它自动连接解码器以创建解码管道。
面板
每个元素都有某种类型的连接点来处理数据输入和输出。GStreamer 称它们为垫。因此,一个元素对象可以有一个或多个称为接收垫的“接收器垫”,它接受来自管道中前一个元素的数据。同样,还有从元素中取出数据作为管道中下一个元素(如果有的话)输入的源垫。以下是一个非常简单的示例,展示了如何指定源和接收垫。
>gst-launch-0.10.exe fakesrc num-bufferes=1 ! fakesink
fakesrc
是管道中的第一个元素。因此,它只有一个源垫。它将数据传输到下一个linkedelement
,即fakesink
,它只有一个接收垫来接受元素。请注意,在这种情况下,由于这些是fakesrc
和fakesink
,只是交换了空缓冲区。垫是由gst.Pad
类定义的。垫可以通过gst.Element.add_pad()
方法附加到元素对象上。
以下是一个带有垫的 GStreamer 元素的示意图。它说明了管道内的两个 GStreamer 元素,它们有一个单独的源垫和接收垫。
现在我们已经了解了垫的工作方式,让我们讨论一些特殊类型的垫。在示例中,我们假设元素的垫总是“在那里”。然而,在某些情况下,元素并不总是有可用的垫。这些元素在运行时请求它们需要的垫。这种垫被称为动态垫。另一种类型的垫称为幽灵垫。这些类型在本节中讨论。
动态垫
一些对象,例如decodebin
,在创建时并没有定义垫(pads)。这些元素决定了在运行时使用哪种类型的垫。例如,根据正在处理的媒体文件输入,decodebin
将创建一个垫。这通常被称为动态垫,有时也称为可用垫,因为它并不总是在像decodebin
这样的元素中可用。
幽灵垫
如在Bin部分所述,bin对象可以充当一个抽象元素。它是如何实现的?为了做到这一点,bin 使用“幽灵垫”或“伪链接垫”。bin 的幽灵垫用于连接其内部适当的位置。幽灵垫可以使用gst.GhostPad
类创建。
Caps
元素对象通过垫发送和接收数据。元素对象将处理哪种类型的媒体数据由caps(能力的简称)决定。它是一个描述元素支持的媒体格式的结构。caps 是由gst.Caps
类定义的。
Bus
bus
指的是传递 GStreamer 生成消息的对象。消息是一个gst.Message
对象,它通知应用程序有关管道中的事件。消息是通过gst.Bus.gst_bus_post()
方法放在总线上的。以下代码展示了bus
的一个示例用法。
1 bus = pipeline.get_bus()
2 bus.add_signal_watch()
3 bus.connect("message", message_handler)
代码中的第一行创建了一个 gst.Bus
实例。在这里,管道是 gst.PipeLine
的一个实例。在下一行,我们添加了一个信号监视器,以便总线发布该总线上的所有消息。第 3 行将信号连接到一个 Python 方法。在这个例子中,消息是信号字符串,它调用的方法是 message_handler
。
Playbin/Playbin2
Playbin 是一个 GStreamer 插件,它提供了一个高级音频/视频播放器。它可以处理许多事情,例如自动检测输入媒体文件格式、自动确定解码器、音频可视化以及音量控制等。以下代码行创建了一个 playbin
元素。
playbin = gst.element_factory_make("playbin")
它定义了一个名为 uri
的属性。URI(统一资源标识符)应该是您计算机或网络上文件的绝对路径。根据 GStreamer 文档,Playbin2 是最新的不稳定版本,但一旦稳定,它将取代 Playbin。
可以像创建 Playbin 实例一样创建 Playbin2 实例。
gst-inspect-0.10 playbin2
在有了这个基本理解之后,让我们学习使用 GStreamer 和 Python 的各种音频处理技术。
播放音乐
给定一个音频文件,你首先要做的事情就是播放这个音频文件,对吧?在 GStreamer 中,我们需要哪些基本元素来播放音频?以下列出了必需的元素。
-
我们首先需要打开一个音频文件进行读取
-
接下来,我们需要一个解码器来转换编码信息
-
然后,需要一个元素来转换音频格式,使其成为音频设备(如扬声器)所需的“可播放”格式。
-
最后,一个将启用实际播放音频文件的元素
你将如何使用 GStreamer 的命令行版本播放音频文件?一种使用命令行执行的方法如下:
$gstlaunch-0.10 filesrc location=/path/to/audio.mp3 ! decodebin ! audioconvert ! autoaudiosink
注意
autoaudiosink
会自动检测您计算机上正确的音频设备来播放音频。这已在装有 Windows XP 的机器上测试过,效果良好。如果在播放音频时出现任何错误,请检查您计算机上的音频设备是否正常工作。您还可以尝试使用输出到声卡的元素 sdlaudiosink
,通过 SDLAUDIO
。如果这也不起作用,并且您想在此安装 audiosink
插件,以下是一些 GStreamer 插件的列表:www.gstreamer.net/data/doc/gstreamer/head/gst-plugins-good-plugins/html/
Mac OS X 用户可以尝试安装 osxaudiosink
,如果默认的 autoaudiosink
不起作用。
音频文件应该使用此命令开始播放,除非有任何插件缺失。
行动时间 - 播放音频:方法 1
使用 Python 和 GStreamer 播放音频有多种方法。让我们从一个简单的例子开始。在本节中,我们将使用一个命令字符串,类似于您使用 GStreamer 命令行版本所指定的方式。这个字符串将用于在 Python 程序中构建一个 gst.Pipeline
实例。
所以,从这里开始吧!
-
首先,在 Python 源文件中创建一个
AudioPlayer
类。只需定义以下代码片段中所示的空方法。我们将在后续步骤中扩展这些方法。1 import thread 2 import gobject 3 import pygst 4 pygst.require("0.10") 5 import gst 6 7 class AudioPlayer: 8 def __init__(self): 9 pass 10 def constructPipeline(self): 11 pass 12 def connectSignals(self): 13 pass 14 def play(self): 15 pass 16 def message_handler(self): 17 pass 18 19 # Now run the program 20 player = AudioPlayer() 21 thread.start_new_thread(player.play, ()) 22 gobject.threads_init() 23 evt_loop = gobject.MainLoop() 24 evt_loop.run()
-
代码的第 1 到 5 行导入了必要的模块。如安装先决条件部分所述,首先导入 pygst 包。然后我们调用 pygst.require 以启用 gst 模块的导入。
-
现在关注第 19 到 24 行之间的代码块。这是主要的执行代码。它使程序运行直到音乐播放完毕。我们将在这本书的整个过程中使用此代码或类似代码来运行我们的音频应用程序。
在第 21 行,使用线程模块创建一个新的线程来播放音频。将
AudioPlayer.play
方法发送到这个线程。thread.start_new_thread
的第二个参数是要传递给play
方法的参数列表。在这个例子中,我们不支持任何命令行参数。因此,传递了一个空元组。Python 在操作系统线程之上添加了自己的线程管理功能。当这样的线程调用外部函数(如 C 函数)时,它会将 '全局解释器锁' 放在其他线程上,直到例如 C 函数返回一个值。gobject.threads_init()
是一个初始化函数,用于在 gobject 模块中方便地使用 Python 线程。它可以在调用 C 函数时启用或禁用线程。我们在运行主事件循环之前调用它。执行此程序的主事件循环是在第 23 行使用 gobject 创建的,并且通过调用evt_loop.run()
启动此循环。 -
接下来,填充
AudioPlayer
类的方法代码。首先,编写类的构造函数。1 def __init__(self): 2 self.constructPipeline() 3 self.is_playing = False 4 self.connectSignals()
-
管道是通过第 2 行的调用方法构建的。
self.is_playing
标志初始化为 False。它将用于确定正在播放的音频是否已到达流的末尾。在第 4 行,调用self.connectSignals
方法,以捕获在总线上发布的消息。我们将在下一节讨论这两个方法。 -
播放声音的主要驱动程序是以下
gst
命令:"filesrc location=C:/AudioFiles/my_music.mp3 "\ "! decodebin ! audioconvert ! autoaudiosink"
- 前面的字符串由四个元素组成,这些元素之间用符号 ! 分隔。这些元素代表我们之前简要讨论过的组件。
-
第一个元素
filesrc location=C:/AudioFiles/my_music.mp3
定义了源元素,它从给定位置加载音频文件。在这个字符串中,只需将表示location
的音频文件路径替换为您计算机上的适当文件路径。您也可以指定一个磁盘驱动器上的文件。注意
如果文件名包含命名空间,请确保您指定路径时使用引号。例如,如果文件名是 my sound.mp3,请按照以下方式指定:
filesrc location =\"C:/AudioFiles/my sound.mp3\"
-
下一个元素是加载文件。此元素连接到一个
decodebin
。如前所述,decodebin
是 GStreamer 的一个插件,它继承自gst.Bin
。根据输入音频格式,它确定要使用的正确类型的解码器元素。第三个元素是 audioconvert。它将解码的音频数据转换为音频设备可播放的格式。
最后一个元素 autoaudiosink 是一个插件;它自动检测音频输出设备。
现在我们有足够的信息来创建一个 gst.Pipeline 实例。编写以下方法。
1 def constructPipeline(self): 2 myPipelineString = \ 3 "filesrc location=C:/AudioFiles/my_music.mp3 "\ 4 "! decodebin ! audioconvert ! autoaudiosink" 5 self.player = gst.parse_launch(myPipelineString)
在第 5 行使用 gst.parse_launch 方法创建了一个 gst.Pipeline 实例。
-
现在编写类
AudioPlayer
的以下方法。1 def connectSignals(self): 2 # In this case, we only capture the messages 3 # put on the bus. 4 bus = self.player.get_bus() 5 bus.add_signal_watch() 6 bus.connect("message", self.message_handler)
在第 4 行,创建了一个 gst.Bus 实例。在 GStreamer 的介绍部分,我们已经学习了第 4 到 6 行之间的代码做了什么。这个总线负责从流线程中传递它上面的消息。add_signal_watch 调用使得总线为每个发布的消息发出消息信号。这个信号被 message_handler 方法用来采取适当的行动。
编写以下方法:
1 def play(self): 2 self.is_playing = True 3 self.player.set_state(gst.STATE_PLAYING) 4 while self.is_playing: 5 time.sleep(1) 6 evt_loop.quit()
在第 2 行,我们将 gst 管道的状态设置为 gst.STATE_PLAYING 以启动音频流。self.is_playing 标志控制第 4 行的 while 循环。这个循环确保在音频流结束之前主事件循环不会被终止。在循环中,time.sleep 的调用只是为音频流的完成争取了一些时间。标志的值在监视总线消息的方法 message_handler 中改变。在第 6 行,主事件循环被终止。这会在发出流结束消息或播放音频时发生错误时被调用。
-
接下来,开发方法
AudioPlayer.message_handler
。此方法设置适当的标志以终止主循环,并负责更改管道的播放状态。1 def message_handler(self, bus, message): 2 # Capture the messages on the bus and 3 # set the appropriate flag. 4 msgType = message.type 5 if msgType == gst.MESSAGE_ERROR: 6 self.player.set_state(gst.STATE_NULL) 7 self.is_playing = False 8 print "\n Unable to play audio. Error: ", \ 9 message.parse_error() 10 elif msgType == gst.MESSAGE_EOS: 11 self.player.set_state(gst.STATE_NULL) 12 self.is_playing = False
-
在这个方法中,我们只检查两件事:总线上的消息是否表示流音频已到达其末尾(gst.MESSAGE_EOS)或播放音频流时是否发生了错误(gst.MESSAGE_ERROR)。对于这两个消息,gst 管道的状态从 gst.STATE_PLAYING 更改为 gst.STATE_NULL。self.is_playing 标志被更新以指示程序终止主事件循环。
我们已经定义了所有必要的代码来播放音频。将文件保存为 PlayingAudio.py,然后按照以下命令从命令行运行应用程序:
$python PlayingAudio.py
- 这将开始播放输入音频文件。一旦播放完毕,程序将被终止。您可以在 Windows 或 Linux 上按 Ctrl + C 来中断音频文件的播放。它将终止程序。
刚才发生了什么?
我们开发了一个非常简单的音频播放器,它可以播放输入的音频文件。我们编写的代码涵盖了 GStreamer 的一些最重要的组件。这些组件将在本章中非常有用。程序的核心组件是一个 GStreamer 管道,其中包含播放给定音频文件的指令。此外,我们还学习了如何创建一个线程,然后启动一个gobject
事件循环,以确保音频文件播放到结束。
尝试英雄播放播放列表中的音频
我们开发了一个非常简单的音频播放器,它只能播放单个音频文件,其路径在构建的 GStreamer 管道中是硬编码的。修改此程序,使其能够播放“播放列表”中的音频。在这种情况下,播放列表应定义您想要依次播放的音频文件的完整路径。例如,您可以将文件路径指定为该应用程序的参数,或从文本文件中加载路径,或从目录中加载所有音频文件。提示:在后面的章节中,我们将开发一个音频文件转换实用程序。看看您是否可以使用其中的一些代码。
从元素构建管道
在上一节中,gst.parse_launch
方法为我们自动构建了一个gst.Pipeline
。它所需的所有只是一个合适的命令字符串,类似于运行 GStreamer 命令行版本时指定的命令字符串。元素的创建和链接由该方法内部处理。在本节中,我们将看到如何通过添加和链接单个元素对象来构建管道。'GStreamer Pipeline'构建是我们将在本章以及与其他音频和视频处理相关的章节中使用的根本技术。
行动时间 - 播放音频:方法 2
我们已经编写了播放音频的代码。现在让我们调整AudioPlayer.constructPipeline
方法,使用不同的元素对象构建gst.Pipeline
。
-
将
constructPipeline
方法重写如下。您还可以从 Packt 网站下载PlayingAudio.py
文件以供参考。此文件包含我们在本节和前几节中讨论的所有代码。1 def constructPipeline(self): 2 self.player = gst.Pipeline() 3 self.filesrc = gst.element_factory_make("filesrc") 4 self.filesrc.set_property("location", 5 "C:/AudioFiles/my_music.mp3") 6 7 self.decodebin = gst.element_factory_make("decodebin", 8 "decodebin") 9 # Connect decodebin signal with a method. 10 # You can move this call to self.connectSignals) 11 self.decodebin.connect("pad_added", 12 self.decodebin_pad_added) 13 14 self.audioconvert = \ 15 gst.element_factory_make("audioconvert", 16 "audioconvert") 17 18 self.audiosink = \ 19 gst.element_factory_make("autoaudiosink", 20 "a_a_sink") 21 22 # Construct the pipeline 23 self.player.add(self.filesrc, self.decodebin, 24 self.audioconvert, self.audiosink) 25 # Link elements in the pipeline. 26 gst.element_link_many(self.filesrc, self.decodebin) 27 gst.element_link_many(self.audioconvert,self.audiosink)
-
我们首先创建
gst.Pipeline
类的一个实例。 -
接下来,在第 2 行,我们创建用于加载音频文件的元素。任何新的
gst
元素都可以使用 API 方法gst.element_factory_make
创建。该方法需要一个元素名称(字符串)作为参数。例如,在第 3 行,此参数指定为"filesrc"
,以便创建GstFileSrc
元素的实例。每个元素都将有一组属性。输入音频文件的路径存储在self.filesrc
元素的属性location
中。此属性在第 4 行设置。将文件路径字符串替换为适当的音频文件路径。小贴士
您可以通过在控制窗口中运行
'gst-inspect-0.10'
命令来获取所有属性的列表。有关GSreamer的更多详细信息,请参阅简介部分。 -
第二个可选参数用作创建的对象的定制名称。例如,在第 20 行,
autoaudiosink
对象的名称指定为a_a_sink
。以此类推,我们创建了构建管道所需的所有基本元素。 -
在代码的第 23 行,通过调用
gst.Pipeline.add
方法将所有元素放入管道中。 -
方法
gst.element_link_many
建立了两个或更多元素之间的连接,以便音频数据可以在它们之间流动。这些元素通过第 26 行和第 27 行的代码相互连接。然而,请注意,我们没有将元素self.decodebin
和self.audioconvert
连接在一起。为什么?答案将在下文中揭晓。 -
我们不能在创建管道时将
decodebin
元素与audioconvert
元素连接起来。这是因为decodebin
使用动态垫。当管道创建时,这些垫不可用于与audioconvert
元素连接。根据输入数据,它将创建一个垫。因此,我们需要注意当decodebin
添加垫时发出的信号!我们如何做到这一点?这是通过上面代码片段中的第 11 行代码完成的。“pad-added”信号与decodebin_pad_added
方法连接。每当decodebin
添加一个动态垫时,此方法就会被调用。 -
因此,我们所需做的就是手动在
decodebin
和audioconvert
元素之间建立连接,在decodebin_pad_added
方法中编写以下方法。1 def decodebin_pad_added(self, decodebin, pad ): 2 caps = pad.get_caps() 3 compatible_pad = \ 4 self.audioconvert.get_compatible_pad(pad, caps) 5 6 pad.link(compatible_pad)
-
该方法接受元素(在这种情况下是 self.decodebin)和垫作为参数。垫是 decodebin 元素的新垫。我们需要将这个垫与 self.audioconvert 上的适当垫连接起来。
-
在此代码片段的第 2 行,我们找到了垫处理哪种类型的媒体数据。一旦知道了能力(caps),我们就将此信息传递给
self.audioconvert
对象的get_compatible_pad
方法。此方法返回一个兼容的垫,然后与第 6 行的pad
连接。 -
代码的其余部分与前面章节中展示的代码相同。你可以按照前面描述的方式运行此程序。
刚才发生了什么?
我们学习了 GStreamer 框架的一些非常关键组件。以简单的音频播放器为例,我们通过创建各种元素对象并将它们连接起来,从头开始创建了一个 GStreamer 管道。我们还学习了如何通过“手动”连接它们的垫来连接两个元素,以及为什么这对于 self.decodebin
元素是必需的。
突击测验 - 元素连接
在前面的例子中,管道中的大多数元素在 AudioPlayer.constructPipeline
方法中使用 gst.element_link_many
连接。然而,我们在构建管道时没有将 self.decodebin
和 self.audioconvert
元素连接起来。为什么?从以下选项中选择正确答案。
-
我们只是尝试了手动连接这些元素的不同技术。
-
Decodebin
使用在运行时创建的动态垫。当管道创建时,这个垫不可用。 -
我们不需要在管道中链接这些元素。媒体数据会以某种方式找到自己的路径。
-
你在说什么?无论你尝试什么,都无法连接
decodebin
和audioconvert
元素。
从网站上播放音频
如果网站上某个地方有你想播放的音频,我们基本上可以使用之前开发的相同的 AudioPlayer 类。在本节中,我们将说明如何使用 gst.Playbin2 通过指定 URL 来播放音频。下面的代码片段显示了修改后的 AudioPlayer.constructPipeline 方法。这个方法的名字应该更改,因为它创建的是 playbin 对象。
1 def constructPipeline(self):
2 file_url = "http://path/to/audiofile.wav"
3 buf_size = 1024000
4 self.player = gst.element_factory_make("playbin2")
5 self.player.set_property("uri", file_url)
6 self.player.set_property("buffer-size", buf_size)
7 self.is_playing = False
8 self.connectSignals()
在第 4 行,使用gst.element_factory_make
方法创建了 gst.Playbin2 元素。这个方法的参数是一个描述要创建的元素的字符串。在这种情况下是"playbin2"。你也可以通过向这个方法提供一个可选的第二个参数来为这个对象定义一个自定义名称。接下来,在第 5 行和第 6 行,我们为 uri 和 buffer-size 属性赋值。将 uri 属性设置为适当的 URL,即你想播放的音频文件的完整路径。
小贴士
注意:当你执行这个程序时,Python 应用程序会尝试访问互联网。你电脑上安装的杀毒软件可能会阻止程序执行。在这种情况下,你需要允许这个程序访问互联网。同时,你也需要小心黑客。如果你从不可信的来源获取了fil_url
,请执行安全检查,例如assert not re.match("file://", file_url)
。
英雄尝试使用'playbin'播放本地音频
在最后几个部分,我们学习了使用 Python 和 GStreamer 播放音频文件的不同方法。在前一个部分,你可能已经注意到了另一种实现这一点的简单方法,即使用 playbin 或 playbin2 对象来播放音频。在前一个部分,我们学习了如何从 URL 播放音频文件。修改这段代码,使这个程序现在可以播放位于你电脑驱动器上的音频文件。提示:你需要使用正确的"uri"路径。使用 Python 的模块urllib.pathname2url
转换文件路径,然后将其附加到字符串:"file://`"。
转换音频文件格式
假设你有一大批 wav 格式的歌曲想要加载到手机上。但你发现手机内存卡没有足够的空间来存储所有这些。你会怎么做?你可能试图减小歌曲文件的大小,对吧?将文件转换为 mp3 格式会减小文件大小。当然,你可以使用某些媒体播放器来完成这个转换操作。让我们学习如何使用 Python 和 GStreamer 执行这个转换操作。稍后我们将开发一个简单的命令行工具,可以用于对所需的所有文件进行批量转换。
-
就像在之前的示例中一样,让我们首先列出我们需要完成文件转换的重要构建块。前三个元素保持不变。
-
就像之前一样,我们首先需要加载一个音频文件进行读取。
-
接下来,我们需要一个解码器来转换编码信息。
-
然后,需要有一个元素将原始音频缓冲区转换为适当的格式。
-
需要一个编码器,它将原始音频数据编码为适当的文件格式以写入。
-
需要一个元素,将编码的数据流式传输到其中。在这种情况下,它是我们的输出音频文件。
好的,接下来是什么?在跳入代码之前,首先检查你是否可以使用 GStreamer 的命令行版本实现你想要的功能。
$gstlaunch-0.10.exe filesrc location=/path/to/input.wav ! decodebin ! audioconvert ! lame ! Filesink location=/path/to/output.mp3
指定正确的输入和输出文件路径,并运行此命令将波形文件转换为 mp3。如果它工作,我们就准备好继续了。否则,检查是否有缺少的插件。
你应该参考 GStreamer API 文档来了解上述各种元素的性质。相信我,gst-inspect-0.10
(或 Windows 用户的gst-inspect-0.10.exe
)命令是一个非常实用的工具,它将帮助你理解 GStreamer 插件的组件。关于运行此工具的说明已经在本章前面讨论过了。
行动时间 - 音频文件格式转换器
让我们编写一个简单的音频文件转换器。这个实用程序将批量处理输入音频文件,并将它们保存为用户指定的文件格式。要开始,请从 Packt 网站下载文件 AudioConverter.py。此文件可以从命令行运行,如下所示:
python AudioConverter.py [options]
其中,[options]
如下:
-
--input_dir
: 从中读取要转换的输入音频文件(们)的目录。 -
--input_format:
输入文件的音频格式。格式应该在一个支持的格式列表中。支持的格式有 "mp3"、"ogg" 和 "wav"。如果没有指定格式,它将使用默认格式 ".wav"。 -
--output_dir
: 转换文件将被保存的输出目录。如果没有指定输出目录,它将在输入目录中创建一个名为OUTPUT_AUDIOS
的文件夹。 -
--output_format:
输出文件的音频格式。支持的输出格式有 "wav" 和 "mp3"。现在让我们写下这段代码。
-
首先,导入必要的模块。
import os, sys, time import thread import getopt, glob import gobject import pygst pygst.require("0.10") import gst
-
现在声明以下类和实用函数。你会注意到,其中许多方法与之前的方法名称相同。这些方法的底层功能将与我们已经讨论过的类似。在本节中,我们将仅回顾这个类中最重要的方法。你可以参考文件
AudioConverter.py
中的其他方法或自行开发这些方法。def audioFileExists(fil): return os.path.isfile(fil) class AudioConverter: def __init__(self): pass def constructPipeline(self): pass def connectSignals(self): pass def decodebin_pad_added(self, decodebin, pad): pass def processArgs(self): pass def convert(self): pass def convert_single_audio(self, inPath, outPath): pass def message_handler(self, bus, message): pass def printUsage(self): pass def printFinalStatus(self, inputFileList, starttime, endtime): pass # Run the converter converter = AudioConverter() thread.start_new_thread(converter.convert, ()) gobject.threads_init() evt_loop = gobject.MainLoop() evt_loop.run()
-
看看上面代码的最后几行。这正是我们在播放音乐部分使用的相同代码。唯一的区别是类的名称和在
thread.start_new_thread
调用中放在线程上的方法名称。一开始,声明了audioFileExists()
函数。它将被用来检查指定的路径是否是一个有效的文件路径。 -
现在编写类的构造函数。在这里,我们初始化各种变量。
def __init__(self): # Initialize various attrs self.inputDir = os.getcwd() self.inputFormat = "wav" self.outputDir = "" self.outputFormat = "" self.error_message = "" self.encoders = {"mp3":"lame", "wav": "wavenc"} self.supportedOutputFormats = self.encoders.keys() self.supportedInputFormats = ("ogg", "mp3", "wav") self.pipeline = None self.is_playing = False self.processArgs() self.constructPipeline() self.connectSignals()
-
self.supportedOutputFormats
是一个元组,用于存储支持的输出格式。self.supportedInputFormats
是从self.encoders
的键中获取的列表,用于存储支持的输入格式。这些对象在self.processArguments
中用于进行必要的检查。字典self.encoders
提供了用于创建 GStreamer 管道的编码器元素对象的正确类型的编码器字符串。正如其名所示,调用self.constructPipeline()
构建了一个gst.Pipeline
实例,并且使用self.connectSignals()
连接了各种信号。 -
接下来,准备一个 GStreamer 管道。
def constructPipeline(self): self.pipeline = gst.Pipeline("pipeline") self.filesrc = gst.element_factory_make("filesrc") self.decodebin = gst.element_factory_make("decodebin") self.audioconvert = gst.element_factory_make( "audioconvert") self.filesink = gst.element_factory_make("filesink") encoder_str = self.encoders[self.outputFormat] self.encoder= gst.element_factory_make(encoder_str) self.pipeline.add( self.filesrc, self.decodebin, self.audioconvert, self.encoder, self.filesink) gst.element_link_many(self.filesrc, self.decodebin) gst.element_link_many(self.audioconvert, self.encoder, self.filesink)
-
这段代码与我们之前在播放音乐子节中开发的代码类似。然而,有一些明显的不同。在音频播放器示例中,我们使用了
autoaudiosink
插件作为最后一个元素。在音频转换器中,我们用self.encoder
和self.filesink
元素替换了它。前者将self.audioconvert
输出的音频数据进行编码。编码器将被连接到接收器元素。在这种情况下,它是一个filesink
。self.filesink
是将音频数据写入由location
属性指定的文件的地方。 -
编码器字符串
encoder_str
决定了要创建的编码器元素的类型。例如,如果输出格式指定为"mp3",则相应的编码器是"lame" mp3 编码器。您可以通过运行 gst-inspect-0.10 命令来了解更多关于lame
mp3 编码器的信息。以下命令可以在 Linux 的 shell 上运行。$gst-inspect-0.10 lame
-
元素被添加到管道中,然后相互连接。和之前一样,在这个方法中,
self.decodebin
和self.audioconvert
没有连接,因为decodebin
插件使用动态垫。self.decodebin
的pad_added
信号在self.connectSignals()
方法中被连接。 -
另一个明显的改变是我们没有为
self.filesrc
和self.filesink
两个属性设置location
属性。这些属性将在运行时设置。由于这个工具是一个批处理工具,输入和输出文件的存储位置会不断变化。 -
让我们编写控制转换过程的主方法。
1 def convert(self): 2 pattern = "*." + self.inputFormat 3 filetype = os.path.join(self.inputDir, pattern) 4 fileList = glob.glob(filetype) 5 inputFileList = filter(audioFileExists, fileList) 6 7 if not inputFileList: 8 print "\n No audio files with extension %s "\ 9 "located in dir %s"%( 10 self.outputFormat, self.inputDir) 11 return 12 else: 13 # Record time before beginning audio conversion 14 starttime = time.clock() 15 print "\n Converting Audio files.." 16 17 # Save the audio into specified file format. 18 # Do it in a for loop If the audio by that name already 19 # exists, do not overwrite it 20 for inPath in inputFileList: 21 dir, fil = os.path.split(inPath) 22 fil, ext = os.path.splitext(fil) 23 outPath = os.path.join( 24 self.outputDir, 25 fil + "." + self.outputFormat) 26 27 28 print "\n Input File: %s%s, Conversion STARTED..."\ 29 % (fil, ext) 30 self.convert_single_audio(inPath, outPath) 31 if self.error_message: 32 print "\n Input File: %s%s, ERROR OCCURED" \ 33 % (fil, ext) 34 print self.error_message 35 else: 36 print "\nInput File: %s%s,Conversion COMPLETE"\ 37 % (fil, ext) 38 39 endtime = time.clock() 40 41 self.printFinalStatus(inputFileList, starttime, 42 endtime) 43 evt_loop.quit()
-
第 2 行到 26 行之间的代码与本书中图像文件转换实用程序中开发的代码类似。请参阅第二章处理图像的“读取和写入图像”部分,了解该代码的功能。所有输入音频文件都由第 2 行到 6 行之间的代码收集到列表
inputFileList
中。然后,我们遍历这些文件中的每一个。首先,根据用户输入推导出输出文件路径,然后是输入文件路径。 -
突出的代码行是工作马力的方法,
AudioConverter.convert_single_audio
,它实际上执行了转换输入音频的工作。我们将在下一节讨论该方法。在第 43 行,终止了主事件循环。convert
方法中的其余代码是自我解释的。 -
方法
convert_single_audio
中的代码如下所示。1 def convert_single_audio(self, inPath, outPath): 2 inPth = repr(inPath) 3 outPth = repr(outPath) 4 5 # Set the location property for file source and sink 6 self.filesrc.set_property("location", inPth[1:-1]) 7 self.filesink.set_property("location", outPth[1:-1]) 8 9 self.is_playing = True 10 self.pipeline.set_state(gst.STATE_PLAYING) 11 while self.is_playing: 12 time.sleep(1)
-
如上一步所述,
convert_single_audio
方法在self.convert()
中的 for 循环中被调用。for 循环遍历包含输入音频文件路径的列表。输入和输出文件路径作为参数传递给此方法。第 8-12 行之间的代码看起来与 播放音频 部分中展示的AudioPlayer.play()
方法大致相似。唯一的区别是此方法中没有终止主事件循环。之前我们没有为文件源和汇设置位置属性。这些属性分别在第 6 行和第 7 行设置。 -
现在关于第 2 行和第 3 行的代码是什么情况?调用
repr(inPath)
返回字符串inPath
的可打印表示。inPath
是从 'for 循环' 中获得的。os.path.normpath
不适用于这个字符串。在 Windows 上,如果你直接使用inPath
,GStreamer 在处理这样的路径字符串时会抛出错误。处理这个问题的一种方法是用repr(string)
,这将返回包括引号在内的整个字符串。例如:如果inPath
是 "C:/AudioFiles/my_music.mp3",那么repr(inPath)
将返回"'C:\\\\AudioFiles\\\\my_music.mp3'"
。注意,它有两个单引号。我们需要通过切片字符串为inPth[1:-1]
来去除开头和结尾的额外单引号。可能还有其他更好的方法。你可以想出一个,然后只需使用那个代码作为路径字符串即可! -
让我们快速浏览几个更多的方法。记下来:
def connectSignals(self): # Connect the signals. # Catch the messages on the bus bus = self.pipeline.get_bus() bus.add_signal_watch() bus.connect("message", self.message_handler) # Connect the decodebin "pad_added" signal. self.decodebin.connect("pad_added", self.decodebin_pad_added) def decodebin_pad_added(self, decodebin, pad): caps = pad.get_caps() compatible_pad=\ self.audioconvert.get_compatible_pad(pad, caps) pad.link(compatible_pad)
-
connectSignal
方法与 播放音乐 部分中讨论的方法相同,不同之处在于我们还连接了decodebin
信号与decodebin_pad_added
方法。在decodebin_pad_added
中添加一个打印语句以检查何时被调用。这将帮助你理解动态垫的工作方式!程序从处理第一个音频文件开始。调用convert_single_audio
方法。在这里,我们设置了必要的文件路径。之后,它开始播放音频文件。此时,生成pad_added
信号。因此,根据输入文件数据,decodebin
将创建垫。 -
其余的方法,如
processArgs, printUsage
和message_handler
都是不言自明的。您可以从AudioConverter.py
文件中查看这些方法。 -
现在音频转换器应该已经准备好使用了!请确保所有方法都已正确定义,然后通过指定适当的输入参数来运行代码。以下截图显示了在 Windows XP 上音频转换实用程序的示例运行。在这里,它将批量处理目录
C:\AudioFiles
中所有扩展名为.ogg
的音频文件,并将它们转换为 mp3 文件格式。结果 mp3 文件将创建在目录C:\AudioFiles\OUTPUT_AUDIOS
中。
刚才发生了什么?
在上一节中开发了一个基本的音频转换实用程序。此实用程序可以将 ogg 或 mp3 或 wav 格式的音频文件批量转换为用户指定的输出格式(其中支持的格式是 wav 和 mp3)。我们学习了如何指定编码器和 filesink 元素,并在 GStreamer 管道中链接它们。为了完成此任务,我们还应用了之前章节中获得的有关创建 GStreamer 管道、捕获总线消息、运行主事件循环等方面的知识。
让英雄尝试使用音频转换器做更多的事情
我们编写的音频转换器相当简单。它值得升级。
扩展此应用程序以支持更多音频输出格式,例如ogg, flac
等。以下管道展示了将输入音频文件转换为 ogg 文件格式的一种方法。
filesrc location=input.mp3 ! decodebin ! audioconvert ! vorbisenc ! oggmux ! filesink location=output.ogg
注意,我们有一个音频复用器,oggmux,需要与编码器 vorbisenc 链接。同样,要创建 MP4 音频文件,它将需要{ faac ! mp4mux}作为编码器和音频复用器。最简单的事情之一是定义适当的元素(如编码器和复用器),而不是从单个元素构建管道,而是使用我们之前学习过的gst.parse_launch
方法,并让它自动使用命令字符串创建和链接元素。每次音频转换被调用时,您都可以创建一个管道实例。但在这个情况下,您还需要在每次创建管道时连接信号。另一种更好、更简单的方法是在AudioConverter.constructPipeline
方法中链接音频复用器。您只需根据您使用的编码器插件类型检查是否需要它。在这种情况下,代码将是:
gst.element_link_many(self.audioconvert, self.encoder,
self.audiomuxer, self.filesink)
本例中展示的音频转换器仅接受单一音频文件格式的输入文件。这可以轻松扩展以接受所有支持的音频文件格式(除了由--output_format
选项指定的类型)。decodebin
应负责解码给定的输入数据。扩展音频转换器以支持此功能。您需要修改AudioConverter.convert()
方法中的代码,其中确定了输入文件列表。
提取音频的一部分
假设你已经记录了你最喜欢的音乐家或歌手的现场音乐会。你将这些内容保存到一个 MP3 格式的单个文件中,但现在你希望将这个文件分割成小块。使用 Python 和 GStreamer 有多种方法可以实现这一点。我们将使用最简单且可能是最有效的方法从音频轨道中切割一小部分。它利用了一个优秀的 GStreamer 插件,称为 Gnonlin。
Gnonlin 插件
多媒体编辑可以分为线性或非线性。非线性多媒体编辑允许以交互式方式控制媒体进度。例如,它允许你控制源应该执行的顺序。同时,它还允许修改媒体轨道中的位置。在进行所有这些操作时,请注意原始源(如音频文件)保持不变。因此,编辑是非破坏性的。Gnonlin 或(G-Non-Linear)为多媒体的非线性编辑提供了基本元素。它有五个主要元素,即gnlfilesource, gnlurisource, gnlcomposition, gnloperation
和gnlsource
。要了解更多关于它们属性的信息,请在每个元素上运行gst-inspect-0.10
命令。
在这里,我们只关注元素 gnlfilesource 及其一些属性。这实际上是一个 GStreamer bin 元素。像 decodebin 一样,它在运行时确定使用哪些垫。正如其名所示,它处理输入媒体文件。你需要指定的只是它需要处理的输入媒体源。媒体文件格式可以是任何支持的媒体格式。gnlfilesource 定义了多个属性。要提取音频的一部分,我们只需要考虑其中三个:
-
media-start:
输入媒体文件中的位置,它将成为提取媒体的开始位置。这也以纳秒为单位指定。 -
media-duration:
提取媒体文件的总体时长(从media-start
开始)。这以纳秒为单位指定。 -
uri:
输入媒体文件的完整路径。例如,如果它位于你的本地硬盘上,uri
将类似于file:///C:/AudioFiles/my_music.mp3
。如果文件位于网站上,那么uri
将类似于这种形式:http://path/to/file.mp3
。
gnlfilesource 内部执行诸如加载和解码文件、定位到指定的位置等操作。这使得我们的工作变得简单。我们只需要创建一些基本元素来处理 gnlfilesource 提供的信息,以创建一个输出音频文件。现在我们已经了解了 gnlfilesource 的基础知识,让我们尝试构建一个 GStreamer 管道,该管道可以从输入音频文件中切割一部分。
-
首先,gnlfilesource 元素执行至关重要的任务,包括加载、解码文件、定位到正确的起始位置,最后向我们展示代表要提取的轨道部分的音频数据。
-
一个
audioconvert
元素,将此数据转换成适当的音频格式。 -
一个编码器,将此数据进一步编码成我们想要的最终音频格式。
-
一个输出数据被丢弃的容器。这指定了输出音频文件。
尝试从命令提示符运行以下命令,用你电脑上的适当文件路径替换 uri 和位置路径。
$gst-launch-0.10.exe gnlfilesource uri=file:///C:/my_music.mp3
media-start=0 media-duration=15000000000 !
audioconvert !
lame !
filesink location=C:/my_chunk.mp3
这应该创建一个时长为 15 秒的提取音频文件,从原始文件上的初始位置开始。请注意,media-start 和 media-duration 属性以纳秒为单位接收输入。这正是我们接下来要做的本质。
行动时间 - MP3 切割器!
在本节中,我们将开发一个工具,它将从一个 MP3 格式的音频中提取一部分并保存为单独的文件。
-
请将文件
AudioCutter.py
放在手边。你可以从 Packt 网站下载它。在这里,我们只讨论重要的方法。这里未讨论的方法与早期示例中的类似。请查看包含运行此应用程序所需所有源代码的文件AudioCutter.py
。 -
按照常规方式开始。进行必要的导入并编写以下骨架代码。
import os, sys, time import thread import gobject import pygst pygst.require("0.10") import gst class AudioCutter: def __init__(self): pass def constructPipeline(self): pass def gnonlin_pad_added(self, gnonlin_elem, pad): pass def connectSignals(self): pass def run(self): pass def printFinalStatus(self): pass def message_handler(self, bus, message): pass #Run the program audioCutter = AudioCutter() thread.start_new_thread(audioCutter.run, ()) gobject.threads_init() evt_loop = gobject.MainLoop() evt_loop.run()
-
整体代码布局看起来很熟悉,不是吗?代码与我们本章早期开发的代码非常相似。关键是适当选择文件源元素并将其与管道的其余部分连接起来!代码的最后几行创建了一个线程,并运行了之前看到的主事件循环。
-
现在填写类的构造函数。这次我们将保持简单。我们需要的东西将硬编码在
class AudioCutter
的构造函数中。实现一个processArgs()
方法非常简单,就像之前多次做的那样。将代码片段中的输入和输出文件位置替换为电脑上的正确音频文件路径。def __init__(self): self.is_playing = False # Flag used for printing purpose only. self.error_msg = '' self.media_start_time = 100 self.media_duration = 30 self.inFileLocation = "C:\AudioFiles\my_music.mp3" self.outFileLocation = "C:\AudioFiles\my_music_chunk.mp3" self.constructPipeline() self.connectSignals()
-
self.media_start_time
是 mp3 文件的新起始位置(以秒为单位)。这是提取输出音频的新起始位置。self.duration
变量存储提取的音频的总时长。因此,如果你有一个总时长为 5 分钟的音频文件,提取的音频将对应于原始轨道上的 1 分钟,40 秒。此输出文件的总时长将是 30 秒,即结束时间将对应于原始轨道上的 2 分钟,10 秒。此方法最后两行代码构建了一个管道并将信号与类方法连接起来。 -
接下来,构建 GStreamer 管道。
1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 self.filesrc = gst.element_factory_make( 4 "gnlfilesource") 5 6 # Set properties of filesrc element 7 # Note: the gnlfilesource signal will be connected 8 # in self.connectSignals() 9 self.filesrc.set_property("uri", 10 "file:///" + self.inFileLocation) 11 self.filesrc.set_property("media-start", 12 self.media_start_time*gst.SECOND) 13 self.filesrc.set_property("media-duration", 14 self.media_duration*gst.SECOND) 15 16 self.audioconvert = \ 17 gst.element_factory_make("audioconvert") 18 19 self.encoder = \ 20 gst.element_factory_make("lame", "mp3_encoder") 21 22 self.filesink = \ 23 gst.element_factory_make("filesink") 24 25 self.filesink.set_property("location", 26 self.outFileLocation) 27 28 #Add elements to the pipeline 29 self.pipeline.add(self.filesrc, self.audioconvert, 30 self.encoder, self.filesink) 31 # Link elements 32 gst.element_link_many(self.audioconvert,self.encoder, 33 self.filesink)
-
突出的代码行(第 3 行)创建了 gnlfilesource。我们将其称为 self.filesrc。如前所述,它负责加载和解码音频数据,并仅提供我们需要的音频数据部分。它为主管道提供了更高层次抽象。
-
第 9 行到第 13 行之间的代码设置了
gnlfilesource
的三个属性:uri
、media-start
和media-duration
。media-start
和media-duration
以纳秒为单位指定。因此,我们将参数值(以秒为单位)乘以gst.SECOND
,以处理单位。 -
其余的代码看起来与音频转换器示例非常相似。在这种情况下,我们只支持以 mp3 音频格式保存文件。编码器元素在第 19 行定义。
self.filesink
确定输出文件将保存的位置。通过self.pipeline.add
调用将元素添加到管道中,并在第 32 行将它们连接起来。请注意,在构建管道时,gnlfilesource
元素self.filesrc
没有与self.audioconvert
连接。像decodebin
一样,gnlfilesource
实现了动态垫。因此,垫在构建管道时不可用。它根据指定的输入音频格式在运行时创建。gnlfilesource
的pad_added
信号与self.gnonlin_pad_added
方法连接起来。 -
现在编写
connectSignals
和gnonlin_pad_added
方法。def connectSignals(self): # capture the messages put on the bus. bus = self.pipeline.get_bus() bus.add_signal_watch() bus.connect("message", self.message_handler) # gnlsource plugin uses dynamic pads. # Capture the pad_added signal. self.filesrc.connect("pad-added",self.gnonlin_pad_added) def gnonlin_pad_added(self, gnonlin_elem, pad): pad.get_caps() compatible_pad = \ self.audioconvert.get_compatible_pad(pad, caps) pad.link(compatible_pad)
-
在
connectSignals
方法中高亮的代码行将gnlfilesource
的pad_added
信号与gnonlin_pad_added
方法连接起来。gnonlin_pad_added
方法与之前开发的AudioConverter
类的decodebin_pad_added
方法相同。每当gnlfilesource
在运行时创建一个垫时,此方法就会被调用,在这里,我们手动将gnlfilesource
的垫与self.audioconvert
上的兼容垫连接起来。 -
其余的代码与播放音频部分中开发的代码非常相似。例如,
AudioCutter.run
方法相当于AudioPlayer.play
等等。你可以从AudioCutter.py
文件中查看剩余方法的代码。 -
一切准备就绪后,从命令行运行程序,如下所示:
-
这应该会创建一个新的 MP3 文件,它只是原始音频文件的一个特定部分。
$python AudioCutter.py
刚才发生了什么?
我们完成了创建一个可以切割 MP3 音频文件的一部分(同时保持原始文件不变)的实用程序。这个音频片段被保存为一个单独的 MP3 文件。我们了解了一个非常实用的插件,称为 Gnonlin,用于非线性多媒体编辑。我们学习了此插件中gnlfilesource
元素的一些基本属性,用于提取音频文件。
尝试扩展 MP3 切割器
-
修改此程序,以便将如
media_start_time
之类的参数作为参数传递给程序。你需要一个像processArguments()
这样的方法。你可以使用getopt
或OptionParser
模块来解析参数。 -
添加对其他文件格式的支持。例如,扩展此代码,使其能够从 wav 格式的音频中提取一段并保存为 MP3 音频文件。输入部分将由
gnlfilesource
处理。根据输出文件格式类型,你可能需要一个特定的编码器,可能还需要一个音频多路复用器元素。然后添加并链接这些元素到主 GStreamer 管道中。
录制
在学习如何从我们最喜欢的音乐曲目中剪裁片段之后,接下来令人兴奋的事情将是一个“自产”的音频录制器。然后你可以按你喜欢的方式用它来录制音乐、模仿或简单的讲话!
还记得我们用来播放音频的管道吗?播放音频的管道元素包括 filesrc ! decodebin ! audioconvert ! autoaudiosink
。autoaudiosink 执行自动检测计算机上的输出音频设备的工作。
对于录制目的,音频源将来自连接到你的计算机的麦克风。因此,将不会有任何 filesrc
元素。我们将用自动检测输入音频设备的 GStreamer 插件来替换它。按照类似的思路,你可能还想将录音保存到文件中。因此,autoaudiosink
元素将被替换为 filesink
元素。
autoaudiosrc 是一个我们可以用来检测输入音频源的元素。然而,在 Windows XP 上测试这个程序时,autoaudiosrc 由于未知原因无法检测音频源。因此,我们将使用名为 dshowaudiosrc 的 Directshow
音频捕获源插件来完成录制任务。运行 gst-inspect-0.10 dshowaudiosrc
命令以确保它已安装,并了解此元素的各种属性。将此插件放入管道在 Windows XP 上运行良好。dshowaudiosrc
与 audioconvert 相连接。
使用这个信息,让我们尝试使用 GStreamer 的命令行版本。确保你的计算机连接了麦克风或内置了麦克风。为了改变一下,我们将输出文件保存为 ogg
格式。
gst-launch-0.10.exe dshowaudiosrc num-buffers=1000 !
audioconvert ! audioresample !
vorbisenc ! oggmux !
filesink location=C:/my_voice.ogg
audioresample 使用不同的采样率重新采样原始音频数据。然后编码元素对其进行编码。如果存在,多路复用器或 mux 将编码数据放入单个通道。录制的音频文件将写入由 filesink 元素指定的位置。
行动时间 - 录制
好的,现在是时候编写一些代码来为我们进行音频录制了。
-
下载文件
RecordingAudio.py
并查看代码。你会注意到唯一重要的任务是为音频录制设置一个合适的管道。在内容上,其他代码与我们之前在章节中学到的非常相似。它将有一些小的差异,例如方法名称和打印语句。在本节中,我们将仅讨论class AudioRecorder
中的重要方法。 -
编写构造函数。
def __init__(self): self.is_playing = False self.num_buffers = -1 self.error_message = "" self.processArgs() self.constructPipeline() self.connectSignals()
-
这与
AudioPlayer.__init__()
类似,但我们添加了对processArgs()
的调用,并初始化了错误报告变量self.error_message
以及表示录音总时长的变量。 -
通过编写
constructPipeline
方法来构建 GStreamer 管道。1 def constructPipeline(self): 2 # Create the pipeline instance 3 self.recorder = gst.Pipeline() 4 5 # Define pipeline elements 6 self.audiosrc = \ 7 gst.element_factory_make("dshowaudiosrc") 8 9 self.audiosrc.set_property("num-buffers", 10 self.num_buffers) 11 12 self.audioconvert = \ 13 gst.element_factory_make("audioconvert") 14 15 self.audioresample = \ 16 gst.element_factory_make("audioresample") 17 18 self.encoder = \ 19 gst.element_factory_make("lame") 20 21 self.filesink = \ 22 gst.element_factory_make("filesink") 23 24 self.filesink.set_property("location", 25 self.outFileLocation) 26 27 # Add elements to the pipeline 28 self.recorder.add(self.audiosrc, self.audioconvert, 29 self.audioresample, 30 self.encoder, self.filesink) 31 32 # Link elements in the pipeline. 33 gst.element_link_many(self.audiosrc,self.audioconvert, 34 self.audioresample, 35 self.encoder,self.filesink)
-
我们使用
dshowaudiosrc
(Directshow audiosrc)插件作为音频源元素。它找出输入音频源,例如,来自麦克风的音频输入。 -
在第 9 行,我们将缓冲区数量属性设置为
self.num_buffers
指定的值。默认值为-1
,表示没有缓冲区数量的限制。例如,如果你将其指定为500
,它将在发送流结束消息以结束程序运行之前输出500
个缓冲区(5 秒时长)。 -
在第 15 行,创建了一个名为'audioresample'的元素实例。该元素从
self.audioconvert
获取原始音频缓冲区,并将其重新采样到不同的采样率。编码器元素随后将音频数据编码成合适的格式,并将录音文件写入由self.filesink
指定的位置。 -
第 28 行到 35 行之间的代码向管道添加了各种元素并将它们连接起来。
-
查看文件
RecordingAudio.py
中的代码以添加剩余的代码。然后运行程序以录制你的声音或任何你想录制的可听声音!以下是一些示例命令行参数。此程序将录制 5 秒钟的音频。$python RecordingAudio.py -num_buffers=500 - out_file=C:/my_voice.mp3
刚才发生了什么?
我们学习了如何使用 Python 和 GStreamer 录制音频。我们开发了一个简单的音频录制工具来完成这项任务。GStreamer 插件 dshowaudiosrc 为我们捕获了音频输入。我们通过添加这个和其他元素创建了主要的 GStreamer 管道,并用于音频录制程序。
概述
本章让我们对使用 Python 和 GStreamer 多媒体框架进行音频处理的基础有了更深入的了解。我们使用了 GStreamer 的几个重要组件来开发一些常用的音频处理工具。本章的主要学习要点可以总结如下:
-
GStreamer 安装:我们学习了如何在各种平台上安装 GStreamer 及其依赖包。这为学习音频处理技术搭建了舞台,也将对下一章的音频/视频处理章节有所帮助。
-
GStreamer 入门:快速入门 GStreamer 帮助我们理解了媒体处理所需的重要元素。
-
使用 GStreamer API 开发音频工具:我们学习了如何使用 GStreamer API 进行通用音频处理。这帮助我们开发了音频播放器、文件格式转换器、MP3 切割器和音频录制器等工具。
现在我们已经学习了使用 GStreamer 进行基本音频处理,我们准备给音频添加一些“风味”。在下一章中,我们将学习一些技巧,这些技巧将帮助我们为音频添加特殊效果。
第六章。音频控制和效果
在上一章中,我们的重点是学习音频处理的基础知识。它向我们介绍了 GStreamer 多媒体框架。我们应用这些知识开发了一些常用的音频处理工具。在这一章中,我们将更进一步,开发添加音频效果、混音音频轨道、创建自定义音乐轨道等工具。
在这一章中,我们将:
-
学习如何控制流音频。
-
通过添加如淡入淡出、回声和全景等效果来增强音频。
-
在一个项目中工作,通过组合不同的音频片段创建自定义音乐轨道。
-
向流音频添加可视化效果。
-
将两个音频流混合成单个轨道。例如,将只包含人声轨道的音频与只包含背景音乐轨道的音频混合。
那么让我们开始吧。
控制播放
在音频播放器中,各种选项如播放、暂停、停止等,提供了一种控制流音频的方式。这些播放控制也在其他音频处理技术中得到应用。我们已经在第五章 处理音频 中使用了一些播放控制。在这一章中,我们将研究更多的控制选项。
播放
在上一章中,我们使用 GStreamer 开发了一个初步的命令行音频播放器。可以通过指示 GStreamer 管道开始音频数据的流动来启动音频流。这是通过以下代码实现的:
self.pipeline.set_state(gst.STATE_PLAYING)
根据上述指令,音频将一直流出到流结束。参考第五章 处理音频 中 播放音频 部分的代码,看看周围的代码是什么样的。如果你为简单的音频播放器开发用户界面,可以将“播放”按钮连接到一个方法,该方法将设置管道状态为 gst.STATE_PLAYING
。
暂停/恢复
可以通过将 GStreamer 管道状态设置为 gst.STATE_PAUSED
来暂时暂停流音频。在音频播放器中暂停音乐是另一个常见的操作。但这在执行一些特殊音频处理时也有用途。
行动时间 - 暂停和恢复播放音频流
我们现在将回顾一个非常简单的示例,演示各种播放控制技术。在接下来的几个部分中,将使用相同的示例。这个练习将在“使用播放控制提取音频”项目中进行时是一个理想的准备。那么,让我们开始吧!
-
从 Packt 网站下载文件
PlaybackControlExamples.py
。此文件包含展示各种播放控制的必要代码。以下是对整体类及其方法的参考说明。查看源文件以了解更多关于这些方法的信息。class AudioPlayer: def __init__(self): pass def constructPipeline(self): pass def connectSignals(self): pass def decodebin_pad_added(self, decodebin, pad ): pass def play(self): pass def runExamples(self): pass def runPauseExample(self): pass def runStopExample(self): pass def runSeekExample(self): pass def okToRunExamples(self): pass def message_handler(self, bus, message): pass
-
整体代码布局与第五章“与音频工作”部分中“播放音频”小节开发的代码非常相似。因此,我们只需回顾一些与该部分相关的新增方法。
-
这是
self.play
方法的代码。1 def play(self): 2 self.is_playing = True 3 self.player.set_state(gst.STATE_PLAYING) 4 self.position = None 5 while self.is_playing: 6 time.sleep(0.5) 7 try: 9 self.position = ( 10 self.player.query_position(gst.FORMAT_TIME, 11 None) [0] ) 16 except gst.QueryError: 17 # The pipeline has probably reached 18 # the end of the audio, (and thus has 'reset' itself. 19 # So, it may be unable to query the current position. 20 # In this case, do nothing except to reset 21 # self.position to None. 22 self.position = None 23 24 if not self.position is None: 25 #Convert the duration into seconds. 26 self.position = self.position/gst.SECOND 27 print "\n Current playing time: ", 28 self.position 29 30 self.runExamples() 31 evt_loop.quit()
-
在 while 循环中,第 9 行使用
query_position
调用查询流式音频的当前位置。这是 GStreamer Pipeline 对象的 API 方法。当流接近结束时,在查询当前位置时可能会抛出错误。因此,我们在 try-except 块中捕获gst.QueryError
异常。在进入 try-except 块之前,time.sleep
调用非常重要。它确保每 0.5 秒查询一次位置。如果你删除这个调用,下一行代码将在每个增量的小步骤上执行。从性能的角度来看,这是不必要的。因此获得的位置以纳秒表示,因此,如果时间是 0.1 秒,它将以 10 000 000 纳秒获得。为了将其转换为秒,它被除以 GStreamer 常量gst.SECOND
。在第 30 行,调用运行各种音频控制示例的主方法。 -
现在让我们看看
self.runExamples
方法中的代码。1 def runExamples(self): 2 3 if not self.okToRunExamples(): 4 return 5 6 # The example will be roughly be run when the streaming 7 # crosses 5 second mark. 8 if self.position >= 5 and self.position < 8: 9 if self.pause_example: 10 self.runPauseExample() 11 elif self.stop_example: 12 self.runStopExample() 13 elif self.seek_example: 14 self.runSeekExample() 15 # this flag ensures that an example is run 16 # only once. 17 self.ranExample = True
-
self.okToRunExamples
方法进行一些初步的错误检查,并确保总流式传输时间超过 20 秒。这里不会讨论这个方法。当当前轨道位置达到 5 秒时,将运行一个示例。运行哪个示例由相应的布尔标志确定。例如,如果self.pause_example
标志设置为 True,它将运行将“暂停”音频流的代码。其他示例也是如此。这三个标志在__init__
方法中初始化为 False。 -
我们将要回顾的最后一个方法是
self.runPauseExample
。1 def runPauseExample(self): 2 print ("\n Pause example: Playback will be paused" 3 " for 5 seconds and will then be resumed...") 4 self.player.set_state(gst.STATE_PAUSED) 5 time.sleep(5) 6 print "\n .. OK now resuming the playback" 7 self.player.set_state(gst.STATE_PLAYING)
-
流式音频在第四行被调用暂停。
time.sleep
调用将使音频暂停 5 秒钟,然后通过第七行的调用恢复音频播放。 -
确保在
__init__
方法中将self.pause_example
标志设置为 True,并为self.inFileLocation
变量指定正确的音频文件路径。然后从命令提示符运行此示例:$python PlaybackControlExamples.py
- 音频将播放前 5 秒钟。然后暂停 5 秒钟,最后恢复播放。
刚才发生了什么?
通过一个简单的示例,我们学习了如何暂停流式音频。我们还看到了如何查询流式音频的当前位置。这些知识将在本章后面的项目中使用。
停止
将 GStreamer 管道的状态设置为gst.STATE_NULL
将停止音频流。回想一下前一章“播放音频”部分中解释的message_handler
方法。我们在将流结束消息放在bus
上时使用了这个状态。在PlaybackControlExamples.py
文件中,以下代码停止了音频的流式传输。
def runStopExample(self):
print ("\n STOP example: Playback will be STOPPED"
" and then the application will be terminated.")
self.player.set_state(gst.STATE_NULL)
self.is_playing = False
在这个文件中,将标志self.stop_example
设置为True
,然后从命令行运行程序以查看这个示例。
快进/倒带
快进或倒带一个音轨简单来说就是将正在播放的音频轨道上的当前位置移动到另一个位置。这也被称为在音轨上定位位置。GStreamer 的pipeline
元素定义了一个 API 方法seek_simple
,它简化了在流式音频中跳转到音轨上指定位置的操作。在PlabackControlExamples.py
文件中,这通过以下方法进行了说明。
def runSeekExample(self):
print ("\n SEEK example: Now jumping to position at 15 seconds"
"the audio will continue to stream after this")
self.player.seek_simple(gst.FORMAT_TIME,
gst.SEEK_FLAG_FLUSH,
15*gst.SECOND)
self.player.set_state(gst.STATE_PAUSED)
print "\n starting playback in 2 seconds.."
time.sleep(2)
self.player.set_state(gst.STATE_PLAYING)
当调用此方法时,当前音频位置将移动到音频轨道上对应 15 秒持续时间的位置。代码中的高亮行是关键。seek_simple
方法接受三个参数。第一个参数gst.FORMAT_TIME
代表音轨上的时间。第二个参数gst.SEEK_FLAG_FLUSH
是一个“定位标志”。它告诉管道清除当前正在播放的音频数据。换句话说,它指示清空管道。根据文档,这使得定位操作更快。还有其他几个定位标志。请参阅 GStreamer 文档以了解更多关于这些标志的信息。第三个参数指定了音轨上的时间,这将成为流式音频的新“当前位置”。这次我指定了纳秒,因此它乘以一个常数gst.SECOND
。请注意,在调用seek_simple
方法之前,管道应该处于播放状态。
项目:使用播放控制提取音频
在最后一章,我们学习了如何使用gnonlin
插件提取音频片段。Gnonlin
使我们的工作变得非常简单。在这个项目中,我们将看到另一种提取音频文件的方法,即通过使用 GStreamer 的基本音频处理技术。我们将使用刚刚学到的某些音频播放控制。这个项目将作为 GStreamer API 各种基本组件的复习。
行动时间 - 从基本原理开始的 MP3 裁剪
让我们创建一个从“基本原理”开始的 MP3 裁剪器。也就是说,我们不会使用gnonlin
来完成这个任务。在这个项目中,我们将应用关于定位播放音轨、暂停管道以及基本音频处理操作的知识。
此实用程序可以从命令行运行:
$python AudioCutter_Method2.py [options]
其中,[options]
如下:
-
--input_file:
需要从其中裁剪音频片段的 MP3 格式的输入音频文件。 -
--output_file:
存储提取音频的输出文件路径。这需要是 MP3 格式。 -
--start_time:
在原始音轨上的秒数位置。这将是提取音频的起始位置。 -
--end_time:
在原始音轨上的秒数位置。这将是提取音频的结束位置。 -
--verbose_mode:
在提取音频时打印有用的信息,例如音轨的当前位置(以秒为单位)。默认情况下,此标志设置为False
。
-
从 Packt 网站下载文件
AudioCutter_Method2.py
。在这里,我们只讨论最重要的方法。你可以参考这个文件中的源代码来开发其余的代码。 -
我们将像往常一样开始,定义一个具有空方法的类。
import os, sys, time import thread import gobject from optparse import OptionParser import pygst pygst.require("0.10") import gst class AudioCutter: def __init__(self): pass def constructPipeline(self): pass def decodebin_pad_added(self, decodebin, pad): pass def connectSignals(self): pass def run(self): pass def extractAudio(self): pass def processArgs(self): pass def printUsage(self): pass def printFinalStatus(self): pass def message_handler(self, bus, message): pass audioCutter = AudioCutter() thread.start_new_thread(audioCutter.run, ()) gobject.threads_init() evt_loop = gobject.MainLoop() evt_loop.run()
-
如你所见,整体结构和方法名与前面章节中的 MP3 切割示例非常一致。我们不再有
gnonlin_pad_added
方法,而是有decodebin_pad_added
,这表明我们将捕获decodebin
的pad_added
信号。此外,还有新的方法run
和extractAudio
。我们将详细讨论这些方法。 -
现在我们来回顾一下类的构造函数。
1 def __init__(self): 2 self.start_time = None 3 self.end_time = None 4 self.is_playing = False 5 self.seek_done = False 6 self.position = 0 7 self.duration = None 8 #Flag used for printing purpose only. 9 self.error_msg = '' 10 self.verbose_mode = False 11 12 self.processArgs() 13 self.constructPipeline() 14 self.connectSignals()
-
__init__
方法调用方法处理用户输入,然后通过调用constructPipeline()
方法构建 GStreamer 管道。这与我们在前面几个示例中看到的情况类似。 -
想想这个问题。要提取音频,你需要哪些元素?我们需要在上一章开发的音频转换工具中使用的所有元素。请注意,在这个示例中,我们将输出保存为与输入相同的音频格式。让我们尝试构建一个初始管道。
1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 self.fakesink = gst.element_factory_make("fakesink") 4 filesrc = gst.element_factory_make("filesrc") 5 filesrc.set_property("location", self.inFileLocation) 6 7 autoaudiosink = gst.element_factory_make( 8 "autoaudiosink") 9 10 self.decodebin = gst.element_factory_make("decodebin") 11 12 self.audioconvert = gst.element_factory_make( 13 "audioconvert") 14 15 self.encoder = gst.element_factory_make("lame", 16 "mp3_encoder") 17 18 self.filesink = gst.element_factory_make("filesink") 19 self.filesink.set_property("location", 20 self.outFileLocation) 21 22 self.pipeline.add(filesrc, self.decodebin, 23 self.audioconvert, 24 self.encoder, self.fakesink) 25 26 gst.element_link_many(filesrc, self.decodebin) 27 gst.element_link_many(self.audioconvert, 28 self.encoder, self.fakesink)
-
我们已经熟悉了包含在这个管道中的大多数元素。管道看起来与音频转换工具中的管道相同,只是 sink 元素不同。注意,在第 18 行创建了
filesink
元素。但是,它没有被添加到管道中!相反,我们添加了一个fakesink
元素。你能猜到为什么吗?这是一个提取工具。我们只需要保存输入音频文件的一部分。提取部分的起始位置可能不是原始轨道的起始位置。因此,在这个时候,我们不会将filesink
添加到管道中。 -
接下来编写
AudioCutter.run
方法。1 def run(self): 2 self.is_playing = True 3 print "\n Converting audio. Please be patient.." 4 self.pipeline.set_state(gst.STATE_PLAYING) 5 time.sleep(1) 6 while self.is_playing: 7 self.extractAudio() 8 self.printFinalStatus() 9 evt_loop.quit()
-
在第 4 行,我们应用了一个播放控制命令来指示管道开始播放。输入音频的状态被设置为
STATE_PLAYING
。如前所述,self.is_playing
标志在message_handler
方法中被更改。在while
循环中,调用工作马方法self.extractAudio()
。其余的代码是自解释的。 -
现在我们将回顾执行切割输入音频片段工作的方法。让我们首先看看在
extractAudio()
方法中考虑的重要事项。然后,理解代码将会非常容易。下面的插图列出了这些重要事项。- 在
AudioCutter.extractAudio()
方法中考虑的重要步骤在前面的图像中显示。
- 在
-
要从输入中提取一段音频,需要通过管道的数据流开始。然后,我们需要跳转到输入音频中对应于要提取的音频文件起始位置的点。一旦确定了起始位置,GStreamer 管道需要调整,以便有一个
filesink
元素。filesink
将指定输出音频文件。设置好管道后,我们需要开始数据流。当达到用户指定的结束位置时,程序执行应停止。现在让我们编写代码。1 def extractAudio(self): 2 if not self.seek_done: 3 time.sleep(0.1) 4 self.duration = \ 5 self.pipeline.query_duration(gst.FORMAT_TIME, 6 None) [0] 7 self.duration = self.duration/gst.SECOND
8 9 if self.start_time > self.duration: 10 print "\n start time specified" \ 11 " is more than the total audio duration"\ 12 " resetting the start time to 0 sec" 13 self.start_time = 0.0 14 15 self.pipeline.seek_simple(gst.FORMAT_TIME, 16 gst.SEEK_FLAG_FLUSH, 17 self.start_time*gst.SECOND) 18 19 self.pipeline.set_state(gst.STATE_PAUSED) 20 self.seek_done = True 21 self.pipeline.remove(self.fakesink) 22 23 self.pipeline.add(self.filesink) 24 gst.element_link_many(self.encoder, self.filesink) 25 self.pipeline.set_state(gst.STATE_PLAYING) 26 27 time.sleep(0.1) 28 try: 29 self.position = self.pipeline.query_position( 30 gst.FORMAT_TIME, None)[0] 31 self.position = self.position/gst.SECOND 32 except gst.QueryError: 33 # The pipeline has probably reached 34 # the end of the audio, (and thus has 'reset' itself) 35 if self.duration is None: 36 self.error_msg = "\n Error cutting the audio 37 file.Unable to determine the audio duration." 38 self.pipeline.set_state(gst.STATE_NULL) 39 self.is_playing = False 40 if ( self.position <= self.duration and 41 self.position > (self.duration - 10) ): 42 # Position close to the end of file. 43 # Do nothing to avoid a possible traceback. 44 #The audio cutting should work 45 pass 46 else: 47 self.error_msg =" Error cutting the audio file" 48 self.pipeline.set_state(gst.STATE_NULL) 49 self.is_playing = False 50 51 if not self.end_time is None: 52 if self.position >= self.end_time: 53 self.pipeline.set_state(gst.STATE_NULL) 54 self.is_playing = False 55 56 if self.verbose_mode: 57 print "\n Current play time: =", self.position
-
代码块在第 3 到 25 行之间执行,仅在程序第一次进入此方法时执行。标志
self.seek_done
确保它只执行一次。这是上述插图中的矩形块表示的步骤 2 到 5 的重要代码片段。现在让我们详细回顾这段代码。 -
在第 3 行,我们通过
time.sleep
调用让程序等待 0.1 秒。这是为了下一行代码查询播放总时长所必需的。API 方法query duration
返回播放的总时长。参数gst.FORMAT_TIME
确保返回值是以时间格式(纳秒)表示的。为了将其转换为秒,我们需要将其除以gst.SECOND
。 -
接下来,在第 15-17 行,我们跳转到输入音频轨道中与用户提供的参数
self.start_time
相对应的位置。请注意,方法seek_simple
中的时间参数需要以纳秒为单位。因此,它乘以gst.SECOND
。 -
在第 19 行,
gst.STATE_PAUSED
调用暂停了管道中的数据流。通过self.pipline.remove
调用从管道中移除fakesink
元素。这也将其从管道中解链。然后,在第 23 和 24 行将self.filesink
元素添加到管道中并连接。这样,我们就准备好再次播放音频文件了。从现在开始,音频数据将保存到由filesink
元素指示的音频文件中。 -
在第 27 行,查询正在播放的当前位置。请注意,这是在 try-except 块中完成的,以避免在音频接近文件末尾时查询位置时出现任何可能的错误。当
self.position
达到指定的self.end_time
时,通过gst.STATE_NULL
调用停止通过管道的数据流。 -
编写其他方法,如
decodebin_pad_added
、connectSignals
。源代码可以在文件AudioCutter_Method2.py
中找到。 -
现在我们已经准备好运行程序了。通过指定本节开头提到的适当参数,从命令行运行它。
刚才发生了什么?
通过应用基本的音频处理技术,我们开发了一个 MP3 切割工具。这是提取音频的另一种方式。我们通过利用前面章节中学到的各种播放控制完成了这项任务。
调整音量
我们执行的最常见的音频操作之一是调整正在播放的音频的音量级别。假设你在电脑上有一系列你最喜欢的歌曲。多年来,你从各种来源添加歌曲到这个收藏夹,并创建了一个 '播放列表',以便你可以一首接一首地听。但有些歌曲的音量比其他歌曲大得多。当然,每次这样的歌曲开始播放时,你可以调整音量,但这不是你想要的,对吧?你想要解决这个问题,但怎么做呢?让我们来学习一下!
GStreamer 中的 volume
元素可以用来控制流音频的音量。它被归类为一种音频过滤器。运行 gst-inspect-0.10
命令来了解其属性的更多详细信息。
你将如何使用 GStreamer 命令行版本调整音量?以下是在 Windows XP 上完成此操作的命令。你应该使用正斜杠,因为反斜杠不能被 'location' 属性正确解析。
$gstlaunch-0.10 filesrc location=/path/to/audio.mp3 ! decodebin ! Audioconvert ! volume volume=0.8 ! autoaudiosink
此管道与音频播放示例非常相似。我们只是添加了一个 volume
元素在 audioconvert
之后。
行动时间 - 调整音量
现在,让我们开发一个修改音频文件音量的 Python 示例。我们将编写一个实用程序,它可以接受一个输入音频文件,并以增加或减少默认音量级别的方式写入输出文件。该实用程序将支持写入 MP3 格式的音频文件。如果你需要其他格式,你可以扩展此应用程序。参考我们在上一章中做的 Audio Converter 项目。
-
从 Packt 网站下载
AudioEffects.py
文件。它包含此示例以及 淡入淡出效果 的源代码。 -
编写
AudioEffects
类的构造函数。1 def __init__(self): 2 self.is_playing = False 3 # Flag used for printing purpose only. 4 self.error_msg = '' 5 self.fade_example = False 6 self.inFileLocation = "C:/AudioFiles/audio1.mp3" 7 self.outFileLocation = ( 8 "C:/AudioFiles/audio1_out.mp3" ) 9 10 self.constructPipeline() 11 self.connectSignals()
-
在此示例中,
self.fade_example
标志应设置为False
。现在你可以忽略它。它将在 淡入淡出效果 部分中使用。在 6 和 8 行指定适当的输入和输出音频文件路径。 -
我们将接下来查看
self.constructPipeline()
方法。1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 4 self.filesrc = gst.element_factory_make("filesrc") 5 self.filesrc.set_property("location", 6 self.inFileLocation) 7 8 self.decodebin = gst.element_factory_make("decodebin") 9 self.audioconvert = gst.element_factory_make( 10 "audioconvert") 11 self.encoder = gst.element_factory_make("lame") 12 13 self.filesink = gst.element_factory_make("filesink") 14 self.filesink.set_property("location", 15 self.outFileLocation) 16 17 self.volume = gst.element_factory_make("volume") 18 self.volumeLevel = 2.0 19 20 if self.fade_example: 21 self.setupVolumeControl() 22 else: 23 self.volume.set_property("volume", 24 self.volumeLevel) 25 26 27 self.pipeline.add(self.filesrc, 28 self.decodebin, 29 self.audioconvert, 30 self.volume, 31 self.encoder, 32 self.filesink) 33 34 gst.element_link_many( self.filesrc, self.decodebin) 35 gst.element_link_many(self.audioconvert, 36 self.volume, 37 self.encoder, 38 self.filesink)
-
以通常的方式创建各种 GStreamer 元素。在第 17 行,创建了音量元素。
-
volume
元素有一个 "volume" 属性。这决定了流音频的音量级别。默认情况下,此值为 1.0,表示音频的当前默认音量的 100%。0.0 的值表示没有音量。大于 1.0 的值将使音频比原始级别更响亮。让我们将此级别设置为 2.0,这意味着生成的音量将比原始音量更大。此方法中的其余代码在 GStreamer 管道中添加并链接元素。 -
查看之前提到的文件中的其余代码。它很直观。
-
在命令提示符下运行程序:
-
播放生成的音频,并将其默认音量与原始音频进行比较。
$python AudioEffects.py
刚才发生了什么?
通过一个非常简单的示例,我们学习了如何更改音频文件的默认音量级别。如果您想在音频的某些点有不同的音量级别怎么办?我们将在 淡入淡出效果 部分很快讨论这个问题。
音频效果
就像人们为了改善食物的味道而添加香料一样,为了增强音乐或任何声音,我们添加音频效果。GStreamer 中有各种各样的音频效果插件。在接下来的章节中,我们将讨论一些常用的音频效果。
淡入淡出效果
淡入淡出是音频音量级别的逐渐增加或减少。淡出意味着随着音频接近结尾时逐渐降低音频文件的音量。通常,在结尾时,音量级别被设置为 0。类似地,淡入效果从音频的开始逐渐增加音量级别。在本章中,我们将学习如何向音频添加淡出效果。一旦我们学会了这一点,实现淡入效果就变得非常简单了。
动手时间 - 淡入淡出效果
让我们向输入音频添加淡出效果。我们将使用与 调整音量 部分中相同的源文件。
-
如果您还没有下载,请下载包含此示例源代码的文件
AudioEffects.py
。 -
在这个类的
__init__
方法中,您需要做一个小改动。将标志self.fade_example
设置为True
,这样它现在将运行添加淡出效果的代码。 -
我们已经在 调整音量 部分中回顾了
self.constructPipeline()
方法。它调用了self.setupVolumeControl()
方法。1 def setupVolumeControl(self): 2 self.volumeControl = gst.Controller(self.volume, 3 "volume") 4 self.volumeControl.set("volume", 0.0*gst.SECOND, 5 self.volumeLevel) 6 self.volumeControl.set_interpolation_mode("volume", 7 gst.INTERPOLATE_LINEAR)
-
GStreamer 的
Controller
对象在第 2 行创建。它是一个轻量级对象,提供了一种控制 GStreamer 对象各种属性的方式。在这种情况下,它将被用来调整self.volume
的 'volume' 属性。Controller
的设置方法接受三个参数,即需要控制的属性("volume")、需要在音频轨道上更改属性的时间,以及该属性的新值(self.volumeLevel)。在这里,音频开始时的音量级别被设置为self.volumeLevel
。接下来,为volume
属性设置由Controller
对象调整的插值模式。在这里,我们要求self.volumeControl
线性地从其早期值变化到新值,随着音频轨道的进行。例如,如果开始时的音量级别设置为 1.0,在 30 秒时设置为 0.5,那么在 0 到 30 秒之间的音量级别将在轨道上线性插值。在这种情况下,它将从 0 秒的 1.0 级别线性降低到 30 秒的 0.5 级别。小贴士
GStreamer 文档建议
Controller.set_interpolation_mode
已被弃用(但在本书使用的 0.10.5 版本中仍然向后兼容)。请参阅文件AudioEffects.py
中的 'TODO' 注释。 -
为了在音频末尾添加淡出效果,首先我们需要获取正在播放的音频的总时长。我们只能在音频被设置为播放后查询时长(例如,当它处于
gst.STATE_PLAYING
模式时)。这是在self.play()
方法中完成的。def play(self): self.is_playing = True self.pipeline.set_state(gst.STATE_PLAYING) if self.fade_example: self.addFadingEffect() while self.is_playing: time.sleep(1) self.printFinalStatus() evt_loop.quit()
-
一旦将管道的状态设置为
gst.STATE_PLAYING
,就会调用self.addFadingEffects()
方法,如代码中高亮显示的行所示。 -
我们现在将回顾这个方法。
1 def addFadingEffect(self): 2 # Fist make sure that we can add the fading effect! 3 if not self.is_playing: 4 print ("\n Error: unable to add fade effect" 5 "addFadingEffect() called erroniously") 6 return 7 8 time.sleep(0.1) 9 try: 10 duration = ( 11 self.pipeline.query_duration(gst.FORMAT_TIME, 12 None) [0] ) 13 #Convert the duration into seconds. 14 duration = duration/gst.SECOND 15 except gst.QueryError: 16 # The pipeline has probably reached 17 # the end of the audio, (and thus has 'reset' itself) 18 print ("\n Error: unable to determine duration." 19 "Fading effect not added." ) 20 return 21 22 if duration < 4: 23 print ("ERROR: unable to add fading effect." 24 "\n duration too short.") 25 return 26 27 fade_start = duration - 4 28 fade_volume = self.volumeLevel 29 fade_end = duration 30 31 self.volumeControl.set("volume", 32 fade_start * gst.SECOND, 33 fade_volume) 34 35 self.volumeControl.set("volume", 36 fade_end * gst.SECOND, 37 fade_volume*0.01)
-
首先,我们确保正在播放的音频时长可以无错误地计算。这是通过代码块 2-24 完成的。接下来,定义了
fade_start
时间。在这个控制点,淡出效果将开始。淡出将在音频结束前 4 秒开始。音量将从fade_start
时间线性减少到fade_end
时间。fade_volume
是淡出开始时的参考音量级别。在第 30 行和第 34 行,我们实际上为self.volumeController
设置了这些淡出时间和音量参数,self.volumeController
是调整音量的Controller
对象。通过gst.INTERPOLATE_LINEAR
(在早期步骤中讨论过)实现了音量级别的逐渐降低。 -
使用参考文件
AudioEffects.py
开发或审查剩余的代码。确保为变量self.inFileLocation
和self.outFileLocation
分别指定适当的输入和输出音频路径。然后从命令行运行程序,如下所示: -
这应该创建一个输出音频文件,其中淡出效果在文件结束前 4 秒开始。
$python AudioEffects.py
刚才发生了什么?
我们学习了如何使用 GStreamer 多媒体框架向音频文件添加淡变效果。我们使用了与调整音量部分相同的 GStreamer 管道,但这次,音量级别是通过 GStreamer 中的Controller
对象控制的。我们刚刚学到的技术将在本章后面的“组合音频片段”项目中派上用场。
尝试添加淡入效果
这将非常直接。我们之前添加了淡出效果。现在通过添加淡入效果到输入音频来扩展这个实用程序。使用总淡入时长为 4 秒。在这种情况下,fade_start
时间将是 0 秒。尝试使用gst.INTERPOLATE_CUBIC
作为插值模式。
嗡嗡嗡...
响应是原声在短时间内听到的声音的反射。在音频处理中,为了实现这种效果,输入音频信号被记录下来,然后在指定的'延迟时间'后以指定的强度播放。可以使用 GStreamer 中的audioecho
插件添加回声效果。音频回声插件应默认包含在您的 GStreamer 安装中。可以通过运行以下命令来检查:
$gst-inspect-0.10 audioecho
如果它不可用,您需要单独安装它。请参阅 GStreamer 网站以获取安装说明。
添加回声效果的行动时间
让我们编写代码将回声效果添加到输入音频。代码与在前面部分讨论的AudioEffects.py
文件中的代码非常相似。为了简化问题,我们将使用EchoEffect.py
文件中的代码,以便更容易理解。稍后,你可以轻松地将此代码与AudioEffects.py
中的代码集成。
-
下载包含添加音频回声效果的源代码的文件
EchoEffect.py
。该文件包含名为AudioEffects
的类,其构造函数具有以下代码。def __init__(self): self.is_playing = False # Flag used for printing purpose only. self.error_msg = '' #the flag that determines whether to use # a gst Controller object to adjust the # intensity of echo while playing the audio. self.use_echo_controller = False self.inFileLocation = "C:/AudioFiles/audio1.mp3" self.outFileLocation = "C:/AudioFiles/audio1_out.mp3" self.constructPipeline() self.connectSignals()
-
它与在“衰减效果”部分讨论的
__init__
方法类似。这里的一个区别是标志self.use_echo_controller
。如果它被设置为 True,GStreamer 控制器对象将在音频流传输时调整某些回声属性。我们首先将看到如何实现一个简单的回声效果,然后讨论回声控制细节。指定音频文件路径变量self.inFileLocation
和self.outFileLocation
的适当值。 -
让我们构建 GStreamer 管道。
1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 4 self.filesrc = gst.element_factory_make("filesrc") 5 self.filesrc.set_property("location", 6 self.inFileLocation) 7 8 self.decodebin = gst.element_factory_make("decodebin") 9 10 self.audioconvert = gst.element_factory_make( 11 "audioconvert") 12 self.audioconvert2 = gst.element_factory_make( 13 "audioconvert") 14 15 self.encoder = gst.element_factory_make("lame") 16 17 self.filesink = gst.element_factory_make("filesink") 18 self.filesink.set_property("location", 19 self.outFileLocation) 20 21 self.echo = gst.element_factory_make("audioecho") 22 self.echo.set_property("delay", 1*gst.SECOND) 23 self.echo.set_property("feedback", 0.3) 24 25 if self.use_echo_controller: 26 self.setupEchoControl() 27 else: 28 self.echo.set_property("intensity", 0.5) 29 30 self.pipeline.add(self.filesrc,self.decodebin, 31 self.audioconvert, 32 self.echo, 33 self.audioconvert2, 34 self.encoder, 35 self.filesink) 36 37 gst.element_link_many( self.filesrc, self.decodebin) 38 gst.element_link_many(self.audioconvert, 39 self.echo, 40 self.audioconvert2, 44 self.encoder, 45 self.filesink)
-
在第 21 行创建了
audioecho
元素。属性delay
指定了回声声音播放的持续时间。我们将其指定为 1 秒,你可以进一步增加或减少这个值。回声反馈值设置为 0.3。在第 28 行,强度属性设置为 0.5。它可以在 0.0 到 1.0 的范围内设置,并决定了回声的声音强度。因此,如果你将其设置为 0.0,回声将不会被听到。 -
注意有两个
audioconvert
元素。第一个audioconvert
将解码的音频流转换为可播放的格式,作为self.echo
元素的输入。同样,在回声元素的另一端,我们需要audioconvert
元素来处理回声效果添加后的音频格式。然后,该音频以 MP3 格式编码并保存到由self.filesink
指定的位置。 -
从命令行运行程序如下:
$python EchoEffect.py
- 如果你播放输出文件,回声声音将在整个播放期间可闻。
-
现在我们将添加一个功能,允许我们只为音频轨道的一定持续时间添加回声效果。在
__init__
方法中,将标志self.use_echo_controller
设置为True
。 -
我们现在将回顾在
self.constructPipeline()
中调用的方法self.setupEchoControl()
。def setupEchoControl(self): self.echoControl = gst.Controller(self.echo, "intensity") self.echoControl.set("intensity", 0*gst.SECOND, 0.5) self.echoControl.set("intensity", 4*gst.SECOND, 0.0)
-
设置
gst.Controller
对象与在“衰减效果”部分开发的类似。在这里,我们要求Controller
对象self.echoControl
控制audioecho
元素self.echo
的属性intensity
。在播放开始时(0 秒),我们将回声强度设置为0.5
。在播放过程中 4 秒时,我们添加另一个控制点并将intensity
级别设置为0.0
。这实际上意味着我们不想在音频播放的前 4 秒后听到任何回声! -
再次从命令行运行程序如下:
$python EchoEffect.py
- 注意,这里所做的唯一更改是将标志 self.use_echo_controller 的值设置为 True。播放输出文件;回声声音在播放过程中仅在最初的 4 秒内可听见。
刚才发生了什么?
我们学习了如何向音频剪辑添加回声。为了完成这个任务,我们在 GStreamer 管道中添加并链接了 audioecho
元素。我们还学习了如何使用 GStreamer Controller
对象有选择性地添加回声效果到音频中。
尝试添加混响效果,英雄们!
假设你在一个剧院里。当舞台中央的演员说话时,声波会在到达你的耳朵之前从剧院的表面反射。因此,你所听到的是一系列这些反射的声音。这被称为混响效果。根据 audioecho
插件文档,如果你将 audioecho
元素的 delay
属性设置为小于 0.2 秒的值,它会产生混响效果。尝试设置不同的 delay
值,小于 0.2 秒,看看它如何影响输出音频。注意,这个参数被视为整数。因此,请以纳秒为单位指定此值。例如,将 0.05 秒指定为 50000000
而不是 0.05*gst.SECOND
。这在下文中进行了说明。
self.echo.set_property("delay", 50000000)
播放/全景
可以通过使用 audiopanorama
插件(audiofx
插件的一部分)将立体声全景效果添加到声音中。这个插件应该默认包含在你的 GStreamer 安装中。使用 gst-inspect-0.10
来验证它是否存在,并了解更多关于其属性的信息。从 Packt 网站下载文件 PanoramaEffect.py
。这个文件与 AudioEffects.py
或 EchoEffect.py
大致相同。以下是从文件 PanoramaEffect.py
中的 self.constructPipeline
方法的一个代码片段。
1 # Stereo panorama effect
2 self.panorama = gst.element_factory_make("audiopanorama")
3 self.panorama.set_property("panorama", 1.0)
4
5
6 self.pipeline.add(self.filesrc,
7 self.decodebin,
8 self.audioconvert,
9 self.panorama,
10 self.encoder,
11 self.filesink)
12
13
14 gst.element_link_many( self.filesrc, self.decodebin)
15 gst.element_link_many(self.audioconvert,
16 self.panorama,
17 self.encoder,
18 self.filesink)
我们已经讨论过很多次了。让我们再次回顾一下代码,作为复习……以防你之前错过了。代码块 6-11 将所有元素添加到 GStreamer 管道中。请注意,我们调用了 gst.element_link_many
两次。你还记得为什么吗?第 14 行的第一次调用在 self.filesrc
和 self.decodebin
之间建立了一个连接。当我们第二次调用 gst.element_link_many
时,有一个重要点需要注意。请注意,我们没有将 self.decodebin
与 self.audioconvert
链接。这是因为 self.decodebin
实现了动态垫。因此,我们使用回调方法 decodebin_pad_added
在运行时将其连接。
您可以从此文件中查看其余的代码。audiopanorama
元素在代码片段的第 2 行创建。panorama
属性可以有一个在-1.0
到1.0
范围内的值。如果您连接了立体声扬声器,当指定值为-1.0
时,声音将完全来自左扬声器。同样,1.0
的值将使声音仅来自右扬声器。在上面的代码片段中,我们指示程序仅使用右扬声器进行音频流传输。如果值在这两个限制之间,则两个扬声器都会传输音频。每个扬声器的贡献将由实际值确定。
尝试控制全景效果和更多...
“移动”声音!向self.panorama
元素的panorama
属性添加一个 GStreamer Controller
对象进行调整。这与我们在EchoEffect.py
中做的是类似的。在音频流中添加一些控制点,就像之前做的那样,并为panorama
属性指定不同的值。
将此功能与本章前面讨论的AudioEffects.py
中的代码集成。
项目:组合音频片段
是时候进行一个项目了!在这个项目中,我们将创建一个单一的音频文件,该文件将依次附加自定义音频剪辑。在这里,我们将应用之前章节和上一章中学习到的几个东西。
创建一个新的音频文件,该文件是您选择的几个音频轨道的组合,涉及以下步骤:
-
首先我们需要的是需要包含的音频文件。根据我们的需求,我们可能只需要音频轨道的一小部分。因此,我们将开发一个通用应用程序,考虑到这种可能性。这在前面的时间线中得到了说明。
-
接下来,我们需要确保这些音频片段按指定的顺序播放。
-
在两个音频片段之间应该有一个“空白”或“静音”音频。
-
接下来,我们还将为轨道中的每个片段实现音频淡出效果。这将确保音频不会突然结束。
媒体“时间线”解释
在我们开始这个项目之前,理解时间线的概念非常重要。时间线可以看作是控制单个音频片段播放时间的路径的整体表示。
在这个项目中,由于我们正在保存结果音频,所以它与结果音频的总播放时间相同。在这个时间线中,我们可以指定音频需要播放的时间和需要播放多长时间。以下插图更好地解释了这一点。考虑一个总时长为 250 秒的时间线。这由中央粗线及其末端的圆圈表示。假设有三个音频剪辑,分别是媒体#1、媒体#2
和媒体#3
,如插图所示。我们希望在主时间线(要保存的音频文件)中包含这些音频剪辑的每个部分。在主媒体时间线中,从 0 秒到 80 秒的音频代表来自媒体#1
的部分。它对应于媒体#1
中从 30 秒到 110 秒的音频。同样,主媒体时间线上的 90 至 200 秒的音频代表来自媒体#2
的部分,依此类推。因此,我们可以调整主媒体时间线中单个音频剪辑的优先级和位置,以创建所需的音频输出。
在前面的图像中,主媒体时间线由多个媒体轨道表示。
行动时间 - 通过组合剪辑创建自定义音频
让我们开发一个应用程序,我们将把多个音频剪辑组合成一个单独的音频文件。
-
下载文件
CombiningAudio.py
。此文件包含运行此应用程序所需的所有代码。与之前一样,我们只讨论此类中最重要的方法。 -
写下以下代码。
1 import os, sys, time 2 import thread 3 import gobject 4 from optparse import OptionParser 5 6 import pygst 7 pygst.require("0.10") 8 import gst 9 10 class AudioMerger: 11 def __init__(self): 12 pass 13 def constructPipeline(self): 14 pass 15 def addFadingEffect(self): 16 pass 17 def setupFadeBin(self): 18 pass 19 def addGnlFileSources(self): 20 pass 21 def gnonlin_pad_added(self, gnonlin_elem, pad): 22 pass 23 def run(self): 24 pass 25 def connectSignals(self): 26 pass 27 def printUsage(self): 28 pass 29 def printFinalStatus(self): 30 pass 31 def message_handler(self, bus, message): 32 pass 33 #Run the program 34 audioMerger = AudioMerger() 35 thread.start_new_thread(audioMerger.run, ()) 36 gobject.threads_init() 37 evt_loop = gobject.MainLoop() 38 evt_loop.run()
-
代码的整体结构与本书中的其他几个示例相同。在接下来的步骤中,我们将扩展一些类方法,如 addFadingEffect、setupFadeBin。
-
现在,让我们回顾一下
constructPipeline
方法。1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 self.composition = ( 4 gst.element_factory_make("gnlcomposition") ) 5 6 # Add audio tracks to the gnl Composition 7 self.addGnlFileSources() 8 9 self.encoder = gst.element_factory_make("lame", 10 "mp3_encoder") 11 self.filesink = gst.element_factory_make("filesink") 12 self.filesink.set_property("location", 13 self.outFileLocation) 14 15 # Fade out the individual audio pieces 16 # when that audio piece is approaching end 17 self.addFadingEffect() 18 19 self.pipeline.add(self.composition, 20 self.fadeBin, 21 self.encoder, 22 self.filesink) 23 24 gst.element_link_many(self.fadeBin, 25 self.encoder, 26 self.filesink)
-
在之前的部分实现音频淡入淡出效果时,我们使用了 gnlcomposition、gnlcontroller 等功能。这些模块也将在此项目中使用。在第 7 行,我们希望包含的所有音频剪辑都被添加到时间线或 gnlcomposition 中。我们将在稍后回顾此方法。请注意,gnlcomposition 使用动态垫。垫添加的信号连接在 self.connectSignals 中。在第 17 行,为音频剪辑设置了淡入淡出效果。这确保了时间线中单个音频剪辑的平滑终止。最后,第 19 至 26 行的代码块构建了管道并链接了管道中的各种 GStreamer 元素。让我们逐一回顾这个类中的其他重要方法。
-
方法
self.addGnlFileSources
执行多项操作。它按所需顺序将音频剪辑添加到主时间线中。此方法还确保在任意两个音频剪辑之间有一些“呼吸空间”或短时间的空白音频。写下以下方法。1 def addGnlFileSources(self): 2 #Parameters for gnlfilesources 3 start_time_1 = 0 4 duration_1 = 20 5 media_start_time_1 = 20 6 media_duration_1 = 20 7 inFileLocation_1 = "C:/AudioFiles/audio1.mp3" 8 9 start_time_2 = duration_1 + 3 10 duration_2 = 30 11 media_start_time_2 = 20 12 media_duration_2 = 30 13 inFileLocation_2 ="C:/AudioFiles/audio2.mp3" 14 15 #Parameters for blank audio between 2 tracks 16 blank_start_time = 0 17 blank_duration = start_time_2 + duration_2 + 3 18 19 # These timings will be used for adding fade effects 20 # See method self.addFadingEffect() 21 self.fade_start_1 = duration_1 - 3 22 self.fade_start_2 = start_time_2 + duration_2 - 3 23 self.fade_end_1 = start_time_1 + duration_1 24 self.fade_end_2 = start_time_2 + duration_2 25 26 filesrc1 = gst.element_factory_make("gnlfilesource") 27 filesrc1.set_property("uri", 28 "file:///" + inFileLocation_1) 29 filesrc1.set_property("start", start_time_1*gst.SECOND) 30 filesrc1.set_property("duration", 31 duration_1 * gst.SECOND ) 32 filesrc1.set_property("media-start", 33 media_start_time_1*gst.SECOND) 34 filesrc1.set_property("media-duration", 35 media_duration_1*gst.SECOND) 36 filesrc1.set_property("priority", 1) 37 38 # Setup a gnl source that will act like a blank audio 39 # source. 40 gnlBlankAudio= gst.element_factory_make("gnlsource") 41 gnlBlankAudio.set_property("priority", 4294967295) 42 gnlBlankAudio.set_property("start",blank_start_time) 43 gnlBlankAudio.set_property("duration", 44 blank_duration * gst.SECOND) 45 46 blankAudio = gst.element_factory_make("audiotestsrc") 47 blankAudio.set_property("wave", 4) 48 gnlBlankAudio.add(blankAudio) 49 50 filesrc2 = gst.element_factory_make("gnlfilesource") 51 filesrc2.set_property("uri", 52 "file:///" + inFileLocation_2) 53 filesrc2.set_property("start", 54 start_time_2 * gst.SECOND) 55 filesrc2.set_property("duration", 56 duration_2 * gst.SECOND ) 57 filesrc2.set_property("media-start", 58 media_start_time_2*gst.SECOND) 59 filesrc2.set_property("media-duration", 60 media_duration_2*gst.SECOND) 61 filesrc2.set_property("priority", 2) 63 63 self.composition.add(gnlBlankAudio) 64 self.composition.add(filesrc1) 65 self.composition.add(filesrc2)
-
首先,我们声明将音频片段放入主时间线所需的各种参数。在这里,音频片段主要是
gnlfilesource
元素,而时间线是输出音频轨道的总长度。这个参数设置是通过第 3 到 13 行的代码完成的。在这个例子中,我们只结合了两个音频片段。将第 7 行和第 13 行的音频文件路径替换为你的机器上的适当文件路径。小贴士
重要提示:对于 Windows 用户,请确保按照代码片段第 13 行的示例使用正斜杠
/
指定文件路径。如果路径被指定为,例如C:\AudioFiles\audio2.mp3
,GStreamer 会以不同的方式处理\a
!一种解决方案是将路径标准化,或者始终在指定路径时使用正斜杠。在这种情况下C:/AudioFiles/audio2.mp3
。- 第一个媒体文件将在主时间线上放置 20 秒。音频的总时长由参数
media_duration_1
指定。参数media_start_1
指定第一个音频文件的实际时间,这将是主时间线上的start_time_1
。时间线背后的基本概念在本节中已解释。尝试调整一些参数,以更好地理解时间线的工作原理。对于第二个音频,注意start_time_2
是如何指定的。它等于duration_1 + 3
。添加 3 秒的时间是为了在两个轨道之间有一个“无声的寂静”。你可以将其更改为你选择的任何静音时长。
- 第一个媒体文件将在主时间线上放置 20 秒。音频的总时长由参数
-
接下来,定义了空白音频所需的参数。一般来说,
gnlcomposition
会在没有其他音频播放时播放空白音频(这是基于已设置适当的priority
)。我们定义这个静音轨道的总时长足够长,比所有音频片段的总时长都要长,这样在需要的时候这个轨道就可以“可供播放”。请注意,gnlcomposition
不会播放整个静音轨道!这只是确保我们有一个足够长的轨道可以在不同的点播放。在这个项目中,我们只使用两个音频文件。因此,没有必要将空白时长参数设置为大于或等于总时间线时长。如果只有 3 秒也行。但想象一下,如果你有超过 2 个音频片段。静音音频将在第 1 和第 2 个轨道之间播放,但之后它将不再适用于第 2 和第 3 个轨道之间的轨道!如果我们有 3 个音频轨道,那么空白音频的时长可以按照以下代码片段所示设置,并且通过向self.composition
添加另一个gnlfilesource
来实现。你也可以通过指定blank_duration = 3
来测试生成的音频文件。在这种情况下,音频片段 2 和 3 之间不会有静音轨道!start_time_3 = start_time_2 + duration_2 + 3 duration_3 = 30 media_start_time_3 = 0 media_duration_3 = 30 inFileLocation_3 ="C:\AudioFiles\audio3.mp3" # Parameters for blank audio between 2 tracks blank_start_time = 0 blank_duration = start_time_3 + duration_3 + 3
-
第 19 行到第 24 行之间的代码设置了添加到
gnlcomposition
中单个音频片段淡出效果所需的实例变量。这些将在self.addFadingEffect
方法中使用。 -
代码块 26-36 和 50-61 定义了要添加到
self.composition
中的gnlfilesource
元素及其属性。我们已经学习了gnlfilesource
,因此这些代码块应该是自解释的。然而,请看第 36 行和第 61 行的代码?在这里,我们设置了主时间线中音频片段的优先级。这是一个重要的步骤。如果你没有定义优先级,默认情况下,每个gnlsource
都将具有最高优先级,表示为值'0'。这有点棘手。最好通过调整某些值并实际播放输出音频来解释。现在让我们保持简单。请看下一个“尝试一下英雄”部分,它要求你尝试一些与priority
相关的实验。 -
让我们回顾一下代码块 40-44。在这里,第 40 行创建了一个
gnlsource
(而不是gnlfilesource
)。我们称之为gnlBlankAudio
。第 41 行非常重要。它告诉程序将此元素视为最后。也就是说,gnlBlankAudio
被设置为添加到gnlcomposition
中的元素中最低可能的优先级。这确保了空白音频片段仅在轨道之间播放,而不是作为一个单独的音频片段。每当gnlcomposition
中下一个音频的起始点接近时,它将gnlBlankAudio
推到次要位置,并开始播放这个新的音频片段。这是因为其他音频片段的priority
设置高于gnlBlankAudio
。你可能想知道priority
的值4294967295
代表什么。如果你在gnlsource
上运行gst-inspect-0.10
命令,你会注意到priority
的范围是从0
到 4294967295。因此,最低可能的优先级级别是4294967295
。在这个例子中,我们可以通过适当地指定blank_duration
参数来避免使用3
的优先级级别。但是,假设你事先不知道blank_duration
应该是多少,并将其设置为一个很大的数字。在这种情况下,如果你将gnlBlankAudio
的优先级设置为3
,在输出音频的末尾将播放gnlBlankAudio
的剩余部分。因此,总轨道持续时间将不必要地增加。相反,如果你使用4294967295
作为优先级,它将不会播放空白音频的剩余部分。如果你有多个音频轨道,并且它们的数量一开始就不知道,我们使用的最低优先级级别是空白音频片段最安全的值。如前所述,以下gnlBlankAudio
的优先级也应该有效。gnlBlankAudio.set_property("priority", 3)
-
在第 46 行,创建了一个
audiotestsrc
元素。这个插件应该包含在你的 GStreamer 安装中。这个插件可以用来生成多种基本的音频信号,例如正弦波形、静音波形等。在audiotestsrc
上运行gst-inspect-0.10
来查看它可以生成哪些类型的音频信号。所需的音频信号类型可以通过audiotestsrc
的wave
属性来指定。wave
属性的值为 4 对应于静音波形。值为 3 生成三角波形等。在第 48 行,将audiotestsrc
元素添加到gnlsource
元素(gnlBlankAudio)中。这仅仅意味着当我们开始播放gnlcomposition
时,与gnlsource
元素相关的静音音频将通过它内部的audiotestsrc
元素生成。 -
最后,第 63-65 行之间的代码将
gnlfilesource
和gnlsource
元素添加到self.composition
中。 -
现在我们将快速回顾一下
self.addFadingEffect()
方法。1 def addFadingEffect(self): 2 self.setupFadeBin() 3 4 #Volume control element 5 self.volumeControl = gst.Controller(self.volume, 6 "volume") 7 self.volumeControl.set_interpolation_mode("volume", 8 gst.INTERPOLATE_LINEAR) 9 10 fade_time = 20 11 fade_volume = 0.5 12 fade_end_time = 30 13 14 reset_time = self.fade_end_1 + 1 15 16 self.volumeControl.set("volume", 17 self.fade_start_1 * gst.SECOND, 18 1.0) 19 self.volumeControl.set("volume", 20 self.fade_end_1 * gst.SECOND, 21 fade_volume*0.2) 22 self.volumeControl.set("volume", 23 reset_time * gst.SECOND, 24 1.0) 25 self.volumeControl.set("volume", 26 self.fade_start_2 * gst.SECOND, 27 1.0) 28 self.volumeControl.set("volume", 29 self.fade_end_2 * gst.SECOND, 30 fade_volume*0.2)
-
在渐变效果部分,我们向音频文件添加了淡出效果。在那个部分,我们添加并链接了主管道中的单个元素,如音频转换和音量。在这里,我们将采取不同的方法,以便在 GStreamer 中学习更多。我们将创建一个 GStreamer
bin
元素,以向音频剪辑添加淡出效果。你可以选择使用旧方法,但创建bin
提供了一定程度的抽象。bin
元素是通过高亮显示的代码行创建的。我们将在下一个方法中回顾它。这个方法中的其余代码与之前开发的非常相似。self.volumeControl
是一个 GStreamerController
元素。我们在时间轴上指定适当的音量间隔来实现单个音频剪辑的淡出效果。在每次fade_end
时间后调整音量级别回到原始值非常重要。这确保了下一个剪辑以适当的音量级别开始。这是通过第 22-24 行之间的代码实现的。 -
现在我们来看看如何构建一个用于渐变效果的 GStreamer bin 元素。
1 def setupFadeBin(self): 2 self.audioconvert = gst.element_factory_make( 3 "audioconvert") 4 self.volume = gst.element_factory_make("volume") 5 self.audioconvert2 = gst.element_factory_make( 6 "audioconvert") 7 8 self.fadeBin = gst.element_factory_make("bin", 9 "fadeBin") 10 self.fadeBin.add(self.audioconvert, 11 self.volume, 12 self.audioconvert2) 13 14 gst.element_link_many(self.audioconvert, 15 self.volume, 16 self.audioconvert2) 17 18 # Create Ghost pads for fadeBin 19 sinkPad = self.audioconvert.get_pad("sink") 20 self.fadeBinSink = gst.GhostPad("sink", sinkPad) 21 self.fadeBinSrc = ( 22 gst.GhostPad("src", self.audioconvert2.get_pad("src")) ) 23 24 self.fadeBin.add_pad(self.fadeBinSink) 25 self.fadeBin.add_pad(self.fadeBinSrc)
-
在第 2-6 行,我们定义了在 GStreamer 管道中改变音频音量的必要元素。这没有什么新奇的。在第 8 行,我们创建了
self.fadeBin
,一个 GStreamer bin 元素。bin
是一个容器,用于管理添加到其中的元素对象。基本元素在第 10 行添加到这个 bin 中。然后,元素以与我们在 GStreamer 管道中链接元素相同的方式链接。bin 本身已经基本设置好了。但还有一件更重要的事情。我们需要确保这个 bin 可以与 GStreamer 管道中的其他元素链接。为此,我们需要创建幽灵垫。 -
回想一下上一章中提到的“幽灵垫”(
ghost pad
)。一个bin
元素是一个“抽象元素”。它没有自己的pads
。但是为了像元素一样工作,它需要pads
来连接管道内的其他元素。因此,bin
使用其内部元素的pad
,就像它是自己的pad
一样。这被称为幽灵垫。因此,幽灵垫用于连接bin
内的适当元素。这使得可以使用bin
对象作为 GStreamer 管道中的抽象元素。我们创建了两个幽灵垫。一个作为src pad
,另一个作为sink pad
。这是通过第 19-22 行的代码完成的。注意,我们使用self.audioconvert
的sink pad
作为bin
的sink ghost pad
,以及self.audioconvert2
的src pad
作为src ghost pad
。哪个pad
用作 src 或 sink 取决于我们在bin
内如何链接元素。查看第 14 到 17 行之间的代码将使问题变得清晰。最后,在第 24 和 25 行将幽灵垫添加到self.fadeBin
。 -
当
self.composition
发出pad-added
信号时,会调用self.gnonlin_pad_added()
方法。注意,在这个方法中,compatible_pad
是从self.fadeBin
获得的。def gnonlin_pad_added(self, gnonlin_elem, pad): caps = pad.get_caps() compatible_pad = \ self.fadeBin.get_compatible_pad(pad, caps) pad.link(compatible_pad)
-
通过查看
CombiningAudio.py
文件中的代码来开发其余的方法。务必指定适当的输入和输出音频文件位置。一旦所有部分都到位,运行程序如下:python CombiningAudio.py
- 这应该会创建包含合并在一起的音频剪辑的输出音频文件!
刚才发生了什么?
在这个项目中,我们开发了一个可以将两个或更多音频剪辑合并成一个音频文件的应用程序。为了实现这一点,我们使用了在早期章节和上一章中学习的许多音频处理技术。我们使用了gnonlin
插件中的各种元素,如gnlcomposition
、gnlfilesource
和gnlsource
。我们学习了如何创建和链接 GStreamer bin
容器,以在管道中表示淡出效果作为抽象元素。除此之外,我们还学习了如何在音频剪辑之间插入空白音频。
尝试更改gnlfilesource
的各种属性
在早期的“行动时间”部分,我们为添加到gnlcomposition
的gnlfilesource
元素设置了优先级属性。调整两个gnlfilesource
元素的start
和priority
属性,看看输出音频会发生什么。例如,交换两个gnlfilesource
元素的优先级,并将start_time_2
改为duration_1
,看看会发生什么。注意它如何影响第一个音频剪辑的播放!
音频混音
假设你有一些乐器音乐文件在你的收藏中。你有一个成为播放歌手的隐藏愿望,并希望与背景音乐一起唱这些歌曲。你会怎么做?好吧,最简单的事情就是戴上耳机播放任何乐器音乐。然后跟着唱并录制你的声音。好的,下一步是什么?你需要将乐器音乐和你的声音混合在一起,以得到你想要的效果!
让我们看看如何混合两个音频轨道。interleave
是一个 GStreamer 插件,它简化了两个音频轨道的混合。它以非连续的方式将多个单声道输入音频合并成一个音频流。这个插件应该包含在你的默认 GStreamer 安装中。
行动时间 - 混合音频轨道
让我们编写一个可以混合两个音频流在一起的实用程序。
-
下载包含此实用程序源代码的文件
AudioMixer.py
。 -
现在我们将回顾
constructPipeline
方法。在上一章中解释的 API 方法gst.parse_launch()
将在这里使用。1 def constructPipeline(self): 2 audio1_str = (" filesrc location=%s ! " 3 "decodebin ! audioconvert ! " 4 % (self.inFileLocation_1) ) 5 6 audio2_str = ( " filesrc location=%s " 7 "! decodebin ! audioconvert ! " 8 %(self.inFileLocation_2) ) 9 10 interleave_str = ( "interleave name=mix ! " 11 " audioconvert ! lame ! " 12 " filesink location=%s"%self.outFileLocation ) 13 14 queue_str = " ! queue ! mix." 15 16 myPipelineString = ( 17 interleave_str + audio1_str + queue_str + 18 audio2_str + queue_str ) 19 20 self.pipeline = gst.parse_launch(myPipelineString)
-
audio1_str
和audio2_str
是主管道字符串的部分。每个都包含filesrc
、decodebin
和audioconvert
元素。filesrc
提供了相应输入音频文件的位置。到目前为止,我们非常清楚这个 GStreamer 管道部分的作用。 -
在第 10-12 行,
interleave_str
定义了主管道字符串的另一部分。从interleave
元素输出的数据需要转换成编码器元素期望的格式。然后,编码器连接到filesink
元素,输出音频将存储在那里。 -
如前所述,
interleave
将多个音频通道合并成一个单一的音频流。在这种情况下,interleave
元素通过队列元素从两个不同的音频流中读取数据。队列元素的 sink pad 与 audioconvert 元素相连。队列元素是一个缓冲区,音频 convert 的音频数据被写入其中。然后这些数据被 interleave 元素进一步读取。GStreamer 管道中的这种链接可以用以下字符串表示 "audioconvert ! queue ! mix."。注意,'mix' 后的点 '.' 是重要的。它是使用
gst.parse_launch
时的语法的一部分。 -
总结一下,从管道的部分
audio1_str
和audio2_str
流出的数据最终将通过 'queue' 元素被interleave
读取,然后它将跟随由interleave_str
表示的管道的其余部分。在第 20 行,管道字符串被输入到
gst.parse_launch
中以创建一个 GStreamer 管道实例。 -
查看源文件
AudioMixer.py
的其余代码。更改由self.inFileLocation_1, self.inFileLocation_2
和self.outFileLocation
表示的输入和输出音频文件路径字符串,然后按照以下方式运行代码:$python AudioMixer.py
- 这应该会创建交错音频输出。如果您播放此音频文件,您将听到两个音频剪辑同时播放。尝试仅选择单个音频通道,例如“左”通道或“右”通道。在这种情况下,您会注意到每个音频剪辑都存储在单独的通道上。例如,如果您只播放左通道,则只会听到这些音频剪辑中的一个,其他通道也是如此。
刚才发生了什么?
使用interleave
元素,我们将两个音频轨道合并以创建一个交错音频。这可以用作音频混音工具。我们学习了如何使用queue
元素作为音频数据缓冲区,然后由交错元素读取。
可视化音频轨道
大多数流行的音频播放器都提供了一种将正在播放的音频进行“可视化”的功能。这种可视化效果通常是即时生成的,并与音频信号同步。通常,可视化器会响应音频频率和音量水平等属性的变化。这些变化通过动画图形来展示。GStreamer 提供了一些插件来可视化音轨。'monoscope'可视化插件通常包含在默认的 GStreamer 安装中。它显示流音频的高度稳定波形。请确保通过运行gst-inspect-0.10
命令,GStreamer 安装中包含monoscope
插件。还有一些其他流行的插件,如goom
和libvisual
。但在 Windows XP 上安装的 GStreamer 二进制文件中,这些插件默认不可用。您可以安装这些插件并尝试使用它们来添加可视化效果。
行动时间 - 音频可视化
可以使用不同的技术将可视化效果添加到流音频中。我们将使用所有方法中最简单的方法来开发一个音乐可视化工具。
在这里,我们将使用 GStreamer 的playbin
插件。回想一下,playbin
首次在“与音频一起工作”章节的“从网站播放音频”部分中使用。这个插件提供了一个高级的音频/视频播放器,并且应该包含在默认的 GStreamer 安装中。
-
从 Packt 网站下载
MusicVisualizer.py
文件。这是一个小程序。下面展示了类方法。查看此文件中的代码以获取更多详细信息。class AudioPlayer: def __init__(self): pass def connectSignals(self): pass def play(self): pass def message_handler(self, bus, message): pass
-
大部分代码与上一章“从网站播放音频”部分中展示的代码相同。这里唯一的区别是类的构造函数,其中定义了 playbin 元素的各种属性。
现在我们来回顾一下
AudioPlayer
类的构造函数。1 def __init__(self): 2 self.is_playing = False 3 inFileLocation = "C:/AudioFiles/audio1.mp3" 4 5 #Create a playbin element 6 self.player = gst.element_factory_make("playbin") 7 8 # Create the audio visualization element. 9 self.monoscope = gst.element_factory_make("monoscope") 10 self.player.set_property("uri", 11 "file:///" + inFileLocation) 12 self.player.set_property("vis-plugin", self.monoscope) 13 self.connectSignals()
-
将第 3 行的
inFileLocation
修改为匹配你电脑上的音频文件路径。在第 6 行和第 8 行,创建了playbin
和monoscope
元素。后者是一个插件,它能够实现音频可视化。在第 12 行,我们将属性vis-plugin
的值设置为之前创建的monoscope
元素。vis-plugin
代表'可视化插件',playbin
元素应该使用它来可视化音乐。 -
就这些!你可以从文件
MusicVisualizer.py
中查看其余的代码。现在从命令行运行程序,如下所示:$python MusicVisualizer.py
- 这应该开始播放输入音频文件,同时,它还应该弹出一个小窗口,你可以在其中'可视化'流式音频。
小贴士
注意:此应用程序的整体性能可能取决于程序运行时正在运行的进程数量。它也可能取决于你电脑的规格,如处理器速度。
- 在这里,随着音乐的播放,稳定的音频波形将显示出来。以下显示了在两个不同时间段的这个可视化窗口的快照。
使用'monoscope'的音频可视化工具在某个随机时间段的快照如下所示。
刚才发生了什么?
我们使用了 GStreamer 插件playbin
和monoscope
开发了一个用于流式音频的音频可视化工具。monoscope
元素提供了一种可视化高度稳定的音频波形的方法。
尝试使用其他可视化插件
为了说明音频的视觉效果,使用了monoscope
插件。如果你在 GStreamer 安装中有其他可视化插件,可以使用它们来创建不同的视觉效果。以下是一些可用于此目的的插件:goom, goom2k1, libvisual
和synaesthesia
。下一张插图显示了由synaesthesia
插件实现的音频可视化。
使用'通感'的音频可视化:这里展示了某些随机时间段的快照。
摘要
在本章中,我们关于各种音频增强和控制技术学到了很多。我们使用了 GStreamer 多媒体框架来完成这些工作。我们特别介绍了:
-
音频控制:如何控制音频数据的流。通过编码示例,我们学习了播放控制(如播放、暂停、搜索和停止)。然后,这些控制被用于一个项目中,从音频中提取了一部分。
-
添加效果:通过添加音频效果(如淡入、回声/混响等)来增强音频。
-
非线性音频编辑:如何将两个或多个音频流合并成一个单独的轨道。这是我们承担的一个项目中完成的。
-
音频混合技术,用于将多个单声道音频流合并成一个单声道交错音频。
此外,我们还学习了诸如可视化音频等技巧。这标志着我们使用 GStreamer 框架在 Python 中处理音频讨论的结束。
在下一章中,我们将学习如何使用 Python 处理视频。
第七章. 处理视频
照片捕捉瞬间,但视频帮助我们重温那一刻!视频已成为我们生活的重要组成部分。我们通过在摄像机上记录家庭假期来保存我们的记忆。当涉及到数字保存这些记录的记忆时,数字视频处理起着重要作用。在前一章中,为了学习各种音频处理技术,我们使用了 GStreamer 多媒体框架。我们将继续使用 GStreamer 来学习视频处理的基础知识。
在本章中,我们将:
-
开发一个简单的命令行视频播放器
-
执行基本的视频操作,如裁剪、调整大小以及调整亮度、对比度和饱和度等参数。
-
在视频流上添加文本字符串
-
学习如何在不同视频格式之间转换视频
-
编写一个实用程序,从输入视频文件中分离音频和视频轨道
-
将音频和视频轨道混合以创建单个视频文件
-
将一个或多个视频帧保存为静态图像
那么,让我们开始吧。
安装先决条件
我们将使用 GStreamer 多媒体框架的 Python 绑定来处理视频数据。请参阅第五章,“处理音频”中的安装说明,以安装 GStreamer 和其他依赖项。
对于视频处理,我们将使用一些之前未介绍过的 GStreamer 插件。请确保这些插件在您的 GStreamer 安装中可用,通过从控制台运行gst-inspect-0.10
命令(Windows XP 用户为 gst-inspect-0.10.exe)。否则,您需要安装这些插件或使用可用的替代方案。
以下是我们将在本章中使用的附加插件列表:
-
autoconvert:
根据能力确定合适的转换器。它将在本章中广泛使用。 -
autovideosink:
自动选择视频输出设备以显示流媒体视频。 -
ffmpegcolorspace:
将颜色空间转换为视频输出设备可以显示的颜色空间格式。 -
capsfilter:
是用于限制流向下传递的媒体数据类型的特性过滤器,在本章中进行了详细讨论。 -
textoverlay:
在流媒体视频上叠加文本字符串。用于“在视频流中添加文本和时间”部分。 -
timeoverlay:
在视频缓冲区顶部添加时间戳。 -
clockoverlay:
在流媒体视频上显示当前时钟时间。 -
videobalance:
用于调整图像的亮度、对比度和饱和度。它在“视频操作和效果”部分中使用。 -
videobox:
通过指定像素数裁剪视频帧,用于“裁剪”部分。 -
ffmux_mp4:
为 MP4 视频复用提供muxer
元素。 -
ffenc_mpeg4:
将数据编码为 MPEG4 格式。 -
ffenc_png:
将数据编码为 PNG 格式,用于“将视频帧保存为图像”部分。
播放视频
之前,我们看到了如何播放音频。与音频一样,视频流式传输有不同的方法。这些方法中最简单的是使用playbin
插件。另一种方法是遵循基本原理,即创建传统的管道并创建和连接所需的管道元素。如果我们只想播放视频文件的“视频”轨道,那么后一种技术与音频播放中展示的方法非常相似。然而,几乎总是,人们还希望听到正在流式传输的视频的音频轨道。为此需要额外的工作。以下图是代表 GStreamer 管道的图,展示了视频播放时数据是如何流动的。
在这个说明中,decodebin
使用适当的解码器从源元素解码媒体数据。根据数据类型(音频或视频),它随后通过queue
元素进一步流式传输到音频或视频处理元素。两个queue
元素,queue1
和queue2
,分别作为音频和视频数据的媒体数据缓冲区。当队列元素在管道中添加并连接时,GStreamer 会内部处理管道中的线程创建。
行动时间 - 视频播放器!
让我们编写一个简单的视频播放器实用工具。在这里,我们不会使用playbin
插件。playbin
的使用将在后面的子节中说明。我们将通过构建 GStreamer 管道来开发这个实用工具。关键在于使用队列作为数据缓冲区。音频和视频数据需要被引导,以便分别通过管道的音频或视频处理部分进行“流动”。
-
从 Packt 网站下载文件
PlayingVidio.py
。该文件包含此视频播放器实用工具的源代码。 -
以下代码概述了视频播放器类及其方法。
import time import thread import gobject import pygst pygst.require("0.10") import gst import os class VideoPlayer: def __init__(self): pass def constructPipeline(self): pass def connectSignals(self): pass def decodebin_pad_added(self, decodebin, pad): pass def play(self): pass def message_handler(self, bus, message): pass # Run the program player = VideoPlayer() thread.start_new_thread(player.play, ()) gobject.threads_init() evt_loop = gobject.MainLoop() evt_loop.run()
-
如您所见,代码的整体结构和主要程序执行代码与音频处理示例中的相同。线程模块用于创建一个新线程来播放视频。VideoPlayer.play 方法被发送到这个线程。gobject.threads_init()是一个初始化函数,用于促进在 gobject 模块中使用 Python 线程。创建用于执行此程序的主要事件循环使用 gobject,并通过调用 evt_loop.run()启动此循环。
小贴士
除了使用
thread
模块外,您还可以使用threading
模块。使用它的代码将类似于: -
import threading
-
threading.Thread(target=player.play).start()
您需要将早期代码片段中的
thread.start_new_thread(player.play, ())
行替换为这个笔记中代码片段中展示的第 2 行。自己试试看! -
现在,让我们讨论一些重要的方法,从
self.contructPipeline:
开始。1 def constructPipeline(self): 2 # Create the pipeline instance 3 self.player = gst.Pipeline() 4 5 # Define pipeline elements 6 self.filesrc = gst.element_factory_make("filesrc") 7 self.filesrc.set_property("location", 8 self.inFileLocation) 9 self.decodebin = gst.element_factory_make("decodebin") 10 11 # audioconvert for audio processing pipeline 12 self.audioconvert = gst.element_factory_make( 13 "audioconvert") 14 # Autoconvert element for video processing 15 self.autoconvert = gst.element_factory_make( 16 "autoconvert") 17 self.audiosink = gst.element_factory_make( 18 "autoaudiosink") 19 20 self.videosink = gst.element_factory_make( 21 "autovideosink") 22 23 # As a precaution add videio capability filter 24 # in the video processing pipeline. 25 videocap = gst.Caps("video/x-raw-yuv") 26 self.filter = gst.element_factory_make("capsfilter") 27 self.filter.set_property("caps", videocap) 28 # Converts the video from one colorspace to another 29 self.colorSpace = gst.element_factory_make( 30 "ffmpegcolorspace") 31 32 self.videoQueue = gst.element_factory_make("queue") 33 self.audioQueue = gst.element_factory_make("queue") 34 35 # Add elements to the pipeline 36 self.player.add(self.filesrc, 37 self.decodebin, 38 self.autoconvert, 39 self.audioconvert, 40 self.videoQueue, 41 self.audioQueue, 42 self.filter, 43 self.colorSpace, 44 self.audiosink, 45 self.videosink) 46 47 # Link elements in the pipeline. 48 gst.element_link_many(self.filesrc, self.decodebin) 49 50 gst.element_link_many(self.videoQueue, self.autoconvert, 51 self.filter, self.colorSpace, 52 self.videosink) 53 54 gst.element_link_many(self.audioQueue,self.audioconvert, 55 self.audiosink)
-
在各种音频处理应用中,我们已经使用了在此方法中定义的几个元素。首先,创建管道对象
self.player
。self.filesrc
元素指定了输入视频文件。此元素连接到decodebin
。 -
在第 15 行,创建了
autoconvert
元素。它是一个 GStreamerbin
,能够根据能力(caps)自动选择转换器。它将decodebin
输出的解码数据转换成视频设备可播放的格式。请注意,在到达视频输出端之前,这些数据会通过一个capsfilter
和ffmpegcolorspace
转换器。capsfilter
元素在第 26 行定义。它是一个限制允许能力的过滤器,即将通过它的媒体数据类型。在这种情况下,第 25 行定义的videoCap
对象指示过滤器只允许video-xraw-yuv
能力。 -
ffmpegcolorspace
是一个可以将视频帧转换为不同颜色空间格式的插件。此时,有必要解释一下什么是颜色空间。通过使用基本颜色,可以创建各种颜色。这些颜色形成我们所说的颜色空间。一个常见的例子是 rgb 颜色空间,其中可以通过红色、绿色和蓝色颜色的组合创建一系列颜色。颜色空间转换是将视频帧或图像从一个颜色空间转换到另一个颜色空间的过程。转换是以一种方式进行的,即转换后的视频帧或图像是原始帧或图像的更接近的表示。小贴士
即使不使用
capsfilter
和ffmpegcolorspace
的组合,也可以进行视频流传输。然而,视频可能会出现扭曲。因此,建议使用capsfilter
和ffmpegcolorspace
转换器。尝试直接将autoconvert
元素链接到autovideosink
,看看是否有所改变。 -
注意,我们已经创建了两个输出端,一个用于音频输出,另一个用于视频。两个
queue
元素是在第 32 行和第 33 行创建的。如前所述,这些元素作为媒体数据缓冲区,用于将数据发送到 GStreamer 管道的音频和视频处理部分。代码块 35-45 向管道添加了所有必需的元素。 -
接下来,将管道中的各个元素进行链接。正如我们已经知道的,
decodebin
是一个插件,用于确定使用正确的解码器类型。此元素使用动态垫。在开发音频处理工具时,我们将decodebin
的pad-added
信号连接到了decodebin_pad_added
方法。我们在这里也将做同样的事情;然而,这个方法的内容将会有所不同。我们将在稍后讨论这一点。 -
在第 50-52 行,管道的视频处理部分被链接。
self.videoQueue
从decodebin
接收视频数据。它与之前讨论过的autoconvert
元素相连接。capsfilter
只允许video-xraw-yuv
数据进一步流式传输。capsfilter
连接到ffmpegcolorspace
元素,该元素将数据转换为不同的颜色空间。最后,数据被流式传输到videosink
,在这种情况下,是一个autovideosink
元素。这使您可以“查看”输入视频。管道的音频处理部分与早期章节中使用的方法非常相似。 -
现在我们将回顾
decodebin_pad_added
方法。1 def decodebin_pad_added(self, decodebin, pad): 2 compatible_pad = None 3 caps = pad.get_caps() 4 name = caps[0].get_name() 5 print "\n cap name is =%s"%name 6 if name[:5] == 'video': 7 compatible_pad = ( 8 self.videoQueue.get_compatible_pad(pad, caps) ) 9 elif name[:5] == 'audio': 10 compatible_pad = ( 11 self.audioQueue.get_compatible_pad(pad, caps) ) 12 13 if compatible_pad: 14 pad.link(compatible_pad)
-
此方法捕获当
decodebin
创建一个动态垫时发出的pad-added
信号。在早期章节中,我们只是将decodebin
垫与autoaudioconvert
元素上的兼容垫相连接。我们可以这样做,因为正在流式传输的caps
或媒体数据类型始终是音频数据。然而,在这里,媒体数据可以代表音频或视频数据。因此,当在decodebin
上创建一个动态垫时,我们必须检查这个pad
有什么caps
。caps
对象的get_name
方法的名称返回处理媒体数据类型。例如,名称可以是video/x-raw-rgb
的形式,当它是视频数据时,或者对于音频数据是audio/x-raw-int
。我们只检查前五个字符以查看它是否是视频或音频媒体类型。这是通过代码片段中的代码块 4-11 完成的。具有视频媒体类型的decodebin pad
与self.videoQueue
元素上的兼容垫相连接。同样,具有音频caps
的pad
与self.audioQueue
上的一个相连接。 -
从
PlayingVideo.py
中查看其余的代码。确保为变量self.inFileLocation
指定一个合适的视频文件路径,然后从命令提示符运行此程序,如下所示:$python PlayingVideo.py
- 这应该会打开一个 GUI 窗口,视频将在其中进行流式传输。音频输出将与播放的视频同步。
刚才发生了什么?
我们创建了一个命令行视频播放器实用程序。我们学习了如何创建一个可以播放同步音频和视频流的 GStreamer 管道。它解释了如何使用queue
元素在管道中处理音频和视频数据。在这个例子中,展示了如何使用 GStreamer 插件,如capsfilter
和ffmpegcolorspace
。本节中获得的知识将在本章后续部分的应用中体现。
尝试添加播放控制
在第六章中,我们学习了不同的技术来控制音频的播放。开发命令行实用程序,允许您暂停视频或直接跳转到视频轨道上的指定位置。
使用playbin
播放视频
上一节的目标是向您介绍处理输入视频流的基本方法。我们将在未来的讨论中以某种方式使用该方法。如果您只想进行视频播放,那么最简单的方法是通过playbin
插件来实现。只需将文件PlayingVideo.py
中的VideoPlayer.constructPipeline
方法替换为以下代码即可播放视频。在这里,self.player
是一个playbin
元素。playbin
的uri
属性被设置为输入视频文件路径。
上一节的目标是向您介绍处理输入视频流的基本方法。我们将在未来的讨论中以某种方式使用该方法。如果您只想进行视频播放,那么最简单的方法是通过playbin
插件来实现。只需将文件PlayingVideo.py
中的VideoPlayer.constructPipeline
方法替换为以下代码即可播放视频。在这里,self.player
是一个playbin
元素。playbin
的uri
属性被设置为输入视频文件路径。
def constructPipeline(self):
self.player = gst.element_factory_make("playbin")
self.player.set_property("uri",
"file:///" + self.inFileLocation)
视频格式转换
将视频保存为不同的文件格式是经常执行的任务之一,例如,将摄像机上录制的视频转换为 DVD 播放器可播放的格式。因此,让我们列出在管道中执行视频格式转换所需的元素。
-
一个
filesrc
元素用于流式传输视频文件,一个decodebin
用于解码编码的输入媒体数据。 -
接下来,管道中的音频处理元素,例如
audioconvert
,这是一个编码器,用于将原始音频数据编码成适当的音频格式以便写入。 -
管道中的视频处理元素,例如一个视频编码器元素,用于编码视频数据。
-
一个复用器或复用器,它将编码的音频和视频数据流合并到一个单独的通道中。
-
需要一个元素,根据媒体类型,可以将媒体数据发送到适当的处理单元。这是通过
queue
元素实现的,它们充当数据缓冲区。根据是音频还是视频数据,它将被流式传输到音频或视频处理元素。队列还需要将编码数据从音频管道流式传输到复用器。 -
最后,一个
filesink
元素用于保存转换后的视频文件(包含音频和视频轨道)。
行动时间 - 视频格式转换器
我们将创建一个视频转换实用程序,该实用程序将输入视频文件转换为用户指定的格式。您需要从 Packt 网站下载的文件是VideoConverter.py
。此文件可以从命令行运行,如下所示:
python VideoConverter.py [options]
其中,选项如下:
-
--input_path:
我们希望转换的视频文件的完整路径。输入文件的视频格式。格式应在一个支持的格式列表中。支持的输入格式有 MP4、OGG、AVI 和 MOV。 -
--output_path:
输出视频文件的完整路径。如果没有指定,它将在输入目录中创建一个名为OUTPUT_VIDEOS
的文件夹,并将文件以相同的名称保存在那里。 -
--output_format:
输出文件的音频格式。支持的输出格式有 OGG 和 MP4。提示
由于我们将使用
decodebin
元素进行输入媒体数据的解码;实际上,这个实用程序可以处理更广泛的输入格式。修改VideoPlayer.processArguments
中的代码或向字典VideoPlayer.supportedInputFormats
中添加更多格式。
-
如果尚未完成,请从 Packt 网站下载文件
VideoConverter.py
。 -
代码的整体结构如下:
import os, sys, time import thread import getopt, glob import gobject import pygst pygst.require("0.10") import gst class VideoConverter: def __init__(self): pass def constructPipeline(self): pass def connectSignals(self): pass def decodebin_pad_added(self, decodebin, pad): pass def processArgs(self): pass def printUsage(self): pass def printFinalStatus(self, starttime, endtime): pass def convert(self): pass def message_handler(self, bus, message): pass # Run the converter converter = VideoConverter() thread.start_new_thread(converter.convert, ()) gobject.threads_init() evt_loop = gobject.MainLoop()
evt_loop.run()
通过调用
thread.start_new_thread
创建一个新线程来运行应用程序。将VideoConverter.convert
方法发送到这个线程。这与之前讨论的VideoPlayer.play
方法类似。让我们回顾一下VideoConverter
类的一些关键方法。 -
__init__
方法包含初始化代码。它还调用方法处理命令行参数,然后构建管道。代码如下所示:1 def __init__(self): 2 # Initialize various attrs 3 self.inFileLocation = "" 4 self.outFileLocation = "" 5 self.inputFormat = "ogg" 6 self.outputFormat = "" 7 self.error_message = "" 8 # Create dictionary objects for 9 # Audio / Video encoders for supported 10 # file format 11 self.audioEncoders = {"mp4":"lame", 12 "ogg": "vorbisenc"} 13 14 self.videoEncoders={"mp4":"ffenc_mpeg4", 15 "ogg": "theoraenc"} 16 17 self.muxers = {"mp4":"ffmux_mp4", 18 "ogg":"oggmux" } 19 20 self.supportedOutputFormats = self.audioEncoders.keys() 21 22 self.supportedInputFormats = ("ogg", "mp4", 23 "avi", "mov") 24 25 self.pipeline = None 26 self.is_playing = False 27 28 self.processArgs() 29 self.constructPipeline() 30 self.connectSignals()
-
要处理视频文件,我们需要音频和视频编码器。这个实用程序将只支持转换为 MP4 和 OGG 文件格式。通过添加适当的编码器和复用器插件,可以轻松扩展以包括更多格式。self.audioEncoders 和 self.videoEncoders 字典对象的值分别指定用于流式传输音频和视频数据的编码器。因此,为了将视频数据存储为 MP4 格式,我们使用 ffenc_mp4 编码器。代码片段中显示的编码器应该是您计算机上 GStreamer 安装的一部分。如果不是,请访问 GStreamer 网站,了解如何安装这些插件。self.muxers 字典的值表示在特定输出格式中使用的复用器。
-
constructPipeline
方法执行主要的转换工作。它构建所需的管道,然后在convert
方法中将管道设置为播放状态。1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline("pipeline") 3 4 self.filesrc = gst.element_factory_make("filesrc") 5 self.filesrc.set_property("location", 6 self.inFileLocation) 7 8 self.filesink = gst.element_factory_make("filesink") 9 self.filesink.set_property("location", 10 self.outFileLocation) 11 12 self.decodebin = gst.element_factory_make("decodebin") 13 self.audioconvert = gst.element_factory_make( 14 "audioconvert") 15 16 audio_encoder = self.audioEncoders[self.outputFormat] 17 muxer_str = self.muxers[self.outputFormat] 18 video_encoder = self.videoEncoders[self.outputFormat] 19 20 self.audio_encoder= gst.element_factory_make( 21 audio_encoder) 22 self.muxer = gst.element_factory_make(muxer_str) 23 self.video_encoder = gst.element_factory_make( 24 video_encoder) 25 26 self.videoQueue = gst.element_factory_make("queue") 27 self.audioQueue = gst.element_factory_make("queue") 28 self.queue3 = gst.element_factory_make("queue") 29 30 self.pipeline.add( self.filesrc, 31 self.decodebin, 32 self.video_encoder, 33 self.muxer, 34 self.videoQueue, 35 self.audioQueue, 36 self.queue3, 37 self.audioconvert, 38 self.audio_encoder, 39 self.filesink) 40 41 gst.element_link_many(self.filesrc, self.decodebin) 42 43 gst.element_link_many(self.videoQueue, 44 self.video_encoder, self.muxer, self.filesink) 45 46 gst.element_link_many(self.audioQueue,self.audioconvert, 47 self.audio_encoder, self.queue3, 48 self.muxer)
-
在前面的部分,我们介绍了之前管道中使用的几个元素。第 43 到 48 行的代码为音频和视频处理元素建立了连接。在第 44 行,复用器 self.muxer 与视频编码器元素连接。它将流的不同部分(在这种情况下,视频和音频数据)放入一个单独的文件中。音频编码器 self.audio_encoder 输出的数据通过队列元素 self.queue3 流向复用器。然后,来自 self.muxer 的复用数据流到 self.filesink。
-
快速回顾一下
VideoConverter.convert
方法。1 def convert(self): 2 # Record time before beginning Video conversion 3 starttime = time.clock() 4 5 print "\n Converting Video file.." 6 print "\n Input File: %s, Conversion STARTED..." % 7 self.inFileLocation 8 9 self.is_playing = True 10 self.pipeline.set_state(gst.STATE_PLAYING) 11 while self.is_playing: 12 time.sleep(1) 13 14 if self.error_message: 15 print "\n Input File: %s, ERROR OCCURED." % 16 self.inFileLocation 17 print self.error_message 18 else: 19 print "\n Input File: %s, Conversion COMPLETE " % 20 self.inFileLocation 21 22 endtime = time.clock() 23 self.printFinalStatus(starttime, endtime) 24 evt_loop.quit()
-
在第 10 行,之前构建的 GStreamer 管道被设置为播放。当转换完成后,它将生成流的结束(EOS)消息。在
self.message_handler
方法中修改了self.is_playing
标志。第 11 行的 while 循环执行,直到 EOS 消息在总线上发布或发生某些错误。最后,在第 24 行,主执行循环被终止。小贴士
在第 3 行,我们调用了
time.clock()
。这实际上给出了该进程所消耗的 CPU 时间。 -
其他方法,如
VideoConverter.decodebin_pad_added
与播放视频部分中开发的方法相同。请回顾VideoConverter.py
文件中的剩余方法,然后通过指定适当的命令行参数运行此实用工具。以下截图显示了从控制台窗口运行程序时的示例输出消息。- 这是视频转换实用工具从控制台运行的示例运行。
刚才发生了什么?
我们创建了一个另一个有用的实用工具,可以将视频文件从一种格式转换为另一种格式。我们学习了如何将音频和视频数据编码成所需的输出格式,然后使用复用器将这些两个数据流放入一个单独的文件中。
尝试批量转换视频文件
在前面的章节中开发的视频转换器一次只能转换一个视频文件。你能将其制作成一个批量处理实用工具吗?请参考处理音频章节中开发的音频转换器代码。整体结构将非常相似。然而,由于使用了队列元素,转换多个视频文件可能会遇到挑战。例如,当第一个文件转换完成时,当我们开始转换其他文件时,队列中的数据可能不会被刷新。一种粗略的解决方法是在每个音频文件上重建整个管道并连接信号。然而,将会有一个更有效的方法来做这件事。想想看!
视频操作和效果
假设你有一个需要以调整后的默认亮度级别保存的视频文件。或者,你可能想保存另一个具有不同宽高比的视频。在本节中,我们将学习一些视频的基本且最常执行的操作。我们将使用 Python 和 GStreamer 开发代码,以执行诸如调整视频大小或调整其对比度级别等任务。
调整大小
可以通过一个元素上 pad 的能力(caps)来描述通过该元素的数据。如果一个 decodebin 元素正在解码视频数据,其动态 pad 的能力将被描述为,例如,video/x-raw-yuv
。使用 GStreamer 多媒体框架调整视频大小可以通过使用具有指定 width
和 height
参数的 capsfilter
元素来实现。如前所述,capsfilter
元素限制了两个元素之间可以传输的媒体数据类型。例如,由字符串描述的 cap
对象,video/x-raw-yuv, width=800, height=600
将将视频宽度设置为 800 像素,高度设置为 600 像素。
行动时间 - 调整视频大小
现在我们将看到如何使用由 GStreamer cap
对象描述的 width
和 height
参数来调整流视频的大小。
-
从 Packt 网站下载文件
VideoManipulations.py
。整体类设计与在 播放视频 部分研究的设计相同。 -
self.constructAudioPipeline()
和self.constructVideoPipeline()
方法分别定义和链接与主管道对象self.player
的音频和视频部分相关的元素。由于我们已经在前面章节中讨论了大多数音频/视频处理元素,我们在这里只回顾constructVideoPipeline
方法。1 def constructVideoPipeline(self): 2 # Autoconvert element for video processing 3 self.autoconvert = gst.element_factory_make( 4 "autoconvert") 5 self.videosink = gst.element_factory_make( 6 "autovideosink") 7 8 # Set the capsfilter 9 if self.video_width and self.video_height: 10 videocap = gst.Caps( 11 "video/x-raw-yuv," "width=%d, height=%d"% 12 (self.video_width,self.video_height)) 13 else: 14 videocap = gst.Caps("video/x-raw-yuv") 15 16 self.capsFilter = gst.element_factory_make( 17 "capsfilter") 18 self.capsFilter.set_property("caps", videocap) 19 20 # Converts the video from one colorspace to another 21 self.colorSpace = gst.element_factory_make( 22 "ffmpegcolorspace") 23 24 self.videoQueue = gst.element_factory_make("queue") 25 26 self.player.add(self.videoQueue, 27 self.autoconvert, 28 self.capsFilter, 29 self.colorSpace, 30 self.videosink) 31 32 gst.element_link_many(self.videoQueue, 33 self.autoconvert, 34 self.capsFilter, 35 self.colorSpace, 36 self.videosink)
-
capsfilter 元素在第 16 行定义。它是一个限制将通过它的媒体数据类型的过滤器。videocap 是在第 10 行创建的 GStreamer cap 对象。此 cap 指定了流视频的宽度和高度参数。它被设置为 capsfilter 的属性,self.capsFilter。它指示过滤器只流式传输由 videocap 对象指定的宽度和高度的视频-xraw-yuv 数据。
小贴士
在源文件中,你会看到在管道中链接了一个额外的元素
self.videobox
。在上面的代码片段中省略了它。我们将在下一节中看到这个元素的作用。 -
代码的其余部分很简单。我们已经在之前的讨论中介绍了类似的方法。通过回顾文件
VideoManipulations.py
来开发代码的其余部分。确保为变量self.inFileLocation
指定一个合适的视频文件路径。然后从命令提示符运行此程序,如下所示:$python VideoManipulations.py
- 这应该会打开一个 GUI 窗口,视频将在其中流式传输。此窗口的默认大小将由代码中指定的
self.video_width
和self.video_height
参数控制。
- 这应该会打开一个 GUI 窗口,视频将在其中流式传输。此窗口的默认大小将由代码中指定的
刚才发生了什么?
我们之前开发的命令行视频播放器在刚刚开发的示例中得到了扩展。我们使用了 capsfilter
插件来指定流视频的 width
和 height
参数,从而调整视频大小。
裁剪
假设你有一个视频在底部有较大的“空白空间”或者你想要裁剪掉的一侧的不需要的部分。videobox
GStreamer 插件可以方便地从左侧、右侧、顶部或底部裁剪视频。
是时候行动起来 - 裁剪视频
让我们给之前开发的命令行视频播放器添加另一个视频处理功能。
-
我们需要的文件是前面章节中使用的文件,即
VideoManipulations.py
。 -
再次强调,我们将关注
VideoPlayer
类中的constructVideoPipeline
方法。以下代码片段来自此方法。此方法中的其余代码与前面章节中审查的代码相同。1 self.videobox = gst.element_factory_make("videobox") 2 self.videobox.set_property("bottom", self.crop_bottom ) 3 self.videobox.set_property("top", self.crop_top ) 4 self.videobox.set_property("left", self.crop_left ) 5 self.videobox.set_property("right", self.crop_right ) 6 7 self.player.add(self.videoQueue, 8 self.autoconvert, 9 self.videobox, 10 self.capsFilter, 11 self.colorSpace, 12 self.videosink) 13 14 gst.element_link_many(self.videoQueue, 15 self.autoconvert, 16 self.videobox, 17 self.capsFilter, 18 self.colorSpace, 19 self.videosink)
-
代码是自解释的。
videobox
元素在第 1 行创建。用于裁剪流媒体视频的videobox
属性在第 2-5 行设置。它从autoconvert
元素接收媒体数据。videobox
的源端连接到capsfilter
的接收端或直接连接到ffmpegcolorspace
元素。 -
通过审查文件
VideoManipulations.py
来开发剩余的代码。确保为变量self.inFileLocation
指定一个合适的视频文件路径。然后从命令提示符运行此程序,如下所示:$python VideoManipulations.py
这应该会打开一个 GUI 窗口,视频将通过该窗口进行流式传输。视频将从左侧、右侧、底部和顶部通过
self.crop_left
、self.crop_right
、self.crop_bottom
和self.crop_top
参数分别进行裁剪。
刚才发生了什么?
我们进一步扩展了视频播放器应用程序,添加了一个可以裁剪视频帧的 GStreamer 元素。我们使用了videobox
插件来完成这个任务。
尝试给视频添加边框
-
在上一节中,我们使用了
videobox
元素从视频两侧进行裁剪。同一个插件也可以用来给视频添加边框。如果你为videobox
属性设置负值,例如底部、顶部、左侧和右侧,那么不是裁剪视频,而是在视频周围添加黑色边框。将self.crop_left
等参数设置为负值来查看这个效果。 -
可以通过使用
videocrop
插件来完成视频裁剪。它与videobox
插件类似,但不支持给视频帧添加边框。修改代码并使用此插件来裁剪视频。
调整亮度和对比度
我们在第三章增强图像中看到了如何调整亮度和对比度级别。如果您有一个在光线条件不佳的情况下录制的家庭视频,您可能会调整其亮度级别。对比度级别强调每个视频帧的颜色和亮度级别之间的差异。videobalance
插件可用于调整亮度、对比度、色调和饱和度。以下代码片段创建此元素并设置亮度和对比度属性。亮度属性可以接受-1
到1
范围内的值,默认(原始)亮度级别为0
。对比度可以具有0
到2
范围内的值,默认值为1
。
self.videobalance = gst.element_factory_make("videobalance")
self.videobalance.set_property("brightness", 0.5)
self.videobalance.set_property("contrast", 0.5)
然后,videobalance
在 GStreamer 管道中链接如下:
gst.element_link_many(self.videoQueue,
self.autoconvert,
self.videobalance,
self.capsFilter,
self.colorSpace,
self.videosink)
查看文件VideoEffects.py
中的其余代码。
创建灰度视频
通过调整videobalance
插件的饱和度属性,可以将视频渲染为灰度。饱和度可以具有0
到2
范围内的值。默认值是1
。将此值设置为0.0
将图像转换为灰度。代码如下所示:
self.videobalance.set_property("saturation", 0.0)
您可以参考文件VideoEffects.py
,它展示了如何使用videobalance
插件调整饱和度和其他在前面章节中讨论的参数。
在视频流中添加文本和时间
在处理视频时,将文本字符串或字幕轨道添加到视频的能力是另一个需要的特性。GStreamer 插件textoverlay
允许在视频流上叠加信息性文本字符串,例如文件名。其他有用的插件,如timeoverlay
和clockoverlay
,提供了一种将视频缓冲区时间戳和 CPU 时钟时间放在流视频顶部的方法。
行动时间 - 在视频轨道上叠加文本
让我们看看如何在视频轨道上添加文本字符串。我们将编写一个简单的实用工具,其代码结构本质上与我们之前在播放视频部分开发的结构相同。此工具还将添加缓冲区时间戳和当前 CPU 时钟时间到视频顶部。对于本节,重要的是您在 GStreamer 安装中拥有textoverlay
、timeoverlay
和clockoverlay
插件。否则,您需要安装这些插件或使用其他插件,如可用的cairotextoverlay
。
-
从 Packt 网站下载文件
VideoTextOverlay.py
。 -
类
VideoPlayer
的constructVideoPipeline
方法在以下代码片段中展示:1 def constructVideoPipeline(self): 2 # Autoconvert element for video processing 3 self.autoconvert = gst.element_factory_make( 4 "autoconvert") 5 self.videosink = gst.element_factory_make( 6 "autovideosink") 7 8 # Set the capsfilter 9 videocap = gst.Caps("video/x-raw-yuv") 10 self.capsFilter = gst.element_factory_make( 11 "capsfilter") 12 self.capsFilter.set_property("caps", videocap) 13 14 # Converts the video from one colorspace to another 15 self.colorSpace = gst.element_factory_make( 16 "ffmpegcolorspace") 17 18 self.videoQueue = gst.element_factory_make("queue") 19 20 self.textOverlay = gst.element_factory_make( 21 "textoverlay") 22 self.textOverlay.set_property("text", "hello") 23 self.textOverlay.set_property("shaded-background", 24 True) 25 26 self.timeOverlay = gst.element_factory_make( 27 "timeoverlay") 28 self.timeOverlay.set_property("valign", "top") 29 self.timeOverlay.set_property("shaded-background", 30 True) 31 32 self.clockOverlay = gst.element_factory_make( 33 "clockoverlay") 34 self.clockOverlay.set_property("valign", "bottom") 35 self.clockOverlay.set_property("halign", "right") 36 self.clockOverlay.set_property("shaded-background", 37 True) 38 39 self.player.add(self.videoQueue, 40 self.autoconvert, 41 self.textOverlay, 42 self.timeOverlay, 43 self.clockOverlay, 44 self.capsFilter, 45 self.colorSpace, 46 self.videosink) 47 48 gst.element_link_many(self.videoQueue, 49 self.autoconvert, 50 self.capsFilter, 51 self.textOverlay, 52 self.timeOverlay, 53 self.clockOverlay, 54 self.colorSpace, 55 self.videosink)
-
如你所见,用于叠加文本、时间或时钟的元素可以像其他元素一样简单地添加并连接到 GStreamer 流程中。现在让我们讨论这些元素的各个属性。在第 20-23 行,定义了
textoverlay
元素。文本属性设置了在流式视频上出现的文本字符串。为了确保文本字符串在视频中清晰可见,我们为此文本添加了背景对比。这是通过在第 23 行将带阴影的背景属性设置为 True 来实现的。此插件的其他属性有助于在视频中固定文本位置。运行gst-inspect-0.10
在textoverlay
插件上,以查看这些属性。 -
接下来,在 25-36 行,定义了时间和时钟叠加元素。它们的属性与
textoverlay
插件中可用的属性类似。时钟时间将出现在流式视频的左下角。这是通过设置valign
和halign
属性来实现的。这三个元素随后在 GStreamer 流程中连接。它们连接的内部顺序并不重要。 -
通过查阅文件
VideoTextOverlay.py
开发剩余的代码。确保为变量self.inFileLocation
指定适当的视频文件路径。然后从命令提示符运行此程序:$python VideoTextOverlay.py
- 这应该会打开一个 GUI 窗口,视频将在其中流式传输。视频将显示文本字符串 "hello",以及运行时间和时钟时间。这可以通过以下视频帧的快照来展示。
截图展示了包含文本、时间和时钟叠加的视频帧。
刚才发生了什么?
我们学习了如何在 GStreamer 流程中使用 textoverlay
、timeoverlay
和 clockoverlay
等元素,分别在上面的视频缓冲区上添加文本字符串、时间戳和时钟。textoverlay
元素还可以进一步用来给视频文件添加字幕轨道。
尝试英雄添加视频轨道字幕!
将我们刚刚开发的代码扩展以向视频文件添加字幕轨道。要添加字幕轨道,你需要 subparse
插件。请注意,此插件在 GStreamer-WinBuilds 二进制文件安装的 GStreamer Windows 版本中默认不可用。因此,Windows 用户可能需要单独安装此插件。查阅 subparse
插件参考以了解如何完成此任务。以下代码片段展示了如何创建 subparse
元素。
self.subtitlesrc = gst.element_factory_make("filesrc")
self.subtitlesrc.set_property("location",
"/path/to/subtitles/file")
self.subparse = gst.element_factory_make("subparse")
分离音频和视频轨道
有时候,你可能想要分离音频和视频轨道。想象一下,你有一系列你最喜欢的视频歌曲。你将进行长途驾驶,而你车上的老式 CD 播放器只能播放特定格式的音频文件。让我们编写一个可以将音频从视频文件中分离出来的实用程序!
行动时间 - 音频和视频轨道
我们将开发代码,该代码以视频文件作为输入,然后创建两个输出文件,一个仅包含原始文件的音频轨道,另一个包含视频部分。
-
从 Packt 网站下载文件
SeparatingAudio.py
。类AudioSeparator
的结构与在播放视频部分看到的结构相似。我们将回顾这个类的两个方法,constructPipeline
和decodebin_pad_added
。 -
让我们从
constructPipeline
方法中的代码开始。1 def constructPipeline(self): 2 # Create the pipeline instance 3 self.player = gst.Pipeline() 4 5 # Define pipeline elements 6 self.filesrc = gst.element_factory_make("filesrc") 7 8 self.filesrc.set_property("location", 9 self.inFileLocation) 10 11 self.decodebin = gst.element_factory_make("decodebin") 12 13 self.autoconvert = gst.element_factory_make( 14 "autoconvert") 15 16 self.audioconvert = gst.element_factory_make( 17 "audioconvert") 18 19 self.audio_encoder = gst.element_factory_make("lame") 20 21 self.audiosink = gst.element_factory_make("filesink") 22 self.audiosink.set_property("location", 23 self.audioOutLocation) 24 25 self.video_encoder = gst.element_factory_make(" 26 ffenc_mpeg4") 27 self.muxer = gst.element_factory_make("ffmux_mp4") 28 29 self.videosink = gst.element_factory_make("filesink") 30 self.videosink.set_property("location", 31 self.videoOutLocation) 32 33 self.videoQueue = gst.element_factory_make("queue") 34 self.audioQueue = gst.element_factory_make("queue") 35 # Add elements to the pipeline 36 self.player.add(self.filesrc, 37 self.decodebin, 38 self.videoQueue, 39 self.autoconvert, 40 self.video_encoder, 41 self.muxer, 42 self.videosink, 43 self.audioQueue, 44 self.audioconvert, 45 self.audio_encoder, 46 self.audiosink) 47 49 # Link elements in the pipeline. 50 gst.element_link_many(self.filesrc, self.decodebin) 51 52 gst.element_link_many(self. videoQueue, 53 self.autoconvert, 54 self.video_encoder, 55 self.muxer, 56 self.videosink) 57 58 gst.element_link_many(self.audioQueue, 59 self.audioconvert, 60 self.audio_encoder, 61 self.audiosink)
-
我们已经在各种示例中使用了所有必要的元素。关键是正确地将它们连接起来。self.audiosink 和 self.videoSink 元素是 filesink 元素,分别定义音频和视频输出文件的位置。请注意,在这个例子中,我们将输出音频保存为 MP3 格式,视频保存为 MP4 格式。因此,我们使用 lame 编码器进行音频文件,而使用编码器 ffenc_mpeg4 和 multiplexer ffmux_mp4 进行视频输出。请注意,我们没有使用 ffmpegcolorspace 元素。它只是帮助为视频 sink(在这种情况下,输出视频文件)获取适当的颜色空间格式。在这种情况下,它是不需要的。如果输出文件没有适当地显示视频帧,您始终可以在管道中将其链接。由 self.decodebin 解码的媒体数据需要通过队列元素作为数据缓冲区流式传输到管道的音频和视频部分。
-
decodebin
创建动态 pad 以解码输入音频和视频数据。decodebin_pad_added
方法需要检查decodebin
动态 pad 上的功能(caps)。1 def decodebin_pad_added(self, decodebin, pad): 2 compatible_pad = None 3 caps = pad.get_caps() 4 name = caps[0].get_name() 5 print "\n cap name is = ", name 6 if name[:5] == 'video': 7 compatible_pad = ( 8 self.videoQueue.get_compatible_pad(pad, caps) ) 9 elif name[:5] == 'audio': 10 compatible_pad = ( 11 self. audioQueue.get_compatible_pad(pad,caps) ) 12 13 if compatible_pad: 14 pad.link(compatible_pad)
-
这个检查是通过代码块 6-12 完成的。如果功能表明它是音频数据,则
decodebin pad
连接到self.audioQueue
上的兼容 pad。同样,当caps
表明它是视频数据时,在self.videoQueue
和self.decodebin
之间创建一个链接。 -
您可以处理文件
SeparatingAudio.py
中剩余的代码。将self.inFileLocation
、self.audioOutLocation
和self.videoOutLocation
表示的路径替换为您计算机上的适当路径,然后以以下方式运行此实用程序:$python SeparatingAudio.py
- 这应该会创建两个输出文件,一个包含输入文件中仅有的音频轨道的 MP3 格式的文件,另一个包含视频轨道的 MP4 格式的文件。
刚才发生了什么?
我们构建了一个 GStreamer 管道,从输入视频文件中分离音频和视频轨道。我们之前在多个示例中学习到的几个 GStreamer 元素被用于开发此实用程序。我们还学习了如何使用decodebin
动态 pad 上的功能(caps)在decodebin
和queue
元素之间建立适当的链接。
混合音频和视频轨道
假设你用摄像机记录了你朋友的婚礼。对于一些特定的时刻,你希望静音所有其他声音,并用背景音乐替换它们。为了完成这个任务,首先你需要将视频轨道(不含音频)保存为单独的文件。我们刚刚学习了这项技术。然后你需要将这个视频轨道与包含你希望播放的背景音乐的音频轨道合并。现在让我们学习如何将音频和视频轨道混合成一个单独的视频文件。
行动时间 - 音频/视频轨道混合器
我们将开发一个程序,通过混合音频和视频轨道来生成视频输出文件。思考一下,与之前开发的音频/视频轨道分离实用程序相比,我们需要整合哪些变化。在那个应用程序中,需要两个filesink
元素,因为创建了两个输出文件。在这里,我们需要相反的。我们需要两个包含音频和视频数据的filesrc
元素,以及一个包含音频和视频轨道的单个filesink
元素。
-
从 Packt 网站下载文件
AudioVideoMixing.py
。我们将回顾AudioVideoMixer
类的一些重要方法。 -
与往常一样,
constructPipeline
方法构建了包含所有必要元素的 GStreamer 管道。1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline("pipeline") 3 4 self.audiosrc = gst.element_factory_make("filesrc") 5 self.audiosrc.set_property("location", 6 self.audioInLocation) 7 8 self.videosrc = gst.element_factory_make("filesrc") 9 self.videosrc.set_property("location", 10 self.videoInLocation) 11 12 self.filesink = gst.element_factory_make("filesink") 13 self.filesink.set_property("location", 14 self.outFileLocation) 15 16 self.audio_decodebin = gst.element_factory_make( 17 "decodebin") 18 self.video_decodebin= gst.element_factory_make( 19 "decodebin") 20 21 self.audioconvert = gst.element_factory_make( 22 "audioconvert") 23 self.audio_encoder= gst.element_factory_make("lame") 24 25 self.video_encoder = ( 26 gst.element_factory_make("ffenc_mpeg4") ) 27 self.muxer = gst.element_factory_make("ffmux_mp4") 28 self.queue = gst.element_factory_make("queue") 29 audio-video track mixerdeveloping30 31 videocap = gst.Caps("video/x-raw-yuv") 32 self.capsFilter = gst.element_factory_make( 33 "capsfilter") 34 self.capsFilter.set_property("caps", videocap) 35 # Converts the video from one colorspace to another 36 self.colorSpace = gst.element_factory_make( 37 "ffmpegcolorspace") 38 39 self.pipeline.add( self.videosrc, 40 self. video_decodebin, 41 self.capsFilter, 42 self.colorSpace, 43 self.video_encoder, 44 self.muxer, 45 self.filesink) 46 47 self.pipeline.add(self.audiosrc, 48 self.audio_decodebin, 49 self.audioconvert, 50 self.audio_encoder, 51 self.queue) 52 53 # Link audio elements 54 gst.element_link_many(self.audiosrc, 55 self.audio_decodebin) 56 gst.element_link_many( self.audioconvert, 57 self.audio_encoder, 58 self.queue, self.muxer) 59 #Link video elements 60 gst.element_link_many(self.videosrc, 61 self.video_decodebin) 62 gst.element_link_many(self.capsFilter, 63 self.colorSpace, 64 self.video_encoder, 65 self.muxer, 66 self.filesink)
-
音频和视频文件源分别由元素
self.audiosrc
和self.videosrc
定义。它们连接到两个单独的decodebins
(见第 54 行和第 59 行)。self.audio_decodebin
和self.video_decodebin
的pad-added
信号在connectSignals
方法中连接。音频和视频数据随后分别通过音频和视频处理元素的链。数据由各自的编码器进行编码。编码后的数据流被合并,使得输出视频文件包含音频和视频轨道。这项工作由多路复用器 self.muxer 完成。它与视频编码器元素相连接。音频数据通过队列元素(第 57 行)流到多路复用器。数据被‘多路复用’并输入到 filesink 元素 self.filesink。请注意,ffmpegcolorspace 元素和 capsfilter,self.capsfiter 实际上并不是必需的。在这种情况下,输出视频应该具有正确的显示格式。你可以尝试移除这两个元素来运行此应用程序,看看是否会有任何区别。
-
在
decodebin_pad_added
方法中,我们在链接动态垫片之前会检查一些额外的事项。1 def decodebin_pad_added(self, decodebin, pad): 2 compatible_pad = None 3 caps = pad.get_caps() 4 name = caps[0].get_name() 5 print "\n cap name is =%s"%name 6 if ( name[:5] == 'video' and 7 (decodebin is self.video_decodebin) ): 8 compatible_pad = ( 9 self.capsFilter.get_compatible_pad(pad, caps) ) 10 elif ( name[:5] == 'audio' and 11 (decodebin is self.audio_decodebin) ): 12 compatible_pad = ( 13 self.audioconvert.get_compatible_pad(pad, caps) ) 14 15 if compatible_pad: 16 pad.link(compatible_pad)
-
可能会发生每个输入文件都包含音频和视频数据的情况。例如,self.audiosrc 和 self.videosrc 代表不同的视频文件,这些文件都包含音频和视频数据。self.audiosrc 文件连接到 self.audio_decodebin。因此,我们应该确保当 self.audio_decodebin 生成垫片添加信号时,只有当其 caps 包含音频数据时才将其动态垫片连接。按照类似的方式,只有当 caps 代表视频数据时,self.video_decodebin 上的垫片才被连接。这是通过代码块 6 13 确保的。
-
通过审查文件
AudioVideoMixer.py
来开发其余的代码。将表示为self.audioInLocation
、self.videoInLocation
和self.outFileLocation
的路径替换为您的计算机上的适当路径,然后运行此实用程序:$python AudioVideoMixer.py
- 这应该会创建一个包含指定输入文件中的音频和视频轨道的 MP4 格式的输出视频文件!
刚才发生了什么?
我们开发了一个工具,它可以将输入音频和视频轨道混合,并将它们存储到一个单独的输出文件中。为了完成这个任务,我们使用了在视频转换工具中使用的几乎所有音频/视频处理元素。我们学习了如何根据decodebin
的caps
表示的流数据链接其动态垫片。使用了多路复用插件ffmux_mp4
元素来将音频和视频数据组合在一起。
将视频帧保存为图像
想象一下,你有一个野生动物视频,它记录了一个非常特别的时刻。你想要保存这张图片。让我们学习如何使用 GStreamer 框架实现这一点。
行动时间 - 将视频帧保存为图像
此文件可以从命令行运行:
python ImagesFromVideo.py [options]
这里[options]
是:
-
--input_file:
需要从其中捕获一个或多个帧并保存为图像的输入视频文件的完整路径。 -
--start_time:
视频轨道上的位置(以秒为单位)。这将是捕获一个或多个视频帧作为静态图像的起始位置。第一张快照总是在start_time
。 -
--duration:
从start_time
开始的视频轨道的持续时间(以秒为单位)。从start_time
开始将捕获N
个帧。 -
--num_of_captures:
从start_time
(包括它)到end_time= start_time + duration
(但不包括end_time
处的静态图像)需要捕获的总帧数。
-
如果尚未完成,请从 Packt 网站下载文件
ImagesFromVideo.py
。以下是保存视频帧的代码概述。import os, sys, time import thread import gobject import pygst pygst.require("0.10") import gst from optparse import OptionParser class ImageCapture: def __init__(self): pass def connectSignals(self): pass def constructPipeline(self): pass def gnonlin_pad_added(self, gnonlin_elem, pad): pass def captureImage(self): pass def capture_single_image(self, media_start_time): pass def message_handler(self, bus, message): pass def printUsage(self): pass def printFinalStatus(self, starttime, endtime): pass # Run the program imgCapture = ImageCapture() thread.start_new_thread(imgCapture.captureImage, ()) gobject.threads_init() evt_loop = gobject.MainLoop() evt_loop.run()
-
程序执行从调用
captureImage
方法开始。在音频处理章节中讨论的 gnlfilesource 元素将在这里用于在流视频上查找特定帧。capture_single_image
执行将单个帧保存为图像的主要工作。我们将在下面讨论这些方法中的一些。 -
让我们从
constructPipeline
方法开始,该方法定义并链接了捕获视频帧所需的各种元素。1 def constructPipeline(self): 2 self.pipeline = gst.Pipeline() 3 self.gnlfilesrc = ( 4 gst.element_factory_make("gnlfilesource") ) 5 6 self.gnlfilesrc.set_property("uri", 7 "file:///" + self.inFileLocation) 8 self.colorSpace = gst.element_factory_make( 9 "ffmpegcolorspace") 10 11 self.encoder= gst.element_factory_make("ffenc_png") 12 13 self.filesink = gst.element_factory_make("filesink") 14 15 self.pipeline.add(self.gnlfilesrc, 16 self.colorSpace, 17 self.encoder, 18 self.filesink) 19 20 gst.element_link_many(self.colorSpace, 21 self.encoder, 22 self.filesink)
-
我们已经知道如何创建和连接
gnlfilesource
元素(称为self.gnlfilesrc
)。在迄今为止的示例中,GStreamer 管道中使用的编码器元素将流媒体数据编码为音频或视频格式。在第 11 行,我们定义了一个新的编码器元素,它允许将流视频中的特定帧保存为图像。在这个例子中,我们使用编码器ffenc_png
将视频帧保存为 PNG 格式的图像文件。此插件应默认在您的 GStreamer 安装中可用。如果不适用,您将需要安装它。有类似的插件可用于保存不同文件格式的图像。例如,使用jpegenc
插件将其保存为 JPEG 图像等。self.gnlfilesrc
使用动态垫,该垫连接到之前讨论的ffmpegcolorspace
的适当垫。self.colorspace
元素转换颜色空间,然后此视频数据由ffenc_png
元素编码。self.filesink
定义了保存特定视频帧为图像的位置。 -
captureImage
是主要的控制方法。整体结构非常类似于第五章(Chapter 5)中音频转换实用工具的开发者,Working with Audios。此方法运行程序参数指定的顶层控制循环以捕获帧。1 def captureImage(self): 2 # Record start time 3 starttime = time.clock() 4 5 # Note: all times are in nano-seconds 6 media_end = self.media_start_time + self.media_duration 7 start = self.media_start_time 8 while start < media_end: 9 self.capture_single_image(start) 10 start += self.deltaTime 11 12 endtime = time.clock() 13 self.printFinalStatus(starttime, endtime) 14 evt_loop.quit()
-
capture_single_image
方法负责保存这些帧中的每一个。self.media_start_time
定义了从哪个位置开始,这个实用工具应该开始保存视频帧作为图像。这作为命令行参数指定给这个实用工具。media_end
变量定义了视频轨道上程序应该“停止”捕获静态图像(视频帧)的位置。self.media_start_time
是第一个视频帧将被保存为图像的时间。这是分配给局部变量start
的初始值,然后在循环中递增。当前行号 8-10 的
while
循环为我们要保存为图像的每个视频帧调用capture_single_image
方法。self.deltaTime
变量定义了捕获视频帧的增量时间步。其值在构造函数中如下确定:self.deltaTime = int(self.media_duration / self.numberOfCaptures)
-
在这里,
self.numberOfCaptures
被指定为一个参数。如果没有指定此参数,它将只保存一个帧作为图像。它用于递增变量start
。 -
现在,让我们看看
ImageCapture.capture_single_image
做了什么。正如其名所示,其任务是保存与流视频中media_start_time
对应的单个图像。1 def capture_single_image(self, media_start_time): 2 # Set media_duration as int as 3 # gnlfilesrc takes it as integer argument 4 media_duration = int(0.01*gst.SECOND) 5 6 self.gnlfilesrc.set_property("media-start", 7 media_start_time) 8 self.gnlfilesrc.set_property("media-duration", 9 media_duration) 10 11 # time stamp in seconds, added to the name of the 12 # image to be saved. 13 time_stamp = float(media_start_time)/gst.SECOND 14 outFile = os.path.join(self.outputDirPath, 15 "still_%.4f.png"%time_stamp ) 16 print "\n outfile = ", outFile 17 self.filesink.set_property("location", outFile) 18 self.is_playing = True 19 self.pipeline.set_state(gst.STATE_PLAYING) 20 while self.is_playing: 21 time.sleep(1)
-
媒体持续时间被设置为非常小的值(0.01 秒),仅足够在 media_start_time 处播放视频帧。media_start_time 和 media_duration 用于设置 self.gnlfilesrc 表示的 gnlfilesource 的属性。在第 14 行指定了输出图像文件的位置。请注意,文件名附加了一个时间戳,它代表了在流媒体视频时间线上这个快照被捕获的时间。在设置必要的参数后,在第 20 行“启动”管道,并将播放直到在总线上发布 EOS 消息,即完成输出 PNG 文件的写入。
回顾文件 ImagesFromVideo.py 中剩余的方法,然后通过指定适当的命令行参数运行此实用程序。以下截图显示了从控制台窗口运行程序时的示例输出消息。
刚才发生了什么?
我们开发了一个非常有用的应用程序,可以将流媒体视频中的指定帧保存为图像文件。为了实现这一点,我们重新使用了之前研究过的几个 GStreamer 元素/插件。例如,使用gnlfilesource, ffmpegcolorspace
等元素来构建 GStreamer 管道。此外,我们还使用了一个图像编码器将视频数据保存为图像格式。
摘要
在之前的音频处理章节中,我们学习了 GStreamer API 的基础知识。
在本章中,我们进一步开发了一些有用的视频处理实用程序,使用 Python 和 GStreamer。为了完成这个任务,我们学习了几个新的 GStreamer 插件,这些插件是处理视频所必需的。
具体来说,我们涵盖了:
-
处理音频和视频的管道:我们学习了如何构建一个 GStreamer 管道,它可以处理输入视频文件中的音频和视频轨道。这被用来“播放”视频文件,也是本章中开发的一些视频处理工具的基本管道。
-
分离音频/视频:通过示例,我们学习了如何将视频文件的音频/视频轨道保存到两个不同的文件中。
-
音频/视频混合:我们编写了一个程序,可以将音频和视频流混合到一个单独的视频文件中。
-
视频效果:如何调整流媒体视频的亮度、对比度和饱和度等属性。
-
文本叠加:我们开发了一个实用程序,可以在流媒体视频上添加文本、时间戳和时钟字符串。
-
视频中的静态图像:我们学习了如何将流媒体视频的帧保存为图像。
这就结束了我们关于使用 Python 和 GStreamer 进行视频处理的讨论。对于音频和视频处理,我们主要开发了各种命令行工具。这让我们对多媒体框架底层组件的使用有了很好的理解。在我们的讨论中没有涉及用户界面组件。默认的 GUI 只会在播放视频时出现。
下一章的重点将集中在基于图形用户界面的音频和视频应用上。
第八章。使用 QT Phonon 的基于 GUI 的媒体播放器
早期章节主要关注开发音频和视频处理工具。我们故意将图形用户界面(GUI)放在一边,以便我们可以使用 GStreamer 框架学习“纯”多媒体处理技术。然而,仅仅“播放”音频或视频,我们总是更喜欢具有用户界面、提供简单控制播放、调整音量等功能的应用程序。
在本章中,我们将:
-
使用 QT 开发音频和视频播放器的 GUI
-
学习 Phonon 框架的基本组件,例如
MediaObject, MediaSource, AudioOutput
等,以构建媒体图 -
学习如何使用 QT Phonon 框架创建具有图形用户界面的媒体播放器
让我们开始吧。
安装先决条件
我们将在本节中介绍安装 QT Python 的先决条件。
PyQt4
此软件包为 QT 库提供 Python 绑定。我们将使用 PyQt4 为本章稍后开发的图像处理应用程序生成 GUI。GPL 版本可在以下位置获取:
www.riverbankcomputing.co.uk/software/pyqt/download
注意,您应该为 Python 2.6 版本安装 PyQt4 二进制文件。对于 Python 2.5 或更早版本,PyQt4 可能不支持 Phonon 模块。请查阅 PyQt4 文档以获取更多信息。PyQt4 的安装说明已在第二章“处理图像”中讨论。请参阅该章节以获取更多详细信息。以下表格总结了安装先决条件。
安装先决条件总结
软件包 | 下载位置 | 版本 | Windows 平台 | Linux/Unix/OS X 平台 |
---|---|---|---|---|
Python | python.org/download/releases/ |
2.6.4(或任何 2.6.x 版本) | 使用二进制发行版进行安装 | 从二进制文件安装。还必须安装额外的开发包(例如,对于基于 rpm 的 Linux 发行版,在软件包名称中包含 python-devel)。从源 tarball 构建和安装。 |
PyQt4 | www.riverbankcomputing.co.uk/software/pyqt/download |
4.6.2 或更高版本 | 使用针对 Python2.6 的二进制文件进行安装 | 首先安装 SIP 4.9 或更高版本。然后安装 PyQt4。 |
QT Phonon 简介
在早期关于音频和视频处理的章节中,我们广泛使用了 GStreamer 多媒体框架。Phonon是 QT 用于提供音频/视频播放的多媒体框架。在掌握 GStreamer API 知识的基础上,应该很容易理解 Phonon 多媒体框架背后的基本概念。
主要组件
让我们简要讨论 Phonon 架构背后的基本组件和概念。
媒体图
这类似于 GStreamer 的 pipeline
。媒体图指定了处理媒体流的各个 节点(类似于 GStreamer 元素)。例如,输出端 节点提供媒体数据作为输出。要在 Graph
中开始流式传输媒体数据,我们调用 MediaObject
模块的 play()
方法。
媒体对象
此对象用于媒体播放。它类似于 GStreamer 管道中处理输入媒体数据的部分。用于此目的的 MediaObject
类实例。它提供了控制播放的方法,例如播放、暂停和停止流媒体。
输出端
就像在 GStreamer 中一样,Phonon 有一个媒体 Sink
。例如,音频输出设备用于输出音频。
路径
Path
对象用于在 Phonon 的媒体图中连接节点。例如,MediaObject
节点连接到 AudioOutput
节点以流式传输音频。
效果
要操作流媒体,我们需要在 Graph
中插入 Effects
节点,在源(MediaObject)和 Sink
节点之间。这些节点也称为处理器。Phonon 框架的 Effect
类简化了向流媒体添加各种效果的过程。
后端
这是一个 backend
,在 Phonon 中执行繁重的工作,即处理媒体流。在 Windows 平台上,后端框架是 DirectShow。如果您使用 Linux,Phonon 的后端框架是 GStreamer,如果您使用 Mac OS X,则是 QuickTime。支持的功能(例如,支持的媒体格式)可能因平台而异。
命名空间 Phonon.BackendCapabilities
包含提供有关 Phonon 后端能够做什么的信息的函数。例如,函数 BackendCapabilities.availableMimeTypes()
返回后端能够解码的所有 MIME 类型的列表。此外,它还提供了有关可用音频输出设备和可用效果的信息。
模块
Qt Phonon 包含几个模块,有助于快速开发音频和视频播放的应用程序。我们将简要讨论几个重要的模块。
媒体节点
这是 Phonon 媒体图中所有节点的超类。因此,它被 MediaObject
、Effect
和 AudioOutput
等模块继承,这些模块将在下面讨论。
媒体源
如其名称所示,这是用于输入媒体源。MediaObject
使用它提供的媒体数据。以下代码行显示了这是如何实现的。
self.mediaObj.setCurrentSource(self.mediaSource)
MediaObject
类的 API 方法 setCurrentSource
用于指定从其中获取媒体数据的 MediaSource
对象。
MediaObject
如前所述,MediaObject
模块定义了一个用于管理播放的 API。play()
、pause()
和 stop()
等方法提供了播放控制。
路径
Path
类将图中的节点链接起来。它可以使用 API 方法Phonon.createPath
创建。以下代码片段显示了示例用法:
self.audioPath = Phonon.createPath(self.mediaObj, self.audioSink)
在这里,self.audioPath
是Path
类的实例。它将MediaObject
的实例与self.audioSink
(AudioOutPut
类的实例)链接起来。可以通过使用Path.insertEffect
添加更多节点到图中。
AudioOutput
这个类的实例在 Phonon 媒体图中提供了一个音频输出节点。输出设备通常是声卡。AudioOutput
通过我们刚才讨论的Path
对象连接到MediaObject
(以及Effect
实例)。属性AudioOutput.outputDevice()
包含有关输出设备的信息。
Effect
类Effect
的实例可以作为节点插入到媒体图中。Path.insertEffect
可以实现这种效果,而Path.removeEffect
则有助于从图中移除该节点。此对象修改流媒体数据。例如,回声效果将为音频添加回声。使用BackendCapabilities.availableAudioEffects
可以找出 Phonon 后端支持哪些效果。
VideoPlayer
这个类提供了一种重要的功能。它具有几个内置特性,可以消除显式创建节点(如MediaObject
)的需求。我们将在开发视频播放器应用程序时详细讨论这一点。
SeekSlider
SeekSlider
是一个 GUI 小部件。这个类提供了一个滑块来定位流媒体中的特定位置。它内部处理所有必要的更新和信号连接。它只需要媒体对象实例。
volumeSlider
这个类提供了一个用于控制音量的小部件。它通过内部连接信号使程序员的任务变得简单。以下代码行设置了音量滑块的音频输出设备。
volumeSlider.setAudioOutput(self.audioSink)
在这里,volumeSlider
将控制self.audioSink
的音频输出设备的音量。
项目:基于 GUI 的音乐播放器
让我们直接进入正题。我们将使用 QT Phonon 开发一个简单的基于 GUI 的音乐播放器。这个项目的目标是学习如何组合之前讨论过的 Phonon 框架的重要组件。这将帮助我们熟悉整个 Phonon 框架。在第二个项目中,我们将学习一种更简单的方法来完成同样的任务。
在这里开发的应用程序将播放打开的音频文件。它将具有 GUI 小部件来控制播放并添加各种效果到流媒体音频。音乐播放器应用程序的截图如下所示,展示了其图形用户界面:
音乐播放器中的 GUI 元素
所示的音乐播放器应用程序使用了以下 QT 小部件。
-
QMainWindow
:这个类提供了主应用程序窗口。在这个窗口中,其他元素(如按钮和菜单)被添加到布局中。 -
QToolButton:
使用QToolButton
类创建播放、暂停和停止按钮。这些QToolButton
的外观可以通过多个属性进行调整;例如,调用QToolButtoon.setAutoRaise(True):
移除凸起按钮效果。在鼠标悬停时,按钮将被突出显示并出现在表面之上。 -
VolumeSlider:
如前所述,音量滑块小部件用于控制输出音频设备的音量。 -
SeekSlider:
用于在流媒体中定位位置。当音乐播放时,其位置会自动更新。您可以使用鼠标拖动滑块来跳转到曲目上的不同位置。 -
QLineEdit:
此小部件用于显示当前正在播放的媒体文件的完整路径。 -
QMenubar:
这是位于QLineEdit
上方的菜单栏。在这里,我们添加了不同的菜单,如文件和效果。 -
QAction:
将各种音频效果选项添加到效果菜单中,作为QAction
实例。
以下是对音乐播放器应用程序显示的各种 QT 小部件的说明图中指出了一些刚才讨论的这些 QT 元素:
生成 UI 代码
使用 QT Designer 应用程序构建所需的 GUI。这应包含在 PyQT4 的二进制安装程序中。QT Designer 提供了一种快速设计和发展用户界面代码的方法。它支持许多常用 QT 小部件。可以将这些小部件交互式地添加到布局中。此工具也非常有用,可以增强应用程序的美观。例如,可以使用 QT Designer 中提供的各种功能轻松更改小部件颜色和其他属性。
生成 UI 代码的时间
此应用程序所需的 UI 文件已经为您创建。本节的目的不是向您展示如何从头开始生成 UI。它只是将说明使用 QT Designer 开发此应用程序的一些重要方面。然后您可以进一步实验,向音乐播放器应用程序添加新小部件。我们在第二章“处理图像”中开发“缩略图制作器”应用程序时使用了 QT Designer。我们将在本节中也介绍一些这些内容。
-
从 Packt 网站下载文件
Ui_AudioPlayerDialog.ui
。 -
启动 PyQt4 安装中包含的 QT Designer 应用程序。
-
在 QT Designer 中打开此文件。点击此音频播放器对话框中的每个小部件元素。与所选小部件关联的 QT 类将在 QT Designer 的属性编辑器面板中显示。
-
注意对话框中各种 UI 小部件周围的红色边界。这些边界指示了一个“布局”,其中小部件被排列。布局是通过
QLayout
类及其各种子类创建的。它是使用 QT 进行用户界面设计的临界组件。如果没有布局,当运行应用程序并例如调整对话框大小时,UI 元素可能会出现扭曲。以下插图显示了在 QT Designer 中打开时的对话框外观,即音乐播放器对话框(.ui 文件)。
- 仔细观察围绕小部件的边界,这些边界指示了布局的存在。你会注意到有多个边界。这表明我们已经在多个布局中放置了小部件。例如,播放、暂停和停止按钮被排列在水平布局 QHBoxLayout 中。这些按钮进一步排列在另一个包含 volumeSlider 元素的横向布局中。有关如何在布局中排列小部件的信息,请参阅 QT4 和 QT Designer 文档。
-
如果你点击菜单栏中的在此处输入占位符,它就会变为可编辑状态。有了这个,你可以在菜单栏中添加一个新的菜单。同样,你可以通过打开这些菜单并点击在此处输入菜单项来向文件和效果菜单添加菜单项。文件菜单有两个菜单项:打开和退出。请注意,效果菜单是空的。我们将在稍后向此菜单添加菜单项。在接下来的几个步骤中,我们将对这个对话框进行一些小的修改,以便了解 QT Designer。
-
我们现在将添加一个可以显示数字的小部件。这个小部件可以用来更新流媒体播放时间信息。左侧面板显示了一组可以鼠标拖放到音频播放器对话框窗口内部的小部件。以下截图说明了这一点:
-
你可以看到 QT Designer 的显示小部件面板和插入到对话框中的 LCD 数字小部件。
右侧的截图显示了插入的 LCD 数字小部件被选中。它被插入到一个布局中,该布局以横向方式排列 QToolButtons 和 volumeSlider 小部件。此外,请注意插入的 LCD 数字小部件有一个默认大小。这个大小需要调整,以便其他小部件能够获得它们的空间份额。可以使用 QT Designer 中的属性编辑面板调整此小部件的各种参数。在这里,我们调整了最大尺寸值,如下面的截图所示。
-
LCD 数字小部件的属性编辑器,右侧的截图显示了编辑后的尺寸参数。
一旦调整了最大宽度和高度参数,LCD 数字小部件就可以很好地适应水平布局。下一个插图显示了结果对话框。
- 就这样!你可以暂时将这个 LCD 数字小部件保留在对话框中。项目完成后,你可以使用它来添加显示流媒体时间信息的功能。请注意,LCD 数字小部件不是显示播放时间的唯一选项。你甚至可以使用 QTextLabel 并更新标签字符串来显示时间。
-
-
如章节中所述,第二章,处理图像,QT 设计器使用扩展名
.ui
保存用户界面文件。要将此转换为 Python 源代码,PyQt4 提供了一个名为pyuic4
的转换工具。在 Windows XP 上,对于标准的 Python 安装,此工具的路径为C:\Python26\Lib\site-packages\PyQt4\pyuic4.bat
。将此路径添加到环境变量中。或者,每次你想将.ui
文件转换为 Python 源文件时,指定整个路径。可以从命令提示符运行转换工具,如下所示:pyuic4 UI_AudioPlayerDialog.ui -o Ui_AudioPlayerDialog.py
- 此脚本将从输入的 .ui 文件创建一个 Python 源文件,名为
Ui_AudioPlayerDialog.py
。你可以进一步审查此文件以了解 UI 代码是如何设置的。我们将直接使用此文件进行后续讨论。
小贴士
修改自动生成的 Python 源文件
Ui_AudioPlayerDialog.py
并不是一个好主意;如果你对 QT 设计器的相应.ui
文件进行了更改并再次运行pyuic4
脚本,它将 覆盖 之前的 Python 源文件Ui_AudioPlayerDialog.py
,前提是我们使用相同的文件名。相反,你可以使用自动生成的文件作为基类,并创建一个子类来以编程方式添加自定义 UI 元素。 - 此脚本将从输入的 .ui 文件创建一个 Python 源文件,名为
刚才发生了什么?
本节让我们对使用 QT 设计器调整用户界面有了实际了解。为了体验用户界面编辑,我们在音频播放器对话框中添加了一个 LCD 数字小部件。我们学习了如何从使用 QT 设计器创建的 .ui
文件自动生成 Python 源代码。
连接小部件
命令行工具pyuic4
(Windows 用户的 pyuic4.bat)可以将 QT-Designer 创建的用户界面转换为 Python 源文件。然而,这个 UI 中的各种小部件需要响应用户操作。例如,当播放按钮被点击时,它必须开始流式传输媒体文件。因此,我们需要添加必要的代码来指示这些小部件在特定事件发生时应该做什么。这是通过槽和信号实现的。当特定的 GUI事件
发生时,会发出一个signal
。例如,当用户点击暂停按钮时,会发出一个"clicked()"
信号。slot
是调用此signal
的方法。这与我们在早期章节中将pad-added
信号通过decodebin
元素连接到decodebin_pad_added
方法的方式非常相似。请参阅 PyQt4/QT4 文档,其中包含各种小部件可用的signals
的完整列表。
行动时间 - 连接小部件
让我们学习如何使小部件对特定的用户操作做出响应,例如按钮点击。
-
从 Packt 网站下载文件
AudioPlayerDialog.py
。它定义了AudioPlayerDialog
类。 -
我们现在将回顾将类方法与发出的信号连接的方法。这些信号在特定“事件”发生时生成。
1 def _connect(self): 2 """ 3 Connect slots with signals. 4 """ 5 self.connect(self._dialog.fileOpenAction, 6 SIGNAL("triggered()"), 7 self._openFileDialog) 8 9 self.connect(self._dialog.fileExitAction, 10 SIGNAL("triggered()"), 11 self.close) 12 13 self.connect(self._dialog.menuAudioEffects, 14 SIGNAL("triggered(QAction*)"), 15 self._changeAudioEffects) 16 17 self.connect(self._dialog.playToolButton, 18 SIGNAL("clicked()"), 19 self._playMedia) 20 21 self.connect(self._dialog.stopToolButton, 22 SIGNAL("clicked()"), 23 self._stopMedia) 24 25 self.connect(self._dialog.pauseToolButton, 26 SIGNAL("clicked()"), 27 self._pauseMedia)
-
在这里,self._dialog 是
Ui_AudioPlayerDialog
类的一个实例。请注意,self.connect 是 QT 类 QMainWindow 继承的方法。它接受以下参数(QObject,SIGNAL,callable)。QObject 是任何小部件类型;SIGNAL 是在特定事件发生时生成的。Callable 是处理此事件的方法。AudioPlayer._connect 方法将所有必要的信号与类方法连接起来。 -
音频播放器对话框中的文件菜单包含两个
QActions
,即fileOpenAction
和fileExitAction
。当选择文件->打开时,为QAction
生成一个“触发”信号。我们需要注意这个信号,然后调用一个将执行打开文件任务的方法。这个信号通过第 5-7 行之间的代码连接。因此,当“triggered()”信号被发出时,对于fileopenAction
,会调用一个名为AudioPlayer._openFileDialog
的方法,该方法包含打开音频文件所需的代码。 -
让我们回顾第 9-12 行的代码。这段代码将
QMenu
内的所有QActions
连接到AudioPlayer
类的方法。第一个参数self._dialog.menuAudioEffects
是菜单栏中的效果菜单。这是一个QMenu
。第二个参数SIGNAL("triggered(QAction*)")
告诉 QT 我们想要捕获 效果 菜单内任何QActions
的触发信号。这最好用一个例子来解释。想象一下音频 效果 菜单有菜单项(QActions
),如回声和失真。当用户选择 效果 | 回声 或 效果 | 失真 时,会发出triggered(QAction*)
信号。QAction*
参数只是一个指向该QAction
的指针。第三个参数是接收方法,self._changeAudioEffects
,当此信号发出时会被调用。- 当点击 QToolButton,例如播放、暂停或停止按钮时,会发出 clicked() 信号。此信号通过代码块 13-23 连接到 AudioPlayer 类的适当方法。
-
注意我们没有连接
SeekSlider
和VolumeSlider
。这些小部件的信号是内部连接的。您需要做的只是分别为这些小部件设置MediaObject
和AudioOutput
。我们将在下一节中学习如何做到这一点。
刚才发生了什么?
我们回顾了 AudioPlayerDialog._connect()
方法,以了解 Audio Player 对话框内各种小部件是如何连接到内部方法的。这帮助我们学习了使用 QT 进行 GUI 编程的一些初步概念。
开发音频播放器代码
到目前为止的讨论一直集中在图形用户界面。我们学习了如何使用 QT Designer 创建用户界面,然后生成代表此 UI 的 Python 源文件。我们还回顾了将应用程序的前端与后端(类方法)连接的代码。现在,是时候回顾负责播放音频、控制播放和添加音频效果等操作的音频处理代码了。
操作时间 - 开发音频播放器代码
在前面的部分中使用的源文件 AudioPlayerDialog.py
也将在这里使用。AudioPlayerDialog
类继承自 QMainWindow
。
-
如果您还没有这样做,请下载 Python 源文件
AudioPlayerDialog.py
。 -
让我们从类的构造函数
AudioPlayerDialog
开始。1 def __init__(self): 2 QMainWindow.__init__(self) 3 self.mediaSource = None 4 self.audioPath = '' 5 self.addedEffects = {} 6 self.effectsDict = {} 7 8 # Initialize some other variables. 9 self._filePath = '' 10 self._dirPath = '' 11 self._dialog = None 12 # Create media object , audio sink and path 13 self.mediaObj = phonon.Phonon.MediaObject(self) 14 self.audioSink = Phonon.AudioOutput( 15 Phonon.MusicCategory, 16 self) 17 self.audioPath = Phonon.createPath(self.mediaObj, 18 self.audioSink) 19 20 # Create self._dialog instance and call 21 # necessary methods to create a user interface 22 self._createUI() 23 24 # Connect slots with signals. 25 self._connect() 26 27 # Show the Audio player. 28 self.show()
-
从第 2 行到第 6 行的代码块初始化了一些实例变量,这些变量将在以后使用。字典对象 self.effectsDict 将用于存储有关可用音频效果的信息。而 self.addedEffects 用于检查音频效果是否已添加到流媒体中。
在第 13 行,创建了 Phonon.MediaObject 的实例。它将被用于控制 MediaSource 的播放。
在 Phonon 媒体图中,通过第 14-16 行的代码创建了一个音频输出节点。我们将称之为 self.audioSink,这是在早期章节中使用的术语。AudioOutput 的第一个参数用于指定类别。它是一个 Phonon.Category 类的对象。由于这是一个音乐播放器应用程序,我们定义类别为 Phonon.MusicCategory。查看 QT 文档以了解更多关于类别的信息。第二个参数用作此音频接收器的父级。
Phonon.Path 类将媒体图中的节点链接起来。此对象是通过 API 方法 Phonon.createPath 创建的。在第 17 行,Path self.audioPath 将媒体对象 self.mediaObject 与音频输出 self.audioSink 链接起来。
_createUI
方法的调用处理用户界面的定义。我们已经学习了前端如何通过在_connect
方法中设置的联系与后端通信。最后,在第 28 行,API 方法 QMainWindow.show()显示音频播放器。
-
_createUI
方法将大部分 GUI 创建委托给UI_AudioPlayerDialog
类。该方法还包括进一步修改 GUI 的代码。1 def _createUI(self): 2 # Define the instance to access the the UI elements 3 defined in class Ui_AudioPlayerDialog. 4 self._dialog = Ui_AudioPlayerDialog() 5 self._dialog.setupUi(self) 6 self._dialog.retranslateUi(self) 7 playIcon= QIcon("play.png") 8 pauseIcon= QIcon("pause.png") 9 stopIcon= QIcon("stop.png") 10 musicIcon= QIcon("music.png") 11 12 self._dialog.playToolButton.setIcon(playIcon) 13 self._dialog.pauseToolButton.setIcon(pauseIcon) 14 self._dialog.stopToolButton.setIcon(stopIcon) 15 self.setWindowIcon(musicIcon) 16 self._setupEffectsMenu() 17 self._dialog.seekSlider.setMediaObject(self.mediaObj) 18 self._dialog.volumeSlider.setAudioOutput( 19 self.audioSink)
-
在第 4 行创建了
UI_AudioPlayerDialog
类的实例。setupUI
和retranslateUI
是自动生成的方法。这些方法是在将包含 UI 的 QT Designer 文件转换为 Python 源文件时生成的。AudioPlayerDialog 实例作为参数传递给这两个方法。代码块 7 到 14 设置了三个 QToolButton 实例的图标。setIcon API 方法接受 QIcon 实例作为参数。音乐播放器图标在标题(对话框的左上角)是通过第 15 行的代码创建的。如前所述,Phonon.SeekSlider 信号是内部连接的。我们只需要告诉它将处理哪个 MediaObject。这是在第 17 行完成的。同样,在第 18 行,volumeSlider 的 setAudioOutput 方法设置了 self.audiosink 作为此 volumeSlider 的音频输出。在设置 UI 设计时,我们没有向效果菜单中添加任何菜单项。现在,通过在第 16 行调用
_setupEffectsMenu
方法来完成这项工作。 -
让我们回顾一下
_setupEffectsMenu
方法。它将 Phonon 框架中可用的各种音频效果作为菜单项添加到效果菜单中。1 def _setupEffectsMenu(self): 2 availableEffects = ( 3 Phonon.BackendCapabilities.availableAudioEffects()) 4 for e in availableEffects: 5 effectName = e.name() 6 self.effectsDict[effectName] = e 7 action = QAction(effectName, 8 self._dialog.menuAudioEffects) 9 action.setCheckable(True) 10 self._dialog.menuAudioEffects.addAction(action)
-
命名空间 Phonon.BackendCapabilities 包含提供有关 Phonon 后端功能信息的函数。
BackendCapabilities.availableAudioeffects()
返回一个列表,其中包含在特定平台上由 Phonon 支持的所有音频效果。列表availableEffects
包含Phonon.EffectDescription
类的对象。self.effectsDict
存储了效果名称和EffectDescription
对象作为键值对。这个字典将在以后使用。效果菜单menuAudioEffects
用对应于每个可用音频效果的QAction
实例填充。QAction
在第 6 行创建。QAction
的setCheckable
属性通过鼠标点击切换动作的选中状态。以下截图显示了 Windows XP 上的效果菜单项 -
当从音乐播放器点击文件 | 打开时,会调用
_openFileDialog
方法。1 def _openFileDialog(self): 2 3 self._filePath = '' 4 5 self._filePath = ( 6 str(QFileDialog.getOpenFileName( 7 self, 8 "Open Audio File", 9 "", 10 "MP3 file (*.mp3);;wav(*.wav);;All Files 11 (*.*);;")) ) 12 if self._filePath: 13 self._filePath = os.path.normpath(self._filePath) 14 self._dialog.fileLineEdit.setText(self._filePath) 15 self._loadNewMedia()
-
这会弹出一个
QFileDialog
,其文件类型过滤器与第 10 行指定的过滤器相同。要了解支持的媒体格式,可以使用以下代码行中的Phonon.BackEndCapabilities
。types = Phonon.BackendCapabilities.availableMimeTypes()
-
其中,
types
是可用 MIME 类型的列表。然后将用户指定的文件路径存储在变量
self._filePath
中。此路径在对话框的文件LineEdit
字段中显示。在第 15 行,调用了_loadNewMedia
方法。我们将在下一节中回顾它。 -
_loadNewMedia
方法为MediaObject
设置媒体源。1 def _loadNewMedia(self): 2 # This is required so that the player can play another 3 # file, if loaded while first one is still playing. 4 if self.mediaSource: 5 self._stopMedia() 6 del self.mediaSource 7 self.mediaSource = phonon.Phonon.MediaSource( 8 self._filePath) 9 self.mediaObj.setCurrentSource(self.mediaSource)
-
第 4 行的
if
块确保在将新媒体源设置为播放状态之前,由媒体对象停止当前流式传输的音频(如果有的话)。尽管这不是必需的,但第 6 行的代码会清理MediaSource
对象占用的内存。第 8 行创建MediaSource
类的新实例。使用该类的MediaObject
的setCurrentSource
API 方法指定提供媒体数据的MediaSource
。这样,我们的媒体播放器就设置好了以流式传输音频文件。 -
当你在音乐播放器中点击播放按钮时,会调用
AudioPlayerDialog._playMedia
方法。1 def _playMedia(self): 2 if not self._okToPlayPauseStop(): 3 return 4 5 if self.mediaObj is None: 6 print "Error playing Audio" 7 return 8 9 self.mediaObj.play()
-
首先,程序执行一些基本检查以确保媒体可播放,然后调用
Phonon.MediaObject
的play()
方法,开始流式传输音频。AudioPlayerDialog
类的_pauseMedia
和_stopMedia
方法包含类似的代码。我们刚刚学习了如何设置媒体图、流式传输媒体和控制其播放。现在让我们看看如何向这个流式媒体添加音频效果。如果效果菜单中的任何项被点击,会调用
AudioPlayerDialog._changeAudioEffects
方法:1 def _changeAudioEffects(self, action): 2 effectName = action.text() 3 4 if action.isChecked(): 5 effectDescription = self.effectsDict[effectName] 6 effect = Phonon.Effect(effectDescription) 7 self.addedEffects[effectName] = effect 8 self.audioPath.insertEffect(effect) 9 else: 10 effect = self.addedEffects[effectName] 11 self.audioPath.removeEffect(effect) 12 del self.addedEffects[effectName]
-
前面的代码片段中的 if 和 else 块分别向媒体图中添加和删除效果节点。当 Effects 菜单中的动作被选中时执行 if 块。当已选中的动作被切换时,程序执行 else 块。在 if 块中,在第 6 行创建了一个 Phonon.Effect 实例。这个实例需要一个 EffectDescription 对象作为参数。如 _setupEffectsMenu 方法中所示,self.effectsDict 将 EffectDescription 对象存储为字典值。在第 8 行,此效果被插入为媒体图中的一个节点。self.audioPath 将媒体图中的所有节点链接起来。
字典 self.addedEffects 跟踪媒体图中插入的所有音频效果。else 块移除已添加的效果。
在第 11 行,通过调用 Phonon.Path 的 removeEffect API 方法移除了一个附加效果。在第 12 行,也删除了 self.addedEffects 中对应的键值对。这也确保了没有内存泄漏。
小贴士
QT Phonon 允许多次添加相同的音频效果。例如,您可以使用
Path.insertEffect
在媒体图中创建多个“合唱”效果节点。每个添加的效果都将有自己的贡献。然而,在我们的应用程序中,我们只支持添加一个效果。您可以扩展此功能以支持多次添加相同的效果。为此,您需要调整效果菜单 UI,并在代码中进行一些其他更改以跟踪添加的效果。 -
如果在播放音频文件时关闭 GUI 窗口,应用程序将无法正确终止。为了安全地终止应用程序而不产生内存泄漏,
AudioPlayerDialog
重写了QMainWindow.closeEvent
。在关闭窗口之前,我们进行必要的清理以避免内存泄漏。下面的代码展示了这一过程。1 def closeEvent(self, evt): 2 print "\n in close event" 3 if self.mediaObj: 4 self.mediaObj.stop() 5 6 self.mediaObj = None 7 self._clearEffectsObjects() 8 QMainWindow.closeEvent(self, evt)
-
如果有流媒体,首先会停止。调用 _clearEffectsObject 会删除所有 Phonon.Effect 和 Phonon.EffectDescription 对象(如果存在)。_clearEffectsObject 方法名本身就说明了其功能。
-
以下代码创建了一个
QApplication
实例并执行此程序。1 app = QApplication(sys.argv) 2 musicPlayer = AudioPlayerDialog() 3 app.exec_()
-
检查文件
AudioPlayerDialog.py
中的其余代码,然后按照以下方式运行音乐播放器:$python AudioPlayerDialog.py
- 这应该会显示音乐播放器 GUI 窗口。使用文件 | 打开来指定音乐文件,然后点击播放按钮来享受音乐!
刚才发生了什么?
我们刚刚创建了自己的音乐播放器!我们使用 QT Phonon 多媒体框架开发了此音乐播放器的前端。我们详细讨论了 QT Phonon 中各种模块的使用。我们学习了如何通过使用MediaObject, AudioOutput, Path
等模块在媒体图中设置音频控制和效果。我们还通过 QT 获得了对 GUI 编程方面的一些高级理解。
尝试为音频播放器添加更多功能
在生成 UI 代码部分,我们向音乐播放器 GUI 窗口添加了一个小部件。这是一个可以显示帧数的 LCD 数字小部件。将其连接到音频播放器后端,以便它可以显示当前媒体时间。以下插图显示了用于流式音频文件的此 LCD 数字小部件的工作情况。
-
扩展这个音乐播放器应用程序,使其能够依次播放目录或 CD 中的所有歌曲。创建用于显示文件的用户界面有多种方法。例如,你可以尝试使用
QDirectoryView
、QTreeView
或QTableWidget
等小部件。音乐播放器在 LCD 数字小部件中显示帧数(时间):
项目:基于 GUI 的视频播放器
在第一个项目中,我们学习了 QT Phonon 框架的基础知识。在本项目中,我们将通过开发视频播放器来进一步扩展这些知识。音频播放器是通过构建媒体图来开发的。通过创建Phonon.Path
,将各种节点如MediaObject
、AudioOutput
和Effects
链接在一起。如果目标仅仅是开发一个简单的音频或视频播放器,那么工作会更加简单。Phonon 有一个名为VideoPlayer
的模块,它提供了一种抽象的方式来播放音频或视频,无需显式创建MediaObject
、AudioOutput
和其他一些对象。它只需要一个MediaSource
。也可以通过添加各种音频效果节点来创建自定义媒体图。我们将在稍后看到如何做到这一点。现在,让我们使用 QT Phonon 编写一个简单的视频播放器应用程序。以下插图显示了视频播放器的工作情况。
生成 UI 代码
QT Designer 是一个交互式生成用户界面的优秀资源。正如我们所见,本项目的大部分用户界面都是使用 QT Designer 构建的。此外,还介绍了使用 QT 进行用户界面设计的一些重要要点。本节将指导您使用 QT Designer 和pyuic4
生成此应用程序的用户界面。
动手实践 - 生成 UI 代码
.ui
文件已经创建。在以下讨论中,我们将简单地使用此文件,并介绍一些此应用程序所需的 GUI 元素。
-
从 Packt 网站下载文件
Ui_VideoPlayerDialog.ui
。 -
在 QT Designer 中打开此文件。点击每个小部件元素。与所选小部件关联的 QT 类将在右侧的属性编辑器面板中显示。这里使用的多数小部件与早期项目中使用的相同。唯一不同的小部件是
Phonon.VideoPlayer
。以下插图显示了在 QT Designer 中打开时的对话框外观。它还指出了对话框中使用的各种 Phonon 小部件。- QT Designer 中的视频播放器小部件看起来如前一张截图所示。
-
点击前面插图所示的
VideoPlayer
小部件。属性编辑器将显示其属性。请注意这个小部件的大小是如何定义的。此视频播放器将支持在全屏
模式下查看视频的功能。因此,VideoPlayer
小部件的最大尺寸参数被设置为较高的值,如下一张截图所示。实际上,我们正在使用 QT 默认值作为最大尺寸属性。唯一修改的属性是小部件的最小尺寸。这个最小尺寸将是视频播放器小部件的默认尺寸。视频播放器小部件的属性编辑器:
-
其余的小部件与之前项目中使用的相同。您可以添加 LCD 数字小部件或一个简单的文本标签来显示流媒体当前时间。有关添加此类小部件的说明,请参阅最后一个项目。
-
接下来,使用
pyuic4
转换工具将.ui
文件转换为.py
。转换工具可以从命令提示符运行,如下所示:pyuic4 UI_VideoPlayerDialog.ui -o Ui_VideoPlayerDialog.py
- 此脚本将从输入的.ui 文件创建一个 Python 源文件,名为 Ui_VideoPlayerDialog.py。我们将直接使用此文件进行后续讨论。
刚才发生了什么?
之前的讨论作为使用 QT Designer 生成此项目所需的大部分用户界面元素的复习。使用了pyuic4
转换工具将.ui
文件转换为 Python 源文件。
连接小部件
在之前的项目中使用的多数小部件在此处被重新使用。因此,这将是一个简短的讨论。在这个项目中,我们没有包含效果菜单。因此,VideoPlayerDialog._connect
方法有细微的变化。此方法如下所示:
def _connect(self):
self.connect(self._dialog.fileOpenAction,
SIGNAL("triggered()"),
self._openFileDialog)
self.connect(self._dialog.fileExitAction,
SIGNAL("triggered()"),
self.close)
self.connect(self._dialog.fullScreenAction,
SIGNAL("toggled(bool)"),
self._toggleFullScreen)
self.connect(self._dialog.playToolButton,
SIGNAL("clicked()"),
self._playMedia)
self.connect(self._dialog.stopToolButton,
SIGNAL("clicked()"),
self._stopMedia)
self.connect(self._dialog.pauseToolButton,
SIGNAL("clicked()"),
self._pauseMedia)
突出的代码行是一个新的小部件连接。其余的连接与之前项目中讨论的相同。当选择视图 | 全屏时,fullScreenAction
的toggled(bool)
信号被触发。当发生这种情况时,会调用slot
方法self._toggleFullScreen
。下一节将详细介绍此方法。
开发视频播放器代码
生成的前端连接到后端以处理媒体。在本节中,我们将回顾实际流式传输媒体和控制播放和音量的视频播放器后端。在这里我们的工作会更容易。我们在早期项目中做的许多好工作将在此处重新使用。将有一些小的修改,因为我们将使用Phonon.VideoPlayer
进行视频处理,而不是显式创建如MediaObject
之类的对象。
行动时间 - 开发视频播放器代码
让我们开发视频播放器后端的其余部分。我们将从 AudioPlayerDialog
类中重用几个方法,并进行一些小的修改。本节将只涵盖重要的方法。
-
从 Packt 网站下载文件
VideoPlayerDialog.py
。 -
类的构造函数如下所示。
1 def __init__(self): 2 QMainWindow.__init__(self) 3 self.mediaSource = None 4 self.audioPath = '' 5 6 # Initialize some other variables. 7 self._filePath = '' 8 self._dialog = None 9 10 # Create self._dialog instance and call 11 # necessary methods to create a user interface 12 self._createUI() 13 14 self.mediaObj = self._dialog.videoPlayer.mediaObject() 15 self.audioSink = self._dialog.videoPlayer.audioOutput() 16 17 self._dialog.seekSlider.setMediaObject(self.mediaObj) 18 self._dialog.volumeSlider.setAudioOutput( 19 self.audioSink) 20 21 # Connect slots with signals. 22 self._connect() 23 24 # Show the Audio player. 25 self.show()
-
self._dialog 创建了 Phonon.VideoPlayer 类的实例。一旦指定了媒体源,self._dialog.videoPlayer 就能够流式传输媒体。因此,对于媒体流本身,我们不需要显式创建 MediaObject 和 AudioOutput 节点;Phonon.VideoPlayer 内部构建媒体图。然而,MediaObject 和 AudioOutput 分别需要用于 seekSlider 和 volumeControl 小部件。在第 14 和 15 行,这些对象从 self._dialog.videoPlayer 获取。
-
_createUI
方法几乎与AudioPlayerDialog
中的相应方法相同,只是没有与 效果 菜单相关的代码。 -
接着,要审查的方法是
_playMedia:
1 def _playMedia(self): 2 if not self._okToPlayPauseStop(): 3 return 4 self._dialog.videoPlayer.play(self.mediaSource)
-
代码是自我解释的。在
VideoPlayerDialog._loadNewMedia
中设置了 self.mediaSource。这个 MediaSource 实例作为参数传递给 API 方法 VideoPlayer.play。然后视频播放器内部构建媒体图并播放指定的媒体文件。 -
这个简单的视频播放器支持全屏模式查看流媒体视频的选项。
QMainWindow
类提供了一个简单的方法来在全屏和正常观看模式之间切换视图。这是通过_toggleFullScreen
方法实现的。def _toggleFullScreen(self, val): """ Change between normal and full screen mode. """ # Note: The program starts in Normal viewing mode # by default. if val: self.showFullScreen() else:
self.showNormal()
-
方法 self.showFullScreen() 是从 QMainWindow 类继承的。如果视图菜单(视图 | 全屏)中的 QAction 被选中,视频播放器窗口将设置为全屏。QMainWindow.showNormal() 将视频播放器切换回正常观看模式。以下截图显示了全屏模式下的视频播放器。注意,在全屏模式下,窗口标题栏被隐藏。
下一个图像展示了全屏模式下的视频播放器:
-
-
审查来自文件
VideoPlayerDialog.py
的其余代码。将此文件与Ui_VideoPlayerDialog.py
放在同一目录下,然后从命令行运行程序,如下所示: -
视频播放器 GUI 窗口将出现。打开任何支持的音频或视频文件,然后点击 播放 按钮开始播放。
$python VideoPlayerDialog.py
刚才发生了什么?
我们编写了自己的视频播放器。这个视频播放器能够播放支持格式的视频和音频文件。在这里使用了 Phonon.VideoPlayer
模块,它使媒体播放和控制成为可能。我们还学习了如何使用 QMainWindow 类的 API 方法在全屏和正常观看模式之间切换视图。
尝试使用视频播放器做更多的事情
-
这里有一个简单的增强。全屏模式显示播放控制按钮、快进和音量滑块等小部件。当检查 视图 | 全屏 动作时,隐藏这些小部件。此外,添加一个键盘快捷键来在正常和全屏视图模式之间切换。
-
向视频播放器 GUI 添加音频效果。我们已经在第一个项目中学习了如何向媒体图添加音频效果。你可以在这里重用那段代码。然而,你需要一个合适的
Phonon.Path
对象,effects
节点需要添加到这个对象中。在上一个项目中,我们使用了Phonon.createPath
,但我们不能创建一个新的路径,因为它是由 VideoPlayer 内部创建的。相反,你可以使用 API 方法MediaObject.outputPaths()
来获取路径。下面是一个示例代码行。self.audioPath = self.mediaObj.outputPaths()[0]
-
然而,要注意内存泄漏问题。如果你添加了音频效果,然后退出应用程序,程序可能会冻结。这可能是因为效果节点没有被从原始音频路径中删除。或者,你可以从基本原理构建视频播放器。也就是说,不要使用 Phonon.VideoPlayer。相反,构建一个自定义的媒体图,就像我们在音频播放器项目中做的那样。在这种情况下,你需要使用诸如 Phonon.VideoWidget 这样的模块。
摘要
本章教会了我们关于使用 QT 开发 GUI 多媒体应用程序的几个要点。我们完成了两个令人兴奋的项目,其中开发了使用 QT Phonon 框架的音频和视频播放器。为了完成这些任务,我们:
-
使用 QT Designer 生成 UI 源代码
-
通过将槽(类方法)与信号连接来处理 QT 生成的事件
-
使用 Phonon 框架设置音频和视频流媒体图的媒体图