پردازش تصویر در OpenCV
استخراج تعاملی پیشزمینه با استفاده از الگوریتم GrabCut
هدف#
در این فصل
- الگوریتم GrabCut را برای استخراج پیشزمینه در تصاویر خواهیم دید.
- یک برنامه تعاملی برای این منظور ایجاد خواهیم کرد.
تئوری#
الگوریتم GrabCut توسط Carsten Rother، Vladimir Kolmogorov و Andrew Blake از Microsoft Research Cambridge، انگلستان در مقاله خود با عنوان «GrabCut»: استخراج تعاملی پیشزمینه با استفاده از برشهای گراف تکراری طراحی شد. به الگوریتمی با حداقل تعامل کاربر برای استخراج پیشزمینه نیاز بود و نتیجه آن GrabCut شد.
از دیدگاه کاربر چگونه کار میکند؟ در ابتدا کاربر یک مستطیل دور ناحیه پیشزمینه رسم میکند (ناحیه پیشزمینه باید کاملاً داخل مستطیل باشد). سپس الگوریتم به صورت تکراری آن را بخشبندی میکند تا بهترین نتیجه را به دست آورد. تمام. اما در برخی موارد بخشبندی دقیق نخواهد بود، مثلاً ممکن است برخی نواحی پیشزمینه به عنوان پسزمینه علامتگذاری شده باشند و برعکس. در این صورت کاربر باید دستکاریهای ظریف انجام دهد. فقط روی تصویر، در جایی که نتایج نادرست وجود دارد، چند ضربه قلم بزنید. این ضربههای قلم اساساً میگویند «این ناحیه باید پیشزمینه باشد، شما آن را پسزمینه علامت زدید، در تکرار بعدی آن را اصلاح کنید» یا عکس آن برای پسزمینه. سپس در تکرار بعدی نتایج بهتری خواهید داشت.
تصویر زیر را ببینید. ابتدا بازیکن و توپ فوتبال در یک مستطیل آبی محصور شدهاند. سپس برخی دستکاریهای نهایی با ضربههای سفید (نشاندهنده پیشزمینه) و ضربههای سیاه (نشاندهنده پسزمینه) انجام شده است. و نتیجه خوبی به دست میآوریم.

پس در پسزمینه چه اتفاقی میافتد؟
- کاربر مستطیل را مشخص میکند. همه چیز خارج از این مستطیل به عنوان پسزمینه قطعی در نظر گرفته میشود (به همین دلیل قبلاً ذکر شد که مستطیل باید شامل تمام اشیاء باشد). هر چیزی داخل مستطیل ناشناخته است. به همین ترتیب هر ورودی کاربر که پیشزمینه و پسزمینه را مشخص میکند به عنوان برچسبگذاری سخت (hard-labelling) در نظر گرفته میشود، یعنی در طول فرآیند تغییر نمیکنند.
- کامپیوتر یک برچسبگذاری اولیه بر اساس دادهای که دادیم انجام میدهد. پیکسلهای پیشزمینه و پسزمینه را برچسب میزند (یا آنها را به صورت سخت برچسبگذاری میکند).
- حالا یک مدل ترکیب گاوسی (Gaussian Mixture Model - GMM) برای مدلسازی پیشزمینه و پسزمینه استفاده میشود.
- بسته به دادهای که دادیم، GMM یاد میگیرد و توزیع پیکسلهای جدیدی ایجاد میکند. یعنی پیکسلهای ناشناخته بر اساس رابطهشان با پیکسلهای برچسبگذاریشده سخت از نظر آمار رنگ، یا به عنوان پیشزمینه احتمالی یا پسزمینه احتمالی برچسب میخورند (این شبیه به خوشهبندی است).
- از این توزیع پیکسلها یک گراف ساخته میشود. گرههای گراف پیکسلها هستند. دو گره اضافی اضافه میشوند: گره منبع (Source node) و گره مقصد (Sink node). هر پیکسل پیشزمینه به گره منبع و هر پیکسل پسزمینه به گره مقصد متصل است.
- وزنهای یالهایی که پیکسلها را به گره منبع/مقصد متصل میکنند بر اساس احتمال پیشزمینه/پسزمینه بودن پیکسل تعریف میشوند. وزنهای بین پیکسلها بر اساس اطلاعات لبه یا شباهت پیکسلها تعریف میشوند. اگر تفاوت زیادی در رنگ پیکسلها باشد، یال بین آنها وزن کمی خواهد داشت.
- سپس یک الگوریتم برش کمینه (mincut) برای بخشبندی گراف استفاده میشود. این الگوریتم گراف را به دو قسمت جدا میکند که گره منبع و گره مقصد را با کمترین تابع هزینه از هم جدا میکند. تابع هزینه مجموع وزنهای تمام یالهایی است که برش میخورند. پس از برش، تمام پیکسلهای متصل به گره منبع پیشزمینه و آنهایی که به گره مقصد متصلند پسزمینه میشوند.
- این فرآیند تا زمانی که طبقهبندی همگرا شود ادامه مییابد.
این موضوع در تصویر زیر نشان داده شده است (منبع تصویر: http://www.cs.ru.ac.za/research/g02m1682/)

نمایش عملی#
حالا به سراغ الگوریتم GrabCut با OpenCV میرویم. OpenCV تابع cv.grabCut() را برای این منظور دارد. ابتدا آرگومانهای آن را میبینیم:
- img - تصویر ورودی
- mask - این یک تصویر ماسک است که در آن مشخص میکنیم کدام نواحی پسزمینه، پیشزمینه یا پسزمینه/پیشزمینه احتمالی هستند. این کار با پرچمهای زیر انجام میشود: cv.GC_BGD، cv.GC_FGD، cv.GC_PR_BGD، cv.GC_PR_FGD، یا به سادگی اعداد 0، 1، 2، 3 را به تصویر ارسال کنید.
- rect - مختصات مستطیلی است که شامل شیء پیشزمینه است به فرمت (x,y,w,h)
- bdgModel، fgdModel - اینها آرایههایی هستند که الگوریتم به صورت داخلی از آنها استفاده میکند. فقط دو آرایه صفر از نوع np.float64 با اندازه (1,65) ایجاد کنید.
- iterCount - تعداد تکرارهایی که الگوریتم باید اجرا شود.
- mode - باید cv.GC_INIT_WITH_RECT یا cv.GC_INIT_WITH_MASK یا ترکیب آنها باشد که تعیین میکند آیا در حال رسم مستطیل هستیم یا ضربههای دستکاری نهایی.
ابتدا با حالت مستطیلی (rectangular mode) شروع میکنیم. تصویر را بارگذاری میکنیم، یک تصویر ماسک مشابه ایجاد میکنیم. fgdModel و bgdModel را ایجاد میکنیم. پارامترهای مستطیل را مشخص میکنیم. همه چیز ساده است. بگذارید الگوریتم ۵ تکرار اجرا کند. حالت باید cv.GC_INIT_WITH_RECT باشد چرا که از مستطیل استفاده میکنیم. سپس grabcut را اجرا کنید. این تابع تصویر ماسک را تغییر میدهد. در تصویر ماسک جدید، پیکسلها با چهار پرچم مشخص میشوند که پسزمینه/پیشزمینه را همانطور که در بالا ذکر شد نشان میدهند. پس ماسک را به گونهای تغییر میدهیم که تمام پیکسلهای ۰ و ۲ به ۰ تبدیل شوند (یعنی پسزمینه) و تمام پیکسلهای ۱ و ۳ به ۱ تبدیل شوند (یعنی پیکسلهای پیشزمینه). حالا ماسک نهایی ما آماده است. فقط آن را در تصویر ورودی ضرب کنید تا تصویر بخشبندیشده را بدست آورید.
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
mask = np.zeros(img.shape[:2],np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
rect = (50,50,450,290)
cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
نتایج زیر را ببینید:

اوه، موهای مسی از بین رفته است. چه کسی مسی را بدون موهایش دوست دارد؟ باید آنها را برگردانیم. پس آنجا را با ۱-پیکسل (پیشزمینه قطعی) دستکاری ظریف خواهیم کرد. در همان حال، بخشی از زمین در تصویر آمده که ما نمیخواهیم، و همچنین برخی لوگوها. باید آنها را حذف کنیم. آنجا چند ۰-پیکسل دستکاری (پسزمینه قطعی) میدهیم. پس ماسک نتیجه قبلی را همانطور که گفتیم تغییر میدهیم.
کاری که در واقع انجام دادم این بود که تصویر ورودی را در برنامه نقاشی (paint) باز کردم و یک لایه دیگر به تصویر اضافه کردم. با استفاده از ابزار قلم در برنامه نقاشی، پیشزمینههای از دست رفته (مو، کفش، توپ و غیره) را با سفید و پسزمینههای ناخواسته (مانند لوگو، زمین و غیره) را با سیاه روی این لایه جدید علامتگذاری کردم. سپس پسزمینه باقیمانده را با خاکستری پر کردم. سپس آن تصویر ماسک را در OpenCV بارگذاری کردم، تصویر ماسک اصلی که به دست آوردیم را با مقادیر متناظر در تصویر ماسک جدید ویرایش کردم. کد زیر را بررسی کنید:
# newmask is the mask image I manually labelled
newmask = cv.imread('newmask.png', cv.IMREAD_GRAYSCALE)
assert newmask is not None, "file could not be read, check with os.path.exists()"
# wherever it is marked white (sure foreground), change mask=1
# wherever it is marked black (sure background), change mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
نتیجه زیر را ببینید:

این بود. در اینجا به جای اینکه در حالت rect شروع کنید، میتوانید مستقیماً به حالت mask بروید. فقط ناحیه مستطیل را در تصویر ماسک با ۲-پیکسل یا ۳-پیکسل (پسزمینه/پیشزمینه احتمالی) علامتگذاری کنید. سپس sure_foreground ما را با ۱-پیکسل مانند کاری که در مثال دوم انجام دادیم علامت بزنید. سپس مستقیماً تابع grabCut را با حالت mask اعمال کنید.
تمرینها#
- نمونههای OpenCV شامل یک نمونه grabcut.py است که ابزاری تعاملی با استفاده از grabcut است. آن را بررسی کنید. همچنین این ویدیوی یوتیوب را تماشا کنید که نحوه استفاده از آن را نشان میدهد.
- اینجا میتوانید این را به یک نمونه تعاملی تبدیل کنید که با موس مستطیل و ضربههای قلم رسم میکند، یک trackbar برای تنظیم عرض ضربه قلم ایجاد کنید و غیره.