我们通常会有这样的一类图像识别问题:并不是要将图像进行多分类,而是在确定的区域,检测有没有目标图像出现,例如,在手机上一些界面,检测点击按钮是否出现,开屏动画时,检测右上角固定区域是否出现了叉号,这里是个图像匹配问题。
phash 原理
图像匹配问题是个二分类问题,即两者相像或不同。图像相似度方法主要有 指纹比较、ahash、phash、SIFT 等方法,我们这里折中选择了 phash 算法。
具体到我们的使用中,有以下几个步骤:
1、对图像统一缩放到某一大小,如 32 * 32
2、对图像通道降维,转换为灰色或提取某一通道的数据
3、进行 DCT 变换
4、裁剪 DCT 特征 (图像信息主要集中在左上角低频频谱上)
5、计算 DCT 均值
6、遍历特征值,小偶遇均值的记为0,大于均值的记为1,得到图像指纹。
代码参考如下:
1 | def get_hash(img, resize_to=None, channel_id=1, hash_type='pHash', principle=(12, 6)): # principle stored in order x, y |
朴素贝叶斯原理
得到 hash 之后,我们一般可以使用曼哈顿距离进行两张图片相似度的比较。但是实际使用中,要匹配的对象其实可能因为背景变化的关系,指纹也会有细微的变化,如果运气不好选作模板的指纹刚好是概率较低的那个,会造成后续无法正常识别匹配,又需要重新选择模板。
为此,我们需要考虑一种方法,使得指纹模板能够覆盖更多样本的分布,于是想到了用朴素贝叶斯的方法。
有关朴素贝叶斯算法的理解,建议参考这篇文章。
简单来说,就是考虑每个特征在某一事件下出现概率的联合概率越大,则事件发生的概率越大。
所以,给定同一目标$Y_j$的多个样本作为模板,我们将他们作同样的特征处理,特征共有$N$维,记为$X=(x_1, x_2, … x_n)$,记 $x_i=1$ 的统计概率为 $a_ji$,则其伯努利形式概率质量函数为
$$
P(x_i|Y_j)=a_{ji}^{x_i}(1-a_{ji})^{1-x_i} , x_i=0或1
$$
根据朴素贝叶斯的概率公式
$$
P(Y_j|X) = \frac{P(Y_j)}{P(X)}\prod_{i=0}^NP(x_i|Y_j)
$$
,带入后有:
$$
P(Y_j|X) = \frac{logP(Y_j)\prod_{i=0}^Na_{ji}^{x_i}(1-a_{ji})^{1-x_i}}{P(X)}
$$
其中P(X)为常数,可省去,再将两边取对数,得到如下:
$$
log(P(Y_j|X)) = logP(Y_j) + \sum_{i=0}^Nx_ilog(a_{ji}) + \sum_{i=0}^N (1-x_i)log(1-a_{ji})
$$
在训练时,统计${Y_j}$的先验概率$Y_j$以及上每个特征等于1的时候的概率 ${a_{ji}}$, 在验证时,把特征向量以${x_i}$带入上式,比较不同Y下的值,取argmax作为结果。
对于图像匹配问题,同一分布内,$Y_j$包括属于或不属于某一类对照,共两个类别,由于负样本收集标注起来较麻烦,为简化问题,我这里只计算了正样本每个特征的概率,并假定正负类别概率一样,所以我在代码里干脆把先验概率 $logP(Y_j)$ 一项也给省掉了,换用一个经验阈值来判断是否属于正样本类别。(正常做法应该是正负类别分别统计,验证时分别计算概率估计,取大的一方作为结果。)
另外,为避免被取log的值为0得到无穷小,我们还需要进行数值上的平滑。一般采用拉普拉斯平滑,需要在计算每个$a_{ji}$时,分子分母各加一定值,不过我的代码里是在最后 log 前加了一个极小值进行平滑,区别较大,但统计的代码简单了许多。具体算法流程可以参考这篇 sklearn 源码里引用的文章-IR-book-the-bernoulli-model。
代码参考如下:
1 | def cmpHashDiff(template, target): |
1、在实际使用中,如果统计样本本身特征之间 variance 比较大,则可以适当调大阈值,从而在验证时,在特征变化稍大的情况也能正常匹配成功。当然,这 variance 太大时也说明样本本身变化较大或不可靠,这时候就要考虑其他算法了。
2、我们在计算对数时,引入了一个 epsilon,避免当趋于0时算得的值过小,平滑特征的响应。
对应工具的一般使用方法
(Hidden Content)
准备视频文件和对应标注
切分得到 iframe,并整理样本集
使用 split_video_to_frames 或其他脚本切分出截图,然后根据 stage 类别将所需样本分类到不同文件夹内。
手动裁剪得到关心区域的截图,并整理文件组织结构
1 | crops_1026_mobius/rois_template$ tree -L 2 |
使用 phash_helper 在样本集上自动切出目标区域
设置好 iframes 文件夹、目标区域模板文件夹、保存目录等参数,执行代码段中的 get_roi_rect_info_and_crops 方法。 该方法内部会继而调用 find_roi_target 方法,通过 cv2.matchTemplate 找到大图中最匹配的坐标位置及框选大小,随后会遍历 iframes 文件夹,把每张大图都切出相同区域,保存到目标目录。这个时候可以停下来,到具体目录看切下来的图片是否符合预期、是否与给的模板小图一致。
使用 phash_helper 生成目标样本的 phash 数据
执行代码段中的 update_roi_hash 方法,将会进行各个类别样本在其内的统计分布,此时已完成 phash 数据的生成,保存在 roi_infos 数据结构中。
在生成过程中,会打印以下信息:
我们需要关注 variance 的大小,如果样本方差较大,则很可能类别内样本差异较大,需要手动进行数据清洗或者重选ROI区域。
清洗数据优化 ROI 参数
例如下个例子中,该类别的 variance 大于 1,是因为有如下框出来的错位的样本,我们把它手动删掉。
最终 variance 越小越好,先验分布集中,后验时概率较尖锐,这样我们就可以在 inference 时把概率阈值设置的更小,上个例子修改后的 variance 如下:
打印 roi_infos 可以得到所有目标的模板指纹、裁剪缩放等流水线以及类别名,需要根据具体业务再使用相应信息。
增加或修改某个ROI
结合实际场景,我们可能会在终端上直接得到大量已裁剪好的目标区域样本,这时候就不需要 get_roi_rect_info_and_crops 动作了,直接统计计算得到指纹模板即可。
为此,请准备好符合 roi_info 对应格式的对象, 把样本区分类别放置在一起各自文件夹内,文件夹名改为各类别名,然后调用 update_roi_hash 。
相关用法的例子已在原脚本中有体现,多多尝试。