使用 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 Imaging Library)的“友好分支”,可在当前版本的 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》,并继续在[有时]每周播客《开源创意播客》中吐露他的经验。在 @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 (未验证)

© . All rights reserved.