標籤:

這是用什麼演算法實現的?


補充:我前天蛋疼隨便寫寫來著,這個流程應該算能運行,但我只是寫個答案而已完全沒考慮穩定性和實用性(不過話說回來有grabcut嘛),並且因為沒有meta data所以一個非常關鍵的步驟我直接略過了:Camera Calibration,還有其他答案里提到的大角度下的抗鋸齒等。就我寫的這個流程的話,後來總結了個稍微詳細的步驟放在這裡:利用OpenCV檢測圖像中的長方形畫布或紙張並提取圖像內容

=========== 分割線 ===========

寫個基於OpenCV的思路供參考。

紙張四角的坐標(圖中紅點)已知的情況

如果左圖的4個紅點是可以手動標註,那麼就簡單了:用OpenCV的Perspective Transform就可以了。具體步驟如下:

將標註好的點存入一個叫corner的變數里,而對應的正方形圖案的四個對應的corner放到canvas_corner的變數里,假設原圖叫image,代碼如下:

M= cv2.getPerspectiveTransform(corners, canvas_corner)
corrected_image = cv2.warpPerspective(image, M, (0, 0))

用題目里的照片試了試,結果如下:

紙張四角的坐標未知或難以準確標註的情況

一個思路是先手動標出大概的紙張區域,然後檢測紙張邊緣,然後生成直線和四角,再用Perpective Transform就可以了。試了一下下面這套流程大概可以:1) 大概標定紙張區域

引入手動輔助的分割,這裡我用的是OpenCV的grabcut,mask和結果如下:

可以看到,分割後的結果雖然能基本區分紙張形狀了,可是邊緣並不準確。另外鍵盤和部分桌面沒能區分開來,這時可以繼續用grabcut得到只有紙張的分割,或者為了用戶友好的話可以先檢測邊緣,再做後期處理。那麼得到的mask可以這麼用:保留mask邊緣附近的信息用於真正的邊緣檢測,而把其他部分都模糊處理,也就是說基於上面得到的mask做出下面的mask:

基於這個mask把除了mask以外的區域全都模糊處理掉:

然後用OpenCV的Canny演算法得到邊緣:

接下來OpenCV的Hough(我用的是cv2.HoughLinesP())直線檢測:

可以看到,有些線幾乎重合在一起了,這是難以避免的,上圖中一共檢測到9條線,其中兩對(下、右邊緣)重合。可以通過距離判斷和直線相對角度來判斷並把重合線段合為一條:

只剩下7條線了,那麼如何選取紙張邊緣的四條線呢(即使圖像分割步驟非常好得分開了紙張和其他部分,這在有些情況下還是難以避免的,比如圖案里有和邊緣平行的線條),可以沿著提取線段的兩邊採樣像素的灰度,另外在使用者分割圖像的時候能夠提取出大致的紙張中心位置,基於這個位置可以給圖像的兩個端點排序,從而曲分出每條線的「左」和「右」的像素值。具體的做法是先對所有線段的端點重新排序,計算兩個端點到各自到中心點的位移向量,然後計算這兩個位移向量的叉積。如果定義叉積&>0時中心點在「左」,則算出叉積&<0時交換兩個端點的順序。然後分別採樣左右兩側的像素灰度值,下面是7條線段對應的兩側像素灰度值分布:

可以看到其中有4個點距離非常近,說明他們的像素灰度分布也很接近,把這4條選出來,結果如下:

正是要的結果:

接下來計算四條線的交點,會得到6個結果,因為在這種應用場景中,方形的物體在透視變換下不會出現凹角,所以直接捨棄離紙張中心最遠的兩個交點就得到了四個角的坐標,結果如下:

這樣就回到了答案一開始的透視變換。


透視變換就不說了,

我貢獻一個找矩形的思路,類似於 OpenCV 自帶例子 squares.cpp 里的弄法。

方法是:

  1. RGB -&> HSV,然後通過飽和度來確定白色(白色飽和度低)

  2. 用一系列閾值進行閾值化,findContours 找聯通區域,approxPolyDP來擬合多邊形。(閾值化時用到了一個非常呵呵的判斷策略:如果70%的圖像全白了,那麼沒必要繼續往上加閾值了,不這樣的話,整幅圖像全白時,就是最大的矩形,我們不希望如此)

  3. 找到符合條件的最大矩形區域(的四個頂點)。

優點是不用人工參與,不足之處是,太簡單粗暴了,所以只能處理題主這種簡單情況,無法處理光照這類複雜問題。

代碼:

double angle(Point pt1, Point pt2, Point pt0)
{
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return (dx1*dx2 + dy1*dy2) / sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}

vector& largestRect(Mat image)
{
Mat hsv;
cvtColor(image, hsv, CV_BGR2HSV);
vector& channels;
split(hsv, channels);
vector& rect;
double maxArea = 0.0;
for (int i = 0; i &<= 90; i += 1) { // 提取 Hue 通道 Mat binaryImage = channels[1] &< i; // 如果超過 70% 的像素都變白了,那麼閾值化過頭了,直接退出 if (countNonZero(binaryImage) &> image.rows * image.cols * 0.7)
{
break;
}
// 找到連通區域
vector& &> contours;
findContours(binaryImage, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
// 多邊形近似
for (int i = 0; i &< contours.size(); ++i) { vector& polygon;
approxPolyDP(contours[i], polygon, arcLength(contours[i], 1) * 0.02, 1);
double area = fabs(contourArea(polygon));
// 把不可能是矩形的區域丟掉
if (isContourConvex(polygon)
polygon.size() == 4
area &> maxArea)
{
double maxCosine = 0;
for (int j = 2; j &< 5; j++) { double cosine = fabs(angle(polygon[j % 4], polygon[j - 2], polygon[j - 1])); maxCosine = MAX(maxCosine, cosine); } if (maxCosine &< 0.3) { maxArea = area; rect = polygon; } } } } return rect; }

效果:

自己用手機拍的圖


@達聞西 答案很贊!

補充一下,從圖中效果看,似乎還涉及到點陣圖矢量化(或者點陣圖的抗鋸齒變換)。有許多種不同的方案可以做到。


轉到頻率域求旋轉角度


哥們 你用我的圖啊 這個演算法我早就搞定了 非常精確 角度隨便


這是我偉大的ps 10分鐘搞定 改變你一下透視角 剪切 調對比色階具體教程網上找


前幾天剛寫過跟這個一模一樣的,透視變換,加上插值

四個角點是手動標吧


推薦閱讀:

遞歸有什麼意義?
計算機是怎樣進行開方和冪運算的?
求一千萬以內由兩個素數相乘的數,並按從小到大排列,如:6=2*3,10=2*5。有什麼比較好的思路嗎?
在學習數據結構與演算法的時候,一旦出現遞歸就很難理解。請問對於遞歸有沒有什麼好的理解方法?
世界上最複雜的程序演算法有哪些?是如何設計和用來做什麼的?

TAG:演算法 |