C# 自制OCR获取图片中的电子数字
- 0.前言
- 1.项目背景
- 1.思路分析
- 1.1 找对应电子数字字体
- 1.2 数字字体分析
- 1.2.1 将数字【8】截图
- 1.2.2 根据数字【8】截图像素标识辨识点
- 1.2.2.1 上下部分的中点 O~1~、O~2~
- 1.2.2.2 笔画中点 A ~ G
- 1.2.2.3 外围点
- 2. 流程图
- 3.初见代码
- 4.初见代码运行效果
- 4.1运行结果
- *4.2 黑白二值化、柔化后的效果图
- *4.3 运行效率
- *4.3.1 如果想效率高点的话可以试下
- *4.3.2.1 只黑白化、二值化、柔化识别范围内的图像区域
- *4.3.2.2 配置多线程
- 5.添加Winform的UI处理的成品
- 5.1.字体亮度
- 5.2.柔化、消除散点
- 5.3.运行效果图
- 6.样例下载链接
0.前言
1.项目背景
由于公司某些数控机器不便增加或换置带通讯功能的仪表
将机器运行数据传输到电脑
所以使用【OCR】模式通过摄像头间断性获取机器照片
分析图片中的电子数字来获取运行数据
如下图:
由于这是电子数字一般的【OCR】接口、软件基本识别不出或者不准确然后就针对这种电子数字写个自定义的【OCR】识别
1.思路分析
现在最重要是【教】程序怎么识别图片
根据图片中的像素【颜色】判断获取图片电子数字
1.1 找对应电子数字字体
根据图片中的数字字体在网上找到相似的字体
上图中的数字是有点左斜体
于是选择了比较合适的字体【PUTHIAfont】
如需其他字体可参考:
选择对应字体库【ttf】下载并安装到电脑
不安装也可以直接双击打开,截图里面最大的【8】用到下一步
1.2 数字字体分析
1.2.1 将数字【8】截图
因为数字【8】是【笔画】最全的数字
所以用数字【8】来作为取点标准
在文档软件(例如记事本、Word)中使用较大字号的数字【8】
然后截图(QQ、微信的截图功能就行)复制到【画图】中
在【画图】中开启【网格线】将字体截图放到最大(800%)
1.2.2 根据数字【8】截图像素标识辨识点
这里的每一个格子都是一个像素
数字会变,但可以根据不变的【笔画】及必须是【空】的像素点确定一个电子数字
其中单位长度 L = yO1 - yA
1.2.2.1 上下部分的中点 O1、O2
绿色点 O1、O2
这两点必须是【非笔画】
1.2.2.2 笔画中点 A ~ G
红色点A~G是各个数字【笔画】的【中点】
根据该点及方向延展的红色线段(代码中该线段长度未单位长度【L】)
下面将根据这些【笔画】的【有 / 无】形成的组合判断是数字几
1.2.2.3 外围点
这16个橙色十字星点必须是【非笔画】
因为是从左往右取像素点识别
如果没有外围点的判断【8】、【0】、【6】这3个数字
因为【B】【E】边都是【笔画】会误判识别出【1】
这里如果严谨点可以将【外围点】围成【外围圈】整个圈都必须是【非笔画】
注意:像素坐标的纵坐标是自上而下增大
2. 流程图
3.初见代码
根据画图坐标获取范围坐标值
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
/// <summary>
/// 方向
/// </summary>
public enum Direction
{
/// <summary>
/// 中(无方向)
/// </summary>
Middle,
/// <summary>
/// 左
/// </summary>
Left,
/// <summary>
/// 上
/// </summary>
Up,
/// <summary>
/// 右
/// </summary>
Right,
/// <summary>
/// 下
/// </summary>
Down,
}
static class Program
{
/// <summary>
/// 电子数字最小像素单位长度(详见【像素图解】)
/// </summary>
private const int L_min = 3;
/// <summary>
/// 电子数字最大像素单位长度(详见【像素图解】)
/// </summary>
private const int L_max = 15;
/// <summary>
/// 【白色数字】的 32 位 ARGB 值
/// </summary>
private static readonly int NumArgb = Color.White.ToArgb();
/// <summary>
/// 【黑色背景】的 32 位 ARGB 值
/// </summary>
private static readonly int backgroundArgb = Color.Black.ToArgb();
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
using (Bitmap bitmap = new Bitmap(@"C:\Users\Administrator\Desktop\微信图片_20220217162526.jpg"))
{
#region 黑白化二值化
for (int x = 0; x < bitmap.Width; x++)
for (int y = 0; y < bitmap.Height; y++)
{
Color pointColor = bitmap.GetPixel(x, y);
int r = pointColor.R, g = pointColor.G, b = pointColor.B, Result;
#region 平均值法
//Result = ((r + g + b) / 3);
#endregion
#region 最大值法
//Result = r > g ? r : g;
//Result = Result > b ? Result : b;
#endregion
Result = (int)(0.7 * r) + (int)(0.2 * g) + (int)(0.1 * b);//加权平均值法
Result = Result < 255 / 5 * 4 ? 0 : 255;// 黑白图片二值化(根据字体亮度调节)
bitmap.SetPixel(x, y, Color.FromArgb(Result, Result, Result));
}
#endregion
#region 消除散点及凸点
List<Point> turnBlackPoints = new List<Point>();
List<Point> turnWhitePoints = new List<Point>();
for (int x = 1; x < bitmap.Width - 1; x++)
for (int y = 1; y < bitmap.Height - 1; y++)
{
int pointColorArgb = bitmap.GetPixel(x, y).ToArgb();
if (NeedSoften(bitmap, x, y, pointColorArgb))
if (pointColorArgb == NumArgb)
turnBlackPoints.Add(new Point(x, y));
else if (pointColorArgb == backgroundArgb)
turnWhitePoints.Add(new Point(x, y));
}
foreach (Point point in turnBlackPoints)
bitmap.SetPixel(point.X, point.Y, Color.Black);
foreach (Point point in turnWhitePoints)
bitmap.SetPixel(point.X, point.Y, Color.White);
#endregion
bitmap.Save(@"C:\Users\Administrator\Desktop\微信图片_20220217162526 test.jpg");// 可选: 保存黑白二值化截图
int numLength = 4;// 数字位数
#region 另一种存储对象
//List<Rectangle> rectangles = new List<Rectangle>
//{
// new Rectangle(new Point(496,873),new Size(575-496,897-873)),
// new Rectangle(new Point(787,874),new Size(873-787,899-874)),
// new Rectangle(new Point(1104,876),new Size(1194-1104,900-876)),
//};
#endregion
List<int> x_mins = new List<int> { 496, 787, 1104 };
List<int> x_maxs = new List<int> { 575, 873, 1194 };
List<int> y_mins = new List<int> { 873, 874, 876 };
List<int> y_maxs = new List<int> { 897, 899, 900 };
List<int> resultNums = new List<int>();
for (int i = 0; i < x_mins.Count; i++)
{
int x_min = x_mins[i];
int x_max = x_maxs[i];
int y_min = y_mins[i];
int y_max = y_maxs[i];
Dictionary<int, Dictionary<Point, int>> l_pointMsgs = new Dictionary<int, Dictionary<Point, int>>();// 不同单位长度的识别结果
// 以A点为起点获取其他点的坐标(共9个点)
//for (int l = L_min; l < bitmap.Height / 4 && l < bitmap.Width / 2 && l < L_max; l++)// 全图
for (int l = L_min; l < (y_max - y_min) / 4 && l < (x_max - x_min) / 2 && l < L_max; l++)//
{
Dictionary<Point, int> point_num = new Dictionary<Point, int>();
Point lastPoint = new Point();// 最后一个点
for (int y = y_min; y < y_max - l * 4; y++)
for (int x = l + 1 * 2 + x_min; x < x_max - l * 1; x++)
{
int num = GetNum(bitmap, x, y, l);
if (num == -1)// 非数字的合并,负值越大范围越宽
{
if (x > l + 1 * 2 + x_min)
{
int lastNum = point_num[lastPoint];
if (lastNum < 0)
{
if (1 - lastNum == x_max - x_min - l * 2 - 1 * 2)// 等效于 lastNum - 1 == -((x_max - x_min - l * 1) - (l + 1 * 2))
point_num.Remove(lastPoint);// 是否整行都没有识别到
else
point_num[lastPoint] = lastNum - 1;
continue;
}
}
lastPoint = new Point(x, y);
point_num.Add(lastPoint, -1);
}
else// 数字则记录 test
{
lastPoint = new Point(x, y);
point_num.Add(new Point(x, y), num);
}
}
l_pointMsgs.Add(l, point_num);
}
List<string> numStrings = new List<string>(); // 不同单位长度的结果
foreach (KeyValuePair<int, Dictionary<Point, int>> l_pointMsg in l_pointMsgs)
{
string numString = "";
int preY = 0;
foreach (KeyValuePair<Point, int> point_num in l_pointMsg.Value.ToList())
{
if (preY != point_num.Key.Y)
{
if (preY != 0)
numString += "*";// 以【*】分割不同纵坐标的结果
preY = point_num.Key.Y;
}
if (point_num.Value > -1)
{
if (string.IsNullOrWhiteSpace(numString)
|| numString.Last() == '#'
|| (char.TryParse(point_num.Value + "", out char numChar) && numString.Last() != numChar))// 合并相近且相同的识别结果
numString += point_num.Value;
}
else
numString += "#";// 以【#】(-1)分割单独识别的数字
}
string preNum = "";
List<string> discussNums = new List<string>();// 如果匹配结果有不同则集中讨论分析
foreach (string num in numString.Replace("#", "").Split('*'))
{
if (string.IsNullOrWhiteSpace(preNum))
preNum = num;
else if (preNum != num)
preNum = num;
if (!discussNums.Contains(num) && num.Length == numLength)// 只对匹配出目标位数的进行讨论
discussNums.Add(num);
}
if (discussNums.Count == 0)
Console.WriteLine("单位长度l为【" + l_pointMsg.Key + "】在【(" + x_min + "," + y_min + ") ~ (" + x_max + "," + y_max + ")】范围内无匹配结果!");
else if (discussNums.Count == 1)
{
if (!numStrings.Contains(discussNums.First()))
numStrings.Add(discussNums.First());// 集中当前单位长度的结果
Console.WriteLine("单位长度l为【" + l_pointMsg.Key + "】在【(" + x_min + "," + y_min + ") ~ (" + x_max + "," + y_max + ")】范围内匹配结果为【" + discussNums.First() + "】");
}
else if (discussNums.Count > 1)
{
string result = "";
foreach (string discussNum in discussNums)
result += discussNum + ",";
Console.WriteLine("单位长度l为【" + l_pointMsg.Key + "】在【(" + x_min + "," + y_min + ") ~ (" + x_max + "," + y_max + ")】范围内有多匹配结果【" + result.TrimEnd(',') + "】!");
}
}
if (numStrings.Count > 1)
{
string result = "";
foreach (string numString in numStrings)
result += numString + ",";
Console.WriteLine("在【(" + x_min + "," + y_min + ") ~ (" + x_max + "," + y_max + ")】范围内有多匹配结果【" + result.TrimEnd(',') + "】!");
}
else if (numStrings.Count == 1 && int.TryParse(numStrings.First(), out int num))
resultNums.Add(num);
else
Console.WriteLine("在【(" + x_min + "," + y_min + ") ~ (" + x_max + "," + y_max + ")】范围内无匹配结果!");
}
}
}
/// <summary>
/// 以A点为起点识别数字
/// </summary>
/// <param name="bitmap"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="l"></param>
/// <returns></returns>
private static int GetNum(Bitmap bitmap, int x, int y, int l)
{
int argb_O1 = GetPointArgbFromBitmap(bitmap,x, y + l);// O1点
if (argb_O1 == NumArgb)
return -1;
int argb_O2 = GetPointArgbFromBitmap( bitmap,x - 1, y + l * 3);// O2点
if (argb_O2 == NumArgb)
return -1;
if (false
#region 数字周围的点必须是背景色
|| GetPointArgbFromBitmap(bitmap, x - l, y - l) == NumArgb // A左
|| GetPointArgbFromBitmap(bitmap, x , y-l) == NumArgb// A中
|| GetPointArgbFromBitmap(bitmap, x + l, y - l) == NumArgb // A右
|| GetPointArgbFromBitmap(bitmap, x + l * 2, y) == NumArgb // C上
|| GetPointArgbFromBitmap(bitmap, x + l * 2, y + l * 1) == NumArgb // C中
|| GetPointArgbFromBitmap(bitmap, x + l * 2 - 1, y + l * 2) == NumArgb // C下&D右&F上
|| GetPointArgbFromBitmap(bitmap, x + l * 2 - 1, y + l * 3) == NumArgb// F中
|| GetPointArgbFromBitmap(bitmap, x + l * 2 - 1, y + l * 4) == NumArgb// F下
|| GetPointArgbFromBitmap(bitmap, x + l - 2, y + l * 5) == NumArgb// G右
|| GetPointArgbFromBitmap(bitmap, x - 2, y + l * 5) == NumArgb// G中
|| GetPointArgbFromBitmap(bitmap, x - l - 2, y + l * 5) == NumArgb // G左
|| GetPointArgbFromBitmap(bitmap, x - l * 2 - 1, y + l * 4) == NumArgb// E下
|| GetPointArgbFromBitmap(bitmap, x - l * 2 - 1, y + l * 3) == NumArgb// E中
|| GetPointArgbFromBitmap(bitmap, x - l * 2 - 1, y + l * 2) == NumArgb// E上&D左&B下
|| GetPointArgbFromBitmap(bitmap, x - l * 2, y + l * 1) == NumArgb// B中
|| GetPointArgbFromBitmap(bitmap, x - l * 2, y) == NumArgb// B上
#endregion
#region 获取笔画中点颜色 且 判断线性范围是否都为同色
|| !TryGetLineFromPointArgb(bitmap, x, y, l, true, out int argb_A)// A点
|| !TryGetLineFromPointArgb(bitmap, x - l, y + l, l, false, out int argb_B)// B点
|| !TryGetLineFromPointArgb(bitmap, x + l, y + l, l, false, out int argb_C)// C点
|| !TryGetLineFromPointArgb(bitmap, x - 1, y + l * 2, l, true, out int argb_D)// D点
|| !TryGetLineFromPointArgb(bitmap, x - l - 1, y + l * 3, l, false, out int argb_E)// E点
|| !TryGetLineFromPointArgb(bitmap, x + l - 1, y + l * 3, l, false, out int argb_F)// F点
|| !TryGetLineFromPointArgb(bitmap, x - 2, y + l * 4, l, true, out int argb_G)// G点
#endregion
)
return -1;
#region 数字笔画
if (
//new List<int> { argb_O1, argb_O2 }.All((c) => { return c == backgroundArgb; })// 全黑
//&&
new List<int> { argb_A, argb_B, argb_C, argb_D, argb_E, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 8;
else if (
new List<int> { argb_D }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_B, argb_C, argb_E, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 0;
else if (
new List<int> { argb_C }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_B, argb_D, argb_E, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 6;
else if (
new List<int> { argb_E }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_B, argb_C, argb_D, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 9;
else if (
new List<int> { argb_B, argb_F }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_C, argb_D, argb_E, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 2;
else if (
new List<int> { argb_B, argb_E }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_C, argb_D, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 3;
else if (
new List<int> { argb_C, argb_E }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_B, argb_D, argb_F, argb_G }.All((c) => { return c == NumArgb; })// 全白
)
return 5;
else if (
new List<int> { argb_A, argb_E, argb_G }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_B, argb_C, argb_D, argb_F }.All((c) => { return c == NumArgb; })// 全白
)
return 4;
else if (
new List<int> { argb_B, argb_D, argb_E, argb_G }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_A, argb_C, argb_F }.All((c) => { return c == NumArgb; })// 全白
)
return 7;
else if (
new List<int> { argb_A, argb_B, argb_D, argb_E, argb_G }.All((c) => { return c == backgroundArgb; })// 全黑
&&
new List<int> { argb_C, argb_F }.All((c) => { return c == NumArgb; })// 全白
)
return 1;
else
return -1;
#endregion
}
/// <summary>
/// 获取该点所在直线颜色(非纯色时返回【Color.Empty】)
/// </summary>
/// <param name="bitmap">图像</param>
/// <param name="x">横坐标</param>
/// <param name="y">纵坐标</param>
/// <param name="l">单位长度</param>
/// <param name="isLine">横线true/直线false</param>
/// <returns>是否合法中点</returns>
private static bool TryGetLineFromPointArgb(Bitmap bitmap, int x, int y, int l, bool isLine, out int pointArgb)
{
pointArgb = bitmap.GetPixel(x, y).ToArgb();
if (isLine)// 横线笔画
{
for (int i = x > l / 2 ? x - l / 2 : x; i < x + l / 2; i++)
if (bitmap.GetPixel(i, y).ToArgb() != pointArgb)
return false;// 目标点小范围直线的颜色不纯
}
else// 纵线笔画
{
for (int i = y > l / 2 ? y - l / 2 : y; i < y + l / 2; i++)
if (bitmap.GetPixel(x, i).ToArgb() != pointArgb)
return false;// 颜色不纯
}
return true;
}
/// <summary>
/// 获取画布中某点Argb值
/// </summary>
/// <param name="l"></param>
/// <returns></returns>
private static int GetPointArgbFromBitmap(Bitmap bitmap, int x, int y)
{
if (x < 0 || x >= bitmap.Width || y < 0 || y >= bitmap.Height)
return Color.Empty.ToArgb();
return bitmap.GetPixel(x, y).ToArgb();
}
/// <summary>
/// 该点是否需要柔化
/// </summary>
/// <param name="bitmap">画布</param>
/// <param name="x">当前点的横坐标</param>
/// <param name="y">当前点的纵坐标</param>
/// <param name="pointColorArgb">当前点的颜色Argb</param>
/// <returns></returns>
private static bool NeedSoften(Bitmap bitmap, int x, int y, int pointColorArgb)
{
int nearPointsColorArgb = backgroundArgb;
if (pointColorArgb == nearPointsColorArgb)
nearPointsColorArgb = NumArgb;
List<Direction> directions = new List<Direction> { Direction.Left, Direction.Right, Direction.Up, Direction.Down };
int nearColorCount = 0;
if (bitmap.GetPixel(x - 1, y).ToArgb() == nearPointsColorArgb)
{
directions.Remove(Direction.Left);
nearColorCount++;
}
if (bitmap.GetPixel(x + 1, y).ToArgb() == nearPointsColorArgb)
{
directions.Remove(Direction.Right);
nearColorCount++;
}
if (bitmap.GetPixel(x, y - 1).ToArgb() == nearPointsColorArgb)
{
directions.Remove(Direction.Up);
nearColorCount++;
}
if (bitmap.GetPixel(x, y + 1).ToArgb() == nearPointsColorArgb)
{
directions.Remove(Direction.Down);
nearColorCount++;
}
if (nearColorCount == 4)
return true;
else if (nearColorCount == 3)
{
Direction direction = directions.First();// 仍是反色的方向
switch (direction)
{
case Direction.Left:
if (bitmap.GetPixel(x - 1, y - 1).ToArgb() == pointColorArgb // 左上
&& bitmap.GetPixel(x - 1, y + 1).ToArgb() == pointColorArgb// 左下
&& bitmap.GetPixel(x + 1, y + 1).ToArgb() == nearPointsColorArgb// 右下
&& bitmap.GetPixel(x + 1, y - 1).ToArgb() == nearPointsColorArgb// 右上
)
return true;
break;
case Direction.Up:
if (bitmap.GetPixel(x - 1, y - 1).ToArgb() == pointColorArgb // 左上
&& bitmap.GetPixel(x - 1, y + 1).ToArgb() == nearPointsColorArgb// 左下
&& bitmap.GetPixel(x + 1, y - 1).ToArgb() == pointColorArgb// 右上
&& bitmap.GetPixel(x + 1, y + 1).ToArgb() == nearPointsColorArgb// 右下
)
return true;
break;
case Direction.Right:
if (bitmap.GetPixel(x - 1, y - 1).ToArgb() == nearPointsColorArgb // 左上
&& bitmap.GetPixel(x - 1, y + 1).ToArgb() == nearPointsColorArgb// 左下
&& bitmap.GetPixel(x + 1, y - 1).ToArgb() == pointColorArgb// 右上
&& bitmap.GetPixel(x + 1, y + 1).ToArgb() == pointColorArgb// 右下
)
return true;
break;
case Direction.Down:
if (bitmap.GetPixel(x - 1, y - 1).ToArgb() == nearPointsColorArgb // 左上
&& bitmap.GetPixel(x - 1, y + 1).ToArgb() == pointColorArgb// 左下
&& bitmap.GetPixel(x + 1, y - 1).ToArgb() == nearPointsColorArgb// 右上
&& bitmap.GetPixel(x + 1, y + 1).ToArgb() == pointColorArgb// 右下
)
return true;
break;
}
}
return false;
}
4.初见代码运行效果
4.1运行结果
*4.2 黑白二值化、柔化后的效果图
*4.3 运行效率
因为后面还有其他处理
代码思路就演示到这了
*4.3.1 如果想效率高点的话可以试下
*4.3.2.1 只黑白化、二值化、柔化识别范围内的图像区域
类似这样
*4.3.2.2 配置多线程
5.添加Winform的UI处理的成品
将按需将识别配置放出来调节就行~
5.1.字体亮度
这里的【字体亮度】是要重点关注的配置
例如下图红绿数字是需要不同的【字体亮度】去识别
原理是红色、绿色色系亮度不一样
在【黑白二值化】处理这步会根据图片亮度切分黑白的颜色像素
详见初见代码【76】行
Result = Result < 255 / 5 * 4 ? 0 : 255;// 黑白图片二值化(根据字体亮度调节)
使得程序只看到【黑】和【白】下图是调好后的效果
但对于亮度较低的字体颜色可能会在【二值化】处理时被视为【黑色】
本该视为【白色】字体的【笔画】视为【黑色】因此而丢失
【笔画】确实自然无法识别或识别有误
下图就是用取红色28的【字体亮度】处理绿色30的效果
反之,设置【字体亮度】过高会让过多的【背景】视为【白色】的【笔画】
5.2.柔化、消除散点
可反复执行,执行次数越多
【GetNum】方法越准确,耗时越少
预处理的耗时会变高
所以这个值最好根据照片调节
5.3.运行效果图
可以看到照片上的由于光照灯导致仪器示数牌背景亮度不一
6.样例下载链接
赶时间的可以看这个源码↓