C#-opencv-图像中数字提取
本人初学者,正在学习C#中的opencv操作,下述代码目的是通过图像识别对银行卡的卡号进行识别并提取,要求位置置于银行卡原图中卡号正上方;
此次学习过程中通过查询python中的轮廓排序算法,手写了一个简易算法,方能实现此次学习的目的,同时加深了解了matchtemplate与matchshapes的应用区别,希望大家在阅读期间发现的问题的,及时反馈,本人会加以修正并提升!!!
卡片图像(百度获取)
数字模板图像(百度获取)
0.准备工作
using System;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using Size = OpenCvSharp.Size;
using Point = OpenCvSharp.Point;
1.定义轮廓排序简易函数-contours_sort
排序原理简单如下:
由上到下-通过轮廓获取外边界矩形(x,y,width,height)-y坐标从小到大排序
由下到上-通过轮廓获取外边界矩形(x,y,width,height)-y坐标+height(矩形高度)从大到小排序
由左到右-通过轮廓获取外边界矩形(x,y,width,height)-x坐标从小到大排序
由右到左-通过轮廓获取外边界矩形(x,y,width,height)-x坐标+width(矩形宽度)从大到小排序
轮廓面积由小到大-cv2.contourarea获取轮廓面积
轮廓面积由大到小
轮廓周长由小到大-cv2.arclength获取轮廓周长
轮廓周长由大到小
//轮廓排序函数简易C#版
static private Point[][] contours_sort(Point[][] cnts,string sortMethod)
{
var contours = cnts;
int cnt_num = contours.Length;
Rect[] rects = new Rect[cnt_num];
for(int i = 0; i < cnt_num; i++) rects[i] = Cv2.BoundingRect(contours[i]);
string[] sortType = {
"top-to-bottom", //0-坐标由上到下排序
"bottom-to-top", //1-坐标由下到上排序
"left-to-right", //2-坐标由左到右排序
"right-to-left", //3-坐标由右到左排序
"area-min-to-max", //4-面积-从小到大
"area-max-to-min", //5-面积-从大到小
"length-min-to-max", //6-周长-从小到达
"length-max-to-min" //7周长从大到小
};
int id = -1;
for(int i = 0; i < sortType.Length; i++)
{
if (sortMethod == sortType[i]) id = i;
}
switch (id) {
case 0:
for(int i = 0; i < cnt_num; i++)
{
for(int j = i+1; j < cnt_num; j++)
{
if (rects[j].Y <= rects[i].Y)
{
Rect tpr = rects[i];
rects[i] = rects[j];
rects[j] = tpr;
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 1:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (rects[j].Y+ rects[j].Height >= rects[i].Y + rects[i].Height)
{
Rect tpr = rects[i];
rects[i] = rects[j];
rects[j] = tpr;
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 2:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (rects[j].X <= rects[i].X)
{
Rect tpr = rects[i];
rects[i] = rects[j];
rects[j] = tpr;
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 3:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (rects[j].X+rects[j].Width >= rects[i].X + rects[i].Width)
{
Rect tpr = rects[i];
rects[i] = rects[j];
rects[j] = tpr;
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 4:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (Cv2.ContourArea(contours[j])<= Cv2.ContourArea(contours[j]))
{
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 5:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (Cv2.ContourArea(contours[j]) >= Cv2.ContourArea(contours[j]))
{
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 6:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (Cv2.ArcLength(contours[j],false) <= Cv2.ArcLength(contours[j], false))
{
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
case 7:
for (int i = 0; i < cnt_num; i++)
{
for (int j = i + 1; j < cnt_num; j++)
{
if (Cv2.ArcLength(contours[j], false) >= Cv2.ArcLength(contours[j], false))
{
var temp = contours[i];
contours[i] = contours[j];
contours[j] = temp;
}
}
}
break;
}
return contours;
}
2.常规参数设置
//标准尺寸设定
int stand_width = 57;
int stand_height = 88;
//图像路径
string c_path = "D://Jay.Lee//Study//2023//imgs//card.png";
string t_path = "D://Jay.Lee//Study//2023//imgs//number.png";
//图像获取
Mat card = Cv2.ImRead(c_path);
Mat template = Cv2.ImRead(t_path);
//图像显示
Cv2.ImShow("card原图显示", card);
Cv2.ImShow("数字模板显示", template);
3.数字模板处理
//1.数字模板处理
Mat tgray = new Mat();
//1.1图像灰度处理
Cv2.CvtColor(template, tgray, ColorConversionCodes.BGR2GRAY);
//1.2图像二值化
Cv2.Threshold(tgray, tgray, 50, 255, ThresholdTypes.BinaryInv);
//1.3图像图像轮廓查找
Cv2.FindContours(tgray.Clone(), out Point[][] tcnts, out HierarchyIndex[] hierarchy,
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
//此处注意-后期依旧使用tgray图像,此处使用tgray.clone()
//1.4轮廓从左向右排序
tcnts = contours_sort(tcnts, "left-to-right");
//1.5从二值化图中扣取每个存在外轮廓的矩形
Mat[] templateImages = new Mat[tcnts.Length];
for (int i = 0; i < tcnts.Length; i++)
{
Rect r = Cv2.BoundingRect(tcnts[i]);
templateImages[i] = new Mat(tgray, new Rect(r.X , r.Y , r.Width , r.Height ));
Cv2.Resize(templateImages[i], templateImages[i], new Size(stand_width, stand_height),0,0);
}
4.卡片图像预处理
//2.卡片预处理
//2.1形态学核定义
var rowkernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(11, 3));
var gapkernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(9, 9));
Mat cgray = new Mat();
//2.2图像灰度处理
Cv2.CvtColor(card, cgray, ColorConversionCodes.BGR2GRAY);
//2.3图像二值化(Binary和Otsu自动判断)
Cv2.Threshold(cgray, cgray, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
//2.4图像缩小,便于后期形态学处理
Mat small = cgray.Clone();
Cv2.Resize(small, small, new Size(0, 0), 0.5, 0.5);
//2.5两次闭处理,保证数字全部提取
Cv2.MorphologyEx(small, small, MorphTypes.Close, rowkernel);
Cv2.MorphologyEx(small, small, MorphTypes.Close, gapkernel);
//2.6再次二值化
Cv2.Threshold(small, small, 2, 255, ThresholdTypes.Binary);
//2.6二值化图像放大至原图像尺寸
Cv2.Resize(small, small, new Size(0, 0), 2, 2);
//2.7二值化图像中轮廓提取
Cv2.FindContours(small.Clone(), out Point[][] ccnts, out HierarchyIndex[] chierarchy,
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
//2.8查询待得知数字轮廓及特征并进行筛选,此处得知高度为30
int select_parts = 0;
for (int i = 0; i < ccnts.Length; i++)
{
Rect rect = Cv2.BoundingRect(ccnts[i]);
if (rect.Height == 30) select_parts += 1;
}
Point[][] sccnts = new Point[select_parts][];
for(int i = 0; i < ccnts.Length; i++)
{
Rect rect = Cv2.BoundingRect(ccnts[i]);
if (rect.Height == 30)
//筛选高度满足卡号数字特征的轮廓
{
for(int j = 0; j < select_parts; j++)
{
if (sccnts[j] == null)
{
sccnts[j] = ccnts[i];
break;
}
}
}
}
//2.9轮廓由左向右排序
sccnts = contours_sort(sccnts, "left-to-right");
Rect[] rects = new Rect[select_parts];
//3.模板中数字查询并添加至原图
for (int i = 0; i < select_parts; i++) rects[i] = Cv2.BoundingRect(sccnts[i]);
//3.1对上述的每个轮廓切为仅存在一个外轮廓图像
for(int i = 0; i < select_parts; i++)
{
string numbers = null;//初始化待添加至图像的字符串
Mat card_parts = new Mat(cgray, rects[i]));//初始化存在卡号的局部卡片图像
Cv2.FindContours(card_parts.Clone(), out Point[][] partcnts, out HierarchyIndex[] part_hierarchy,
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
//寻找全部外轮廓
Cv2.Threshold(card_parts, card_parts, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
//卡片局部图像二值化
partcnts = contours_sort(partcnts, "left-to-right");
Rect[] part_rects = new Rect[partcnts.Length];
5.模板数字与卡片扣取数字匹配
//3.2扣取每个单个轮廓中的数字,并与模板匹配,添加至原图
for(int j = 0; j < partcnts.Length; j++)
{
part_rects[j] = Cv2.BoundingRect(partcnts[j]);
Mat part = new Mat(card_parts, rects[j]);
Cv2.Resize(part, part, new Size(stand_width, stand_height));
//将扣取数字图像尺寸与模板数字图像尺寸相同
double max_score = double.NegativeInfinity;
//初始化每个图像的最佳成绩
double max_id = -1;
//初始化对应最佳模板的id
for(int k = 0; k < templateImages.Length; k++)
{
Mat matchr = new Mat();
Cv2.MatchTemplate(templateImages[k], part, matchr, TemplateMatchModes.CCoeff);
//将图像与模板中每个图像进行模板匹配
Cv2.MinMaxLoc(matchr, out double minval, out double score);
//获取最佳成绩及相应id
if (score >= max_score)
{
max_score = score;
max_id = k;
}
}
numbers += max_id.ToString();//存储至字符串,以备后期添加
}
//将字符串添加至原图
Cv2.PutText(card, numbers, new Point(rects[i].X, rects[i].Y - 20), HersheyFonts.HersheySimplex, 1, new Scalar(0, 0, 255), 2);
}
Cv2.ImShow("卡片数字显示", card);
Cv2.WaitKey();
Cv2.DestroyAllWindows();
Cv2.WaitKey(0);
}
}
图像处理的最终结果