一、背景
本人准備用python做圖像和視頻編輯的操作,卻發現opencv和PIL的效率並不是很理想,並且同樣的需求有多種不同的寫法並有着不同的效率。見全網並無較完整的效率對比文檔,遂決定自己豐衣足食。
二、目的
本篇文章將對Python下的opencv接口函數及PIL(Pillow)函數的常用部分進行逐個運行並計時(多次測算取平均時間和最短時間,次數一般在100次以上),並簡單使用numba、ctypes、cython等方法優化代碼。
三、測試方法及環境
1.硬件
CPU:Intel(R) Core(TM) i3-3220 CPU @ 3.30GHz 3.30 GHz
內存:4.00 GB
硬盤:ATA WDC WD5000AAKX-7 SCSI Disk Device
2.軟件:
操作系統:Windows 7 Service Pack 1 Ultimate 64bit zh-cn
Python解釋器:3.7.5 64bit (provided by Anaconda)
各模塊:皆為最新
(事情有所變化,暫時使用下面機房電腦的配置進行測試)
1.硬件
CPU:Intel(R) Xeon(R) Silver 4116 CPU @ 2.10GHz 2.10 GHz
內存:3.00 GB
硬盤:VMware Virtual disk SCSI Disk Service
2.軟件:
操作系統:Windows 7 Service Pack 1 Ultimate 64bit zh-cn (powered by VMware Horizon View Client)
Python解釋器:3.7.3 64bit (provided by Anaconda)
各模塊:皆為最新
四、具體實現
1.待測試函數
以下定義新建的視頻規定為MP4格式、mp4v編碼、1920*1080尺寸、60幀速率;定義新建的圖片為JPG格式、1920*1080尺寸、RGB通道。
根據實際需要(其實是我自己的需要),先暫定測試以下函數[1][2]:
1)創建視頻
2)視頻幀讀取(視頻不好做測試數據,故使用了手頭上現成的。in.mp4參數:時長27秒,尺寸1920x1080,數據速率17073kbps,總比特率17331kbps,幀速率29fps,大小55.7MB)
3)視頻幀寫入[3] (PS:為什么Opencv官方教程中沒有這個函數...)
4)寫入視頻(后來發現這個應該類似於file.close(),只是一個釋放文件對象的過程,並不是真的在這個時候寫入所有的數據。之前看見在release之前文件是空的應該是數據還沒有從內存寫入磁盤導致的)
5)創建圖片 ( matrix & pillow object )
6)圖片讀取(opencv & pillow)(使用新建的圖片,滿足上面的定義,大小33kb)
7)圖片數據結構轉換
8)圖片點操作(matrix & pillow object )
9)圖片其他繪圖操作(matrix & pillow object & opencv )
這里我們測試畫直線、畫矩形、畫圓(不包括matrix)、畫橢圓操作(不包括matrix)、繪制文字(不包括matrix)。
注:pillow中默認繪制的圖形都是實心的[4],而opencv要設置線寬為負值才是實心的[5]。
其中opencv的字體參數參考:[6]
10)圖片其他操作
11)寫入圖片( Pillow & OpenCV)
2.時間計算工具
這里的時間計算工具用一個類實現給定次數的循環和智能循環(自動控制循環次數)的功能,並能給出每次循環的函數返回值、循環次數、平均時間、最短時間、最長時間、總共用時。
對於自動判斷循環次數的算法參考了Python的timeit模塊源碼(autorange函數)[7]:
1 # -*- coding: utf-8 -*-
2
3 import time 4 import cv2 5 from PIL import Image, ImageDraw, ImageFont 6 import numpy as np 7
8 # Class
9 class FunctionTimer(object): 10 MAX_WAIT_SEC = 0.5
11 INF = 2147483647
12 SMART_LOOP = -1
13
14 def __init__(self, timer=None, count=None): 15 self._timer = timer if timer != None else time.perf_counter 16 self._count = count if count != None else 100
17
18 def _get_single_time(self, func, *args, **kwargs): 19 s = self._timer() 20 ret = func(*args, **kwargs) 21 f = self._timer() 22 return ret, f - s 23
24 def _get_repeat_time(self, number, func, *args, **kwargs): 25 time_min, time_max, time_sum = self.INF, 0, 0 26 for i in range(number): 27 ret, delta = self._get_single_time(func, *args, **kwargs) 28 time_min = min(time_min, delta) 29 time_max = max(time_max, delta) 30 time_sum += delta 31 return func, ret, number, time_sum / number, time_min, time_max, time_sum 32
33 def gettime(self, func, *args, **kwargs): 34 if self._count != self.SMART_LOOP: 35 return self._get_repeat_time(self._count, func, *args, **kwargs) 36 else: 37 # Arrange loop count automatically
38 # Refer to Lib/timeit.py
39 i = 1
40 while True: 41 for j in 1, 2, 5: 42 number = i * j 43 func, ret, number, time_ave, time_min, time_max, time_sum = self._get_repeat_time(number, func, *args, **kwargs) 44 if time_sum >= self.MAX_WAIT_SEC: 45 return func, ret, number, time_ave, time_min, time_max, time_sum 46 i *= 10
47
48 def better_print(self, params): 49 func, ret, count, ave, minn, maxn, sumn = params 50 print('========================================') 51 print(' Function name:') 52 print(' ' + func.__repr__()) 53 print('========================================') 54 print(' Function has the return content below:') 55 print(' ' + ret.__name__) 56 print('========================================') 57 print(' Summary of Function Timer:') 58 print(' Count of loops: {}'.format(count)) 59 print(' Average time of loops: {} (sec)'.format(ave)) 60 print(' Minimum of every loop time: {} (sec)'.format(minn)) 61 print(' Maximum of every loop time: {} (sec)'.format(maxn)) 62 print(' Total time of loops: {} (sec)'.format(sumn)) 63 print('========================================') 64
65 # Function
66 def testfunc(x=10000000): 67 for i in range(x): 68 pass
69 return i 70
71 # Main Function
72 timer = FunctionTimer()
測試結果(將整個文件作為模塊以op為名字調用):
3.完整代碼
1 # opencv_pil_time.py
2
3 # -*- coding: utf-8 -*-
4
5 import time 6 import cv2 7 from PIL import Image, ImageDraw, ImageFont 8 import numpy as np 9
10 # Class
11 class FunctionTimer(object): 12 MAX_WAIT_SEC = 0.5
13 INF = 2147483647
14 SMART_LOOP = -1
15
16 def __init__(self, timer=None, count=None): 17 self._timer = timer if timer != None else time.perf_counter 18 self._count = count if count != None else 100
19
20 def _get_single_time(self, func, *args, **kwargs): 21 s = self._timer() 22 ret = func(*args, **kwargs) 23 f = self._timer() 24 return ret, f - s 25
26 def _get_repeat_time(self, number, func, *args, **kwargs): 27 time_min, time_max, time_sum = self.INF, 0, 0 28 for i in range(number): 29 ret, delta = self._get_single_time(func, *args, **kwargs) 30 time_min = min(time_min, delta) 31 time_max = max(time_max, delta) 32 time_sum += delta 33 return func, ret, number, time_sum / number, time_min, time_max, time_sum 34
35 def gettime(self, func, *args, **kwargs): 36 if self._count != self.SMART_LOOP: 37 return self._get_repeat_time(self._count, func, *args, **kwargs) 38 else: 39 # Arrange loop count automatically
40 # Refer to Lib/timeit.py
41 i = 1
42 while True: 43 for j in 1, 2, 5: 44 number = i * j 45 func, ret, number, time_ave, time_min, time_max, time_sum = self._get_repeat_time(number, func, *args, **kwargs) 46 if time_sum >= self.MAX_WAIT_SEC: 47 return func, ret, number, time_ave, time_min, time_max, time_sum 48 i *= 10
49
50 def better_print(self, params): 51 func, ret, count, ave, minn, maxn, sumn = params 52 print('========================================') 53 print(' Function name:') 54 print(' ' + func.__name__) 55 print('========================================') 56 print(' Function has the return content below:') 57 print(' ' + ret.__repr__()) 58 print('========================================') 59 print(' Summary of Function Timer:') 60 print(' Count of loops: {}'.format(count)) 61 print(' Average time of loops: {} (sec)'.format(ave)) 62 print(' Minimum of every loop time: {} (sec)'.format(minn)) 63 print(' Maximum of every loop time: {} (sec)'.format(maxn)) 64 print(' Total time of loops: {} (sec)'.format(sumn)) 65 print('========================================') 66
67 # Function
68 # Debug
69 def testfunc(x=10000000): 70 for i in range(x): 71 pass
72 return i 73
74 # Test Function
75 def task_1(): 76 vw = cv2.VideoWriter('out.mp4', cv2.VideoWriter_fourcc(*'mp4v'), 60, (1920, 1080)) 77
78 def task_2(): 79 cap = cv2.VideoCapture('in.mp4') 80 while cap.isOpened(): 81 ret, frame = cap.read() 82 if not ret: 83 break
84 cap.release() 85
86 def task_3(vw, frame): # Use a new blank video file when testing
87 vw.write(frame) 88
89 def task_4(vw): 90 vw.release() 91
92 def task_5_matrix(): 93 arr = np.zeros((1080, 1920, 3), dtype=np.uint8) 94
95 def task_5_pillow(): 96 img = Image.new('RGB', (1920, 1080)) 97
98 def task_6_opencv(): 99 arr = cv2.imread('in.jpg') 100
101 def task_6_pillow(): 102 img = Image.open('in.jpg') 103
104 def task_7_list(img): 105 arr1 = list(img.im) 106
107 def task_7_asarray(img): 108 arr2 = np.asarray(img) 109
110 def task_7_array(img): 111 arr3 = np.array(img) 112
113 def task_8_matrix(arr3): 114 arr3[0][0] = (255, 255, 255) 115
116 def task_8_pillow_putpixel(img): 117 img.putpixel((0, 0), (255, 255, 255)) 118
119 def task_8_pillow_point(draw): 120 draw.point((0, 0), (255, 255, 255)) 121
122 def task_9_line_matrix(arr3): 123 for x in range(100, 500): 124 arr3[100][x] = (255, 255, 255) 125
126 def task_9_line_pillow(draw): 127 draw.line((100, 100, 500, 100), (255, 255, 255)) 128
129 def task_9_line_opencv(arr): 130 cv2.line(arr, (100, 100), (500, 100), (255, 255, 255), 1) 131
132 def task_9_rectangle_matrix(arr3): 133 for x in range(100, 500): 134 for y in range(100, 500): 135 arr3[y][x] = (255, 255, 255) 136
137 def task_9_rectangle_pillow(draw): 138 draw.rectangle((100, 100, 500, 500), (255, 255, 255)) 139
140 def task_9_rectangle_opencv(arr): 141 cv2.rectangle(arr, (100, 100), (500, 500), (255, 255, 255), -1) 142
143 def task_9_circle_pillow_arc(draw): 144 draw.arc((100, 100, 500, 500), 0, 360, (255, 255, 255)) 145
146 def task_9_circle_pillow_ellipse(draw): 147 draw.ellipse((100, 100, 500, 500), (255, 255, 255)) 148
149 def task_9_circle_opencv_circle(arr): 150 cv2.circle(arr, (300, 300), 200, (255, 255, 255), -1) 151
152 def task_9_circle_opencv_ellipse(arr): 153 cv2.ellipse(arr, (300, 300), (200, 200), 0, 0, 360, (255, 255, 255), -1) 154
155 def task_9_ellipse_pillow(draw): 156 draw.ellipse((100, 100, 700, 500), (255, 255, 255)) 157
158 def task_9_ellipse_opencv(arr): 159 cv2.ellipse(arr, (400, 300), (300, 200), 0, 0, 360, (255, 255, 255), -1) 160
161 def task_9_text_pillow(draw, font): 162 draw.text((100, 100), 'Hello, world!', (255, 255, 255), font) 163
164 def task_9_text_opencv(arr, font): 165 cv2.putText(arr, 'Hello, world!', (100, 200), font, 2, (255, 255, 255), 1, cv2.LINE_AA) 166
167 def task_10(): 168 pass
169
170 def task_11_pillow(img): 171 img.save('out.jpg') 172
173 def task_11_opencv_imread(arr): 174 cv2.imwrite('out.jpg', arr) 175
176 def task_11_opencv_asarray(arr2): 177 cv2.imwrite('out.jpg', arr2) 178
179 def task_11_opencv_array(arr3): 180 cv2.imwrite('out.jpg', arr3) 181
182 # Main Function
183 if __name__ == '__main__': 184 timer = FunctionTimer() 185 # timer.better_print(timer.gettime(func, *args, **kwargs))
186 timer.better_print(timer.gettime(task_1)) 187 vw = cv2.VideoWriter('out.mp4', cv2.VideoWriter_fourcc(*'mp4v'), 60, (1920, 1080)) 188 # timer.better_print(timer.gettime(task_2)) # task_2 takes up much time and we don't test it!
189 frame = np.zeros((1080, 1920, 3), dtype=np.uint8) 190 timer.better_print(timer.gettime(task_3, vw, frame)) 191 timer.better_print(timer.gettime(task_4, vw)) 192 timer.better_print(timer.gettime(task_5_matrix)) 193 timer.better_print(timer.gettime(task_5_pillow)) 194 timer.better_print(timer.gettime(task_6_opencv)) 195 arr = cv2.imread('in.jpg') 196 timer.better_print(timer.gettime(task_6_pillow)) 197 img = Image.new('RGB', (1920, 1080)) 198 timer.better_print(timer.gettime(task_7_list, img)) 199 timer.better_print(timer.gettime(task_7_asarray, img)) 200 timer.better_print(timer.gettime(task_7_array, img)) 201 arr2 = np.asarray(img) 202 arr3 = np.array(img) 203 timer.better_print(timer.gettime(task_8_matrix, arr3)) 204 timer.better_print(timer.gettime(task_8_pillow_putpixel, img)) 205 draw = ImageDraw.Draw(img) 206 timer.better_print(timer.gettime(task_8_pillow_point, draw)) 207 timer.better_print(timer.gettime(task_9_line_matrix, arr3)) 208 timer.better_print(timer.gettime(task_9_line_pillow, draw)) 209 timer.better_print(timer.gettime(task_9_line_opencv, arr)) 210 timer.better_print(timer.gettime(task_9_rectangle_matrix, arr3)) 211 timer.better_print(timer.gettime(task_9_rectangle_pillow, draw)) 212 timer.better_print(timer.gettime(task_9_rectangle_opencv, arr)) 213 timer.better_print(timer.gettime(task_9_circle_pillow_arc, draw)) 214 timer.better_print(timer.gettime(task_9_circle_pillow_ellipse, draw)) 215 timer.better_print(timer.gettime(task_9_circle_opencv_circle, arr)) 216 timer.better_print(timer.gettime(task_9_circle_opencv_ellipse, arr)) 217 timer.better_print(timer.gettime(task_9_ellipse_pillow, draw)) 218 timer.better_print(timer.gettime(task_9_ellipse_opencv, arr)) 219 font = ImageFont.truetype('simkai.ttf', 32) 220 timer.better_print(timer.gettime(task_9_text_pillow, draw, font)) 221 font = cv2.FONT_HERSHEY_SIMPLEX 222 timer.better_print(timer.gettime(task_9_text_opencv, arr, font)) 223 timer.better_print(timer.gettime(task_11_pillow, img)) 224 timer.better_print(timer.gettime(task_11_opencv_imread, arr)) 225 timer.better_print(timer.gettime(task_11_opencv_asarray, arr2)) 226 timer.better_print(timer.gettime(task_11_opencv_array, arr3))
在此我先停一下,各位可以猜猜哪種方式更勝一籌。
flag
flag
flag
flag
flag
flag
flag
flag
flag
flag
flag
flag
flag
五、結果
1.現象
其中task_2(讀取視頻文件)占用時間過多,我們不予循環測試,下面的結果欄中將給出單次運行的結果(取第一次)。
cmder.exe中運行結果:
(很奇怪為什么循環次數都是100次,感覺可能timer算法有問題)
時間單位:秒,精確度:3位有效數字,制作成表格(紅字表示所在子操作名中平均時間最短的函數,如若平均時間最短按照時間排列順序依次比較)(圖片讀取一欄的紅字標錯位置了,應該打在pillow的下面):

2.結論
1)前四項由於沒有對比就不多說了,不過感覺opencv讀取視頻的速度確實有些慢(6.5MB/s,90.8frame/s)。當然寫入數據也很慢(75.8frame/s),不過尺寸不同,就不互相比較了。
2)創建圖片操作numpy數組要比pillow的對象要快一些(也就兩個數量級吧~)。
3)數據結構轉換中numpy比list快幾乎是顯然的hhh,其中asarray要比array略快一點,大概是因為array深復制而asarray淺復制;當然asarray的結果是not writable的,估計是因為image對象存儲的數組本身就是只讀的吧。如果只是為了讀取圖片方便塞視頻里就用asarray。
4)沒想到圖片點操作里面numpy的索引賦值竟然比putpixel還要慢一點!真是大開眼界。。。果然pillow源碼里面說“自帶api要快一點”是真的。。。
5)圖片讀取、圖片繪圖絕大多數情況下pillow秒殺numpy和opencv,只有在寫文字的時候opencv體現出比較大的效率優勢,但是opencv的字體有很多限制,還是棄置了。(我手頭上有一套字模,還是可以測試一下numpy寫字速度的,不過估計還是要慢一些,而且字模做起來也比較臃腫,就不試了~)
6)寫圖片還是opencv要快一點點,當然asarray和array在多精確幾個數字就是asarray快了,如果只有三位那就是array更快一點。
六、優化
(待續)
七、總結反思
這個項目我大概從一個月前就有想法了,最近一周一直在抽時間做,凈時間估計都有十幾個小時了。最后一天(11月16日)晚上我拖到12點,作業還沒做完,困得要死,也就做了個大概--沒有優化的部分,也沒有表格,還因為事先沒查好api返工了好幾次。這件事讓我深感個人的力量的薄弱 ,以及我自己水平的低下。
不過這次的項目讓我掌握了多方面搜索數據(尤其是api)的能力,諸如找官方文檔啊,看源碼啊之類的,晦澀難懂的源代碼和英文文檔我也盡可能啃掉了,也算是一大進步了吧。
然后就是項目的內容。本次的測試我盡可能從自己能想到的角度給出足夠多的實現方法來對比運行效率,孰優孰劣一下子就清楚了。不過也要看情況,比如說給定的數據全是數組,你要是為了追求圖像處理函數的效率而全部轉成pil對象,也並不是好的。除了時間效率的差距,我們也可以看出PIL的圖像處理能力果然還是上等,opencv只是視頻庫附帶一個簡陋的圖像處理能力,真正到解決圖像問題時候還是應該選擇PIL。
當然,這次的實驗也有不科學的地方,諸如沒有控制好無關變量,甚至可能導致相反的結果。我不是專業搞cs得,而且我還是高二生,實在無力全身心投入其中。實驗方法帶來的誤差以及內容的錯漏,尚希見諒!
最后希望各位能在這篇充滿艱辛的博客中得到點什么。哪怕是一點處理編程項目時的教訓而不是博客內容本身,我也心滿意足了。
參考資料:
[1]Pillow (PIL Fork) 7.0.0.dev0 英文文檔
[2]OpenCV Python Tutorials 翻譯 OpenCV-Python Tutorials
[3]Python&OpenCV - 讀寫(read&write)視頻(video) 詳解 及 代碼
[4]Python圖像處理庫PIL的ImageDraw模塊介紹
