使用Phash结合朴素贝叶斯进行目标图像匹配检测

我们通常会有这样的一类图像识别问题:并不是要将图像进行多分类,而是在确定的区域,检测有没有目标图像出现,例如,在手机上一些界面,检测点击按钮是否出现,开屏动画时,检测右上角固定区域是否出现了叉号,这里是个图像匹配问题。

phash 原理

图像匹配问题是个二分类问题,即两者相像或不同。图像相似度方法主要有 指纹比较、ahash、phash、SIFT 等方法,我们这里折中选择了 phash 算法。

具体到我们的使用中,有以下几个步骤:

1、对图像统一缩放到某一大小,如 32 * 32

2、对图像通道降维,转换为灰色或提取某一通道的数据

3、进行 DCT 变换

4、裁剪 DCT 特征 (图像信息主要集中在左上角低频频谱上)

5、计算 DCT 均值

6、遍历特征值,小偶遇均值的记为0,大于均值的记为1,得到图像指纹。

代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_hash(img, resize_to=None, channel_id=1, hash_type='pHash', principle=(12, 6)): # principle stored in order x, y
if resize_to is not None:
img = cv2.resize(img, resize_to)

if channel_id <= 2: # extract single channel
img_single_channel = np.zeros(img.shape[:-1], dtype=np.uint8)
cv2.mixChannels([img], [img_single_channel], [channel_id, 0])
img = img_single_channel

img = img.astype("float32")

if hash_type == 'pHash':
dct = cv2.dct(img)
dct_roi = dct[0:principle[1], 0:principle[0]]
img_list = dct_roi.flatten()
ave = cv2.mean(img_list)[0]
ret = np.array([0.0 if i<ave else 1.0 for i in img_list])

elif hash_type == 'aHash':
img_list = img.flatten()
ave = cv2.mean(img_list)[0]
ret = np.array([0.0 if i<ave else 1.0 for i in img_list])

return ret

朴素贝叶斯原理

得到 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
2
3
4
5
6
7
8
9
10
11
12
def cmpHashDiff(template, target):
t = template.copy()

epsilon = 0.0000001
t[t==1] = 1-epsilon
t[t==0] = epsilon

t_log_positive = np.log(t)
t_log_negative = np.log(1-t)
ret_matrice = (1-target)*t_log_negative + target*t_log_positive
ret = np.sum(ret_matrice)
return ret

1、在实际使用中,如果统计样本本身特征之间 variance 比较大,则可以适当调大阈值,从而在验证时,在特征变化稍大的情况也能正常匹配成功。当然,这 variance 太大时也说明样本本身变化较大或不可靠,这时候就要考虑其他算法了。

2、我们在计算对数时,引入了一个 epsilon,避免当趋于0时算得的值过小,平滑特征的响应。

对应工具的一般使用方法

(Hidden Content)

准备视频文件和对应标注

image-20201026154719346

切分得到 iframe,并整理样本集

使用 split_video_to_frames 或其他脚本切分出截图,然后根据 stage 类别将所需样本分类到不同文件夹内。

image-20201026154847812

手动裁剪得到关心区域的截图,并整理文件组织结构

计算机生成了可选文字: ax: -23.0  xv. -0.664  : 253863  Shift + X  Ta b  Pause  Alt+C  Alt+M  Alt* Enter  Ctrl+W  a 30203  4209.  _uap  11.11ijß%-jHü  a 15588  Size. 0.03  *SCC)

1
2
3
4
5
6
7
8
9
10
11
12
crops_1026_mobius/rois_template$ tree -L 2
.
├── stage_buxipai
│ ├── title_blueiconjingdianpai.jpeg
│ └── title_putong.jpeg
├── stage_jingdianpai
│ ├── title_blueiconbuxipai.jpeg
│ └── title_putong.jpeg
├── stage_moshixuanze
│ └── title_jingdian.jpeg
└── stage_wanfaxuanze
└── title_dianwan.jpeg

image-20201026153743078

使用 phash_helper 在样本集上自动切出目标区域

image-20201026153747113

设置好 iframes 文件夹、目标区域模板文件夹、保存目录等参数,执行代码段中的 get_roi_rect_info_and_crops 方法。 该方法内部会继而调用 find_roi_target 方法,通过 cv2.matchTemplate 找到大图中最匹配的坐标位置及框选大小,随后会遍历 iframes 文件夹,把每张大图都切出相同区域,保存到目标目录。这个时候可以停下来,到具体目录看切下来的图片是否符合预期、是否与给的模板小图一致。

使用 phash_helper 生成目标样本的 phash 数据

执行代码段中的 update_roi_hash 方法,将会进行各个类别样本在其内的统计分布,此时已完成 phash 数据的生成,保存在 roi_infos 数据结构中。

在生成过程中,会打印以下信息:

image-20201026154139041

我们需要关注 variance 的大小,如果样本方差较大,则很可能类别内样本差异较大,需要手动进行数据清洗或者重选ROI区域。

清洗数据优化 ROI 参数

例如下个例子中,该类别的 variance 大于 1,是因为有如下框出来的错位的样本,我们把它手动删掉。

image-20201026154239654

最终 variance 越小越好,先验分布集中,后验时概率较尖锐,这样我们就可以在 inference 时把概率阈值设置的更小,上个例子修改后的 variance 如下:

image-20201026154446133

打印 roi_infos 可以得到所有目标的模板指纹、裁剪缩放等流水线以及类别名,需要根据具体业务再使用相应信息。

增加或修改某个ROI

结合实际场景,我们可能会在终端上直接得到大量已裁剪好的目标区域样本,这时候就不需要 get_roi_rect_info_and_crops 动作了,直接统计计算得到指纹模板即可。

为此,请准备好符合 roi_info 对应格式的对象, 把样本区分类别放置在一起各自文件夹内,文件夹名改为各类别名,然后调用 update_roi_hash 。

相关用法的例子已在原脚本中有体现,多多尝试。