به نام خدا : تو این مطلب میخوام به کمک کتابخونه OpenCV، تصاویر موزاییکی ایجاد کنم، تصاویر موزاییکی از کنار هم قرار دادن تعداد زیادی از تصاویر کنار هم ایجاد میشن که این تصاویر طوری کنار هم قرار میگیرن که تشکیل یه تصویر جدید میدن؛ کاربرد خاصی براش به ذهنم نمیرسه غیر از این که برا قشنگی بکار میاد، یه افکت تصویر میشه بهش گفت، شاید هم نمیشه!؛ در ادامه درباره ساخت این مدل تصاویر و روش هاش صحبت میکنیم.
کد پروژه :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
#include "opencv2/highgui.hpp" #include "opencv2/imgproc.hpp" #include <iostream> //#include <tuple> const int TILE_SIZE = 200; // height/width of mosaic tiles in pixels const int RATIO_SIZE = 5; // RATIO_SIZE >=1 // خوندن تمام کاشی ها و تبدیلشون به کاشی های مربعی و تغییر اندازشون std::vector<cv::Mat> read_all_tiles(std::string path) { std::cout << "Reading tiles..."; // search the tiles directory recursively std::vector<cv::String> png_paths, jpg_paths; cv::glob(path + "/*.png", png_paths); cv::glob(path + "/*.jpg", jpg_paths); //--- std::vector<cv::Mat> tiles_orginal; for(int i = 0; i < png_paths.size(); i++) tiles_orginal.push_back(cv::imread(png_paths[i])); for(int i = 0; i < jpg_paths.size(); i++) tiles_orginal.push_back(cv::imread(jpg_paths[i])); // edit tiles std::vector<cv::Mat> tiles_modified; for(int i = 0; i < tiles_orginal.size(); i++) { // [ crop image, from center image ] tiles must be square, so get the largest square that fits inside the image int w = tiles_orginal[i].size().width; int h = tiles_orginal[i].size().height; int min_dimension = std::min(w, h); int w_crop = (w - min_dimension) / 2; int h_crop = (h - min_dimension) / 2; tiles_orginal[i] = tiles_orginal[i](cv::Range(h_crop, h - h_crop), cv::Range(w_crop, w - w_crop)); // [ resize image ] cv::Mat tile; cv::resize(tiles_orginal[i], tile, cv::Size(TILE_SIZE, TILE_SIZE)); // store as modified tile tiles_modified.push_back(tile); } std::cout << ", Processed : " << tiles_modified.size() << " tiles." << std::endl; return tiles_modified; } // محاسبه رنگ میانگین هر کاشی std::vector<cv::Scalar> calculate_tiles_averageColor(std::vector<cv::Mat> tiles) { std::cout << "Calculate tiles average color..."; std::vector<cv::Scalar> averageColor; for(int i = 0; i < tiles.size(); i++) { cv::Scalar color = cv::mean(tiles[i]); // average color averageColor.push_back(color); } std::cout << ", done." << std::endl; return averageColor; } // پیدا کردن شبیه ترین کاشی به رنگ میانگین مدنظر ما cv::Mat get_suitable_tile(std::vector<cv::Mat> tiles, std::vector<cv::Scalar> tiles_averageColor, cv::Scalar targetColor) { //std::cout << "get suitable tile..."; double similarity_distance = 999999999; int index = 0; for(int i = 0; i < tiles_averageColor.size(); i++) { double distance = cv::sqrt( cv::pow(tiles_averageColor[i][0] - targetColor[0], 2) + cv::pow(tiles_averageColor[i][1] - targetColor[1], 2) + cv::pow(tiles_averageColor[i][2] - targetColor[2], 2) ); if( distance < similarity_distance ) { similarity_distance = distance; index = i; } } //std::cout << ", done, index is : " << index << std::endl; return tiles[index]; } // خوندن تصویر اصلی و برش دادنش تا متناسب با کاشی ها بشه؛ برای سادگی پروژه و ترتمیز شدن کار cv::Mat read_target_image(std::string path) { cv::Mat target = cv::imread(path); std::cout << "read_target_image, orginal size = " << target.size(); // resize cv::resize(target, target, cv::Size(), RATIO_SIZE, RATIO_SIZE); std::cout << ", resize = " << target.size(); // [ crop image, from top left corner ] int w = target.size().width; int h = target.size().height; target = target(cv::Range(0, h - h%TILE_SIZE), cv::Range(0, w - w%TILE_SIZE)); std::cout << ", crop = " << target.size() << ", done" << std::endl;; return target; } int main() { cv::Mat target = read_target_image("C:/Users/Mahdi/Desktop/target.jpg"); std::vector<cv::Mat> tiles = read_all_tiles("C:/Users/Mahdi/Desktop/tiles"); std::vector<cv::Scalar> tiles_averageColor = calculate_tiles_averageColor(tiles); cv::Mat tiles_result(target.size(), target.type()); cv::Mat tiles_addWeighted_result(target.size(), target.type()); // https://dmf313.ir/?p=22244 double pixels_counter = 0; double pixels_count = target.rows * target.cols; for(int r = 0; r < target.rows; r+=TILE_SIZE) { for(int c = 0; c < target.cols; c+=TILE_SIZE) { /// Show progress percentage pixels_counter += TILE_SIZE * TILE_SIZE; double progress_percentage = (pixels_counter * 100.0) / pixels_count; std::cout << "[ loop ] progress percentage = " << progress_percentage << std::endl; /// The average color of the corresponding area std::vector<cv::Point> points; //--- int newC = c + TILE_SIZE; int newR = r + TILE_SIZE; //--- points.push_back(cv::Point(c, r)); points.push_back(cv::Point(newC, r)); points.push_back(cv::Point(newC, newR)); points.push_back(cv::Point(c, newR)); //--- // Create the mask with the polygon cv::Mat mask(target.rows, target.cols, CV_8UC1, cv::Scalar::all(0)); cv::fillPoly(mask, points, cv::Scalar::all(255)); //--- // Compute the mean with the computed mask cv::Scalar target_averageColor = cv::mean(target, mask); /// 1) tiles_result // find suitable tile cv::Mat suitable_tile = get_suitable_tile(tiles, tiles_averageColor, target_averageColor); //--- // draw [ copy tile to result image ] suitable_tile.copyTo(tiles_result(cv::Rect(c, r, suitable_tile.cols, suitable_tile.rows))); /// 2) tiles_addWeighted_result // get target roi cv::Mat roi_addWeighted = target(cv::Range(r, r+TILE_SIZE), cv::Range(c, c+TILE_SIZE)); //--- // addWeighted cv::Mat tile_addWeighted; cv::addWeighted(suitable_tile, 0.2, roi_addWeighted, 0.8, 0, tile_addWeighted); //--- tile_addWeighted.copyTo(tiles_addWeighted_result(cv::Rect(c, r, tile_addWeighted.cols, tile_addWeighted.rows))); } } cv::imwrite("C:/Users/Mahdi/Desktop/tiles_result.tiff", tiles_result); cv::imwrite("C:/Users/Mahdi/Desktop/tiles_addWeighted_result.tiff", tiles_addWeighted_result); cv::waitKey(); return 0; } |
الگوریتم پروژه : بنظرم قبل از هر پروژه ای اگه حوصله کنید و الگوریتم کار رو روی کاغذ بنویسید، کارتون از نظر زمانی سریعتر انجام میشه و فقط کار هم ساده تر؛ الان میخوام الگوریتم پروژه رو یه توضیح مختصری بدم تا موقعی که کدهای پروژه رو بررسی میکنید، روال کار کدها دستتون باشه و راحت تر متوجه بشید.
یه سری تصاویر ورودی داریم با نام tile ( کاشی ) و یه تصویر ورودی با نام target ( هدف )؛ کاری که میخوایم بکنیم اینه که تصاویر tile رو کنار هم قرار بدیم و تصویر target ورودی رو بازسازی کنیم و یه تصویر جدید ( تصویر خروجی، target خروجی ) رو ایجاد کنیم؛ خب هر چی تصاویر tile تعدادشون ( تنوع رنگ ) بیشتر باشه، خب نتیجه کار بهتر میشه؛ بعد از این که تصاویر tile رو خوندیم باید اندازه شون رو مربعی کنیم ( تا موقع کنار هم قرار دادنشون و ساخت تصویر target خروجی، کارمون ساده تر و تصویر target خروجی هم زیباتر بشه ) و بعد اندازه tile هارو تغییر میدیم ( مثلا 50×50 پیکسل )، که خب هر چی اندازه tile ها بزرگتر باشه، تو تصویر target خروجی، وقتی زوم میکنیم، کاشی ها، اندازشون بزرگتر هستش و بطبع کیفیت کارمون هم بهتر میشه ( اندازه کاشی ها به کمک ثابت TILE_SIZE تنظیم میشه ).
خب حالا مثلا اندازه تصویر target ورودی 800×600 هستش، خب بیایم از tile هایی با اندازه 50×50 استفاده کنیم کیفیت خروجی خیلی بد میشه، برا حل این مشکل، اندازه تصویر target ورودی رو چند برابر میکنیم، اینطوری در خروجی تصویر 8000×6000 داریم که وقتی زوم میکنیم کاشی ها رو با کیفیت اصلی میبینیم و تصویر خروجی هم شبیه تصویر target ورودی خواهد بود.
حالا میایم رنگ کاشی ها رو محاسبه میکنیم و در یه آرایه ای ذخیره میکنیم؛ پس تا این جا آرایه ای داریم که حاوی تصاویر tile هستش و آرایه ای داریم که حاوی رنگ میانگین تصاویر tile هستش؛ در مرحله بعد یه حلقه ایجاد میکنیم تا تصویر target ورودی رو پیمایش کنه و کاشی به کاشی تصویر رو پیش بریم، رنگ میانگین اون کاشی ( اون قسمت ) از تصویر target ورودی رو محاسبه میکنیم و بعد با اون آرایه ای که حاوی رنگ میانگین تصاویر tile بود، مقایسه میکنیم و بهترین tile ( که بیشترین شباهت رنگی رو داره ) رو انتخاب میکنیم و ازش استفاده میکنیم؛ تو پروژه من اسمشو گزاشتم suitable_tile ( کاشی مناسب!، مناسب از نظر رنگ! )
خب حالا 2 تا روش داریم ( هنوز تو حلقه هستیم )، یکی این که تصویر tile رو بدون هیچ تغییری داخل تصویر target ( در موقع مربوطه ) قرار بدیم، که اسم تصویری که به این روش ایجاد میشه رو گزاشتم tiles_result؛ که خب وابستگی شدیدی به تعداد tile ها و تنوع رنگیش داره، درواقع روش اصلی کار هم همینه ولی خب چون کیفیت خروجی زیاد جالب نمیشه، میایم یه کار دیگه ای میکنیم و اونم این که میایم و تصویر suitable_tile رو با کاشی متناظر در تصویر target ورودی، جمع وزنی میکنیم ( به کمک تابع addWeighted )، که خب اسم تصوری که به این روش ایجاد میشه رو گزاشتم tile_addWeighted، این روش وابستی زیادی به تعداد tile ها و تنوع رنگیشون نداره، به طوری که اگه حتی از 1 تصویر tile هم استفاده کنید، خروجی قابل قبولی میده، هرچند که با افزایش تعداد tile ها، تصویر خروجی زیباتر میشه و حرفه ای تر بنظر میرسه، این روش وابسته به وزن تصویر tile و تصویر target هستش که تو تابع addWeighted چه وزنی براشون انتخاب میکنید، در ادامه مطلب، نمونه ای از 2 وزن رو قرار میدم تا تفاوت رو حس کنید!
البته تصاویری که تو این مطلب گزاشتم رو با فوتوشاب اندازه و حجمشون رو اصلاح کردم، چون تو تستی که من کردم برا تصویر ورودی 800×500 و حدود 200 تا tile، اندازه tile حدود 200×200 پیکسل و ضریب افزایش تصویر target ورودی هم 20، با این شرایط، حجم هریک از تصاویر خروجی تقریببا 200 میگ شد!
نتیجه پروژه : در زیر نمونه ای از نتیجه کد بالا رو مشاهده میکنید؛ نتایج زیر به ازای TILE_SIZE = 200 و RATIO_SIZE = 5 هستند؛ تصویر ورودی با تصاویر خروجی اندازشون فرق داره، چون همونطور که قبلا گفتم، تصویر ورودی برش میخوره و اندازش اصلاح میشه تو پروژه؛ در ضمن تصاویر خروجی رو هم با فوتوشاب اندازه و حجمشون رو اصلاح کردم ( هر تصویر 15 میگ تقریبا به 300 کیلوبایت کاهش دادم ) :
امیدوارم این مطلب جالب بوده باشه براتون، تا مطلب بعد اگه زنده بودیم و قسمت شد ( که یه مطلب دیگه بنویسیم! )، یا علی.
مهمان
لبیک یا خامنه ای
تشکر از شما دوست گرانقدر برای سایت بسیار زیبا و مطالب کاربردی