我想识别信用卡中的数字。更糟糕的是,源图像不能保证高质量。 OCR将通过神经网络实现,但这不应该是这里的主题。
目前的问题是图像预处理。由于信用卡可以具有背景和其他复杂图形,因此文本不像扫描文档那样清晰。我用边缘检测(Canny Edge,Sobel)进行了实验,但并没有那么成功。 同时计算灰度图像和模糊图像之间的差异(如Remove background color in image processing for OCR所述)也不会导致OCRable结果。
我认为大多数方法都失败了,因为特定数字与其背景之间的对比度不够强。可能需要将图像分割成块并为每个块找到最佳的预处理解决方案吗?
您对如何将源转换为可读二进制图像有任何建议吗? 边缘检测是要走的路还是我应该坚持基本的颜色阈值?
以下是灰度阈值方法的示例(我显然对结果不满意):
原始图片:
灰度图像:
阈值图像:
感谢您的任何建议, 的Valentin
答案 0 :(得分:5)
我如何解决问题的方法是将卡片分成不同的部分。没有很多独特的信用卡可以从(MasterCard,Visa,列表由您决定)开始,因此您可以像下拉菜单那样指定它是哪个信用卡。这样,您可以消除并指定像素区域:
示例:
仅适用于距离底部20个像素的区域,距离的30个像素 左到右边10个像素到底部30个像素(创建一个 矩形) - 这将涵盖所有万事达卡
当我使用图像处理程序(有趣的项目)时,我调高了图像的对比度,将其转换为灰度,将每个RGB值的平均值取为1像素,并将其与周围像素进行比较:
示例:强>
PixAvg[i,j] = (Pix.R + Pix.G + Pix.B)/3
if ((PixAvg[i,j] - PixAvg[i,j+1])>30)
boolEdge == true;
30将是您希望图像的清晰程度。差异越小,容差就越低。
在我的项目中,为了查看边缘检测,我制作了一个单独的布尔数组,其中包含来自boolEdge和像素数组的值。像素阵列仅填充黑色和白色点。它得到了布尔数组中的值,其中boolEdge = true是一个白点,boolEdge = false是一个黑点。所以最后,你最终会得到一个只包含白点和黑点的像素阵列(全图)。
从那里,可以更容易地检测数字的开始位置和数字的完成位置。
答案 1 :(得分:5)
如果可能的话,请求使用更好的照明来捕捉图像。低角度灯光会照亮凸起(或凹陷)字符的边缘,从而大大提高图像质量。如果要通过机器分析图像,则应优化照明以确保机器的可读性。
那就是说,你应该研究的一个算法是笔画宽度变换,它用于从自然图像中提取字符。
Stroke Width Transform (SWT) implementation (Java, C#...)
全局阈值(对于二值化或削波边缘强度)可能不会为此应用程序削减它,而是应该查看本地化阈值。在您的示例图像中,“31”之后的“02”特别弱,因此在该区域中搜索最强的局部边缘将比使用单个阈值过滤字符串中的所有边缘更好。
如果您可以识别部分字符段,那么您可以使用一些方向形态操作来帮助连接段。例如,如果你有两个几乎水平的段,如下所示,其中0是背景,1是前景......
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 0 0 1 1 1 1 1 1 0 0 0
0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 0 0 0
然后您可以沿水平方向执行形态学“关闭”操作,仅加入这些段。内核可能类似于
x x x x x
1 1 1 1 1
x x x x x
有更复杂的方法可以使用Bezier拟合或甚至Euler螺旋(也就是使用的)来完成曲线完成,但是预处理以识别要连接的段和后处理以消除不良连接可能会变得非常棘手。
答案 2 :(得分:1)
在我的实现中,我尝试使用此处的代码:http://rnd.azoft.com/algorithm-identifying-barely-legible-embossed-text-image/ 结果更好但不够...... 我发现很难为纹理卡找到合适的参数。
(void)processingByStrokesMethod:(cv::Mat)src dst:(cv::Mat*)dst {
cv::Mat tmp;
cv::GaussianBlur(src, tmp, cv::Size(3,3), 2.0); // gaussian blur
tmp = cv::abs(src - tmp); // matrix of differences between source image and blur iamge
//Binarization:
cv::threshold(tmp, tmp, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
//Using method of strokes:
int Wout = 12;
int Win = Wout/2;
int startXY = Win;
int endY = src.rows - Win;
int endX = src.cols - Win;
for (int j = startXY; j < endY; j++) {
for (int i = startXY; i < endX; i++) {
//Only edge pixels:
if (tmp.at<unsigned char="">(j,i) == 255)
{
//Calculating maxP and minP within Win-region:
unsigned char minP = src.at<unsigned char="">(j,i);
unsigned char maxP = src.at<unsigned char="">(j,i);
int offsetInWin = Win/2;
for (int m = - offsetInWin; m < offsetInWin; m++) {
for (int n = - offsetInWin; n < offsetInWin; n++) {
if (src.at<unsigned char="">(j+m,i+n) < minP) {
minP = src.at<unsigned char="">(j+m,i+n);
}else if (src.at<unsigned char="">(j+m,i+n) > maxP) {
maxP = src.at<unsigned char="">(j+m,i+n);
}
}
}
//Voiting:
unsigned char meanP = lroundf((minP+maxP)/2.0);
for (int l = -Win; l < Win; l++) {
for (int k = -Win; k < Win; k++) {
if (src.at<unsigned char="">(j+l,i+k) >= meanP) {
dst->at<unsigned char="">(j+l,i+k)++;
}
}
}
}
}
}
///// Normalization of imageOut:
unsigned char maxValue = dst->at<unsigned char="">(0,0);
for (int j = 0; j < dst->rows; j++) { //finding max value of imageOut
for (int i = 0; i < dst->cols; i++) {
if (dst->at<unsigned char="">(j,i) > maxValue)
maxValue = dst->at<unsigned char="">(j,i);
}
}
float knorm = 255.0 / maxValue;
for (int j = 0; j < dst->rows; j++) { //normalization of imageOut
for (int i = 0; i < dst->cols; i++) {
dst->at<unsigned char="">(j,i) = lroundf(dst->at<unsigned char="">(j,i)*knorm);
}
}