字符切割步骤
要做文字识别,第一步要考虑的就是怎么将每一个字符从图片中切割下来,然后才可以送入我们设计好的模型进行字符识别。现在就以下面这张图片为例,说一说最一般的字符切割的步骤是哪些。
我们实际上要识别的图片很可能没上面那张图片如此整洁,很可能是倾斜的,或者是带噪声的,又或者这张图片是用手机拍下来下来的,变得歪歪扭扭,所以需要进行图片预处理,把文本位置矫正,把噪声去除,然后才可以进行进一步的字符分割和文字识别。这些预处理的方法在我的前面几篇博客都有提到了。
1、图像矫正:
2.1、adaptiveThreshold —— 自适应阈值操作:
ADAPTIVE_THRESH_MEAN_C:在一个邻域内计算出邻域的平均值再减去第七个参数double C的值,作为阈值
2.2、OTSU算法的理论依据是:假定图像包含两类像素(前景像素和背景像素),直方图为双峰直方图,然后计算使得两类像素能分开的最佳阈值(类内方差),或等价的类间方差最大。
threshold(src, src, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
在预处理工作做好之后,我们就可以开始切割字符了。最普通的切割算法可以总结为以下几个步骤:
- 对图片进行水平投影,找到每一行的上界限和下界限,进行行切割
- 对切割出来的每一行,进行垂直投影,找到每一个字符的左右边界,进行单个字符的切割
首先是行切割。这里提到了水平投影的概念。水平投影,就是对一张图片的每一行元素进行统计(就是往水平方向统计),然后我们根据这个统计结果画出统计结果图,进而确定每一行的起始点和结束点。下面提到的垂直投影也是类似的,只是它的投影方向是往下的,即统计每一列的元素个数。
通过上面的水平投影图,我们很容易就能确定每一行文字的位置,确定的思路如下:我们可以以每个小山峰的起始结束点作为我们文本行的起始结束点,当然我们要对这些山峰做些约束,比如这些山峰的跨度不能太小。这样子我们就得到每一个文本行的位置,接着我们就根据这些位置将每个文本行切割下来用于接下来的单个字符的切割。
切割每一行,然后我们得到了一行文本,我们继续对这行文本进行垂直投影。
紧接着我们根据垂直投影求出来每个字符的边界值进行单个字符切割。方法与垂直投影的方法一样,只不过,因为字符排列得比较紧密,仅通过投影确定字符得到的结果往往不够准确的。不过先不管了,先切下来看看。
源码:
#include<opencv2\imgproc.hpp>
#include<opencv2\highgui.hpp>
#include<iostream>
using namespace std;
using namespace cv;
#define V_PROJECT 1
#define H_PROJECT 2
//添加typedef后,char_range不再是一个变量(char_range.a),而是一个结构体类型,必须先char_range s2;然后 s2.a ;
typedef struct
{
int begin;
int end;
}char_range;
//画出投影图
void draw(vector<int>& pos, int mode) {
vector<int>::iterator max = max_element(begin(pos), end(pos));//迭代器找最大值
if (mode == V_PROJECT) {
int heigth = *max;
int width = pos.size();
Mat project(heigth, width, CV_8UC1, Scalar(0, 0, 0));
for (int i = 0; i < width; i++)
for (int j = heigth - 1; j > heigth - pos[i]-1; j--)
project.at < uchar >(j,i)= 255;//注意Mat图像的坐标系与图像索引的差异
imshow("vertiacl", project);
}
else if (mode == H_PROJECT) {
int width = *max;
int heigth = pos.size();
Mat project = Mat::zeros(heigth, width, CV_8UC1);
for (int i = 0; i < heigth; i++)
for (int j = 0; j < pos[i] ; j++)
project.at <uchar>(i, j) = 255;
imshow("horizational", project);
}
}
//获取文本的投影用于分割字符(垂直,水平),并画出投影图
int GetTextProject(Mat &src,vector<int>& pos,int mode){//vector pos就是用于存储垂直投影和水平投影的位置
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]=pos[j]+1;
for (int j = 0; j < src.cols; j++)
if (src.at<uchar>(i, j) == 0)
pos[j]++;
}
draw(pos, mode);
}
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]++;
draw(pos, mode);
}
return 0;
}
//获取分割字符每行的范围,min_thresh:波峰的最小幅度,min_range:两个波峰的最小间隔
int GetPeekRange(vector<int>& vertical_pos,vector<char_range>& 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) {//该判断一行字符应具有的最小heigth
char_range range;
range.begin = begin;
range.end = end;
peek_range.push_back(range);
begin = 0;
end = 0;
}
}
if (vertical_pos[i] < min_thresh || begin == 0)
continue;
else
printf("error");//print只是输出,没有格式控制,而printf.可以根据需要,输出你需要的格式
}
return 0;
}
//切割每一行文本的字符
int CutChar(Mat& raw, const vector<char_range>v_peek_range, vector<char_range> h_peek_range, vector<Mat>&chars_set) {
int count = 0;
int char_width = raw.rows;//假定字符是个正方形
Mat show_img = raw.clone();
cvtColor(show_img, show_img, CV_GRAY2BGR);//为了显示彩色框
for (int i = 0; i < v_peek_range.size(); i++) {
int char_gap = v_peek_range[i].end - v_peek_range[i].begin;//识别出的一个字符的宽度
//if (char_gap <= (int)(char_width*1.2) && char_gap >= (int)(char_width*0.8))
{
int x = v_peek_range[i].begin - 2>0 ? v_peek_range[i].begin - 1 : 0;
int width = char_gap + 4 <= raw.rows ? char_gap : raw.rows;
Rect r(x, 0, width, raw.rows); //x对应col
rectangle(show_img, r, Scalar(0, 0, 255), 1);
Mat single_char = raw(r).clone();
chars_set.push_back(single_char);
//save_cut(single_char, count);
count++;
}
}
namedWindow("cut", WINDOW_NORMAL);
imshow("cut", show_img);
return 0;
}
vector<Mat> CutSingleChar(Mat & img) {
Mat src;
cvtColor(img, src, CV_BGR2GRAY);
threshold(src, src, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
vector<int> horizion_pos(src.rows, 0);
vector<char_range> h_peek_range;//行的范围
GetTextProject(src, horizion_pos, H_PROJECT);//行投影
GetPeekRange(horizion_pos, h_peek_range, 2, 10);//求每行文本的范围
#if 1
vector<Mat> lines_set;
for (int i = 0; i < h_peek_range.size(); i++) {
Mat line = src(Rect(0,h_peek_range[i].begin, src.cols, h_peek_range[i].end - h_peek_range[i].begin)).clone();//是否有误
lines_set.push_back(line);
}
vector<Mat> chars_set; //保存每个字符的图片
for (int i = 0; i < lines_set.size(); i++) {
Mat line = lines_set[i];
imshow("line", line);
vector<int> vertical_pos(line.cols,0);
vector<char_range> v_peek_range;
GetTextProject(line, vertical_pos, V_PROJECT);
GetPeekRange(vertical_pos, v_peek_range,2,3);
CutChar(line, v_peek_range, h_peek_range, chars_set);
}
#endif
return chars_set;
}
//文本预处理,矫正
Mat txt_correction(Mat image) { //Mat image,传入的图像不随image的改变而变
if (image.cols>1000 || image.rows>800) {//图片过大,进行降采样
pyrDown(image, image);
pyrDown(image, image);
pyrDown(image, image);
}
Mat grayImage, binaryImage;
cvtColor(image, grayImage, CV_BGR2GRAY);//转化灰度图
adaptiveThreshold(grayImage, binaryImage, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 7, 0);//自适应滤波
vector<vector<Point> > contours;
//RETR_EXTERNAL:表示只检测最外层轮廓
//CHAIN_APPROX_NONE:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1
findContours(binaryImage, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
//获得矩形包围框,之所以先用boundRect,是为了使用它的area方法求面积,而RotatedRect类不具备该方法
float area = boundingRect(contours[0]).area();
int index = 0;
for (int i = 1; i<contours.size(); i++)
{
if (boundingRect(contours[i]).area()>area)
{
area = boundingRect(contours[i]).area();
index = i;
}
}
Rect maxRect = boundingRect(contours[index]);//找出最大的那个矩形框(即最大轮廓)
Mat ROI = binaryImage(maxRect);
//imshow("maxROI", ROI);
RotatedRect rect = minAreaRect(contours[index]);//获取对应的最小矩形框,这个长方形是倾斜的
Point2f rectPoint[4];
rect.points(rectPoint);//获取四个顶点坐标,这是RotatedRect类定义的方法
double angle = rect.angle;
//angle += 90;
Point2f center = rect.center;
drawContours(binaryImage, contours, -1, Scalar(255), CV_FILLED);
// image.copyTo(RoiSrcImg,binaryImage);
Mat RoiSrcImg = Mat::zeros(image.size(), image.type());
image.copyTo(RoiSrcImg);
Mat Matrix = getRotationMatrix2D(center, angle, 0.8);//得到旋转矩阵算子,0.8缩放因子
warpAffine(RoiSrcImg, RoiSrcImg, Matrix, RoiSrcImg.size(), 1, 0, Scalar(255, 255, 255));//边界用白色填充
return RoiSrcImg;
}
int main()
{
//Mat srcImage = imread("D:\\Program Files\\OpenCV\\opencv\\sources\\samples\\data\\imageTextR.png");
//Mat RoiSrcImg=txt_correction(srcImage);
//imshow("recorrected", RoiSrcImg);//旋转后的原图
Mat RoiSrcImg = imread("imageTextN.png");
RoiSrcImg = imread("3.jpg");
vector<Mat> chars_set = CutSingleChar(RoiSrcImg);
while (waitKey() != 'q') {}
return 0;
}