به نام خدا : ان شاء الله که مطلب کامل و مفیدی در این زمینه بنویسم؛ این ماژول، زیر مجموعه ای از ماژول Image Processing هستش؛ تو این مطلب اول درباره مثلث بندی دلونی و نمودار ورنوی صحبت میکنیم و بعد میریم سراغ توضیح توابع کلاس Subdiv2D، یه چندتا کد نمونه ساده هم قرار میدیم برا آشنایی بیشتر با کلاس فوق و نحوه کار با توابع.
1) دایره محیطی چیست؟
دایره محیطی ( Circumscribed Circle و یا Circumcircle ) از یک چندضلعی، دایره ای است که از تمام رئوس آن چندضلعی میگذرد؛ مرکز این دایره را «مرکز دایره محیطی» ( Circumcenter ) و شعاعش را «شعاع دایره محیطی» ( Circumradius ) مینامند.
همه چندضلعیها لزوماً دایره محیطی ندارند؛ چندضلعی که دایره محیطی داشته باشد را «چندضلعی محاطی» ( چون توسط یک دایره احاطه شده )، «چندضلعی دایرهای» ( Cyclic Polygon )، یا «چندضلعی همدایره» ( Concyclic Polygon ) نیز مینامند، چرا که رئوس آن همدایره اند؛ تمام مثلثها، تمامی چندضلعیهای ساده منتظم، تمام مستطیلها، تمام ذوزنقههای متساوی الساقین، و همچنین تمام کایتهای راستگوشه جزو چندضلعیهای محاطی اند.
تصویر زیر : دایره محیطی C، و مرکز دایره محیطی O مربوط به یک چندجملهای محاطی P :
مثلث بندی دلونی ( Delaunay Triangulation )
یک مثلث بندی دلونی ( دیلانی ) برای یک مجموعه از نقاط به نام P در یک صفحه، یک مثلث بندی به نام (DT(P است به نحوی که هیچ یک از نقاط P درون هیچ یک از دایره های محیطی مثلثهای (DT(P نباشد؛ این مثلث بندی کمینه زاویههای مثلثها را به بیشترین مقدار ممکن میرساند و به این ترتیب از به وجود آمدن مثلثهای باریک جلوگیری میکند؛ این مثلث بندی توسط Boris Delaunay در سال 1934 ابداع شد.
تصاویر زیر با فرمت SVG هستن، برای دیدن در اندازه بزرگتر، در صفحه جدید بازشون کنید! ( محدودیت zoom ندارند تصاویر SVG )
- تصویر 1 : مثلث بندی دلونی با دوایر محیطی.
- تصویر 2 : مرکز دوایر محیطی رسم شده.
- تصویر 3 : با وصل کردن مراکز دوایر محیطی میتوان دیاگرام ورونی را تشکیل داد؛ فلذا : مثلث بندی Delaunay با خطوط سیاه و نمودار Voronoi با خطوط قرمز مشخص شده است!
نمودار ورنوی ( Voronoi diagram )
نمودار ورنوی صفحه ( plane ) را به تعدادی ناحیه تقسیم میکند؛ به این نواحی، سلولهای ورونوی ( Voronoi cell ) گفته میشود؛ برای هر نقطه از مجموعه نقاط، یک ناحیه تعریف میشود؛ تمام نقاط داخل هر ناحیه ای به نقطه تولیدکننده آن ناحیه نزدیکتر میباشد!؛ از کاربردهای این دیاگرام در مثلث بندی دلونی ( Delaunay triangulations ) میباشد.
کاربردهای نمودار ورونوی ( Voronoi diagram applications )
لیست تقریبا جامعی از این کاربرد ها رو میتونید در ویکی پدیا ( Voronoi diagram ) مشاهده کنید؛ چون کاربرهاش تخصصی و تو رشته های مختلف هستش، اینجا هم بنویسم کسی چیزی متوجه نمیشه همونطور که خودم هم کاربردهاشو متوجه نشدم! ( درک کاربردها تو حوزه های تخصصی با 1 خط توضیح.... )
این لینک هم کاربرد نمودار ورونوی تو حوضه هوش مصنوعی ( AI = Artificial Intelligence ) در بحث بازی سازی مثال زده : نمودار ورونی یا Voronoi Diagram چیست؟
توضیح توابع کلاس Subdiv2D
به کمک این کلاس میتونیم نقاطی به شیء ساخته شده از کلاس فوق بدیم تا برامون «مثلث بندی دلونی» و «نمودار Voronoi» رو محاسبه و ایجاد کنه و…
تقسیمبندیهایی که این کلاس انجام میده رو میتوان برای تبدیل یک صفحه به تکه یی سه بعدی ( 3D piece-wise )، تغییر شکل ( morphing )، مکانیابی سریع نقاط روی صفحه، ساختن نمودارهای خاص ( مانند NNG و یا RNG ) و… استفاده کرد.
1) سازنده Subdiv2D : ایجاد شیء از کلاس Subdiv2D؛ اگر از تعریف اول استفاده کنید، برای ایجاد یک Delaunay subdivision خالی، باید تابع initDelaunay رو هم فراخونی کنید :
1 2 |
CV_WRAP Subdiv2D(); CV_WRAP Subdiv2D(Rect rect); |
توضیح پارامترها :
rect : مستطیلی که شامل تمام نقاط دو بعدی است که قرار است به subdivision اضافه شوند.
2) تابع initDelaunay : در قسمت سازنده ها، دربارش صحبت کردیم.
1 |
CV_WRAP void initDelaunay(Rect rect); |
3) تابع insert : اضافه کردن نقطه/نقاط به مثلث بندی دلونی ( Delaunay Triangulation )؛ اگه نقطه تکراری به تابع بدید، نقطه فوق رو اضافه نمیکنه؛ تابع اولی، در خروجی، ID نقطه فوق رو برمیگردونه.
1 2 |
CV_WRAP int insert(Point2f pt); CV_WRAP void insert(const std::vector<Point2f>& ptvec); |
به کمک تابع اولی، نقاط رو دونه دونه وارد میکنید و در خروجی این تابع، شناسه راس ( vertex id ) هم return میشه، برای استفاده ازش به صورت زیر عمل کنید :
1 2 3 |
// std::vector<cv::Point2f> points; // your points! std::vector<int> idx; for (int i=0; i<points.size(); i++) idx.push_back( subdiv.insert(points[i]) ); |
به کمک تابع دومی هم میتونید تمام نقاط رو یکجا وارد کنید که خب تو این تابع دیگه شناسه راس ( vertex id ) ها رو ارسال نمیکنه، برای استفاده ازش به صورت زیر عمل کنید :
1 2 |
// std::vector<cv::Point2f> points; // your points! subdiv.insert(points); |
4) تابع getTriangleList : دریافت لیست مثلث های Delaunay؛ این تابع برداری از نوع Vec6f به ما میده، که هر Vec6f، حاوی 6 مقدار هستش؛ که این 6 تا مقدار حاوی : 3 تا مقادر X و 3 تا مقدار Y، که میشن مختصات 3 تا نقطه!؛ فلذا هر Vec6f به ما مختصات 3 نقطه رو میده ( رئوس مثلث رو میده! )
1 |
CV_WRAP void getTriangleList(CV_OUT std::vector<Vec6f>& triangleList) const; |
برای رسم مثلث بندی دلونی ( Delaunay Triangulation ) از تابع زیر استفاده میکنیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Draw delaunay triangles void planar_subdivision_example1::draw_delaunay( cv::Mat& img, cv::Subdiv2D& subdiv, cv::Scalar color ) { std::vector<cv::Vec6f> triangleList; subdiv.getTriangleList(triangleList); cv::Rect rect(cv::Point(0, 0), img.size()); for( size_t i = 0; i < triangleList.size(); i++ ) { std::vector<cv::Point> pt = { cv::Point(cvRound(triangleList[i][0]), cvRound(triangleList[i][1])), cv::Point(cvRound(triangleList[i][2]), cvRound(triangleList[i][3])), cv::Point(cvRound(triangleList[i][4]), cvRound(triangleList[i][5])) }; // Draw rectangles completely inside the image. if( rect.contains(pt[0]) && rect.contains(pt[1]) && rect.contains(pt[2])) cv::polylines(img, pt, true, color, 1, cv::LINE_AA); } } |
5) تابع getVoronoiFacetList : دریافت لیست چند ظلعی ( facet ) های Voronoi؛
1 2 3 4 |
CV_WRAP void getVoronoiFacetList( const std::vector<int>& idx, CV_OUT std::vector<std::vector<Point2f> >& facetList, CV_OUT std::vector<Point2f>& facetCenters ); |
توضیح پارامترها :
- idx : لیست رئوس ( نقاطی ) که میخواید برگشت داده بشه رو تعین کنید، که خب باید لیستی از ID نقاط مدنظر رو به این پارامتر بدید؛ اگه تمام رئوس رو میخواید، بردار خالی به این پارامتر بدید.
- facetList : برداری حاوی چند ظلعی های ورونوی.
- facetCenters : بردار خروجی نقاط مرکزی ورونوی ( این نقاط، همون نقاطی هستند که به کمک تابع insert به شیء مون اضافه کردیم ).
برای رسم نمودار ورنوی ( Voronoi diagram ) از تابع زیر استفاده میکنیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//Draw voronoi diagrams void planar_subdivision_example1::draw_voronoi( cv::Mat& img, cv::Subdiv2D& subdiv ) { std::vector<std::vector<cv::Point2f> > facets; std::vector<cv::Point2f> centers; subdiv.getVoronoiFacetList(std::vector<int>(), facets, centers); for( size_t i = 0; i < facets.size(); i++ ) { // copy data to new array std::vector<cv::Point> facet(facets[i].size()); for( size_t j = 0; j < facets[i].size(); j++ ) facet[j] = facets[i][j]; // define colors cv::Scalar color_random = cv::Scalar(rand()&255, rand()&255, rand()&255); cv::Scalar color_black = cv::Scalar(0, 0, 0); // draw cv::fillConvexPoly(img, facet, color_random, cv::LINE_AA); cv::polylines(img, facet, true, color_black, 1, cv::LINE_AA, 0); cv::circle(img, centers[i], 3, color_black, cv::FILLED, cv::LINE_AA, 0); } } |
توابع اصلی رو در بالا گفتم، از اینجا به بعد توابع فرعی رو توضیح میدم…
6) تابع getEdgeList : تو نمودار Delaunay، یه سری مثلث داریم، که خب هر مثلثی از 3 ضلع تشکل شده، به این ضلع ها، لبه ( edge ) میگن؛ برای خوندن لیست این لبه ها، از این تابع استفاده میکنیم؛ که لیستی از Vec4f ها بهمون میده که هر Vec4f حاوی 4 تا مقدار هستش، 2 تا X و 2 تا Y، که میشن مختصات 2 تا نقطه، که مختصات رئوس لبه فوق هستند.
1 |
CV_WRAP void getEdgeList(CV_OUT std::vector<Vec4f>& edgeList) const; |
برای خوندن لیست این لبه ها، و نمایششون از کد زیر میتونید استفاده کنید ( در کد زیر، یه عکس از ورودی گرفتم، یه سری نقاط تصادفی ایجاد کردم، شیء از کلاس Subdiv2D ایجاد کردم، نقاطی که ایجاد کردم رو با رنگ قرمز رسم کردم، بعد لبه های نمودار Delaunay رو خوندم و بعد با رنگ آبی رسمشون کردم، تمام! ) :
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 |
static void getEdgeList_testFunction() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create some random points! std::vector<cv::Point2f> points; for (int i=0; i<100; i++) { points.push_back( cv::Point2f( QRandomGenerator::global()->bounded(0, img.cols-1), QRandomGenerator::global()->bounded(0, img.rows-1) ) ); } // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object subdiv.insert(points); // Draw Points cv::Mat points_img = img.clone(); // make a copy for (int i=0; i<points.size(); i++) cv::circle(points_img, points[i], 5, cv::Scalar(0,0,255),cv::FILLED, cv::LINE_AA); // getEdgeList std::vector<cv::Vec4f> edgeList; subdiv.getEdgeList(edgeList); // draw edgeList for (int i=0; i<edgeList.size(); i++) { cv::Point2f org = cv::Point2f(edgeList[i][0], edgeList[i][1]); cv::Point2f dst = cv::Point2f(edgeList[i][2], edgeList[i][3]); cv::line(points_img, org, dst, cv::Scalar(255,0,0), 1, cv::LINE_AA); // draw cv::imshow( "points + edges", points_img ); // show image cv::waitKey(10); // delay! } } |
7) تابع findNearest : به کمک این تابع میتونید نزدیک ترین نقطه ( نقاطی که قبلا به کمک تابع insert وارد شیء cv::Subdiv2D کرده بودید ) به نقطه مدنظرتون ( که به تابع میدید ) رو پیدا کنید.
1 |
CV_WRAP int findNearest(Point2f pt, CV_OUT Point2f* nearestPt = 0); |
توضیح پارامترها و خروجی :
- pt : نقطه ورودی. ( پارامتر ورودی )
- nearestPt : نزدیک ترین نطقه به pt. ( پارامتر خروجی )
- خروجی تابع : vertex ID
برای درک هرچه بهتر این تابع، کد زیر رو بررسی و تست کنید :
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 |
static void findNearest_testFunction() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create some random points! std::vector<cv::Point2f> points; for (int i=0; i<100; i++) { points.push_back( cv::Point2f( QRandomGenerator::global()->bounded(0, img.cols-1), QRandomGenerator::global()->bounded(0, img.rows-1) ) ); } // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object subdiv.insert(points); // Draw Points cv::Mat points_img = img.clone(); // make a copy for (int i=0; i<points.size(); i++) cv::circle(points_img, points[i], 10, cv::Scalar(0,0,255),cv::FILLED, cv::LINE_AA); // findNearest cv::Point2f this_point = cv::Point2f(200,200); cv::Point2f nearest_point; int vertex_id = subdiv.findNearest(this_point, &nearest_point); cv::circle(points_img, this_point, 5, cv::Scalar(0,255,0),cv::FILLED, cv::LINE_AA); // draw cv::circle(points_img, nearest_point, 5, cv::Scalar(255,0,0),cv::FILLED, cv::LINE_AA); // draw cv::imshow( "points + edges", points_img ); // show image } |
نتیجه کد بالا :
8) تابع locate : مکان یک نقطه در یک مثلث دلونی را برمیگرداند؛ پارامتر pt، نقطه مدنظر ما هستش، پارامتر های خروجی edge و vertex هم اختیاری هستن.
1 |
CV_WRAP int locate(Point2f pt, CV_OUT int& edge, CV_OUT int& vertex); |
خروجی تابع فوق رو به کمک enum زیر میخونیم :
مثلا : cv::Subdiv2D::PTLOC_VERTEX
1 2 3 4 5 6 7 8 |
enum { PTLOC_ERROR = -2, //!< Point location error PTLOC_OUTSIDE_RECT = -1, //!< Point outside the subdivision bounding rect PTLOC_INSIDE = 0, //!< Point inside some facet PTLOC_VERTEX = 1, //!< Point coincides with one of the subdivision vertices PTLOC_ON_EDGE = 2 //!< Point on some edge }; |
توضح آیتم ها :
- PTLOC_ERROR : خطای مکان نقطه!؛ هرکاری کردم نتونستم این خطا رو ایجاد کنم! نمیدونم تحت چه شراطی ایجاد میشه.
- PTLOC_OUTSIDE_RECT : زمانی که نطقه ما خارج از 4 ظلعی subdivision bounding باشه ( این 4 ظلعی رو موقع ایجاد شیء به عنوان پارامتر ورودی به شیء میدید، یا به تابع initDelaunay مدید )
- PTLOC_INSIDE : زمانی که نقطه ما روی facet های نمودار Voronoi قرار بگیره.
- PTLOC_VERTEX : زمانی که نطقه ما روی یکی از نقاط subdivision vertices ( نقاطی که به کمک تابع insert وارد کردید ) قرار بگیره.
- PTLOC_ON_EDGE : زمانی که نطقه ما روی لبه ( edge ) های مثلث بندی دلونی قرار بگیره.
برای مثال، این کد رو تست کنید :
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 |
void planar_subdivision_example1::locate_testFunction() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create some points! std::vector<cv::Point2f> points = { cv::Point2f(50,50), cv::Point2f(400,50), cv::Point2f(500,200), cv::Point2f(400,400), cv::Point2f(50,400), }; // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object subdiv.insert(points); std::vector<std::vector<cv::Point2f> > facets; std::vector<cv::Point2f> centers; subdiv.getVoronoiFacetList(std::vector<int>(), facets, centers); // define some point for "subdiv.locate" std::vector<cv::Point2f> locate_this_points = { cv::Point2f(50,50), // 0) Inside the point cv::Point2f(200,50), // 1) Inside the delaunay-edge cv::Point2f(200,300), // 2) Inside the delaunay-triangle cv::Point2f(500,300), // 3) Outside the delaunay-triangle facets[0][0], // 4) facets facets[1][0], // 5) facets cv::Point2f( facets[1][0].x-40, facets[1][0].y) // 6) facets }; // Draw delaunay triangles cv::Mat dt_img = img.clone(); // make a copy draw_delaunay( dt_img, subdiv, cv::Scalar(0,0,255) ); // draw // Draw voronoi diagrams cv::Mat vd_img = img.clone(); // make a copy draw_voronoi( vd_img, subdiv ); // draw // Draw all Points [ in dt_img & vd_img ] //cv::Mat dst_img = img.clone(); // make a copy for (int i=0; i<points.size(); i++) { cv::circle(dt_img, points[i], 10, cv::Scalar(0,0,255),cv::FILLED, cv::LINE_AA); cv::circle(vd_img, points[i], 10, cv::Scalar(0,0,255),cv::FILLED, cv::LINE_AA); } for (int i=0; i<locate_this_points.size(); i++) { cv::circle(dt_img, locate_this_points[i], 5, cv::Scalar(0,255,0),cv::FILLED, cv::LINE_AA); cv::putText(dt_img, std::to_string(i), locate_this_points[i], cv::HersheyFonts::FONT_HERSHEY_COMPLEX, 0.8, cv::Scalar(255, 255, 255), 2); cv::circle(vd_img, locate_this_points[i], 5, cv::Scalar(0,255,0),cv::FILLED, cv::LINE_AA); cv::putText(vd_img, std::to_string(i), locate_this_points[i], cv::HersheyFonts::FONT_HERSHEY_COMPLEX, 0.8, cv::Scalar(255, 255, 255), 2); } cv::imwrite( "C:/Users/Mahdi/Desktop/delaunay_triangles2.jpg", dt_img ); // save image cv::imwrite( "C:/Users/Mahdi/Desktop/voronoi_diagrams2.jpg", vd_img ); // save image // subdiv.locate for (int i=0; i<locate_this_points.size(); i++) { int edge, vertex; int result = subdiv.locate(locate_this_points[i], edge, vertex); QString text = QString::number(i) + ") "; switch (result) { case cv::Subdiv2D::PTLOC_ERROR : text += "Point location error"; break; case cv::Subdiv2D::PTLOC_OUTSIDE_RECT : text += "Point outside the subdivision bounding rect"; break; case cv::Subdiv2D::PTLOC_INSIDE : text += "Point inside some facet"; break; case cv::Subdiv2D::PTLOC_VERTEX : text += "Point coincides with one of the subdivision vertices"; break; case cv::Subdiv2D::PTLOC_ON_EDGE : text +="Point on some edge"; break; } qDebug() << text; } } |
چیزی که در خروجی چاپ میشه :
1 2 3 4 5 6 7 |
"0) Point coincides with one of the subdivision vertices" "1) Point on some edge" "2) Point inside some facet" "3) Point inside some facet" "4) Point on some edge" "5) Point inside some facet" "6) Point inside some facet" |
اینم از تصاویر خروجی :
9) getVertex : خوندن موقعیت یک راس ( vertex ) به کمک شناسه اش ( vertex ID )؛ با پارامتر اختیاری firstEdge ( که خروجی هستش ) فعلا کاری نداریم ( تو مثالی که در ادامه قرار میدم )
1 |
CV_WRAP Point2f getVertex(int vertex, CV_OUT int* firstEdge = 0) const; |
حالا شاید سوال کنید که شناسه راس ( vertex ID ) ها رو از کجا گیر بیاریم؟ به قسمت توضیحات "تابع insert" مراجعه کنید؛ برا دیدن نحوه استفاده از این تابع، کد زیر رو ببینید :
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 |
void planar_subdivision_example1::getVertex_testFunction() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create some points! std::vector<cv::Point2f> points = { cv::Point2f(50,50), cv::Point2f(400,50), cv::Point2f(500,200), cv::Point2f(400,400), cv::Point2f(50,400), }; // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object [ read verex_id too! ] std::vector<int> idx; for (int i=0; i<points.size(); i++) idx.push_back( subdiv.insert(points[i]) ); // Draw Points cv::Mat dst_img = img.clone(); for (int i=0; i<points.size(); i++) { cv::circle(dst_img, points[i], 25, cv::Scalar(0,0,255),cv::FILLED, cv::LINE_AA); } // getVertex int vertex_id = idx[0]; cv::Point2f point = subdiv.getVertex(vertex_id); cv::circle(dst_img, points[0], 15, cv::Scalar(0,255,0),cv::FILLED, cv::LINE_AA); cv::circle(dst_img, point, 5, cv::Scalar(255,0,0),cv::FILLED, cv::LINE_AA); // save image cv::imwrite( "C:/Users/Mahdi/Desktop/dst_img.jpg", dst_img ); } |
نتیجه کد بالا :
10) بقیه توابع : این توابع مربوط به edge ها هستند که خب حوصلم نشد دربارشون مطالعه کنم!
1 2 3 4 5 6 |
CV_WRAP int getEdge( int edge, int nextEdgeType ) const; CV_WRAP int nextEdge(int edge) const; CV_WRAP int rotateEdge(int edge, int rotate) const; CV_WRAP int symEdge(int edge) const; CV_WRAP int edgeOrg(int edge, CV_OUT Point2f* orgpt = 0) const; CV_WRAP int edgeDst(int edge, CV_OUT Point2f* dstpt = 0) const; |
در تابع getEdge، از enum زیر استفاده میکنیم :
1 2 3 4 5 6 7 8 9 10 11 12 |
/** Subdiv2D edge type navigation (see: getEdge()) */ enum { NEXT_AROUND_ORG = 0x00, // next around the edge origin ( eOnext on the picture below if e is the input edge) NEXT_AROUND_DST = 0x22, // next around the edge vertex ( eDnext ) PREV_AROUND_ORG = 0x11, // previous around the edge origin (reversed eRnext ) PREV_AROUND_DST = 0x33, // previous around the edge destination (reversed eLnext ) NEXT_AROUND_LEFT = 0x13, // next around the left facet ( eLnext ) NEXT_AROUND_RIGHT = 0x31, // next around the right facet ( eRnext ) PREV_AROUND_LEFT = 0x20, // previous around the left facet (reversed eOnext ) PREV_AROUND_RIGHT = 0x02 // previous around the right facet (reversed eDnext ) }; |
و اینم عکس راهنمای این تابع و enum بالا :
نمونه کدهایی از کلاس Subdiv2D :
کد نمونه 1 : خب یه کد خیلی ساده، که یه عکس رو میخونیم، بعد یه سری نقاط تصادفی ایجاد میکنیم، بعد نمودار های delaunay و voronoi رو ایجاد میکنیم.
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 |
void planar_subdivision_example1::Subdiv2D_Project1_SimpleCode() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create some random points! std::vector<cv::Point2f> points = { // add corner points [ for beauty! ] cv::Point2f(0 ,0), cv::Point2f(img.cols-1, 0), cv::Point2f(0, img.rows-1), cv::Point2f(img.cols-1, img.rows-1) }; for (int i=0; i<100; i++) { points.push_back( cv::Point2f( QRandomGenerator::global()->bounded(0, img.cols-1), QRandomGenerator::global()->bounded(0, img.rows-1) ) ); } // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object // method 1 [ if u dont need id ] subdiv.insert(points); // method 2 [ if u need id ] //std::vector<int> idx; //for (int i=0; i<points.size(); i++) idx.push_back( subdiv.insert(points[i]) ); // Draw delaunay triangles cv::Mat dt_img = img.clone(); // make a copy draw_delaunay( dt_img, subdiv, cv::Scalar(0,0,255) ); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/delaunay_triangles.jpg", dt_img ); // save image // Draw voronoi diagrams cv::Mat vd_img = img.clone(); // make a copy draw_voronoi( vd_img, subdiv ); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/voronoi_diagrams.jpg", vd_img ); // save image } |
نتیجه کد بالا :
کد نمونه 2 : کاری به کاربرد های این کلاس ندارم و این که کدهایی که میزنیم کاربردی دارن یا نه، فعلا میخوام یکم با توابع بازی میکنیم تا کار باهاشو یاد بگیرید؛ خب نظر موافقتون چیه که تصویر ورودی رو بیام نقاط کلیدی شو پیدا کنم و بعد به کمک این نقاط کلیدی، نمودارهای فوق رو ایجاد کنم؟ و مثلا تو نمودار voronoi بیام به جای این که از رنگ تصادفی استفاده کنم، از رنگ معادل تصویر ورودی در اون محیط استفاده کنم؟
برا بحث پیدا کردن نقاط کلیدی به این مطلب مراجعه کنید : آموزش ماژول 2D Features Framework در OpenCV
اگه میخواید تصویر خروجی، دقتش بره بالا و بیشتر شبیه تصویر ورودی بشه، از الگوریتم FastFeatureDetector استفاده کنید که تعداد KeyPoint های بیشتری میده.
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 |
void planar_subdivision_example1::Subdiv2D_Project2_FunnyCode() { // read image cv::Mat img = cv::imread("C:/Users/Mahdi/Desktop/shahid-hadi-zolfaghari.jpg"); // create point with Feature2D! cv::Ptr<cv::Feature2D> feature2D = cv::SIFT::create(); // FastFeatureDetector is better ;) std::vector<cv::KeyPoint> keypoints; feature2D->detect(img, keypoints); // Draw keypoints cv::Mat kp_img = img.clone(); // make a copy cv::drawKeypoints(kp_img, keypoints, kp_img, cv::Scalar(0,0,255)); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/keypoints.jpg", kp_img ); // save image // [ std::vector<cv::KeyPoint> ] to [ std::vector<cv::Point2f> ] std::vector<cv::Point2f> points = { // add corner points [ for beauty! ] cv::Point2f(0 ,0), cv::Point2f(img.cols-1, 0), cv::Point2f(0, img.rows-1), cv::Point2f(img.cols-1, img.rows-1) }; for (int i=0; i<keypoints.size(); i++) points.push_back(keypoints[i].pt); // create object [ from Subdiv2D class ] cv::Rect rect = cv::Rect(cv::Point(0, 0), img.size()); cv::Subdiv2D subdiv(rect); // add points to object // method 1 [ if u dont need id ] subdiv.insert(points); // method 2 [ if u need id ] //std::vector<int> idx; //for (int i=0; i<points.size(); i++) idx.push_back( subdiv.insert(points[i]) ); // Draw delaunay triangles cv::Mat dt_img = img.clone(); // make a copy draw_delaunay( dt_img, subdiv, cv::Scalar(0,0,255) ); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/delaunay_triangles.jpg", dt_img ); // save image // Draw voronoi diagrams cv::Mat vda_img = img.clone(); // make a copy draw_voronoi_AverageColors( vda_img, subdiv ); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/voronoi_diagrams_AverageColor.jpg", vda_img ); // save image //--- cv::Mat vdr_img = img.clone(); // make a copy draw_voronoi( vdr_img, subdiv ); // draw cv::imwrite( "C:/Users/Mahdi/Desktop/voronoi_diagrams_RandomColor.jpg", vdr_img ); // save image } void planar_subdivision_example1::draw_voronoi_AverageColors( cv::Mat& img, cv::Subdiv2D& subdiv ) { std::vector<std::vector<cv::Point2f> > facets; std::vector<cv::Point2f> centers; subdiv.getVoronoiFacetList(std::vector<int>(), facets, centers); for( size_t i = 0; i < facets.size(); i++ ) { // copy data to new array std::vector<cv::Point> facet(facets[i].size()); for( size_t j = 0; j < facets[i].size(); j++ ) facet[j] = facets[i][j]; // The average color of the corresponding area // Create the mask with the polygon cv::Mat mask(img.rows, img.cols, uchar(0)); cv::fillPoly(mask, facet, cv::Scalar(255)); // Compute the mean with the computed mask cv::Scalar color = cv::mean(img, mask); // draw cv::fillConvexPoly(img, facet, color, cv::LINE_AA); } } |
نتیجه کد بالا :
کد نمونه 3 : خب بریم سراغ یه کد دیگه، تو این کد ابتدا به وبکم وصل میشیم، به کمک الگوریتم landmark، چهره ها رو شناسایی میکنیم ( فعلا مطلبو نزاشتم تو سایت، هر وقت گزاشتم، لینک آموزششو قرار میدم )، اگه کلید ESC رو فشار بدیم، عکس گرفته میشه ( عکس 1 )، بعد به کمک الگوریتم Subdiv2D، نمودار مثلث بندی Delaunay رو رسم میکنیم ( عکس 2 ) و در آخر هم طرح Voronoi رو رسم میکنیم ( عکس 3 )؛ این کد ساده شده و اصلا شده کدی هستش که ته مطلب به عنوان مرجع ذکر کردم.
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 187 188 189 190 191 192 193 194 195 196 197 198 |
// Draw delaunay triangles void planar_subdivision_example1::draw_delaunay( cv::Mat& img, cv::Subdiv2D& subdiv, cv::Scalar color ) { std::vector<cv::Vec6f> triangleList; subdiv.getTriangleList(triangleList); cv::Rect rect(cv::Point(0, 0), img.size()); for( size_t i = 0; i < triangleList.size(); i++ ) { std::vector<cv::Point> pt = { cv::Point(cvRound(triangleList[i][0]), cvRound(triangleList[i][1])), cv::Point(cvRound(triangleList[i][2]), cvRound(triangleList[i][3])), cv::Point(cvRound(triangleList[i][4]), cvRound(triangleList[i][5])) }; // Draw rectangles completely inside the image. if( rect.contains(pt[0]) && rect.contains(pt[1]) && rect.contains(pt[2])) cv::polylines(img, pt, true, color, 1, cv::LINE_AA); } } //--- //Draw voronoi diagrams void planar_subdivision_example1::draw_voronoi( cv::Mat& img, cv::Subdiv2D& subdiv ) { std::vector<std::vector<cv::Point2f> > facets; std::vector<cv::Point2f> centers; subdiv.getVoronoiFacetList(std::vector<int>(), facets, centers); for( size_t i = 0; i < facets.size(); i++ ) { // copy data to new array std::vector<cv::Point> facet(facets[i].size()); for( size_t j = 0; j < facets[i].size(); j++ ) facet[j] = facets[i][j]; // define colors cv::Scalar color_random = cv::Scalar(rand()&255, rand()&255, rand()&255); cv::Scalar color_black = cv::Scalar(0, 0, 0); // draw cv::fillConvexPoly(img, facet, color_random, cv::LINE_AA); cv::polylines(img, facet, true, color_black, 1, cv::LINE_AA, 0); cv::circle(img, centers[i], 3, color_black, cv::FILLED, cv::LINE_AA, 0); } } //--- void planar_subdivision_example1::subdiv2D_main_code(cv::Mat img, std::vector<cv::Point2f> points) { if(img.empty()) return; // Define window names std::string win_delaunay = "Delaunay Triangulation"; std::string win_voronoi = "Voronoi Diagram"; // Keep a copy around cv::Mat img_orig = img.clone(); // Create an instance of Subdiv2D cv::Subdiv2D subdiv( cv::Rect(cv::Point(0, 0), img.size()) ); // Insert points into a Delaunay triangulation. subdiv.insert(points); // Draw delaunay triangles draw_delaunay( img, subdiv, cv::Scalar(255,255,255) ); // Draw points for(int i=0; i<points.size(); i++) cv::circle( img, points[i], 2, cv::Scalar(0, 0, 255), cv::FILLED, cv::LINE_AA, 0 ); // Allocate space for voronoi Diagram cv::Mat img_voronoi = cv::Mat::zeros(img.rows, img.cols, CV_8UC3); // Draw voronoi diagram draw_voronoi( img_voronoi, subdiv ); // Show results. imshow( win_delaunay, img); imshow( win_voronoi, img_voronoi); imwrite( "C:/Users/Mahdi/Desktop/AAA2.jpg", img); imwrite( "C:/Users/Mahdi/Desktop/AAA3.jpg", img_voronoi); } void planar_subdivision_example1::drawPolyline(cv::Mat &im, const std::vector<cv::Point2f> &landmarks, const int start, const int end, bool isClosed = false) { // The cv::polylines function does not support Point2f // convert [ std::vector<cv::Point2f> ] to [ std::vector<cv::Point> ] std::vector<cv::Point> points; for (int i = start; i <= end; i++) points.push_back(cv::Point(landmarks[i].x, landmarks[i].y)); // Draw polylines cv::polylines(im, points, isClosed, cv::Scalar(255, 200,0), 2, 16); } void planar_subdivision_example1::drawLandmarks(cv::Mat &im, std::vector<cv::Point2f> &landmarks) { if (landmarks.size() == 68) { // Draw face for the 68-point model. drawPolyline(im, landmarks, 0, 16); // Jaw line drawPolyline(im, landmarks, 17, 21); // Left eyebrow drawPolyline(im, landmarks, 22, 26); // Right eyebrow drawPolyline(im, landmarks, 27, 30); // Nose bridge drawPolyline(im, landmarks, 30, 35, true); // Lower nose drawPolyline(im, landmarks, 36, 41, true); // Left eye drawPolyline(im, landmarks, 42, 47, true); // Right Eye drawPolyline(im, landmarks, 48, 59, true); // Outer lip drawPolyline(im, landmarks, 60, 67, true); // Inner lip } else { // If the number of points is not 68, we do not know which // points correspond to which facial features. So, we draw // one dot per landamrk. for(int i = 0; i < landmarks.size(); i++) { circle(im,landmarks[i],3, cv::Scalar(255, 200,0), cv::FILLED); } } } //--- planar_subdivision_example1::landmarks_return_format planar_subdivision_example1::landmarks_main_code() { // Load Face Detector cv::CascadeClassifier faceDetector("C:/Users/Mahdi/Desktop/haarcascade_frontalface_alt2.xml"); // Create an instance of Facemark cv::Ptr<cv::face::Facemark> facemark = cv::face::FacemarkLBF::create(); // Load landmark detector std::string lbfmodel_path = "C:/Users/Mahdi/Desktop/lbfmodel.yaml"; qDebug() << "loading data from : " << QString::fromStdString(lbfmodel_path) << " [ pls w8! ]"; facemark->loadModel("C:/Users/Mahdi/Desktop/lbfmodel.yaml"); // create object -> open webcam -> set webcam frames size cv::VideoCapture vc(0); //--- //vc.set(cv::CAP_PROP_FRAME_WIDTH, 1920); //vc.set(cv::CAP_PROP_FRAME_HEIGHT, 1080); // Variable to store a video frame and its grayscale cv::Mat frame, gray; // Read a frame while(vc.read(frame)) { cv::Mat frame_orginal = frame.clone(); // Find face std::vector<cv::Rect> faces; // Convert frame to grayscale because // faceDetector requires grayscale image. cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // Detect faces faceDetector.detectMultiScale(gray, faces); // Variable for landmarks. // Landmarks for one face is a vector of points // There can be more than one face in the image. Hence, we // use a vector of vector of points. std::vector<std::vector<cv::Point2f>> landmarks; // Run landmark detector bool success = facemark->fit(frame, faces, landmarks); //qDebug() << "number of face detected = " << landmarks.size() << endl; if(success) { // If successful, render the landmarks on the face for(int i = 0; i < landmarks.size(); i++) { drawLandmarks(frame, landmarks[i]); } } // Display results imshow("Facial Landmark Detection", frame); // Exit loop if ESC is pressed if (cv::waitKey(1) == 27) { if(success && landmarks.size() >= 1) { cv::imwrite( "C:/Users/Mahdi/Desktop/AAA1.jpg", frame); return landmarks_return_format { frame_orginal.clone(), landmarks[0] }; break; } } } return landmarks_return_format { cv::Mat(), std::vector<cv::Point2f>() }; } |
2 کد زیر رو در جایی که میخواید پروژه بالا اجرا بشه، قرار بدید ( فراخونی توابع اصلی پروژه بالا! ) :
1 2 |
landmarks_return_format data = landmarks_main_code(); // landmarks subdiv2D_main_code(data.frame, data.landmarks); // Subdiv2D |
نتیجه کد بالا :
فایل های مورد نیاز این پروژه :
1) فایل lbfmodel.yaml در مسیر زیر قرار داره ( چون حجم فایل 54 میگ هستش، بهتره فایل متنی زیر رو دانلود کنید! و بازش نکنید تو مرورگر! ):
https://raw.githubusercontent.com/kurnianggoro/GSOC2017/master/data/lbfmodel.yaml
2) فایل haarcascade_frontalface_alt2 در مسیر زیر قرار داره ( مسیری که opencv رو build کردید! ) :
[opencv]/data/haarcascades/haarcascade_frontalface_alt2.xml
اگه پیدا نکردید، از مسیر زیر میتونید دانلود کنید :
https://github.com/opencv/opencv/blob/master/data/haarcascades/haarcascade_frontalface_alt2.xml
دانلود کدهای پروژه + فایل های bfmodel.yaml و haarcascade_frontalface_alt2.xml مورد نیازدر پروژه 3 + تصویر خام شهید بزرگوار که تو پروژه ها استفاده کردم ( کلا هرچیزی که مربوطه به این پروژه رو داخل فایل زیر قرار دادم تا جایی که یادمه! ) :
منابع :
- ویکی پدیا : Voronoi diagram و نسخه فارسیش ( نمودار ورنوی )
- ویکی پدیا : Delaunay triangulation و نسخه فارسیش ( مثلث بندی دیلانی )
- ویکی پدیا : دایره محیطی
- Delaunay Triangulation and Voronoi Diagram using OpenCV
- Planar Subdivision
عکسی که تو این مطلب ( پروژه 1 و 2 ) استفاده کردم مربوطه به شهید محمد هادی ذوالفقاری، که وقتی مطبی از یکی از سایت ها رو میخوندم با ایشون آشنا شدیم ( البته قبلا دیده بودم عکس این شهید بزرگوار رو و کلیپی ازش ولی خب اطلاعات دیگه ای دربارشون نداشتم )، برای مطالعه درباره این شهید به لینک های زیر برید :
قسمت 1؛ قسمت 2؛ قسمت 3؛ قسمت 4، قسمت 5 ( و پایانی )
برای شادی روح تمام شهدا، مخصوصا این شهید بزرگوار و امام شهدا، صلواتی بفرستید ( اگه حسش بود فاتحه، نبود همون صلوات ^_^ )؛ تا مطلب بعد یا علی.