MCNN: 多列卷積神經網絡的單圖像人群計數


MCNN網絡

論文PDF
作者源碼,使用matlab處理數據集,torch實現網絡。
MCNN是上海科技大學在CVPR 2016上的一篇論文,使用3列卷積網絡進行人群密度估計。

摘要

本文旨在提出一種弄可以從具有任意人群密度和角度的的單張圖像准確估計人群數量的方法。為了實現這個目的,我們提出一種簡單但是很有效的多列卷積神經網絡(MCNN),將圖像映射到它的人群密度圖。MCNN允許輸入圖像是任意大小和分辨率。通過利用不同大小的濾波器(核)有不同大小的感受野,每一列的CNN學習到的特征由於透視效果或者圖像分辨率而適應對人頭大小的變化(這句大概就這意思)。此外,真實密度圖的准確計算依賴於幾何自適應核(geometry-adaptive kernels ),它不需要知道輸入圖像的透視圖(???)。由於現有的人群計數數據集不足以涵蓋我們工作中考慮的所有具有挑戰性的情況,因此我們收集並標記了一個大型的新數據集,其中包括1198幅圖像,其中包含約330,000個頭。在這個具有挑戰性的新數據集以及所有現有數據集上,我們進行了廣泛的實驗,以驗證所提出的模型和方法的有效性。特別是,通過提出的簡單MCNN模型,我們的方法優於所有現有方法。此外,實驗表明,一旦在一個數據集上進行訓練,我們的模型就可以輕松轉移到新的數據集上。

論文解讀

該論文是在CVPR 2012的《 Multi-column deep neural networks for image classification》基礎上提出的。MCNN的輸出是人群密度圖,它的積分值就是整張圖像的人數估計,網絡包含三列CNN,每一列的核大小等不同。

論文的三個貢獻點:1. 使用多列結構,對應於大中小三種不同感受野大小的核,每列的特征可以適應人/頭的較大差異變化。2. 使用1x1的卷積替換全連接層,因此,輸入圖像可以是任意大小以避免失真。 3. 收集圖像制作了一個新的人群計數數據集。

通過CNN進行圖像中的人數估計有兩種思路:1. 輸入圖像輸出估計的人頭數目。2.輸出人群密度圖,如每平米多少人,然后通過積分計算人數。MCNN使用的是第二種。

由於需要對CNN進行訓練以從輸入圖像中估計人群密度圖,因此訓練數據中給出的密度質量在很大程度上決定了方法的性能。我們首先描述如何將帶有標簽人頭的圖像轉換為人群密度圖。

在像素點\(x_i\)處如果有人頭,我們將其表示為函數\(\delta(x-x_i)\),即在\(x=x_i\)處為1,其他地方全為0的函數。那么,一張圖有\(N\)個人頭的話,可以描述為函數\(H(x) = \sum_{i=1}^N \delta(x-x_i)\),為了將它轉為一個連續密度函數,使用高斯核[1] \(G_{\sigma}\),密度可以描述為\(F(x)=H(x)*G_{\sigma}(x)\),但是這種密度函數假設了\(x_i\)是圖像平面的獨立樣本,而實際上每個\(x_i\)都是場景中人去密度的樣本(意思應該是,密度函數的假設是人頭只占了一個像素點,是單獨的獨立樣本,但是實際上,圖像上每個點都是密度圖的樣本,由於透視畸變,與不同像本關聯的像素點在場景中對應於不同大小的區域)。

In fact, each $x_i $ is a sample of the crowd density on the ground in the 3D scene and due to the perspective distortion, and the pixels associated with different samples \(x_i\) correspond to areas of different sizes in the scene.

我們假設每個人的頭部周圍比較均勻,頭部和它在圖像中的\(k\) 個鄰域的平均距離給出幾何變形的合理估計,因此根據圖像中每個人的頭部大小確定傳播參數\(\sigma\)。我們發現人頭大小通常與擁擠場景中兩個相鄰人的中心之間的距離有關,作為一種折中,對於那些擁擠場景的密度圖,我們建議根據每個人與鄰域的平均距離來自適應地確定每個人的傳播參數

對於圖像中給定的每個頭部\(x_i\),我們定義該像素點到它的\(k\)個最近鄰域的距離為\(\{d^i_1,d^i_2,..,d^i_m\}\),那么平均距離則為\({\overline {d^i}}= \frac 1 m \sum_{j=1}^m d^i_j\),最終定義的密度函數為

\[F(x) = \sum\limits_{i=1}^N\delta(x-x_i)*G_{\sigma_i}(x) ,\ \ with \ \ \ \sigma_i = \beta \overline {d^i} \]

我們將標簽H與密度核進行卷積,密度核適應每個數據點周圍的局部幾何體,稱為幾何適應核。實驗發現系數\(\beta=0.3\)表現最好。

通過圖片和標注的頭部坐標生成密度圖的Python測試代碼如下,根據原作者提供的matlab版本代碼修改而來。

import  scipy.io as io   # 讀取mat文件的坐標
import cv2  # 讀取圖像和在高斯核計算時使用
import numpy as np
from matplotlib import pyplot as plt  # 顯示
def fspecial(ksize_x=5, ksize_y = 5, sigma=4):
    # 返回大小為(ksize,ksize)的二維高斯濾波器核矩陣
    # 完全等價於matlab中的fspecial('Gaussian',[ksize, ksize],sigma);
    kx = cv2.getGaussianKernel(ksize_x, sigma)
    ky = cv2.getGaussianKernel(ksize_y, sigma)
    return np.multiply(kx,np.transpose(ky))

pixes = io.loadmat('../GT_IMG_1.mat')  # 讀取標簽,格式是dict
counts = pixes['image_info'][0][0][0][0][1][0][0]  # 標簽總人數
xy = pixes['image_info'][0][0][0][0][0]  # (counts,2)的數組
img = cv2.imread('../IMG_1.jpg')  # 原圖
h, w  = img.shape[0:2] 
labels = np.zeros(shape=(h,w)) # 密度圖 初始化全0

for loc in xy:  # 遍歷每個頭部坐標
    f_size = 15  # 核大小,也是要考慮的鄰域大小
    sgma = 4.0
    H = fspecial(f_size, f_size , sigma)  # 返回一個(15,15)的高斯核矩陣 
    x = min(abs(int(loc[0])),int(w))  # 頭部坐標
    y = min(abs(int(loc[1])),int(h))  # 防止越界
    
    # 鄰域的對角 考慮選擇 (x1,y1) 到 (x2,y2) 區域
    x1 = x - f_sz/2 ; y1 = y - f_sz/2
    x2 = x + f_sz/2 ; y2 = y + f_sz/2
    
    dfx1 = 0; dfy1 = 0; dfx2 = 0; dfy2 = 0  # 偏移
    change_H = False
    
    if x1 < 0 :
        # 左上角在圖像外了
        dfx1 = abs(x1) # 偏移量就是它的絕對值
        x1 = 0 # 左上角直接置零,源碼中是1,因為matlab矩陣索引從0開始
        change_H = True
     if y1 < 0:
        dfy1 = abs(y1)
        y1 = 0
        change_H = True
    if x2 > w:
        dfx2 = x2 - w
        x2 =w-1  # 右下角超出,那么直接是最后一行/列,索引值是h-1和w-1
        change_H =True
    if y2 > h:
        dfy2 = y2 -h
        y2 = h-1
        change_H =True
    x1h = 1+dfx1
    y1h = 1 + dfy1  
    x2h = f_size - dfx2 # x2h-x1h +1 = f_size - dfx2 - dfx1  其中dfx2+dfx1 
    y2h = f_size - dfy2 # y2h-y1h +1 =  f_size - dfy2 -1 - dfy1 +1 = f_size - dfy2 - dfy1
    if change_H:
        H = fspecial(int(y2h-y1h+1), int(x2h-x1h+1),sigma)
    labels[int(y1):int(y2), int(x1):int(x2)] = labels[int(y1):int(y2), int(x1):int(x2)] + H

plt.imshow(labels)

生成的ground_truth密度圖如下。

MCNN的結構

MCNN受多列DNN的啟發,用三列並行的感受野大小不一的卷積網絡構成,所有的列都使用了類似的結構(conv-pool-conv-pool),只是在數量和大小上有所區別。較大尺寸的卷積核采用較少的數量,堆疊所有的輸出,然后將其映射到密度圖,映射的方法采用了1x1卷積,然后使用歐式距離衡量預測密度圖和gt的差異。損失函數定義為

\[L(\Theta) = \frac 1 {2N} \sum_{i=1}^N ||F(X_i;\Theta) -F_i || ^2_2 \]

其中\(\Theta\)是要學習的參數,\(N\)是圖像數目,\(X_i\)是輸入圖像,\(F_i\)是真實的密度圖,\(F(X_i;\Theta)\)表示預估的\(X_i\)的密度圖,

注意 : 由於采用了兩次池化,每次都是2x2,因此每張圖像的空間分辨率后悔降低\(\frac 1 4\),因此,在訓練階段,生成密度圖之前,我們要對每個樣本圖像進行1/4的降采樣,另外與MDNN不同的是,在最后堆疊每一列網絡的輸出時,MDNN采用的是直接平均,而MCNN使用的是1x1卷積。

另外,受RBM的啟發,我們直接將第四列卷積的輸出映射到特征圖,分別進行訓練,然后使用這些預訓練的CNN參數初始化整個MCNN,並進行微調。

torch實現

數據准備

上海科大數據集有兩個part,A的人群比較密集,B則比較稀疏,標簽格式是mat文件。對mat的處理在之前已經說明,通過它生成每張圖的gt密度圖。論文提供的matlab代碼,將每張圖生成密度圖后,保存為csv文件,然后在訓練時讀取csv文件。這里我們直接使用密度圖,不保存。

另外,論文中提到數據增強時,將圖像中裁剪9個小塊,每個小塊是原圖像的 1/4 大小。源碼中也使用了這個,從圖上摳出9個小塊。這里我沒有考慮,直接將整張圖送入訓練的。

import cv2
import scipy.io as sio
from torch.utils.data import DataLoader,Dataset
from torchvision import transforms
import os
import numpy as np
import torch
unloader = transforms.ToPILImage()
class myDatasets(Dataset):
    def __init__(self,img_path, ann_path, down_sample=False):
        # 圖像路徑文件夾 和 標簽文件 文件夾 采用絕對路徑
        self.pre_img_path = img_path  # 文件夾路徑
        self.pre_ann_path = ann_path
        # 圖像的文件名是 IMG_15.jpg 則 標簽是 GT_IMG_15.mat
        # 因此不需要listdir標簽路徑
        self.img_names = os.listdir(img_path) 
        self.down = down_sample
        

    def __getitem__(self, index):
        # 該函數用於返回每一個sample
        img_name = self.img_names[index]  # index對應的圖片名
        mat_name = 'GT_' + img_name.replace('jpg','mat')

        img = cv2.imread(self.pre_img_path + img_name,0)
        anno = sio.loadmat(self.pre_ann_path + mat_name)
        xy = anno['image_info'][0][0][0][0][0]  # N,2的坐標數組
        density_map = self.get_density(img, xy) # 密度圖

        img = img.astype(np.float32)
        density_map = density_map.astype(np.float32)
        h = img.shape[0]
        w = img.shape[1]  

        ht1 = h //4 * 4
        wd1 = w //4 * 4
        img = cv2.resize(img, (wd1, ht1))


        if self.down :
            wd1 = wd1 //4
            ht1 = ht1 //4
            density_map = cv2.resize(density_map, (wd1, ht1))
            density_map = density_map * ( (w*h) /( wd1 * ht1) ) # 放大16倍多
        else:
            density_map = cv2.resize(density_map, (wd1, ht1))
            density_map = density_map * ( (w*h) /( wd1 * ht1) )

        img = torch.from_numpy(img.reshape(1,img.shape[0], img.shape[1]))
        density_map = torch.from_numpy(density_map.reshape(1,density_map.shape[0],density_map.shape[1]))
        return img, density_map


    def __len__(self):
        return len(self.img_names)

    def get_density(self,img, points):
        h, w  = img.shape[0], img.shape[1]
        # 密度圖 初始化全0
        labels = np.zeros(shape=(h,w))
        for loc in points:
            f_sz = 17  # 濾波器尺寸 預設為15 也是鄰域的尺寸
            sigma = 4.0  # sigma參數
            H = fspecial(f_sz, f_sz , sigma)  # 高斯核矩陣
            x = min(max(0,abs(int(loc[0]))),int(w))  # 頭部坐標
            y = min(max(0,abs(int(loc[1]))),int(h))
            if x > w or y > h:
                continue
            x1 = x - f_sz/2 ; y1 = y - f_sz/2
            x2 = x + f_sz/2 ; y2 = y + f_sz/2
            dfx1 = 0; dfy1 = 0; dfx2 = 0; dfy2 = 0

            change_H = False
            if x1 < 0:
                dfx1 = abs(x1);x1 = 0 ;change_H = True
            if y1 < 0:
                dfy1 = abs(y1); y1 = 0 ; change_H = True
            if x2 > w:
                dfx2 = x2-w ; x2 =w-1 ; change_H =True
            if y2 > h:
                dfy2 = y2 -h ; y2 = h-1 ; change_H =True
            x1h =  1 + dfx1 ; y1h =  1 + dfy1
            x2h = f_sz - dfx2 ; y2h = f_sz - dfy2
            if change_H :
                H = fspecial(int(y2h-y1h+1), int(x2h-x1h+1),sigma)
            labels[int(y1):int(y2), int(x1):int(x2)] = labels[int(y1):int(y2), int(x1):int(x2)] + H
        return labels

    def fspecial(self,ksize_x=5, ksize_y = 5, sigma=4):
        kx = cv2.getGaussianKernel(ksize_x, sigma)
        ky = cv2.getGaussianKernel(ksize_y, sigma)
        return np.multiply(kx,np.transpose(ky))
 
path1 = 'D://Datasets//part_B_final//train_data//images//'
path2 = 'D://Datasets//part_B_final//train_data//ground_truth//'
datasets = myDatasets(path1, path2)
train_loader = DataLoader(datasets, batch_size=1) 
'''
x1 = None ; y1 = None
for x, y in train_loader:
	x1 = x 
	y1 = y 
	break
unloader(x1.squeeze(0).squeeze(0))
unloader(y1.squeeze(0).squeeze(0))  # 可以簡單測試查看數據
'''

模型搭建

MCNN一共有三個分支,每個分支都由卷積-池化-激活的小模塊構成。最后的輸出通道合並,通過1x1卷積映射到密度圖。

import torch
import torch.nn as nn
class Conv2d(nn.Module):
    # 定義了卷積 - 池化 -激活 的小模塊
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, relu=True, same_padding=False, bn=False):
        super(Conv2d, self).__init__()
        padding = int((kernel_size - 1) / 2) if same_padding else 0
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding=padding)
        self.bn = nn.BatchNorm2d(out_channels, eps=0.001, momentum=0, affine=True) if bn else None
        self.relu = nn.ReLU(inplace=True) if relu else None

    def forward(self, x):
        x = self.conv(x)
        if self.bn is not None:
            x = self.bn(x)
        if self.relu is not None:
            x = self.relu(x)
        return x

    
class MCNN(nn.Module):
    # MCNN模塊
    def __init__(self, bn=False):
        super(MCNN, self).__init__()
        # 分支1 輸出有8個通道
        self.branch1 = nn.Sequential(Conv2d( 1, 16, 9, same_padding=True, bn=bn), 
                                     nn.MaxPool2d(2),
                                     Conv2d(16, 32, 7, same_padding=True, bn=bn),
                                     nn.MaxPool2d(2),
                                     Conv2d(32, 16, 7, same_padding=True, bn=bn),
                                     Conv2d(16,  8, 7, same_padding=True, bn=bn))
        
        
        self.branch2 = nn.Sequential(Conv2d( 1, 20, 7, same_padding=True, bn=bn),
                                     nn.MaxPool2d(2),
                                     Conv2d(20, 40, 5, same_padding=True, bn=bn),
                                     nn.MaxPool2d(2),
                                     Conv2d(40, 20, 5, same_padding=True, bn=bn),
                                     Conv2d(20, 10, 5, same_padding=True, bn=bn))
        
        self.branch3 = nn.Sequential(Conv2d( 1, 24, 5, same_padding=True, bn=bn),
                                     nn.MaxPool2d(2),
                                     Conv2d(24, 48, 3, same_padding=True, bn=bn),
                                     nn.MaxPool2d(2),
                                     Conv2d(48, 24, 3, same_padding=True, bn=bn),
                                     Conv2d(24, 12, 3, same_padding=True, bn=bn))
        
        self.fuse = nn.Sequential(Conv2d( 30, 1, 1, same_padding=True, bn=bn))
        
    def forward(self, im_data):
        x1 = self.branch1(im_data)
        x2 = self.branch2(im_data)
        x3 = self.branch3(im_data)
        x = torch.cat((x1,x2,x3),1)  # 三個分支的輸出合並
        x = self.fuse(x)  # 1x1 卷積 映射到 密度圖
        
        return x

訓練

訓練部分可以參考作者源碼,修改數據的讀取部分即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM