在近几年人群计数领域的研究中,大多数论文都沿用了MCNN( Multi-column Convolutional Neural Network )中提出的生成密度图的方法,本文将通过代码和CSRNet网络中生成密度图的实例来详细讲解该方法。
在进行人群计数的研究时,数据集通常由原始图片和标注文件共同构成,本文使用的数据集是shanghaiTech,该数据集包含了part_A_final,part_B_final两部分,标注文件是mat格式,里面记录了每个注释人头的二维坐标和总人头数量[1]。
由标注文件生成密度图的过程如下:首先构造一个和原始图片大小相同的矩阵,并将其全部置为0,然后将每个被标记的人头对应的位置置为1,这样就得到了一个只有0和1的矩阵,最后通过高斯核函数进行卷积得到一个连续的密度图。
以ShanghaiTech数据集为例,path_sets中保存的就是part_B部分的训练集和测试集
# set the root to the Shanghai dataset you download
root = 'E:/数据集/ShanghaiTech_Crowd_Counting_Dataset/'
# now generate the ShanghaiA's ground truth
part_A_train = os.path.join(root, 'part_A_final/train_data', 'images')
part_A_test = os.path.join(root, 'part_A_final/test_data', 'images')
part_B_train = os.path.join(root, 'part_B_final/train_data', 'images')
part_B_test = os.path.join(root, 'part_B_final/test_data', 'images')
path_sets = [part_B_train, part_B_test] # 将训练集和测试集放在一起
然后依次读取数据集中的每一张图片将其放到列表img_paths中
img_paths = []
for path in path_sets:
for img_path in glob.glob(os.path.join(path, '*.jpg')):
img_paths.append(img_path)
print('图片数量:', len(img_paths))
for img_path in img_paths:
print(img_path)
# 获取每张图片对应的mat标记文件
mat = io.loadmat(img_path.replace('images', 'ground_truth').replace('IMG_', 'GT_IMG_').replace('.jpg', '.mat'))
img = plt.imread(img_path)
# 生成密度图
gt_density_map = np.zeros((img.shape[0], img.shape[1]))
gt = mat["image_info"][0, 0][0, 0][0]
for i in range(0, len(gt)):
if int(gt[i][1]) < img.shape[0] and int(gt[i][0]) < img.shape[1]:
gt_density_map[int(gt[i][1]), int(gt[i][0])] = 1
gt_density_map = gaussian_filter_density(gt_density_map)
# 保存生成的密度图
with h5py.File(img_path.replace('images', 'ground_truth').replace('.jpg', '.h5'), 'w') as hf:
hf['density'] = gt_density_map
#测试
print('总数量=',len(gt))
print('密度图=',gt_density_map.sum())
原始图片和其对应的mat文件在两个不同的文件夹下,文件名和后缀名有一定的区别,所以通过replace()方法将图片的路径img_path转换成mat文件的路径并读取。
生成密度图时首先生成一个和原始图像大小相同的全0矩阵(np.zeros()),然后遍历标注文件中每一个位置坐标,将矩阵中对应的点置为1,最后调用高斯核函数生成密度图并保存成h5py格式的文件。
首先给出公式:
其中Xi为每个人头标注点的位置,具有N个人头的标签可以表示为H(x)。这个H(x)可能理解起来比较抽象,要注意的是这里的x表示的是一个二维坐标,所以H(x)也就是上文中提到的只有0和1的矩阵。
我们可以使用高斯核函数G(x)对这个函数卷积得到密度函数F(x),结合公式一和公式二可以得到公式三。
放上二维高斯核函数的公式:
由这个公式我们可以看到输入的是二维坐标点(x,y),其中参数是σ,作用是控制函数的径向作用范围。公式三中使用的是x来表示这个二维坐标,很多人可能没有理解过来。
综上所述,生成密度图的过程就是将只有0和1的矩阵经过高斯卷积计算即可。
高斯核的函数图像如下所示,它是一个正态分布钟形线,坐标越趋近中心点,值就越大,反之越小。也就是说离中心点越近权值就越大,离中心点越远,权值就越小[2]。
在真实图片中,每个人头是具有一定大小的,对应图片中的一小片区域,而在标注文件中我们将一个像素点的值置为1来表示这个人头,这显然是十分不合理的。所以我们使用高斯核函数将这个中心点的像素值用它周围点的像素值的加权平均代替,周围像素点的权值相加起来等于1;这样既不影响生成的密度图中总人头数,又能够比较真实的反应每个人头在空间里面的位置特征。
另一方面,我们需要考虑到透视畸变对人头大小的影响。简单地说,就是在拍摄照片时,距离镜头越远的物体在照片上显得越小。如下图所示,距离镜头近的人头较大,占用的空间像素点较多,而距离镜头远的人头较小,占用的空间像素点较少。所以在使用高斯核函数时需要根据不同人头大小设置不同的模糊半径,即根据图像中每个人头部大小来确定参数σ。
但是在实际情况下,我们不可能准确的获得每个头部的尺寸大小,而且很难找到头部尺寸和密度图之间的关系。接下来我们继续研究上面的图片,通过观察图片我们可以发现每个头部大小通常与相邻人头中心点的距离有关。举个例子,距离镜头最近的那个女士与其相邻两个人头的距离要明显大于远处那个男士距离其相邻两个人头的距离。
经过分析,在拥挤场景中,我们可以使用相邻k个人头与该人头的平均距离来作为高斯核函数的参数σ,这样就能够更好地表示人头大小的特征信息。
def gaussian_filter_density(gt):
print(gt.shape)
density = np.zeros(gt.shape, dtype=np.float32)
gt_count = np.count_nonzero(gt)
if gt_count == 0:
return density
pts = np.array(list(zip(np.nonzero(gt)[1], np.nonzero(gt)[0])))
#构造KDTree寻找相邻的人头位置
tree = scipy.spatial.KDTree(pts.copy(), leafsize=2048)
distances, locations = tree.query(pts, k=4)
print('generate density...')
for i, pt in enumerate(pts):
pt2d = np.zeros(gt.shape, dtype=np.float32)
pt2d[pt[1],pt[0]] = 1.
if gt_count > 1:
#相邻三个人头的平均距离,其中beta=0.3
sigma = (distances[i][1]+distances[i][2]+distances[i][3])*0.1
else:
sigma = np.average(np.array(gt.shape))/2./2. #case: 1 point
density += scipy.ndimage.filters.gaussian_filter(pt2d, sigma, mode='constant')
print('done.')
return density
这里有几个地方需要解释一下:
1、构造KDTree调用的是scipy包中封装好的函数,其中leafsize表示的是最大叶子数,如果图片中人头数量过多,可以自行修改其数值。
2、tree.query()中参数k=4查询的是与当前结点相邻三个结点的位置信息,因为distances[i][0]表示的是当前结点。
3、在论文中beta=0.3,因为这里计算的是三个点的平均距离,所以除以3然后乘以beta相当于直接乘以0.1。
高斯核函数的参数σ在不同的数据集和方法中有所不同,自适应高斯核由以上方法计算得到,当然也可以将σ设置成一个固定数值进行计算。
在生成密度图时涉及到的参数还有k(相邻k个人头的平均距离),以及系数beta,在进行人群计数时可以根据不同的数据集进行改变和调参。
由于相邻人头的距离太近,可能会导致进行高斯卷积时部分区域重叠,所以由此方法生成的真实密度图经过sum计算之后得到的人头数和标注文件mat中的人头数存在一定误差。而卷积神经网络计算误差MAE、MSE时往往采用真实密度图和预测密度图进行计算。
ICCV2019 Adaptive Density Map Generation for Crowd Counting (ADMG)
在过去的方法中,密度图一旦制作完成就不再去修改;该论文提出了一种在训练过程中微调密度图的方法,这是一个很好的思路,后续也可以进行深入研究。由于编者没有仔细阅读该论文,仅提供论文链接[3]。
如果读者关于以上内容有任何疑问或者建议,欢迎在评论区留言,编者看到了一定会进行回复。
参考文献:
[1] https://www.pianshen.com/article/95011021159/
[2] https://www.cnblogs.com/herenzhiming/articles/5276106.html
[3] http://visal.cs.cityu.edu.hk/static/pubs/conf/iccv19-dmapgen.pdf