【问题标题】:How can I optimize the performance of an Image comparison script?如何优化图像比较脚本的性能?
【发布时间】:2014-09-16 22:43:28
【问题描述】:

我编写了一个脚本,该脚本使用均方根比较来比较大量图像(超过 4500 个文件)。首先,它将每个图像的大小调整为 800x600 并获取直方图。之后,它构建一个组合数组并将它们平均分配到四个线程,这些线程计算每个组合的均方根。 RMS 低于 500 的图像将被移动到文件夹中,以便稍后手动排序。

#!/usr/bin/python3

import sys
import os
import math
import operator
import functools
import datetime
import threading
import queue
import itertools
from PIL import Image


def calc_rms(hist1, hist2):
    return math.sqrt(
        functools.reduce(operator.add, map(
            lambda a, b: (a - b) ** 2, hist1, hist2
        )) / len(hist1)
    )


def make_histogram(imgs, path, qout):
    for img in imgs:
        try:
            tmp = Image.open(os.path.join(path, img))
            tmp = tmp.resize((800, 600), Image.ANTIALIAS)
            qout.put([img, tmp.histogram()])
        except Exception:
            print('bad image: ' + img)
    return


def compare_hist(pairs, path):
    for pair in pairs:
        rms = calc_rms(pair[0][1], pair[1][1])
        if rms < 500:
            folder = 'maybe duplicates'
            if rms == 0:
                folder = 'exact duplicates'
            try:
                os.rename(os.path.join(path, pair[0][0]), os.path.join(path, folder, pair[0][0]))
            except Exception:
                pass
            try:
                os.rename(os.path.join(path, pair[1][0]), os.path.join(path, folder, pair[1][0]))
            except Exception:
                pass
    return


def get_time():
    return datetime.datetime.now().strftime("%H:%M:%S")


def chunkify(lst, n):
    return [lst[i::n] for i in range(n)]


def main(path):
    starttime = get_time()
    qout = queue.Queue()
    images = []
    for img in os.listdir(path):
        if os.path.isfile(os.path.join(path, img)):
            images.append(img)
    imglen = len(images)
    print('Resizing ' + str(imglen) + ' Images ' + starttime)
    images = chunkify(images, 4)
    threads = []
    for x in range(4):
        threads.append(threading.Thread(target=make_histogram, args=(images[x], path, qout)))

    [x.start() for x in threads]
    [x.join() for x in threads]

    resizetime = get_time()
    print('Done resizing ' + resizetime)

    histlist = []
    for i in qout.queue:
        histlist.append(i)

    if not os.path.exists(os.path.join(path, 'exact duplicates')):
        os.makedirs(os.path.join(path, 'exact duplicates'))
    if not os.path.exists(os.path.join(path, 'maybe duplicates')):
        os.makedirs(os.path.join(path, 'maybe duplicates'))

    combinations = []
    for img1, img2 in itertools.combinations(histlist, 2):
        combinations.append([img1, img2])

    combicount = len(combinations)
    print('Going through ' + str(combicount) + ' combinations of ' + str(imglen) + ' Images. Please stand by')
    combinations = chunkify(combinations, 4)

    threads = []

    for x in range(4):
        threads.append(threading.Thread(target=compare_hist, args=(combinations[x], path)))

    [x.start() for x in threads]
    [x.join() for x in threads]

    print('\nstarted at ' + starttime)
    print('resizing done at ' + resizetime)
    print('went through ' + str(combicount) + ' combinations of ' + str(imglen) + ' Images')
    print('all done at ' + get_time())

if __name__ == '__main__':
    main(sys.argv[1]) # sys.argv[1] has to be a folder of images to compare

这可行,但在 15 到 20 分钟内完成调整大小后,比较会运行数小时。起初我认为这是一个锁定队列,工作人员从中获取他们的组合,所以我用预定义的数组块替换它。这并没有减少执行时间。我也在不移动文件的情况下运行它以排除可能的硬盘问题。

使用 cProfile 对其进行分析提供以下输出。

Resizing 4566 Images 23:51:05
Done resizing 00:05:07
Going through 10421895 combinations of 4566 Images. Please stand by

started at 23:51:05
resizing done at 00:05:07
went through 10421895 combinations of 4566 Images
all done at 03:09:41
         10584539 function calls (10584414 primitive calls) in 11918.945 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     16/1    0.001    0.000 11918.945 11918.945 {built-in method exec}
        1    2.962    2.962 11918.945 11918.945 imcomp.py:3(<module>)
        1   19.530   19.530 11915.876 11915.876 imcomp.py:60(main)
       51 11892.690  233.190 11892.690  233.190 {method 'acquire' of '_thread.lock' objects}
        8    0.000    0.000 11892.507 1486.563 threading.py:1028(join)
        8    0.000    0.000 11892.507 1486.563 threading.py:1066(_wait_for_tstate_lock)
        1    0.000    0.000 11051.467 11051.467 imcomp.py:105(<listcomp>)
        1    0.000    0.000  841.040  841.040 imcomp.py:76(<listcomp>)
 10431210    1.808    0.000    1.808    0.000 {method 'append' of 'list' objects}
     4667    1.382    0.000    1.382    0.000 {built-in method stat}

完整的分析器输出可以在here找到。

考虑到第四行,我猜测线程以某种方式锁定。但是,无论图像数量如何,为什么以及为什么恰好是 51 次?

我在 Windows 7 64 位上运行它。

提前致谢。

【问题讨论】:

  • 请使用指定库进行计算。 Python 并非旨在以这种方式使用。考虑 NumPy 或 OpenCv 绑定。
  • 使用 python 内置方法有什么问题?
  • 什么都没有,除了 Python 中没有内置的图像比较功能。

标签: python multithreading performance python-3.x


【解决方案1】:

一个主要问题是您使用线程来完成至少部分受 CPU 限制的工作。由于全局解释器锁,一次只能运行一个 CPython 线程,这意味着您无法利用多个 CPU 内核。这将使 CPU 密集型任务的多线程性能充其量与单核执行没有什么不同,甚至可能更糟,因为线程增加了额外的开销。这在threading documentation 中注明:

CPython 实现细节:在 CPython 中,由于 Global 解释器锁,一次只能有一个线程执行Python代码 (即使某些面向性能的库可能会克服 这个限制)。如果您希望您的应用程序更好地利用 多核机器的计算资源,建议您 使用multiprocessing。但是,线程仍然是一个合适的模型 如果您想同时运行多个 I/O 密集型任务。

要绕过 GIL 的限制,您应该按照文档中的说明进行操作,并使用 multiprocessing 库而不是 threading 库:

import multiprocessing
...

qout = multiprocessing.Queue()

for x in range(4):
    threads.append(multiprocessing.Process(target=make_histogram, args=(images[x], path, qout)))

...
for x in range(4):
    threads.append(multiprocessing.Process(target=compare_hist, args=(combinations[x], path)))

如您所见,multiprocessing 在大多数情况下是threading 的直接替代品,因此更改应该不会太难。唯一的复杂情况是,如果您在进程之间传递的任何参数都不可挑选,尽管我认为所有这些参数都属于您的情况。在进程之间发送 Python 数据结构的 IPC 成本也会增加,但我怀疑真正并行计算的好处将超过额外的开销。

话虽如此,由于依赖于对磁盘的读/写,您可能仍然受到 I/O 的限制。并行化不会使您的磁盘 I/O 更快,因此在那里可以做的事情不多。

【讨论】:

  • 感谢您的意见 :)。我用进程替换了线程,它们按预期工作,但由于大小限制,队列导致死锁。我改为使用multiprocessing.Manager().list(),它似乎可以完成这项工作。不过,我会在大集合上运行它过夜,看看多处理需要多长时间。
  • 成功了!我能够将计算时间从三个小时减少到大约 45 分钟。谢谢!
【解决方案2】:

要比较 4500 张图像,我建议在文件级别进行多处理,而不是(必须)在图像中进行多线程处理。正如@dano 所指出的那样,GIL 会阻碍这一点。我的策略是:

  1. 每个核心(或配置的数量)一个工作进程;
  2. 一个编排过程,它分叉了上述内容;做一些 IPC 来协调工人的工作。

(简要地)查看您的代码看起来会从惰性语言中受益;我没有看到任何试图短路比较的尝试。例如,如果您对图像的每个片段进行 RMS 比较,一旦您确定块足够不同,就可以在结束比较块后停止比较。然后,您可能还关心更改迭代块的方式,以及块的大小/形状。

除此之外,我会考虑寻找更便宜的机制来避免做一些平方根;可能使用创建“近似”平方根的方法,也可能使用查找表。

如果我没记错的话,您还可以创建一个中间形式(直方图),您应该暂时保留它。无需保存 800x600 图像。

此外,了解您在此练习中“平等”的意思会很有用。

【讨论】:

  • 我没有保存 800x600 的图像。调整大小发生在内存中,图像对象将在获取直方图后立即被覆盖。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多