【论文笔记】R-CNN系列之代码实现
代码源码
前情回顾:【论文笔记】R-CNN系列之论文理解
整体架构
由三部分组成
(1)提取特征的卷积网络extractor
(2)输入特征获得建议框rois的rpn网络
(3)传入rois和特征图,获得分类结果和回归结果的分类网络classifier
伪代码:
class FasterRCNN(nn.Module):
def __init__(self, ...):
super(FasterRCNN, self).__init__()
# 构建特征提取网络
self.extractor, classifier = decom_vgg16(...)
# 构建建议框网络
self.rpn = RegionProposalNetwork(...)
# 构建分类器网络
self.head = VGG16RoIHead(..., classifier=classifier) def forward(self, x, scale=1.):
# 计算输入图片的大小
img_size = x.shape[2:]
# 利用主干网络提取特征
base_feature = self.extractor.forward(x)
# 获得建议框
_, _, rois, roi_indices, _ = self.rpn.forward(base_feature, img_size, scale)
# 获得classifier的分类结果和回归结果
roi_cls_locs, roi_scores = self.head.forward(base_feature, rois, roi_indices, img_size)
return roi_cls_locs, roi_scores, rois, roi_indices def freeze_bn(self):
for m in self.modules():
if isinstance(m, nn.BatchNorm2d):
m.eval()
1.卷积网络提取特征
1.1 关于VGG16
代码
import torch
import torch.nn as nn cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']
'''
假设输入图像为(600, 600, 3),随着cfg的循环,特征层变化如下:
600,600,3 -> 600,600,64 -> 600,600,64 -> 300,300,64 -> 300,300,128 -> 300,300,128 -> 150,150,128 -> 150,150,256 -> 150,150,256 -> 150,150,256
-> 75,75,256 -> 75,75,512 -> 75,75,512 -> 75,75,512 -> 37,37,512 -> 37,37,512 -> 37,37,512 -> 37,37,512
到cfg结束,我们获得了一个37,37,512的特征层
'''
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__() self.feature = make_layers(cfg, batch_norm=False)
self.avgpool = nn.AdaptiveAvgPool2d((7, 7)) #不管cfg結束后特征层大小是多少,统一变成7*7
self.classifier = nn.Sequential(
nn.Linear(7 * 7 * 512, out_features=4096, bias=False),
nn.ReLU(inplace=True), # 原地操作更加节省内存
nn.Linear(in_features=4096, out_features=4096, bias=False),
nn.ReLU(inplace=True),
nn.Linear(in_features=4096, out_features=1000, bias=False),
) def forward(self, x):
x = self.feature(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x def make_layers(cfg, batch_norm=False):
input_channels = 3
layers = []
for v in cfg:
if v == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
conv2d = nn.Conv2d(in_channels=input_channels, out_channels=v, kernel_size=3, stride=1, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)] # layers.append()不能加list
input_channels = v
return nn.Sequential(*layers) input = torch.randn(1, 3, 600, 600)
model = VGG16()
output = model(input)
print(output.shape)
1.2 调整VGG16
- 利用VGG16的特征提取部分作为Faster R-CNN的特征提取网络
- 利用后半部分的全连接网络作为Faster R-CNN的分类网络
def decom_vgg16(pretrained = False):
model = VGG16()
feature = list(model.feature) #转化为list
classifier = list(model.classifier)
del classifier[-1] # 删去最后的全连接层,如果有dropout层也删掉
feature = nn.Sequential(*feature) # 再转为Sequential
classifier = nn.Sequential(*classifier)
return feature,classifier
打印一下classifier :
2. RPN生成区域提议
图中的intermediate layer可以用3*3的卷积实现,cls layer和reg layer都可以用1*1的卷积实现。
self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
cls layer后接softmax层
reg layer后接筛选层
2.1 anchor
(1)计算出3*3滑动窗口中心点对应原始图像上的中心点
特征图上所有的点,对应原图上固定间隔的点
import matplotlib.pyplot as plt
import numpy as np if __name__ == "__main__":
width = 38
height = 38
feat_stride = 16
x = np.arange(0, width * feat_stride, feat_stride)
y = np.arange(0, height * feat_stride, feat_stride)
X, Y = np.meshgrid(x, y)
plt.plot(X, Y,
color='limegreen', # 设置颜色为limegreen
marker='.', # 设置点类型为圆点
linestyle='') # 设置线型为空,也即没有线连接点
plt.grid(True)
plt.show()
原图 特征图
(2)生成anchor box
首先在原图(0,0)位置生成一组(9个)anchor box,然后在原图上滑动(stride=16),得到(38*38*9个)anchor box
# 生成基础的9个先验框
def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2], anchor_scales=[8, 16, 32]):
anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4), dtype=np.float32)
for i in range(len(ratios)):
for j in range(len(anchor_scales)):
h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i]) index = i * len(anchor_scales) + j
anchor_base[index, 0] = - h / 2. #框的四个参数分别是左上的(x,y)右下的(x,y)
anchor_base[index, 1] = - w / 2.
anchor_base[index, 2] = h / 2.
anchor_base[index, 3] = w / 2.
return anchor_base # 对基础先验框移动对应到所有特征点上(特征点在特征图上是相连的,但对应到原图上间隔feat_stride)
def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):
# 计算网格中心点
shift_x = np.arange(0, width * feat_stride, feat_stride)
shift_y = np.arange(0, height * feat_stride, feat_stride)
shift_x, shift_y =np.meshgrid(shift_x, shift_y)
shift = np.stack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel(),), axis=1)
# 每个网格点上的9个先验框
A = anchor_base.shape[0]
K = shift.shape[0]
anchor = anchor_base.reshape((1, A, 4)) + shift.reshape((K, 1, 4))#应为anchor_base的四个参数都是坐标,所以都要平移
anchor = anchor.reshape((K * A, 4)).astype(np.float32)# 所有的先验框
return anchor # 38*38*9个
(3)获得预测框
根据rpn得到的k个(dx,dy,dw,dh),以及上一步得到的k个anchor box的位置(Px,Py,Pw,Ph),可以计算得到预测框的G(Gx,Gy,Gw,Gh)
def loc2bbox(src_bbox, loc):
if src_bbox.size()[0] == 0:
return torch.zeros((0, 4), dtype=loc.dtype)
# src_bbox就是anchor,四个参数为左上角的坐标和右下角的,所以要转换得到(Px,Py,Pw,Ph)
src_width = torch.unsqueeze(src_bbox[:, 2] - src_bbox[:, 0], -1)
src_height = torch.unsqueeze(src_bbox[:, 3] - src_bbox[:, 1], -1)
src_ctr_x = torch.unsqueeze(src_bbox[:, 0], -1) + 0.5 * src_width
src_ctr_y = torch.unsqueeze(src_bbox[:, 1], -1) + 0.5 * src_height
# rpn得到的回归参数
dx = loc[:, 0::4]
dy = loc[:, 1::4]
dw = loc[:, 2::4]
dh = loc[:, 3::4]
# 计算得到预测框的G(Gx,Gy,Gw,Gh)
ctr_x = dx * src_width + src_ctr_x
ctr_y = dy * src_height + src_ctr_y
w = torch.exp(dw) * src_width
h = torch.exp(dh) * src_height
# dst_bbox又是左上角右下角格式
dst_bbox = torch.zeros_like(loc)
dst_bbox[:, 0::4] = ctr_x - 0.5 * w
dst_bbox[:, 1::4] = ctr_y - 0.5 * h
dst_bbox[:, 2::4] = ctr_x + 0.5 * w
dst_bbox[:, 3::4] = ctr_y + 0.5 * h return dst_bbox
(4)筛选anchor
# 建议框筛选 (1)出界的不要
roi[:, [0, 2]] = torch.clamp(roi[:, [0, 2]], min=0, max=img_size[1])
roi[:, [1, 3]] = torch.clamp(roi[:, [1, 3]], min=0, max=img_size[0])
# 建议框筛选(2)小于16的不要
min_size = self.min_size * scale
keep = torch.where(((roi[:, 2] - roi[:, 0]) >= min_size) & ((roi[:, 3] - roi[:, 1]) >= min_size))[0]
roi = roi[keep, :]
score = score[keep]
# 建议框筛选(3)按概率选出前n_pre_nms个
order = torch.argsort(score, descending=True)
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
score = score[order]
# 建议框筛选(4)利用nms选出n_post_nms个
keep = nms(roi, score, self.nms_iou)
keep = keep[:n_post_nms]
roi = roi[keep]
3.分类网络
分类网络主要分为三部分
(1)ROI pooling layer:将rois转换为固定大小
(2)classifier:VGG16的后半部分全连接层
(3)cls layer 和 loc layer:两个全连接层
伪代码:
class VGG16RoIHead(nn.Module):
def __init__(self, n_class, roi_size, spatial_scale, classifier):
super(VGG16RoIHead, self).__init__()
self.roi = RoIPool((roi_size, roi_size), spatial_scale)
self.classifier = classifier
self.cls_loc = nn.Linear(4096, n_class * 4)
self.score = nn.Linear(4096, n_class)
def forward(self, x, rois, roi_indices, img_size):
...
# (1) ROI pooling layer
pool = self.roi(x, indices_and_rois)
pool = pool.view(pool.size(0), -1)
# (2) classifier layer
fc7 = self.classifier(pool)
# (3) cls layer 和 loc layer
roi_cls_locs = self.cls_loc(fc7)
roi_scores = self.score(fc7)
roi_cls_locs = roi_cls_locs.view(n, -1, roi_cls_locs.size(1))
roi_scores = roi_scores.view(n, -1, roi_scores.size(1))
return roi_cls_locs, roi_scores
3. 1 ROI pooling layer
将区域提议对应的feature map转换成小的固定大小(H*W)的feature map,不限制输入map的尺寸。
ROI pooling layer的输入有两项:
(1)提取特征的网络的最后一个feature map;
(2)一个表示图片中所有ROI的N*5的矩阵,其中N表示 ROI的数目,5表示图像index和坐标参数(x,y,h,w) 。坐标的参考系不是针对feature map这张图的,而是针对原图的。
...
self.roi = RoIPool((roi_size, roi_size), spatial_scale)
...
pool = self.roi(x, indices_and_rois)# 利用建议框对公用特征层进行截取
(roi_size, roi_size):执行裁剪后的输出大小(int或Tuple[int,int]),如(高度、宽度)
spatial_scale:将输入坐标映射到框坐标的比例因子。默认值:1.0。
4. 训练
分为几步:
(1)获取公用特征层
(2)利用rpn网络获得调整参数、得分、建议框、先验框
(3)计算损失
4.1 计算建议框网络rpn的回归损失和分类损失
(0)rpn后我们得到rpn_locs, rpn_scores, rois, roi_indices, anchor,其中anchor是先验框,roi是预测框。
(1)为每个先验框anchor找到对应的最大IOU的真实框
def _calc_ious(self, anchor, bbox):
# 获得的ious的shape为[num_anchors, num_gt]
ious = bbox_iou(anchor, bbox)
if len(bbox)==0:
return np.zeros(len(anchor), np.int32), np.zeros(len(anchor)), np.zeros(len(bbox))
# 获得每一个先验框最对应的真实框 [num_anchors, ]
argmax_ious = ious.argmax(axis=1)
# 找出每一个先验框最对应的真实框的iou [num_anchors, ]
max_ious = np.max(ious, axis=1)
# 获得每一个真实框最对应的先验框 [num_gt, ]
gt_argmax_ious = ious.argmax(axis=0)
# 保证每一个真实框都存在对应的先验框
for i in range(len(gt_argmax_ious)):
argmax_ious[gt_argmax_ious[i]] = i
return argmax_ious, max_ious, gt_argmax_ious
# argmax_ious为每个先验框对应的最大的真实框的序号 [num_anchors, ]
# max_ious为每个真实框对应的最大的真实框的iou [num_anchors, ]
# gt_argmax_ious为每一个真实框对应的最大的先验框的序号 [num_gt, ]
(2)采样256个anchor计算损失函数,正负样本比为1:1,如果正样本少于128个,用负样本填充。每个ground truth至少对应一个先验框。
我们分配正标签前景给两类anchor:
1)与某个ground truth有最高的IoU重叠的anchor(也许不到0.7)
2)与任意ground truth有大于0.7的IoU交叠的anchor。
我们分配负标签(背景)给与所有ground truth的IoU比率都低于0.3的anchor。
(3)根据每一个先验框最对应的真实框,得到256个真实框bbox的loc gt_rpn_loc =(dx,dy,dw,dh)
(4)由rpn网络得到的rpn_loc,rpn_score和真实框得到的gt_rpn_loc,gt_rpn_label计算损失
rpn_loc_loss = self._fast_rcnn_loc_loss(rpn_loc, gt_rpn_loc, gt_rpn_label, self.rpn_sigma)
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label, ignore_index=-1)
4.2 计算Classifier网络的回归损失和分类损失
(0)rpn后我们得到rpn_locs, rpn_scores, rois, roi_indices, anchor,其中anchor是先验框,roi是预测框。
(1)获得每一个建议框roi最对应的真实框
(2)采样n_sample个建议框roi计算损失函数
满足建议框和真实框重合程度大于neg_iou_thresh_high的作为正样本
将正样本的数量限制在self.pos_roi_per_image以内满足建议框和真实框重合程度小于neg_iou_thresh_high大于neg_iou_thresh_low作为负样本
将正样本的数量和负样本的数量的总和固定成self.n_sample
(3)得到真实框bbox的loc gt_roi_loc =(dx,dy,dw,dh)
(4)由Classifier网络得到的roi_loc,roi_score,和真实框得到的gt_roi_loc,gt_roi_label计算损失
roi_loc_loss = self._fast_rcnn_loc_loss(roi_loc, gt_roi_loc, gt_roi_label.data, self.roi_sigma)
roi_cls_loss = nn.CrossEntropyLoss(roi_score[0], gt_roi_label)
Tip:
上图为4个真实框ground truth,在rpn层中,我们将roi筛选至n_post_nms个(600个)。
在rpn计算的是先验框anchor和真实框bbox的IOU,分类层用的是建议框roi和真实框bbox的IOU
5. 评价指标
5.1计算交并比IOU
def bbox_iou(bbox_a, bbox_b):
if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
print(bbox_a, bbox_b)
raise IndexError
tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
return area_i / (area_a[:, None] + area_b - area_i)
5.2 计算AP
(1)获得预测结果
- 图片送入网络得到预测结果 roi_cls_locs, roi_scores, rois等
- 对建议框进行解码,获得预测框 [ top, left, bottom, right, score, predicted_class ]
- 预测结果写入txt
(2)获得真实框
- 从"VOC2007/Annotations/"+image_id+".xml"中找到xmin,ymin,xmax,ymax
- 写入txt
(3)按照置信度对预测框排序
假设一共有7张图片,绿色框是GT(15个),红色框是预测框(24个)并带有置信度。
现在假设IOU=30%,按照置信度排序得到下表。
其中TP表示预测正确、FP表示预测错误、acc TP表示从头到该位置累计正确个数、precision表示从头到该位置的精确率、recall表示从头到该位置的召回率。
下图表示的就是从头到尾,依次加入新的样本时,P和R的变化情况:
代码:
- 按照置信度排序
bounding_boxes.sort(key=lambda x:float(x['confidence']), reverse=True)
- 遍历bounding_boxes,计算IOU,大于min_overlap为TP,不然为FP
- 计算precision和recall
for idx, val in enumerate(tp):
rec[idx] = float(tp[idx])/np.maximum(gt_counter_per_class[class_name], 1) # rec=tp/(tp+fn)
# gt_counter_per_class 计算每个类有多少个gt,gt的个数=tp+fn
for idx, val in enumerate(tp):
prec[idx] = float(tp[idx]) / np.maximum((fp[idx] + tp[idx]), 1) # prec=tp/(tp+fp)
(4)使精度单调下降,计算PR曲线的面积
蓝色线就是前面那张PR图,红色虚线的纵坐标是单调减小的,每次减小到右侧蓝线的最高点。APall就是红色虚线下方的面积。
def voc_ap(rec, prec):
rec.insert(0, 0.0) # insert 0.0 at begining of list
rec.append(1.0) # insert 1.0 at end of list
mrec = rec[:]
prec.insert(0, 0.0) # insert 0.0 at begining of list
prec.append(0.0) # insert 0.0 at end of list
mpre = prec[:]
# 1. 这一部分使精度单调下降
i_list = []
for i in range(1, len(mrec)):
if mrec[i] != mrec[i-1]:
i_list.append(i) # if it was matlab would be i + 1
# 2. 平均精度(AP)是曲线下的面积
ap = 0.0
for i in i_list:
ap += ((mrec[i]-mrec[i-1])*mpre[i])
return ap, mrec, mpre
5.3 mAP指标
如果是多类别目标检测任务,就要使用mean AP(mAP),其定义为:
即,对所有的类别进行AP的计算,然后取均值。
AP50指的是IOU的值取50%,AP70同理。
AP@50:5:95指的是IOU的值从50%取到95%,步长为5%,然后算在在这些IOU下的AP的均值。
参考文献:
1. 目标检测01:常用评价指标(AP、AP50、AP@50:5:95、mAP)
3. Pytorch 搭建自己的Faster-RCNN目标检测平台(视频)
4. 深度学习小技巧-mAP精度概念详解与计算绘制(视频)
【论文笔记】R-CNN系列之代码实现的更多相关文章
- 论文笔记:CNN经典结构2(WideResNet,FractalNet,DenseNet,ResNeXt,DPN,SENet)
前言 在论文笔记:CNN经典结构1中主要讲了2012-2015年的一些经典CNN结构.本文主要讲解2016-2017年的一些经典CNN结构. CIFAR和SVHN上,DenseNet-BC优于ResN ...
- 论文笔记:CNN经典结构1(AlexNet,ZFNet,OverFeat,VGG,GoogleNet,ResNet)
前言 本文主要介绍2012-2015年的一些经典CNN结构,从AlexNet,ZFNet,OverFeat到VGG,GoogleNetv1-v4,ResNetv1-v2. 在论文笔记:CNN经典结构2 ...
- 【论文笔记】CNN for NLP
什么是Convolutional Neural Network(卷积神经网络)? 最早应该是LeCun(1998)年论文提出,其结果如下:运用于手写数字识别.详细就不介绍,可参考zouxy09的专栏, ...
- Deep Learning论文笔记之(四)CNN卷积神经网络推导和实现(转)
Deep Learning论文笔记之(四)CNN卷积神经网络推导和实现 zouxy09@qq.com http://blog.csdn.net/zouxy09 自己平时看了一些论文, ...
- 论文笔记系列-Auto-DeepLab:Hierarchical Neural Architecture Search for Semantic Image Segmentation
Pytorch实现代码:https://github.com/MenghaoGuo/AutoDeeplab 创新点 cell-level and network-level search 以往的NAS ...
- Person Re-identification 系列论文笔记(一):Scalable Person Re-identification: A Benchmark
打算整理一个关于Person Re-identification的系列论文笔记,主要记录近年CNN快速发展中的部分有亮点和借鉴意义的论文. 论文笔记流程采用contributions->algo ...
- 【论文笔记系列】AutoML:A Survey of State-of-the-art (下)
[论文笔记系列]AutoML:A Survey of State-of-the-art (上) 上一篇文章介绍了Data preparation,Feature Engineering,Model S ...
- 论文笔记系列-Neural Network Search :A Survey
论文笔记系列-Neural Network Search :A Survey 论文 笔记 NAS automl survey review reinforcement learning Bayesia ...
- Multimodal —— 看图说话(Image Caption)任务的论文笔记(一)评价指标和NIC模型
看图说话(Image Caption)任务是结合CV和NLP两个领域的一种比较综合的任务,Image Caption模型的输入是一幅图像,输出是对该幅图像进行描述的一段文字.这项任务要求模型可以识别图 ...
- Deep Reinforcement Learning for Visual Object Tracking in Videos 论文笔记
Deep Reinforcement Learning for Visual Object Tracking in Videos 论文笔记 arXiv 摘要:本文提出了一种 DRL 算法进行单目标跟踪 ...
随机推荐
- RC4Drop加密技术:原理、实践与安全性探究
第一章:介绍 1.1 加密技术的重要性 加密技术在当今信息社会中扮演着至关重要的角色.通过加密,我们可以保护敏感信息的机密性,防止信息被未经授权的用户访问.窃取或篡改.加密技术还可以确保数据在传输过程 ...
- threading的定时器模块,python,每间隔一段时间执行一次任务
工作中常有一些定时任务要处理,比如使用百度的接口,它的access_token是一个更新一次的,每次使用时总是请求会很慢,所以我们把它保存起来,用定时器模块,定时在过期之前请求一次,或者定时数据同步, ...
- steam社区留言红小作文模板
steam社区留言红小作文模板 Dear steam: Im a steam user which most play csgo.i saw i had be banned in steam comm ...
- MySQL正则表达式:REGEXP 和 LIKE
正则表达式作用: 根据指定的匹配模式匹配文中符合要求的特殊字符. REGEXP : ①操作符中常用的匹配列表: ②匹配特殊字符使用\\进行转义 \\. 能够匹配 . \\f 换页 \\n 换行 ...
- 剑指offer51(Java)-数组中的逆序对(困难)
题目: 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 示例1: 输入: [7,5,6,4] 输出: 5 限制: 0 &l ...
- 【产品能力深度解读】连续入围Gartner魔力象限的Quick BI有何魔力?
简介: 国际权威分析机构Gartner发布2021年商业智能和分析平台魔力象限报告,阿里云Quick BI再度入选,并继续成为该领域魔力象限唯一入选的中国企业. Quick BI凭借在增强分析能力上的 ...
- Scheduled SQL: SLS 大规模日志上的全局分析与调度
简介: 本文总结了大规模日志全局分析的需求,讨论SLS上现有的典型分析方案,并延伸到 SLS 原生数据处理方案,介绍 Schedueld SQL 功能与最佳实践. 大规模日志全局分析的需求 数据大规模 ...
- Python静态类型解析工具简介和实践
简介: Python是一门强类型的动态类型语言,开发者可以给对象动态指定类型,但类型不匹配的操作是不被允许的.动态类型帮助开发者写代码轻松愉快,然而,俗话说:动态一时爽,重构火葬场.动态类型也带来了许 ...
- WPF 双向绑定到非公开 set 方法属性在 NET 45 和 NET Core 行为的不同
本文记录 WPF 在 .NET Framework 4.5 和 .NET Core 3.0 或更高版本对使用 Binding 下的 TwoWay 双向绑定模式绑定到非公开的 set 属性上的行为变更 ...
- docker.from_env() 获取docker守护进程时出现 TypeError: load_config() got an unexpected keyword argument 'config_dict' 异常
某天使用python重启docker容器时,出现了一个令人费解的BUG,我的代码为 1 def restart_docker(container_name): 2 # 连接到docker守护进程 3 ...