
فیلترهای فتوشاپ در OpenCV
فیلترهای عکس همیشه یکی از محبوبترین و جالبترین برنامههای پردازش عکس بوده و هستند. و چه برنامهای بهتر از فتوشاپ برای طراحی فیلترهای عکس! در این نوشتار به فیلترهای فتوشاپ در OpenCV خواهیم پرداخت.
یکی از ویژگیهای جالب فیلترهای فتوشاپ در OpenCV وجود trackbar ها هستند که میتوان با آنها شدت فیلترها را کنترل کرد و موجب میشود جلوه (effect) عالی و مناسبی بر روی عکس اجرا شود. فیلترهای فتوشاپ در OpenCV که در این مقاله اجرا خواهیم کرد، عبارتند از:
- Brightness (روشنایی)
- 60’s TV )تلویزیون دهه 60)
- Emboss (برجسته کردن)
- Duo-Tone (جلوههای رنگی دوتایی)
- Sepia (رنگ سوبیایی)
1- روشنایی
منظور از روشنایی شدت نور است. با استفاده از کانال رنگی HSV میتوان روشنایی را در تصاویر دستکاری کرد. اگر خلاصه بخواهیم بگوییم دومین و سومین کانال رنگی HSV:
- بُعد saturation مشابه رنگهای روشن مختلف است. بُعد Value ترکیبی از این رنگها با مقادیر مختلفی از رنگهای سیاه و سفید شباهت است.
معنای جمله فوق در خودِ جمله نهفته است و به ما نشان میدهد چرا استفاده از کانال رنگی HSV برای این مسئله گزینهای عالی و فوقالعاده است. همانگونه که در تصویر مقابل مشاهده میکنید همزمان با افزایش معیارهای Saturation و Value از چپ به راست، جلوهها هم تغییر میکنند.

به تصویر بالا نگاه کنید. همزمان با اینکه مقادیر این معیارها را افزایش میدهیم، رنگها روشنتر میشوند. برای ویرایش روشنایی و روشنتر کردن تصویر، مقادیر Saturation و Value را افزایش دهید.
کد پایتون:
def brightness(img): cv2.namedWindow('image') cv2.createTrackbar('val', 'image', 100, 150, nothing) while True: hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hsv = np.array(hsv, dtype=np.float64) val = cv2.getTrackbarPos('val', 'image') val = val/100 # dividing by 100 to get in range 0-1.5 # scale pixel values up or down for channel 1(Saturation) hsv[:, :, 1] = hsv[:, :, 1] * val hsv[:, :, 1][hsv[:, :, 1] > 255] = 255 # setting values > 255 to 255. # scale pixel values up or down for channel 2(Value) hsv[:, :, 2] = hsv[:, :, 2] * val hsv[:, :, 2][hsv[:, :, 2] > 255] = 255 # setting values > 255 to 255. hsv = np.array(hsv, dtype=np.uint8) res = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) cv2.imshow("original", img) cv2.imshow('image', res) if cv2.waitKey(1) & 0xFF == ord('q'): break cv2.destroyAllWindows()
کد C++:
#include <iostream> #include <string> #include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp> using namespace std; using namespace cv; void nothing(int x, void* data) {} void brightness(Mat img) { namedWindow("image"); int slider = 100; createTrackbar("val","image",&slider,150,nothing); Mat hsv; while (true) { cvtColor(img, hsv, COLOR_BGR2HSV); float val = getTrackbarPos("val","image"); val=val/100.0; Mat channels[3]; split(hsv,channels); Mat H = channels[0]; H.convertTo(H,CV_32F); Mat S = channels[1]; S.convertTo(S,CV_32F); Mat V = channels[2]; V.convertTo(V,CV_32F); for (int i=0; i < H.size().height; i++){ for (int j=0; j < H.size().width; j++){ // scale pixel values up or down for channel 1(Saturation) S.at<float>(i,j) *= val; if (S.at<float>(i,j) > 255) S.at<float>(i,j) = 255; // scale pixel values up or down for channel 2(Value) V.at<float>(i,j) *= val; if (V.at<float>(i,j) > 255) V.at<float>(i,j) = 255; } } H.convertTo(H,CV_8U); S.convertTo(S,CV_8U); V.convertTo(V,CV_8U); vector<Mat> hsvChannels{H,S,V}; Mat hsvNew; merge(hsvChannels,hsvNew); Mat res; cvtColor(hsvNew,res,COLOR_HSV2BGR); imshow("original",img); imshow("image",res); if (waitKey(1) == 'q') break; } destroyAllWindows(); }
در کد فوق، تصویر را به یک فضای رنگی HSV تبدیل کردیم که نوع داده آن float.64 است. فقط به این دلیل تصویر را تبدیل میکنیم که مطمئن شویم حاصل ضرب دو عدد اعشاری در هم خطایی نخواهد داشت. با اجرای تابع cv2.createTrackbar میتوانیم track bar ایجاد کنیم. با استفاده از track bar میتوانیم مقدار سطوح روشنایی را کنترل کنیم. تابع track bar عدد اعشاری به دست نمیدهد. با این حال در این تابع از اعداد بزرگ استفاده میشود و این اعداد بر 100 تقسیم میشوند.
نکته: این فرایند برای تمامی فیلترها یکسان است و میتوان آن را بر روی سایر فیلترها اجرا کرد.
سپس مقدار به دست آمده در ماتریسها ضرب میشود. در نتیجه سقف مجاز رقم برابر با 255 خواهد بود. در گام نهایی مجدداً ماتریسها را به RGB و uint8 تبدیل میکنیم.
تماشای ویدیو از طریق آدرس زیر:
https://www.youtube.com/watch?v=H4JCtL_l4w8&feature=youtu.be
2– (تلویزیون دهه 60)
خب من هیچ کدام از برنامههای تلویزیونی دهه 60 را تماشا نکردهام. ولی فکر میکنم برنامههای تلویزیونی آن زمان سیاه و سفید بودند و نویز و برفک داشتهاند. البته من تنها کسی نیستم که این عقیده را دارم. نسخه موبایلی Adobe Photoshop هم حرف مرا تأئید میکند.

سادهترین راه اضافه کردن نویز salt and pepper به عکس است. اگر این نویز را بر روی عکس اعمال کنیم، پیکسلهای عکس تغییر میکنند. پس از آن میتوانیم یک مقدار به پیکسل اضافه کنیم و یا یک مقدار از آن کم کنیم (که البته یک مقدار خاص و مشخص خواهد بود). این مقدار مشتقی از مقدار اصلی خواهد بود.
کد پایتون:
def tv_60(img): cv2.namedWindow('image') cv2.createTrackbar('val', 'image', 0, 255, nothing) cv2.createTrackbar('threshold', 'image', 0, 100, nothing) while True: height, width = img.shape[:2] gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) thresh = cv2.getTrackbarPos('threshold', 'image') val = cv2.getTrackbarPos('val', 'image') for i in range(height): for j in range(width): if np.random.randint(100) <= thresh: if np.random.randint(2) == 0: gray[i, j] = min(gray[i, j] + np.random.randint(0, val+1), 255) # adding noise to image and setting values > 255 to 255. else: gray[i, j] = max(gray[i, j] - np.random.randint(0, val+1), 0) # subtracting noise to image and setting values < 0 to 0. cv2.imshow('Original', img) cv2.imshow('image', gray) if cv2.waitKey(1) & 0xFF == ord('q'): break cv2.destroyAllWindows()
کد C++:
void tv_60(Mat img) { namedWindow("image"); int slider = 0; int slider2 = 0; createTrackbar("val","image",&slider,255,nothing); createTrackbar("threshold","image",&slider2,100,nothing); while (true) { int height = img.size().height; int width = img.size().width; Mat gray; cvtColor(img, gray, COLOR_BGR2GRAY); float thresh = getTrackbarPos("threshold","image"); float val = getTrackbarPos("val","image"); for (int i=0; i < height; i++){ for (int j=0; j < width; j++){ if (rand()%100 <= thresh){ if (rand()%2 == 0) gray.at<uchar>(i,j) = std::min(gray.at<uchar>(i,j) + rand()%((int)val+1), 255); else gray.at<uchar>(i,j) = std::max(gray.at<uchar>(i,j) - rand()%((int)val+1), 0); } } } imshow("original",img); imshow("image",gray); if (waitKey(1) == 'q') break; } destroyAllWindows(); }
در گام اول عکس، سیاه و سفید میشود. در این فرایند از دو track bar کشویی استفاده میکنیم. در اولین track bar حداکثر تعداد نویزی که باید به عکس اضافه شود و یا از عکس کم شود، ذخیره میشود. دومین track bar نشاندهنده درصد پیکسلهایی است که نویز بر روی آنها اجرا میشود. در زمان جمع و تفریق، اگر مقدار یک پیکس بیشتر از 255 و یا کمتر از 0 باشد، به ترتیب بر روی 255 یا 0 تنظیم میشود.
تماشای ویدیو از طریق آدرس زیر:
https://youtu.be/GY-FVfwABr0
3– برجسته سازی
فیلتر برجسته سازی موجب میشود قسمتی از شی برجستهتر سایر بخشهای آن شود. طبق تعریفی که ویکیپدیا از آن ارائه داده:
برجسته کردن عکس یک تکنیک گرافیکی است که در آن هر یک از پیکسلهای عکس یا برجسته میشوند و یا سایه میخورند (که بستگی به مرزهای تاریکی و روشنایی عکس اصلی دارد). نواحی که شفافیت پایین دارند با یک پسزمینه خاکستری جایگزین میشوند.
اعمال این فیلتر در OpenCV ساده است. در OpenCV با استفاده از کرنلهای خاصی میتوان این فیلتر را اجرا کرد. البته بسته به اندازه کرنلها، جهت فیلتر برجسته سازی متغیر است.

ایجاد یک کرنل کافی است. با دوران آن به اندازه کافی کرنل به دست میآوریم. تغییر اندازه کرنل بر شدت برجستگی تأثیر میگذارد. هرچه اندازه کرنل بیشتر باشد، تأثیر برجستگی هم بزرگتر خواهد بود. توجه داشته باشید که حداقل اندازه کرنل 2×2 است.
کد پایتون:
def kernel_generator(size): kernel = np.zeros((size, size), dtype=np.int8) for i in range(size): for j in range(size): if i < j: kernel[i][j] = -1 elif i > j: kernel[i][j] = 1 return kernel def emboss(img): cv2.namedWindow('image') cv2.createTrackbar('size', 'image', 0, 8, nothing) switch = '0 : BL n1 : BR n2 : TR n3 : BR' cv2.createTrackbar(switch, 'image', 0, 3, nothing) while True: size = cv2.getTrackbarPos('size', 'image') size += 2 # adding 2 to kernel as it a size of 2 is the minimum required. s = cv2.getTrackbarPos(switch, 'image') height, width = img.shape[:2] y = np.ones((height, width), np.uint8) * 128 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) kernel = kernel_generator(size) # generating kernel for bottom left kernel kernel = np.rot90(kernel, s) # switching kernel according to direction res = cv2.add(cv2.filter2D(gray, -1, kernel), y) cv2.imshow('Original', img) cv2.imshow('image', res) if cv2.waitKey(1) & 0xFF == ord('q'): break cv2.destroyAllWindows()
کد C++:
Mat kernel_generator(int size){ Mat kernel = Mat(size,size,CV_8S,Scalar(0)); for (int i=0; i<size; i++){ for (int j=0; j<size; j++){ if (i < j){ kernel.at<schar>(i,j) = -1; } else if (i > j){ kernel.at<schar>(i,j) = 1; } } } return kernel; } void emboss(Mat img){ namedWindow("image"); int slider = 0; int slider2 = 0; createTrackbar("size","image",&slider,8,nothing); createTrackbar("0 : BL n1 : BR n2 : TR n3 : BR","image",&slider2,3,nothing); while (true){ int size = getTrackbarPos("size","image"); size += 2; int s = getTrackbarPos("0 : BL n1 : BR n2 : TR n3 : BR","image"); int height = img.size().height; int width = img.size().width; Mat y = Mat(height,width,CV_8U,Scalar(128)); Mat gray; cvtColor(img,gray,COLOR_BGR2GRAY); Mat kernel = kernel_generator(size); for (int i=0; i<s; i++) rotate(kernel,kernel,ROTATE_90_COUNTERCLOCKWISE); Mat dst; filter2D(gray,dst,-1,kernel); Mat res; add(dst,y,res); imshow("Original",img); imshow("image",res); if (waitKey(1) == 'q') break; } destroyAllWindows(); }
همانگونه که پیشتر نیز گفتیم، در این فرایند از دو track bar کشویی استفاده میکنیم. یک track bar اندازه کرنل را کنترل میکند. دومین track bar جهتی را که فیلتر برجسته سازی روی آن اعمال میشود را کنترل میکند. سپس یک ماسک اضافی (روی متغیر y) ایجاد میشود که همه مقادیر آن 128 هستند تا یک پسزمینه خاکستری ایجاد کند.
مولد کرنل یک نوع کرنل پایین ایجاد میکند. فرض کنید i ردیفها و j ستونها را نشان میدهد. سپس برای تمام مقدار 1 و برای تمامی مقدار 1- را مشخص میکنیم و زمانی که برابر باشند از 0 استفاده میکنیم. برای ایجاد یک کرنل پایین-راست (bottom-right) باید یک بار عکس را در جهت خلاف عقریههای ساعت بچرخانیم. برای ایجاد کرنل بالا-راست (top-right) عکس را دوبار و برای ایجاد کرنل بالا-چپ (top-left) عکس را سه بار دوران می دهیم. مقادیر گزینهای (switch) که نوع کرنل را کنترل میکند با استفاده از np.rot90 تنظیم میشوند، به نحوی که یک ماتریس را در جهت خلاف عقربههای ساعت میچرخاند، ماتریس مورد نیاز را در اختیار ما میگذارد.
با استفاده از cv2.filter2D هم کرنل با نسخه سیاه و سفید عکس ورودی میچرخد. آرگومان میانی عمق مورد نیاز برای ماتریس خروجی را نشان میدهد. در این حالت اگر یک مقدار منفی برای مثال 1- تعیین شود، عمق ثابت باقی میماند و تغییر نمیکند.
تماشای و.یدئو از طریق آدرس زیر:
https://youtu.be/t3j5ujVlZ84
سمت چپ: تصویر اصلی / سمت راست: نتیجه اعمال فیلتر Emboss بر روی عکس. عکس از Anastase Maragos در Unsplash
4- Duo-Tone
همانند فیلتر 60s TV این فیلتر هم از Adobe Photoshop الهام گرفته شده است. این فیلتر چه کاری انجام میدهد؟
همانگونه که در gif فوق مشاهده میکنید، فیلتر duo-tone یک سایه رنگی بر روی عکس اعمال میکند. برای به دست آوردن این جلوه، کانال رنگی گزینهای برای افزایش مقدار خواهد داشت. در نتیجه افزایش مقدار هم سایه روشنتر خواهد شد. مقادیر سایر کانالها را میتوان کاهش داد و یا بر روی صفر تنظیم کرد تا یک سایه روشن یا یکدست بر روی تصویر اعمال شود.
کد پایتون:
def exponential_function(channel, exp): table = np.array([min((i**exp), 255) for i in np.arange(0, 256)]).astype("uint8") # generating table for exponential function channel = cv2.LUT(channel, table) return channel def duo_tone(img): cv2.namedWindow('image') cv2.createTrackbar('exponent', 'image', 0, 10, nothing) switch1 = '0 : BLUE n1 : GREEN n2 : RED' cv2.createTrackbar(switch1, 'image', 1, 2, nothing) switch2 = '0 : BLUE n1 : GREEN n2 : RED n3 : NONE' cv2.createTrackbar(switch2, 'image', 3, 3, nothing) switch3 = '0 : DARK n1 : LIGHT' cv2.createTrackbar(switch3, 'image', 0, 1, nothing) while True: exp = cv2.getTrackbarPos('exponent', 'image') exp = 1 + exp/100 # converting exponent to range 1-2 s1 = cv2.getTrackbarPos(switch1, 'image') s2 = cv2.getTrackbarPos(switch2, 'image') s3 = cv2.getTrackbarPos(switch3, 'image') res = img.copy() for i in range(3): if i in (s1, s2): # if channel is present res[:, :, i] = exponential_function(res[:, :, i], exp) # increasing the values if channel selected else: if s3: # for light res[:, :, i] = exponential_function(res[:, :, i], 2 - exp) # reducing value to make the channels light else: # for dark res[:, :, i] = 0 # converting the whole channel to 0 cv2.imshow('Original', img) cv2.imshow('image', res) if cv2.waitKey(1) & 0xFF == ord('q'): break cv2.destroyAllWindows()
کد C++:
Mat exponential_function(Mat channel, float exp){ Mat table(1, 256, CV_8U); for (int i = 0; i < 256; i++) table.at<uchar>(i) = min((int)pow(i,exp),255); LUT(channel,table,channel); return channel; } void duo_tone(Mat img){ namedWindow("image"); int slider1 = 0; int slider2 = 1; int slider3 = 3; int slider4 = 0; string switch1 = "0 : BLUE n1 : GREEN n2 : RED"; string switch2 = "0 : BLUE n1 : GREEN n2 : RED n3 : NONE"; string switch3 = "0 : DARK n1 : LIGHT"; createTrackbar("exponent","image",&slider1,10,nothing); createTrackbar(switch1,"image",&slider2,2,nothing); createTrackbar(switch2,"image",&slider3,3,nothing); createTrackbar(switch3,"image",&slider4,1,nothing); while(true){ int exp1 = getTrackbarPos("exponent","image"); float exp = 1 + exp1/100.0; int s1 = getTrackbarPos(switch1,"image"); int s2 = getTrackbarPos(switch2,"image"); int s3 = getTrackbarPos(switch3,"image"); Mat res = img.clone(); Mat channels[3]; split(img,channels); for (int i=0; i<3; i++){ if ((i == s1)||(i==s2)){ channels[i] = exponential_function(channels[i],exp); } else{ if (s3){ channels[i] = exponential_function(channels[i],2-exp); } else{ channels[i] = Mat::zeros(channels[i].size(),CV_8UC1); } } } vector<Mat> newChannels{channels[0],channels[1],channels[2]}; merge(newChannels,res); imshow("Original",img); imshow("image",res); if (waitKey(1) == 'q') break; } destroyAllWindows(); }
از اولین track bar برای به دست آوردن یک مقدار بین 0 تا 10 استفاده میکنیم. سپس مقادیر نرمالسازی میشوند تا بین 0 و 1 باشند. سپس مقدار 1 اضافه میشود که یک مقدار نمایی خواهد بود. سپس تابع نمایی با استفاده از cv2.LUT بر روی عکس اجرا میشود. این تابع یک کانال را دریافت میکند و با استفاده از یک جدول مراجعه (lookup table) ان را تبدیل میکند. سپس سه گزینه ایجاد میشوند و مقادیر آبی، سبز و قرمز به آنها اختصاص داده میشود. البته گزینه دوم یک گزینه اضافی none هم دارد. گزینه سوم مشخص میکند که آیا حالت تاریک باید استفاده شود یا حالت روشن. تمامی این گزینهها این امکان را برای ما فراهم میکنند تا از میان 6 پالت رنگی مختلف یکی را انتخاب و بر روی عکس اعمال کنیم.
با توجه به رنگی که کاربر انتخاب کرده، مقادیر آن بر اساس تابع نمایی افزایش مییابند. در غیر این صورت مقدار بر روی صفر تنظیم میشود و کاهش مییابد.
تماشای ویدئو از طریق آدرس زیر:
https://youtu.be/eN2KlZVx5RE
5– Sepia (رنگ سوبیایی)
فیلتر sepia رنگ عکس اصلی را به رنگ قرمز متمایل به قهوهای (سوبیایی) تغییر میدهد. با استفاده از این فیلتر میتوان ظاهر نمای کلی تصویر را تغییر دارد. فیلتر Sepia فیلتر رایج و متدوالی است و تقریباً در تمامی نرمافزارهای ویرایش عکس یافت میشود.
اجرای این فیلتر در OpenCV آسان است. رنگ این فیلتر ثابت است و تغییر نمیکند. علاوه بر این، این فیلتر یک ماتریس استانداردسازیشده دارد که میتوان به عنوان پیشفرض از آن استفاده کرد. البته توجه داشته باشید که OpenCV از فرمت رنگی BGR استفاده میکند، اما ماتریسی که به صورت آنلاین در دسترس شما قرار میگیرد برای فضای رنگی RGB است. بنابراین باید آن را به RGB تبدیل کنیم و پس از پردازش و پیش از نمایش دادن تصویر، آن را مجدداً به فرمت BGR برگردانیم.
کد پایتون:
def sepia(img): res = img.copy() res = cv2.cvtColor(res, cv2.COLOR_BGR2RGB) # converting to RGB as sepia matrix is for RGB res = np.array(res, dtype=np.float64) res = cv2.transform(res, np.matrix([[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]])) res[np.where(res > 255)] = 255 # clipping values greater than 255 to 255 res = np.array(res, dtype=np.uint8) res = cv2.cvtColor(res, cv2.COLOR_RGB2BGR) cv2.imshow("original", img) cv2.imshow("Sepia", res) cv2.waitKey(0) cv2.destroyAllWindows()
کد C++:
void sepia(Mat img){ Mat res = img.clone(); cvtColor(res,res,COLOR_BGR2RGB); transform(res,res,Matx33f(0.393,0.769,0.189, 0.349,0.686,0.168, 0.272,0.534,0.131)); cvtColor(res,res,COLOR_RGB2BGR); imshow("original",img); imshow("Sepia",res); waitKey(0); destroyAllWindows(); }
پیش از اینکه از cv2.transform استفاده کنیم تا جلوه دلخواه را بر روی عکس اجرا کنیم، نوع داده تصویر به float تغییر میکند. هر مقداری که بیشتر از 255 باشد به 255 محدود میشود. سپس نوع داده به np.uint8 و فضای رنگی به BGR تغییر میکند.
سمت چپ: تصویر اصلی / سمت راست: نتیجه اعمال فیلتر Sepia بر روی عکس. عکس از Quino AI در Unsplash
ممکن است این سؤال برایتان پیش بیاید که چرا ویدئویی برای این فیلتر تهیه نکردیم و شدت آن را به وسیله trach bar کنترل کردیم؟ برای اینکه فیلتر Sepia یک رنگ ثابت دارد و با یک آرایه ثابت میتوان آن را به دست آورد.
چکیده
در این مقاله به شما نشان دادیم که چگونه میتوان با استفاده از توابع OpenCV، 5 فیلتر مختلف ایجاد کرد. Track barها این امکان را برای کاربران فراهم میکنند تا شدت فیلتر را بر اساس نیاز و سلیقه خود تنظیم کنند. در این مقاله فقط گوشه کوچکی از قابتهای بیشمار OpenCV را به شما معرفی کردیم.
انواع کاربردهای هوش مصنوعی در صنایع مختلف را در هوشیو بخوانید