Press "Enter" to skip to content

高效的王者荣耀视频英雄检测与识别

Last updated on 2019年5月18日

为了能够对自动提取王者荣耀视频标签,我们需要对王者荣耀游戏视频中的英雄进行检测与识别,判断该视频中我方英雄以及友方和敌方,这就需要首先在视频中检测出英雄的位置和数量,然后对每个检测到的英雄,判断英雄的类别(我方/友方/敌方)并识别出英雄的姓名。笔者在实验过程中,发现了一种two-stage算法能够较好地解决这一问题,采用基于血条模版匹配+后处理的方法实现英雄的检测,第二阶段采用深度神经网络对检测到的英雄进行识别。

 

一、算法调研

 

针对通用的图像或视频中的目标检测与识别任务,目前主流的算法中有两大类:一是检测与识别作为两个问题来解决,第一阶段先在图像中检测特定的目标,第二阶段将检测到的目标送到训练好的CNN分类器里,识别出检测到的各个英雄;二是直接使用类似于SSD或者YOLO这样的一步目标检测与识别算法,直接在图像上运行一遍算法即可检测并识别出所有出现的英雄。

 

考虑到王者荣耀这款具体游戏,每个英雄头顶上都会有一个固定形状的血条。由于血条的外观轮廓每个英雄都相同,仅仅是血条的颜色、血量、冷却时间上存在差异,因此本文采用二阶段法,第一阶段通过模板匹配在图像中寻找所有英雄血条的位置,并根据血条位置抠取出对应的英雄图像;第二阶段训练一个深度CNN网络对抠取出的英雄图像进行分类识别。之所以选用二阶段的算法,是考虑到英雄血条的轮廓比较规整,使用模板匹配算法会有较高的准确率,因此重心只需放在训练图像分类网络,简化算法的复杂性。

 

二、素材收集

 

我们计划使用基于CNN分类器的方法进行英雄识别,因此需要收集大量的视频并从中提取英雄的图片用于训练。经过寻找,发现企鹅电竞网站的素材库里有一个高光时刻栏目,里面有大量网友上传的王者荣耀精彩时刻视频,且已经按英雄名称进行了分类,因此可以直接下载使用。需要注意的是这个栏目里的视频有些是网友加了自己的后期处理,比如在游戏视频里叠加了一些宣传文字或主播头像之类。我们尽量排除这种情况,筛选出取原始的、干净的游戏视频,使得算法不受这些人为因素的干扰。由于使用模板匹配检测血条的方法在图像中检测英雄,所以必须保证所有输入图像中血条的长宽比保持一致。经研究发现,部分视频长宽比并非标准的16:9,如果直接将该视频调整为16:9的长宽比将导致血条形状失真而无法进行模板匹配,而保持视频原有的长宽比则不会改变血条的长宽比。因此,对于非标准比例的视频画面,我们仅将其高度调整为与血条模板对应的高度(720像素),而保留长宽比不变。

 

三、王者荣耀游戏视频中的英雄检测

 

1. 血条模版匹配

 

我们通过检测英雄血条的位置来定位英雄,由于血条中的血量、颜色、冷却时间等内容会不断变化,因此我们要设置掩码,只对所有血条都有的相同部分进行模板匹配,而忽略不同的部分。具体来说,我们选取如下图所示的血条及模板图片

 

 

 

使用OpenCV自带的模板匹配函数matchTemplate,可以方便地完成血条的模板匹配,由于matchTemplate函数仅支持单通道图像模板匹配,因此需将所有三通道彩色图像转为单通道灰度图像。此外,由于读入的视频可能不是标准分辨率(我们设定最常见的1280*720分辨率为标准分辨率),而血条和掩码都是按照标准分辨率制作的。同时为了尽量减小计算量,我们选择缩放模版及掩码而不选择缩放图像,这是因为模版及掩码像素很少,缩放一次计算量小,而且只需在处理前缩放一次;缩放图像需要对每帧视频都进行缩放,计算量较大。OpenCV缩放模版及掩码图像的代码如下,其中scale_ratio为根据输入视频高度与标准高度之比计算出的缩放因子。

 

img_template = cv2.resize(img_template, None, fx = scale_ratio, fy = scale_ratio, interpolation = cv2.INTER_LINEAR)img_mask = cv2.resize(img_mask, None, fx = scale_ratio, fy = scale_ratio, interpolation = cv2.INTER_LINEAR)

 

我们假设原图像为img_gray,血条模板图像为img_template,血条掩码图像为img_mask,OpenCV模板匹配函数如下:

 

img_result = cv2.matchTemplate(img_gray, img_template, cv2.TM_CCORR_NORMED, mask=img_mask)

 

匹配后的img_result图像是一个单通道32位浮点图像,各像素值代表模板在该点与原图像的匹配程度,值越大表示匹配度越高。由于我们要检测图像中的所有英雄的血条,而英雄数量是未知的,所以我们不能取固定的匹配度阈值。实践也证明了固定匹配度的阈值效果也不好,容易产生漏检和误检的现象。为了解决这一问题,我们首先观察原图像和血条模版匹配后生成的匹配值图像,如图所示:

 

 

 

不难发现,对于原图像中每个英雄血条的位置,在匹配图中都会表现为一定范围内的局部极大值点,即中间亮周围暗的模式,如匹配图中的红框所示,而在匹配图的其他区域则这种局部极大值效应不明显。于是我们可以检测匹配图中的局部极大值来滤除虚假响应,使用scipy函数库的maximum_filter函数完成此功能。其中filter_size为滤波器核半径,与图像大小成比例,对于标准分辨率(1280*720)输入图像,取值为10左右效果较好。该函数输出仍然是一幅图像,输出图像各点的像素值代表该点周围filter_size邻域内的极大值点的像素值。

 

img_max = ndimage.maximum_filter(img, size = filter_size * 2)

 

使用该函数求得的图像局部极大值点图像如下图所示:

 

 

可以看出,在匹配图上对应红框位置的四个点,局部极大值滤波器有较大的响应,意味着在匹配图中这些位置确实存在局部极大值。通过逐个像素比较局部极大值滤波前后的图像,即可找出图像的局部极大值点。在匹配图中查找局部极大值的代码如下,其中img和img_max分别为匹配图及maximum_filter之后的图像。

 

maximum_idx = np.argwhere(img == img_max)

 

该函数输出一个所有找到的极大值的列表。一般情况下该函数会输出大约几百个局部极大值,我们需要对这些值进行进一步的处理。我们可以计算每个极大值点与邻域内其他点的亮度差异的绝对值,并在邻域内求平均,并与极大值点本身的亮度值加权求和,从而得到每个极大值点的score。实践中我们发现,计算数百个极大值点的score值比较耗时。由于图像中出现的英雄数量不会多于10个,没有必要对几百个极大值点都计算score值。于是我们在计算score操作之前,首先按照各极大值点的亮度值对所有的极大值点进行排序,取前20个点计算score,可以大大降低计算量且基本不会漏掉真实的血条。

 

我们维护一个极大值点的列表maximum_value_list,其元素为一个三元组(x, y, value),分别代表极大值点的x、y坐标及极大值,按value值降序排序代码如下:

 

maximum_value_list.sort(key = operator.itemgetter(2), reverse = True)

 

在选取前20个极大值点之后,我们对每个点分别计算score值,并将这20个点按score值从高到低排序(后续非极大抑制用)。由于无法预知图像中会有多少个英雄,仍然需要使用一个阈值来判断。在这个阶段使用阈值的效果已经远远好于在模版匹配图上使用阈值的效果。只要选取一个合适的固定阈值,就可以实现对视频中的各帧以及各个不同的视频均有很好的检测效果。检测效果如图所示:

 

 

可见画面中的英雄血条均已正确检测出来。

 

2. 非极大抑制

 

由于用于匹配的模版和掩码均包含明显的水平长线条(血槽的上下边缘),导致在模版匹配时,模版在真实血条附近左右滑动一些像素时,并不会显着减小匹配的响应值。因此,有时候模版匹配会在水平条带上产生多个邻近的检测结果,如图所示:

 

 

为了应付这种情况,我们引入了非极大抑制算法,对于基本处在同一水平位置,且相互之间邻近的血条检测结果,我们只取其中具有最大score值的检测结果,而删除其他的血条检测结果,经过非极大抑制后的血条检测结果如图:

 

 

可见,通过非极大抑制,保留了正确的血条检测结果而去掉了邻近的虚假检测结果。

 

3. 判断英雄类别

 

通过判断血条的颜色(绿色/蓝色/红色),可以将英雄先分为三类(我方、友方和敌方)。这部分算法比较简单,仅仅是根据血条检测结果定位到血量的最左边一格的颜色(考虑到英雄的血量可能会很低)。具体规则如下:当某个颜色通道的亮度值较大,且明显大于另外两个通道时,则直接给出对应的英雄类别;如果三个通道亮度值接近,且均在(70,100)区间内,则认为是空血条;如果以上两种情况都不是,则认为是血条检测错误,丢弃该血条检测结果,可以进一步提高血条检测的准确率。下图为游戏中切换道具的界面,该界面不应当检测出有英雄的存在。但我们可以看到,在血条颜色检测之前,会有一些误检,如红框所示:

 

 

在血条颜色检测之后,由于对应位置的颜色与任何一种血条颜色(包括空血)都不符,因此误检被滤除,如图所示:

 

 

四、王者荣耀游戏中的英雄识别

 

我们可以利用上述的英雄检测算法自动帮助提取用于训练和验证英雄分类器的样本。由于下载的视频文件仅标注了主角英雄。因此我们在提取训练和验证样本阶段,仅将检测范围限定在图像中间的一个小范围内(因为主角基本固定出现在画面中间位置)。使用上述算法对每个英雄的多段视频进行检测后,我们可以得到大量带标注的英雄图片。将提取出的每个英雄图片分别存放在各自名称的文件夹下面,文件名可以任意。下图显示了某个英雄文件夹下的样本集图片:

 

 

完成样本收集后,我们采用基于Tensorflow且开源的TF-Slim库完成训练任务。采用TF-Slim库的优点是可以直接使用自带的多种主流CNN模型(包括AlexNet/VGG/Inception/Resnet等),不需要或仅仅编写少量代码即可完成深度CNN网络的样本集生成、训练与验证。

 

1. 训练样本集生成

 

首先将前述英雄检测算法中提取的我方英雄图片按照英雄姓名放在各自的文件夹里,每个英雄约有1000-2000张图片。目前我们收集了12个英雄,共17931张英雄图片。修改TF-Slim库的相关代码,在其自带的三个数据集基础上,增加一个我们的英雄图片数据集pvp_heros。我们随机选择其中的16000张图片作为训练集,剩下1931张图片作为验证集。

 

使用修改后的download_and_convert_data.py将这些图片转换为TF Record格式的文件,命令行如下:

 

$ DATA_DIR=/tmp/data/pvp_heros$ python download_and_convert_data.py \    --dataset_name=pvp_heros \    --dataset_dir="${DATA_DIR}"

 

程序运行结束后将在DATA_DIR目录下分别生成一系列的以.tfrecord为扩展名的训练集和相应的验证集(具体每个数据集生成几个.tfrecord文件可以在修改相应代码中的_NUM_SHARDS常量设定),并生成一个labels.txt文件,该文件将类别名称与数字建立一一对应关系,类似这样:

 

0:周瑜1:墨子2:妲己3:嬴政4:安琪拉5:小乔6:扁鹊7:武则天8:甄姬9:芈月10:貂蝉11:高渐离……

 

2. 训练CNN模型

 

我们选用流行且准确率非常高的Inception-ResNet-v2模型。使用train_image_classifer.py来训练该模型的命令行如下:

 

DATASET_DIR=/tmp/data/pvp_herosTRAIN_DIR=/tmp/checkpointspython train_image_classifier.py \    --train_dir=${TRAIN_DIR} \    --dataset_name=pvp_heros \    --dataset_split_name=train \    --dataset_dir=${DATASET_DIR} \    --model_name=inception_resnet_v2

 

DATASET_DIR为存放.tfrecord格式的训练样本所在的目录,TRAIN_DIR为保存训练后的网络权值的目录。训练完成后,在TRAIN_DIR目录下将会生成一系列checkpoint文件,即为训练后的网络权值。

 

3. 验证模型

 

假设训练后的最新checkpoint文件名为inception_resnet_v2.ckpt,我们使用如下代码来验证刚训练好的CNN模型,命令行如下:

 

$ python eval_image_classifier.py \    --alsologtostderr \    --checkpoint_path=${TRAIN_DIR}/inception_resnet_v2.ckpt \    --dataset_dir=${DATASET_DIR} \     --dataset_name=pvp_heros \     --dataset_split_name=validation \     --model_name=inception_resnet_v2

 

其中TRAIN_DIR和DATASET_DIR含义如上所述。在验证集上我们取得了Top-1准确率78%,Top-5准确率95%的成绩。

 

4. 导出模型与权值

 

CNN网络训练完成后,为了能在英雄检测程序中在线识别英雄,我们需要将模型图和权值导出,首先是导出模型的结构图:

 

$ python export_inference_graph.py \  --alsologtostderr \  --model_name=inception_resnet_v2 \  --output_file=${TRAIN_DIR}/inception_resnet_v2_inf_graph.pb \  --dataset_name=pvp_heros

 

然后我们需要冻结模型图,即将模型的结构图与权值冻结在一起,为此我们需要首先编译一个tensorflow自带的freeze_graph工具,然后用freeze_graph工具来实现模型图的冻结操作:

 

bazel build tensorflow/python/tools:freeze_graph
bazel-bin/tensorflow/python/tools/freeze_graph \ --input_graph=${TRAIN_DIR}/inception_resnet_v2_inf_graph.pb \  --input_checkpoint=${TRAIN_DIR}/inception_v3.ckpt \  --input_binary=true \  --output_graph=${TRAIN_DIR}/frozen_inception_resnet_v2.pb \  --output_node_names=InceptionResnetV2/Logits/Predictions

 

其中输出层的名字可以使用tensorflow自带的summarize_graph工具查看:

 

bazel build tensorflow/tools/graph_transforms:summarize_graph
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph \  --in_graph=${TRAIN_DIR}/inception_resnet_v2_inf_graph.pb

 

5. 在图像上测试CNN模型

 

得到最终冻结后的模型图,我们就可以在输入图像上测试我们训练后的inception-resnet-v2模型了,假设输入图像已经读入image_data中,输出其英雄类别的关键代码如下:

 

with tf.Session() as sess:    softmax_tensor = sess.graph.get_tensor_by_name('InceptionResnetV2/Logits/Predictions:0')    predictions = sess.run(softmax_tensor, {'input:0': image_data})    predictions = np.squeeze(predictions)
    # Creates node ID --> English string lookup.    node_lookup = NodeLookup(FLAGS.label_path)
    top_k = predictions.argsort()[-FLAGS.num_top_predictions:][::-1]    for node_id in top_k:      human_string = node_lookup.id_to_string(node_id)      score = predictions[node_id]      print('%s (score = %.5f)' % (human_string, score))

 

这段代码将输出英雄的名称(与labels.txt中的类别对应)及对应的分类器输出score。我们在视频中逐帧运行这段代码,即可得到我方英雄的名称,如图所示:

 

 

图像中显示的c和s分别为英雄的类别和分类器输出score,通过查询labels.txt,得知类别6对应的名称是扁鹊,和实际我方英雄名称相符,英雄识别正确。

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注