本系列文章由@淺墨_毛星雲 出品,轉載請註明出處。
文章鏈接: http://blog.csdn.net/poem_qianmo/article/details/28261997
作者:毛星雲(淺墨) 微博:http://weibo.com/u/1723155442
知乎:http://www.zhihu.com/people/mao-xing-yun
寫作當前博文時配套使用的OpenCV版本: 2.4.9
本篇文章中,我們一起探討了OpenCV填充算法中漫水填充算法相關的知識點,以及瞭解了OpenCV中實現漫水填充算法的兩個版本的floodFill函數的使用方法。此篇博文一共有兩個個配套的示例程序,其詳細註釋過的代碼都在文中貼出,且文章最後提供了綜合示例程序的下載。
先嚐鮮一下最終示例程序的運行截圖吧,這個示例程序實現了類似於PhotoShop中魔棒的圖像處理效果,我們可以用鼠標對窗口中的圖片一頓狂點,得到一片一片的彩色區域:
效果圖 原始圖
一、引言 · 漫水填充的定義
漫水填充法是一種用特定的顏色填充聯通區域,通過設置可連通像素的上下限以及連通方式來達到不同的填充效果的方法。漫水填充經常被用來標記或分離圖像的一部分以便對其進行進一步處理或分析,也可以用來從輸入圖像獲取掩碼區域,掩碼會加速處理過程,或只處理掩碼指定的像素點,操作的結果總是某個連續的區域。
另外,floodfill官方譯作“漫水填充”,但是淺墨總是喜歡說成“水漫填充”,因爲受從小到大深入骨髓的“水漫金山”這個成語的影響,囧。
二、漫水填充法的基本思想
所謂漫水填充,簡單來說,就是自動選中了和種子點相連的區域,接着將該區域替換成指定的顏色,這是個非常有用的功能,經常用來標記或者分離圖像的一部分進行處理或分析.漫水填充也可以用來從輸入圖像獲取掩碼區域,掩碼會加速處理過程,或者只處理掩碼指定的像素點.
以此填充算法爲基礎,類似photoshop的魔術棒選擇工具就很容易實現了。漫水填充(FloodFill)是查找和種子點聯通的顏色相同的點,魔術棒選擇工具則是查找和種子點聯通的顏色相近的點,將和初始種子像素顏色相近的點壓進棧作爲新種子
在OpenCV中,漫水填充是填充算法中最通用的方法。且在OpenCV 2.X中,使用C++重寫過的FloodFill函數有兩個版本。一個不帶掩膜mask的版本,和一個帶mask的版本。這個掩膜mask,就是用於進一步控制哪些區域將被填充顏色(比如說當對同一圖像進行多次填充時)。這兩個版本的FloodFill,都必須在圖像中選擇一個種子點,然後把臨近區域所有相似點填充上同樣的顏色,不同的是,不一定將所有的鄰近像素點都染上同一顏色,漫水填充操作的結果總是某個連續的區域。當鄰近像素點位於給定的範圍(從loDiff到upDiff)內或在原始seedPoint像素值範圍內時,FloodFill函數就會爲這個點塗上顏色。
三、floodFill函數詳解
在OpenCV中,漫水填充算法由floodFill函數實現,其作用是用我們指定的顏色從種子點開始填充一個連接域。連通性由像素值的接近程度來衡量。OpenCV2.X有兩個C++重寫版本的floodFill。
第一個版本的floodFill:
- int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )
int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )
第二個版本的floodFill:
- int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint,Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )
int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint,Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )
下面是一起介紹的參數詳解。除了第二個參數外,其他的參數都是共用的。
- 第一個參數,InputOutputArray類型的image, 輸入/輸出1通道或3通道,8位或浮點圖像,具體參數由之後的參數具體指明。
- 第二個參數, InputOutputArray類型的mask,這是第二個版本的floodFill獨享的參數,表示操作掩模,。它應該爲單通道、8位、長和寬上都比輸入圖像 image 大兩個像素點的圖像。第二個版本的floodFill需要使用以及更新掩膜,所以這個mask參數我們一定要將其準備好並填在此處。需要注意的是,漫水填充不會填充掩膜mask的非零像素區域。例如,一個邊緣檢測算子的輸出可以用來作爲掩膜,以防止填充到邊緣。同樣的,也可以在多次的函數調用中使用同一個掩膜,以保證填充的區域不會重疊。另外需要注意的是,掩膜mask會比需填充的圖像大,所以 mask 中與輸入圖像(x,y)像素點相對應的點的座標爲(x+1,y+1)。
- 第三個參數,Point類型的seedPoint,漫水填充算法的起始點。
- 第四個參數,Scalar類型的newVal,像素點被染色的值,即在重繪區域像素的新值。
- 第五個參數,Rect*類型的rect,有默認值0,一個可選的參數,用於設置floodFill函數將要重繪區域的最小邊界矩形區域。
- 第六個參數,Scalar類型的loDiff,有默認值Scalar( ),表示當前觀察像素值與其部件鄰域像素值或者待加入該部件的種子像素之間的亮度或顏色之負差(lower brightness/color difference)的最大值。
- 第七個參數,Scalar類型的upDiff,有默認值Scalar( ),表示當前觀察像素值與其部件鄰域像素值或者待加入該部件的種子像素之間的亮度或顏色之正差(lower brightness/color difference)的最大值。
- 第八個參數,int類型的flags,操作標誌符,此參數包含三個部分,比較複雜,我們一起詳細看看。
- 低八位(第0~7位)用於控制算法的連通性,可取4 (4爲缺省值) 或者 8。如果設爲4,表示填充算法只考慮當前像素水平方向和垂直方向的相鄰點;如果設爲 8,除上述相鄰點外,還會包含對角線方向的相鄰點。
- 高八位部分(16~23位)可以爲0 或者如下兩種選項標識符的組合:
- FLOODFILL_FIXED_RANGE - 如果設置爲這個標識符的話,就會考慮當前像素與種子像素之間的差,否則就考慮當前像素與其相鄰像素的差。也就是說,這個範圍是浮動的。
- FLOODFILL_MASK_ONLY - 如果設置爲這個標識符的話,函數不會去填充改變原始圖像 (也就是忽略第三個參數newVal), 而是去填充掩模圖像(mask)。這個標識符只對第二個版本的floodFill有用,因第一個版本里面壓根就沒有mask參數。
- 中間八位部分,上面關於高八位FLOODFILL_MASK_ONLY標識符中已經說的很明顯,需要輸入符合要求的掩碼。Floodfill的flags參數的中間八位的值就是用於指定填充掩碼圖像的值的。但如果flags中間八位的值爲0,則掩碼會用1來填充。
而所有flags可以用or操作符連接起來,即“|”。例如,如果想用8鄰域填充,並填充固定像素值範圍,填充掩碼而不是填充源圖像,以及設填充值爲38,那麼輸入的參數是這樣:
flags=8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE | (38<<8)
接着,來看一個關於Floodfill的簡單的調用範例:
- //-----------------------------------【頭文件包含部分】---------------------------------------
- // 描述:包含程序所依賴的頭文件
- //----------------------------------------------------------------------------------------------
- #include <opencv2/opencv.hpp>
- #include <opencv2/imgproc/imgproc.hpp>
- //-----------------------------------【命名空間聲明部分】---------------------------------------
- // 描述:包含程序所使用的命名空間
- //-----------------------------------------------------------------------------------------------
- using namespace cv;
- //-----------------------------------【main( )函數】--------------------------------------------
- // 描述:控制檯應用程序的入口函數,我們的程序從這裏開始
- //-----------------------------------------------------------------------------------------------
- int main( )
- {
- Mat src = imread("1.jpg");
- imshow("【原始圖】",src);
- Rect ccomp;
- floodFill(src, Point(50,300), Scalar(155, 255,55), &ccomp, Scalar(20, 20, 20),Scalar(20, 20, 20));
- imshow("【效果圖】",src);
- waitKey(0);
- return 0;
- }
//-----------------------------------【頭文件包含部分】--------------------------------------- // 描述:包含程序所依賴的頭文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【命名空間聲明部分】--------------------------------------- // 描述:包含程序所使用的命名空間 //----------------------------------------------------------------------------------------------- using namespace cv; //-----------------------------------【main( )函數】-------------------------------------------- // 描述:控制檯應用程序的入口函數,我們的程序從這裏開始 //----------------------------------------------------------------------------------------------- int main( ) { Mat src = imread("1.jpg"); imshow("【原始圖】",src); Rect ccomp; floodFill(src, Point(50,300), Scalar(155, 255,55), &ccomp, Scalar(20, 20, 20),Scalar(20, 20, 20)); imshow("【效果圖】",src); waitKey(0); return 0; }
運行截圖,原始圖:
效果圖:
五、floodFill函數在OpenCV中的實現源代碼
這個部分貼出OpenCV中本文相關函數的源碼實現細節,來給想了解實現細節的小夥伴們參考。淺墨暫時不在源碼的細節上挖深作詳細註釋。
5.1 OpenCV2.X中兩個版本的floodFill函數源碼
第一個,不帶mask版本的floodFill:
- int cv::floodFill( InputOutputArray _image,Point seedPoint,
- Scalar newVal, Rect* rect,
- Scalar loDiff, ScalarupDiff, int flags )
- {
- CvConnectedComp ccomp;
- CvMat c_image = _image.getMat();
- cvFloodFill(&c_image, seedPoint, newVal, loDiff, upDiff, &ccomp,flags, 0);
- if( rect )
- *rect = ccomp.rect;
- return cvRound(ccomp.area);
- }
int cv::floodFill( InputOutputArray _image,Point seedPoint, Scalar newVal, Rect* rect, Scalar loDiff, ScalarupDiff, int flags ) { CvConnectedComp ccomp; CvMat c_image = _image.getMat(); cvFloodFill(&c_image, seedPoint, newVal, loDiff, upDiff, &ccomp,flags, 0); if( rect ) *rect = ccomp.rect; return cvRound(ccomp.area); }
第二個,帶mask版本的floodFill:
- int cv::floodFill( InputOutputArray _image,InputOutputArray _mask,
- Point seedPoint, ScalarnewVal, Rect* rect,
- Scalar loDiff, ScalarupDiff, int flags )
- {
- CvConnectedComp ccomp;
- CvMat c_image = _image.getMat(), c_mask = _mask.getMat();
- cvFloodFill(&c_image, seedPoint, newVal,loDiff, upDiff, &ccomp, flags, c_mask.data.ptr ? &c_mask : 0);
- if( rect )
- *rect = ccomp.rect;
- return cvRound(ccomp.area);
- }
int cv::floodFill( InputOutputArray _image,InputOutputArray _mask, Point seedPoint, ScalarnewVal, Rect* rect, Scalar loDiff, ScalarupDiff, int flags ) { CvConnectedComp ccomp; CvMat c_image = _image.getMat(), c_mask = _mask.getMat(); cvFloodFill(&c_image, seedPoint, newVal,loDiff, upDiff, &ccomp, flags, c_mask.data.ptr ? &c_mask : 0); if( rect ) *rect = ccomp.rect; return cvRound(ccomp.area); }
我們依然可以發現,其內部實現是基於OpenCV 1.X舊版的cvFloodFill函數,我們再來看看其舊版函數的源碼。
5.2 OpenCV2.X中cvFloodFill()函數的實現源碼
- CV_IMPL void
- cvFloodFill( CvArr* arr, CvPointseed_point,
- CvScalar newVal, CvScalar lo_diff, CvScalar up_diff,
- CvConnectedComp* comp, int flags, CvArr* maskarr )
- {
- cv::Ptr<CvMat> tempMask;
- std::vector<CvFFillSegment> buffer;
- if( comp )
- memset( comp, 0, sizeof(*comp) );
- int i, type, depth, cn, is_simple;
- int buffer_size, connectivity = flags & 255;
- union {
- uchar b[4];
- int i[4];
- float f[4];
- double _[4];
- }nv_buf;
- nv_buf._[0] = nv_buf._[1] = nv_buf._[2] = nv_buf._[3] = 0;
- struct { cv::Vec3b b; cv::Vec3i i; cv::Vec3f f; } ld_buf, ud_buf;
- CvMat stub, *img = cvGetMat(arr, &stub);
- CvMat maskstub, *mask = (CvMat*)maskarr;
- CvSize size;
- type = CV_MAT_TYPE( img->type );
- depth = CV_MAT_DEPTH(type);
- cn = CV_MAT_CN(type);
- if( connectivity == 0 )
- connectivity = 4;
- else if( connectivity != 4 && connectivity != 8 )
- CV_Error( CV_StsBadFlag, "Connectivity must be 4, 0(=4) or 8");
- is_simple = mask == 0 && (flags & CV_FLOODFILL_MASK_ONLY) ==0;
- for( i = 0; i < cn; i++ )
- {
- if( lo_diff.val[i] < 0 || up_diff.val[i] < 0 )
- CV_Error( CV_StsBadArg, "lo_diff and up_diff must benon-negative" );
- is_simple &= fabs(lo_diff.val[i]) < DBL_EPSILON &&fabs(up_diff.val[i]) < DBL_EPSILON;
- }
- size = cvGetMatSize( img );
- if( (unsigned)seed_point.x >= (unsigned)size.width ||
- (unsigned)seed_point.y >=(unsigned)size.height )
- CV_Error( CV_StsOutOfRange, "Seed point is outside of image");
- cvScalarToRawData( &newVal, &nv_buf, type, 0 );
- buffer_size = MAX( size.width, size.height ) * 2;
- buffer.resize( buffer_size );
- if( is_simple )
- {
- int elem_size = CV_ELEM_SIZE(type);
- const uchar* seed_ptr = img->data.ptr + img->step*seed_point.y +elem_size*seed_point.x;
- for(i = 0; i < elem_size; i++)
- if (seed_ptr[i] != nv_buf.b[i])
- break;
- if (i != elem_size)
- {
- if( type == CV_8UC1 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.b[0],
- comp, flags, &buffer);
- else if( type == CV_8UC3 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3b(nv_buf.b),
- comp, flags,&buffer);
- else if( type == CV_32SC1 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.i[0],
- comp, flags,&buffer);
- else if( type == CV_32FC1 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.f[0],
- comp, flags,&buffer);
- else if( type == CV_32SC3 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3i(nv_buf.i),
- comp, flags, &buffer);
- else if( type == CV_32FC3 )
- icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3f(nv_buf.f),
- comp, flags,&buffer);
- else
- CV_Error(CV_StsUnsupportedFormat, "" );
- return;
- }
- }
- if( !mask )
- {
- /* created mask will be 8-byte aligned */
- tempMask = cvCreateMat( size.height + 2, (size.width + 9) & -8,CV_8UC1 );
- mask = tempMask;
- }
- else
- {
- mask = cvGetMat( mask, &maskstub );
- if( !CV_IS_MASK_ARR( mask ))
- CV_Error( CV_StsBadMask, "" );
- if( mask->width != size.width + 2 || mask->height != size.height +2 )
- CV_Error( CV_StsUnmatchedSizes, "mask must be 2 pixel wider "
- "and 2pixel taller than filled image" );
- }
- int width = tempMask ? mask->step : size.width + 2;
- uchar* mask_row = mask->data.ptr + mask->step;
- memset(mask_row - mask->step, 1, width );
- for( i = 1; i <= size.height; i++, mask_row += mask->step )
- {
- if( tempMask )
- memset( mask_row, 0, width );
- mask_row[0] = mask_row[size.width+1] = (uchar)1;
- }
- memset( mask_row, 1, width );
- if( depth == CV_8U )
- for( i = 0; i < cn; i++ )
- {
- int t = cvFloor(lo_diff.val[i]);
- ld_buf.b[i] = CV_CAST_8U(t);
- t = cvFloor(up_diff.val[i]);
- ud_buf.b[i] = CV_CAST_8U(t);
- }
- else if( depth == CV_32S )
- for( i = 0; i < cn; i++ )
- {
- int t = cvFloor(lo_diff.val[i]);
- ld_buf.i[i] = t;
- t = cvFloor(up_diff.val[i]);
- ud_buf.i[i] = t;
- }
- else if( depth == CV_32F )
- for( i = 0; i < cn; i++ )
- {
- ld_buf.f[i] = (float)lo_diff.val[i];
- ud_buf.f[i] = (float)up_diff.val[i];
- }
- else
- CV_Error( CV_StsUnsupportedFormat, "" );
- if( type == CV_8UC1 )
- icvFloodFillGrad_CnIR<uchar, int, Diff8uC1>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,nv_buf.b[0],
- Diff8uC1(ld_buf.b[0],ud_buf.b[0]),
- comp, flags,&buffer);
- else if( type == CV_8UC3 )
- icvFloodFillGrad_CnIR<cv::Vec3b, cv::Vec3i, Diff8uC3>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,cv::Vec3b(nv_buf.b),
- Diff8uC3(ld_buf.b, ud_buf.b),
- comp, flags,&buffer);
- else if( type == CV_32SC1 )
- icvFloodFillGrad_CnIR<int, int, Diff32sC1>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,nv_buf.i[0],
- Diff32sC1(ld_buf.i[0], ud_buf.i[0]),
- comp, flags, &buffer);
- else if( type == CV_32SC3 )
- icvFloodFillGrad_CnIR<cv::Vec3i, cv::Vec3i, Diff32sC3>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,cv::Vec3i(nv_buf.i),
- Diff32sC3(ld_buf.i, ud_buf.i),
- comp, flags,&buffer);
- else if( type == CV_32FC1 )
- icvFloodFillGrad_CnIR<float, float, Diff32fC1>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,nv_buf.f[0],
- Diff32fC1(ld_buf.f[0], ud_buf.f[0]),
- comp, flags,&buffer);
- else if( type == CV_32FC3 )
- icvFloodFillGrad_CnIR<cv::Vec3f, cv::Vec3f, Diff32fC3>(
- img->data.ptr,img->step, mask->data.ptr, mask->step,
- size, seed_point,cv::Vec3f(nv_buf.f),
- Diff32fC3(ld_buf.f,ud_buf.f),
- comp, flags,&buffer);
- else
- CV_Error(CV_StsUnsupportedFormat, "");
- }
CV_IMPL void cvFloodFill( CvArr* arr, CvPointseed_point, CvScalar newVal, CvScalar lo_diff, CvScalar up_diff, CvConnectedComp* comp, int flags, CvArr* maskarr ) { cv::Ptr<CvMat> tempMask; std::vector<CvFFillSegment> buffer; if( comp ) memset( comp, 0, sizeof(*comp) ); int i, type, depth, cn, is_simple; int buffer_size, connectivity = flags & 255; union { uchar b[4]; int i[4]; float f[4]; double _[4]; }nv_buf; nv_buf._[0] = nv_buf._[1] = nv_buf._[2] = nv_buf._[3] = 0; struct { cv::Vec3b b; cv::Vec3i i; cv::Vec3f f; } ld_buf, ud_buf; CvMat stub, *img = cvGetMat(arr, &stub); CvMat maskstub, *mask = (CvMat*)maskarr; CvSize size; type = CV_MAT_TYPE( img->type ); depth = CV_MAT_DEPTH(type); cn = CV_MAT_CN(type); if( connectivity == 0 ) connectivity = 4; else if( connectivity != 4 && connectivity != 8 ) CV_Error( CV_StsBadFlag, "Connectivity must be 4, 0(=4) or 8"); is_simple = mask == 0 && (flags & CV_FLOODFILL_MASK_ONLY) ==0; for( i = 0; i < cn; i++ ) { if( lo_diff.val[i] < 0 || up_diff.val[i] < 0 ) CV_Error( CV_StsBadArg, "lo_diff and up_diff must benon-negative" ); is_simple &= fabs(lo_diff.val[i]) < DBL_EPSILON &&fabs(up_diff.val[i]) < DBL_EPSILON; } size = cvGetMatSize( img ); if( (unsigned)seed_point.x >= (unsigned)size.width || (unsigned)seed_point.y >=(unsigned)size.height ) CV_Error( CV_StsOutOfRange, "Seed point is outside of image"); cvScalarToRawData( &newVal, &nv_buf, type, 0 ); buffer_size = MAX( size.width, size.height ) * 2; buffer.resize( buffer_size ); if( is_simple ) { int elem_size = CV_ELEM_SIZE(type); const uchar* seed_ptr = img->data.ptr + img->step*seed_point.y +elem_size*seed_point.x; for(i = 0; i < elem_size; i++) if (seed_ptr[i] != nv_buf.b[i]) break; if (i != elem_size) { if( type == CV_8UC1 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.b[0], comp, flags, &buffer); else if( type == CV_8UC3 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3b(nv_buf.b), comp, flags,&buffer); else if( type == CV_32SC1 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.i[0], comp, flags,&buffer); else if( type == CV_32FC1 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,nv_buf.f[0], comp, flags,&buffer); else if( type == CV_32SC3 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3i(nv_buf.i), comp, flags, &buffer); else if( type == CV_32FC3 ) icvFloodFill_CnIR(img->data.ptr, img->step, size, seed_point,cv::Vec3f(nv_buf.f), comp, flags,&buffer); else CV_Error(CV_StsUnsupportedFormat, "" ); return; } } if( !mask ) { /* created mask will be 8-byte aligned */ tempMask = cvCreateMat( size.height + 2, (size.width + 9) & -8,CV_8UC1 ); mask = tempMask; } else { mask = cvGetMat( mask, &maskstub ); if( !CV_IS_MASK_ARR( mask )) CV_Error( CV_StsBadMask, "" ); if( mask->width != size.width + 2 || mask->height != size.height +2 ) CV_Error( CV_StsUnmatchedSizes, "mask must be 2 pixel wider " "and 2pixel taller than filled image" ); } int width = tempMask ? mask->step : size.width + 2; uchar* mask_row = mask->data.ptr + mask->step; memset(mask_row - mask->step, 1, width ); for( i = 1; i <= size.height; i++, mask_row += mask->step ) { if( tempMask ) memset( mask_row, 0, width ); mask_row[0] = mask_row[size.width+1] = (uchar)1; } memset( mask_row, 1, width ); if( depth == CV_8U ) for( i = 0; i < cn; i++ ) { int t = cvFloor(lo_diff.val[i]); ld_buf.b[i] = CV_CAST_8U(t); t = cvFloor(up_diff.val[i]); ud_buf.b[i] = CV_CAST_8U(t); } else if( depth == CV_32S ) for( i = 0; i < cn; i++ ) { int t = cvFloor(lo_diff.val[i]); ld_buf.i[i] = t; t = cvFloor(up_diff.val[i]); ud_buf.i[i] = t; } else if( depth == CV_32F ) for( i = 0; i < cn; i++ ) { ld_buf.f[i] = (float)lo_diff.val[i]; ud_buf.f[i] = (float)up_diff.val[i]; } else CV_Error( CV_StsUnsupportedFormat, "" ); if( type == CV_8UC1 ) icvFloodFillGrad_CnIR<uchar, int, Diff8uC1>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,nv_buf.b[0], Diff8uC1(ld_buf.b[0],ud_buf.b[0]), comp, flags,&buffer); else if( type == CV_8UC3 ) icvFloodFillGrad_CnIR<cv::Vec3b, cv::Vec3i, Diff8uC3>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,cv::Vec3b(nv_buf.b), Diff8uC3(ld_buf.b, ud_buf.b), comp, flags,&buffer); else if( type == CV_32SC1 ) icvFloodFillGrad_CnIR<int, int, Diff32sC1>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,nv_buf.i[0], Diff32sC1(ld_buf.i[0], ud_buf.i[0]), comp, flags, &buffer); else if( type == CV_32SC3 ) icvFloodFillGrad_CnIR<cv::Vec3i, cv::Vec3i, Diff32sC3>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,cv::Vec3i(nv_buf.i), Diff32sC3(ld_buf.i, ud_buf.i), comp, flags,&buffer); else if( type == CV_32FC1 ) icvFloodFillGrad_CnIR<float, float, Diff32fC1>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,nv_buf.f[0], Diff32fC1(ld_buf.f[0], ud_buf.f[0]), comp, flags,&buffer); else if( type == CV_32FC3 ) icvFloodFillGrad_CnIR<cv::Vec3f, cv::Vec3f, Diff32fC3>( img->data.ptr,img->step, mask->data.ptr, mask->step, size, seed_point,cv::Vec3f(nv_buf.f), Diff32fC3(ld_buf.f,ud_buf.f), comp, flags,&buffer); else CV_Error(CV_StsUnsupportedFormat, ""); }
四、關於SetMouseCallback函數
因爲下面示例程序中有用到SetMouseCallback函數,我們在這裏講一講。SetMouseCallback函數爲指定的窗口設置鼠標回調函數。
C++: void setMouseCallback(conststring& winname, MouseCallback onMouse, void* userdata=0 )
- 第一個參數,const string&類型的winname,爲窗口的名字。
- 第二個參數,MouseCallback類型的onMouse,指定窗口裏每次鼠標時間發生的時候,被調用的函數指針。這個函數的原型應該爲voidFoo(int event, int x, int y, int flags, void* param);其中event是 CV_EVENT_*變量之一, x和y是鼠標指針在圖像座標系的座標(不是窗口座標系), flags是CV_EVENT_FLAG的組合, param是用戶定義的傳遞到cvSetMouseCallback函數調用的參數。
- 第三個參數,void*類型的userdata,用戶定義的傳遞到回調函數的參數,有默認值0。
五、綜合示例部分
本次的綜合示例爲OpenCV文檔中自帶的一個程序。淺墨將其做了適當的修改並詳細註釋,放出來供大家消化理解。
操作說明如下:
可以看到,此程序着不少的按鍵功能。而我們拿着鼠標對窗口中的圖形一頓狂點,就可以得到類似PhotoShop中魔棒的效果,當然,就這短短的兩百來行代碼寫出來的東西,體驗是比不上PS的魔棒工具的。
廢話不多說,程序詳細註釋的源碼如下:
- //-----------------------------------【程序說明】----------------------------------------------
- // 程序名稱::《【OpenCV入門教程之十五】水漫金山:OpenCV漫水填充算法(Floodfill)》 博文配套源碼
- // 開發所用IDE版本:Visual Studio 2010
- // 開發所用OpenCV版本: 2.4.9
- // 2014年6月3日 Created by 淺墨
- // 淺墨的微博:@淺墨_毛星雲 http://weibo.com/1723155442/profile?topnav=1&wvr=5&user=1
- // 淺墨的知乎:http://www.zhihu.com/people/mao-xing-yun
- // 淺墨的豆瓣:http://www.douban.com/people/53426472/
- //----------------------------------------------------------------------------------------------
- //-----------------------------------【頭文件包含部分】---------------------------------------
- // 描述:包含程序所依賴的頭文件
- //----------------------------------------------------------------------------------------------
- #include "opencv2/imgproc/imgproc.hpp"
- #include "opencv2/highgui/highgui.hpp"
- #include <iostream>
- //-----------------------------------【命名空間聲明部分】---------------------------------------
- // 描述:包含程序所使用的命名空間
- //-----------------------------------------------------------------------------------------------
- using namespace cv;
- using namespace std;
- //-----------------------------------【全局變量聲明部分】--------------------------------------
- // 描述:全局變量聲明
- //-----------------------------------------------------------------------------------------------
- Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;//定義原始圖、目標圖、灰度圖、掩模圖
- int g_nFillMode = 1;//漫水填充的模式
- int g_nLowDifference = 20, g_nUpDifference = 20;//負差最大值、正差最大值
- int g_nConnectivity = 4;//表示floodFill函數標識符低八位的連通值
- int g_bIsColor = true;//是否爲彩色圖的標識符布爾值
- bool g_bUseMask = false;//是否顯示掩膜窗口的布爾值
- int g_nNewMaskVal = 255;//新的重新繪製的像素值
- //-----------------------------------【ShowHelpText( )函數】----------------------------------
- // 描述:輸出一些幫助信息
- //----------------------------------------------------------------------------------------------
- static void ShowHelpText()
- {
- //輸出一些幫助信息
- printf("\n\n\n\t歡迎來到漫水填充示例程序~\n\n");
- printf( "\n\n\t按鍵操作說明: \n\n"
- "\t\t鼠標點擊圖中區域- 進行漫水填充操作\n"
- "\t\t鍵盤按鍵【ESC】- 退出程序\n"
- "\t\t鍵盤按鍵【1】- 切換彩色圖/灰度圖模式\n"
- "\t\t鍵盤按鍵【2】- 顯示/隱藏掩膜窗口\n"
- "\t\t鍵盤按鍵【3】- 恢復原始圖像\n"
- "\t\t鍵盤按鍵【4】- 使用空範圍的漫水填充\n"
- "\t\t鍵盤按鍵【5】- 使用漸變、固定範圍的漫水填充\n"
- "\t\t鍵盤按鍵【6】- 使用漸變、浮動範圍的漫水填充\n"
- "\t\t鍵盤按鍵【7】- 操作標誌符的低八位使用4位的連接模式\n"
- "\t\t鍵盤按鍵【8】- 操作標誌符的低八位使用8位的連接模式\n"
- "\n\n\t\t\t\t\t\t\t\t by淺墨\n\n\n"
- );
- }
- //-----------------------------------【onMouse( )函數】--------------------------------------
- // 描述:鼠標消息onMouse回調函數
- //---------------------------------------------------------------------------------------------
- static void onMouse( int event, int x, int y, int, void* )
- {
- // 若鼠標左鍵沒有按下,便返回
- if( event != CV_EVENT_LBUTTONDOWN )
- return;
- //-------------------【<1>調用floodFill函數之前的參數準備部分】---------------
- Point seed = Point(x,y);
- int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;//空範圍的漫水填充,此值設爲0,否則設爲全局的g_nLowDifference
- int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;//空範圍的漫水填充,此值設爲0,否則設爲全局的g_nUpDifference
- int flags = g_nConnectivity + (g_nNewMaskVal << 8) +
- (g_nFillMode == 1 ? CV_FLOODFILL_FIXED_RANGE : 0);//標識符的0~7位爲g_nConnectivity,8~15位爲g_nNewMaskVal左移8位的值,16~23位爲CV_FLOODFILL_FIXED_RANGE或者0。
- //隨機生成bgr值
- int b = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值
- int g = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值
- int r = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值
- Rect ccomp;//定義重繪區域的最小邊界矩形區域
- Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r*0.299 + g*0.587 + b*0.114);//在重繪區域像素的新值,若是彩色圖模式,取Scalar(b, g, r);若是灰度圖模式,取Scalar(r*0.299 + g*0.587 + b*0.114)
- Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目標圖的賦值
- int area;
- //--------------------【<2>正式調用floodFill函數】-----------------------------
- if( g_bUseMask )
- {
- threshold(g_maskImage, g_maskImage, 1, 128, CV_THRESH_BINARY);
- area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
- Scalar(UpDifference, UpDifference, UpDifference), flags);
- imshow( "mask", g_maskImage );
- }
- else
- {
- area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
- Scalar(UpDifference, UpDifference, UpDifference), flags);
- }
- imshow("效果圖", dst);
- cout << area << " 個像素被重繪\n";
- }
- //-----------------------------------【main( )函數】--------------------------------------------
- // 描述:控制檯應用程序的入口函數,我們的程序從這裏開始
- //-----------------------------------------------------------------------------------------------
- int main( int argc, char** argv )
- {
- //改變console字體顏色
- system("color 2F");
- //載入原圖
- g_srcImage = imread("1.jpg", 1);
- if( !g_srcImage.data ) { printf("Oh,no,讀取圖片image0錯誤~! \n"); return false; }
- //顯示幫助文字
- ShowHelpText();
- g_srcImage.copyTo(g_dstImage);//拷貝源圖到目標圖
- cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);//轉換三通道的image0到灰度圖
- g_maskImage.create(g_srcImage.rows+2, g_srcImage.cols+2, CV_8UC1);//利用image0的尺寸來初始化掩膜mask
- namedWindow( "效果圖",CV_WINDOW_AUTOSIZE );
- //創建Trackbar
- createTrackbar( "負差最大值", "效果圖", &g_nLowDifference, 255, 0 );
- createTrackbar( "正差最大值" ,"效果圖", &g_nUpDifference, 255, 0 );
- //鼠標回調函數
- setMouseCallback( "效果圖", onMouse, 0 );
- //循環輪詢按鍵
- while(1)
- {
- //先顯示效果圖
- imshow("效果圖", g_bIsColor ? g_dstImage : g_grayImage);
- //獲取鍵盤按鍵
- int c = waitKey(0);
- //判斷ESC是否按下,若按下便退出
- if( (c & 255) == 27 )
- {
- cout << "程序退出...........\n";
- break;
- }
- //根據按鍵的不同,進行各種操作
- switch( (char)c )
- {
- //如果鍵盤“1”被按下,效果圖在在灰度圖,彩色圖之間互換
- case '1':
- if( g_bIsColor )//若原來爲彩色,轉爲灰度圖,並且將掩膜mask所有元素設置爲0
- {
- cout << "鍵盤“1”被按下,切換彩色/灰度模式,當前操作爲將【彩色模式】切換爲【灰度模式】\n";
- cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
- g_maskImage = Scalar::all(0); //將mask所有元素設置爲0
- g_bIsColor = false; //將標識符置爲false,表示當前圖像不爲彩色,而是灰度
- }
- else//若原來爲灰度圖,便將原來的彩圖image0再次拷貝給image,並且將掩膜mask所有元素設置爲0
- {
- cout << "鍵盤“1”被按下,切換彩色/灰度模式,當前操作爲將【彩色模式】切換爲【灰度模式】\n";
- g_srcImage.copyTo(g_dstImage);
- g_maskImage = Scalar::all(0);
- g_bIsColor = true;//將標識符置爲true,表示當前圖像模式爲彩色
- }
- break;
- //如果鍵盤按鍵“2”被按下,顯示/隱藏掩膜窗口
- case '2':
- if( g_bUseMask )
- {
- destroyWindow( "mask" );
- g_bUseMask = false;
- }
- else
- {
- namedWindow( "mask", 0 );
- g_maskImage = Scalar::all(0);
- imshow("mask", g_maskImage);
- g_bUseMask = true;
- }
- break;
- //如果鍵盤按鍵“3”被按下,恢復原始圖像
- case '3':
- cout << "按鍵“3”被按下,恢復原始圖像\n";
- g_srcImage.copyTo(g_dstImage);
- cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY);
- g_maskImage = Scalar::all(0);
- break;
- //如果鍵盤按鍵“4”被按下,使用空範圍的漫水填充
- case '4':
- cout << "按鍵“4”被按下,使用空範圍的漫水填充\n";
- g_nFillMode = 0;
- break;
- //如果鍵盤按鍵“5”被按下,使用漸變、固定範圍的漫水填充
- case '5':
- cout << "按鍵“5”被按下,使用漸變、固定範圍的漫水填充\n";
- g_nFillMode = 1;
- break;
- //如果鍵盤按鍵“6”被按下,使用漸變、浮動範圍的漫水填充
- case '6':
- cout << "按鍵“6”被按下,使用漸變、浮動範圍的漫水填充\n";
- g_nFillMode = 2;
- break;
- //如果鍵盤按鍵“7”被按下,操作標誌符的低八位使用4位的連接模式
- case '7':
- cout << "按鍵“7”被按下,操作標誌符的低八位使用4位的連接模式\n";
- g_nConnectivity = 4;
- break;
- //如果鍵盤按鍵“8”被按下,操作標誌符的低八位使用8位的連接模式
- case '8':
- cout << "按鍵“8”被按下,操作標誌符的低八位使用8位的連接模式\n";
- g_nConnectivity = 8;
- break;
- }
- }
- return 0;
- }
//-----------------------------------【程序說明】---------------------------------------------- // 程序名稱::《【OpenCV入門教程之十五】水漫金山:OpenCV漫水填充算法(Floodfill)》 博文配套源碼 // 開發所用IDE版本:Visual Studio 2010 // 開發所用OpenCV版本: 2.4.9 // 2014年6月3日 Created by 淺墨 // 淺墨的微博:@淺墨_毛星雲 http://weibo.com/1723155442/profile?topnav=1&wvr=5&user=1 // 淺墨的知乎:http://www.zhihu.com/people/mao-xing-yun // 淺墨的豆瓣:http://www.douban.com/people/53426472/ //---------------------------------------------------------------------------------------------- //-----------------------------------【頭文件包含部分】--------------------------------------- // 描述:包含程序所依賴的頭文件 //---------------------------------------------------------------------------------------------- #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> //-----------------------------------【命名空間聲明部分】--------------------------------------- // 描述:包含程序所使用的命名空間 //----------------------------------------------------------------------------------------------- using namespace cv; using namespace std; //-----------------------------------【全局變量聲明部分】-------------------------------------- // 描述:全局變量聲明 //----------------------------------------------------------------------------------------------- Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;//定義原始圖、目標圖、灰度圖、掩模圖 int g_nFillMode = 1;//漫水填充的模式 int g_nLowDifference = 20, g_nUpDifference = 20;//負差最大值、正差最大值 int g_nConnectivity = 4;//表示floodFill函數標識符低八位的連通值 int g_bIsColor = true;//是否爲彩色圖的標識符布爾值 bool g_bUseMask = false;//是否顯示掩膜窗口的布爾值 int g_nNewMaskVal = 255;//新的重新繪製的像素值 //-----------------------------------【ShowHelpText( )函數】---------------------------------- // 描述:輸出一些幫助信息 //---------------------------------------------------------------------------------------------- static void ShowHelpText() { //輸出一些幫助信息 printf("\n\n\n\t歡迎來到漫水填充示例程序~\n\n"); printf( "\n\n\t按鍵操作說明: \n\n" "\t\t鼠標點擊圖中區域- 進行漫水填充操作\n" "\t\t鍵盤按鍵【ESC】- 退出程序\n" "\t\t鍵盤按鍵【1】- 切換彩色圖/灰度圖模式\n" "\t\t鍵盤按鍵【2】- 顯示/隱藏掩膜窗口\n" "\t\t鍵盤按鍵【3】- 恢復原始圖像\n" "\t\t鍵盤按鍵【4】- 使用空範圍的漫水填充\n" "\t\t鍵盤按鍵【5】- 使用漸變、固定範圍的漫水填充\n" "\t\t鍵盤按鍵【6】- 使用漸變、浮動範圍的漫水填充\n" "\t\t鍵盤按鍵【7】- 操作標誌符的低八位使用4位的連接模式\n" "\t\t鍵盤按鍵【8】- 操作標誌符的低八位使用8位的連接模式\n" "\n\n\t\t\t\t\t\t\t\t by淺墨\n\n\n" ); } //-----------------------------------【onMouse( )函數】-------------------------------------- // 描述:鼠標消息onMouse回調函數 //--------------------------------------------------------------------------------------------- static void onMouse( int event, int x, int y, int, void* ) { // 若鼠標左鍵沒有按下,便返回 if( event != CV_EVENT_LBUTTONDOWN ) return; //-------------------【<1>調用floodFill函數之前的參數準備部分】--------------- Point seed = Point(x,y); int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;//空範圍的漫水填充,此值設爲0,否則設爲全局的g_nLowDifference int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;//空範圍的漫水填充,此值設爲0,否則設爲全局的g_nUpDifference int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? CV_FLOODFILL_FIXED_RANGE : 0);//標識符的0~7位爲g_nConnectivity,8~15位爲g_nNewMaskVal左移8位的值,16~23位爲CV_FLOODFILL_FIXED_RANGE或者0。 //隨機生成bgr值 int b = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值 int g = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值 int r = (unsigned)theRNG() & 255;//隨機返回一個0~255之間的值 Rect ccomp;//定義重繪區域的最小邊界矩形區域 Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r*0.299 + g*0.587 + b*0.114);//在重繪區域像素的新值,若是彩色圖模式,取Scalar(b, g, r);若是灰度圖模式,取Scalar(r*0.299 + g*0.587 + b*0.114) Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目標圖的賦值 int area; //--------------------【<2>正式調用floodFill函數】----------------------------- if( g_bUseMask ) { threshold(g_maskImage, g_maskImage, 1, 128, CV_THRESH_BINARY); area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags); imshow( "mask", g_maskImage ); } else { area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags); } imshow("效果圖", dst); cout << area << " 個像素被重繪\n"; } //-----------------------------------【main( )函數】-------------------------------------------- // 描述:控制檯應用程序的入口函數,我們的程序從這裏開始 //----------------------------------------------------------------------------------------------- int main( int argc, char** argv ) { //改變console字體顏色 system("color 2F"); //載入原圖 g_srcImage = imread("1.jpg", 1); if( !g_srcImage.data ) { printf("Oh,no,讀取圖片image0錯誤~! \n"); return false; } //顯示幫助文字 ShowHelpText(); g_srcImage.copyTo(g_dstImage);//拷貝源圖到目標圖 cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);//轉換三通道的image0到灰度圖 g_maskImage.create(g_srcImage.rows+2, g_srcImage.cols+2, CV_8UC1);//利用image0的尺寸來初始化掩膜mask namedWindow( "效果圖",CV_WINDOW_AUTOSIZE ); //創建Trackbar createTrackbar( "負差最大值", "效果圖", &g_nLowDifference, 255, 0 ); createTrackbar( "正差最大值" ,"效果圖", &g_nUpDifference, 255, 0 ); //鼠標回調函數 setMouseCallback( "效果圖", onMouse, 0 ); //循環輪詢按鍵 while(1) { //先顯示效果圖 imshow("效果圖", g_bIsColor ? g_dstImage : g_grayImage); //獲取鍵盤按鍵 int c = waitKey(0); //判斷ESC是否按下,若按下便退出 if( (c & 255) == 27 ) { cout << "程序退出...........\n"; break; } //根據按鍵的不同,進行各種操作 switch( (char)c ) { //如果鍵盤“1”被按下,效果圖在在灰度圖,彩色圖之間互換 case '1': if( g_bIsColor )//若原來爲彩色,轉爲灰度圖,並且將掩膜mask所有元素設置爲0 { cout << "鍵盤“1”被按下,切換彩色/灰度模式,當前操作爲將【彩色模式】切換爲【灰度模式】\n"; cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY); g_maskImage = Scalar::all(0); //將mask所有元素設置爲0 g_bIsColor = false; //將標識符置爲false,表示當前圖像不爲彩色,而是灰度 } else//若原來爲灰度圖,便將原來的彩圖image0再次拷貝給image,並且將掩膜mask所有元素設置爲0 { cout << "鍵盤“1”被按下,切換彩色/灰度模式,當前操作爲將【彩色模式】切換爲【灰度模式】\n"; g_srcImage.copyTo(g_dstImage); g_maskImage = Scalar::all(0); g_bIsColor = true;//將標識符置爲true,表示當前圖像模式爲彩色 } break; //如果鍵盤按鍵“2”被按下,顯示/隱藏掩膜窗口 case '2': if( g_bUseMask ) { destroyWindow( "mask" ); g_bUseMask = false; } else { namedWindow( "mask", 0 ); g_maskImage = Scalar::all(0); imshow("mask", g_maskImage); g_bUseMask = true; } break; //如果鍵盤按鍵“3”被按下,恢復原始圖像 case '3': cout << "按鍵“3”被按下,恢復原始圖像\n"; g_srcImage.copyTo(g_dstImage); cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY); g_maskImage = Scalar::all(0); break; //如果鍵盤按鍵“4”被按下,使用空範圍的漫水填充 case '4': cout << "按鍵“4”被按下,使用空範圍的漫水填充\n"; g_nFillMode = 0; break; //如果鍵盤按鍵“5”被按下,使用漸變、固定範圍的漫水填充 case '5': cout << "按鍵“5”被按下,使用漸變、固定範圍的漫水填充\n"; g_nFillMode = 1; break; //如果鍵盤按鍵“6”被按下,使用漸變、浮動範圍的漫水填充 case '6': cout << "按鍵“6”被按下,使用漸變、浮動範圍的漫水填充\n"; g_nFillMode = 2; break; //如果鍵盤按鍵“7”被按下,操作標誌符的低八位使用4位的連接模式 case '7': cout << "按鍵“7”被按下,操作標誌符的低八位使用4位的連接模式\n"; g_nConnectivity = 4; break; //如果鍵盤按鍵“8”被按下,操作標誌符的低八位使用8位的連接模式 case '8': cout << "按鍵“8”被按下,操作標誌符的低八位使用8位的連接模式\n"; g_nConnectivity = 8; break; } } return 0; }
一些運行截圖,首先是運行後的原始圖:
點鼠標啊點鼠標:
滑滾動條啊滑滾動條:
地球已經阻止不了我們的鼠標了,點出來的圖,已經有點恐怖。。。。。看妹子的手。。。。
如果鼠標點到妹子臉上的話。。。。呃,更多驚悚的圖,淺墨不放出了,免得。。。。。
隨着我們鼠標的點擊,程序會記下我們的操作:
接着看一張灰度圖模式的漫水填充效果和掩碼圖:
再來一張彩色窗戶:
程序功能還是很多的,有鼠標操作,鍵盤8個按鍵的操作,還可以調滾動條:
好了,更多的功能我們就不在這裏示範了,大家下載了程序自己回去玩吧。
本篇文章的配套源代碼請點擊這裏下載: