-
大体思想
银行卡号码的识别可简单分为目标检测及字符识别两个部分。
现在网络上能找的到资料比较少,而且多是处理印刷类银行卡的方法。按着二值化-腐蚀膨胀就能够定位到卡号区域;而字符识别靠的是固定阈值分割(如大津法)- 垂直投影法分割字符序列,最后做单一字符的识别。
但这些简易的方法几乎无法识别卡号凸起的银行卡。所以我在做了各种尝试后对其中卡号定位和字符分割部分进行了改进,准确率和识别速度都较不错,可在 1s 内完成识别,同时也支持识别身份证号。
- 操作流程
- 若图片太大,归一化图片(否则影响处理速度); 并灰度化
public class CVGrayTransfer{
public static Mat grayTransferBeforeScale(String fileName) {
Mat src = Imgcodecs.imread(fileName);
final int mw = src.width() > 1024 ? 1024 : src.width();
return grayTransferBeforeScale(src, mw);
}
public static Mat grayTransferBeforeScale(Mat m, int resizeWidth) {
Mat resize;
resize = resizeMat(m, resizeWidth);
Mat dst = new Mat();
Imgproc.cvtColor(resize, dst, Imgproc.COLOR_BGR2GRAY); //灰度化
return dst;
}
public static Mat resizeMat(Mat m, int resizeWidth) {
Imgproc.resize(m, scaleMat, new Size(resizeWidth,
(float)m.height() / m.width() * resizeWidth), 0, 0, INTER_AREA);
return scaleMat;
}
}
public class CardOCR {
public static void main(String []args) {
String fileName= "/Users/...";
Mat gray = CVGrayTransfer.grayTransferBeforeScale(fileName);
}
}
-
寻找卡号区域,即进行特征提取。涉及到的图形学算法:Top-hat/Black-hat 形态学变换,高斯模糊,边缘检测,膨胀。先放上这部分代码的效果图。
这些算法可以使得灰度图像二值化增强卡号特征,忽略银行卡背景细节,理想情况下应该是保留白色 (255) 长条形区域,其余噪声都被去除 (0).
但由于银行卡面纹理图文信息繁杂,加上光线强弱影响,所以会使得长条形区域不规整,出现其余白色区域块等。这需要我们另外实现算法加以甄别定位,第三部分将会给出我采用的方法。
以下是二值特征化的代码,为了加强算法鲁棒性,这里做了两路处理,分别对应强亮度和低亮度的环境。如果发现强亮度特征化不明显,则再使用低亮度算法。
/**
* Created by chenqiu on 3/6/19.
*/
public class CVDilate {
public static Mat dilateBrightRegion(Mat gray0) {
Mat dst = new Mat();
// top-hat enhance contrast
Imgproc.morphologyEx(gray0, dst, Imgproc.MORPH_TOPHAT,
Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(9, 3)));
Imgproc.GaussianBlur(dst, dst, new Size(13, 13), 0);
Imgproc.Canny(dst, dst, 300, 600, 5, true);
Imgproc.dilate(dst, dst, new Mat(), new Point(-1, -1), 5);
Size heavy = new Size(35, 5);
// apply a second dilate operation to the binary image
Imgproc.dilate(dst, dst,
Imgproc.getStructuringElement(Imgproc.MORPH_RECT, heavy));
return dst;
}
public static Mat dilateDarkRegion(Mat gray0) {
Mat dst = new Mat();
// enhance black area by black-hat
Imgproc.morphologyEx(gray0, dst, Imgproc.MORPH_BLACKHAT,
Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(18,10)));
Imgproc.GaussianBlur(dst, dst, new Size(13, 13), 0);
Imgproc.Canny(dst, dst, 300, 600, 5, true);
Size heavy = new Size(35, 3);
Imgproc.dilate(dst, dst,
Imgproc.getStructuringElement(Imgproc.MORPH_RECT, heavy));
return dst;
}
- 银行卡号排列方式有几种,有4-4-4-4的,2-16的,连串的等等,这导致了二值化后卡号区域被膨胀成一块长条形区域或是4块白色区域,见上图。我们的区域定位算法要求能满足所有的特征类型。
首先对图像边缘用黑色(0) 填充,避免后面连通域分析受到干扰。
大致过程:CVRegion 内部类 Filter 实现了定位4-4-4-4 区域的算法,即先得到所有的白色连通域,使用 boundingRect 求它们的外接矩形;对矩形的 Y 轴坐标排序得到最相近的 4 块矩形区域,且这其中每个矩形的 Y 坐标最大不超过 15 pixel,若不满足4块,可以降低到 3 块区域,根据此3块矩形长度和间隙估计出第 4 块矩形位置;合并 4 块矩形,得到卡号区域。
public class CVRegion {
public static final int border = 10;
// 定位后卡号区域二值图像
private Mat binDigitRegion;
public CVRegion(Mat graySrc) {
super(graySrc);
binDigitRegion = null;
}
/**
* fill image border with black pix
* @param m
*/
public static void fillBorder(Mat m) {
int cols = m.cols();
int rows = m.rows();
byte buff[] = new byte[cols * rows];
m.get(0, 0, buff);
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
if ((i > border && j > border) &&
(i < cols - border && j < rows - border))
continue;
buff[j * cols + i] = 0;
}
}
m.put(0, 0, buff);
}
/**
* loc the digit area
* @param src mat proc by binary, top-hat, dilate and closed opr
* @return
*/
public Rect digitRegion(Mat src) throws Exception {
if (src.cols() < 20 || src.rows() < 20)
throw new Exception("error: image.cols() < 20 || image.rows() < 20 in function 'digitRegion(Mat m)'");
fillBorder(src);
// 连通域分析,提取所有白色 (255) 区域
List<MatOfPoint> contours = new ArrayList<>();
Imgproc.findContours(src, contours, new Mat(),
Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Filter filter = new Filter(src);
Rect rect;
rect = filter.boundingIdRect(contours);
if (rect == null) {
/**
检测类型为2-16或连串的长条形区域
参考到第4节
*/
}
if (rect == null)
return null;
// 详细请移步第4节
cutEdgeOfX(rect);
/**
成功定位卡号矩形区域
*/
return rect;
}
}
/**
* 新增一个内部类,提供简单的区域定位 (4-4-4-4)。
*/
final static class Filter extends ImgFilter {
private Mat src;
public Filter(Mat src) {
this.src = src;
}
private void sortMap(int [][]a) {
for (int i = 0; i < a[1].length - 1; i++) {
int k = i;
for (int j = i + 1; j < a[1].length; j++) {
if (a[1][k] > a[1][j]) {
k = j;
}
}
if (k != i) {
a[1][k] = a[1][k] + a[1][i];
a[1][i] = a[1][k] - a[1][i];
a[1][k] = a[1][k] - a[1][i];
a[0][i] = a[0][k] + a[0][i];
a[0][k] = a[0][i] - a[0][k];
a[0][i] = a[0][i] - a[0][k];
}
}
}
/**
* get rect area of id numbers, only work at the 4-4-4-4 type
* @param contours
* @return null if rect of id area not found
*/
public Rect boundingIdRect(List<MatOfPoint> contours) {
Rect rect;
List<Rect> rectSet = new ArrayList<>();
for (int i = 0; i < contours.size(); i++) {
rect = Imgproc.boundingRect(contours.get(i));
rectSet.add(rect);
}
rect = rectSet.get(0);
int dist[][] = new int[2][rectSet.size()];
for (int i = 0; i < rectSet.size(); i++) {
dist[0][i] = i;
dist[1][i] = rectSet.get(i).y - rect.y;
}
sortMap(dist);
final int verBias = 15;
for (int i = 0; i < dist[1].length - 2; i++) {
if (dist[1][i + 2] - dist[1][i] < verBias) {
int k;
/**
* Upper left and lower right corners
*/
int sx = src.width();
int sy = src.height();
int mx = -1;
int my = -1;
// max width between these id-digit area
int mw = 0;
int sw = src.width();
for (k = 0; k < 3; k ++) {
rect = rectSet.get(dist[0][k + i]);
if (!isDigitRegion(rect, src.width(),src.height()))
break;
sx = Math.min(rect.x, sx);
sy = Math.min(rect.y, sy);
mx = Math.max(rect.x + rect.width, mx);
my = Math.max(rect.y + rect.height, my);
mw = Math.max(rect.width, mw);
sw = Math.min(rect.width, sw);
}
// less than 3 area, find next
if (k < 3) {
continue;
}
if (i < dist[1].length - 3) {
if (dist[1][i + 3] - dist[1][i] < verBias &&
isDigitRegion(rect =
rectSet.get(dist[0][i + 3]),
src.width(), src.height())) {
sx = Math.min(sx, rect.x);
sy = Math.min(sy, rect.y);
mx = Math.max(rect.x + rect.width, mx);
my = Math.max(rect.y + rect.height, my);
// finding out all 4 digit area
return new Rect(sx, sy, mx - sx, my - sy);
}
}
// completing 4th digit area
int mg;
//to make the gap largest,avoiding losing digit message
int gap = (mx - sx - sw * 3) >> 1;
Rect rt;
if (sx < (mg = src.width() - mx - gap)) {
rt = mg > mw ?
new Rect(sx, sy, mx + mw + gap - sx , my - sy):
new Rect(10, sy, src.width() - 20, my - sy);
}
else {
mg = sx - gap;
rt = mg > mw ?
new Rect(sx - mw -gap, sy, mx - sx +mw, my -sy):
new Rect(10, sy, src.width() - 20, my - sy);
}
return rt;
}
}
return null;
}
}
/**
* Created by chenqiu on 2/20/19.
*/
public class ImgFilter implements RectFilter {
@Override
public boolean isDigitRegion(Rect rect, int srcWidth, int srcHeight) {
if (rect.width * rect.height < this.MIN_AREA) {
return false;
}
if (srcHeight * this.MIN_HEIGHT_RATE > rect.height ||
srcHeight * this.MAX_HEIGHT_RATE < rect.height) {
return false;
}
if (srcWidth * this.MIN_WIDTH_RATE > rect.width) {
return false;
}
return true;
}
/**
先不管
*/
@Override
public int IDRegionSimilarity(Mat m, Rect r, int rows, int cols) {
int origin = 0;
if (r.y < this.MIN_HEIGHT_RATE * rows)
return origin;
if (r.y > (1 - this.MIN_HEIGHT_RATE) * rows)
return origin;
int y_score = 9;
int bottom = r.y + r.height;
if (r.y > rows * 0.8)
y_score = 5;
if (bottom > rows * 0.9)
y_score = 3;
origin += r.y * y_score;
float avgSimilarity = 0;
List<MatOfPoint> cnt = new ArrayList<>();
Imgproc.findContours(m, cnt, new Mat(), Imgproc.RETR_EXTERNAL,
Imgproc.CHAIN_APPROX_SIMPLE);
for (MatOfPoint contour : cnt) {
Rect rect = Imgproc.boundingRect(contour);
if (rect.height < MIN_AREA)
continue;
double cntArea = Imgproc.contourArea(contour);
int frameSize = rect.width * r.height;
avgSimilarity += (cntArea / frameSize);
origin += cntArea;
}
avgSimilarity /= cnt.size();
origin *= avgSimilarity;
return origin;
}
/**
先不管
*/
@Override
public void findMaxRect(Mat m, Rect r) {
int mainLeft = (int)(r.width * 0.1f) + r.x;
int mainRight = (int)(r.width * 0.9f) + r.x;
int mainCenter = (r.width >> 1) + r.x;
int minWidth = (int)(m.cols() * MIN_WIDTH_RATE);
List<MatOfPoint> contours = new ArrayList<>();
Imgproc.findContours(m, contours, new Mat(), Imgproc.RETR_EXTERNAL,
Imgproc.CHAIN_APPROX_SIMPLE);
for (MatOfPoint contour : contours) {
Rect rect = Imgproc.boundingRect(contour);
int center = rect.x + rect.width / 2;
if (center < mainLeft || center > mainRight ||
rect.height < MIN_AREA)
continue;
// rect frame 【 】
int frameSize = rect.width * r.height;
// white region size in frame 【==】
int frameArea = Core.countNonZero(new Mat(m,
new Rect(rect.x, r.y, rect.width, r.height)));
if (frameArea < frameSize * FULL_AREA_RATIO ||
(rect.height < r.height * FRAME_H_RATIO && rect.width > minWidth)){
if (center < mainCenter) {
r.width -= (center - r.x);
r.x = center;
} else
r.width = center - r.x;
break;
}
}
}
}
/**
* Created by chenqiu on 2/20/19.
*/
public interface RectFilter {
/**
* minimum height or size in roi
*/
int MIN_AREA = 10;
float MIN_HEIGHT_RATE = 0.038f;
float MAX_HEIGHT_RATE = 0.15f;
float MIN_WIDTH_RATE = 0.12f;
/**
* filter out irrelevant areas of the credit card
* @param rect
* @return
*/
boolean isDigitRegion(Rect rect, int srcWidth, int srcHeight);
int HEIGHT_SCORE = 6;
int WIDTH_SCORE = 3;
int IDRegionSimilarity(Mat m, Rect r, int rows, int cols);
float FULL_AREA_RATIO = 0.8f;
float FRAME_H_RATIO = 0.7f;
void findMaxRect(Mat m, Rect r);
}
-
检测非4-4-4-4 特征的算法比较繁琐,也做了比较多的优化提升它的准确度同时保证较快的速度。实现方法是找出近似的长矩形区域,这个矩形区域的长度和高度限制在一定范围内,这样会得到一个多个结果的集合,根据它们的 Y 坐标、长度、矩形相似度/饱满度(白色区域占比)得分对比。最后确定卡号区域。
近似长矩形的确定给这里带来了比较多的麻烦,因为它可能是这里凹一块,那里凸出来一块,或是与上下的噪声区域粘合在一起,又或者是比较短或扁。所以需要设计一个的算法去筛选判定。
简略步骤:首先是横向扫描二值图像,找出最长的白线(1 pixel) 作为基线,对基线上每个 pixel 在 Y 方向延伸,即这个位置的高度;如果连续的 k 个 pixel 的高度不在 [0.038H, 0.15H] 范围 (H:图像高度),则舍弃这个区域。重复至所有区域都筛除完为止。为了避免不必要的过多的扫描,我们需要先锁定数个可疑的连通域:选取图像中最大的前 5 个连通域,且把它们合并。方法类似,由 boundingIdRect 得到的连通域矩形,合并 Y 方向存在重叠的矩形。
public class CVRegion {
....
public Rect digitRegion(Mat src) throws Exception {
....
if (rect == null) {
// if cannot bounding digit area, start separating rect areas which are large
Collections.sort(contours, new Comparator<MatOfPoint>() {
@Override
public int compare(MatOfPoint o1, MatOfPoint o2) {
return -((int) (Imgproc.contourArea(o1) -
Imgproc.contourArea(o2))); // decrease
}
});
final int detectDepth = Math.min(5, contours.size());
int maxScore = 0;
List<Rect> brs = new ArrayList<>();
for (int t = 0; t < detectDepth; t++) {
Rect br = Imgproc.boundingRect(contours.get(t));
filter.combineRect(brs, br);
}
// detect region
// 对每一个合并后的矩形区域进行扫描,得到近似长矩形的区域集合
for (Rect br : brs) {
List<Rect> separates = this.rectSeparate(src, br);
for (Rect r : separates) {
Mat roi = drawRectRegion(src, r);
filter.findMaxRect(roi, r);
// 对集合中的每个长矩形区域计算得分
int score = filter.IDRegionSimilarity(roi, r,
src.rows(), src.cols());
if (score > maxScore) {
maxScore = score;
rect = r;
}
}
}
}
....
}
public static Mat drawRectRegion(Mat src, Rect roi) {
byte buff[] = new byte[src.rows() * src.cols()];
src.get(0, 0, buff);
Mat m = Mat.zeros(src.size(), src.type());
byte out[] = new byte[buff.length];
int row = roi.y + roi.height;
for (int i = roi.y; i < row; i++) {
System.arraycopy(buff, i * src.cols() + roi.x, out,
i * src.cols() + roi.x, roi.width);
}
m.put(0, 0, out);
return m;
}
final static class Filter extends ImgFilter {
....
public void combineRect(List<Rect> combinedList, Rect input) {
int v;
for (v = 0; v < combinedList.size(); v++) {
Rect cell = combinedList.get(v);
int bb = input.y + input.height;
if (bb >= cell.y) {
int bc = cell.y + cell.height;
if (bb <= bc) {
cell.y = Math.min(input.y, cell.y);
// update height
cell.height = bc - cell.y;
break;
}
if (input.y <= bc) {
cell.y = Math.min(input.y, cell.y);
// update height
cell.height = bb - cell.y;
break;
}
}
}
if (v == combinedList.size())
combinedList.add(input);
}
}
}
这里要将 CVRegion 扩展一下,避免类过于臃肿。
使其继承自抽象类 ImgSeparator
public class CVRegion extends ImgSeparator {
....
/**
* remove left and right edge of id region
* @param rect
*/
// (第三节补充)消除图像左右边界的影响
protected void cutEdgeOfX(Rect rect) {
Mat dst = new Mat();
Imgproc.GaussianBlur(grayMat, dst, new Size(13, 13), 0);
Imgproc.Canny(dst, dst, 300, 600, 5, true);
Imgproc.dilate(dst, dst, new Mat(), new Point(-1, -1), 1);
Mat m = new Mat(dst, rect);
byte buff[] = new byte[m.rows() * m.cols()];
m.get(0, 0, buff);
int rows = rect.height;
int cols = rect.width;
int left = rect.x;
int right = rect.x + rect.width;
int w = 0;
for (int i = 0; i < (cols >> 1); i++) {
int h = 0;
for (int j = 0; j < rows; j++) {
int at = j * cols + i;
if (buff[at] == 0 && w == 0) {
break;
}
if (buff[at] != 0) ++h;
}
if (w > 0 && h == 0) break;
if (h == rows) ++w;
if (w > 0)
left = rect.x + i;
}
byte b[] = new byte[dst.cols() * dst.rows()];
dst.get(0, 0 ,b);
if (w > 0) {
int max = 0;
for (int i = 0; i < w; i++) {
int h = extendHeight(b, dst.cols(), left - i, rect.y);
max = Math.max(max, h);
}
// reset
if (max < rect.height * 1.5)
left = rect.x;
}
// right edge
w = 0;
for (int i = cols - 1; i > (cols >> 1); i--) {
int h = 0;
for (int j = 0; j < rows; j++) {
int at = j * cols + i;
if (buff[at] == 0 && w == 0)
break;
if (buff[at] != 0) ++h;
}
if (w > 0 && h == 0) break;
if (h == rows) w++;
if (w > 0)
right = rect.x + i;
}
if (w > 0) {
int max = 0;
for (int i = 0; i < w; i++) {
int h = extendHeight(b, dst.cols(), right + i, rect.y);
max = Math.max(max, h);
}
if (max < rect.height * 1.5)
right = rect.x + rect.width;
}
rect.x = left;
rect.width = right - left;
}
....
}
/**
* Created by chenqiu on 2/21/19.
*/
public abstract class ImgSeparator implements RectSeparator, DigitSeparator{
public Mat grayMat;
// 用于存放单个字符的列表(第 2 章会用到)
protected List<Mat> matListOfDigit;
// 得到的卡号矩形区域
protected Rect rectOfDigitRow;
public ImgSeparator(Mat graySrc) {
this.grayMat = graySrc;
matListOfDigit = new ArrayList<>();
rectOfDigitRow = null;
}
@Override
public List<Rect> rectSeparate(Mat src, Rect region) throws Exception {
if (src.channels() != 1)
throw new Exception("error: image.channels() != 1 in
function 'rectSeparate(Mat m,Rect r)'");
// fist step, remove abnormal height area, fill with 0
int cols = src.cols();
int rows = src.rows();
byte buff[] = new byte[cols * rows];
src.get(0, 0, buff);
List<Rect> stack = new LinkedList<>();
List<Rect> separates = new ArrayList<>();
stack.add(region);
while (!stack.isEmpty()) {
Rect ret = new Rect();
Rect head;
Rect scan = findEdge(buff, cols, rows,
head = stack.remove(0), ret);
if (ret.x > 0 && ret.y > 0) {
separates.add(ret);
}
// separate region
int upper = scan.y - head.y;
int lower = head.y + head.height - scan.y - scan.height;
if (upper > 0) {
stack.add(new Rect(head.x, head.y, head.width, upper));
}
if (lower > 0) {
stack.add(new Rect(head.x, scan.y + scan.height,
head.width, lower));
}
}
return separates;
}
/**
* return rect scanned bounding, remove it for avoiding scanning overtimes
* <p>if finding failed, out.x = out.y = -1</p>
* @param buff
* @param cols
* @param rows
* @param region
* @param out
* @return 扫描出的近似长矩形区域
*/
private Rect findEdge(byte buff[], int cols, int rows, Rect region, Rect out) {
// thresh of `thin`
final int thinH = (int)(RectFilter.MIN_HEIGHT_RATE * rows);
out.x = out.y = -1;
if (region.height < thinH) {
return region.clone();
}
int w = region.x + region.width;
int h = region.y + region.height;
int pivot[] = new int[3]; // the longest continuous line
int len = 0; // length of the line
//找到最长白线作基线
for (int i = region.y; i < h; i++) {
int tLen = 0;
int start = 0;
int gap = 0;
for (int j = 0; j < cols; j++) {
int index = i * cols + j;
if (buff[index] != 0) {
if (tLen++ == 0)
start = j;
if (tLen > len) {
len = tLen;
pivot[0] = start; // start x-pos
pivot[1] = i;
pivot[2] = j; // end x-pos
}
gap = 0;
} else if (++gap > RectFilter.MIN_WIDTH_RATE * cols) {
tLen = 0;
}
}
}
int line = pivot[2] - pivot[0];
if (len < cols * (RectFilter.MIN_WIDTH_RATE * 3)) { // too short
return region.clone();
}
int upperY, lowerY, cnt;
upperY = lowerY = cnt = 0;
int []ha = new int[line];
for (int i = 0; i < line; i++) {
ha[i] = extendHeight(buff, cols,i + pivot[0], pivot[1]);
}
final int normalH = (int)(RectFilter.MAX_HEIGHT_RATE * rows);
// when continuous thin area is too long, assert fail
final int thinW = (int)(RectFilter.MIN_WIDTH_RATE * len);
final int normalW = (int)(0.1 * len);
int cw = 0; // continuous width that fitted normal height
int ctl = 0; // continuous thin len
int y2[][] = new int[2][line];
byte next = -1;
// 扩展 Y 方向获得高度
for (int c = 0; c < line; c++) {
int []ey2 = extendY(buff, cols, c + pivot[0], pivot[1]);
if (ha[c] < normalH) {
if (ha[c] < thinH) {
++ctl;
if (ctl > thinW) {
next = 0; // cannot be changed
}
} else {
ctl = 0;
cw ++;
upperY += ey2[0];
lowerY += ey2[1];
cnt++;
if (cw > normalW && next != 0) {
next = 1;
}
}
} else {
cw = 0;
}
y2[0][c] = ey2[0];
y2[1][c] = ey2[1];
}
// find median
Arrays.sort(y2[0]);
Arrays.sort(y2[1]);
int my1, my2, b = y2[0].length >> 1;
my1 = y2[0][b];
my2 = y2[1][b];
if ((y2[0].length & 0x1) == 0) {
my1 = (y2[0][b] + y2[0][b - 1]) >> 1;
my2 = (y2[1][b] + y2[1][b - 1]) >> 1;
}
Rect scanRect = new Rect(region.x, my1, region.width, my2-my1 +1);
if (next < 1) {
return scanRect;
}
upperY /= cnt;
lowerY /= cnt;
Debug.log("upper: " + upperY + ", lower: " + lowerY);
out.x = pivot[0];
out.y = upperY;
out.width = line;
out.height = lowerY - upperY + 1;
return scanRect;
}
abstract protected Rect cutEdgeOfY(Mat binSingleDigit);
abstract protected void cutEdgeOfX(Rect rect);
public void setRectOfDigitRow(Rect rectOfDigitRow) {
this.rectOfDigitRow = rectOfDigitRow;
}
}
/**
* Created by chenqiu on 2/21/19.
*/
public interface RectSeparator {
List<Rect> rectSeparate(Mat src, Rect region) throws Exception;
}
/**
* Created by chenqiu on 2/21/19.
*/
public interface DigitSeparator {
void digitSeparate() throws Exception;
}
-
到了这里那么我们的工作就已经完成了一半!!即找到了银行卡号所在位置(矩形区域)。
通过调试可以画出定位结果,如下图所示,相似度得分已标注。
最终扩展成的主函数
public class CVGrayTransfer{
....
public static Mat resizeMat(String fileName) {
Mat m = Imgcodecs.imread(fileName);
final int mw = src.width() > 1024 ? 1024 : src.width();
return resizeMat(src, mw);
return scaleMat;
}
}
public class CardOCR {
static class Producer extends CVRegion {
public Producer(Mat graySrc) {
super(graySrc);
}
/**
定位矩形位置
*/
public Rect findMainRect() {
boolean findBright = false;
Mat gray = this.grayMat;
Rect bestRect = new Rect();
final float fullWidth = gray.cols() - Producer.border * 2;
boolean chose;
for ( ; ; findBright = true) {
Mat dilate = CVDilate.fastDilate(gray, findBright);
Rect idRect = null;
chose = false;
try {
idRect = this.digitRegion(dilate);
} catch (Exception e) {
e.printStackTrace();
}
if (idRect != null) {
if (bestRect.width == 0)
chose = true;
else if (idRect.width < fullWidth) {
if (bestRect.width == fullWidth ||
idRect.width > bestRect.width)
chose = true;
}
if (chose) {
bestRect = idRect;
}
}
if (findBright) break;
}
if (bestRect.width == 0) {
System.err.println("OCR Failed.");
exit(1);
}
return bestRect;
}
}
public static void main(String []args) {
String fileName= "/Users/xxx.jpg";
Mat gray = CVGrayTransfer.grayTransferBeforeScale(fileName);
Producer producer = new Producer(gray);
// 定位卡号矩形区域
Rect mainRect = producer.findMainRect();
// 设置矩形区域
producer.setRectOfDigitRow(mainRect);
// 窗口输出
// HighGui.imshow("id numbers", new Mat(gray, mainRect));
}
}
- 后续请看下一篇《Java + OpenCV 实现银行卡号识别 (2)》