使用深度学习解决拍照文档复杂背景二值化问题
前言
1.在手持拍照設備對文檔進行拍照時,很容易出現光線不均、陰影、過暗等,或者有些舊的文檔,古籍文檔都有蟲洞、透背、字跡不清現象,為了方便閱讀、打印文檔,或者OCR識別,這些干擾都對處理結果有很多不良的影響。
2.在文檔處理過程,往往分這幾步,圖像預處理、文檔圖像二值化、版面分析、文本檢測與識別等環節,在文檔才二值這塊,有很多傳統的算法可以使用,比如大津法,自適應二值化等,但在使用的過程,這些傳統的算法只對針對某些特定的干擾做深度調參,并不能達到對所有文檔都有高魯棒性。
一.傳統方法
1.傳統數字圖像處理里面,對圖像二值化有好多用可用辦法,我自己試過幾種方法,從最常用的大津法、自適應二值化,到比較偏門的積分二值化。下面來對比下這幾種方法的效果。
效果圖像第一是灰度圖像,第二張是自適應二值化,第三張是大津法,第四張是積分二值化。
第一種場景,手寫文檔,光線不均,有少些陰影:
原圖:
二值化效果圖像:
第二種場景,帶大面積陰影的印刷文檔,而且陰影比較明顯:
原圖:
二值圖像效果圖:
第三種場景,有很重透背的文檔:
原圖:
二值圖像效果圖:
第四種場景,紙張帶有底色的古籍手抄文檔:
原圖:
二值圖像效果圖:
2.從以上效果來看,當使用場景有干擾或者光線有變化的情況下,傳統的圖像圖像處理并不能完美的解決文檔圖像二值化的問題,表現稍微好一些積分二值化(效果圖像第四格)也不能勝任大部分環境。但有些使用場景相對穩定的情況下,可以選擇積分二值化這個方法。下面是積分二值化的代碼,是基于OpenCV C++寫的。
/// <summary> /// 積分二值化 /// </summary> /// <param name="inputMat">輸入圖像</param> /// <param name="thre">閾值(1.0)</param> /// <param name="outputMat">輸出圖像</param> void thresholdIntegral(cv::Mat& inputMat, double thre, cv::Mat& outputMat) {// accept only char type matricesCV_Assert(!inputMat.empty());CV_Assert(inputMat.depth() == CV_8U);CV_Assert(inputMat.channels() == 1);outputMat = cv::Mat(inputMat.size(), CV_8UC1, 1);// rows -> height -> yint nRows = inputMat.rows;// cols -> width -> xint nCols = inputMat.cols;// create the integral imagecv::Mat sumMat;cv::integral(inputMat, sumMat);CV_Assert(sumMat.depth() == CV_32S);CV_Assert(sizeof(int) == 4);int S = MAX(nRows, nCols) / 8;double T = 0.15;// perform thresholdingint s2 = S / 2;int x1, y1, x2, y2, count, sum;// CV_Assert(sizeof(int) == 4);int* p_y1, * p_y2;uchar* p_inputMat, * p_outputMat;for (int i = 0; i < nRows; ++i){y1 = i - s2;y2 = i + s2;if (y1 < 0){y1 = 0;}if (y2 >= nRows){y2 = nRows - 1;}p_y1 = sumMat.ptr<int>(y1);p_y2 = sumMat.ptr<int>(y2);p_inputMat = inputMat.ptr<uchar>(i);p_outputMat = outputMat.ptr<uchar>(i);for (int j = 0; j < nCols; ++j){// set the SxS regionx1 = j - s2;x2 = j + s2;if (x1 < 0){x1 = 0;}if (x2 >= nCols){x2 = nCols - 1;}count = (x2 - x1) * (y2 - y1);// I(x,y)=s(x2,y2)-s(x1,y2)-s(x2,y1)+s(x1,x1)sum = p_y2[x2] - p_y1[x2] - p_y2[x1] + p_y1[x1];if ((int)(p_inputMat[j] * count) < (int)(sum * (1.0 - T) * thre))p_outputMat[j] = 0;elsep_outputMat[j] = 255;}} }3.當使用環境不確定或者使用環境比較復雜的時候,傳統方法再什么調參也不能完美解決,這個時候只能考慮使用深度學習了。
二.基于U-net的圖像二值化
1.Unet 網絡
U-Net一開始就是針對生物醫學圖片的分割用的,一直到現在許多對醫學圖像的分割網絡中,很大一部分會采取U-Net作為網絡的主干。
算法部分我這里參考的U-Net關于視網膜血管分割這個項目,github地址:https://github.com/orobix/retina-unet 。我們可以看看它對視網膜血管分割。
2.深度學習框架用的Pytorch,參考了這個項目:https://github.com/milesial/Pytorch-UNet添加鏈接描述 。
3.但U-Net只能訓練尺寸為512的圖像,但對于拍攝的文檔,如果把尺寸都壓到512來做標簽和訓練,肯定會丟失好多細節上的東西,我這里把網絡按ENet的結構做了微調。
三.模型推理
1.我的測試環境是win10,vs2019,OpenCV 4.5,模型推理這里為了方便,就直接用OpenCV的dnn。
2.代碼:
DirtyDocUnet類:
#pragma once #include <iostream> #include <string> #include <opencv2/opencv.hpp> #include <opencv2/dnn/dnn.hpp>class DirtyDocUnet { public:DirtyDocUnet(std::string _model_path);void dnnInference(cv::Mat &cv_src, cv::Mat &cv_dst);void docBin(const cv::Mat& cv_src, cv::Mat& cv_dst); private:std::string model_path;cv::dnn::Net doc_net;int target_w = 1560;int target_h = 1560; }; #include "DirtyDocUnet.h"DirtyDocUnet::DirtyDocUnet(std::string _model_path) {model_path = _model_path;doc_net = cv::dnn::readNet(model_path); }void DirtyDocUnet::dnnInference(cv::Mat &cv_src, cv::Mat &cv_dst) {cv::Size reso(this->target_w,this->target_h);cv::Mat cv_gray;cv::cvtColor(cv_src, cv_gray, cv::COLOR_BGR2GRAY);cv::Mat bold = cv::dnn::blobFromImage(cv_gray, 1.0 / 255, reso, cv::Scalar(0, 0, 0), false, false);doc_net.setInput(bold);cv::Mat cv_out = doc_net.forward();cv::Mat cv_seg = cv::Mat::zeros(cv_out.size[2], cv_out.size[3], CV_8UC1);for (int i = 0; i < cv_out.size[2] * cv_out.size[3]; i++){cv_seg.data[i] = cv_out.ptr<float>(0, 0)[i] * 255;} cv::resize(cv_seg, cv_dst, cv_src.size()); }/// <summary> /// 二值圖像的邊緣光滑處理 /// </summary> /// <param name="src">輸入圖像</param> /// <param name="dst">輸出圖像</param> /// <param name="uthreshold">寬度閾值</param> /// <param name="vthreshold">高度閾值</param> /// <param name="type">突出部的顏色,0表示黑色,1代表白色</param> void deleteZigzag(cv::Mat& src, cv::Mat& dst, int uthreshold, int vthreshold, int type) {//int threshold;src.copyTo(dst);int height = dst.rows;int width = dst.cols;int k; //用于循環計數傳遞到外部for (int i = 0; i < height - 1; i++){uchar* p = dst.ptr<uchar>(i);for (int j = 0; j < width - 1; j++){if (type == 0){//行消除if (p[j] == 255 && p[j + 1] == 0){if (j + uthreshold >= width){for (int k = j + 1; k < width; k++){p[k] = 255;}}else{for (k = j + 2; k <= j + uthreshold; k++){if (p[k] == 255){break;}}if (p[k] == 255){for (int h = j + 1; h < k; h++){p[h] = 255;}}}}//列消除if (p[j] == 255 && p[j + width] == 0){if (i + vthreshold >= height){for (k = j + width; k < j + (height - i) * width; k += width){p[k] = 255;}}else{for (k = j + 2 * width; k <= j + vthreshold * width; k += width){if (p[k] == 255) break;}if (p[k] == 255){for (int h = j + width; h < k; h += width)p[h] = 255;}}}}else //type = 1{//行消除if (p[j] == 0 && p[j + 1] == 255){if (j + uthreshold >= width){for (int k = j + 1; k < width; k++)p[k] = 0;}else{for (k = j + 2; k <= j + uthreshold; k++){if (p[k] == 0) break;}if (p[k] == 0){for (int h = j + 1; h < k; h++)p[h] = 0;}}}//列消除if (p[j] == 0 && p[j + width] == 255){if (i + vthreshold >= height){for (k = j + width; k < j + (height - i) * width; k += width)p[k] = 0;}else{for (k = j + 2 * width; k <= j + vthreshold * width; k += width){if (p[k] == 0) break;}if (p[k] == 0){for (int h = j + width; h < k; h += width)p[h] = 0;}}}}}} }void DirtyDocUnet::docBin(const cv::Mat& cv_src, cv::Mat& cv_dst) {if (cv_src.empty()){return;}std::vector<cv::Mat> cv_pieces;cv_pieces.push_back(cv_src(cv::Rect(0, 0, cv_src.cols, cv_src.rows / 2)));cv_pieces.push_back(cv_src(cv::Rect(0, cv_src.rows / 2, cv_src.cols, cv_src.rows / 2)));cv::Mat cv_pars;for (auto v : cv_pieces){cv::Mat cv_temp;dnnInference(v, cv_temp);cv_pars.push_back(cv_temp);}cv::Mat cv_resize;cv::resize(~cv_pars, cv_resize, cv::Size(4096, 4096), cv::INTER_CUBIC);cv::Mat cv_zig;deleteZigzag(cv_resize, cv_zig, 5, 5, 0);cv::Mat cv_bin;cv::resize(~cv_zig, cv_dst, cv::Size(cv_src.cols, cv_src.rows), cv::INTER_LINEAR); } #include "DirtyDocUnet.h"DirtyDocUnet::DirtyDocUnet(std::string _model_path) {model_path = _model_path;doc_net = cv::dnn::readNet(model_path); }void DirtyDocUnet::dnnInference(cv::Mat &cv_src, cv::Mat &cv_dst) {cv::Size reso(this->target_w,this->target_h);cv::Mat cv_gray;cv::cvtColor(cv_src, cv_gray, cv::COLOR_BGR2GRAY);cv::Mat bold = cv::dnn::blobFromImage(cv_gray, 1.0 / 255, reso, cv::Scalar(0, 0, 0), false, false);doc_net.setInput(bold);cv::Mat cv_out = doc_net.forward();cv::Mat cv_seg = cv::Mat::zeros(cv_out.size[2], cv_out.size[3], CV_8UC1);for (int i = 0; i < cv_out.size[2] * cv_out.size[3]; i++){cv_seg.data[i] = cv_out.ptr<float>(0, 0)[i] * 255;} cv::resize(cv_seg, cv_dst, cv_src.size()); }/// <summary> /// 二值圖像的邊緣光滑處理 /// </summary> /// <param name="src">輸入圖像</param> /// <param name="dst">輸出圖像</param> /// <param name="uthreshold">寬度閾值</param> /// <param name="vthreshold">高度閾值</param> /// <param name="type">突出部的顏色,0表示黑色,1代表白色</param> void deleteZigzag(cv::Mat& src, cv::Mat& dst, int uthreshold, int vthreshold, int type) {//int threshold;src.copyTo(dst);int height = dst.rows;int width = dst.cols;int k; //用于循環計數傳遞到外部for (int i = 0; i < height - 1; i++){uchar* p = dst.ptr<uchar>(i);for (int j = 0; j < width - 1; j++){if (type == 0){//行消除if (p[j] == 255 && p[j + 1] == 0){if (j + uthreshold >= width){for (int k = j + 1; k < width; k++){p[k] = 255;}}else{for (k = j + 2; k <= j + uthreshold; k++){if (p[k] == 255){break;}}if (p[k] == 255){for (int h = j + 1; h < k; h++){p[h] = 255;}}}}//列消除if (p[j] == 255 && p[j + width] == 0){if (i + vthreshold >= height){for (k = j + width; k < j + (height - i) * width; k += width){p[k] = 255;}}else{for (k = j + 2 * width; k <= j + vthreshold * width; k += width){if (p[k] == 255) break;}if (p[k] == 255){for (int h = j + width; h < k; h += width)p[h] = 255;}}}}else //type = 1{//行消除if (p[j] == 0 && p[j + 1] == 255){if (j + uthreshold >= width){for (int k = j + 1; k < width; k++)p[k] = 0;}else{for (k = j + 2; k <= j + uthreshold; k++){if (p[k] == 0) break;}if (p[k] == 0){for (int h = j + 1; h < k; h++)p[h] = 0;}}}//列消除if (p[j] == 0 && p[j + width] == 255){if (i + vthreshold >= height){for (k = j + width; k < j + (height - i) * width; k += width)p[k] = 0;}else{for (k = j + 2 * width; k <= j + vthreshold * width; k += width){if (p[k] == 0) break;}if (p[k] == 0){for (int h = j + width; h < k; h += width)p[h] = 0;}}}}}} }void DirtyDocUnet::docBin(const cv::Mat& cv_src, cv::Mat& cv_dst) {if (cv_src.empty()){return;}std::vector<cv::Mat> cv_pieces;cv_pieces.push_back(cv_src(cv::Rect(0, 0, cv_src.cols, cv_src.rows / 2)));cv_pieces.push_back(cv_src(cv::Rect(0, cv_src.rows / 2, cv_src.cols, cv_src.rows / 2)));cv::Mat cv_pars;for (auto v : cv_pieces){cv::Mat cv_temp;dnnInference(v, cv_temp);cv_pars.push_back(cv_temp);}cv::Mat cv_resize;cv::resize(~cv_pars, cv_resize, cv::Size(4096, 4096), cv::INTER_CUBIC);cv::Mat cv_zig;deleteZigzag(cv_resize, cv_zig, 5, 5, 0);cv::Mat cv_bin;cv::resize(~cv_zig, cv_dst, cv::Size(cv_src.cols, cv_src.rows), cv::INTER_LINEAR); }調用類
main.cpp
3.對比下處理效果
原圖:
二值圖像,第一張是自適應二值化,第二張是積分二值化,第三張是大津法二值化,第四張是UNet二值化的效果:
原圖:
二值圖像,第一張是自適應二值化,第二張是積分二值化,第三張是大津法二值化,第四張是UNet二值化的效果:
原圖:
二值圖像,第一張是自適應二值化,第二張是積分二值化,第三張是大津法二值化,第四張是UNet二值化的效果:
原圖:
二值圖像,第一張是自適應二值化,第二張是積分二值化,第三張是大津法二值化,第四張是UNet二值化的效果:
原圖:
二值圖像,第一張是自適應二值化,第二張是積分二值化,第三張是大津法二值化,第四張是UNet二值化的效果:
4.從整體的效果上看, 使用深度學習方法,最終的效果不管在什么樣的環境在,都能得到一個不錯的。其實這個效果還有可提升的空間,我當前用的訓練集大概是2000張左右的樣本,如果還能增加更多環境下的樣本,那模型泛化會更好。
5.這個效果在一些手機掃描類APP里面也有類似的功能,一般叫省墨模式,或者黑白掃描,我們在安卓和iOS上都移植了這個算法,下面是我們iOS APP里面的效果,對移動端掃描APP感興趣的可以去試試《掃描家》這個APP。
6.可執行文件和源碼都上傳到CSDN,可執行文件把圖像放到images,執行exe文件就在當前目錄下保存幾種效果的對比,地址:https://download.csdn.net/download/matt45m/50653819 ,源碼:https://download.csdn.net/download/matt45m/50654793
總結
以上是生活随笔為你收集整理的使用深度学习解决拍照文档复杂背景二值化问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android NDK开发——Andro
- 下一篇: Android App开发——添加APP