opencv基本圖片操作:
因?yàn)閛pencv有2.0 和 3.0 的版本區(qū)別,所以網(wǎng)上搜到的函數(shù)或類型都是兩種格式,建議用新版的,什么impImage* 類型的都是2.0版本的寫法,我全部使用的是Mat。一定要統(tǒng)一好,不要一會新的一會舊的,會報錯的。
讀圖片imread,顯示imshow,等待waitKey等等,這些要先熟悉
opencv的強(qiáng)大之處在于幾乎所有的圖像操作它都有現(xiàn)成的函數(shù)可供調(diào)用,非常方便。多谷歌,一定會有函數(shù)已經(jīng)實(shí)現(xiàn)了你想完成的功能。
二值化:不論是原圖還是有劃痕或噪點(diǎn)的圖,背景都不干凈,這對識別的影響還是挺不好的,所以要先二值化,把黑白像素點(diǎn)區(qū)分的開一些。但是圖片右側(cè)明顯要比左側(cè)更暗,所以在閾值選取的時候比較難辦,很難用一個固定的值將兩部分圖像都二值化得很理想,所以就用到了逼格更高的自適應(yīng)二值化(adaptiveThreshold),tips:二值化前先直方圖均衡一下效果會更好。
中值濾波:針對有噪點(diǎn)和有劃痕的圖像,中值濾波是非常好的處理方案,中值的參數(shù)可調(diào),可以很好的消除噪音的影響。缺點(diǎn)就是參數(shù)不好調(diào)啊,調(diào)的想死。。
模板匹配:模板的來源可以是自己從待識別的圖片中摳圖,不過我們作業(yè)提供了模板圖片,所以這一步就可以省掉了。opencv提供了非常強(qiáng)大的matchTemplate函數(shù),可以將給定圖片與模板按照你規(guī)定的計(jì)算方法計(jì)算一個相似度的值,并將對應(yīng)的坐標(biāo)存儲下來,你需要做的只是將值比較大(或小,與你規(guī)定計(jì)算相似度的函數(shù)有關(guān))的圖像框出來即可
窗口掃描:為了提高識別率,我設(shè)定了一個窗口對原圖進(jìn)行掃描,掃描窗口的移動設(shè)定了一點(diǎn)規(guī)則,就是如果前一個窗口沒有匹配到數(shù)字就微調(diào)窗口位置,如果匹配到數(shù)字就將窗口左軸移動到匹配到的數(shù)字的右側(cè),再重復(fù)掃描。
以下是基于OpenCV實(shí)現(xiàn)簡單的數(shù)字識別。這里以游戲Angry Birds為例,通過以下幾個主要步驟對其中右上角的分?jǐn)?shù)部分進(jìn)行自動識別。
1. 學(xué)習(xí)分類器
根據(jù)訓(xùn)練樣本,選取模型訓(xùn)練產(chǎn)生數(shù)字分類器。這里的樣本可以是通用的數(shù)字樣本庫(如NIST等),也可以是針對應(yīng)用場景而制作的專門訓(xùn)練樣本。前者優(yōu)在泛化性,后者強(qiáng)在準(zhǔn)確率,當(dāng)然常用做法是將這兩者結(jié)合,即在通用數(shù)字庫基礎(chǔ)上做修改。另外這里由于模式并不復(fù)雜,計(jì)算量也不大,所以不對樣本進(jìn)行特征提取,對原始樣本作簡單變換后直接作為訓(xùn)練樣本。
具體地,首先是生成訓(xùn)練樣本矩陣,一般樣本是以二維矩陣的方式存在文件當(dāng)中,現(xiàn)在要將它們讀出來,進(jìn)行適當(dāng)?shù)念A(yù)處理,然后生成OpenCV能理解的數(shù)據(jù)結(jié)構(gòu)。
train_X = cvCreateMat(sample_num * class_num, size * size, CV_32FC1);
train_Y = cvCreateMat(sample_num * class_num, 1, CV_32FC1);
for(i = 0; i 《 class_num; i++){
for(j = 0; j 《 sample_num; j++){
src_image = cvLoadImage(file,0);
pimage = preprocessing(src_image, size, size);
。。。
cvGetRow(train_X, &row, i * sample_num + j);
row_vec = cvReshape(&data, &mathdr, 0, 1);
cvCopy(row_vec, &row, NULL);
。。。
cvGetRow(train_Y, &row, i * sample_num + j);
cvSet(&row, cvRealScalar(i));
}
}
訓(xùn)練樣本中的數(shù)字位置形態(tài)各異,因此讀入時需要進(jìn)行規(guī)整化。主要方法是先找到數(shù)字的邊界框,然后以寬和高中大的一邊為基準(zhǔn)進(jìn)行縮放和拉伸,從而使得其可以占滿整個表示單個樣本的矩陣。
IplImage preprocessing(IplImage* img, int w, int h){
。。。
bb = findBoundingBox(img);
cvGetSubRect(img, &data, cvRect(bb.x, bb.y, bb.width, bb.height));
size = (bb.width 》 bb.height) ? bb.width : bb.height;
res = cvCreateImage(cvSize(size, size), 8, 1);
x = floor((float)(size - bb.width) / 2.0f);
y = floor((float)(size - bb.height) / 2.0f);
cvGetSubRect(res, &subdata, cvRect((int)x, (int)y, bb.width, bb.height));
cvCopy(&data, &subdata, NULL);
ret = cvCreateImage(cvSize(w, h), 8, 1);
cvResize(res, ret, CV_INTER_NN);
return *ret;
}
假設(shè)單個樣本可表示為0/1矩陣,那findBoundingBox()只要從x和y方向分別掃描最大最小的非0值就可以了。 訓(xùn)練樣本準(zhǔn)備好后,在OpenCV中創(chuàng)建相應(yīng)的分類器非常方便。這里用的是KNN,當(dāng)然除了KNN外還有其它很多封裝好的分類器(如NN, SVM等)。
knn = new CvKNearest(train_X, train_Y, 0, false, K);
2. 圖像預(yù)處理
前面通過學(xué)習(xí)產(chǎn)生了分類器,但我們輸入圖像中的數(shù)字并不能直接作為測試輸入。圖像中的數(shù)字筆畫有時并不規(guī)整,還可能相互重疊。因?yàn)楸疚睦訛榱撕喕玫氖瞧聊唤貓D,所以位置形變校正,色彩亮度校正等等都省去了,但仍需要一些簡單處理。下面先對輸入圖像進(jìn)行一把簡單的預(yù)處理,主要目的是將數(shù)字之間兩兩分開。方法很簡單,首先將圖像轉(zhuǎn)成二值圖,然后腐蝕一把,數(shù)字之間就分離得比較開了,這樣便于我們下一步分割和識別。這樣做還有個好處,就是把其余的噪聲也順帶去掉了。
cvtColor(input, out_img, CV_BGR2GRAY);
threshold(out_img, out_img, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
。。。
erode(out_img, out_img, elem);
結(jié)果:
3. 圖像分割
接下來,就可以對圖像進(jìn)行分割了。由于我們的分類器只能對數(shù)字一個一個地識別,所以首先要把每個數(shù)字分割出來。基本思想是先用findContours()函數(shù)把基本輪廓找出來,然后通過簡單驗(yàn)證以確認(rèn)是否為數(shù)字的輪廓。對于那些通過驗(yàn)證的輪廓,接下去會用boundingRect()找出它們的包圍盒。
vector《 vector《 Point》 》 contours;
findContours(contour_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
vector《vector《Point》 》::iterator it = contours.begin();
while (it!=contours.end()) {
RotatedRect rect = minAreaRect(Mat(*it));
if(verifyRect(rect)){
++it; // A valid rectangle found
} else {
it= contours.erase(it);
}
}
。。。
vector《Rect》 boundRect(contours.size());
for (int i = 0; i 《 contours.size(); ++i) {
Scalar color = Scalar(200, 200, 200);
boundRect[i] = boundingRect(Mat(contours[i]));
rectangle(out_img, boundRect[i].tl(), boundRect[i].br(), color, 0.2, 8, 0);
CvRect roi = CvRect(boundRect[i]);
IplImage orig = out_img;
IplImage *res = cvCreateImage(cvSize(roi.width, roi.height), orig.depth, orig.nChannels);
cvSetImageROI(&orig, roi);
cvCopy(&orig, res);
cvResetImageROI(&orig);
IplImage *bininv_img;
bininv_img = cvCreateImage(cvSize(128, 128), IPL_DEPTH_8U, 1);
cvResize(res, bininv_img);
cvThreshold(bininv_img, bininv_img, 100, 255, CV_THRESH_BINARY_INV);
int ret = do_ocr(bininv_img);
res_elem elem;
elem.num = ret;
elem.xpos = boundRect[i].tl().x;
res_vec.push_back(elem);
。。。
}
結(jié)果:
4. 應(yīng)用分類器
分割完后就可以應(yīng)用我們前面訓(xùn)練好的分類器對分割結(jié)果進(jìn)行識別了。當(dāng)然,如果感覺結(jié)果不滿意,可以將分類錯誤的樣本加上正確的標(biāo)簽后放入訓(xùn)練樣本重新生成分類器,使得分類器能夠有更好的識別率。上一步中的do_ocr()函數(shù)就是利用先前訓(xùn)練好的分類器識別單個數(shù)字。注意訓(xùn)練樣本進(jìn)行過怎么樣的預(yù)處理,這里也一樣要做。
int do_ocr(IplImage *img)
{
。。。
pimage = preprocessing(img, size, size);
。。。
cvGetSubRect(pimage, &data, cvRect(0, 0, size, size));
CvMat mathdr, *vec;
vec = cvReshape(&data, &mathdr, 0, 1);
ret = knn-》find_nearest(vec, K, 0, 0, nearest, 0);
return (int)ret;
}
5. 后期處理
因?yàn)榉指顖D像時查找數(shù)字輪廓并不保證是按順序來的,所以這兒要將識別結(jié)果按分割時輸出的包圍盒位置信息進(jìn)行排序,最后將它們轉(zhuǎn)換成數(shù)字輸出
sort(res_vec.begin(), res_vec.end(), sort_func);
int j, num = 0;
for (j = 0; j 《 res_vec.size(); ++j) {
num = num * 10 + res_vec[j].num;
}
char resbuf[256];
sprintf(resbuf, “%d”, num);
putText(show_img, resbuf, Point(OUTPUT_X, OUTPUT_Y), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 255, 0), 2);
imshow(“show”, show_img);
結(jié)果:
opencv3實(shí)現(xiàn)簡單的數(shù)字圖像識別(KNN)完整代碼如下:
// knnrecognizenum.cpp:使用knn識別手寫數(shù)字
//
#include “stdafx.h”
#include《iostream》
#include《opencv2mlml.hpp》
#include《highguihighgui.hpp》
using namespace std;
using namespace cv;
using namespace cv::ml;
int main()
{
Mat img = imread(“digits.png”, 0);
int boot = 20;
int m = img.rows / boot; int n = img.cols / boot;
Mat data, labels; //data和labels分別存放
//截取數(shù)據(jù)的時候要按列截取
for (int i = 0; i 《 n; i++)
{
int colNum = i * boot;
for (int j = 0; j 《 m; j++)
{
int rowNum = j * boot;
Mat tmp;
img(Range(rowNum, rowNum + boot), Range(colNum, colNum + boot)).copyTo(tmp);
data.push_back(tmp.reshape(0, 1)); //將圖像轉(zhuǎn)成一維數(shù)組插入到data矩陣中
labels.push_back((int)j / 5); //將圖像對應(yīng)的標(biāo)注插入到labels矩陣中
}
}
data.convertTo(data, CV_32F);
int sampleNum = data.rows;
int trainNum = 3000;
Mat trainData, trainLabel;
trainData = data(Range(0, trainNum), Range::all());
trainLabel = labels(Range(0, trainNum), Range::all());
//使用KNN算法
int k = 5;
Ptr《TrainData》 tData = TrainData::create(trainData,ROW_SAMPLE, trainLabel); //ROW_SAMPLE表示一行一個樣本
Ptr《KNearest》 model = KNearest::create();
model-》setDefaultK(k); model-》setIsClassifier(true);
model-》train(tData);
//預(yù)測分類
/* Mat sample = data.row(500);
float res = model-》predict(sample);
cout 《《 “預(yù)測結(jié)果是:”《《 res 《《 endl;*/ //預(yù)測一個的代碼
double train_hr=0, test_hr=0;
Mat response;
for (int i = 0; i 《 sampleNum; i++)
{
Mat sample = data.row(i);
float r = model-》predict(sample);
r = abs(r - labels.at《int》(i));
if (r 《= FLT_EPSILON)// FLT_EPSILON表示最小的float浮點(diǎn)數(shù),小于它,就是等于0
r = 1.f;
else
r = 0.f;
if (i 《 trainNum)
train_hr=train_hr+r;
else
test_hr=test_hr + r;
}
//cout 《《 train_hr 《《 “ ” 《《 test_hr 《《 endl;
cout 《《 “KNN模型在訓(xùn)練集上的準(zhǔn)確率為” 《《 train_hr / trainNum * 100 《《 “%,在測試集上的準(zhǔn)確率為” 《《 test_hr / (data.rows-trainNum)*100《《“%”《《endl;
system(“pause”);
return 0;
}
評論
查看更多