پردازش تصویر در OpenCV
ویژگیهای کانتور (Contour Features)
هدف#
در این مقاله یاد خواهیم گرفت
- ویژگیهای مختلف کانتورها مانند مساحت، محیط، مرکز جرم (centroid)، کادر محاط (bounding box) و غیره را پیدا کنیم
- با توابع متعدد مرتبط با کانتورها آشنا خواهید شد.
۱. ممانها (Moments)#
ممانهای تصویر (Image Moments) به شما کمک میکنند برخی ویژگیها مانند مرکز جرم شیء، مساحت شیء و غیره را محاسبه کنید. صفحه ویکیپدیا درباره ممانهای تصویر را بررسی کنید.
تابع cv.moments() یک دیکشنری از تمام مقادیر ممان محاسبهشده برمیگرداند. نمونه زیر را ببینید:
import numpy as np
import cv2 as cv
img = cv.imread('star.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
ret,thresh = cv.threshold(img,127,255,0)
contours,hierarchy = cv.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv.moments(cnt)
print( M )
از این ممانها میتوانید دادههای مفیدی مانند مساحت، مرکز جرم و غیره استخراج کنید. مرکز جرم (Centroid) از روابط و به دست میآید. این کار را میتوان به صورت زیر انجام داد:
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
۲. مساحت کانتور (Contour Area)#
مساحت کانتور توسط تابع cv.contourArea() یا از طریق ممانها با M['m00'] به دست میآید.
area = cv.contourArea(cnt)
۳. محیط کانتور (Contour Perimeter)#
این ویژگی به نام طول کمان (arc length) نیز شناخته میشود. با استفاده از تابع cv.arcLength() محاسبه میشود. آرگومان دوم مشخص میکند که آیا شکل یک کانتور بسته است (در صورت ارسال True) یا فقط یک منحنی است.
perimeter = cv.arcLength(cnt,True)
۴. تقریب کانتور (Contour Approximation)#
این تابع شکل یک کانتور را به شکل دیگری با تعداد رأس کمتر تقریب میزند، بسته به دقتی که مشخص میکنیم. این پیادهسازی الگوریتم داگلاس-پوکر (Douglas-Peucker) است. برای مشاهده الگوریتم و نمایش آن صفحه ویکیپدیا را بررسی کنید.
برای درک این موضوع، فرض کنید در حال پیدا کردن یک مربع در یک تصویر هستید، اما به دلیل برخی مشکلات در تصویر، یک مربع کامل نگرفتید، بلکه یک «شکل ناقص» دریافت کردید (همانطور که در اولین تصویر زیر نشان داده شده). اکنون میتوانید از این تابع برای تقریب شکل استفاده کنید. در این تابع، آرگومان دوم epsilon نامیده میشود که حداکثر فاصله از کانتور تا کانتور تقریبی است. این یک پارامتر دقت است. برای به دست آوردن خروجی صحیح، انتخاب مناسب epsilon ضروری است.
epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)
در زیر، در تصویر دوم، خط سبز منحنی تقریبی را برای epsilon = ۱۰٪ طول کمان نشان میدهد. تصویر سوم همین را برای epsilon = ۱٪ طول کمان نشان میدهد. آرگومان سوم مشخص میکند که منحنی بسته است یا خیر.

۵. پوش محدب (Convex Hull)#
پوش محدب (Convex Hull) شبیه به تقریب کانتور به نظر میرسد، اما چنین نیست (هر دو در برخی موارد ممکن است نتایج یکسانی ارائه دهند). در اینجا، تابع cv.convexHull() یک منحنی را از نظر نقصهای تحدب (convexity defects) بررسی کرده و آن را اصلاح میکند. به طور کلی، منحنیهای محدب منحنیهایی هستند که همیشه به بیرون برآمدهاند، یا حداقل صاف هستند. اگر به داخل برجسته باشد، نقص تحدب نامیده میشود. برای مثال، تصویر دست زیر را بررسی کنید. خط قرمز پوش محدب دست را نشان میدهد. فلشهای دو طرفه نقصهای تحدب را نشان میدهند که حداکثر انحرافات موضعی پوش از کانتورها هستند.

چند نکته درباره سینتکس آن وجود دارد که باید توضیح داده شود:
hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]])
جزئیات آرگومانها:
- points کانتورهایی هستند که ارسال میکنیم.
- hull خروجی است که معمولاً از آن صرفنظر میکنیم.
- clockwise : پرچم جهت (Orientation flag). اگر True باشد، پوش محدب خروجی در جهت عقربههای ساعت جهتگیری میشود. در غیر این صورت، در جهت خلاف عقربههای ساعت جهتگیری میشود.
- returnPoints : به طور پیشفرض True است. سپس مختصات نقاط پوش را برمیگرداند. اگر False باشد، ایندکسهای نقاط کانتور متناظر با نقاط پوش را برمیگرداند.
بنابراین برای به دست آوردن پوش محدب مانند تصویر بالا، کد زیر کافی است:
hull = cv.convexHull(cnt)
اما اگر میخواهید نقصهای تحدب را پیدا کنید، باید returnPoints = False را ارسال کنید. برای درک آن، تصویر مستطیل بالا را در نظر میگیریم. ابتدا کانتور آن را به صورت cnt پیدا کردم. حالا پوش محدب آن را با returnPoints = True پیدا کردم و مقادیر زیر را به دست آوردم: [[[234 202]], [[ 51 202]], [[ 51 79]], [[234 79]]] که چهار نقطه گوشه مستطیل هستند. حالا اگر همین کار را با returnPoints = False انجام دهم، نتیجه زیر را میگیرم: [[129],[ 67],[ 0],[142]]. اینها ایندکسهای نقاط متناظر در کانتورها هستند. برای مثال، اولین مقدار را بررسی کنید: cnt[129] = [[234, 202]] که همان نتیجه اول است (و به همین ترتیب برای بقیه).
در ادامه که درباره نقصهای تحدب بحث میکنیم، دوباره آن را خواهید دید.
۶. بررسی تحدب (Checking Convexity)#
تابعی برای بررسی اینکه آیا یک منحنی محدب است یا نه وجود دارد: cv.isContourConvex(). این تابع فقط True یا False برمیگرداند. چیز مهمی نیست.
k = cv.isContourConvex(cnt)
۷. مستطیل محاط (Bounding Rectangle)#
دو نوع مستطیل محاط وجود دارد.
7.a. مستطیل محاط مستقیم (Straight Bounding Rectangle)#
این یک مستطیل مستقیم است که چرخش شیء را در نظر نمیگیرد. بنابراین مساحت مستطیل محاط حداقل نخواهد بود. با تابع cv.boundingRect() پیدا میشود.
فرض کنید (x,y) مختصات گوشه بالا-چپ مستطیل و (w,h) عرض و ارتفاع آن باشد.
x,y,w,h = cv.boundingRect(cnt)
cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
7.b. مستطیل دوار (Rotated Rectangle)#
در اینجا، مستطیل محاط با حداقل مساحت رسم میشود، بنابراین چرخش را نیز در نظر میگیرد. تابع مورد استفاده cv.minAreaRect() است. این تابع یک ساختار Box2D برمیگرداند که شامل جزئیات زیر است - ( مرکز (x,y)، (عرض، ارتفاع)، زاویه چرخش). اما برای رسم این مستطیل، به ۴ گوشه مستطیل نیاز داریم. این مقادیر با تابع cv.boxPoints() به دست میآیند.
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)
هر دو مستطیل در یک تصویر نشان داده شدهاند. مستطیل سبز مستطیل محاط معمولی را نشان میدهد. مستطیل قرمز مستطیل دوار است.

۸. کوچکترین دایره محاط (Minimum Enclosing Circle)#
در مرحله بعد، با استفاده از تابع cv.minEnclosingCircle() محیطدایره (circumcircle) یک شیء را پیدا میکنیم. این یک دایره است که شیء را با حداقل مساحت به طور کامل پوشش میدهد.
(x,y),radius = cv.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
cv.circle(img,center,radius,(0,255,0),2)

۹. برازش بیضی (Fitting an Ellipse)#
مرحله بعدی برازش یک بیضی بر روی شیء است. این تابع مستطیل دواری را برمیگرداند که بیضی درون آن محاط شده است.
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img,ellipse,(0,255,0),2)

۱۰. برازش خط (Fitting a Line)#
به همین ترتیب، میتوانیم یک خط را به مجموعهای از نقاط برازش دهیم. تصویر زیر حاوی مجموعهای از نقاط سفید است. میتوانیم یک خط مستقیم را به آن تقریب بزنیم.
rows,cols = img.shape[:2]
[vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
cv.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)
