以下链接是个人关于DG-Net(行人重识别ReID)所有见解,如有错误欢迎大家指出,我会第一时间纠正。有兴趣的朋友可以加微信:17575010159 相互讨论技术。若是帮助到了你什么,一定要记得点赞!因为这是对我最大的鼓励。文末附带公众号 −海量资源。

行人重识别0-06:DG-GAN(ReID)-目录-史上最新最全:https://blog.csdn.net/weixin_43013761/article/details/102364512

极度推荐的商业级项目: 这是本人落地的行为分析项目,主要包含(1.行人检测,2.行人追踪,3.行为识别三大模块):行为分析(商用级别)00-目录-史上最新无死角讲解

训练测试数据来源

在上篇博客我们存在疑问:

    train_loader_a, train_loader_b, test_loader_a, test_loader_b = get_all_data_loaders(config)

这里四个类别的数据分别来自哪里,有什么作用,先看来源:

def get_all_data_loaders(conf):
    batch_size = conf['batch_size']
    num_workers = conf['num_workers']
    # 重新定义a,b图片的大小
    if 'new_size' in conf:
        new_size_a= conf['new_size']
        new_size_b = conf['new_size']
    else:
        new_size_a = conf['new_size_a']
        new_size_b = conf['new_size_b']
    # 截切图片的高和宽
    height = conf['crop_image_height']
    width = conf['crop_image_width']

    if 'data_root' in conf:
        # 训练图片a来自于pytorch/train_all
        train_loader_a = get_data_loader_folder(os.path.join(conf['data_root'], 'train_all'), batch_size, True,
                                              new_size_a, height, width, num_workers, True)
        # 测试图片a来自于pytorch/query
        test_loader_a = get_data_loader_folder(os.path.join(conf['data_root'], 'query'), batch_size, False,
                                             new_size_a, height, width, num_workers, False)

        # 训练图片b来自于pytorch/train_all
        train_loader_b = get_data_loader_folder(os.path.join(conf['data_root'], 'train_all'), batch_size, True,
                                              new_size_b, height, width, num_workers, True)
        # 测试图片b来自于pytorch/query
        test_loader_b = get_data_loader_folder(os.path.join(conf['data_root'], 'query'), batch_size, False,
                                             new_size_b, height, width, num_workers, False)
    else:
        train_loader_a = get_data_loader_list(conf['data_folder_train_a'], conf['data_list_train_a'], batch_size, True,
                                                new_size_a, height, width, num_workers, True)
        test_loader_a = get_data_loader_list(conf['data_folder_test_a'], conf['data_list_test_a'], batch_size, False,
                                                new_size_a, height, width, num_workers, False)
        train_loader_b = get_data_loader_list(conf['data_folder_train_b'], conf['data_list_train_b'], batch_size, True,
                                                new_size_b, height, width, num_workers, True)
        test_loader_b = get_data_loader_list(conf['data_folder_test_b'], conf['data_list_test_b'], batch_size, False,
                                                new_size_b, height, width, num_workers, False)
    return train_loader_a, train_loader_b, test_loader_a, test_loader_b

pytorch目录显示如下:

这些文件都是通过前面我们执行prepare-market.py生成的,
gallery:包含了官方数据集的所有行人,并且已经按ID分类好了,其中有个-1的文件夹,表示识别不出来的行人。gallery分成(复制)了multi-query与train_all两个文件夹。
query:该文件夹中的图片,都是从multi-query中随机挑选而来,每个ID选取了五张照片。
train_all :包含了所有训练的照片,不含有测试照片,该文件被分成验证集train和训练集val。
val :从train_all中每个ID抽取一张照片
train:train_all 删除掉val的照片都作为train(训练集)

上面是个人分析,但是代码中很明显,应该是把train_all当作训练集,query当作是测试集,具体不是很了解,但是从这里我们可以得到的一个结论是,训练的时候,每次需要a,b两张图片,并且来自同一train_all数据集。大概就是为了让这两张图片换衣服吧。测试图片a,b也是来自同一query数据集。既然知道了这个,我们,我去看网络把,其实我也很好奇,他对a,b图片是怎么处理的。

在看代码之前,还要为大家打个预防针,这个地方把我坑得不容易(现在心脏都在隐隐作痛),在传递的时候,一个ID下面,不是传递一张图片,是两张(也就是一起四张),为什么是两张,我们看下面函数在train.py文件中:

        # 循环获得训练数据,a,b
        for it, ((images_a,labels_a, pos_a),  (images_b, labels_b, pos_b)) in enumerate(zip(train_loader_a, train_loader_b)):
            if num_gpu>1:
                trainer.module.update_learning_rate()
            else:
                trainer.update_learning_rate()

            # images_a[batch_size,3,256,128],images_b[batch_size,3,256,128]
            images_a, images_b = images_a.cuda().detach(), images_b.cuda().detach()

            # 这个地方注意,后面讲解,反正是图片
            # pos_a[batch_size,3,256,128],pos_b[batch_size,3,1024]
            pos_a, pos_b = pos_a.cuda().detach(), pos_b.cuda().detach()

            # labels_a[batch_size],labels_b[atch_size]
            labels_a, labels_b = labels_a.cuda().detach(), labels_b.cuda().detach()

这里的images_a与pos_a同一ID不一样的图片,images_b 与pos_b来自同一ID不一样的图片。labels_a, labels_b分别表示ID类别。

网络框架略朗

首先我们回到train.py文件,找到如下代码:

    # Setup model and data loader
    # 加载,训练数据,进行模型构建和加载
    if opts.trainer == 'DGNet':
        trainer = DGNet_Trainer(config, gpu_ids)
        trainer.cuda()

这里他就把网络框架搭建好了,其中怎么搭建的,我们先不去分析,因为我发现,有的东西需要从细节开始,但是有的东西需要从整体去看待,掌握的全局思路之后再去分析细节,是可以事半功倍的。假设他搭建好了,就要进行前向传播(还是train.py文件):

    with Timer("Elapsed time in update: %f"):
        # Main training code
        # 进行前向传播
        x_ab, x_ba, s_a, s_b, f_a, f_b, p_a, p_b, pp_a, pp_b, x_a_recon, x_b_recon, x_a_recon_p, x_b_recon_p = \
                                                                             trainer.forward(images_a, images_b, pos_a, pos_b)

执行这段代码之后,就进行了前向传播,得到一堆看起来不知道是什么东西的东西,不急,慢慢分析。在pyTrotch中,网络结构习惯用类来搭建,并且继承nn.Module,如trainer.py文件:

class DGNet_Trainer(nn.Module):
	......

其中都会存在一个forward函数,是的,就是trainer.forward这里调用了这个函数。现在我们就来分析这个函数。贴图,贴图,不解释,不结合图,真的不好理解:

下面是函数代码注释

    # 这里是重点,前向传播的时候,该函数会被调用,这个就是论文的总体思路了
    # 大家注意,先打开论文的Figure 2,
    # x_a,x_b, xp_a, xp_b[4, 3, 256, 128]
    def forward(self, x_a, x_b, xp_a, xp_b):
        # 送入x_a,x_b两张图片(来自训练集的不同ID)
        # 通过st 编码器,编码成两个stcode:
        # s_a[batch,128,64,32]
        # s_b[batch,128,64,32]
        # single会根据参数设定判断是否转化为灰度图
        s_a = self.gen_a.encode(self.single(x_a))
        s_b = self.gen_b.encode(self.single(x_b))

        # 先把图片进行下采样,图示我们可以看到ap code的体积比st code是要小的,这样会出现一个情况,那么他们是没有办法直接融合的,所以后面有个全链接成把他们统一
        # f_a[batch_size,2024*4=8192],     p_a[0]=[batch_size, class_num=751], p_a[1]=[batch_size, class_num=751]
        # f_b[batch_size,2024*4=8192],     p_b[0]=[batch_size, class_num=751], p_b[1]=[batch_size, class_num=751]
        # f代表的是经过ap编码器得到的ap code,
        # p表示对身份的预测(有两个身份预测,也就是p_a了两个元素,这里不好解释),
        # 前面提到过,ap编码器,不仅负责编码,还要负责身份的预测(行人重识别),也是我们落实项目的关键所在
        # 这里是第一个重难点,在论文的翻译中提到过,后续详细讲解
        f_a, p_a = self.id_a(scale2(x_a))
        f_b, p_b = self.id_b(scale2(x_b))


        # 进行解码操作,就是Figure 2中的黄色梯形G操作,这里的x_a,与x_b进行衣服互换
        # s_b[batch,128,64,32] f_a[batch_size,2024*4=8192] -->  x_ba[batch_size,3,256,128]
        x_ba = self.gen_a.decode(s_b, f_a)
        x_ab = self.gen_b.decode(s_a, f_b)


        # 进行重构,可以这样理解,x_a穿上自己的衣服,当然还是自己(图片基本不变)
        # x_b也是同理
        x_a_recon = self.gen_a.decode(s_a, f_a)
        x_b_recon = self.gen_b.decode(s_b, f_b)


        # 这里是同一个人,换了一个姿态,p应该表示pos,把姿态经过外貌编码器得到ap code
        fp_a, pp_a = self.id_a(scale2(xp_a))
        fp_b, pp_b = self.id_b(scale2(xp_b))


        # 一个人,穿上另外一个姿态的衣服(衣服相同的),得到初始姿态的图片
        x_a_recon_p = self.gen_a.decode(s_a, fp_a)
        x_b_recon_p = self.gen_b.decode(s_b, fp_b)

        # Random Erasing only effect the ID and PID loss.
        # 把图片擦除一些像素,然后进行ap code编码
        if self.erasing_p > 0:
            # 先把每张图片都擦除一些像素
            x_a_re = self.to_re(scale2(x_a.clone()))
            x_b_re = self.to_re(scale2(x_b.clone()))
            xp_a_re = self.to_re(scale2(xp_a.clone()))
            xp_b_re = self.to_re(scale2(xp_b.clone()))

            # 然后经过编码成ap code,暂时不知道作用,感觉应该是数据增强
            # 类似于,擦除了图片的一些像素,但是已经能够识别出来这些图片是谁
            _, p_a = self.id_a(x_a_re)
            _, p_b = self.id_b(x_b_re)
            # encode the same ID different photo
            _, pp_a = self.id_a(xp_a_re) 
            _, pp_b = self.id_b(xp_b_re)


        # 混合合成图片:x_ab[images_a的st,images_b的ap]    混合合成图片x_ba[images_b的st,images_a的ap]
        # s_a[输入图片images_a经过Es编码得到的 st code]     s_b[输入图片images_b经过Es编码得到的 st code]
         # f_a[输入图片images_a经过Ea编码得到的 ap code]     f_b[输入图片images_b经过Ea编码得到的 ap code]
        # p_a[输入图片images_a经过Ea编码进行身份ID的预测]    p_b[输入图片images_b经过Ea编码进行身份ID的预测]
        # pp_a[输入图片pos_a经过Ea编码进行身份ID的预测]      pp_b[输入图片pos_b经过Ea编码进行身份ID的预测]
        # x_a_recon[输入图片images_a(s_a)与自己(f_a)合成的图片,当然和images_a长得一样]
        # x_b_recon[输入图片images_b(s_b)与自己(f_b)合成的图片,当然和images_b长得一样]
        # x_a_recon_p[输入图片images_a(s_a)与图片pos_a(fp_a)合成的图片,当然和images_a长得一样]
        # x_b_recon_p[输入图片images_a(s_a)与图片pos_b(fp_b)合成的图片,当然和images_b长得一样]
        return x_ab, x_ba, s_a, s_b, f_a, f_b, p_a, p_b, pp_a, pp_b, x_a_recon, x_b_recon, x_a_recon_p, x_b_recon_p
        

注释,都是比较简单,当然也是很形象的,首先我们看到forward函数,需要是个参数 x_a, x_b, xp_a, xp_b。这里的四个参数,就是我们前面说的,分别两个ID的两张图片。在上面的图中,我用蓝色的框,框出了3张照片,那么和这四张照片怎么对应起来呢?可以这样理解,对应关系如下:

x_a = x j

x_b = x i

xp_b = x t

那么出现了一个问题,多余出来的xp_a呢?他怎么办?这里我们思考一个一个问题,假设只传送了3张图片:

把上面的网络跑了一遍,当跑下一遍的时候,我们又传递了3张照片将来。也就是说,跑两次网络,消耗了6张图片。现在我们传递4张将来,先拿x_a,x_b,xp_b跑一遍,在拿x_b,x_a,xp_a跑一遍。这样我们4张照片就跑了2次,节约了2张照片。这里就是简单使用了(输入)角色互换的道理。那么我说他是这样的?就要给出证据(大家这里注意一下:xp表示是x t )。首先代码:

        # 送入x_a,x_b两张图片(来自训练集的不同ID)
        # 通过st 编码器,编码成两个stcode:
        # s_a[batch,128,64,32]
        # s_b[batch,128,64,32]
        s_a = self.gen_a.encode(self.single(x_a))
        s_b = self.gen_b.encode(self.single(x_b))

对应图中的

这里分别对输入的x_a,x_b进行编码,得到对应st code,即代码中s_a,s_b 。红框表示输入,粉框代表输入(后面一样就不重复了)

下面是代码:

        # 先把图片进行下采样,图示我们可以看到ap code的体积比st code是要小的,这样会出现一个情况,那么他们是没有办法直接融合的,所以后面可能有个全链接成把他们统一
        # f_a[batch_size,2024*4=8192],     p_a[0]=[batch_size, class_num=751], p_a[1]=[batch_size, class_num=751]
        # f_b[batch_size,2024*4=8192],     p_b[0]=[batch_size, class_num=751], p_b[1]=[batch_size, class_num=751]
        # f代表的是经过ap编码器得到的ap code, p表示对身份的预测
        f_a, p_a = self.id_a(scale2(x_a))
        f_b, p_b = self.id_b(scale2(x_b))

首先把x_a,x_b两张图片输入,经过Ea(外观编码器),分别得到对应的ap code,即代码中的f_a,f_b。对于p_a,p_b 我后面讲解,从源码猜测,好像是两个ID类别的概率,如下:

即属于的概率(暂时是猜测)

下面是代码:

        # 进行解码操作,就是Figure 2中的黄色梯形G操作,这里的x_a,与x_b进行衣服互换
        # s_b[batch,128,64,32] f_a[batch_size,2024*4=8192] -->  x_ba[batch_size,3,256,128]
        x_ba = self.gen_a.decode(s_b, f_a)
        x_ab = self.gen_b.decode(s_a, f_b)

可以看到,是对应两个输入,但是只有一个输出,这就是结构和衣服融合的过程。self.gen_a.decode表示的是图中的G。

下面是代码:

        # 进行重构,可以这样理解,x_a穿上自己的衣服,当然还是自己(图片基本不变)
        # x_b也是同理
        x_a_recon = self.gen_a.decode(s_a, f_a)
        x_b_recon = self.gen_b.decode(s_b, f_b)

可以看到,依旧是两个输入,得到一个合成图片,但是这里是同一个人。他的思想,就是一个人脱了衣服,然后又穿生衣服,这个人还是原来的人,或者说还是同一张图片。

下面是代码:

        # 这里是同一个人,换了一个姿态,p应该表示pos,把姿态经过外貌编码器得到ap code
        fp_a, pp_a = self.id_a(scale2(xp_a))
        fp_b, pp_b = self.id_b(scale2(xp_b))

前面提到,带p的输入图像,表示的是姿态,或者说和另外一张图片不一样的姿态。p就是pose嘛。

下面是:

        # 一个人,穿上另外一个姿态的衣服(衣服相同的),得到初始姿态的图片
        x_a_recon_p = self.gen_a.decode(s_a, fp_a)
        x_b_recon_p = self.gen_b.decode(s_b, fp_b)

这样,我们网络的每个环节都对应起来了。大家要注意recon表示重构建的意思,重点在重不在构,重复构建出相同的照片,就是重构,表示他们没有换衣服,还是自己的衣服。

最开始的时候,我从细节处开始分析,把自己坑太惨了,好些地方想不明白,主要原因还是第一次接触PyTroch框架,很多东西都比较陌生。还有就是,大家不要奇怪,为什么返回那么多的东西,其实都是为了计算损失,后续详细讲解。

就到这里了,下面我们就开始讲解细节了,如编码器,解码器,鉴别器的详细内容了。记得点赞啊,我相信下篇博客还会见面了,毕竟这叫缘分嘛!