一、效果
识别出银行卡上的数字,并显示
注:本文所用所有知识,均在入门系列提到过
原图:
效果:
二、模板制作
目的,将10个数分成10个模板
2.1 加载模板
var imgTemplate = new Mat("NumberTemplate.png");
CvInvoke.Imshow("template", imgTemplate);
2.2 转为灰度
var imgTemplateGray = new Mat();
CvInvoke.CvtColor(imgTemplate, imgTemplateGray, ColorConversion.Bgr2Gray);
CvInvoke.Imshow("template Gray", imgTemplateGray);
2.3 阈值
var imgThreshold = new Mat();
CvInvoke.Threshold(imgTemplateGray, imgThreshold, 10, 255, ThresholdType.BinaryInv);
CvInvoke.Imshow("imgThreshold", imgThreshold);
2.4 轮廓检测
VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint();
var hierarchy = new Mat();
CvInvoke.FindContours(imgThreshold, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
//RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标
var img0 = imgTemplate.Clone();
CvInvoke.DrawContours(img0, contours, -1, new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgContours", img0);
2.5 轮廓排序
一个简单的冒泡排序,根据其X坐标的大小,将轮廓从左到右一一对应:
var array = contours.ToArrayOfArray();
for (int i = 0; i < contours.Size - 1; i++)
{for (int j = 0; j < contours.Size - i - 1; j++)if (array[j][0].X > array[j + 1][0].X){var temp = array[j + 1];array[j + 1] = array[j];array[j] = temp;}
}
var img1 = imgTemplate.Clone();
VectorOfVectorOfPoint contoursSorted = new VectorOfVectorOfPoint(array);
CvInvoke.DrawContours(img1, contoursSorted, -1, new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgContours2", img1);
2.6 计算外接矩形
Rectangle[] rects = new Rectangle[contours.Size];for (int i = 0; i < contoursSorted.Size; i++){rects[i] = CvInvoke.BoundingRectangle(contoursSorted[i]);}var img2 = imgTemplate.Clone();CvInvoke.Rectangle(img2, rects[3], new MCvScalar(0, 0, 255), 2);CvInvoke.Rectangle(img2, rects[4], new MCvScalar(0, 0, 255), 2);CvInvoke.Imshow("imgContours3", img2);
2.7 模板裁剪
将模板裁剪为10个类似下图的小模板
VectorOfMat imgTemplates = new VectorOfMat();foreach (var r in rects){var mat = new Mat(imgTemplateGray, r);imgTemplates.Push(mat);}CvInvoke.Imshow("imgTemplate3", imgTemplates[3]);
三、银行卡操作
3.1 读取银行卡
3.2 转为灰度
var imgCardGray = new Mat();
CvInvoke.CvtColor(imgCard, imgCardGray, ColorConversion.Bgr2Gray);
CvInvoke.Imshow("CardGray", imgCardGray);
3.3 阈值
var imgCardThreshold = new Mat();
CvInvoke.Threshold(imgCardGray, imgCardThreshold, 0, 255, ThresholdType.Otsu | ThresholdType.Binary);
CvInvoke.Imshow("imgCardThreshold", imgCardThreshold);
3.4 顶帽
var rectKernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(9, 3), new Point(-1, -1));
var imgTophat = new Mat();
CvInvoke.MorphologyEx(imgCardThreshold, imgTophat, MorphOp.Tophat, rectKernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar());
CvInvoke.Imshow("tophat", imgTophat);
3.5 sobel
var imgSobel = new Mat();
CvInvoke.Sobel(imgTophat, imgSobel, DepthType.Cv32F, 1, 0, -1);
CvInvoke.ConvertScaleAbs(imgSobel, imgSobel, 1, 0);
CvInvoke.Imshow("sobel", imgSobel);
3.6 闭运算
var imgClose = new Mat();
CvInvoke.MorphologyEx(imgSobel, imgClose, MorphOp.Close, rectKernel, new Point(-1, -1), 4, BorderType.Default, new MCvScalar());
CvInvoke.Imshow("Close", imgClose);
3.7 查找轮廓
VectorOfVectorOfPoint cardContours = new VectorOfVectorOfPoint();
var cardHierarchy = new Mat();
CvInvoke.FindContours(imgClose, cardContours, cardHierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
// RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标
var imgCardContours = imgCard.Clone();
CvInvoke.DrawContours(imgCardContours, cardContours, -1, new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgCardContours", imgCardContours);
3.8 过滤轮廓
int CardRectCount = 4;
Rectangle[] cardRects = new Rectangle[CardRectCount];
CardRectCount = 0;
var imgCardTest = imgCard.Clone();
for (int i = 0; i < cardContours.Size; i++)
{var rect = CvInvoke.BoundingRectangle(cardContours[i]);var radio = rect.Width / (double)rect.Height;if (radio > 2.5 && radio < 4.0 && rect.Height > 20){cardRects[CardRectCount] = rect;CvInvoke.Rectangle(imgCardTest, rect, new MCvScalar(0, 0, 255), 2);CvInvoke.Imshow("imgCardTest", imgCardTest);CardRectCount++;}
}
3.9 轮廓排序
for (int i = 0; i < cardRects.Length - 1; i++)
{for (int j = 0; j < cardRects.Length - i - 1; j++)if (cardRects[j].X > cardRects[j + 1].X){var temp = cardRects[j + 1];cardRects[j + 1] = cardRects[j];cardRects[j] = temp;}
}
var imgcard1 = imgCard.Clone();
CvInvoke.Rectangle(imgcard1, cardRects[0], new MCvScalar(0, 0, 255), 2);
CvInvoke.Rectangle(imgcard1, cardRects[1], new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgContours2", imgcard1);
四、模板匹配
4.1 裁剪轮廓
VectorOfMat cardResizes = new VectorOfMat();
foreach(var rect in cardRects) { // 裁剪四个轮廓var mat= new Mat(imgCardGray,new Rectangle(rect.X-5,rect.Y-5,rect.Width+10,rect.Height+10) );cardResizes.Push(mat);
}
CvInvoke.Imshow("cardResizes[2]", cardResizes[2]);
4.2 裁剪数字
VectorOfMat NumberVector = new VectorOfMat();
for(int i = 0; i < cardResizes.Size; i++)
{// 二值化阈值var th = new Mat();CvInvoke.Threshold(cardResizes[i], th, 0, 255, ThresholdType.Binary | ThresholdType.Otsu);// CvInvoke.Imshow("t", th);// 查找轮廓VectorOfVectorOfPoint c = new VectorOfVectorOfPoint();var h = new Mat();CvInvoke.FindContours(th, c, h, RetrType.External, ChainApproxMethod.ChainApproxSimple);// RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标var imgTemp = cardResizes[i].Clone();CvInvoke.DrawContours(imgTemp, c, -1, new MCvScalar(0, 0, 0), 2);// CvInvoke.Imshow("c", imgTemp);// 计算外接矩形Rectangle[] rs = new Rectangle[4];for (int j = 0; j < c.Size; j++){rs[j] = CvInvoke.BoundingRectangle(c[j]);}// 排序外接矩形for (int x = 0; x < rs.Length - 1; x++){for (int y = 0; y < rs.Length - x - 1; y++)if (rs[y].X > rs[y + 1].X){var temp = rs[y + 1];rs[y + 1] = rs[y];rs[y] = temp;}}// 裁剪数字foreach (var r in rs) {NumberVector.Push(new Mat(cardResizes[i],r));}
}
CvInvoke.Imshow("NumberVector[2]", NumberVector[2]);
4.3 匹配数字
double minLoc = 0, maxLoc = 0;
Point minPoint = new Point();
Point maxPoint = new Point();
int[] result = new int[16];
double[] score = new double[16];
for (int i = 0; i < 16; i++)
{CvInvoke.Threshold(NumberVector[i], NumberVector[i], 0, 255, ThresholdType.BinaryInv | ThresholdType.Otsu);CvInvoke.Resize(NumberVector[i], NumberVector[i], new Size(20, 40));for (int j = 0; j < imgTemplates.Size; j++) {var res = new Mat();CvInvoke.Resize(imgTemplates[j], imgTemplates[j], new Size(20, 40));CvInvoke.MatchTemplate(NumberVector[i], imgTemplates[j], res, TemplateMatchingType.Ccoeff);CvInvoke.MinMaxLoc(res, ref minLoc, ref maxLoc, ref minPoint, ref maxPoint);if (score[i] < maxLoc) {score[i] = maxLoc;result[i] = j;}}}
五、最终显示
5.1 绘制选择框
var display = imgCard.Clone();
foreach (var r in cardRects)
{CvInvoke.Rectangle(display, r, new MCvScalar(0, 0, 255), 2);}
5.2 显示数字
for (int i = 0; i < 16; i++) {CvInvoke.PutText(display, result[i].ToString(), new Point(cardRects[0].X+i*20+i/4*50+10, cardRects[0].Y-10), FontFace.HersheyTriplex, 1, new MCvScalar(0, 0, 255),2);
}CvInvoke.Imshow("result", display);
六、完整代码
// 一、模板操作
// 1. 读取模板
var imgTemplate = new Mat("NumberTemplate.png");
//CvInvoke.Imshow("template", imgTemplate);// 2. 转换为灰度
var imgTemplateGray = new Mat();
CvInvoke.CvtColor(imgTemplate, imgTemplateGray, ColorConversion.Bgr2Gray);
//CvInvoke.Imshow("template Gray", imgTemplateGray);// 3. 阈值
var imgThreshold = new Mat();
CvInvoke.Threshold(imgTemplateGray, imgThreshold, 10, 255, ThresholdType.BinaryInv);
//CvInvoke.Imshow("imgThreshold", imgThreshold);// 4. 轮廓检测
VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint();
var hierarchy = new Mat();
CvInvoke.FindContours(imgThreshold, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
//RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标
var img0 = imgTemplate.Clone();
CvInvoke.DrawContours(img0, contours, -1, new MCvScalar(0, 0, 255), 2);
// CvInvoke.Imshow("imgContours", img0);// 5. 轮廓排序
var array = contours.ToArrayOfArray();
for (int i = 0; i < contours.Size - 1; i++)
{for (int j = 0; j < contours.Size - i - 1; j++)if (array[j][0].X > array[j + 1][0].X){var temp = array[j + 1];array[j + 1] = array[j];array[j] = temp;}
}
var img1 = imgTemplate.Clone();
VectorOfVectorOfPoint contoursSorted = new VectorOfVectorOfPoint(array);
CvInvoke.DrawContours(img1, contoursSorted, -1, new MCvScalar(0, 0, 255), 2);
// CvInvoke.Imshow("imgContours2", img1);// 6. 计算外接矩形
Rectangle[] rects = new Rectangle[contours.Size];
for (int i = 0; i < contoursSorted.Size; i++)
{rects[i] = CvInvoke.BoundingRectangle(contoursSorted[i]);
}
var img2 = imgTemplate.Clone();
CvInvoke.Rectangle(img2, rects[3], new MCvScalar(0, 0, 255), 2);
CvInvoke.Rectangle(img2, rects[4], new MCvScalar(0, 0, 255), 2);
//CvInvoke.Imshow("imgContours3", img2);// 7. 模板裁剪
VectorOfMat imgTemplates = new VectorOfMat();
foreach (var r in rects)
{var mat = new Mat(imgTemplateGray, r);imgTemplates.Push(mat);
}
//CvInvoke.Imshow("imgTemplate3", imgTemplates[3]);
// CvInvoke.Imshow("imgTemplate5", imgTemplates[5]);// 二、银行卡操作
// 2.1 读取银行卡
var imgCard = new Mat("Card1.png");
CvInvoke.Imshow("card", imgCard);// 2.2 转为灰度
var imgCardGray = new Mat();
CvInvoke.CvtColor(imgCard, imgCardGray, ColorConversion.Bgr2Gray);
CvInvoke.Imshow("CardGray", imgCardGray);// 2.3 阈值
var imgCardThreshold = new Mat();
CvInvoke.Threshold(imgCardGray, imgCardThreshold, 0, 255, ThresholdType.Otsu | ThresholdType.Binary);
CvInvoke.Imshow("imgCardThreshold", imgCardThreshold);// 2.4 顶帽操作
var rectKernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(9, 3), new Point(-1, -1));
var imgTophat = new Mat();
CvInvoke.MorphologyEx(imgCardThreshold, imgTophat, MorphOp.Tophat, rectKernel, new Point(-1, -1), 1, BorderType.Default, new MCvScalar());
CvInvoke.Imshow("tophat", imgTophat);// 2.5 sobel
var imgSobel = new Mat();
CvInvoke.Sobel(imgTophat, imgSobel, DepthType.Cv32F, 1, 0, -1);
CvInvoke.ConvertScaleAbs(imgSobel, imgSobel, 1, 0);
CvInvoke.Imshow("sobel", imgSobel);// 2.6 闭运算
var imgClose = new Mat();
CvInvoke.MorphologyEx(imgSobel, imgClose, MorphOp.Close, rectKernel, new Point(-1, -1), 4, BorderType.Default, new MCvScalar());
CvInvoke.Imshow("Close", imgClose);// 2.7 查找轮廓
VectorOfVectorOfPoint cardContours = new VectorOfVectorOfPoint();
var cardHierarchy = new Mat();
CvInvoke.FindContours(imgClose, cardContours, cardHierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
// RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标
var imgCardContours = imgCard.Clone();
CvInvoke.DrawContours(imgCardContours, cardContours, -1, new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgCardContours", imgCardContours);// 2.8 过滤轮廓
int CardRectCount = 4;
Rectangle[] cardRects = new Rectangle[CardRectCount];
CardRectCount = 0;
var imgCardTest = imgCard.Clone();
for (int i = 0; i < cardContours.Size; i++)
{var rect = CvInvoke.BoundingRectangle(cardContours[i]);var radio = rect.Width / (double)rect.Height;if (radio > 2.5 && radio < 4.0 && rect.Height > 20){cardRects[CardRectCount] = rect;CvInvoke.Rectangle(imgCardTest, rect, new MCvScalar(0, 0, 255), 2);CvInvoke.Imshow("imgCardTest", imgCardTest);CardRectCount++;}
}// 2.9 轮廓排序
for (int i = 0; i < cardRects.Length - 1; i++)
{for (int j = 0; j < cardRects.Length - i - 1; j++)if (cardRects[j].X > cardRects[j + 1].X){var temp = cardRects[j + 1];cardRects[j + 1] = cardRects[j];cardRects[j] = temp;}
}
var imgcard1 = imgCard.Clone();
CvInvoke.Rectangle(imgcard1, cardRects[0], new MCvScalar(0, 0, 255), 2);
CvInvoke.Rectangle(imgcard1, cardRects[1], new MCvScalar(0, 0, 255), 2);
CvInvoke.Imshow("imgContours2", imgcard1);// 三、轮廓匹配
// 3.1 裁剪轮廓
VectorOfMat cardResizes = new VectorOfMat();
foreach(var rect in cardRects) { // 裁剪四个轮廓var mat= new Mat(imgCardGray,new Rectangle(rect.X-5,rect.Y-5,rect.Width+10,rect.Height+10) );cardResizes.Push(mat);
// CvInvoke.Imshow("c",mat);}
CvInvoke.Imshow("cardResizes[2]", cardResizes[2]);
// 3.2 裁剪每一个数字
VectorOfMat NumberVector = new VectorOfMat();
for(int i = 0; i < cardResizes.Size; i++)
{// 二值化阈值var th = new Mat();CvInvoke.Threshold(cardResizes[i], th, 0, 255, ThresholdType.Binary | ThresholdType.Otsu);
// CvInvoke.Imshow("t", th);// 查找轮廓VectorOfVectorOfPoint c = new VectorOfVectorOfPoint();var h = new Mat();CvInvoke.FindContours(th, c, h, RetrType.External, ChainApproxMethod.ChainApproxSimple);// RetrType.External 只检测外轮廓, 2. ChainApproxMethod.ChainApproxSimple 只保留终点坐标var imgTemp = cardResizes[i].Clone();CvInvoke.DrawContours(imgTemp, c, -1, new MCvScalar(0, 0, 0), 2);
// CvInvoke.Imshow("c", imgTemp);// 计算外接矩形Rectangle[] rs = new Rectangle[4];for (int j = 0; j < c.Size; j++){rs[j] = CvInvoke.BoundingRectangle(c[j]);}// 排序外接矩形for (int x = 0; x < rs.Length - 1; x++){for (int y = 0; y < rs.Length - x - 1; y++)if (rs[y].X > rs[y + 1].X){var temp = rs[y + 1];rs[y + 1] = rs[y];rs[y] = temp;}}// 裁剪数字foreach (var r in rs) {NumberVector.Push(new Mat(cardResizes[i],r));}
}
CvInvoke.Imshow("NumberVector[2]", NumberVector[2]);
// 3.3 匹配数字
double minLoc = 0, maxLoc = 0;
Point minPoint = new Point();
Point maxPoint = new Point();
int[] result = new int[16];
double[] score = new double[16];
for (int i = 0; i < 16; i++)
{CvInvoke.Threshold(NumberVector[i], NumberVector[i], 0, 255, ThresholdType.BinaryInv | ThresholdType.Otsu);CvInvoke.Resize(NumberVector[i], NumberVector[i], new Size(20, 40));NumberVector[i].Save($"0{i}.jpg");for (int j = 0; j < imgTemplates.Size; j++) {var res = new Mat();CvInvoke.Resize(imgTemplates[j], imgTemplates[j], new Size(20, 40));imgTemplates[j].Save($"1_{j}.jpg");CvInvoke.MatchTemplate(NumberVector[i], imgTemplates[j], res, TemplateMatchingType.Ccoeff);CvInvoke.MinMaxLoc(res, ref minLoc, ref maxLoc, ref minPoint, ref maxPoint);if (score[i] < maxLoc) {score[i] = maxLoc;result[i] = j;}}}// 四、 最终显示
var display = imgCard.Clone();
// 4.1 绘制选择框
foreach (var r in cardRects)
{CvInvoke.Rectangle(display, r, new MCvScalar(0, 0, 255), 2);}
CvInvoke.Imshow("result1", display);
// 4.2 显示数字
for (int i = 0; i < 16; i++) {CvInvoke.PutText(display, result[i].ToString(), new Point(cardRects[0].X+i*20+i/4*50+10, cardRects[0].Y-10), FontFace.HersheyTriplex, 1, new MCvScalar(0, 0, 255),2);
}CvInvoke.Imshow("result", display);CvInvoke.WaitKey(0);