opencv小項目練習之數(shù)獨求解

如果是給定的是數(shù)組,那么我做這個就基本沒有什么意義了,想要做到的效果:

對于給定的數(shù)獨照片(盡可能干凈整齊),進行一系列處理锹杈,提取位置和數(shù)字信息,這中間可能要用到一系列圖像處理的基本算法迈着,數(shù)字識別時初步打算用knn來做竭望,knn對手寫體的精度一般,這里要求輸入應(yīng)該是打印體裕菠,這樣才能保證正確率咬清,最后通過數(shù)獨求解的算法算出答案。

做這個小項目的主要目的是練手奴潘,opencv那本書早就看完了旧烧,也就寫了些書上的歷程,最復雜的代碼也就是讀了DCF的c++代碼画髓,做了一點小小的修改掘剪,還是希望多寫一些代碼,今天隨意找了一下奈虾,有人用python寫過數(shù)獨求解的這個項目夺谁,源碼也都給了,大體思路也給出了肉微,參考其大體思路匾鸥,我這里用c++寫一下,邊用邊學吧碉纳。

代碼放到這里:數(shù)獨
環(huán)境:win10+vs2015+opencv3.4

一勿负、預(yù)處理


對于這樣一張很干凈的圖像,如何找到每個數(shù)字的位置劳曹,并把數(shù)字識別出來奴愉,是我們進行數(shù)獨求解首先需要關(guān)注的事情琅摩。
先對圖像進行一些銳化,使圖像更加清晰锭硼。然后進行閾值化房资,因為一般是白紙黑字,閾值化的選擇應(yīng)該是反閾值(具體看函數(shù))账忘。
然后為了方便提取志膀,消除一些連通域之間的小間隙熙宇,我們對連通域做一個膨脹操作鳖擒,實驗的時候發(fā)現(xiàn),這個膨脹的多少也是一個比較敏感的參數(shù)烫止,一旦膨脹過多蒋荚,格子就可能和數(shù)字鏈接起來,我只想先把這個流程走下來馆蠕,就不考慮那么多了期升,這個操作中我是進行了兩次3*3十字形膨脹。這樣就可以得到比較理想的預(yù)處理結(jié)果了互躬,下一步在這個基礎(chǔ)上進行數(shù)字分割播赁。
下面結(jié)合代碼說下這個過程,順便熟悉opencv的函數(shù)吼渡。

  1. 圖像轉(zhuǎn)換為灰度容为,做拉普拉斯濾波,閾值化處理寺酪。
    srcImg.copyTo(Img);
    cvtColor(Img, Img_gray,COLOR_BGR2GRAY);
    
    Mat kernel = (Mat_<float>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
    filter2D(Img_gray, Img_g, -1, kernel);
    threshold(Img_g, Img_g, 150, 255, THRESH_BINARY_INV);         //二值閾值化坎背,這個閾值自己選,這里是反著的,因為一般是白紙黑字

這個閾值化后的圖像為:


閾值化

可以看到這個圖像其實已經(jīng)比較理想了寄雀,輪廓線比較清楚得滤,分格線稍微有一點細,分格線的邊緣還有一些小白點盒犹,為了進一步擴張線的寬度和吸收這些小白點懂更,我們對原圖進行膨脹。

  1. 膨脹操作
    膨脹腐蝕操作是圖像形態(tài)學操作最基本也是最常用的兩個操作急膀,對應(yīng)的函數(shù)分別是:
void erode( InputArray src, OutputArray dst, InputArray kernel,
                         Point anchor = Point(-1,-1), int iterations = 1,
                         int borderType = BORDER_CONSTANT,
                         const Scalar& borderValue = morphologyDefaultBorderValue() );

void dilate( InputArray src, OutputArray dst, InputArray kernel,
                          Point anchor = Point(-1,-1), int iterations = 1,
                          int borderType = BORDER_CONSTANT,
                          const Scalar& borderValue = morphologyDefaultBorderValue() );

這兩個函數(shù)都是支持原位操作的膜蛔。這些參數(shù)的意義:

@param src input image; the number of channels can be arbitrary, but the depth should be one of
CV_8U, CV_16U, CV_16S, CV_32F or CV_64F.(輸入圖像)
@param dst output image of the same size and type as src\`.(輸出圖像,和原圖大小相同)
@param kernel structuring element used for dilation; if elemenat=Mat(), a 3 x 3 rectangular
structuring element is used. Kernel can be created using getStructuringElement(膨脹核脖阵,可由getStructuringElement()這個函數(shù)得來)
@param anchor position of the anchor within the element; default value (-1, -1) means that the
anchor is at the element center.  (錨點位置皂股,默認最中心)
@param iterations number of times dilation is applied.(迭代的次數(shù),調(diào)用函數(shù)可以進行多次迭代膨脹或腐蝕命黔,默認是一次)
@param borderType pixel extrapolation method, see cv::BorderTypes
@param borderValue border value in case of a constant border
@sa  erode, morphologyEx, getStructuringElement
(后面兩個參數(shù)都是和邊緣處理相關(guān)的呜呐,一般使用時不用管)

getStructuringElement這個函數(shù)可以獲得不同的核形狀就斤。

Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));
第一個參數(shù)是形狀,可選三種蘑辑,分辨是下面的標志位:
矩形:MORPH_RECT;
交叉形:MORPH_CORSS;
橢圓形:MORPH_ELLIPSE;
第二個是尺寸洋机,必須是奇數(shù),第三個是錨點位置洋魂,默認中心點绷旗。

這樣的話,這里就講的很清楚了副砍,看膨脹后的圖像:


膨脹后

膨脹得到的圖像更加平滑衔肢。得到這樣的結(jié)果我們就可以去檢測輪廓了,這是最關(guān)鍵的一環(huán)豁翎。當然前面的處理在真實情況下肯定不會這么簡單角骤,真實的場景更加復雜,可能還涉及到仿射變換才能把圖像擺正心剥,前面的我就簡化了邦尊。

  1. 查找輪廓及數(shù)字定位
    初步的思路是對圖像進行輪廓查找,然后根據(jù)輪廓之間的拓撲結(jié)構(gòu)來尋找數(shù)字及其在9*9矩陣中的位置优烧。說到輪廓的拓撲結(jié)構(gòu)蝉揍,就不得不說下面要用到的這個函數(shù):
void findContours( InputOutputArray image, OutputArrayOfArrays contours,
                              OutputArray hierarchy, int mode,
                              int method, Point offset = Point());

image: 輸入圖像,應(yīng)該是二值圖像畦娄,如果不是又沾,會被當做二值圖像處理(即非零都被當做1或255)。
contours: 查找到的輪廓纷责,應(yīng)該存儲在vector<vector<Point>>里捍掺,每一條封閉的輪廓中的所有點會被當做一個vector<Point>來存儲。
hierarchy: 存儲圖像中的拓撲結(jié)構(gòu)再膳,規(guī)定如果一個輪廓被另外一個輪廓包含挺勿,則這兩個輪廓稱作父子輪廓,被包含者為子輪廓喂柒,存儲在vector<Vec4i>中不瓶,于contours中的對應(yīng),每一條輪廓都有這樣的一個拓撲信息表灾杰,由四位int型數(shù)字組成,第i個輪廓的拓撲信息為:

標志位 含義 不存在的話
hir[][0] 后一個輪廓編號 -1
hir[][1] 前一個輪廓編號 -1
hir[][2] 父輪廓編號 -1
hir[][3] 子輪廓編號 -1

mode:

       取值一:CV_RETR_EXTERNAL只檢測最外圍輪廓蚊丐,包含在外圍輪廓內(nèi)的內(nèi)圍輪廓被忽略
       取值二:CV_RETR_LIST   檢測所有的輪廓,包括內(nèi)圍艳吠、外圍輪廓麦备,但是檢測到的輪廓不建立等級關(guān)
              系,彼此之間獨立,沒有等級關(guān)系凛篙,這就意味著這個檢索模式下不存在父輪廓或內(nèi)嵌輪廓黍匾,
              所以hierarchy向量內(nèi)所有元素的第3、第4個分量都會被置為-1呛梆,具體下文會講到
       取值三:CV_RETR_CCOMP  檢測所有的輪廓锐涯,但所有輪廓只建立兩個等級關(guān)系,外圍為頂層填物,若外
              內(nèi)的內(nèi)圍輪廓還包含了其他的輪廓信息纹腌,則內(nèi)圍內(nèi)的所有輪廓均歸屬于頂層
       取值四:CV_RETR_TREE, 檢測所有輪廓滞磺,所有輪廓建立一個等級樹結(jié)構(gòu)升薯。外層輪廓包含內(nèi)層輪廓,內(nèi)        
               層輪廓還可以繼續(xù)包含內(nèi)嵌輪廓雁刷。

method:

    尋找輪廓的算法,具體有下面幾種可選:
    取值一:CV_CHAIN_APPROX_NONE 保存物體邊界上所有連續(xù)的輪廓點到contours向量內(nèi)
    取值二:CV_CHAIN_APPROX_SIMPLE 僅保存輪廓的拐點信息覆劈,把所有輪廓拐點處的點保存入contours
            向量內(nèi)保礼,拐點與拐點之間直線段上的信息點不予保留
    取值三和四:CV_CHAIN_APPROX_TC89_L1沛励,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain近
             似算法
    沒有什么特殊要求的話用這幾種算法得到的結(jié)果都是可以接受的,我們這里選用的是SIMPLE這種炮障。

offset:

    Point偏移量目派,所有的輪廓信息相對于原始圖像對應(yīng)點的偏移量,相當于在每一個檢測出的輪廓點上加
    上該偏移量胁赢,并且Point還可以是負值企蹭!
    這個偏移量可能是為了畫出一種類似于投影那樣的效果用的,一般情況用默認值0就可以了智末。

如何利用這些輪廓間的拓撲關(guān)系找到數(shù)字呢谅摄?
參考了別人的一些做法,采取如下策略:
首先找到最大的一個框系馆,即拓撲結(jié)構(gòu)下的0號輪廓送漠,然后找到以0號框為父輪廓的所有二級子輪廓,理想的條件下是有9_9=81個由蘑,這就是81個框闽寡,然后再找到有數(shù)字的框,有數(shù)字的框的特點是這些框還有子輪廓尼酿,而這些子輪廓就是我們要找的數(shù)字爷狈。

核心判斷代碼是這一句
if (hierarchy[i][3] == 0&&hierarchy[i][2]!=-1) //父輪廓是0號輪廓的話,就是小矩形裳擎,存在子輪廓涎永,則子輪廓是數(shù)字

81個小方框里面如果有輪廓的話,我們認為這個輪廓是數(shù)字,下面就是要定位這些數(shù)字羡微,一種直觀的方法是用最小矩形包圍數(shù)字支救,把數(shù)字摳圖摳出來,這樣也便于再后面的處理中對數(shù)字進行識別拷淘,這里要用到另外一個函數(shù):
Rect boundingRect( InputArray points );這個函數(shù)就非常簡單各墨,獲得一個點集的最小包圍矩形,而且是正矩形启涯,邊與坐標軸平行贬堵,不旋轉(zhuǎn),同樣有旋轉(zhuǎn)版本的函數(shù)可用结洼。
這樣的話如果在原圖中畫出黎做,是這樣的效果:

數(shù)字檢測

按照流程下面該做的應(yīng)該是識別數(shù)字了,先把這個問題放下松忍,做到這里的時候我發(fā)現(xiàn)另外一個問題蒸殿,那就是這些數(shù)字如何定位,現(xiàn)在我是得到了26個矩形鸣峭,但是這些矩形在原圖中的對應(yīng)位置是怎樣的宏所?這個我并不知道,而且我依靠拓撲結(jié)構(gòu)選出來的這26個矩形框并沒有一個拓撲關(guān)系可用摊溶,這里卡了有半個多小時爬骤。
最后我想出來一個比較暴力但是有效的方法:
通過矩形的質(zhì)心在整幅圖中的位置來確定這個數(shù)字到底是哪行那列的,這要求數(shù)獨圖像必須基本是正方形莫换,而且邊緣應(yīng)該盡可能的小霞玄。

設(shè)原圖寬度和高度分別是W和H,第i個矩形的質(zhì)心為(x,y).
則其索引應(yīng)該是: 
[i,j]=[x/W*9,y/H*9];
要注意的是拉岁,做乘除之前把int型的數(shù)據(jù)轉(zhuǎn)換為double坷剧,然后最后再轉(zhuǎn)為int,去掉小數(shù)部分(去尾喊暖,不四舍五入)

這樣惫企,可以得到對應(yīng)的位置索引和Rect信息,整合起來放到vector<pair<point,rect>>中備用哄啄。因為還沒有進行數(shù)字識別雅任,所以我把這些小矩形根據(jù)其坐標命名都保存了起來,驗證結(jié)果是否正確,結(jié)果讓我非常開心:


數(shù)字檢測結(jié)果

可以對照著原圖檢查一下,這些數(shù)字的檢測是完全正確的腔召,注意索引是從0開始的,下一步只要把這些圖片對應(yīng)的數(shù)字識別出來禽车,根據(jù)其索引值放在矩陣中寇漫,就可以調(diào)用解數(shù)獨的算法進行計算了。
大程序我就不放了殉摔,現(xiàn)在還是個測試版本的州胳,寫了很多調(diào)試用的輔助程序,放到git中逸月,鏈接見最上面栓撞。


二、數(shù)字識別

數(shù)字識別的話是打算用knn碗硬,因為問題相對比較簡單瓤湘,而且要求準確率也比較高。
knn是最簡單的一種機器學習的算法恩尾,可以用來分類也可以用來回歸弛说,opencv里有相應(yīng)的API可用,用到的時候再介紹翰意。關(guān)于knn算法詳解:knn
knn優(yōu)點和缺點都是比較突出的木人,優(yōu)點就是簡單有效,無需訓練冀偶。缺點是準確率一般醒第,復雜的問題往往束手無策,雖然無需訓練蔫磨,但是每一次分類都必須遍歷所有訓練樣本淘讥,當訓練樣本比較大的時候圃伶,計算量還是很客觀的堤如。數(shù)字識別這里主要有兩個任務(wù),第一窒朋,構(gòu)建訓練樣本搀罢,第二建立分類器進行分離,對于一般的機器學習算法來說侥猩,是有個訓練的過程的榔至,直到損失函數(shù)符合要求,才會進行預(yù)測欺劳,我們這里用的knn就可以省略掉訓練這個過程唧取,而且由于樣本實在太少,交叉驗證也去掉了划提。下面分別介紹枫弟。

  1. 訓練樣本制作
    c++有一個ml模塊,其中有TrainData這個類鹏往,里面介紹了其對訓練數(shù)據(jù)格式的要求淡诗。
    traindata

    簡單的使用只需要關(guān)注前三個參數(shù)。
參數(shù) 意義
samples matrix of samples. It should have CV_32F type.
layout see ml::SampleTypes.
responses matrix of responses. If the responses are scalar, they should be stored as a single row or as a single column. The matrix should have type CV_32F or CV_32S (in the former case the responses are considered as ordered by default; in the latter case - as categorical)

SampleTypes是采樣方式,行采樣或者列采樣韩容,鏈接里有宏定義對應(yīng)的名稱款违。
其中samplesresponses都是Mat型的數(shù)據(jù),分別是樣本和其對應(yīng)的標簽群凶。
samples應(yīng)該是CV_32F類型的數(shù)據(jù)插爹。

這個數(shù)據(jù)格式的要求非常嚴格,如果格式不對请梢,則訓練數(shù)據(jù)構(gòu)建就是不成功的递惋。這一點特別注意。一定要注意數(shù)據(jù)之間的格式轉(zhuǎn)換溢陪,為了識別簡單和帶來不必要的通道問題萍虽,最好一開始就將原圖灰度化或者灰度讀入,這是比較穩(wěn)妥的一種方法形真,c++里面調(diào)試沒有matlab或者Python那么簡單杉编,不要因為這些不注意的低級錯誤影響心情。
另外咆霜,數(shù)據(jù)格式要求一行或者一列是一個數(shù)據(jù)邓馒,所以在放入mat之前,應(yīng)該reshape()成一行或者一列蛾坯。Mat是支持push_back的光酣,一行一行地放入也比較簡單。

responses是樣本對應(yīng)的標簽脉课,應(yīng)該是一個一維向量救军,行或列均可,格式為CV_32F or CV_32S倘零,即32位浮點或者整型都可唱遭,我在64_release下用的int的也可以。為了保證萬無一失呈驶,還是轉(zhuǎn)換一下比較好拷泽。

了解了格式要求,來制作訓練數(shù)據(jù)袖瞻,首先先要找來圖像司致,因為要識別的是打印體,所以我在word里直接打了0-9十個數(shù)字聋迎,變換了十種不同的字體分別截圖脂矫,得到了初始的圖像數(shù)據(jù):


然后想辦法把每個數(shù)字提取出來,放在一個vector<vector<Mat>>矩陣里砌庄,最后的結(jié)果是這樣:

這里為了顯示方便我把它放在一個Mat里畫出來了羹唠,實際上放在vector<vector<Mat>>里就可以進行下一步制作了奕枢。

值得一提的是,opencv里自帶例程里給了手寫體識別的樣本佩微,就是以這樣一張圖片給出的缝彬,那里面是手寫體,樣本很多哺眯,長這樣:


手寫體樣本

怎么得到逐個數(shù)字簡單說一下思路:對于每一張圖像來說谷浅,從左至右有10個數(shù)字,先閾值化奶卓,查找輪廓一疯,沒有父輪廓的輪廓就是數(shù)字的輪廓,然后查找這些輪廓的最小包圍矩形夺姑,把這些矩形按照x坐標進行排序墩邀,排序之后的結(jié)果就是從0-9了,然后分別resize到20x20,放入vector<vector<Mat>>中就可以了盏浙。為此寫了個函數(shù)眉睹,如果有更多的訓練樣本圖像,還是可以加上去的废膘,修改一些參數(shù)就可以了竹海,除了解數(shù)獨的函數(shù),就一共只寫了這一個函數(shù)丐黄,這樣就導致我的主函數(shù)達到了300行斋配,一些測試用的代碼也沒有去掉,有時間再封裝封裝吧灌闺!

void getTrainImg(vector<vector<Mat>>  &TrainImgMat)
{
    Mat tmp_srcImg;
    Mat tmp_Img_thresh;
    vector<vector<Point>> cons;
    vector<Vec4i>  hies;

    vector<Rect>  img0_9;
    for (int i = 0; i < 10; i++)
    {
        //讀入艰争,閾值化
        tmp_srcImg = imread(".\\TrainImg\\" + to_string(i) + ".jpg",0);     
        tmp_srcImg.copyTo(tmp_Img_thresh);        //拷貝一份,閾值化是在原圖上做的
        
        
        threshold(tmp_Img_thresh, tmp_Img_thresh, 150, 255, CV_THRESH_BINARY_INV);
        //3*3十字膨脹
        auto kernel=getStructuringElement(CV_SHAPE_CROSS, Size(3, 3));
        dilate(tmp_Img_thresh, tmp_Img_thresh, kernel);
        //查找輪廓
        findContours(tmp_Img_thresh, cons, hies, RETR_TREE, CHAIN_APPROX_SIMPLE);
        
        //篩選沒有父輪廓的輪廓為數(shù)字菩鲜,并求其最小包圍矩形
        for (int j = 0; j < hies.size(); j++)
        {
            if (hies[j][3] == -1)
            {
                img0_9.push_back(boundingRect(cons[j]));
            }
        }
        //排序园细,按照矩形包圍圈的x坐標排序
        sort(img0_9.begin(), img0_9.end(), [](Rect &a, Rect &b)->bool {return a.x < b.x; });     
        
        
        for (int k = 0; k < img0_9.size(); k++)
        {
            Mat img20;
            
            resize(tmp_srcImg(img0_9[k]), img20, Size(20, 20));
            TrainImgMat[i].push_back(img20);    
        }
        //清除內(nèi)存空間,下次用
        cons.clear();
        hies.clear();
        img0_9.clear(); 
    }
}

然后制作訓練數(shù)據(jù)和標簽接校,也比較簡單:

Mat trainData;
    Mat Label;

    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            trainData.push_back(TrainImgMat[j][i].reshape(0,1));
            //imshow(" s", TrainImgMat[j][i]);
            Label.push_back(i);
        }
    }

    imshow("das",trainData);

如果要顯示的話長這樣:

總共是100個數(shù)據(jù),每個數(shù)據(jù)長度是400狮崩。Label順便也賦值了蛛勉。
到這里訓練數(shù)據(jù)的準備工作就算做好了,下面是訓練數(shù)據(jù)制作睦柴,一句代碼就搞定了:(記得轉(zhuǎn)換格式)

//轉(zhuǎn)換成浮點備用
trainData.convertTo(trainData, CV_32F);           //格式轉(zhuǎn)化必不可少
Ptr<TrainData> tData = TrainData::create(trainData, ROW_SAMPLE, Label); 
  1. knn構(gòu)造和求解
    待檢測的數(shù)據(jù)我是存放在一個Mat里了诽凌,和訓練的數(shù)據(jù)一樣,進行了reshape()操作坦敌。
//Numbers是vector<Mat>類型侣诵,是檢測出來的數(shù)字圖片
    Mat TestData;
    for (auto nums:Numbers)
    {
        TestData.push_back(nums.reshape(0, 1));
    }
    
    TestData.convertTo(TestData, CV_32F);

下面簡單介紹一下KNearest這個類痢法,詳細要看這里

KNearest這個類在opencv的ml模塊中,要使用的話要包含#include<opencv2\ml.hpp>,其中各種命名都包含在cv::ml這個命名空間之中杜顺。

官方的參考程序一般會直接聲明成一個智能指針财搁,然后再進行其他的操作:
成員函數(shù):
static Ptr< KNearest > create ()
還有一些截圖在這里:

image.png

返回類型 函數(shù) 功能
virtual float findNearest (InputArray samples, int k, OutputArray results, OutputArray neighborResponses=noArray(), OutputArray dist=noArray()) const =0 預(yù)測
virtual int getAlgorithmType () const =0 獲取算法類型
virtual int getDefaultK () const =0 獲取默認k值
virtual int getEmax () const =0 KDtree實現(xiàn)的參數(shù)(不懂)
virtual bool getIsClassifier () const =0 是否是分類器
virtual void setAlgorithmType (int val)=0 設(shè)置算法類型
virtual void setDefaultK (int val)=0 設(shè)置k值
virtual void setEmax (int val)=0 設(shè)置KDtree實現(xiàn)的參數(shù)
virtual void setIsClassifier (bool val)=0 設(shè)置做分類還是回歸

還有兩個繼承來的常用的函數(shù):


一個訓練一個預(yù)測,其中訓練的函數(shù)是重載過的躬络〖獗迹可以現(xiàn)場構(gòu)造訓練數(shù)據(jù),也可以用Prt<TrainData>.
預(yù)測的話samples是帶預(yù)測數(shù)據(jù)穷当,可以是一行提茁,返回值就是一個float,我的程序中用的就是這個馁菜。
還要介紹一下最上面的一個函數(shù):
findNearest (InputArray samples, int k, OutputArray results, OutputArrayneighborResponses=noArray(), OutputArray dist=noArray()) const =0
這個函數(shù)有三個參數(shù)茴扁,一個是數(shù)據(jù),一個是k值汪疮,一個是響應(yīng)值丹弱,可以批量計算,得到的結(jié)果儲存在一個Mat里铲咨,這里的k可以設(shè)置的和creat()時不同躲胳。
把我用到knn的時候的代碼放到這里便于理解這些函數(shù)的用法,我也是參考的官方程序和一些博客的寫法纤勒,一個典型的knn的初始化坯苹,訓練和測試應(yīng)該是這樣的:

//---------【knn分類器設(shè)置,k=3,分類打開】-----------------
    Ptr<TrainData> tData = TrainData::create(trainData, ROW_SAMPLE, Label);          //

    Ptr<ml::KNearest> knn = ml::KNearest::create();
    knn->setDefaultK(4);        
    knn->setIsClassifier(true);
    knn->train(tData);



    //---【檢測方法1】--------
    /*Mat predictRes;
    knn->findNearest(TestData, 4, predictRes);
    cout << predictRes;*/
    

    //----【檢測方法2】-----------------
    vector<int>  PredicrRes;
    for (int i = 0; i < TestData.rows; i++)
    {
        Mat tmp = TestData.row(i);
        int response = knn->predict(tmp);
        PredicrRes.push_back(response);
        //cout << response <<"\t"<<i<< endl;

    }

這里就檢測的部分就完成了摇天,結(jié)合上面存儲的這些數(shù)字在圖像中的位置粹湃,我們把檢測到的數(shù)據(jù)打印上去可以看一下是否正確:



這里k取的4,這里k的取值還是挺敏感的泉坐,因為訓練樣本確實太少为鳄。而數(shù)獨的特殊性也要求不能有檢測錯誤,一旦檢測錯誤數(shù)獨可能就無解腕让。

三.數(shù)獨求解及結(jié)果顯示孤钦。

  1. 數(shù)獨求解
    首先根據(jù)上面的檢測結(jié)果來重構(gòu)數(shù)獨矩陣,這就比較簡單了,因為在第一部分我們已經(jīng)獲得了所有的位置纯丸,只需要把一個全零矩陣的對應(yīng)位置寫上數(shù)字就可以了:
vector<vector<int>> ShuDuMat(9,vector<int>());
    //全零矩陣
    for (int i = 0; i < 9; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            ShuDuMat[i].push_back(0);

        }
    }
      //對應(yīng)位置賦值
    for (int i = 0; i < PredicrRes.size(); i++)
    {
        ShuDuMat[PosOfNum[i].y][PosOfNum[i].x] = PredicrRes[i];
    }

重構(gòu)的矩陣:


重構(gòu)矩陣

然后就是矩陣求解了偏形,這個網(wǎng)上的程序很多,用的是所謂的回溯法觉鼻,重點不在這里我也沒細看俊扭,可以參考這里,我就是復制的這個網(wǎng)頁的代碼,加了個矩陣的接口坠陈。
求解的結(jié)果(順便檢測了一下):

  1. 結(jié)果顯示
    按理說到上面就可以結(jié)束了萨惑,不過還是想把結(jié)果顯示到原圖上捐康,這樣才完整一些,現(xiàn)在數(shù)字有了庸蔼,圖也有解总,唯一的一個困難就是原圖上81個方框的位置,在第一步檢測的時候我們是把81個輪廓檢測出來了朱嘴,但是這些輪廓在存儲的時候并沒有按照一個空間的順序倾鲫,所以有必要解決這個問題:
    最后的方法:
    ①求出81個輪廓的最小包圍矩形。
    ②81個矩形按照y坐標進行排序萍嬉,這樣從第一個開始乌昔,每九個應(yīng)該是一行。
    ③81個矩形分別存儲到一個vector<Rect>中壤追,這樣的話每一個應(yīng)該對應(yīng)的是一行磕道。整體放入vector<vector<Rect>>
    ④對③得到的個矩陣中的每一行vector<Rect>按照x坐標進行排序,這樣就對應(yīng)原圖中從左至右行冰。
    ⑤根據(jù)上面得到vector<vector<Rect>>和得到的數(shù)獨結(jié)果溺蕉,顯示在原圖上。
//---------【獲得81個矩形對應(yīng)的位置悼做,矩陣的位置和原圖位置對應(yīng)】----------
    vector<Rect>  Pos;
    for (int i = 0; i < hierarchy.size(); i++)
    {
        Rect tmp;
        if (hierarchy[i][3] == 0)
        {
            Pos.push_back(boundingRect(contours[i]));
        }
    }
    //按照y來排序疯特,一排一排都鏈在一起
    sort(Pos.begin(), Pos.end(), [](Rect &a, Rect &b)->bool{return a.y < b.y; });
    cout << "小矩形的個數(shù)是" << Pos.size() << endl;

    vector<vector<Rect>> PosMat(9,vector<Rect>());
    int index = 0;
    for (int i = 0; i < 81; i+=9)
    {
        PosMat[index++].assign(Pos.begin() + i, Pos.begin() + i + 9);    //賦值每一行
    }
    
    for (int i=0;i<9;i++)
    {
        sort(PosMat[i].begin(), PosMat[i].end(), [](Rect &a, Rect &b)->bool {return a.x < b.x; });  //每一行按照x排列
        cout << PosMat[i].size() << endl;
    }


   //把數(shù)字寫到圖上
    Mat ResultImg;
    cvtColor(srcImg, ResultImg, CV_GRAY2BGR);
    for (int i = 0; i < 9; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            string text = to_string(res[i][j]);
            putText(ResultImg, text, Point(PosMat[i][j].x, PosMat[i][j].y+15), FONT_ITALIC, 1, Scalar(0, 0, 255), 2);   
        }
        
    }

    imshow("最終求解結(jié)果", ResultImg);

最終的結(jié)果:


四、總結(jié)

總共花了大概三個晚上的時間肛走,包括寫這個筆記漓雅,總算把數(shù)獨求解的這個流程走下來了,其中還是遇到了一些困難的朽色,卡的最久的地方是TrainData這個格式不對構(gòu)造不成功邻吞,最后還是看著官方的例程找到了問題。寫完這個感覺學到了很多葫男,比如輪廓查找的這個函數(shù)抱冷,比如knn的這個API。
這還是一個非常脆弱的版本梢褐,主要有三地方:
①數(shù)字提取旺遮,因為用的是找輪廓的方法,目前也只能針對于比較規(guī)整清晰的圖像進行處理利职。
②knn的樣本太少趣效,一共才100個,主要還是著急看最后的結(jié)果猪贪,手動做訓練數(shù)據(jù)還是挺費事的。
③程序略亂讯私,主要還是因為著急看結(jié)果热押,做一步檢查一步西傀,基本所有的代碼都寫在主函數(shù)里了,不過注釋的還算比較清楚桶癣。還有些輔助的調(diào)試輸出代碼我也保留了拥褂,反正是自己做著玩的。

再接再厲吧牙寞!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饺鹃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子间雀,更是在濱河造成了極大的恐慌悔详,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惹挟,死亡現(xiàn)場離奇詭異茄螃,居然都是意外死亡,警方通過查閱死者的電腦和手機连锯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門归苍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人运怖,你說我怎么就攤上這事拼弃。” “怎么了摇展?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵吻氧,是天一觀的道長。 經(jīng)常有香客問我吗购,道長医男,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任捻勉,我火速辦了婚禮镀梭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘踱启。我一直安慰自己报账,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布埠偿。 她就那樣靜靜地躺著透罢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冠蒋。 梳的紋絲不亂的頭發(fā)上羽圃,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音抖剿,去河邊找鬼朽寞。 笑死识窿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的脑融。 我是一名探鬼主播喻频,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肘迎!你這毒婦竟也來了甥温?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妓布,失蹤者是張志新(化名)和其女友劉穎姻蚓,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秋茫,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡史简,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了肛著。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圆兵。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖枢贿,靈堂內(nèi)的尸體忽然破棺而出殉农,到底是詐尸還是另有隱情,我是刑警寧澤局荚,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布超凳,位于F島的核電站,受9級特大地震影響耀态,放射性物質(zhì)發(fā)生泄漏轮傍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一首装、第九天 我趴在偏房一處隱蔽的房頂上張望创夜。 院中可真熱鬧,春花似錦仙逻、人聲如沸驰吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檬贰。三九已至,卻和暖如春缺亮,著一層夾襖步出監(jiān)牢的瞬間翁涤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迷雪,地道東北人限书。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓虫蝶,卻偏偏與公主長得像章咧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子能真,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355