今天介绍一种简单的识别思路,因为总是有些很简单的识别对初学者来说也是比较困难的事情,当然我并不是什么高手,只是写一点想法。最近在玩某些在线FLASH游戏的时候发现一个比较适合简单的数字识别的示例:字体固定,无旋转,绘制位置相对固定(有偏移),内部填充色在一定范围内变化但边缘均接近黑色。于是想到了灰度化后会存在明显的阶梯,从这里下手!经测试识别率。。。。。高达100%。。。看来思路还是正确的。下面就简单介绍一下:
注明出处,省的有些垃圾干不是人的事!
一、基本思路:
首先,我们要图片——用Graphics类的共享方法CopyFromScreen即可,而后对其灰度化,灰度化时我们把R,G,B归一为一个数,所以灰度化的结果是一个二维数组,我采用了整型,实际上用字节型能更节约空间。
然后,我们建立相应的模板,由于只涉及到0-9的10个数字,所以创建10个模板即可。这里需要考虑的问题就是灰度结果的比较——既包括方向也包括大小。
最后,我们根据阀值输出所识别到得数字即可!因为我们涉及的模板精确度还是很高的,所以么有进行字符分割等操作。
二、代码实现:
恰当的灰度化函数可以提高我们的效率和识别率,这里我选择了著名的心理学算法的一个形式:
Public Shared Function Graying(ByVal bmp As Bitmap) As Integer(,)
Dim ret(bmp.Width - 1, bmp.Height - 1) As Integer
Dim clr As Color
Dim r, g, b As Integer
Dim gray As Integer
For x As Integer = 0 To bmp.Width - 1
For y As Integer = 0 To bmp.Height - 1
clr = bmp.GetPixel(x, y)
r = clr.R
g = clr.G
b = clr.B
gray = (r * 30 + g * 59 + b * 11 + 50) / 100
ret(x, y) = IIf(gray > 255, 255, gray)
Next
Next
Return ret
End Function
这样我们就得到了灰度化结果。
灰度化之后,我们将创建模板——作为标准的数据,刚刚已经提到了,我们要包含方向、大小。例如,在被FLASH显示时,某一个关键像素A(位于字符内部或外部边缘的点)的灰度化结果为110,而它左面的像素B灰度要低一些,我们就可以构建一个C<B<A的模板以识别A像素:A像素的灰度值大于左边的像素——大于和左边要同时体现在模板当中。可能你想到要用一个结构或者简单的类,但我推荐一种做法,其效率要高,占用空间要小(听起来貌似违背想提高效率就占用更多内存的一般规律)——我们把方向和比较符存放在同一个数字中,这减少了存储空间并减少了寻址操作(用逻辑运算代替,而逻辑运算仅占用一个CPU运算周期)。于是下面的结构便出现在代码当中:
注明出处,省的有些垃圾干不是人的事!
Public Enum bijiao
xiaoyu = 1
dayu = 2
dengyu = 4
End Enum
Public Enum fangxiang
shang = 8
xia = 16
zuo = 32
you = 64
End Enum
在使用时,我们需要将两者有机结合起来:用OR运算符,例如上面的A灰度大于左边的B灰度,可表示为A = bijiao.dayu or fangxiang.zuo。当然,我这里只是一个粗陋的模板:你在更复杂的图像下应该添加约等于及另外4个方向(左上、左下、右上、右下)在你的代码中,使他们像下面这样:
Public Enum bijiao
xiaoyu = 1
dayu = 2
dengyu = 4
yuedengyu = 8
End Enum
Public Enum fangxiang
shang = 16
xia = 32
zuo = 64
you = 128
zuoshang =256
……
End Enum
但事实证明在我所测试的游戏中,我的模板识别率高达100%了,完全可以胜任,你也需要类似的测试,因为更精确的模板在解码时就需要更多的运行于可怕的多层嵌套循环中的代码,每增加一些精度运算速度都会下降很多——找到一个恰当的既能满足精确度要求又能高效的平衡点将是我们都需要面对的问题。虽然这样说,但我的代码还没有进行其他方面的优化,例如这就是一块很大的肥肉:当检测到某个模板的匹配度不可能达到阀值时,我的代码并没有转向的执行下一个模板检测,而是傻傻的继续计算当前模板,并最终返回(指仅在函数记录)了一个没有什么利用价值的符合度。好了,不继续讨论更细节的优化问题了,只要你更动脑,那么你的程序就可以更高效!下面实现一下解码过程:
这个函数被用来解释编码中的大于小于和等于(当然这个等于被我解释成了“约等于”或“等于”实际上在我的代码中根本没有用到它,只是示范一下如何实现约等于的解码)并返回解码结果。
Private Shared Function Com(ByVal numyx As Integer, ByVal tabyx As Integer, ByVal num2 As Integer, Optional ByVal ddengyu As Integer = 0) As Boolean
If (numyx And bijiao.dayu) = bijiao.dayu Then
Return tabyx > num2
ElseIf (numyx And bijiao.xiaoyu) = bijiao.xiaoyu Then
Return tabyx < num2
ElseIf (numyx And bijiao.dengyu) = bijiao.dengyu Then
Return Math.Abs(tabyx - num2) <= ddengyu
End If
Return False
End Function
类似的,你完全可以写出一个方向解析器。但值得注意的是,方向解析器当中你不能用IF ELSE或SELECT CASE语句,而只能用一组IF语句来解析——因为方向的逻辑关系是与而不是或,所以代码看起来应该向这样:
If (numyx And fangxiang.shang) = fangxiang.shang Then
End If
If (numyx And fangxiang.xia) = fangxiang.xia Then
End If
If (numyx And fangxiang.zuo) = fangxiang.zuo Then
End If
If (numyx And fangxiang.you) = fangxiang.you Then
End If
确实,你还需要把方向解析器与比较解析器结合起来——将比较解析函数的调用放到方向解析的每一个IF END IF之间即可。
好了,下面我们考虑一下我们需要返回哪些信息,其实很简单:所匹配的模板,产生匹配的坐标,匹配度!代码就不贴了,放在后面有一个完整的类。
接下来,我们要设计一个模板类,来完成我们整个的准备工作:
我们需要这个类提供以下信息:
模板,除此之外呢?我们还需要知道某个模板进行了多少次检测用以确定匹配度时的分母,还有模板的大小,当然这不是必要的,但可以把一个取二维数组某一维个数的操作简化为一个小范围寻址,我觉得是值得的。
下面给出一个实例:
注明出处,省的有些垃圾干不是人的事!
Public Class WindDirectionData
Private Shared ForRight(,) As Integer = New Integer(20, 8) { _
{0, 42, 10, 10, 10, 10, 10, 10, 10}, _
{0, 34, 0, 0, 0, 0, 0, 0, 0}, _
{0, 18, 18, 18, 18, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 50, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 50, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 50, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 50}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 42}, _
{0, 0, 0, 0, 0, 0, 0, 0, 34}, _
{0, 0, 0, 0, 0, 0, 0, 42, 0}, _
{0, 0, 0, 0, 0, 0, 42, 0, 0}, _
{0, 0, 0, 0, 0, 42, 0, 0, 0}, _
{0, 0, 0, 42, 10, 0, 0, 0, 0}, _
{0, 42, 10, 0, 0, 0, 0, 0, 0}, _
{0, 50, 18, 18, 18, 18, 18, 18, 18}}
Public Shared ForLeft(,) As Integer = New Integer(20, 8) { _
{10, 10, 10, 10, 10, 10, 10, 10, 74}, _
{0, 0, 0, 0, 0, 0, 0, 0, 66}, _
{0, 0, 0, 0, 0, 18, 18, 18, 82}, _
{0, 0, 0, 0, 82, 0, 0, 0, 0}, _
{0, 0, 0, 82, 0, 0, 0, 0, 0}, _
{0, 0, 82, 0, 0, 0, 0, 0, 0}, _
{0, 82, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 0, 0}, _
{0, 66, 0, 0, 0, 0, 0, 0, 0}, _
{0, 74, 0, 0, 0, 0, 0, 0, 0}, _
{0, 0, 74, 0, 0, 0, 0, 0, 0}, _
{0, 0, 0, 74, 0, 0, 0, 0, 0}, _
{0, 0, 0, 0, 74, 0, 0, 0, 0}, _
{0, 0, 0, 0, 0, 10, 74, 0, 0}, _
{0, 0, 0, 0, 0, 0, 0, 10, 74}, _
{18, 18, 18, 18, 18, 18, 18, 18, 82}}
'模板数组
Public Shared NumMB()(,) As Integer = New Integer()(,) {ForRight, ForLeft}
'模板检测次数
Public Shared NumCount() As Integer = New Integer() {46, 49}
'模板宽度高度
Public Shared NumSize As Size = New Size(9, 21)
End Class
这个类用于检测游戏中的一个标记朝向,和识别数字的代码一样,她可以达到100%的准确。
注明出处,省的有些垃圾干不是人的事!
好了,我把这个类的全部代码——包含了一个共享的灰度化函数贴到下面:
Public Class TemplateMatch
''' <summary>
''' 大小关系标记枚举
''' </summary>
''' <remarks></remarks>
Public Enum bijiao
xiaoyu = 1
dayu = 2
dengyu = 4
End Enum
''' <summary>
''' 方向关系标记枚举
''' </summary>
''' <remarks></remarks>
Public Enum fangxiang
shang = 8
xia = 16
zuo = 32
you = 64
End Enum
''' <summary>
''' 检测结果结构
''' </summary>
''' <remarks></remarks>
Public Structure NumInfo
''' <summary>
''' 检测结果出现的X坐标(表坐标系)
''' </summary>
''' <remarks></remarks>
Public x As Integer
''' <summary>
''' 检测结果出现的Y坐标(表坐标系)
''' </summary>
''' <remarks></remarks>
Public y As Integer
''' <summary>
''' 检测结果所符合模板数组中元素的位置——Index(从0开始)
''' </summary>
''' <remarks></remarks>
Public num As Integer
''' <summary>
''' 检测结果与对应模板的符合度
''' </summary>
''' <remarks></remarks>
Public Matching As Single
''' <summary>
''' 重载ToString以按格式输出全部信息
''' </summary>
''' <returns>转化后的信息</returns>
''' <remarks></remarks>
Public Overrides Function ToString() As String
Return "x: " & x & " y: " & y & " num: " & num & " matching: " & Matching
End Function
End Structure
''' <summary>
''' 灰度化——将一幅图片转化为灰度结果的二维数组注明出处,省的有些垃圾干不是人的事!http://www.cnblogs.com/zcsor/
''' </summary>
''' <param name="bmp">图片(必须为支持GetPixel函数的图片格式)</param>
''' <returns>灰度化的结果(二维数组)</returns>
''' <remarks>应用的是灰度化方法中的视觉心理学灰度化公式</remarks>
Public Shared Function Graying(ByVal bmp As Bitmap) As Integer(,)
Dim ret(bmp.Width - 1, bmp.Height - 1) As Integer
Dim clr As Color
Dim r, g, b As Integer
Dim gray As Integer
For x As Integer = 0 To bmp.Width - 1
For y As Integer = 0 To bmp.Height - 1
clr = bmp.GetPixel(x, y)
r = clr.R
g = clr.G
b = clr.B
gray = (r * 30 + g * 59 + b * 11 + 50) / 100
ret(x, y) = IIf(gray > 255, 255, gray)
Next
Next
Return ret
End Function
''' <summary>
''' 检测某个坐标与某个模板的匹配性
''' </summary>
''' <param name="table">图像灰度结果表格</param>
''' <param name="tx">测试位置X</param>
''' <param name="ty">测试位置Y</param>
''' <param name="num">某个模板</param>
''' <returns></returns>
''' <remarks></remarks>
Private Shared Function Tab(ByVal table(,) As Integer, ByVal tx As Integer, ByVal ty As Integer, ByVal num(,) As Integer) As Integer
'与目标模板的匹配检测数个数
Dim ppcount As Integer = 0
Dim numyx As Integer = 0
Dim numx As Integer = num.GetLength(1) - 1
Dim numy As Integer = num.GetLength(0) - 1
Dim x As Integer = 0, y As Integer = 0
For x = 0 To numx
For y = 0 To numy
numyx = num(y, x)
If numyx > 0 Then
Dim px As Integer = x + tx
Dim py As Integer = y + ty
If (numyx And fangxiang.shang) = fangxiang.shang Then '方向参数解释器,很简单自己看吧。
ppcount += IIf(Com(numyx, table(px, py), table(px, py - 1)), 1, 0)
End If
If (numyx And fangxiang.xia) = fangxiang.xia Then
ppcount += IIf(Com(numyx, table(px, py), table(px, py + 1)), 1, 0)
End If
If (numyx And fangxiang.zuo) = fangxiang.zuo Then
ppcount += IIf(Com(numyx, table(px, py), table(px - 1, py)), 1, 0)
End If
If (numyx And fangxiang.you) = fangxiang.you Then
ppcount += IIf(Com(numyx, table(px, py), table(px + 1, py)), 1, 0)
End If
End If
Next
Next
Return ppcount
End Function
''' <summary>
''' 比较参数解释器,很简单自己看吧。
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Private Shared Function Com(ByVal numyx As Integer, ByVal tabyx As Integer, ByVal num2 As Integer, Optional ByVal ddengyu As Integer = 0) As Boolean
If (numyx And bijiao.dayu) = bijiao.dayu Then
Return tabyx > num2
ElseIf (numyx And bijiao.xiaoyu) = bijiao.xiaoyu Then
Return tabyx < num2
ElseIf (numyx And bijiao.dengyu) = bijiao.dengyu Then
Return Math.Abs(tabyx - num2) <= ddengyu
End If
Return False
End Function
''' <summary>
''' 获取表中与制定模板数组各个元素匹配信息
''' </summary>
''' <param name="Table">灰度化图像</param>
''' <param name="NumMB">模板数组</param>
''' <param name="NumCount">模板中进行检测的次数(不等于点数,因为有复合)</param>
''' <param name="NumSize">该组模板大小</param>
''' <param name="Threshold">阀值</param>
''' <param name="ExitCount">检测结果数组最大下标达到几退出检测(默认-1表示不限制)</param>
''' <returns>匹配结果信息</returns>
''' <remarks></remarks>
Public Shared Function GetNum(ByVal Table(,) As Integer, ByVal NumMB()(,) As Integer, ByVal NumCount() As Integer, ByVal NumSize As Size, Optional ByVal Threshold As Single = 0.95, Optional ByVal ExitCount As Integer = -1) As NumInfo()
Dim ret() As NumInfo = Nothing
Dim index As Integer = 0
Dim MBLength As Integer = NumMB.Length - 1
For tx As Integer = 1 To Table.GetLength(0) - NumSize.Width - 2
For ty As Integer = 1 To Table.GetLength(1) - NumSize.Height - 2
Dim info As NumInfo = New NumInfo
For i As Integer = 0 To MBLength '检测某一点上全部模板的匹配度,取最大匹配
Dim a = Tab(Table, tx, ty, NumMB(i))
Dim b As Single = a / NumCount(i)
If b > info.Matching Then
info.x = tx
info.y = ty
info.num = i
info.Matching = b
End If
Next
If info.Matching > Threshold Then '最大匹配大于阀值则添加到返回值
ReDim Preserve ret(index)
ret(index) = info
index += 1
If index = ExitCount Then Return ret
End If
Next
Next
Return ret
End Function
End Class
这样,我们就完成了模板解析类的设计。参数意义我已经用XML标记的很清楚了,其实我真的不想你直接用这个类,而是理解思路并改进我的代码,在你自己重写的过程中能够得到一些启迪并给我留言告诉我我的代码有哪些拙劣之处需要改进。
注明出处,省的有些垃圾干不是人的事!
在使用时,我们仅需如此:
注明出处,省的有些垃圾干不是人的事!
Dim Mapbmp As Bitmap = New Bitmap(123, 45)
Dim Mapbmpgr As Graphics = Graphics.FromImage(Mapbmp)
Dim MapbmpRect = New Rectangle(0,0,123,45)
Mapbmpgr.CopyFromScreen(MapbmpRect.Location, Point.Empty, MapbmpRect.Size)
Dim table(,) As Integer = TemplateMatch.Graying(Mapbmp)
Dim NumInfos() As TemplateMatch.NumInfo = TemplateMatch.GetNum(table, MiniMapData.NumMB, MiniMapData.NumCount, MiniMapData.NumSize, 0.97F)
用重载的ToString方法依次打印NumInfos的元素——它们就是按X,Y分别递增顺序所识别到的全部内容。
在文章的最后,我想给你提出一个问题:如果使用我的代码来完成一组数字的识别,那么模板设计时有哪些技巧可以保证模板的互斥性(不相互混淆,例如3,8这两个数字)?
好了,到这里,就到这里。如果心情好过几天贴一个抛物运动弹道模拟代码,模拟某游戏的弹道可是达到101%的准哦,注意是弹道模拟,所以任意距离,高差均可,而不是某些垃圾经验数据的查询式的。不过不贴的可能性还是大,别说我逗你玩然后骂我的话恐怕你的动机不纯我不吃这套,想看代码还是可以的不过要在尊重我的劳动的基础上。
注明出处,省的有些垃圾干不是人的事!