C++ OpenCV實(shí)戰(zhàn)之文檔照片轉(zhuǎn)換成掃描文件
一、背景
前段時(shí)間都是基于Python的OpecCV進(jìn)行一些學(xué)習(xí)和實(shí)踐,但小的知識(shí)點(diǎn)并沒(méi)有應(yīng)用到實(shí)際的項(xiàng)目中;并且基于Python的版本的移植性、效率性都較差,在包含硬件的項(xiàng)目中往往都是采用基于C++的版本;
因此本次項(xiàng)目實(shí)戰(zhàn)專(zhuān)題主要是基于C++的版本,并且從大的任務(wù)中剖析小的知識(shí)點(diǎn),實(shí)際項(xiàng)目中算法的選型也是比較難的部分,根據(jù)需求和任務(wù)選用不同的算法,這才是真的掌握了知識(shí)點(diǎn);
本次項(xiàng)目是照片轉(zhuǎn)掃描文件,可以參考下面網(wǎng)址中的案例:
這已經(jīng)是一個(gè)落地且成熟的項(xiàng)目了,下面將結(jié)合多個(gè)知識(shí)點(diǎn)進(jìn)行實(shí)現(xiàn);
二、基礎(chǔ)知識(shí)
首先需要明確實(shí)現(xiàn)該任務(wù)的幾個(gè)步驟:
1、視角轉(zhuǎn)換:不規(guī)則四邊形轉(zhuǎn)變?yōu)榫匦危ㄊ褂猛敢曌儞Q算法);
2、背景降噪:去掉圖中的一些噪聲點(diǎn)(使用中值濾波算法);
3、顏色變換:二值化圖像,使得呈現(xiàn)黑白掃描圖(使用自適應(yīng)高斯閾值算法);
投影變換算法介紹:
圖1代表仿射變換,只需要6個(gè)自由度,并且約束條件是原來(lái)平行的線(xiàn)依舊平行,只需三對(duì)點(diǎn)對(duì)就可以求得未知參數(shù)值;
圖4代表投影變換,需要四對(duì)點(diǎn)對(duì),有8個(gè)自由度,可以將任意四邊形變換為指定的四邊形形狀;
降噪算法介紹:
中值濾波算法示意圖:
找到滑窗中中值的數(shù),替換中間區(qū)域的數(shù)值;
原理上是將一些較亮、較暗的點(diǎn)進(jìn)行降噪,也就是降噪的作用;
像高斯濾波和均值濾波,考慮到一個(gè)全局信息,是起到一個(gè)平滑的作用;
三、方案一:自動(dòng)檢測(cè)點(diǎn)
1、讀取圖片文件(進(jìn)行了指定尺寸縮放)
// 讀取圖片 Mat readFile(String imagePath, int minEdge = 1080) { Mat image = imread(imagePath); int width = image.size().width; int height = image.size().height; int minline = min(width, height); float ratio = minEdge * 1.0 / minline; // 得到縮放比例 int processWith = width * ratio; int peocessHeight = height * ratio; Mat resultImg; // 保存處理后圖像 resize(image, resultImg, Size(processWith, peocessHeight)); return resultImg; }
這里再定義一個(gè)顯示圖像的方法:
// 顯示圖片 void visualize(String winName, Mat image) { namedWindow(winName, WINDOW_NORMAL); imshow(winName, image); waitKey(0); }
2、創(chuàng)建直線(xiàn)類(lèi)并計(jì)算兩條直線(xiàn)的交點(diǎn)
先定義一個(gè)直線(xiàn)類(lèi),包含兩個(gè)端點(diǎn);
假設(shè)已知兩條直線(xiàn)上的兩點(diǎn),怎么求得交點(diǎn)呢?
可以參考這個(gè)網(wǎng)址中的數(shù)學(xué)公式:https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
// 返回兩條直線(xiàn)的交點(diǎn) Point2f linesIntersect(Line lin1, Line lin2) { // 這里直接根據(jù)網(wǎng)上的數(shù)學(xué)公式求得 int x1 = lin1.p1.x, y1 = lin1.p1.y; int x2 = lin1.p2.x, y2 = lin1.p2.y; int x3 = lin2.p1.x, y3 = lin2.p1.y; int x4 = lin2.p2.x, y4 = lin2.p2.y; float D = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (fabs(D) > 1e-6) { return Point2f( ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / D, ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / D); } return Point2f(-1, -1); }
3、圖像邊緣檢測(cè)Canny
Mat canny, gray; cvtColor(image, gray, COLOR_BGR2GRAY); double threshold_low = threshold(gray, canny, 0, 255, THRESH_BINARY | cv::THRESH_OTSU); Canny(gray, canny, threshold_low, threshold_low * 2); return canny;
注意:進(jìn)行Canny邊緣檢測(cè)的效果和圖像的大小有關(guān),可以適當(dāng)對(duì)大圖進(jìn)行縮放;
4、通過(guò)霍夫變換進(jìn)行直線(xiàn)檢測(cè)
檢測(cè)到的直線(xiàn)分為兩類(lèi),一類(lèi)是水平線(xiàn),一類(lèi)是豎直線(xiàn);
為了得到外邊緣框的直線(xiàn),可以先根據(jù)直線(xiàn)的外接矩形長(zhǎng)寬比分為水平和豎直線(xiàn),再根據(jù)中點(diǎn)的位置,找到邊緣直線(xiàn);
如上圖所示,如果x>y,則將該直線(xiàn)分為水平線(xiàn),如果y>x,則將該直線(xiàn)劃分為水平線(xiàn);
隨后再根據(jù)中心點(diǎn)的坐標(biāo)大小確定邊緣線(xiàn);
// 進(jìn)行霍夫直線(xiàn)檢測(cè),保存所有檢測(cè)到的直線(xiàn),并且確保直線(xiàn)大于4條 vector<Vec4i> lines; // 這里的參數(shù)需要根據(jù)圖像大小等因素進(jìn)行微調(diào) HoughLinesP(canny, lines, 1, CV_PI / 180, 80, height/5, 200); if (lines.size() < 4) { cout << "Find only" << lines.size() << "lines, return directly" << endl; } // 將直線(xiàn)分為水平線(xiàn)和垂直線(xiàn) vector<Line> horizontals, verticals; Mat tmp = image.clone(); for (auto v : lines) { double w = fabs(v[0] - v[2]), h = fabs(v[1] - v[3]); Line tmp_line(Point(v[0], v[1]), Point(v[2], v[3])); if (w > h) horizontals.push_back(tmp_line); else verticals.push_back(tmp_line); // 下面兩行代碼是實(shí)現(xiàn)繪制直線(xiàn) //line(tmp, Point(v.val[0], v.val[1]), Point(v.val[2], v.val[3]), Scalar(255, 0, 0), 8); //visualize("hough test", tmp); } // 確保水平線(xiàn)和垂直線(xiàn)至少有兩條 if (horizontals.size() < 2 || verticals.size() < 2) { cout << "Not enough edge lines... " << endl; } // 將水平和垂直線(xiàn)按中心點(diǎn)位置進(jìn)行排序,這里的兩個(gè)排序函數(shù)需要自己實(shí)現(xiàn) sort(horizontals.begin(), horizontals.end(), cmpHeight); sort(verticals.begin(), verticals.end(), cmpWidth); // 繪制出找到的直線(xiàn) line(tmp, horizontals[0].p1, horizontals[0].p2, Scalar(255, 0, 0), 8); line(tmp, horizontals[horizontals.size()-1].p1, horizontals[horizontals.size() - 1].p2, Scalar(255, 0, 0), 8); line(tmp, verticals[0].p1, verticals[0].p2, Scalar(255, 0, 0), 8); line(tmp, verticals[verticals.size()-1].p1, verticals[verticals.size() - 1].p2, Scalar(255, 0, 0), 8); visualize("hough test", tmp);
排序函數(shù)的實(shí)現(xiàn):
// 對(duì)水平線(xiàn)進(jìn)行排序 bool cmpHeight(const Line& l1, const Line& l2) { return l1.center.y < l2.center.y; } // 對(duì)垂直線(xiàn)進(jìn)行排序 bool cmpWidth(const Line& l1, const Line& l2) { return l1.center.x < l2.center.x; }
5、求單應(yīng)性矩陣
現(xiàn)在已知圖像上目標(biāo)的四個(gè)點(diǎn)坐標(biāo),通過(guò)點(diǎn)對(duì)關(guān)系,求得二者之間的單應(yīng)性變換矩陣;
int dst_width = 1080, dst_height = 1920; vector<Point2f> dst_pts; dst_pts.push_back(Point(0, 0)); dst_pts.push_back(Point(dst_width - 1, 0)); dst_pts.push_back(Point(0, dst_height - 1)); dst_pts.push_back(Point(dst_width - 1, dst_height - 1)); Mat warpedImg = Mat::zeros(dst_height, dst_width, CV_8UC3); Mat homo = getPerspectiveTransform(ori_pts, dst_pts); warpPerspective(image, warpedImg, homo, warpedImg.size()); visualize("result", warpedImg);
結(jié)果圖:
6、降噪和二值化
降噪采用中值濾波,閾值過(guò)濾采用自適應(yīng)的二值化;
void postProcess(Mat& image) { medianBlur(image, image, 7); cvtColor(image, image, COLOR_BGR2GRAY); threshold(image, image, 0, 255, THRESH_BINARY | cv::THRESH_OTSU); }
四、方案二:用戶(hù)點(diǎn)選目標(biāo)區(qū)域
方案一是完全基于自動(dòng)化的方式,用戶(hù)只需要傳入圖像,程序會(huì)自動(dòng)選擇合適的區(qū)域;
優(yōu)點(diǎn)在于其節(jié)省了用戶(hù)的人工成本,使得程序更為簡(jiǎn)便;
缺點(diǎn)在于算法具有局限性,對(duì)背景及區(qū)域選取有要求,比如不能在區(qū)域外出現(xiàn)干擾物體,也無(wú)法滿(mǎn)足用戶(hù)的一些特別需求,比如選定大區(qū)域中的小區(qū)域;
方案二的優(yōu)勢(shì)在于其強(qiáng)大的靈活性,用戶(hù)可以根據(jù)自己的需求選擇相應(yīng)的區(qū)域,程序?qū)?duì)所選區(qū)域進(jìn)行轉(zhuǎn)換;
1、命令行解析
加入命令行參數(shù)的功能,用戶(hù)可以從命令行傳入?yún)?shù);
int main(int argc, char** argv) { const String keys = "{help h usage ? | | print this message }" "{path |D: / project / OpenCV / card.jpg| path to file }" ; // 采用opencv命令行解析的方式 CommandLineParser myParser(argc, argv, keys); String path = myParser.get<String>("path"); cout << path << endl; }
2、鼠標(biāo)事件
主要實(shí)現(xiàn)用戶(hù)點(diǎn)擊鼠標(biāo)時(shí)的一些交互功能:
// 這幾個(gè)參數(shù)為默認(rèn)參數(shù) void onMouse(int event, int x, int y, int flags, void* userdata) { // 當(dāng)點(diǎn)數(shù)為四個(gè)時(shí),判斷當(dāng)前用戶(hù)鼠標(biāo)選取的拖動(dòng)點(diǎn)是哪一個(gè) if (srcPts.size() == 4) { for (int i = 0; i < 4; i++) { Point2f v = srcPts[i]; if ((event == EVENT_LBUTTONDOWN) & (abs(v.x - x) < 20) & (abs(v.y - y) < 20)) { isDragging = true; drag_index = i; } } } // 用戶(hù)點(diǎn)擊鼠標(biāo)左鍵時(shí),加入點(diǎn) else if (event == EVENT_LBUTTONDOWN) { srcPts.push_back(Point2f(x, y)); } // 取消拖動(dòng) if (event == EVENT_LBUTTONUP) { isDragging = false; } // 如果鼠標(biāo)移動(dòng)并且一直按著,就改變?cè)瓉?lái)的點(diǎn) if ((event == EVENT_MOUSEMOVE) && isDragging) { srcPts[drag_index].x = x; srcPts[drag_index].y = y; } }
3、主函數(shù)實(shí)現(xiàn)
定義了鼠標(biāo)函數(shù)之后,需要將其中的操作在窗口進(jìn)行展示:
namedWindow(winNameOri, WINDOW_NORMAL); namedWindow(winNameRes, WINDOW_NORMAL); setMouseCallback(winNameOri, onMouse, nullptr); bool done = false; while (!done) { if (srcPts.size() < 4) { img = oriImg.clone(); for (int i = 0; i < srcPts.size(); i++) { circle(img, srcPts[i], 10, Scalar(255, 255, 0), 5); putText(img, labels[i].c_str(), srcPts[i], FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2); } imshow(winNameOri, img); } if (srcPts.size() == 4) { img = oriImg.clone(); for (int i = 0; i < 4; i++) { circle(img, srcPts[i], 10, Scalar(255, 255, 0), 5); line(img, srcPts[i], srcPts[(i + 1) % 4], Scalar(0, 255, 0), 5); putText(img, labels[i].c_str(), srcPts[i], FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2); } imshow(winNameOri, img); }
4、結(jié)果展示
后面的求取單應(yīng)性矩陣以及降噪和二值化同方案一一致,在這里就不進(jìn)行展示了;
結(jié)果圖:
五、總結(jié)
本次項(xiàng)目是基于C++實(shí)現(xiàn)的,后續(xù)我也用Python進(jìn)行了代碼的轉(zhuǎn)換,但在直線(xiàn)檢測(cè)部分用相同函數(shù)卻得到不同的效果,這個(gè)問(wèn)題還沒(méi)有進(jìn)行解決;
采用C++進(jìn)行編寫(xiě)代碼,能夠?qū)φ麄€(gè)項(xiàng)目的每個(gè)變量以及流程更加熟悉,本次項(xiàng)目可以說(shuō)是一個(gè)多個(gè)知識(shí)點(diǎn)的集合項(xiàng)目,不僅僅包含邊緣檢測(cè)、直線(xiàn)檢測(cè)、圖像變換等,其中也有很多值得思考的小點(diǎn);
以上就是C++ OpenCV實(shí)戰(zhàn)之文檔照片轉(zhuǎn)換成掃描文件的詳細(xì)內(nèi)容,更多關(guān)于C++ OpenCV照片轉(zhuǎn)掃描文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
c++ dynamic_cast與static_cast使用方法示例
本文用示例講解了dynamic_cast、static_cast子類(lèi)與基類(lèi)之間轉(zhuǎn)換功能的使用方法2013-11-11C++實(shí)現(xiàn)車(chē)票管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)車(chē)票管理系統(tǒng),連接數(shù)據(jù)庫(kù)MySQL,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03深入學(xué)習(xí)C++智能指針之shared_ptr與右值引用的方法
智能指針的核心實(shí)現(xiàn)技術(shù)是引用計(jì)數(shù),每使用它一次,內(nèi)部引用計(jì)數(shù)加1,每析構(gòu)一次內(nèi)部的引用計(jì)數(shù)減1,減為0時(shí),刪除所指向的堆內(nèi)存,今天通過(guò)本文給大家分享C++智能指針之shared_ptr與右值引用的方法,需要的朋友跟隨小編一起看看吧2021-07-07C++標(biāo)準(zhǔn)模板庫(kù)STL的介紹
今天小編就為大家分享一篇關(guān)于C++標(biāo)準(zhǔn)模板庫(kù)STL的介紹,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12