1. 程式人生 > >java+OpenCV3 +百度OCR(或tesseract) 識別表格資料

java+OpenCV3 +百度OCR(或tesseract) 識別表格資料

原理:先用opencv識別出表格 按點拆分每個單元格圖片 交給百度或tesseract識別 

當然有錢的可以買百度的OCR表格識別。。

package com.test;



import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.json.JSONArray;
import org.json.JSONObject;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;


import com.baidu.aip.ocr.AipOcr;


/**
 * Servlet implementation class TutableRead
 */
@WebServlet("/TutableReadBaidu")
public class TutableReadBaidu extends HttpServlet {
private static final long serialVersionUID = 1L;
       
 
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doPost(request, response);
}
static{
System.load("F:/opencv/build/java/x86/opencv_java310.dll"); 
}

  //註冊百度有設定APPID/AK/SK
    public static final String APP_ID = "";
    public static final String API_KEY = "";
    public static final String SECRET_KEY = "";
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime=System.currentTimeMillis();
String basePath = request.getSession().getServletContext().getRealPath("/images/");
File dir=new File(basePath);
if(dir.isDirectory()){ 
for (File f : dir.listFiles()) {f.delete(); }
}
Mat src = Imgcodecs.imread( "C:/Users/lilin/Desktop/"+request.getParameter("name")+".png");
if(src.empty()){      System.out.println( "not found file" ); return;  }  
Mat gray = new Mat();
Mat erod = new Mat();
Mat blur = new Mat();
int src_height=src.cols(), src_width=src.rows(); 
//先轉為灰度   cvtColor(src, gray, COLOR_BGR2GRAY);
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);

/**
* 腐蝕(黑色區域變大) 
    Mat element = getStructuringElement(MORPH_RECT, Size(erodeSize, erodeSize));
    erode(gray, erod, element);
*/
int erodeSize = src_height / 200;
if (erodeSize % 2 == 0){  erodeSize++; }
Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(erodeSize, erodeSize));
Imgproc.erode(gray, erod, element);

//高斯模糊化
int blurSize = src_height / 200;
if (blurSize % 2 == 0) {blurSize++; }
Imgproc.GaussianBlur(erod, blur,  new Size(blurSize, blurSize), 0, 0);


//封裝的二值化  adaptiveThreshold(~gray, thresh, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 15, -2);
Mat thresh = gray.clone(); 
Mat xx = new Mat();
Core.bitwise_not(gray,xx);//反色
Imgproc.adaptiveThreshold(xx, thresh, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 15, -2);
/*
這部分的思想是將線條從橫縱的方向處理後抽取出來,再進行交叉,矩形的點,進而找到矩形區域的過程

*/
// Create the images that will use to extract the horizonta and vertical lines
//使用二值化後的影象來獲取表格橫縱的線
Mat horizontal = thresh.clone();
Mat vertical = thresh.clone();
//這個值越大,檢測到的直線越多
String parameter = request.getParameter("xian"); if(parameter==null||parameter.equals("")  ){ parameter="20"; }
int scale = Integer.parseInt(parameter); // play with this variable in order to increase/decrease the amount of lines to be detected  使用這個變數來增加/減少待檢測的行數


// Specify size on horizontal axis 指定水平軸上的大小
int horizontalsize = horizontal.cols() / scale;
// Create structure element for extracting horizontal lines through morphology operations 建立通過形態學運算提取水平線的結構元素
// 為了獲取橫向的表格線,設定腐蝕和膨脹的操作區域為一個比較大的橫向直條
Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,new Size(horizontalsize, 1));
// Apply morphology operations
// 先腐蝕再膨脹
    // iterations 最後一個引數,迭代次數,越多,線越多。在頁面清晰的情況下1次即可。
Imgproc.erode(horizontal, horizontal, horizontalStructure,new Point(-1, -1),1 );
Imgproc.dilate(horizontal, horizontal, horizontalStructure,new Point(-1, -1),1);
// dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1)); // expand horizontal lines

// Specify size on vertical axis 同上
int verticalsize = vertical.rows() / scale;
// Create structure element for extracting vertical lines through morphology operations
Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,new Size(1, verticalsize)); 
Imgproc.erode(vertical, vertical, verticalStructure,new Point(-1, -1),1);
Imgproc.dilate(vertical, vertical, verticalStructure, new Point(-1, -1),1);
  /*
         * 合併線條
         * 將垂直線,水平線合併為一張圖
         */
Mat mask = new Mat();
    Core.add(horizontal,vertical,mask);
    /*
         * 通過 bitwise_and 定位橫線、垂直線交匯的點
         */
    Mat joints=new Mat();
    Core.bitwise_and(horizontal, vertical, joints);
    /*
         * 通過 findContours 找輪廓
         *
         * 第一個引數,是輸入影象,影象的格式是8位單通道的影象,並且被解析為二值影象(即圖中的所有非零畫素之間都是相等的)。
         * 第二個引數,是一個 MatOfPoint 陣列,在多數實際的操作中即是STL vectors的STL vector,這裡將使用找到的輪廓的列表進行填充(即,這將是一個contours的vector,其中contours[i]表示一個特定的輪廓,這樣,contours[i][j]將表示contour[i]的一個特定的端點)。
         * 第三個引數,hierarchy,這個引數可以指定,也可以不指定。如果指定的話,輸出hierarchy,將會描述輸出輪廓樹的結構資訊。0號元素表示下一個輪廓(同一層級);1號元素表示前一個輪廓(同一層級);2號元素表示第一個子輪廓(下一層級);3號元素表示父輪廓(上一層級)
         * 第四個引數,輪廓的模式,將會告訴OpenCV你想用何種方式來對輪廓進行提取,有四個可選的值:
         *      CV_RETR_EXTERNAL (0):表示只提取最外面的輪廓;
         *      CV_RETR_LIST (1):表示提取所有輪廓並將其放入列表;
         *      CV_RETR_CCOMP (2):表示提取所有輪廓並將組織成一個兩層結構,其中頂層輪廓是外部輪廓,第二層輪廓是“洞”的輪廓;
         *      CV_RETR_TREE (3):表示提取所有輪廓並組織成輪廓巢狀的完整層級結構。
         * 第五個引數,見識方法,即輪廓如何呈現的方法,有三種可選的方法:
         *      CV_CHAIN_APPROX_NONE (1):將輪廓中的所有點的編碼轉換成點;
         *      CV_CHAIN_APPROX_SIMPLE (2):壓縮水平、垂直和對角直線段,僅保留它們的端點;
         *      CV_CHAIN_APPROX_TC89_L1  (3)or CV_CHAIN_APPROX_TC89_KCOS(4):應用Teh-Chin鏈近似演算法中的一種風格
         * 第六個引數,偏移,可選,如果是定,那麼返回的輪廓中的所有點均作指定量的偏移
         */
    List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(mask,contours,hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE,new Point(0,0));
         
        List<MatOfPoint> contours_poly = contours;
        Rect[] boundRect = new Rect[contours.size()]; 
        List<Mat> tables = new ArrayList<Mat>();
        //my
        List<Rect> haveReacts = new ArrayList();
        Map<String, Map<String, Map<String, Double>>> mappoint=new HashMap<String, Map<String, Map<String, Double>>>();
        //迴圈所有找到的輪廓-點
        for(int i=0 ; i< contours.size(); i++){ 
        //每個表的點
            MatOfPoint point = contours.get(i);
            MatOfPoint contours_poly_point = contours_poly.get(i);
            /*
             * 獲取區域的面積
             * 第一個引數,InputArray contour:輸入的點,一般是影象的輪廓點
             * 第二個引數,bool oriented = false:表示某一個方向上輪廓的的面積值,順時針或者逆時針,一般選擇預設false
             */
            double area = Imgproc.contourArea(contours.get(i));
            //如果小於某個值就忽略,代表是雜線不是表格
            if(area < 100){    continue;   }
            /*
             * approxPolyDP 函式用來逼近區域成為一個形狀,true值表示產生的區域為閉合區域。比如一個帶點幅度的曲線,變成折線
             *
             * MatOfPoint2f curve:畫素點的陣列資料。
             * MatOfPoint2f approxCurve:輸出畫素點轉換後陣列資料。
             * double epsilon:判斷點到相對應的line segment 的距離的閾值。(距離大於此閾值則捨棄,小於此閾值則保留,epsilon越小,折線的形狀越“接近”曲線。)
             * bool closed:曲線是否閉合的標誌位。
             */
            Imgproc.approxPolyDP(new MatOfPoint2f(point.toArray()),new MatOfPoint2f(contours_poly_point.toArray()),3,true);
            //為將這片區域轉化為矩形,此矩形包含輸入的形狀
            boundRect[i] = Imgproc.boundingRect(contours_poly.get(i));
            // 找到交匯處的的表區域物件
            Mat table_image = joints.submat(boundRect[i]);
            
            List<MatOfPoint> table_contours = new ArrayList<MatOfPoint>();
            Mat joint_mat = new Mat();
            Imgproc.findContours(table_image, table_contours,joint_mat, Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE);
            //從表格的特性看,如果這片區域的點數小於4,那就代表沒有一個完整的表格,忽略掉
            if (table_contours.size() < 4){  continue; }
            
            //表格裡面的每個點 
            Map<String, Double> x_zhis=new HashMap<String, Double>();
            Map<String, Double> y_zhis=new HashMap<String, Double>();
            for (MatOfPoint matOfPoint : table_contours) { 
            Point[] array = matOfPoint.toArray();
            for (Point point2 : array) { x_zhis.put("x"+point2.x, point2.x); y_zhis.put("y"+point2.y, point2.y); }
}   
            //System.out.println( boundRect[i].x+"|"+boundRect[i].y+"|"+boundRect[i].width+"|"+boundRect[i].height+"|"+table_contours.size()+">>>>>>>>>>>>>>>>>>>");
            //my add
            haveReacts.add( boundRect[i]); 
Map<String, Map<String, Double>> x =new HashMap<String, Map<String,Double>>(); x.put("x", x_zhis);x.put("y", y_zhis);
mappoint.put("key"+(haveReacts.size()-1),x   );
            
            //儲存圖片
            tables.add(src.submat(boundRect[i]).clone()); 
            //將矩形畫在原圖上
            Imgproc.rectangle(src, boundRect[i].tl(), boundRect[i].br(), new Scalar(255, 0, 255), 1, 8, 0);
            
        } 

        //頁面資料
        Map<String,String> jspdata=new HashMap<String, String>();
         
for(int i=0; i< tables.size(); i++ ){ Mat table = tables.get(i); Rect rect = haveReacts.get(i);
int width = rect.width,height=rect.height;
Map<String, Map<String, Double>> mapdata = mappoint.get("key"+i);
int[] x_z = maptoint(mapdata.get("x"));
int[] y_z = maptoint(mapdata.get("y"));

//縱切 
String px_biao = request.getParameter("x_biao"); if(px_biao==null||px_biao.equals("")  ){ px_biao="5"; }
int x_len=0,x_biao=Integer.parseInt(px_biao);
List<Mat> mats=new ArrayList<Mat>();
for (int j = 0; j < x_z.length; j++) { 
if(j==0){
Mat img=new Mat(table,new Rect(0,0,x_z[j],height ));if(img.cols()>x_biao ){ mats.add(img); x_len++;}
}else{
Mat img=new Mat(table,new Rect(x_z[j-1],0,x_z[j]-x_z[j-1],height )); if(img.cols()>x_biao ){mats.add(img);x_len++;}
if(j==x_z.length-1){//最後一個處理
Mat img1=new Mat(table,new Rect(x_z[x_z.length-1],0,width-x_z[x_z.length-1],height )); if(img.cols()>x_biao ){mats.add(img1); }
}

}
imshow(basePath,table,"table_"+i+".png");//當前table圖
//橫切儲存
String py_biao = request.getParameter("y_biao"); if(py_biao==null||py_biao.equals("")  ){ py_biao="5"; }
int y_len=0,y_biao=Integer.parseInt(py_biao );
for (int j = 0; j <mats.size() ; j++) {   Mat mat = mats.get(j);
int tuwidth = mat.cols(),tugao=mat.rows();
int cy_len=0;
for (int k = 0; k < y_z.length; k++) { 
if(k==0){
Mat img=new Mat(mat,new Rect(0,0,tuwidth , y_z[k] ));if(img.rows()>y_biao ){ imshow(basePath, img,"table_"+i+"_"+j+"_"+cy_len+".png");     cy_len++; }
}else{ 
Mat img=new Mat(mat,new Rect(0,y_z[k-1],tuwidth,y_z[k]-y_z[k-1]));if(img.rows()>y_biao ){  imshow(basePath, img,"table_"+i+"_"+j+"_"+cy_len+".png");  cy_len++;}
if(k==y_z.length-1){//最後一個處理  
Mat img1=new Mat(mat,new Rect(0,y_z[k],tuwidth,tugao-y_z[k] ));if(img.rows()>y_biao ){ imshow(basePath, img1,"table_"+i+"_"+j+"_"+(cy_len)+".png");   }
}
}

y_len=cy_len;
}
//儲存資料資訊
jspdata.put("table_"+i, x_len+"_"+y_len);
}
request.setAttribute("data", jspdata);

//百度識別處理
AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY); 
        // 可選:設定網路連線引數
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);
        
        Map<String,String> jspdata1=new HashMap<String, String>();
int num=0;
for (Map.Entry<String, String> d : jspdata.entrySet()) {
String value= d.getValue();
if(value.indexOf("_")!=-1){ 
//
String x="";
String len[]=value.split("_");
int xlen=Integer.parseInt(len[0]);int ylen=Integer.parseInt(len[1]);
for(int i=0;i<ylen;i++){ 
//行
for(int j=0;j<xlen;j++){ 
String name="table_"+num+"_"+j+"_"+i+".png";
JSONObject res = client.basicGeneral(basePath+"/"+name, new HashMap<String, String>());   
String text=""; 
    try {
   Object words_result = res.get("words_result"); 
   JSONArray array=(JSONArray) words_result;
   text=getjsontext(array);  
    } catch (Exception e) { System.out.println("cuowu");} 
    try {
Thread.sleep(400);//百度qps限制
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
x+=(text.equalsIgnoreCase("")?" ":text)+"&_&";
}
x=x.substring(0, x.lastIndexOf("&_&"));
x+="#_#";
}
// 
jspdata1.put("shibie"+num, x);
}
num++;

long endTime=System.currentTimeMillis();
request.setAttribute("time", (float)(endTime-startTime)/1000);
request.setAttribute("shibiedata", jspdata1);
request.getRequestDispatcher("tutableread.jsp").forward(request,response);

public void imshow(String basePath,Mat dst,String name)  { 
Imgcodecs.imwrite(basePath+"/"+name, dst);
}
public String getjsontext(JSONArray array){
String text="";
        for (int i = 0; i < array.length(); i++) { JSONObject  textx = (JSONObject)array.get(i); text+=textx.get("words");    }
        return text;
}
public int[] maptoint(Map<String, Double> x) {
int[] zhi=new int[x.size()];int num=0;
for (Map.Entry<String, Double> m :x.entrySet())  {  
zhi[num]=m.getValue().intValue(); num++;
        }
Arrays.sort(zhi);
return zhi;
}

}

效果圖(我主要獲取資料的單元格  所以拆分的比較大體 沒對合並的單元格處理哦)


參考文章

https://my.oschina.net/u/3767256/blog/1615720

https://blog.csdn.net/yomo127/article/details/52045146

如果你對此感興趣的可以加群261074724討論