使用 Python 查找损坏的图像

381 位读者喜欢这篇文章。
Using Python to find corrupted images

Jason van Gumster。CC BY-SA 4.0

回顾本系列

第 1 部分:使用 Python 为数字艺术家自动化重复性任务

第 2 部分:数字艺术家的 Python 文件管理技巧


如果你在电脑上处理图像,你迟早会遇到损坏的文件,破坏你的一天。我经常在动画渲染中遇到这种情况(记住,这里的最佳实践是将渲染输出为图像文件序列,而不是单个视频文件)。然而,动画和视觉效果并不是唯一会看到图像损坏的地方。你在其他领域也很容易遇到这种情况。也许你是一位摄影师,你拍摄了一堆包围曝光 HDRI(高动态范围成像)色调映射,并且在从相机传输文件时出现了一些故障。

问题不在于修复或替换损坏的图像需要多少精力,这通常只是重新渲染图像或将好的图像重新复制到你的电脑上,而技巧在于尽早地在过程中找到那些坏图像。你不知道的时间越长,当你真正遇到损坏的图像时,你将面临更大的麻烦。

那么,你该怎么办?好吧,你可以逐个打开每个文件——一次一个——在你的图像编辑器或查看器中,让该程序告诉你存在问题。然而,照片图像很大,并且浏览整个集合只是为了找到一两个坏家伙可能很烦人和耗时。虽然动画渲染通常是较小的文件,但你通常有更多的文件要浏览。就我而言,我经常制作渲染,其中包含超过 44,000 帧的渲染。(不,这不是错别字——四万四千帧。)

解决方案?你猜对了。编写一个脚本。

与本系列之前的文章一样,你将使用 Python 进行脚本编写。第一步:获取文件列表。幸运的是,如果你已经阅读了本系列上一篇文章,你就知道这只是使用 os 模块的问题。假设你要检查的所有图像文件都位于硬盘驱动器上的单个目录中。此外,假设你将从该目录中运行此脚本。使用 Python,你可以使用以下代码获取这些文件的列表

import os
    
for filename in os.listdir('./'):
  print(filename)

如果你愿意,你可以缩小该图像列表(或至少更清楚地指定它;例如,你不希望将此脚本包含在这些文件中),只查找以 PNG 扩展名结尾的文件

import os
    
for filename in os.listdir('./'):
  if filename.endswith('.png'):
    print(filename)

你现在在当前工作目录中有一个 PNG 图像文件列表。现在怎么办?好吧,现在你需要弄清楚这些图像中哪些(如果有)已损坏。在本系列之前的文章中,我们专门使用了 Python 默认附带的模块。不幸的是,在没有任何图像处理能力的情况下发现图像是否损坏是困难的,Python 2 和 Python 3 都没有任何开箱即用的方法来处理这个问题。你需要获取一个图像处理模块来查看这些文件。幸运的是,Python 开发社区为你简化了这一过程。

事实上,你有一个完整的软件包库可供你安装。你只需要知道如何获取它们。让我向你介绍 pip,这是推荐用于安装 Python 包的工具。当你安装 Python 时,它默认安装在大多数平台上。

注意: 我正在使用 Python 3,但如果你正在使用 Python 2,我在本系列中编写的几乎所有内容都可以在这两种语言变体之间转移。此外,许多 Linux 发行版更喜欢你使用他们自己的软件包管理系统而不是使用 pip 来安装 Python 包。如果你愿意,可以坚持使用它。这里建议使用 pip 主要是为了在你可以使用 Python 的所有平台上保持一致。

我将推荐你安装的特定软件包称为 Pillow。它是原始 PIL(Python 图像库)的“友好分支”,可在当前版本的 Python 3 和 Python 2 中使用。你只需要启动一个终端窗口并键入 pip install Pillow 即可安装 Pillow。Python 包工具应该会为你处理剩下的事情。

一旦你安装了 Pillow,你实际上需要在你的脚本中使用它。因为它已安装,你可以像对待任何 Python 附带的模块一样对待它。你使用 import——在这种情况下,你可以使用 import PIL。然而,为了查找损坏的图像,你实际上不需要将整个 Pillow 库导入到我们的脚本中。在 Python 中,你可以只导入模块的单个子组件。这是一个好的实践,因为它减少了脚本的内存占用,并且同样重要的是,它更清楚地表明了你的脚本从一开始就要做什么。此外,当你导入子组件时,一旦你进入脚本的主体,你最终需要键入更少的内容。这总是一个不错的奖励。

要导入模块的子组件,你需要在 import 前面加上 from 指令。在 Pillow 的情况下,你的脚本实际上只需要使用 Image 类。因此,你的导入行看起来像 from PIL import Image。事实上,你也可以对 os 模块执行相同的操作。如果你回顾之前的代码,你可能会注意到你只使用了 os 模块中的 listdir 函数。因此,你可以使用 from os import listdir 而不是 import os。这意味着当你进入你的脚本时,你不再需要键入 os.listdir。相反,你只需要键入 listdir,因为这就是你导入的所有内容。

将所有这些放在一起,你的脚本现在应该看起来像这样

from os import listdir
from PIL import Image
    
for filename in listdir('./'):
  if filename.endswith('.png'):
    print(filename)

你已经加载了 Pillow 中的 Image 类,但你的脚本仍然没有任何作用。现在是时候进入脚本的功能部分了。你将要做的是打开每个图像文件并检查它是否可读的脚本等效操作。如果出现错误,那么你就找到了一个坏文件。为此,你将使用 try/except 块。简而言之,你的脚本将尝试运行一个打开文件的函数。如果该函数返回错误,也称为异常,那么你就知道该图像有问题。特别是,如果异常类型为 IOErrorSyntaxError,那么你就知道你有一个坏图像。

执行 try/except 的语法非常简单。我在下面的代码注释中描述了它

try: # These next functions may produce an exception
  # <some function>
except (IOError, SyntaxError) as e: # These are the exceptions we're looking for
  # <do something... like print an intelligent error message>

在查找损坏的图像文件的情况下,你将要测试两个函数:Image.open()verify()。如果你将它们包装在 try/except 块中,你的损坏图像查找脚本应该如下所示

from os import listdir
from PIL import Image
    
for filename in listdir('./'):
  if filename.endswith('.png'):
    try:
      img = Image.open('./'+filename) # open the image file
      img.verify() # verify that it is, in fact an image
    except (IOError, SyntaxError) as e:
      print('Bad file:', filename) # print out the names of corrupt files

就这样。将此脚本保存在你的图像目录中。当你从命令行运行它时,你应该获得其中所有损坏的图像文件的列表。如果没有任何内容打印出来,那么你可以假设所有这些图像文件都是良好、有效的图像。

当然,能够在任何任意目录上使用此脚本会很好。并且让脚本提示你指示它继续为你删除那些损坏的文件会更好。好消息!你可以让脚本完全做到这一点。我们将在本系列的下一篇文章中介绍这一点。

与此同时,尽情在你图像文件夹中找出损坏的文件吧。

User profile image.
Jason van Gumster 主要编造东西。他写作、制作动画,偶尔也使用开源工具进行教学。他经营一家小型独立动画工作室,编写了《Blender For Dummies》和《GIMP Bible》,并继续在 [有时] 每周播客 Open Source Creative Podcast 中吐露他的经验。在 @monsterjavaguns 上的冒险(和谎言)。

7 条评论

我从没想过要这样做。完全没有。更不用说用 Python 了。

很棒的文章,很棒的技巧。谢谢!

根据我的经验,除了你上面提到的那些之外,还可能发生 struct.error。

很棒的提示!我自己没有遇到过这种情况,但我绝对可以想象它会发生。非常值得添加到脚本中。

回复 作者:Ashwin Vishnu (未验证)

我使用 PIL 与 Scribus 做了很多相关的工作。我想知道串行打开 100、500 或 1,000 个图像文件是否会对资源造成压力。图像是否加载到内存中?

这是一个好问题,老实说,我不确定图像是否加载到内存中。也就是说,正如我在文章中写到的,我经常在包含超过 44,000 个图像的目录中使用此脚本的变体,并且我没有遇到任何异常的内存使用情况。当然,我运行这个脚本的机器具有相当强大的 RAM 规格,所以我肯定需要在下次运行时更加密切地关注它。

总而言之,在 for 循环的末尾添加 img.close() 可能不会有什么坏处(并且可能会更合适)。我认为这应该可以解决那里的大部分问题。

回复 作者:Greg P

当我阅读标题时,我期望某种针对校验和(即 MD5 等)的检查

出于好奇,verify() 函数如何在没有任何其他比较的情况下真正检查完整性?

谢谢。

如果我们 *仅仅* 谈论从相机等设备传输的图像,那么校验和可能会作为一种解决方案。然而,在正在渲染的动画帧的情况下,很难使用校验和来做这类事情,因为正是你提出的问题。我们正在处理原始数据;没有“已知的良好”文件版本可以进行比较。在这种情况下,我们关于数据完整性的问题从“这是否与我们已经知道的另一个文件相同?”变为“这个文件根本可以作为图像读取吗?” 这就是 verify() 的用途。

回复 作者:Monster (未验证)

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.