0%

对Python图像处理库EffectLab进行性能测试

EffectLab也是一个基于PIL的Python的图像库,目的是为了提供更多的特效处理以及更快的测试。

目前EffectLab可以实现的特效可以围观之前的文章:http://everet.org/2012/07/effectlab.html

古人云:_选择了脚本语言_就要忍受其速度。

但是,有时脚本语言的速度已经慢到了无法形容的地步时,我们就开始考虑性能优化了。

寻找性能热点

Python有一对很好的性能测试工具:cProfile与pstats。

我们来选择一个波浪效果来做测试:

python
1
2
3
4
5
6
7
img = Image.new("RGB", (100, 100))
wave = GlobalWaveEffect(1, 0.5)
test = partial(wave, img)

cProfile.run("test()", "profile.data")
p = pstats.Stats("profile.data")
p.strip_dirs().sort_stats("time").print_stats()

我们可以看到其输出:

console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Tue Aug 14 17:21:10 2012    profile.data

417923 function calls (417922 primitive calls) in 0.434 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.150 0.150 0.434 0.434 Effect.py:92(filter)
80000 0.068 0.000 0.068 0.000 {round}
40000 0.051 0.000 0.063 0.000 Effect.py:304(transform)
41889 0.034 0.000 0.034 0.000 {map}
41890 0.029 0.000 0.042 0.000 Image.py:606(load)
40000 0.028 0.000 0.091 0.000 Effect.py:317()
33433 0.025 0.000 0.071 0.000 Image.py:946(getpixel)
41890 0.013 0.000 0.013 0.000 {built-in method pixel_access}
40000 0.012 0.000 0.012 0.000 {math.sin}
33433 0.011 0.000 0.011 0.000 {built-in method getpixel}
8457 0.008 0.000 0.019 0.000 Image.py:1260(putpixel)
8457 0.004 0.000 0.004 0.000 {built-in method putpixel}

可以发现,运行时间最长的函数有第92行的filter,以及第304行的transform。我们可以查看第92行的函数filter,这个函数看上去非常的简短,主要做的是处理每一个像素以及有抗锯齿运算。

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def filter(self, img):
width, height = img.size
new_img = Image.new(img.mode, img.size, Effect.empty_color)

nband = len(img.getpixel((0, 0)))
antialias = self.antialias
left, top, right, bottom = self.box if self.box else (0, 0, width, height)

for x in xrange(left, right):
for y in xrange(top, bottom):
found = 0
psum = (0, ) * nband

# anti-alias
for ai in xrange(antialias):
_x = x + ai / float(antialias)
for aj in xrange(antialias):
_y = y + aj / float(antialias)

u, v = self.formula(_x, _y)

u = int(round(u))
v = int(round(v))
if not (0 <= u < width and 0 <= v < height):
continue
pt = img.getpixel((u, v))
psum = map(operator.add, psum, pt)
found += 1

if found > 0:
psum = map(operator.div, psum, (found, ) * len(psum))
new_img.putpixel((x, y), tuple(psum))

return new_img

以及transform函数:

python
1
2
3
4
5
def transform(self, x, y, width, height, delta_w, delta_h):
radian = 2 * math.pi * (x + self.xoffset) / float(width) * delta_w
offset = 0.5 * sin(radian) * height * delta_h

return x, y + offset

解决性能热点

嗯,这个看上去热点都是纯计算的代码,貌似已经没什么优化的空间了,这时怎么办呢?

鉴于CPython可以非常容易的使用C/C++扩展模块,我们用C语言来实现里面这些纯计算的部分,看看性能有什么提升。

我们用C来实现Filter函数。重新运行cProfile看看,

console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Tue Aug 14 17:38:56 2012    profile.data

12 function calls in 0.002 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.002 0.002 0.002 0.002 {EffectLab.EffectLabCore.wave_warp}
1 0.000 0.000 0.000 0.000 {built-in method copy}
1 0.000 0.000 0.000 0.000 Image.py:460(_new)
1 0.000 0.000 0.000 0.000 Image.py:740(copy)
1 0.000 0.000 0.002 0.002 Effect.py:310(filter)
1 0.000 0.000 0.002 0.002 :1()
1 0.000 0.000 0.000 0.000 Image.py:606(load)
1 0.000 0.000 0.002 0.002 Effect.py:37(__call__)
1 0.000 0.000 0.000 0.000 Image.py:449(__init__)
1 0.000 0.000 0.000 0.000 {built-in method pixel_access}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
1 0.000 0.000 0.000 0.000 {method 'copy' of 'dict' objects}

此时热点函数已经被C语言的模块给替换了。

我们用timeit模块统计一下运行时间,统计代码如下(其中test函数见上面,里面就是调用了波浪处理效果:

python
1
2
3
4
t = Timer('test()', 'from __main__ import test') 
N = 3
TIMES = 30
print sum(t.repeat(N, TIMES)) / N / TIMES * 1000, 'ms'

结果

我们来看看运行3轮,每轮运行30次,平均一次的时间是多少。
Python版本的平均一次时间为:303.63 ms

C版本平均一次时间为:1.91 ms

可见运行速度是原来的159倍