امروزه با کمک پردازش تصویر و هوش مصنوعی میتوان تحلیلهای عمیقی انجام داد. تشخیص چهره، تشخیص چشم، تشخیص خودرو و … از این دسته از تحلیلها میباشند. اما پردازش تصویر چگونه انجام میگیرد یا چگونه میتوان آن را پیاده سازی کرد. امروز در همین خصوص، وارد مبحث Contour یا کانتور در OpenCV خواهیم شد.
ماژول opencv
Opencv یکی از ماژولهای پردازش تصویر و هوش مصنوعی میباشد. با کمک این ماژول میتوان تصاویر را دریافت و بسیاری از عملیاتهای پردازش تصویر را پیاده سازی کرد. جالب است بدانید با opencv میتوان تشخیص چهره، تشخیص چشم و یا حتی تشخیص اعداد را نیز انجام داد. علاوه بر تصاویر، پردازش ویدئو نیز میتوان با کمک این ماژول انجام داد. در این مقاله، کانتور را با کمک OPENCV و زبان برنامهنویسی پایتون پیادهسازی خواهیم کرد. البته بعضی از قسمتها به موجب نیاز، از ماژول Numpy استفاده میگردد. Pdf آموزش Numpy از لینک میتوانید نمایید.
کانتور چیست
کانتور در واقع مرزهای خارجی تصویر میباشد. یعنی منحنیهای پیوستهای هستند که مرز کامل یک جسم را پوشش میدهند. هرچه الگوریتم های پردازش تصویر در زمینه یافتن کانتور تصویر دقیقتر باشند این منحنی بسته به لبههای خارجی اشیا در تصویر نزدیکتر هست. مثلاً اگر تصویر شامل یک گل در یک باغ هست یک مربع که تمام این گل درون آن جای میگیرد هم میتواند از نظر یک الگوریم کانتوریابی ضعیف، به عنوان کانتور تصویر باشد اما هرجه الگوریتم دقیقتر عمل کند ناحیه بستهای که گل را دربرمیگیرد مرزهایش به لبههای تصویر گل نزدیکتر میشود و تصویر کلی را با کانتور به دست میآورند. کاربرد کانتور بیشتر در بخش بندی (segment) و تفکیک کردن تصاویر درون یک عکس دیجیتال هست.
تصویر بالا شامل دو قسمت میشود. سمت چپ تصویر اصلی است و در سمت راست کانتور تصویر است. خطوط سبز رنگ کانتورهای شناسایی شده در تصویر هستند. همانطور که میبینید اشیا درون تصویر توسط کانتور شناسایی و جداسازی شدهاند.
در تصویر بالا از کانتور برای جداسازی background استفاده کرده ایم. حیوان درون تصویر توسط کانتور تشخیص و یک تصویر Mask از آن ایجاد کردهایم (تصویر باینری).
کانتور در OepnCV
در این قسمت وارد پیادهسازی کانتور میشویم. ماژول OpenCV دو تابع برای نمایش و تشخیص کانتور ر ا پیادهسازی کرده است:
- findContours() : جهت تشخیص کانتور
- drawContours(): جهت نمایش کانتور
ابتدا با ورودیهای تابع findContours آشنا میشویم:
- image: تصویر ورودی
- Mode یا Retreival Modes : حالت بازیابی کانتور (در ادامه در مورد این ورودی بیشتر صحبت خواهیم کرد)
- Method یا Contouring Modes: این پارامتر را contour-approximation method یا روش تقریبی کانتور میگویند. Method دو ورودی cv2.CHAIN_APPROX_NONE و cv2.CHAIN_APPROX_SIMPLE را دریافت میکند (در ادامه این دو پارامتر را توضیح خواهیم داد).
ورودیهای تابع drawContours به صورت زیر است:
- Image یا تصویر ورودی
- Contours: کانتورهای مشخص با توجه به خروجی تابع findContours در این قسمت هستند.
- contourIdx یا شناسه کانتور: ورودی بالا (Contours) به صورت لیست بوده و در صورتی که میخواهیم کانتور خاصی رسم کنیم اندیس عنصر کانتور در لیست را در این قسمت وارد میکنیم تا تنها آن کانتور چاپ گردد. اگر این ورودی را عدد منفی قرار دهیم، تمامی کانتور در تصویر نمایان میشود.
- Color: رنگ برای نمایش کانتور
- Thickness ضخامت خط کانتور جهت نمایش
بررسی ورودیهای findContours
سه ورودی image و mode و method که در قسمت قبل معرفی کردیم مربوط به تابع findContours میباشد. پارامتر image یا تصویر ورودی با آن آشنایی داریم و نیاز به بررسی بیشتر ندارد. اما دو پارامتر دیگر نیاز به معرفی بیشتری دارد. ابتدا mode را بررسی کنیم:
- RETR_LIST: تمامی کانتورها را دریافت کرده و هیچگونه رابطه سلسه مراتبی (والد – فرزند) را ذخیرهسازی نمیکند.
- RETR_EXTERNAL: بیرونیترین کانتور را در هر شی تصویر ذخیره میکند.
- RETR_CCOMP: تمامی کانتورها دریافت و رابطه دو سطحی بین آنها ایجاد میکند. کانتور بیرونی سطح بالا (سطح اول) قرار میگیرد و اگر کانتوری درون آن باشد به عنوان سطح دوم ذخیره میگردد. اگر مجدداً درون کانتور سطح دوم، کانتور دیگر یافت شود آن را سطح بالا نامگذاری و کانتور درون آن (در صورت وجود) را سطح دوم نامگذاری کرده و این روال ادامه دارد …
- RETR_TREE: تمامی کانتورها با تمامی سطوح روابط آنها ذخیرهسازی میگردد.
در زمینه روشهای بالا مثالهایی را پیادهسازی خواهیم کرد تا بتوان هر کدام را بهتر تحلیل نمود.
ورودیهای پارامتر method به صورت زیر است:
- CHAIN_APPROX_NONE: تمامی نقاط کانتورها ذخیره سازی میگردد.
- CHAIN_APPROX_SIMPLE: کانتورها تقسیمبندی و تنها نقاط پایانی (end point) ذخیره میشود.
فراخوانی تصویر
ما قبل از ورود به کانتور در openCV نیاز است ماژولهای مورد نیاز را فراخوانی نماییم:
import cv2
import numpy as np
در این قسمت تصویر ورودی را ابتدا میخوانیم:
image = cv2.imread(LP.jpg')
cv2.imshow("Original", image)
تصویر ورودی به صورت زیر میباشد:
تصویر ورودی یک پلاک خودرو میباشد.
پیادهسازی کانتور در OpenCV
قبل از ورود به پیاده سازی کانتور در OpenCV این نکته را باید توجه کنیم. تابع findContours برای تصاویر باینری عملکرد مناسبی دارد. به همین دلیل معمولاً از آستانهگذاری (Thresholding) یا تشخیص لبه (edge detection) قبل از صدا زدن این تابع بکار میرود. اکنون پیاده سازی را آغاز میکنیم.
تصویر ورودی را از تصویر رنگی به grayscale تبدیل میکنیم(برای انجام Thresholding):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
حال آستانهگذاری را اعمال میکنیم:
_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
cv2.imshow('After thresholding', th2)
تصویر خروجی بعد از thrtesholding:
اکنون findContours را صدا میزنیم:
contours, hierarchy = cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
دو خروجی که تابع findContours در اختیار ما قرار میدهد یکی رابطه کانتورها (متغیر hierarchy) و دیگری نقاط کانتورها (متغیر contours) می باشد. توجه کنید دو پارامتر مهم RETR_LIST و CHAIN_APPROX_NONE را در این قسمت استفاده کردهایم. برای نمایش خروجی کانتورها باید تابع drawContours را صدا بزنیم:
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
cv2.imshow('Contours overlaid on original image', image)
متغیر contours که در قسمت قبل توسط تابع findContours ایجاد شد را به ورودی تابع drawContours میدهیم. خروجی این قسمت به صورت زیر میباشد:
خطوط سبز، رنگ کانتورهای مشخص هسند. حروف P و C و X به صورت کامل جداسازی اما عدد 5 یا 0 نقاط کانتور آن کمی از حاشیه خارج شده است (البته قابل اصلاح میباشد).
Retreival Modes
همانطور که در قسمت قبل دیدیم تابع findContours دو خروجی را در اختیار ما قرار میداد. یکی رابطه کانتورها دیگری نقاط کانتورها. در این قسمت رابطه کانتورها را با توجه ورودی Retreival Modes بررسی خواهیم کرد. خروجی رابطه کانتورها (hierarchy) شامل چهار نقطه میشود:
- اندیس کانتور بعدی
- اندیس کانتور قبلی
- اندیس کانتور والد (parent)
- اندیس کانتور فرزند (child)
با یک مثال این قسمت را بررسی میکنیم:
import cv2
import numpy as np
image = cv2.imread('LP.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
cv2.imshow('After thresholding', th2)
contours, hierarchy = cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
cv2.imshow('Contours overlaid on original image', image)
print("Number of Contours found = " + str(len(contours)))
print(hierarchy)
cv2.waitKey(0)
cv2.destroyAllWindows()
همانطور که گفتیم ورودی RETR_LIST تمامی کانتورها را در اختیار ما قرار میدهد و هیچگونه رابطه بین کانتورها را ذخیرهسازی نمیکند. در کد بالا دو دستور print قرار دادهایم. یکی تعداد کنتورها میباشد و دیگری خروجی رابطه بین کانتورها است. خروجی کنسول به صورت زیر است (خروجی تصویرها مانند قسمت قبل است به همین دلیل در این قسمت نیاورده ایم):
Number of Contours found = 38
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[ 6 4 -1 -1]
[ 7 5 -1 -1]
[ 8 6 -1 -1]
[ 9 7 -1 -1]
[10 8 -1 -1]
[11 9 -1 -1]
[12 10 -1 -1]
[13 11 -1 -1]
[14 12 -1 -1]
[15 13 -1 -1]
[16 14 -1 -1]
[17 15 -1 -1]
[18 16 -1 -1]
[19 17 -1 -1]
[20 18 -1 -1]
[21 19 -1 -1]
[22 20 -1 -1]
[23 21 -1 -1]
[24 22 -1 -1]
[25 23 -1 -1]
[26 24 -1 -1]
[27 25 -1 -1]
[28 26 -1 -1]
[29 27 -1 -1]
[30 28 -1 -1]
[31 29 -1 -1]
[32 30 -1 -1]
[33 31 -1 -1]
[34 32 -1 -1]
[35 33 -1 -1]
[36 34 -1 -1]
[37 35 -1 -1]
[-1 36 -1 -1]]]
تعداد کانتورها 38 عدد میباشد. در ادامه خروجی نقاط کانتورها را میبینید. اگر به 4 عدد هر خروجی دقت کنید میتوانید عدد 1- را در همه آنها مشاهده کنید (تمامی ستونهای سوم و چهارم). بدلیل استفاده از روش RETR_LIST و عدم ذخیره سازی رابطه بین کانتورها این اعداد 1- هستند. همچنین دو ستون اول و دوم مربوط به کانتور بعدی و قبلی میباشد که میتوان آنها را براحتی تشخیص داد.
در زیر خروجی تصویر در روش RETR_EXTERNAL آوردهایم:
خروجی کنسول در این روش به صورت زیر است:
Number of Contours found = 16
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[ 6 4 -1 -1]
[ 7 5 -1 -1]
[ 8 6 -1 -1]
[ 9 7 -1 -1]
[10 8 -1 -1]
[11 9 -1 -1]
[12 10 -1 -1]
[13 11 -1 -1]
[14 12 -1 -1]
[15 13 -1 -1]
[-1 14 -1 -1]]]
تعداد کانتور در این قسمت 16 عدد میباشد. همچنین در این روش بدلیل ذخیره سازی بیرونیترین کانتور دیگر رابطه بین کانتورها ذخیره سازی نمیگردد و خروجی رابطه کانتورها 1- میباشد.
خروجی روش RETR_CCOMP به این صورت است:
خروجی کسنول این روش را با هم بررسی کنیم:
Number of Contours found = 38
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[ 6 4 -1 -1]
[ 7 5 -1 -1]
[ 8 6 -1 -1]
[ 9 7 -1 -1]
[10 8 -1 -1]
[17 9 11 -1]
[12 -1 -1 10]
[13 11 -1 10]
[14 12 -1 10]
[15 13 -1 10]
[16 14 -1 10]
[-1 15 -1 10]
[25 10 18 -1]
[19 -1 -1 17]
[20 18 -1 17]
[21 19 -1 17]
[22 20 -1 17]
[23 21 -1 17]
[24 22 -1 17]
[-1 23 -1 17]
[32 17 26 -1]
[27 -1 -1 25]
[28 26 -1 25]
[29 27 -1 25]
[30 28 -1 25]
[31 29 -1 25]
[-1 30 -1 25]
[35 25 33 -1]
[34 -1 -1 32]
[-1 33 -1 32]
[36 32 -1 -1]
[-1 35 37 -1]
[-1 -1 -1 36]]]
38 کانتور بین برخی از آنها رابطه سلسه مراتبی نیز وجود دارد.
خروجی زیر مربوط به روش RETR_TREE میباشد.
خروجی کنسول نیز به صورت زیر است:
Number of Contours found = 38
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[ 6 4 -1 -1]
[ 7 5 -1 -1]
[ 8 6 -1 -1]
[ 9 7 -1 -1]
[10 8 -1 -1]
[17 9 11 -1]
[12 -1 -1 10]
[13 11 -1 10]
[14 12 -1 10]
[15 13 -1 10]
[16 14 -1 10]
[-1 15 -1 10]
[25 10 18 -1]
[19 -1 -1 17]
[20 18 -1 17]
[21 19 -1 17]
[22 20 -1 17]
[23 21 -1 17]
[24 22 -1 17]
[-1 23 -1 17]
[32 17 26 -1]
[27 -1 -1 25]
[28 26 -1 25]
[29 27 -1 25]
[30 28 -1 25]
[31 29 -1 25]
[-1 30 -1 25]
[35 25 33 -1]
[34 -1 -1 32]
[-1 33 -1 32]
[36 32 -1 -1]
[-1 35 37 -1]
[-1 -1 -1 36]]]
در این روش تمامی رابطه سلسه مراتبی ذخیره سازی گردیده است. البته خروجی این قسمت با روش قبل مشابه هستند. چراکه بسیاری از کانتورها در تصویر ما حداکثر دو سطح دارند به همین دلیل خروجیها مشابهی میبینید. همچنین تعداد کانتور 38 عدد میباشد.
پیشنهاد میکنیم برای درک بهتر این بخش تمامی حالتهای بالا را بر روی یک تصویر تست کرده و خروجی تصویر و کنسول آن را مشاهده نمایید.
Contouring Modes
findContours دو خروجی دارد که خروجی رابطه کانتورها در قسمت قبل بررسی کردیم. در این قسمت خروجی بعدی یعنی نقاط کانتورها را با توجه به ورودی Contouring Modes بررسی میکنیم. در بخش اول CHAIN_APPROX_NONE را بررسی میکنیم:
import cv2
import numpy as np
image = cv2.imread('LP.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
cv2.imshow('After thresholding', th2)
contours, hierarchy = cv2.findContours(th2, cv2.RETR_TREE , cv2.CHAIN_APPROX_NONE)
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
cv2.imshow('Contours overlaid on original image', image)
print("Number of Contours found = " + str(len(contours)))
for c in contours:
print(len(c))
cv2.waitKey(0)
cv2.destroyAllWindows()
انتهای کد بالا یک حلقه for قرار دادهایم و طول هر کانتور را چاپ کردهایم. خروجی کنسول مربوط به این کد به صورت زیر است:
Number of Contours found = 38
len: 87
len: 50
len: 7
len: 1
len: 1
len: 1
len: 1
len: 4
len: 2
len: 1
len: 236
len: 80
len: 6
len: 75
len: 10
len: 4
len: 8
len: 426
len: 6
len: 4
len: 4
len: 10
len: 14
len: 21
len: 17
len: 241
len: 6
len: 14
len: 15
len: 155
len: 14
len: 9
len: 358
len: 4
len: 4
len: 347
len: 272
len: 92
اگر به خروجی دقت کنید هر سطر یک عدد قرار داده شده است. اعداد طول کانتورهای شناسایی شده در تصویر هستند. جالب است برخی کانتورها طول یک دارند یعنی شامل یک نقطه میشوند. میتوان فیلتری بر روی طول کانتورها اعمال کرده و آنهایی که بزرگتر از 100 نقطه هستند را نمایش دهد. پیاده سازی این فیلتر را به شما میسپاریم.
مثال بعد در مورد CHAIN_APPROX_SIMPLE میباشد:
import cv2
import numpy as np
image = cv2.imread('LP.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
cv2.imshow('After thresholding', th2)
contours, hierarchy = cv2.findContours(th2, cv2.RETR_TREE , cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
cv2.imshow('Contours overlaid on original image', image)
print("Number of Contours found = " + str(len(contours)))
for c in contours:
print("len:",len(c))
cv2.waitKey(0)
cv2.destroyAllWindows()
خروجی کنسول کد بالا را مشاهده میکنیم:
Number of Contours found = 38
len: 8
len: 10
len: 7
len: 1
len: 1
len: 1
len: 1
len: 4
len: 2
len: 1
len: 114
len: 40
len: 6
len: 37
len: 6
len: 4
len: 8
len: 172
len: 6
len: 4
len: 4
len: 6
len: 6
len: 15
len: 11
len: 121
len: 6
len: 6
len: 8
len: 57
len: 8
len: 7
len: 170
len: 4
len: 4
len: 164
len: 65
len: 27
اگر به اعداد خروجی بالا نگاه کنید، متوجه میشوید نسبت به حالت قبل نقاط کمتری در هر کانتور ذخیره شده است. دقیقاً CHAIN_APPROX_SIMPLE این کار را انجام میدهد یعنی کانتورها را تقسیم بندی کرده و نقاط انتهایی هر قسمت را ذخیره میکند.