最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++ OpenCV實(shí)戰(zhàn)之文檔照片轉(zhuǎn)換成掃描文件

 更新時(shí)間:2022年09月14日 15:53:29   投稿:gwy  
這篇文章主要為大家介紹一個(gè)C++?OpenCV的實(shí)戰(zhàn)——文檔照片轉(zhuǎn)換成掃描文件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

一、背景

前段時(shí)間都是基于Python的OpecCV進(jìn)行一些學(xué)習(xí)和實(shí)踐,但小的知識(shí)點(diǎn)并沒有應(yīng)用到實(shí)際的項(xiàng)目中;并且基于Python的版本的移植性、效率性都較差,在包含硬件的項(xiàng)目中往往都是采用基于C++的版本;

因此本次項(xiàng)目實(shí)戰(zhàn)專題主要是基于C++的版本,并且從大的任務(wù)中剖析小的知識(shí)點(diǎn),實(shí)際項(xiàng)目中算法的選型也是比較難的部分,根據(jù)需求和任務(wù)選用不同的算法,這才是真的掌握了知識(shí)點(diǎn);

本次項(xiàng)目是照片轉(zhuǎn)掃描文件,可以參考下面網(wǎng)址中的案例:

https://www.camscanner.com/

這已經(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)平行的線依舊平行,只需三對(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)建直線類并計(jì)算兩條直線的交點(diǎn)

先定義一個(gè)直線類,包含兩個(gè)端點(diǎn);

假設(shè)已知兩條直線上的兩點(diǎn),怎么求得交點(diǎn)呢?

可以參考這個(gè)網(wǎng)址中的數(shù)學(xué)公式:https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection

// 返回兩條直線的交點(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)行直線檢測(cè)

檢測(cè)到的直線分為兩類,一類是水平線,一類是豎直線;

為了得到外邊緣框的直線,可以先根據(jù)直線的外接矩形長(zhǎng)寬比分為水平和豎直線,再根據(jù)中點(diǎn)的位置,找到邊緣直線;

如上圖所示,如果x>y,則將該直線分為水平線,如果y>x,則將該直線劃分為水平線;

隨后再根據(jù)中心點(diǎn)的坐標(biāo)大小確定邊緣線;

// 進(jìn)行霍夫直線檢測(cè),保存所有檢測(cè)到的直線,并且確保直線大于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;
}

// 將直線分為水平線和垂直線
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)繪制直線
	//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);
}

// 確保水平線和垂直線至少有兩條
if (horizontals.size() < 2 || verticals.size() < 2) {
        cout << "Not enough edge lines... " << endl;
    }
    
// 將水平和垂直線按中心點(diǎn)位置進(jìn)行排序,這里的兩個(gè)排序函數(shù)需要自己實(shí)現(xiàn)
sort(horizontals.begin(), horizontals.end(), cmpHeight);
sort(verticals.begin(), verticals.end(), cmpWidth);

// 繪制出找到的直線
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ì)水平線進(jìn)行排序
bool cmpHeight(const Line& l1, const Line& l2) {
    return l1.center.y < l2.center.y;
}

// 對(duì)垂直線進(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);
}

四、方案二:用戶點(diǎn)選目標(biāo)區(qū)域

方案一是完全基于自動(dòng)化的方式,用戶只需要傳入圖像,程序會(huì)自動(dòng)選擇合適的區(qū)域;

優(yōu)點(diǎn)在于其節(jié)省了用戶的人工成本,使得程序更為簡(jiǎn)便;

缺點(diǎn)在于算法具有局限性,對(duì)背景及區(qū)域選取有要求,比如不能在區(qū)域外出現(xiàn)干擾物體,也無(wú)法滿足用戶的一些特別需求,比如選定大區(qū)域中的小區(qū)域;

方案二的優(yōu)勢(shì)在于其強(qiáng)大的靈活性,用戶可以根據(jù)自己的需求選擇相應(yīng)的區(qū)域,程序?qū)?duì)所選區(qū)域進(jìn)行轉(zhuǎn)換;

1、命令行解析

加入命令行參數(shù)的功能,用戶可以從命令行傳入?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)用戶點(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)前用戶鼠標(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;
            }
        }
    }
    // 用戶點(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)換,但在直線檢測(cè)部分用相同函數(shù)卻得到不同的效果,這個(gè)問(wèn)題還沒有進(jìn)行解決;

采用C++進(jìn)行編寫代碼,能夠?qū)φ麄€(gè)項(xiàng)目的每個(gè)變量以及流程更加熟悉,本次項(xiàng)目可以說(shuō)是一個(gè)多個(gè)知識(shí)點(diǎn)的集合項(xiàng)目,不僅僅包含邊緣檢測(cè)、直線檢測(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++如何實(shí)現(xiàn)并發(fā)中的Barrier

    淺談c++如何實(shí)現(xiàn)并發(fā)中的Barrier

    這篇文章主要介紹了淺談c++如何實(shí)現(xiàn)并發(fā)中的Barrier,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • c++中關(guān)于max_element()函數(shù)解讀

    c++中關(guān)于max_element()函數(shù)解讀

    這篇文章主要介紹了c++中關(guān)于max_element()函數(shù)解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • C++?電話號(hào)碼的字母組合功能實(shí)現(xiàn)

    C++?電話號(hào)碼的字母組合功能實(shí)現(xiàn)

    這篇文章主要介紹了C++?電話號(hào)碼的字母組合,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-08-08
  • C語(yǔ)言位運(yùn)算符:與、或、異或、取反、左移與右移詳細(xì)介紹

    C語(yǔ)言位運(yùn)算符:與、或、異或、取反、左移與右移詳細(xì)介紹

    以下是對(duì)C語(yǔ)言中的位運(yùn)算符:與、或、異或、取反、左移與右移進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過(guò)來(lái)參考下
    2013-08-08
  • C++?Boost?StringAlgorithms超詳細(xì)講解

    C++?Boost?StringAlgorithms超詳細(xì)講解

    Boost是為C++語(yǔ)言標(biāo)準(zhǔn)庫(kù)提供擴(kuò)展的一些C++程序庫(kù)的總稱。Boost庫(kù)是一個(gè)可移植、提供源代碼的C++庫(kù),作為標(biāo)準(zhǔn)庫(kù)的后備,是C++標(biāo)準(zhǔn)化進(jìn)程的開發(fā)引擎之一,是為C++語(yǔ)言標(biāo)準(zhǔn)庫(kù)提供擴(kuò)展的一些C++程序庫(kù)的總稱
    2022-11-11
  • C++ 雙鏈表的基本操作(詳解)

    C++ 雙鏈表的基本操作(詳解)

    下面小編就為大家?guī)?lái)一篇C++ 雙鏈表的基本操作(詳解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-12-12
  • 詳解C/C++內(nèi)存區(qū)域劃分(簡(jiǎn)而易懂)

    詳解C/C++內(nèi)存區(qū)域劃分(簡(jiǎn)而易懂)

    C/C++中,內(nèi)存主要分為、堆、棧、全局/靜態(tài)存儲(chǔ)區(qū)和常量存儲(chǔ)區(qū)。本文重點(diǎn)給大家介紹C/C++內(nèi)存區(qū)域劃分的相關(guān)知識(shí),需要的朋友參考下吧
    2021-06-06
  • 詳解如何將Spire.Doc for C++集成到C++程序中

    詳解如何將Spire.Doc for C++集成到C++程序中

    Spire.Doc for C++是一個(gè)專業(yè)的Word庫(kù),供開發(fā)人員在任何類型的C++應(yīng)用程序中閱讀、創(chuàng)建、編輯、比較和轉(zhuǎn)換 Word 文檔,本文演示了如何以兩種不同的方式將 Spire.Doc for C++ 集成到您的 C++ 應(yīng)用程序中,希望對(duì)大家有所幫助
    2023-05-05
  • 詳解C++11中的lambda匿名函數(shù)

    詳解C++11中的lambda匿名函數(shù)

    匿名函數(shù),簡(jiǎn)單地理解就是沒有名稱的函數(shù),又常被稱為 lambda 函數(shù)或者 lambda 表達(dá)式,這篇文章主要介紹了C++11中的lambda匿名函數(shù),需要的朋友可以參考下
    2022-11-11
  • C++中的函數(shù)指針與函數(shù)對(duì)象的總結(jié)

    C++中的函數(shù)指針與函數(shù)對(duì)象的總結(jié)

    以下是對(duì)C++中的函數(shù)指針與函數(shù)對(duì)象的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以參考下
    2013-07-07

最新評(píng)論

义马市| 荔波县| 青州市| 巴林右旗| 宣武区| 绍兴县| 浪卡子县| 巴楚县| 徐汇区| 酒泉市| 长丰县| 晋州市| 闻喜县| 内江市| 宁明县| 宣汉县| 浙江省| 钟祥市| 同江市| 嘉兴市| 汶上县| 尉氏县| 吉安县| 康保县| 海兴县| 田阳县| 大新县| 盐源县| 北辰区| 上虞市| 扎囊县| 康平县| 麟游县| 泊头市| 南丰县| 小金县| 遂昌县| 来凤县| 孟津县| 台东县| 德庆县|