搞定目标检测(SSD篇)(上)
Figure 1
目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的位置和大小。因为各类物体有不同的形状、大小和数量,加上物体间还会相互遮挡, 因而目标检测一直都是机器视觉领域中最具挑战性的难题之一。
如上图所示,目标检测就是用一个矩形来定位一个物体并判断该物体是什么?现阶段,主流算法中体现最好的是SSD和YOLO,前者就是本文要用到的算法。实际上,不论是用SSD还是YOLO,目标检测过程都可以分解为两个独立的操作:
- 定位(location): 用一个矩形(bounding box)来框定物体,bounding box一般由4个整数组成,分别表示矩形左上角和右下角的x和y坐标,或者矩形的左上角坐标以及矩形的长和高。
- 分类(classification): 识别bounding box中的(最大的)物体。
理解了目标检测的基本原理后,接下来,我会带你在Pascal VOC2012数据集上完成单目标检测和多目标检测的挑战。查看完整源码请访问Github: alexshuang/pascal-voc-pytorch。
Pascal VOC2012 / Notebook
你可以在Pascal的官网上找到我们要使用的数据集。Visual Object Classes Challenge 2012 (VOC2012),是Pascal在2012年举办的机器视觉大赛,现在则作为经典的机器视觉数据集为大家所用,它涵盖了Classification/Detection(目标检测)、Segmentation(图像分割)、Action Classification(行为分类)等领域。
!cd {PATH} && tar xf VOCtrainval_11-May-2012.tar!find {PATH} -type ddata/pascaldata/pascal/VOCdevkitdata/pascal/VOCdevkit/VOC2012data/pascal/VOCdevkit/VOC2012/SegmentationObjectdata/pascal/VOCdevkit/VOC2012/Annotationsdata/pascal/VOCdevkit/VOC2012/JPEGImagesdata/pascal/VOCdevkit/VOC2012/SegmentationClassdata/pascal/VOCdevkit/VOC2012/ImageSetsdata/pascal/VOCdevkit/VOC2012/ImageSets/Segmentationdata/pascal/VOCdevkit/VOC2012/ImageSets/Maindata/pascal/VOCdevkit/VOC2012/ImageSets/Actiondata/pascal/VOCdevkit/VOC2012/ImageSets/Layout
目标检测只要要JPEGImages和Annotations这两个目录中的数据。JPEGImages是图像文件目录,而Annotations目录中记录的是图像样本的标签信息,即目标(物体)的分类信息、bounding box以及物体对应的图像文件编号等。
Pascal官方提供的标签信息是.xml格式的,为了便于读取数据,有人已经将xml转成csv格式,它可以在这里下载。
!cd {PATH} && wget -q https://storage.googleapis.com/coco-dataset/external/PASCAL_VOC.zip!cd {PATH} && unzip -q PASCAL_VOC.ziptrn_json = json.load((METADATA_PATH/'pascal_train2012.json').open())val_json = json.load((METADATA_PATH/'pascal_val2012.json').open())trn_json.keys(), val_json.keys()(dict_keys(['images', 'type', 'annotations', 'categories']), dict_keys(['images', 'type', 'annotations', 'categories']))
可以看到,trn_json和val_json的结构相同,所以我们只要要分析其中一个就可,我选取了val_json进行EDA。
EDA
print("images:")display(val_json['images'][i])print("\nannotations:")display(val_json['annotations'][i])print("\ncategories:")display(val_json['categories'][i])print("\ntype:")display(val_json['type'][i])images:{'file_name': '2008_000002.jpg', 'height': 375, 'id': 2008000002, 'width': 500}annotations:{'area': 117445, 'bbox': [33, 10, 415, 283], 'category_id': 20, 'id': 1, 'ignore': 0, 'image_id': 2008000002, 'iscrowd': 0, 'segmentation': [[33, 10, 33, 293, 448, 293, 448, 10]]}categories:{'id': 1, 'name': 'aeroplane', 'supercategory': 'none'}type:'i'
- images:
‘file_name’: 图像文件名
‘id’: 图像文件id- annotations:
‘bbox’: bounding box,4个数从左到右分别是:矩形左上角的x、y坐标,width,height
‘category_id’: 目标(物体)的分类id
‘image_id’: 目标(物体)所属的图像文件id
‘ignore’: 能否忽略该目标(物体)- categories:
‘id’: 分类id,总共20个分类
‘name’: 分类名
以上就是我们要用到的字段,通过’image_id’和’category_id’即可以把图像和它所有的物体关联起来。
i2clas = {o['id']:o['name'] for o in val_json['categories']}i2fn = {o['id']:o['file_name'] for o in val_json['images']}fnames = [o['file_name'] for o in val_json['images']]annos = collections.defaultdict(lambda: [])for o in val_json['annotations']: if o['ignore'] == 0: annos[i2fn[o['image_id']]].append((o['bbox'], o['category_id']))
annos就是我们所需要的:通过文件名可以找到图像文件中所有物体的bounding box和分类信息。
i = 8fpath = IMG_PATH/fnames[i]fig, ax = plt.subplots(figsize=(6, 4))show_img(ax, fpath)show_bbox(ax, annos[fnames[i]])
The Largest Object
我们先从单目标检测开始,即只定位和识别图像中最大最显著的那个物体。多目标检测的攻略,我把它放到了搞定目标检测(SSD篇)(下)(待更)。
为什么单目标检测的对象是图像中最大的物体?
答复这个问题之前需要你先思考另一问题:卷积神经网络(CNN)是如何识别图像中的物体的?
CNN通过卷积核(kernel)扫描整个图像矩阵来提取图像特征,经过pooling层解决的特征矩阵又成为了下一层的图像矩阵,接着被一下层的kernel提取更深层的特征,如此循环往复,最后这些特征会被全链接层转换为所有分类的概率,概率最高的分类就是物体所属的分类。
图像特征提取的原理是Receptive Field(感受域)。如下图所示,因为图像中央位置的特征被kernel扫描的次数最多,所以提取到的特征最多、颜色最深,而图像四个角只被kernel扫描过1、2次,所以提取到的特征最少、颜色最浅。
Figure 2: 6×6 Receptive Field
因而,CNN对图像中越靠近中央位置、体型越大的物体的判断越精确。换句话说,边缘位置的物体识别难度更大。
Prepare Data
# (y, x, y`, x`) -> (x, y, width, height) def from_bb(bb): return np.array([bb[1], bb[0], bb[3]-bb[1]+1, bb[2]-bb[0]+1])# (x, y, width, height) -> (y, x, y`, x`)def to_bb(bb): return np.array([bb[1], bb[0], bb[1]+bb[3]-1, bb[0]+bb[2]-1])fnames, bboxes, claz = [], [], []for k, (b, c) in train_lrg_annos.items(): fnames.append(k) bboxes.append(' '.join([str(o) for o in to_bb(b)])) claz.append(c)train_lrg_bb_df = pd.DataFrame({'fname': fnames, 'bounding box': bboxes}, columns=['fname', 'bounding box'])display(train_lrg_bb_df.head())train_lrg_bb_df.to_csv(PATH/'train_lrg_bbox.csv', index=False)train_lrg_clas_df = pd.DataFrame({'fname': fnames, 'class': claz}, columns=['fname', 'class'])display(train_lrg_clas_df.head())train_lrg_clas_df.to_csv(PATH/'train_lrg_clas.csv', index=False)
前文已经说过,目标检测可以分为定位和分类两个独立的操作,所以我为他们分别创立了两个数据集,并且通过to_bb()函数转换了bounding box的格式:(x, y, width, height) -> (y, x, y’, x’),(y’, x’)是矩形右下角的坐标,之所以要这样转换是由于后续训练时,神经网络会以 (y, x, y’, x’)的格式来解决bounding box,反之,from_bb()则是用于转换成draw_bbox()所需要的格式。
i = 8fig, ax = plt.subplots(figsize=(6, 4))show_img(ax, IMG_PATH/fnames[i])show_lrg_bbox(ax, (from_bb(np.array(train_lrg_bb_df.loc[i, 'bounding box'].split(' ')).astype(int)), train_lrg_clas_df.loc[i, 'class']))
上图的餐桌就是图像中最大的物体。
Classification
arch = resnet34sz = 224bs = 64tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, crop_type=CropType.NO)md = ImageClassifierData.from_csv(PATH, JPEG_PATH, PATH/'train_lrg_clas.csv', bs=bs, tfms=tfms, val_idxs=val_idxs)lr = 1e-2learn.fit(lr, 1, cycle_len=2, use_clr=(10, 10))learn.unfreeze()lrs = np.array([lr / 100, lr / 10, lr])learn.fit(lrs / 10, 1, cycle_len=2, use_clr=(20, 10))epoch trn_loss val_loss accuracy 0 0.601184 0.528094 0.836655 1 0.334767 0.498876 0.847487
训练参数:pretrained resnet34,H-filp,random rotate(0~30),non-crop, learning rate(0.01)。
之所以要non-crop,是由于对于那些处在图像边缘的物体来说,随机剪切就是灭顶之灾。
图像分类模型的测试结果如下图所示,模型精确率是0.847487,对于20个分类的数据集来说还是精确的,尤其是可以精确识别出处在图像边缘的船。
Bounding Box
arch = resnet34sz = 224bs = 64aug_tfms = [ RandomFlip(tfm_y=TfmType.COORD), RandomRotate(5, p=0.5, tfm_y=TfmType.COORD), RandomLighting(0.1, 0.1, tfm_y=TfmType.COORD)]tfms = tfms_from_model(arch, sz, aug_tfms=aug_tfms, crop_type=CropType.NO, tfm_y=TfmType.COORD)md = ImageClassifierData.from_csv(PATH, JPEG_PATH, PATH/'train_lrg_bbox.csv', bs=bs, tfms=tfms, val_idxs=val_idxs, continuous=True)learn = ConvLearner.pretrained(arch, md)learn.opt_fn = optim.Adamlearn.crit = F.l1_losslr = 1e-1learn.fit(lr, 1, cycle_len=3, use_clr=(10, 10))learn.unfreeze()lrs = np.array([lr / 100, lr / 10, lr])learn.fit(lrs / 20, 1, cycle_len=4, use_clr=(10, 10))epoch trn_loss val_loss 0 24.534589 23.231544 1 21.299118 46.510004 2 19.73664 17.859919 3 18.409843 16.747539
相比于classification模型,bounding box模型的训练参数:
- aug_tfms中定义了tfm_y=TfmType.COORD,这意味着在做data augmentation的时候,y(bounding box)和x一样,也做相同的调整,例如,图像从355×500 scale为224×224,bounding box的坐标也会根据224×224大小重新定位。
- continuous=True,表示y(bounding box)是整型数而不是分类,不需要将其转换成分类index。
- 使用L1损失函数。
Object Detection: classification + bounding box
class ObjectDataset(Dataset): def __init__(self, ds, y): self.ds, self.y = ds, y def __getitem__(self, i): xi, yi = self.ds[i] return (xi, (yi, self.y[i])) @property def c(self): return c @property def is_multi(self): return False @property def is_reg(self): return True @property def sz(self): return szarch = resnet34sz = 224bs = 64aug_tfms = [ RandomFlip(tfm_y=TfmType.COORD), RandomRotate(5, p=0.5, tfm_y=TfmType.COORD), RandomLighting(0.1, 0.1, tfm_y=TfmType.COORD)]tfms = tfms_from_model(arch, sz, aug_tfms=aug_tfms, crop_type=CropType.NO, tfm_y=TfmType.COORD)clas_md = ImageClassifierData.from_csv(PATH, JPEG_PATH, PATH/'train_lrg_clas.csv', bs=bs, tfms=tfms, val_idxs=val_idxs)md = ImageClassifierData.from_csv(PATH, JPEG_PATH, PATH/'train_lrg_bbox.csv', bs=bs, tfms=tfms, val_idxs=val_idxs, continuous=True)c = md.c + clas_md.cmd.trn_dl.dataset = ObjectDataset(md.trn_ds, clas_md.trn_y)md.val_dl.dataset = ObjectDataset(md.val_ds, clas_md.val_y)
统一模型的前提是合并数据。对于两个模型的输出–y,它们的解决方式并不相同,尽管我们可以定义一个新的ImageClassifierData类,但我们这里采用更灵活的方式,复用现有的Fastai library库函数,通过ObjectDataset类将md和clas_md组合起来,当pytorch调用DataLoader来读取mini-batch data时,会调用ObjectDataset的getitem()。
nin = 7 * 7 * 512nf = 512ps = np.array([0.25, 0.5])def custom_head(): return nn.Sequential( Flatten(), nn.ReLU(), nn.Dropout(ps[0]), nn.Linear(nin, nf), nn.ReLU(), nn.Dropout(ps[1]), nn.Linear(nf, c))custom_head_f = custom_head()learn = ConvLearner.pretrained(arch, md, custom_head=custom_head_f)
除了数据集,神经网络模型也需要修改。数据集合并后,模型的输出(y)自然也包括两部分结果:bounding box(前4个数)、分类概率(后20个数),它们不仅长度不同,解决方式也不相同,所以模型的输出层并不是激活函数,而是把输出交给object_loss()、clas_metric()和bb_metric(),让它们自个解决。
def object_loss(preds, targs): bb_t, clas_t = targs bb_p = preds[:, :4] bb_p = F.sigmoid(bb_p) * sz clas_p = preds[:, 4:] return F.l1_loss(bb_p, bb_t) + F.cross_entropy(clas_p, clas_t) * 20def clas_metric(preds, targs): return accuracy(preds[:, 4:], targs[1])def bb_metric(preds, targs): bb_p = preds[:, :4] bb_p = F.sigmoid(bb_p) * sz return F.l1_loss(bb_p, targs[0])learn.crit = object_losslearn.metrics = [clas_metric, bb_metric]
之所以F.cross_entropy(clas_p, clas_t) * 20,是由于clas_loss远小于bbox_loss,模型训练时会只优化bound box而忽略了classification,导致Object Detection模型的分类精确性远低于classification模型。所以,这里我将F.cross_entropy(clas_p, clas_t) * 20,让这两个loss值达到某种平衡。
lr = 1e-3learn.fit(lr, 1, cycle_len=2, use_clr=(10, 10))learn.unfreeze()lrs = np.array([lr / 100, lr / 10, lr])learn.fit(lrs, 1, cycle_len=2, use_clr=(20, 10))epoch trn_loss val_loss clas_metric bb_metric 0 48.056418 37.632276 0.807192 24.481316 1 37.165874 34.25995 0.832322 22.201957
经过简单的模型训练,我们来验证模型的效果。从这几个结果来看,分类是基本正确的,但定位则有些偏差。实际上,这算是预料之中的结果,由于resnet模型是为图像分类开发的,它的强项是图像特征提取,而非图形结构定位,而且resnet的最后会扔掉数据矩阵中所有的空间信息,即对数据矩阵做flatten()或者adaptivepooling()操作,而bounding box恰恰又是需要保留空间信息的,所以bounding box loss才会远大于classification loss。至于它们的loss差距是否缩小,下集揭晓。
x, y = next(iter(md.val_dl))yp = predict_batch(learn.model, x)bb_p, clas_p = yp[:, :4], yp[:, 4:]bb = F.sigmoid(bb_p) * szclas = torch.max(F.log_softmax(clas_p), dim=1)[1]x = md.val_ds.ds.denorm(x)fig, axes = plt.subplots(3, 3, figsize=(8, 6))for i, ax in enumerate(axes.flat): ax.imshow(x[i]) xywh = from_bb(bb[i]) draw_bbox(ax, xywh) draw_txt(ax, xywh[:2], clas_md.classes[clas[i]]) ax.axis('off')plt.tight_layout()
为什么从单目标检测开始?
这个问题是值得深思的。在画面中随便画几个矩形框,视野只集中在矩形内,那是人而非计算机。单目标检测即是开始,也是结束,SSD之所以能同时进行多目标检测又达到较高精确率,本质上它把图像分为N块,而后对每一块做单目标检测。
END
本文详情了目标检测的基本原理:定位 + 分类,并详细分析了单目标检测的原理和实现方法。下一篇博文我会详解如何用SSD搞定多目标检测。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 搞定目标检测(SSD篇)(上)