عملیات اصلی
تکنیکهای سنجش و بهبود کارایی
هدف#
در پردازش تصویر، از آنجا که با تعداد زیادی عملیات در ثانیه سر و کار دارید، ضروری است که کد شما نه تنها پاسخ صحیح را ارائه دهد، بلکه آن را به سریعترین شکل ممکن نیز فراهم کند. بنابراین در این فصل یاد خواهید گرفت:
- سنجش کارایی و سرعت اجرای کد خود.
- نکاتی برای بهبود کارایی و سرعت کد.
- با این توابع آشنا خواهید شد: cv.getTickCount، cv.getTickFrequency و غیره.
به غیر از OpenCV، پایتون ماژولی به نام time را نیز ارائه میدهد که در سنجش زمان اجرا مفید است. ماژول دیگری به نام profile به دریافت گزارش دقیق از کد کمک میکند، مانند اینکه هر تابع چقدر زمان برده، چند بار فراخوانی شده است و غیره. اما اگر از IPython استفاده میکنید، تمام این ویژگیها به روشی کاربرپسند ادغام شدهاند. ما برخی از موارد مهم را خواهیم دید و برای جزئیات بیشتر، پیوندهای بخش منابع اضافی را بررسی کنید.
سنجش کارایی با OpenCV#
تابع cv.getTickCount تعداد چرخههای ساعت (Clock-cycles) را پس از یک رویداد مرجع (مانند لحظهای که دستگاه روشن شده است) تا لحظه فراخوانی این تابع برمیگرداند. بنابراین اگر آن را قبل و بعد از اجرای یک تابع فراخوانی کنید، تعداد چرخههای ساعت مصرفشده برای اجرای تابع را به دست میآورید.
تابع cv.getTickFrequency فرکانس چرخههای ساعت یا تعداد چرخههای ساعت در هر ثانیه را برمیگرداند. بنابراین برای یافتن زمان اجرا بر حسب ثانیه، میتوانید به صورت زیر عمل کنید:
e1 = cv.getTickCount()
# your code execution
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()
ما این موضوع را با مثال زیر نشان خواهیم داد. مثال زیر فیلتر میانه (Median Filtering) را با هستههایی با ابعاد فرد از ۵ تا ۴۹ اعمال میکند. (نگران شکل نتیجه نباشید - این هدف ما نیست):
img1 = cv.imread('messi5.jpg')
assert img1 is not None, "file could not be read, check with os.path.exists()"
e1 = cv.getTickCount()
for i in range(5,49,2):
img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print( t )
# Result I got is 0.521107655 seconds
بهینهسازی پیشفرض در OpenCV#
بسیاری از توابع OpenCV با استفاده از SSE2، AVX و غیره بهینهسازی شدهاند. این کتابخانه شامل کدهای بهینهنشده نیز میباشد. بنابراین اگر سیستم ما از این قابلیتها پشتیبانی میکند، باید از آنها بهره ببریم (تقریباً تمام پردازندههای امروزی از آنها پشتیبانی میکنند). این بهینهسازیها هنگام کامپایل به صورت پیشفرض فعال هستند. بنابراین OpenCV در صورت فعال بودن، کد بهینهشده را اجرا میکند، در غیر این صورت کد بهینهنشده اجرا میشود. میتوانید از ()cv.useOptimized برای بررسی فعال/غیرفعال بودن آن و از ()cv.setUseOptimized برای فعال/غیرفعال کردن آن استفاده کنید. بیایید یک مثال ساده را ببینیم:
# check if optimization is enabled
In [5]: cv.useOptimized()
Out[5]: True
In [6]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop
# Disable it
In [7]: cv.setUseOptimized(False)
In [8]: cv.useOptimized()
Out[8]: False
In [9]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop
همانطور که مشاهده میکنید، فیلتر میانه بهینهشده حدود ۲ برابر سریعتر از نسخه بهینهنشده است. اگر سورس کد آن را بررسی کنید، میبینید که فیلتر میانه با دستورالعملهای SIMD بهینهسازی شده است. بنابراین میتوانید از این تابع برای فعالسازی بهینهسازی در ابتدای کد خود استفاده کنید (به یاد داشته باشید که به طور پیشفرض فعال است).
سنجش کارایی در IPython#
گاهی اوقات ممکن است نیاز داشته باشید کارایی دو عملیات مشابه را با هم مقایسه کنید. محیط IPython یک دستور جادویی بنام timeit% را برای انجام این کار در اختیار شما قرار میدهد. این دستور کد را چندین بار اجرا میکند تا نتایج دقیقتری به دست آورد. بار دیگر یادآوری میشود که این ابزار برای سنجش خطوط تکی کد بسیار مناسب است.
به عنوان مثال، آیا میدانید کدامیک از عملیات جمع زیر بهتر است: x = 5; y = x**2 یا x = 5; y = x*x یا x = np.uint8([5]); y = x*x یا y = np.square(x)؟ ما این موضوع را با timeit% در پوسته IPython بررسی خواهیم کرد.
In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop
میتوانید مشاهده کنید که x = 5 ; y = x*x سریعترین است و در مقایسه با Numpy حدود ۲۰ برابر سریعتر عمل میکند. اگر زمان ایجاد آرایه را نیز در نظر بگیرید، ممکن است تا ۱۰۰ برابر سریعتر هم برسد. جالب است، نه؟ (توسعهدهندگان Numpy در حال کار روی این مسئله هستند)
یک مثال دیگر را امتحان میکنیم. این بار کارایی ()cv.countNonZero و ()np.count_nonzero را برای همان تصویر مقایسه میکنیم:
In [35]: %timeit z = cv.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop
In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop
ببینید، تابع OpenCV تقریباً ۲۵ برابر سریعتر از تابع Numpy است.
دستورات جادویی بیشتر در IPython#
چندین دستور جادویی دیگر برای سنجش کارایی، پروفایلینگ (Profiling)، پروفایلینگ خط به خط، سنجش حافظه و غیره وجود دارد. همه آنها به خوبی مستندسازی شدهاند، بنابراین در اینجا فقط پیوندهایی به آن اسناد ارائه میشود. به علاقهمندان توصیه میشود آنها را امتحان کنند.
تکنیکهای بهینهسازی کارایی#
تکنیکها و روشهای کدنویسی متعددی برای بهرهبرداری از حداکثر کارایی Python و Numpy وجود دارد. در اینجا فقط موارد مرتبط ذکر شده و پیوندهایی به منابع مهم ارائه شده است. نکته اصلی که باید در نظر داشت این است: ابتدا سعی کنید الگوریتم را به روشی ساده پیادهسازی نمایید. هنگامی که به درستی کار کرد، آن را پروفایل کنید، گلوگاهها (bottlenecks) را پیدا کرده و آنها را بهینهسازی نمایید.
- تا حد امکان از استفاده از حلقهها در پایتون، به خصوص حلقههای تودرتو (دوتایی/سهتایی) خودداری کنید؛ آنها ذاباً کند هستند.
- الگوریتم/کد را تا بیشترین حد ممکن برداریسازی (Vectorize) کنید، چرا که Numpy و OpenCV برای عملیات برداری بهینهسازی شدهاند.
- از همبستگی و یکپارچگی حافظه موقت (Cache Coherence) بهره ببرید.
- هرگز از یک آرایه کپی نگیرید مگر اینکه واقعاً ضروری باشد. سعی کنید به جای آن از نماها (Views) استفاده کنید؛ کپی کردن آرایه یک عملیات پرهزینه است.
اگر کد شما پس از انجام تمام این عملیات همچنان کند است، یا اگر استفاده از حلقههای بزرگ اجتنابناپذیر است، از کتابخانههای کمکی مانند Cython برای افزایش سرعت آن استفاده کنید.
منابع اضافی#
- تکنیکهای بهینهسازی پایتون
- یادداشتهای سخنرانی Scipy - آشنایی پیشرفته با Numpy
- زمانبندی و پروفایلینگ در IPython