功能介紹
- 圖片打開和保存
- 圖片矯正(證件掃描、文字糾正…)
- 圖片銳化增強
- 圖片清空
- 閾值設置
項目實現
基本思路(證件掃描)
- 摳圖:提取輪廓
- 矯正:透視變換
- 銳化增強:二值化
算法設計(證件掃描)
第一步:提取邊緣
- 讀取圖像,轉化爲灰度圖
- 降噪,二值化 高斯濾波
GaussianBlur()
- 適當膨脹,提高檢測效率
- 邊緣檢測
Canny()
,打印出二值圖驗證
第二步:輪廓查找與篩選
- 輪廓檢測
findContours()
- 霍夫直線檢測
HoughLines()
- 繪製檢測到的直線並驗證
line()
- 排除距離過近、不相交的直線
- 排除距離過近的兩直線交點
第三步:透視變換
- 由第二步篩選出的四個頂點得出一組座標
- 確定輸出圖像長寬(或自適應),驗證
- 計算透視變換矩陣
GetPerspectiveTransform()
- 透視變換函數
warpPerspective()
第四步:銳化增強
- 必要的二值化
adaptiveThreshold()
- …
- 輸出圖像
UI設計
核心代碼
- 證件掃描
Mat scanning()
{
Mat src = imread(path);
Mat source = src.clone();
Mat bkup = src.clone();
Mat img = src.clone();
//二值化
threshold(img, img, GRAY_THRESH, 255, CV_THRESH_BINARY);
//高斯濾波
GaussianBlur(img, img, Size(5, 5), 0, 0);
//獲取自定義核
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
//適當膨脹
dilate(img, img, element);
//邊緣提取
Canny(img, img, 30, 120, 3);
vector<vector<Point> > contours;
vector<vector<Point> > f_contours;
vector<Point> approx2;
//輪廓檢測
findContours(img, f_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
//求出面積最大的輪廓
int max_area = 0;
int index;
for (int i = 0; i < f_contours.size(); i )
{
double tmparea = fabs(contourArea(f_contours[i]));
if (tmparea > max_area)
{
index = i;
max_area = tmparea;
}
}
//找頂點
Mat f_img = img.clone();
vector<Vec4i> lines;
vector<Point2f> corners;
//驗證輪廓
drawContours(f_img, contours, 0, Scalar(255));
lines.clear();
corners.clear();
//這裏的閾值提供給用戶修改
//直線檢測
HoughLinesP(f_img, lines, 1, PI / 180, HOUGH_VOTE, 30, 10);
//1.過濾不符條件的直線
//2.計算直線交點
//3.過濾不符條件的點
DstSize(corners); //計算輸出尺寸
Mat dst = Mat::zeros(dst_hight, dst_width, CV_8UC3);
vector<Point2f> f_points; //四邊形頂點座標組
f_points.push_back(Point2f(0, 0));
f_points.push_back(Point2f(dst.cols, 0));
f_points.push_back(Point2f(dst.cols, dst.rows));
f_points.push_back(Point2f(0, dst.rows));
Mat temp = getPerspectiveTransform(corners, f_points); //計算透視變換矩陣
warpPerspective(source, dst, temp, dst.size()); //透視變換
//這裏也可以提供給用戶修改
//自動增強
Mat local, gray;
cvtColor(dst, gray, CV_RGB2GRAY);
int blockSize = 25;
int constValue = 10;
//自適應二值化
adaptiveThreshold(gray, local, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, blockSize, constValue);
return local;
}
- 文字糾正
Mat rotate(Mat srcImage)
{
//轉換爲灰度圖
Mat grayImage;
cvtColor(srcImage, grayImage, CV_RGB2GRAY);
//獲取圖片原尺寸
const int nRows = grayImage.rows;
const int nCols = grayImage.cols;
//圖片尺寸轉換,獲取傅里葉變換尺寸
//返回DFT最優尺寸大小的函數
int mRows = getOptimalDFTSize(nRows);
int mCols = getOptimalDFTSize(nCols);
Mat newImage;
//邊界擴充函數
copyMakeBorder(grayImage, newImage, 0, mRows - nRows, 0, mCols - nCols, BORDER_CONSTANT, Scalar::all(0));
//圖像DFT變換
//通道組建立,使用Mat_容器,一個存實部,一個存虛部
Mat groupImage[] = { Mat_<float>(newImage), Mat::zeros(newImage.size(), CV_32F) };
Mat mergeImage;
//合併通道
merge(groupImage, 2, mergeImage);
//離散傅里葉變換即DFT
dft(mergeImage, mergeImage);
//分離通道
split(mergeImage, groupImage);
//調整數據
//計算傅里葉變化各頻率的幅值
magnitude(groupImage[0], groupImage[1], groupImage[0]);
Mat magImage = groupImage[0];
//歸一化操作,幅值加1
magImage = Scalar::all(1);
//取對數
log(magImage, magImage);
//重新分配象限,使(0,0)移動到圖像中心,即把低頻部分移動到中心
//傅里葉變換之前要對源圖像乘以(-1)^(x y),進行中心化
int cx = magImage.cols / 2;
int cy = magImage.rows / 2;
Mat temp;
//左上象限
Mat LT(magImage, Rect(0, 0, cx, cy));
//右上象限
Mat RT(magImage, Rect(cx, 0, cx, cy));
//左下象限
Mat LB(magImage, Rect(0, cy, cx, cy));
//右下象限
Mat RB(magImage, Rect(cx, cy, cx, cy));
//交換象限,左上換右下
LT.copyTo(temp);
RB.copyTo(LT);
temp.copyTo(RB);
//交換象限,右上換左下
RT.copyTo(temp);
LB.copyTo(RT);
temp.copyTo(LB);
//歸一化
//在0-1之間是統計概率分佈,爲了後續操作方便
normalize(magImage, magImage, 0, 1, CV_MINMAX);
//像素強度變換,輸出單通道灰度圖
Mat magImg;
magImage.convertTo(magImg, CV_8UC1, 255, 0);
//imshow("magnitude", magImg);
//檢測直線
//二值化
threshold(magImg, magImg, GRAY_THRESH, 255, CV_THRESH_BINARY);
//構造8UC1格式圖像
vector<Vec2f> lines;
Mat houghImg(magImg.size(), CV_8UC3);
//Houge直線檢測
HoughLines(magImg, lines, 1, CV_PI / 180, HOUGH_VOTE, 0, 0);
// cout << "檢測直線條數: " << lines.size() << endl;
//繪製檢測線
for (int l = 0; l < lines.size(); l )
{
float rho = lines[l][0], theta = lines[l][1];
Point pt1, pt2;
//座標變換生成線表達式
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 1000 * (-b));
pt1.y = cvRound(y0 1000 * (a));
pt2.x = cvRound(x0 - 1000 * (-b));
pt2.y = cvRound(y0 - 1000 * (a));
line(houghImg, pt1, pt2, Scalar(255, 0, 0), 3, 8, 0);
}
// imshow("hough", houghImg);
//獲取角度
float angel = 0;
float m = PI / 90;
float n = PI / 2;
for (int l = 0; l < lines.size(); l )
{
//遍歷檢測直線的角度
float theta = lines[l][1];
if (abs(theta) > m && abs(n - theta) > m)
{
//取有效角度
angel = theta;
break;
}
}
//確保角度在0到90度內
angel = angel < PI / 2 ? angel : angel - PI;
//角度換算
if (angel != PI / 2)
{
//作圖一目瞭然
float angelT = srcImage.rows * tan(angel) / srcImage.cols;
angel = atan(angelT);
}
float angel_rad = angel * 180 / PI;
// cout << "旋轉角度: " << angel_rad << endl;
//取圖像中心
Point2f centerPoint = Point2f(nCols / 2, nRows / 2);
double scale = 1;
//計算旋轉中心
Mat rotateMat = getRotationMatrix2D(centerPoint, angel_rad, scale);
//仿射變換
Mat resultImage(grayImage.size(), srcImage.type());
warpAffine(srcImage, resultImage, rotateMat, srcImage.size(), 1, 0, Scalar(255, 255, 255));
return resultImage;
}
項目截圖
- 證件掃描
- 文字糾正
- 效果對比
- 效果對比(娛樂向)
項目總結
-
本項目基本實現了證件掃描和文字糾正兩大基本功能,其中類似“全能掃描王”的掃描功能被我單獨做了個版本,所以上述截圖UI有些不一樣,特此說明;
-
在這種基於透視變化的算法中,可以看見一定弊端:直線檢測的閾值、輪廓檢測的標準、頂點篩選的嚴密性等,對最終的結果影響很大,所以找到合理的、或者自適應的參數是最關鍵一步;故市面上的掃描軟件一定有更復雜的思路或算法,還需要繼續學習!
-
剛開始寫的時候對OCR等詞彙的理解不當,所以在函數命名和UI設計上出現了失誤,特此指出;OCR(Optical Character Recognition,光學字符識別)意爲文字識別,與本項目的功能不同;
-
希望本文能幫助到那些剛入門圖像處理的同學,咱們一起加油!
-
關於證件掃描算法的疑惑可以參考 這位大神的乾貨文章