OpenCV漫水填充
引言
漫水填充是用一定顏色填充聯通區域,通過設定可連通畫素的上下限以及連通方式來達到不同的填充效果;漫水填充經常被用來標記或分離影象的一部分以便對其進行進一步處理或分析,也可以用來從輸入影象獲取掩碼區域,掩碼會加速處理過程,或只處理掩碼指定的畫素點,操作的結果總是某個連續的區域。
基本思想
種子填充演算法是從多邊形區域內部的一點開始,由此出發找到區域內的所有畫素。它採用的邊界定義是區域邊界上所有畫素具有某個特定的顏色值,區域內部所有畫素均不取這一特定顏色,而邊界外的畫素則可具有與邊界相同的顏色值。具體演算法步驟如下所示:
- 標記種子(x,y)的畫素點 ;
- 檢測該點的顏色,若他與邊界色和填充色均不同,就用填充色填 充該點,否則不填充 ;
- 檢測相鄰位置,繼續 2。這個過程延續到已經檢測區域邊界範圍內的所有畫素為止。
當然在搜尋的時候有兩種檢測相鄰畫素:四向連通和八向連通。四向連通即從區域上一點出發,通過四個方向上、下、左、右來檢索。而八向連通加上了左上、左下、右上、右下四個方向。這種演算法的有點是演算法簡單,易於實現,也可以填充帶有內孔的平面區域。但是此演算法需要更大的儲存空間以實現棧結構,同一個畫素多次入棧和出棧,效率低,運算量大。而掃描線種子填充演算法屬於種子填充演算法,它是以掃描線上的區段為單位操作。所謂區段,就是一條掃描線上相連著的若干內部象素的集合。掃描線種子填充演算法思想:首先填充當前掃描線上的位於給定區域的一區段,然後確定於這一區段相鄰的上下兩條線上位於該區域內是否存在需要填充的新區段,如果存在,則依次把他們儲存起來,反覆這個過程,直到所儲存的各區段都填充完畢。藉助於堆疊,上述演算法實現步驟如下:
初始化堆疊。 種子壓入堆疊。 while(堆疊非空){ 從堆疊彈出種子象素。 如果種子象素尚未填充,則: 求出種子區段:xleft、xright; 填充整個區段。 檢查相鄰的上掃描線的xleft <= x <= xright區間內,是否存在需要填充的新區段,如果存在的話,則把每個新區段在xleft <= x <= xright範圍內的最右邊的象素,作為新的種子象素依次壓入堆疊。 檢查相鄰的下掃描線的xleft <= x <= xright區間內,是否存在需要填充的新區段,如果存在的話,則把每個新區段在xleft <= x <= xright範圍內的最右邊的象素,作為新的種子象素依次壓入堆疊。 }
更進一步演算法,在原演算法中, 種子雖然代表一個區段, 但種子實質上仍是一個象素, 我們必須在種子出棧的時候計算種子區段, 而這裡有很大一部分計算是重複的. 而且, 原演算法的掃描過程如果不用mask的話, 每次都會重複掃描父種子區段所在的掃描線, 而用mask的話又會額外增加開銷。所以,
對原演算法的一個改進就是讓種子攜帶更多的資訊, 讓種子不再是一個象素, 而是一個結構體. 該結構體包含以下資訊: 種子區段的y座標值, 區段的x開始與結束座標, 父種子區段的方向(上或者下), 父種子區段的x開始與結束座標.
struct seed{
int y,
int xleft,
int xright,
int parent_xleft,
int parent_xright,
bool is_parent_up
};
這樣演算法的具體實現變動如下初始化堆疊.
將種子象素擴充成為種子區段(y, xleft, xright, xright+1, xrihgt, true), 填充種子區段, 並壓入堆疊. (這裡有一個構造父種子區段的技巧)
while(堆疊非空){
從堆疊彈出種子區段。
檢查父種子區段所在的掃描線的xleft <= x <= parent_xleft和parent_xright <= x <= xright兩個區間, 如果存在需要填充的新區段, 則將其填充並壓入堆疊.
檢查非父種子區段所在的掃描線的xleft <= x <= xright區間, 如果存在需要填充的新區段, 則將其填充並壓入堆疊.
}
另外, opencv裡的種子填充演算法跟以上方法大致相同, 不同的地方是opencv用了佇列不是堆疊, 而且是由固定長度的陣列來實現的迴圈佇列, 其固定長度為 max(img_width, img_height)*2. 並且push與pop均使用巨集來實現而沒有使用函式. 用固定長度的陣列來實現佇列(或堆疊)意義是顯然的, 能大大減少構造結構, 複製結構等操作, 可以大大提高效率.
參考程式碼
CVAPI(void) cvFloodFill( CvArr* image, CvPoint seed_point,
CvScalar new_val, CvScalar lo_diff CV_DEFAULT(cvScalarAll(0)),
CvScalar up_diff CV_DEFAULT(cvScalarAll(0)),
CvConnectedComp* comp CV_DEFAULT(NULL),
int flags CV_DEFAULT(4),
CvArr* mask CV_DEFAULT(NULL));
其中函式引數:
- image為待處理影象
- seed_point為種子座標
- new_val為填充值
- lo_diff為畫素值的下限差值
- up_diff為畫素值的上限差值
- 從函式形式可看出,該函式可處理多通道影象。
- mask為掩碼, 注意: 設輸入影象大小為width * height, 則掩碼的大小必須為 (width+2) * (height+2) , mask可為輸出,也可作為輸入 ,由flags決定
- flags引數 : 0~7位為0x04或者0x08 即 4連通或者8 連通
- 8~15位為填充mask的值大小 , 若為0 , 則預設用1填充
- 16~23位為 : CV_FLOODFILL_FIXED_RANGE =(1 << 16), CV_FLOODFILL_MASK_ONLY =(1 << 17)
- flags引數通過位與運算處理
當為CV_FLOODFILL_FIXED_RANGE 時,待處理的畫素點與種子點作比較,如果滿足(s - lodiff , s + updiff)之間(s為種子點畫素值),則填充此畫素 . 若無此位設定,則將待處理點與已填充的相鄰點作此比較
CV_FLOODFILL_MASK_ONLY 此位設定填充的對像, 若設定此位,則mask不能為空,此時,函式不填充原始影象img,而是填充掩碼影象. 若無此位設定,則在填充原始影象的時候,也用flags的8~15位標記對應位置的mask.
OpenCV版漫水填充實現
#include <cv.h>
#include <cxcore.h>
#include <highgui.h>
#include <iostream>
using namespace std;
int main() {
cvNamedWindow("source");
cvNamedWindow("dest1");
cvNamedWindow("dest2");
cvNamedWindow("mask0");
cvNamedWindow("mask1");
IplImage * src = cvLoadImage("test.jpg");
IplImage * img=cvCreateImage(cvGetSize(src), 8, 3);
IplImage *img2=cvCreateImage(cvGetSize(src),8,3);
IplImage *pMask = cvCreateImage(cvSize(src->width +2 ,src->height +2),8,1);
cvSetZero(pMask);
cvCopyImage(src, img);
cvCopyImage(src,img2);
cvFloodFill( img, cvPoint(300,310),CV_RGB(255,0,0),cvScalar(20,30,40,0),
cvScalar(5,30,40,0), NULL, CV_FLOODFILL_FIXED_RANGE | (0x9f<<8),pMask);
cvShowImage("mask0",pMask);
cvSetZero(pMask);
cvFloodFill(img2,cvPoint(80,80),CV_RGB(255,0,0),cvScalarAll(29),cvScalarAll(10),
NULL, CV_FLOODFILL_MASK_ONLY | (47<<8) ,pMask);
cvShowImage("source",src);
cvShowImage("dest1", img);
cvShowImage("dest2",img2);
cvShowImage("mask1",pMask);
cvWaitKey(0);
cvReleaseImage(&src);
cvReleaseImage(&img);
cvReleaseImage(&img2);
cvReleaseImage(&pMask);
cvDestroyAllWindows();
return 0;
}
Python版漫水填充實現
#decoding:utf-8
import numpy as np
import cv2
import random
help_message ='''''USAGE: floodfill.py [<image>]
Click on the image to set seed point
Keys:
f - toggle floating range
c - toggle 4/8 connectivity
ESC - exit
'''
if __name__ == '__main__':
import sys
try: fn = sys.argv[1]
except: fn = '../test.jpg'
print help_message
img = cv2.imread(fn, True)
h, w = img.shape[:2] #得到影象的高和寬
mask = np.zeros((h+2, w+2), np.uint8)#掩碼單通道8位元,長和寬都比輸入影象多兩
#個畫素點,滿水填充不會超出掩碼的非零邊緣
seed_pt = None
fixed_range = True
connectivity = 4
def update(dummy=None):
if seed_pt is None:
cv2.imshow('floodfill', img)
return
flooded = img.copy() #以副本的形式進行填充,這樣每次
mask[:] = 0 #掩碼初始為全0
lo = cv2.getTrackbarPos('lo', 'floodfill')#觀察點畫素鄰域負差最大值
hi = cv2.getTrackbarPos('hi', 'floodfill')#觀察點畫素鄰域正差最大值
flags = connectivity #低位位元包含連通值, 4 (預設) 或 8
if fixed_range:
flags |= cv2.FLOODFILL_FIXED_RANGE #考慮當前象素與種子象素之間的差(高位元也可以為0)
#以白色進行漫水填充
cv2.floodFill(flooded, mask, seed_pt, (random.randint(0,255),
random.randint(0,255), random.randint(0,255)),
(lo,)*3, (hi,)*3, flags)
cv2.circle(flooded, seed_pt, 2, (0, 0, 255), -1)#選定基準點用紅色圓點標出
cv2.imshow('floodfill', flooded)
def onmouse(event, x, y, flags, param): #滑鼠響應函式
global seed_pt
if flags & cv2.EVENT_FLAG_LBUTTON: #滑鼠左鍵響應,選擇漫水填充基準點
seed_pt = x, y
update()
update()
cv2.setMouseCallback('floodfill', onmouse)
cv2.createTrackbar('lo', 'floodfill', 20, 255, update)
cv2.createTrackbar('hi', 'floodfill', 20, 255, update)
while True:
ch = 0xFF & cv2.waitKey()
if ch == 27:
break
if ch == ord('f'):
fixed_range = not fixed_range #選定時flags的高位位元位0,也就是鄰域的
#選定為當前畫素與相鄰畫素的的差,這樣的效果就是聯通區域會很大
print 'using %s range' % ('floating', 'fixed')[fixed_range]
update()
if ch == ord('c'):
connectivity = 12-connectivity #選擇4方向或則8方向種子擴散
print 'connectivity =', connectivity
update()
cv2.destroyAllWindows()
關於Image Engineering & Computer Vision的更多討論與交流,敬請關注本部落格和新浪微博songzi_tea.