文字識別(三)--文字定位與切割

Eason.wxd發表於2019-02-18

轉自:https://www.cnblogs.com/skyfsm/p/8029668.html

要做文字識別,第一步要考慮的就是怎麼將每一個字元從圖片中切割下來,然後才可以送入我們設計好的模型進行字元識別。現在就以下面這張圖片為例,說一說最一般的字元切割的步驟是哪些。

當然,我們實際上要識別的圖片很可能沒上面那張圖片如此整潔,很可能是傾斜的,或者是帶噪聲的,又或者這張圖片是用手機拍下來下來的,變得歪歪扭扭,所以需要進行圖片預處理,把文字位置矯正,把噪聲去除,然後才可以進行進一步的字元分割和文字識別。這些預處理的方法在我的前面幾篇部落格都有提到了,大家可以參考參考:
透視矯正
水平矯正

在預處理工作做好之後,我們就可以開始切割字元了。最普通的切割演算法可以總結為以下幾個步驟:

  1. 對圖片進行水平投影,找到每一行的上界限和下界限,進行行切割
  2. 對切割出來的每一行,進行垂直投影,找到每一個字元的左右邊界,進行單個字元的切割

一看只有兩個步驟,好像不太難,馬上程式設計實現看看效果。

首先是行切割。這裡提到了水平投影的概念,估計有的讀者沒聽過這個名詞,我來解釋一下吧。水平投影,就是對一張圖片的每一行元素進行統計(就是往水平方向統計),然後我們根據這個統計結果畫出統計結果圖,進而確定每一行的起始點和結束點。下面提到的垂直投影也是類似的,只是它的投影方向是往下的,即統計每一列的元素個數。

根據上面的解釋,我們可以寫出一個用於水平投影和垂直投影的函式。

#define V_PROJECT 1  //垂直投影(vertical)
#define H_PROJECT 2  //水平投影(horizational)

typedef struct
{
    int begin;
    int end;

}char_range_t;



//獲取文字的投影以用於分割字元(垂直,水平),預設圖片是白底黑色
int GetTextProjection(Mat &src, vector<int>& pos, int mode)
{
    if (mode == V_PROJECT)
    {
        for (int i = 0; i < src.rows; i++)
        {
            uchar* p = src.ptr<uchar>(i);
            for (int j = 0; j < src.cols; j++)
            {
                if (p[j] == 0)  //是黑色畫素
                {
                    pos[j]++;
                }
            }
        }
    }
    else if (mode == H_PROJECT)
    {
        for (int i = 0; i < src.cols; i++)
        {

            for (int j = 0; j < src.rows; j++)
            {
                if (src.at<uchar>(j, i) == 0)
                {
                    pos[j]++;
                }
            }
        }
    }

    return 0;
}

上面程式碼提到的vector pos就是用於儲存垂直投影和水平投影的位置的,我們可以根據它來確定行的位置。我們先把水平投影畫出來。

下面是畫出水平(垂直)投影圖的程式碼實現。

void draw_projection(vector<int>& pos, int mode)
{
    vector<int>::iterator max = std::max_element(std::begin(pos), std::end(pos)); //求最大值
    if (mode == H_PROJECT)
    {
        int height = pos.size();
        int width = *max;
        Mat project = Mat::zeros(height, width, CV_8UC1);
        for (int i = 0; i < project.rows; i++)
        {
            for (int j = 0; j < pos[i]; j++)
            {
                project.at<uchar>(i, j) = 255;
            }
        }
        imshow("horizational projection", project);

    }
    else if (mode == V_PROJECT)
    {
        int height = *max;
        int width = pos.size();
        Mat project = Mat::zeros(height, width, CV_8UC1);
        for (int i = 0; i < project.cols; i++)
        {
            for (int j = project.rows - 1; j >= project.rows - pos[i]; j--)
            {
                //std::cout << "j:" << j << "i:" << i << std::endl;
                project.at<uchar>(j, i) = 255;
            }
        }
        imshow("vertical projection", project);
    }

    waitKey();
}

水平投影圖:

通過上面的水平投影圖,我們很容易就能確定每一行文字的位置,確定的思路如下:我們可以以每個小山峰的起始結束點作為我們文字行的起始結束點,當然我們要對這些山峰做些約束,比如這些山峰的跨度不能太小。這樣子我們就得到每一個文字行的位置,接著我們就根據這些位置將每個文字行切割下來用於接下來的單個字元的切割。

//獲取每個分割字元的範圍,min_thresh:波峰的最小幅度,min_range:兩個波峰的最小間隔
int GetPeekRange(vector<int> &vertical_pos, vector<char_range_t> &peek_range, int min_thresh = 2, int min_range = 10)
{
    int begin = 0;
    int end = 0;
    for (int i = 0; i < vertical_pos.size(); i++)
    {
        if (vertical_pos[i] > min_thresh && begin == 0)
        {
            begin = i;
        }
        else if (vertical_pos[i] > min_thresh && begin != 0)
        {
            continue;
        }
        else if (vertical_pos[i] < min_thresh && begin != 0)
        {
            end = i;
            if (end - begin >= min_range)
            {
                char_range_t tmp;
                tmp.begin = begin;
                tmp.end = end;
                peek_range.push_back(tmp);
                begin = 0;
                end = 0;
            }

        }
        else if (vertical_pos[i] < min_thresh || begin == 0)
        {
            continue;
        }
        else
        {
            //printf("raise error!\n");
        }
    }

    return 0;
}

切割每一行,然後我們得到了一行文字,我們繼續對這行文字進行垂直投影。

緊接著我們根據垂直投影求出來每個字元的邊界值進行單個字元切割。方法與垂直投影的方法一樣,只不過,因為字元排列得比較緊密,僅通過投影確定字元得到的結果往往不夠準確的。不過先不管了,先切下來看看。

從上圖看出,切割效果不太好,那多切割幾行再看看。

效果確實不咋滴,那換成英文文件來測試這個切割演算法。

比如切割這個英語文字圖片

切割效果還是很不錯的:

那為什麼英語的切割效果很好,但中文效果一般呢?

分析其原因,這其實跟中文的字型複雜度有關的,中文的字元的筆畫和形態都比英文的多,更重要的是英文字母都是絕大部分都是聯通體,切割起來很簡單,但是漢字多存在左右結構和上下結構,很容易造成過度切割,即把一個左右偏旁的漢字切成了兩份,比如上面的“則”字。

針對行字元分割,左右偏旁的字難以分割的情況,我覺得可以做以下處理:

  1. 先用通用的分割方法切割字元,得到一堆候選的切割字符集合。
  2. 統計字符集合的大多數字符的尺寸,作為標準尺寸。
  3. 根據標準尺寸選出標準的字元,切割儲存。並對切割儲存好的字元原位置塗成白色
  4. 對剩下下來的圖片進行腐蝕,讓字型粘連。
  5. 用1中演算法再次分割,得到完整字型集合。

因為以上的思路可能只適應於純漢字文字,所以就不貼程式碼了。

最後貼幾張分割字元的圖吧,感覺分割效果不太讓人滿意,主要是漢字的分割確實很有難度,左右偏旁的字經常分割錯誤。

英文的切割還是比較簡單的,畢竟英文字母基本都是聯通體,而且沒有像漢字那樣的左右結構。

對於字型間隔比較寬的漢字文件,總的看來分割任務基本完成,但是左右結構的漢字依然難以正確分割。

 

最後看一下一些字型較小,字型間隔較窄的情況。這類情況確實分割效果大打折扣,因為每個字型粘連過於接近,字型的波谷很難確定下來,進而造成切割字元失敗。

總結

漢字字元切割,看似簡單,做起來其實很難做得很好,我也對此查閱了很多論文,發現其實很多論文也談到了,漢字確實很那做到一個高正確率的分割,直至現在還沒有一統江湖的解決方案。漢字切割的失敗,就會直接導致了後面OCR識別的失敗,這也是當前很多一些很厲害的OCR公司都沒法把漢字做到100%識別的一個原因吧。所以這個問題就必須得到很好的解決。現在解決漢字切割失敗(過切割,一個字被拆成兩個)的較好方法是,在OCR識別中再把它修正。比如“刺”字被分為兩部分了,那麼我們就直接將這兩個“字”送去識別,結果當然是得到一個置信度很低的一個反饋,那麼我們就將這兩個部分往他們身邊最近的、而且沒被成功識別的部分進行合併,再將這個合併後的字送進OCR識別,這樣子我們就可以通過識別反饋來完成漢字的正確分割和識別了。既然一些基於影象處理的方法基本很難把漢字分割的效果做得很好,那深度學習呢?我先去試試,效果好的話再分享給大家。

相關文章