国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

你真的理解Faster RCNN嗎?捋一捋Pytorch官方Faster RCNN代碼

共 30940字,需瀏覽 62分鐘

 ·

2020-11-01 07:42

↑ 點(diǎn)擊藍(lán)字?關(guān)注極市平臺(tái)

作者丨白裳@知乎
來源丨h(huán)ttps://zhuanlan.zhihu.com/p/145842317
編輯丨極市平臺(tái)

極市導(dǎo)讀

?

本文詳細(xì)的介紹了 torchvision 中的 FasterRCNN 代碼實(shí)現(xiàn),并分析了作者認(rèn)為重要的知識(shí)點(diǎn),GeneralizedRCNN的代碼以及FasterRCNN的訓(xùn)練等。幫助入門的小伙伴更好的理解模型細(xì)節(jié)的問題。?>>加入極市CV技術(shù)交流群,走在計(jì)算機(jī)視覺的最前沿

目前 pytorch 已經(jīng)在 torchvision 模塊集成了 FasterRCNN 和 MaskRCNN 代碼。考慮到幫助各位小伙伴理解模型細(xì)節(jié)問題,本文分析一下 FasterRCNN 代碼,幫助新手理解 Two-Stage 檢測(cè)中的主要問題。

這篇文章默認(rèn)讀者已經(jīng)對(duì) FasterRCNN 原理有一定了解。否則請(qǐng)先點(diǎn)擊閱讀上一篇文章:

白裳:一文讀懂Faster RCNN

torchvision 中 FasterRCNN 代碼文檔如下:

https://pytorch.org/docs/stable/torchvision/models.html#faster-r-cnnpytorch.org

在 python 中裝好 torchvision 后,輸入以下命令即可查看版本和代碼位置:

import torchvision

print(torchvision.__version__)
# '0.6.0'
print(torchvision.__path__)
# ['/usr/local/lib/python3.7/site-packages/torchvision']

代碼結(jié)構(gòu)


圖1

作為 torchvision 中目標(biāo)檢測(cè)基類,GeneralizedRCNN 繼承了 torch.nn.Module,后續(xù) FasterRCNN 、MaskRCNN 都繼承 GeneralizedRCNN。

GeneralizedRCNN

GeneralizedRCNN 繼承基類 nn.Module 。首先來看看基類 GeneralizedRCNN 的代碼:

class GeneralizedRCNN(nn.Module):
def __init__(self, backbone, rpn, roi_heads, transform):
super(GeneralizedRCNN, self).__init__()
self.transform = transform
self.backbone = backbone
self.rpn = rpn
self.roi_heads = roi_heads
# used only on torchscript mode
self._has_warned = False

@torch.jit.unused
def eager_outputs(self, losses, detections):
# type: (Dict[str, Tensor], List[Dict[str, Tensor]]) -> Tuple[Dict[str, Tensor], List[Dict[str, Tensor]]]
if self.training:
return losses

return detections

def forward(self, images, targets=None):
if self.training and targets is None:
raise ValueError("In training mode, targets should be passed")
original_image_sizes = torch.jit.annotate(List[Tuple[int, int]], [])
for img in images:
val = img.shape[-2:]
assert len(val) == 2
original_image_sizes.append((val[0], val[1]))

images, targets = self.transform(images, targets)
features = self.backbone(images.tensors)
if isinstance(features, torch.Tensor):
features = OrderedDict([('0', features)])
proposals, proposal_losses = self.rpn(images, features, targets)
detections, detector_losses = self.roi_heads(features, proposals, images.image_sizes, targets)
detections = self.transform.postprocess(detections, images.image_sizes, original_image_sizes)

losses = {}
losses.update(detector_losses)
losses.update(proposal_losses)

if torch.jit.is_scripting():
if not self._has_warned:
warnings.warn("RCNN always returns a (Losses, Detections) tuple in scripting")
self._has_warned = True
return (losses, detections)
else:
return self.eager_outputs(losses, detections)


對(duì)于 GeneralizedRCNN 類,其中有4個(gè)重要的接口:

  1. transform
  2. backbone
  3. rpn
  4. roi_heads

transform

# GeneralizedRCNN.forward(...)
for img in images:
val = img.shape[-2:]
assert len(val) == 2
original_image_sizes.append((val[0], val[1]))

images, targets = self.transform(images, targets)


圖2 transform接口

transform主要做2件事:

  1. 將輸入進(jìn)行標(biāo)準(zhǔn)化(如FasterRCNN是對(duì) 輸入減 image_mean 再除 image_std)
  2. 將圖像縮放到固定大?。ㄍ瑫r(shí)也要對(duì)應(yīng)縮放 targets 中標(biāo)記框 )

需要說明,由于把縮放后的圖像輸入網(wǎng)絡(luò),那么網(wǎng)絡(luò)輸出的檢測(cè)框也是在縮放后的圖像上的。但是實(shí)際中我們需要的是在原始圖像的檢測(cè)框,為了對(duì)應(yīng)起來,所以需要記錄變換前original_images_sizes 。

圖3

這里解釋一下為何要縮放圖像。對(duì)于 FasterRCNN,從純理論上來說確實(shí)可以支持任意大小的圖片。但是實(shí)際中,如果輸入圖像太大(如6000x4000)會(huì)直接撐爆內(nèi)存??紤]到工程問題,縮放是一個(gè)比較穩(wěn)妥的折衷選擇。

backbone + rpn + roi_heads


圖4

完成圖像縮放之后其實(shí)才算是正式進(jìn)入網(wǎng)絡(luò)流程。接下來有4個(gè)步驟:

  • 將 transform 后的圖像輸入到 backbone 模塊提取特征圖
# GeneralizedRCNN.forward(...)
features = self.backbone(images.tensors)

backbone 一般為 VGG、ResNet、MobileNet 等網(wǎng)絡(luò)。

  • 然后經(jīng)過 rpn 模塊生成 proposals 和 proposal_losses
# GeneralizedRCNN.forward(...)
features = self.backbone(images.tensors)
  • 接著進(jìn)入 roi_heads 模塊(即 roi_pooling + 分類)
# GeneralizedRCNN.forward(...)
detections, detector_losses =
self.roi_heads(features, proposals, images.image_sizes, targets
  • 最后經(jīng) postprocess 模塊(進(jìn)行 NMS,同時(shí)將 box 通過 original_images_size映射回原圖)
# GeneralizedRCNN.forward(...)
detections = self.transform.postprocess(detections, images.image_sizes, original_image_sizes)

FasterRCNN

FasterRCNN 繼承基類 GeneralizedRCNN。

class FasterRCNN(GeneralizedRCNN):

def __init__(self, backbone, num_classes=None,
# transform parameters
min_size=800, max_size=1333,
image_mean=None, image_std=None,
# RPN parameters
rpn_anchor_generator=None, rpn_head=None,
rpn_pre_nms_top_n_train=2000, rpn_pre_nms_top_n_test=1000,
rpn_post_nms_top_n_train=2000, rpn_post_nms_top_n_test=1000,
rpn_nms_thresh=0.7,
rpn_fg_iou_thresh=0.7, rpn_bg_iou_thresh=0.3,
rpn_batch_size_per_image=256, rpn_positive_fraction=0.5,
# Box parameters
box_roi_pool=None, box_head=None, box_predictor=None,
box_score_thresh=0.05, box_nms_thresh=0.5, box_detections_per_img=100,
box_fg_iou_thresh=0.5, box_bg_iou_thresh=0.5,
box_batch_size_per_image=512, box_positive_fraction=0.25,
bbox_reg_weights=None):

out_channels = backbone.out_channels

if rpn_anchor_generator is None:
anchor_sizes = ((32,), (64,), (128,), (256,), (512,))
aspect_ratios = ((0.5, 1.0, 2.0),) * len(anchor_sizes)
rpn_anchor_generator = AnchorGenerator(
anchor_sizes, aspect_ratios
)
if rpn_head is None:
rpn_head = RPNHead(
out_channels, rpn_anchor_generator.num_anchors_per_location()[0]
)

rpn_pre_nms_top_n = dict(training=rpn_pre_nms_top_n_train, testing=rpn_pre_nms_top_n_test)
rpn_post_nms_top_n = dict(training=rpn_post_nms_top_n_train, testing=rpn_post_nms_top_n_test)

rpn = RegionProposalNetwork(
rpn_anchor_generator, rpn_head,
rpn_fg_iou_thresh, rpn_bg_iou_thresh,
rpn_batch_size_per_image, rpn_positive_fraction,
rpn_pre_nms_top_n, rpn_post_nms_top_n, rpn_nms_thresh)

if box_roi_pool is None:
box_roi_pool = MultiScaleRoIAlign(
featmap_names=['0', '1', '2', '3'],
output_size=7,
sampling_ratio=2)

if box_head is None:
resolution = box_roi_pool.output_size[0]
representation_size = 1024
box_head = TwoMLPHead(
out_channels * resolution ** 2,
representation_size)

if box_predictor is None:
representation_size = 1024
box_predictor = FastRCNNPredictor(
representation_size,
num_classes)

roi_heads = RoIHeads(
# Box
box_roi_pool, box_head, box_predictor,
box_fg_iou_thresh, box_bg_iou_thresh,
box_batch_size_per_image, box_positive_fraction,
bbox_reg_weights,
box_score_thresh, box_nms_thresh, box_detections_per_img)

if image_mean is None:
image_mean = [0.485, 0.456, 0.406]
if image_std is None:
image_std = [0.229, 0.224, 0.225]
transform = GeneralizedRCNNTransform(min_size, max_size, image_mean, image_std)

super(FasterRCNN, self).__init__(backbone, rpn, roi_heads, transform)


FasterRCNN 實(shí)現(xiàn)了 GeneralizedRCNN 中的 transform、backbone、rpn、roi_heads 接口:

# FasterRCNN.__init__(...)
super(FasterRCNN, self).__init__(backbone, rpn, roi_heads, transform)

對(duì)于 transform 接口,使用 GeneralizedRCNNTransform 實(shí)現(xiàn)。從代碼變量名可以明顯看到包含:

  • 與縮放相關(guān)參數(shù):min_size + max_size
  • 與歸一化相關(guān)參數(shù):image_mean + image_std(對(duì)輸入[0, 1]減去image_mean再除以image_std)
# FasterRCNN.__init__(...)
if image_mean is None:
image_mean = [0.485, 0.456, 0.406]
if image_std is None:
image_std = [0.229, 0.224, 0.225]
transform = GeneralizedRCNNTransform(min_size, max_size, image_mean, image_std)

對(duì)于 backbone 使用 ResNet50 + FPN 結(jié)構(gòu):

def fasterrcnn_resnet50_fpn(pretrained=False, progress=True, num_classes=91, pretrained_backbone=True, **kwargs):
if pretrained:
# no need to download the backbone if pretrained is set
pretrained_backbone = False
backbone = resnet_fpn_backbone('resnet50', pretrained_backbone)
model = FasterRCNN(backbone, num_classes, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls['fasterrcnn_resnet50_fpn_coco'], progress=progress)
model.load_state_dict(state_dict)
return model

ResNet: Deep Residual Learning for Image Recognition

FPN: Feature Pyramid Networks for Object Detection


圖5 FPN

接下來重點(diǎn)介紹 rpn 接口的實(shí)現(xiàn)。首先是 rpn_anchor_generator :

# FasterRCNN.__init__(...)
if rpn_anchor_generator is None:
anchor_sizes = ((32,), (64,), (128,), (256,), (512,))
aspect_ratios = ((0.5, 1.0, 2.0),) * len(anchor_sizes)
rpn_anchor_generator = AnchorGenerator(
anchor_sizes, aspect_ratios
)

對(duì)于普通的 FasterRCNN 只需要將 feature_map 輸入到 rpn 網(wǎng)絡(luò)生成 proposals 即可。但是由于加入 FPN,需要將多個(gè) feature_map 逐個(gè)輸入到 rpn 網(wǎng)絡(luò)。

圖6

接下來看看 AnchorGenerator 具體實(shí)現(xiàn):

class AnchorGenerator(nn.Module):
......

def generate_anchors(self, scales, aspect_ratios, dtype=torch.float32, device="cpu"):
# type: (List[int], List[float], int, Device) # noqa: F821
scales = torch.as_tensor(scales, dtype=dtype, device=device)
aspect_ratios = torch.as_tensor(aspect_ratios, dtype=dtype, device=device)
h_ratios = torch.sqrt(aspect_ratios)
w_ratios = 1 / h_ratios

ws = (w_ratios[:, None] * scales[None, :]).view(-1)
hs = (h_ratios[:, None] * scales[None, :]).view(-1)

base_anchors = torch.stack([-ws, -hs, ws, hs], dim=1) / 2
return base_anchors.round()

def set_cell_anchors(self, dtype, device):
# type: (int, Device) -> None # noqa: F821
......

cell_anchors = [
self.generate_anchors(
sizes,
aspect_ratios,
dtype,
device
)
for sizes, aspect_ratios in zip(self.sizes, self.aspect_ratios)
]
self.cell_anchors = cell_anchors

首先,每個(gè)位置有 5 種 anchor_size 和 3 種 aspect_ratios,所以每個(gè)位置生成 15 個(gè) base_anchors:

[ -23.,  -11.,   23.,   11.]
[ -16., -16., 16., 16.] # w = h = 32, ratio = 1
[ -11., -23., 11., 23.]
[ -45., -23., 45., 23.]
[ -32., -32., 32., 32.] # w = h = 64, ratio = 1
[ -23., -45., 23., 45.]
[ -91., -45., 91., 45.]
[ -64., -64., 64., 64.] # w = h = 128, ratio = 1
[ -45., -91., 45., 91.]
[-181., -91., 181., 91.]
[-128., -128., 128., 128.] # w = h = 256, ratio = 1
[ -91., -181., 91., 181.]
[-362., -181., 362., 181.]
[-256., -256., 256., 256.] # w = h = 512, ratio = 1
[-181., -362., 181., 362.]

注意 base_anchors 的中心都是 點(diǎn),如下圖所示:

圖7 base_anchor(此圖只畫了32/64/128的base_anchor)

接著來看 AnchorGenerator.grid_anchors 函數(shù):

# AnchorGenerator
def grid_anchors(self, grid_sizes, strides):
# type: (List[List[int]], List[List[Tensor]])
anchors = []
cell_anchors = self.cell_anchors
assert cell_anchors is not None

for size, stride, base_anchors in zip(
grid_sizes, strides, cell_anchors
):
grid_height, grid_width = size
stride_height, stride_width = stride
device = base_anchors.device

# For output anchor, compute [x_center, y_center, x_center, y_center]
shifts_x = torch.arange(
0, grid_width, dtype=torch.float32, device=device
) * stride_width
shifts_y = torch.arange(
0, grid_height, dtype=torch.float32, device=device
) * stride_height
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x)
shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)
shifts = torch.stack((shift_x, shift_y, shift_x, shift_y), dim=1)

# For every (base anchor, output anchor) pair,
# offset each zero-centered base anchor by the center of the output anchor.
anchors.append(
(shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)).reshape(-1, 4)
)

return anchors

def forward(self, image_list, feature_maps):
# type: (ImageList, List[Tensor])
grid_sizes = list([feature_map.shape[-2:] for feature_map in feature_maps])
image_size = image_list.tensors.shape[-2:]
dtype, device = feature_maps[0].dtype, feature_maps[0].device
strides = [[torch.tensor(image_size[0] / g[0], dtype=torch.int64, device=device),
torch.tensor(image_size[1] / g[1], dtype=torch.int64, device=device)] for g in grid_sizes]
self.set_cell_anchors(dtype, device)
anchors_over_all_feature_maps = self.cached_grid_anchors(grid_sizes, strides)
......

在之前提到,由于有 FPN 網(wǎng)絡(luò),所以輸入 rpn 的是多個(gè)特征。為了方便介紹,以下都是以某一個(gè)特征進(jìn)行描述,其他特征類似。

假設(shè)有 的特征,首先會(huì)計(jì)算這個(gè)特征相對(duì)于輸入圖像的下采樣倍數(shù) stride:

然后生成一個(gè) 大小的網(wǎng)格,每個(gè)格子長度為 stride,如下圖:

# AnchorGenerator.grid_anchors(...)
shifts_x = torch.arange(0, grid_width, dtype=torch.float32, device=device) * stride_width
shifts_y = torch.arange(0, grid_height, dtype=torch.float32, device=device) * stride_height
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x)

圖8

然后將 base_anchors 的中心從 移動(dòng)到網(wǎng)格的點(diǎn),且在網(wǎng)格的每個(gè)點(diǎn)都放置一組 base_anchors。這樣就在當(dāng)前 feature_map 上有了很多的 anchors。

需要特別說明,stride 代表網(wǎng)絡(luò)的感受野,網(wǎng)絡(luò)不可能檢測(cè)到比 feature_map 更密集的框了!所以才只會(huì)在網(wǎng)格中每個(gè)點(diǎn)設(shè)置 anchors(反過來說,如果在網(wǎng)格的兩個(gè)點(diǎn)之間設(shè)置 anchors,那么就對(duì)應(yīng) feature_map 中半個(gè)點(diǎn),顯然不合理)。

# AnchorGenerator.grid_anchors(...)
anchors.append((shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)).reshape(-1, 4))

圖9 (注:為了方便描述,這里只畫了3個(gè)anchor,實(shí)際每個(gè)點(diǎn)有9個(gè)anchor)

放置好 anchors 后,接下來就要調(diào)整網(wǎng)絡(luò),使網(wǎng)絡(luò)輸出能夠判斷每個(gè) anchor 是否有目標(biāo),同時(shí)還要有 bounding box regression 需要的4個(gè)值 。

class RPNHead(nn.Module):
def __init__(self, in_channels, num_anchors):
super(RPNHead, self).__init__()
self.conv = nn.Conv2d(
in_channels, in_channels, kernel_size=3, stride=1, padding=1
)
self.cls_logits = nn.Conv2d(in_channels, num_anchors, kernel_size=1, stride=1)
self.bbox_pred = nn.Conv2d(
in_channels, num_anchors * 4, kernel_size=1, stride=1
)

def forward(self, x):
logits = []
bbox_reg = []
for feature in x:
t = F.relu(self.conv(feature))
logits.append(self.cls_logits(t))
bbox_reg.append(self.bbox_pred(t))
return logits, bbox_reg

假設(shè) feature 的大小 ,每個(gè)點(diǎn) 個(gè) anchor。從 RPNHead 的代碼中可以明顯看到:

  • 首先進(jìn)行 3x3 卷積
  • 然后對(duì) feature 進(jìn)行卷積,輸出 cls_logits 大小是 ,對(duì)應(yīng)每個(gè) anchor 是否有目標(biāo);
  • 同時(shí)feature 進(jìn)行卷積,輸出 bbox_pred 大小是 ,對(duì)應(yīng)每個(gè)點(diǎn)的4個(gè)框位置回歸信息 。
# RPNHead.__init__(...)
self.cls_logits = nn.Conv2d(in_channels, num_anchors, kernel_size=1, stride=1)
self.bbox_pred = nn.Conv2d(in_channels, num_anchors * 4, kernel_size=1, stride=1)

圖10(注:為了方便描述,這里只畫了3個(gè)anchor,實(shí)際每個(gè)點(diǎn)有9個(gè)anchor)

上述過程只是單個(gè) feature_map 的處理流程。對(duì)于 FPN 網(wǎng)絡(luò)的輸出的多個(gè)大小不同的 feature_maps,每個(gè)特征圖都會(huì)按照上述過程計(jì)算 stride 和網(wǎng)格,并設(shè)置 anchors。當(dāng)處理完后獲得密密麻麻的各種 anchors 了。

接下來進(jìn)入 RegionProposalNetwork 類:

# FasterRCNN.__init__(...)
rpn_pre_nms_top_n = dict(training=rpn_pre_nms_top_n_train, testing=rpn_pre_nms_top_n_test)
rpn_post_nms_top_n = dict(training=rpn_post_nms_top_n_train, testing=rpn_post_nms_top_n_test)

# rpn_anchor_generator 生成anchors
# rpn_head 調(diào)整feature_map獲得cls_logits+bbox_pred
rpn = RegionProposalNetwork(
rpn_anchor_generator, rpn_head,
rpn_fg_iou_thresh, rpn_bg_iou_thresh,
rpn_batch_size_per_image, rpn_positive_fraction,
rpn_pre_nms_top_n, rpn_post_nms_top_n, rpn_nms_thresh)


RegionProposalNetwork 類的用是:

  • test 階段 :計(jì)算有目標(biāo)的 anchor 并進(jìn)行框回歸生成 proposals,然后 NMS
  • train 階段 :除了上面的作用,還計(jì)算 rpn loss
class RegionProposalNetwork(torch.nn.Module):
.......

def forward(self, images, features, targets=None):
features = list(features.values())
objectness, pred_bbox_deltas = self.head(features)
anchors = self.anchor_generator(images, features)

num_images = len(anchors)
num_anchors_per_level_shape_tensors = [o[0].shape for o in objectness]
num_anchors_per_level = [s[0] * s[1] * s[2] for s in num_anchors_per_level_shape_tensors]
objectness, pred_bbox_deltas = \
concat_box_prediction_layers(objectness, pred_bbox_deltas)
# apply pred_bbox_deltas to anchors to obtain the decoded proposals
# note that we detach the deltas because Faster R-CNN do not backprop through
# the proposals
proposals = self.box_coder.decode(pred_bbox_deltas.detach(), anchors)
proposals = proposals.view(num_images, -1, 4)
boxes, scores = self.filter_proposals(proposals, objectness, images.image_sizes, num_anchors_per_level)

losses = {}
if self.training:
assert targets is not None
labels, matched_gt_boxes = self.assign_targets_to_anchors(anchors, targets)
regression_targets = self.box_coder.encode(matched_gt_boxes, anchors)
loss_objectness, loss_rpn_box_reg = self.compute_loss(
objectness, pred_bbox_deltas, labels, regression_targets)
losses = {
"loss_objectness": loss_objectness,
"loss_rpn_box_reg": loss_rpn_box_reg,
}
return boxes, losses

具體來看,首先計(jì)算有目標(biāo)的 anchor 并進(jìn)行框回歸生成 proposals :

# RegionProposalNetwork.forward(...)
objectness, pred_bbox_deltas = self.head(features)
anchors = self.anchor_generator(images, features)
......
proposals = self.box_coder.decode(pred_bbox_deltas.detach(), anchors)
proposals = proposals.view(num_images, -1, 4)

然后依照 objectness 置信由大到小度排序(優(yōu)先提取更可能包含目標(biāo)的的),并 NMS,生成 boxes (即 NMS 后的 proposal boxes ) :

# RegionProposalNetwork.forward(...)
boxes, scores = self.filter_proposals(proposals, objectness, images.image_sizes, num_anchors_per_level)

如果是訓(xùn)練階段,還要將 boxes 與 anchors 進(jìn)行匹配,計(jì)算 cls_logits 的損失 loss_objectness,同時(shí)計(jì)算 bbox_pred 的損失 loss_rpn_box_reg。

在 RegionProposalNetwork 之后已經(jīng)生成了 boxes ,接下來就要提取 boxes 內(nèi)的特征進(jìn)行 roi_pooling :

roi_heads = RoIHeads(
# Box
box_roi_pool, box_head, box_predictor,
box_fg_iou_thresh, box_bg_iou_thresh,
box_batch_size_per_image, box_positive_fraction,
bbox_reg_weights,
box_score_thresh, box_nms_thresh, box_detections_per_img)

這里一點(diǎn)問題是如何計(jì)算 box 所屬的 feature_map:

  • 對(duì)于原始 FasterRCNN,只在 backbone 的最后一層 feature_map 提取 box 對(duì)應(yīng)特征;
  • 當(dāng)加入 FPN 后 backbone 會(huì)輸出多個(gè)特征圖,需要計(jì)算當(dāng)前 boxes 對(duì)應(yīng)于哪一個(gè)特征。

如下圖:

圖11

class MultiScaleRoIAlign(nn.Module):
......

def infer_scale(self, feature, original_size):
# type: (Tensor, List[int])
# assumption: the scale is of the form 2 ** (-k), with k integer
size = feature.shape[-2:]
possible_scales = torch.jit.annotate(List[float], [])
for s1, s2 in zip(size, original_size):
approx_scale = float(s1) / float(s2)
scale = 2 ** float(torch.tensor(approx_scale).log2().round())
possible_scales.append(scale)
assert possible_scales[0] == possible_scales[1]
return possible_scales[0]

def setup_scales(self, features, image_shapes):
# type: (List[Tensor], List[Tuple[int, int]])
assert len(image_shapes) != 0
max_x = 0
max_y = 0
for shape in image_shapes:
max_x = max(shape[0], max_x)
max_y = max(shape[1], max_y)
original_input_shape = (max_x, max_y)

scales = [self.infer_scale(feat, original_input_shape) for feat in features]
# get the levels in the feature map by leveraging the fact that the network always
# downsamples by a factor of 2 at each level.
lvl_min = -torch.log2(torch.tensor(scales[0], dtype=torch.float32)).item()
lvl_max = -torch.log2(torch.tensor(scales[-1], dtype=torch.float32)).item()
self.scales = scales
self.map_levels = initLevelMapper(int(lvl_min), int(lvl_max))

首先計(jì)算每個(gè) feature_map 相對(duì)于網(wǎng)絡(luò)輸入 image 的下采樣倍率 scale。其中 infer_scale 函數(shù)采用如下的近似公式:

該公式相當(dāng)于做了一個(gè)簡單的映射,將不同的 feature_map 與 image 大小比映射到附近的尺度:


圖12

例如對(duì)于 FasterRCNN 實(shí)際值為:

之后設(shè)置 lvl_min=2 和 lvl_max=5:

# MultiScaleRoIAlign.setup_scales(...)
# get the levels in the feature map by leveraging the fact that the network always
# downsamples by a factor of 2 at each level.
lvl_min = -torch.log2(torch.tensor(scales[0], dtype=torch.float32)).item()
lvl_max = -torch.log2(torch.tensor(scales[-1], dtype=torch.float32)).item()

接著使用 FPN 原文中的公式計(jì)算 box 所在 anchor(其中 , 為 box 面積):

class LevelMapper(object)
def __init__(self, k_min, k_max, canonical_scale=224, canonical_level=4, eps=1e-6):
self.k_min = k_min # lvl_min=2
self.k_max = k_max # lvl_max=5
self.s0 = canonical_scale # 224
self.lvl0 = canonical_level # 4
self.eps = eps

def __call__(self, boxlists):
s = torch.sqrt(torch.cat([box_area(boxlist) for boxlist in boxlists]))

# Eqn.(1) in FPN paper
target_lvls = torch.floor(self.lvl0 + torch.log2(s / self.s0) + torch.tensor(self.eps, dtype=s.dtype))
target_lvls = torch.clamp(target_lvls, min=self.k_min, max=self.k_max)
return (target_lvls.to(torch.int64) - self.k_min).to(torch.int64)

其中 torch.clamp(input, min, max) → Tensor 函數(shù)的作用是截?cái)?,防止越界?/p>

可以看到,通過 LevelMapper 類將不同大小的 box 定位到某個(gè) feature_map,如下圖。之后就是按照?qǐng)D11中的流程進(jìn)行 roi_pooling 操作。

圖13

在確定 proposal box 所屬 FPN 中哪個(gè) feature_map 之后,接著來看 MultiScaleRoIAlign 如何進(jìn)行 roi_pooling 操作:

class MultiScaleRoIAlign(nn.Module):
......

def forward(self, x, boxes, image_shapes):
# type: (Dict[str, Tensor], List[Tensor], List[Tuple[int, int]]) -> Tensor
x_filtered = []
for k, v in x.items():
if k in self.featmap_names:
x_filtered.append(v)
num_levels = len(x_filtered)
rois = self.convert_to_roi_format(boxes)
if self.scales is None:
self.setup_scales(x_filtered, image_shapes)

scales = self.scales
assert scales is not None

# 沒有 FPN 時(shí),只有1/32的最后一個(gè)feature_map進(jìn)行roi_pooling
if num_levels == 1:
return roi_align(
x_filtered[0], rois,
output_size=self.output_size,
spatial_scale=scales[0],
sampling_ratio=self.sampling_ratio
)

# 有 FPN 時(shí),有4個(gè)feature_map進(jìn)行roi_pooling
# 首先按照
mapper = self.map_levels
assert mapper is not None

levels = mapper(boxes)

num_rois = len(rois)
num_channels = x_filtered[0].shape[1]

dtype, device = x_filtered[0].dtype, x_filtered[0].device
result = torch.zeros(
(num_rois, num_channels,) + self.output_size,
dtype=dtype,
device=device,
)

tracing_results = []
for level, (per_level_feature, scale) in enumerate(zip(x_filtered, scales)):
idx_in_level = torch.nonzero(levels == level).squeeze(1)
rois_per_level = rois[idx_in_level]

result_idx_in_level = roi_align(
per_level_feature, rois_per_level,
output_size=self.output_size,
spatial_scale=scale, sampling_ratio=self.sampling_ratio)

if torchvision._is_tracing():
tracing_results.append(result_idx_in_level.to(dtype))
else:
result[idx_in_level] = result_idx_in_level

if torchvision._is_tracing():
result = _onnx_merge_levels(levels, tracing_results)

return result

在 MultiScaleRoIAlign.forward(...) 函數(shù)可以看到:

  • 沒有 FPN 時(shí),只有1/32的最后一個(gè) feature_map 進(jìn)行 roi_pooling
   if num_levels == 1:
return roi_align(
x_filtered[0], rois,
output_size=self.output_size,
spatial_scale=scales[0],
sampling_ratio=self.sampling_ratio
)
  • 有 FPN 時(shí),有4個(gè) 的 feature maps 參加計(jì)算。首先計(jì)算每個(gè)每個(gè) box 所屬哪個(gè) feature map ,再在所屬 feature map 進(jìn)行 roi_pooling
# 首先計(jì)算每個(gè)每個(gè) box 所屬哪個(gè) feature map
levels = mapper(boxes)
......

# 再在所屬 feature map 進(jìn)行 roi_pooling
# 即 idx_in_level = torch.nonzero(levels == level).squeeze(1)
for level, (per_level_feature, scale) in enumerate(zip(x_filtered, scales)):
idx_in_level = torch.nonzero(levels == level).squeeze(1)
rois_per_level = rois[idx_in_level]

result_idx_in_level = roi_align(
per_level_feature, rois_per_level,
output_size=self.output_size,
spatial_scale=scale, sampling_ratio=self.sampling_ratio)

之后就獲得了所謂的 7x7 特征(在 FasterRCNN.__init__(...) 中設(shè)置了 output_size=7)。需要說明,原始 FasterRCNN 應(yīng)該是使用 roi_pooling,但是這里使用 roi_align 代替以提升檢測(cè)器性能。

對(duì)于 torchvision.ops.roi_align 函數(shù)輸入的參數(shù),分別為:

  • per_level_feature 代表 FPN 輸出的某一 feature_map
  • rois_per_level 為該特征 feature_map 對(duì)應(yīng)的所有 proposal boxes(之前計(jì)算 level得到)
  • output_size=7 代表輸出為 7x7
  • spatial_scale 代表特征 feature_map 相對(duì)輸入 image 的下采樣尺度(如 1/4,1/8,...)
  • sampling_ratio 為 roi_align 采樣率,有興趣的讀者請(qǐng)自行查閱 MaskRCNN 文章

接下來就是將特征轉(zhuǎn)為最后針對(duì) box 的類別信息(如人、貓、狗、車)和進(jìn)一步的框回歸信息。

class TwoMLPHead(nn.Module):

def __init__(self, in_channels, representation_size):
super(TwoMLPHead, self).__init__()

self.fc6 = nn.Linear(in_channels, representation_size)
self.fc7 = nn.Linear(representation_size, representation_size)

def forward(self, x):
x = x.flatten(start_dim=1)

x = F.relu(self.fc6(x))
x = F.relu(self.fc7(x))

return x


class FastRCNNPredictor(nn.Module):

def __init__(self, in_channels, num_classes):
super(FastRCNNPredictor, self).__init__()
self.cls_score = nn.Linear(in_channels, num_classes)
self.bbox_pred = nn.Linear(in_channels, num_classes * 4)

def forward(self, x):
if x.dim() == 4:
assert list(x.shape[2:]) == [1, 1]
x = x.flatten(start_dim=1)
scores = self.cls_score(x)
bbox_deltas = self.bbox_pred(x)

return scores, bbox_deltas

首先 TwoMLPHead 將 7x7 特征經(jīng)過兩個(gè)全連接層轉(zhuǎn)為 1024,然后 FastRCNNPredictor 將每個(gè) box 對(duì)應(yīng)的 1024 維特征轉(zhuǎn)為 cls_score 和 bbox_pred :

圖14

顯然 cls_score 后接 softmax 即為類別概率,可以確定 box 的類別;在確定類別后,在 bbox_pred 中對(duì)應(yīng)類別的 4個(gè)值即為第二次 bounding box regression 需要的4個(gè)偏移值。

簡單的說,帶有FPN的FasterRCNN網(wǎng)絡(luò)結(jié)構(gòu)可以用下圖表示:


圖15

關(guān)于訓(xùn)練

FasterRCNN模型在兩處地方有損失函數(shù):

  • 在 RegionProposalNetwork 類,需要判別 anchor 中是否包含目標(biāo)從而生成 proposals,這里需要計(jì)算 loss
  • 在 RoIHeads 類,對(duì) roi_pooling 后的全連接生成的 cls_score 和 bbox_pred 進(jìn)行訓(xùn)練,也需要計(jì)算 loss

首先來看 RegionProposalNetwork 類中的 assign_targets_to_anchors 函數(shù)。

def assign_targets_to_anchors(self, anchors, targets):
# type: (List[Tensor], List[Dict[str, Tensor]])
labels = []
matched_gt_boxes = []
for anchors_per_image, targets_per_image in zip(anchors, targets):
gt_boxes = targets_per_image["boxes"]

if gt_boxes.numel() == 0:
# Background image (negative example)
device = anchors_per_image.device
matched_gt_boxes_per_image = torch.zeros(anchors_per_image.shape, dtype=torch.float32, device=device)
labels_per_image = torch.zeros((anchors_per_image.shape[0],), dtype=torch.float32, device=device)
else:
match_quality_matrix = box_ops.box_iou(gt_boxes, anchors_per_image)
matched_idxs = self.proposal_matcher(match_quality_matrix)
# get the targets corresponding GT for each proposal
# NB: need to clamp the indices because we can have a single
# GT in the image, and matched_idxs can be -2, which goes
# out of bounds
matched_gt_boxes_per_image = gt_boxes[matched_idxs.clamp(min=0)]

labels_per_image = matched_idxs >= 0
labels_per_image = labels_per_image.to(dtype=torch.float32)
# Background (negative examples)
bg_indices = matched_idxs == self.proposal_matcher.BELOW_LOW_THRESHOLD
labels_per_image[bg_indices] = torch.tensor(0.0)

# discard indices that are between thresholds
inds_to_discard = matched_idxs == self.proposal_matcher.BETWEEN_THRESHOLDS
labels_per_image[inds_to_discard] = torch.tensor(-1.0)

labels.append(labels_per_image)
matched_gt_boxes.append(matched_gt_boxes_per_image)
return labels, matched_gt_boxes

當(dāng)圖像中沒有 gt_boxes 時(shí),設(shè)置所有 anchor 都為 background(即 label 為 0):

if gt_boxes.numel() == 0
# Background image (negative example)
device = anchors_per_image.device
matched_gt_boxes_per_image = torch.zeros(anchors_per_image.shape, dtype=torch.float32, device=device)
labels_per_image = torch.zeros((anchors_per_image.shape[0],), dtype=torch.float32, device=device)

當(dāng)圖像中有 gt_boxes 時(shí),計(jì)算 anchor 與 gt_box 的 IOU:

  • 選擇 IOU < 0.3 的 anchor 為 background,標(biāo)簽為 0
labels_per_image[bg_indices] = torch.tensor(0.0)
  • 選擇 IOU > 0.7 的 anchor 為 foreground,標(biāo)簽為 1
labels_per_image = matched_idxs >= 0
  • 忽略 0.3 < IOU < 0.7 的 anchor,不參與訓(xùn)練

從 FasterRCNN 類的 __init__ 函數(shù)默認(rèn)參數(shù)就可以清晰的看到這一點(diǎn):

rpn_fg_iou_thresh=0.7, rpn_bg_iou_thresh=0.3,

接著來看 RoIHeads 類中的 assign_targets_to_proposals 函數(shù)。

def assign_targets_to_proposals(self, proposals, gt_boxes, gt_labels):
# type: (List[Tensor], List[Tensor], List[Tensor])
matched_idxs = []
labels = []
for proposals_in_image, gt_boxes_in_image, gt_labels_in_image in zip(proposals, gt_boxes, gt_labels):

if gt_boxes_in_image.numel() == 0:
# Background image
device = proposals_in_image.device
clamped_matched_idxs_in_image = torch.zeros(
(proposals_in_image.shape[0],), dtype=torch.int64, device=device
)
labels_in_image = torch.zeros(
(proposals_in_image.shape[0],), dtype=torch.int64, device=device
)
else:
# set to self.box_similarity when https://github.com/pytorch/pytorch/issues/27495 lands
match_quality_matrix = box_ops.box_iou(gt_boxes_in_image, proposals_in_image)
matched_idxs_in_image = self.proposal_matcher(match_quality_matrix)

clamped_matched_idxs_in_image = matched_idxs_in_image.clamp(min=0)

labels_in_image = gt_labels_in_image[clamped_matched_idxs_in_image]
labels_in_image = labels_in_image.to(dtype=torch.int64)

# Label background (below the low threshold)
bg_inds = matched_idxs_in_image == self.proposal_matcher.BELOW_LOW_THRESHOLD
labels_in_image[bg_inds] = torch.tensor(0)

# Label ignore proposals (between low and high thresholds)
ignore_inds = matched_idxs_in_image == self.proposal_matcher.BETWEEN_THRESHOLDS
labels_in_image[ignore_inds] = torch.tensor(-1) # -1 is ignored by sampler

matched_idxs.append(clamped_matched_idxs_in_image)
labels.append(labels_in_image)
return matched_idxs, labels


與 assign_targets_to_anchors 不同,該函數(shù)設(shè)置:

box_fg_iou_thresh=0.5, box_bg_iou_thresh=0.5,
  • IOU > 0.5 的 proposal 為 foreground,標(biāo)簽為對(duì)應(yīng)的 class_id
labels_in_image = gt_labels_in_image[clamped_matched_idxs_in_image]

這里與上面不同:RegionProposalNetwork 只需要判斷 anchor 是否有目標(biāo),正類別為1;RoIHeads 需要判斷 proposal 的具體類別,所以正類別為具體的 class_id。

  • IOU < 0.5 的為 background,標(biāo)簽為 0
labels_in_image[bg_inds] = torch.tensor(0)

寫在最后

本文簡要的介紹了 torchvision 中的 FasterRCNN 實(shí)現(xiàn),并分析我認(rèn)為重要的知識(shí)點(diǎn)。寫這篇文章的目的是為閱讀代碼困難的小伙伴做個(gè)指引,鼓勵(lì)入門新手能夠多看看代碼實(shí)現(xiàn)。若要真正的理解模型(不被面試官問?。?,要是要看代碼!

創(chuàng)作不易,不想被白嫖,求點(diǎn)贊、關(guān)注、收藏(作者)三連!


推薦閱讀



添加極市小助手微信(ID : cvmart2),備注:姓名-學(xué)校/公司-研究方向-城市(如:小極-北大-目標(biāo)檢測(cè)-深圳),即可申請(qǐng)加入極市目標(biāo)檢測(cè)/圖像分割/工業(yè)檢測(cè)/人臉/醫(yī)學(xué)影像/3D/SLAM/自動(dòng)駕駛/超分辨率/姿態(tài)估計(jì)/ReID/GAN/圖像增強(qiáng)/OCR/視頻理解等技術(shù)交流群:月大咖直播分享、真實(shí)項(xiàng)目需求對(duì)接、求職內(nèi)推、算法競(jìng)賽、干貨資訊匯總、與?10000+來自港科大、北大、清華、中科院、CMU、騰訊、百度等名校名企視覺開發(fā)者互動(dòng)交流~

△長按添加極市小助手

△長按關(guān)注極市平臺(tái),獲取最新CV干貨

覺得有用麻煩給個(gè)在看啦~??
瀏覽 32
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 免费成人AV| 偷拍精品视频| 91AV无码| 在线无码av| 国产成人精品免费视频| 超碰97免费| 欧美视频一区| 大香煮伊在75| 免费无码国产在线| 亚洲日韩在线a成| 永久免费AV无码| 成人视频18+在线观看| 日韩精品一区二区在线观看| 大香蕉久久久久| 一区二区三区无码在线观看| 超碰中文字幕| 欧美综合视频在线观看| 曰曰操| 嫩BX区二区三区的区别| 午夜视频在线播放| 桃色AV| 色色天堂成人电影| 欧美大胆视频| 久久99免费视频| 风流老熟女一区二区三区| 精品国产免费无码久久噜噜噜AV | 思思在线视频| 日本有码中文字幕| 麻豆精品一区二区三区| 一级黄色A片| 婷婷综合五月| 五月丁香婷婷色| 3344在线观看免费下载视频 | 久久大陆| 性爱福利视频| 黄色视频免费网站| 亚洲婷婷精品国产成人| 日本无码视频在线观看毒| 国产无码一区二区三区| 日韩在线中文| 欧美亚洲天堂网| 男人午夜天堂| 亚洲中文字幕免费在线观看 | 亚洲午夜免费视频| 北条麻妃在线一区二区| 中文字幕在线无码视频| 日本人妻在线播放| 欧美黄色网视频| 日韩成人在线观看| 欧美黄色A片| 特极西西444WWW大胆无码 | 成人做爰A片AAA毛真人| 成人特级毛片全部免费播放| 91丨九色丨熟女丰满| 91爱在线| 精品一区二区三区四| 国产农村乱婬片A片AAA图片| 99久99| 超碰在线观看2407| 北条麻妃一区二区三区-免费免费高清观看 | 做爱网站免费| 人人妻人人爱人人操| 天天日天天拍| 欧美在线视频一区| 国产高清久久| 五月丁香视频在线观看| 淫香淫色天天影视| 艹美女视频| 伊人视频网| 777国产盗摄偷窥精品0000| 999久久精品| 黄色毛片,男人天堂| 日韩免费视频在线观看| 国产无码激情视频| 亚洲AV无码成人精品涩涩麻豆| 国产九九九九| 色五婷婷| 成人做爰100部免费网站| 天天做天天爽| 翔田千里无码破解| 亚洲精品中文字幕乱码三区91| 欧美一级爱| 国产99精品视频| 久久群交| 国产视频无码在线| 精品无码一区二区三区在线| 91三级片在线观看| 视频一区中文字幕| 婷婷网五月天| 国产一区二区在线播放| 中文无码AV在线| 免费高清无码视频在线观看| 水多多成人视频| 韩国一区二区在线观看| 亚洲黑人av| 免费高清无码视频在线观看| 尹人大香蕉网| 免费无码国产| 国产伦精品一级A片视频夜夜| 91精品少妇高潮一区二区三区不卡| 日本在线不卡视频| 亚洲天堂2016| 韩国一区二区在线观看| 大地资源中文第二页导读内容 | 日韩无码少妇| 免费久草视频| 国产农村妇女精品一二区| 免费视频A| 亚洲A片一区二区三区电影网| 成人黄A片免费| 精品人妻一区二区免费蜜桃| 国产性色AV| 中文字字幕在线| 免费色色| 九九热8| 最新中文字幕av| A级片免费| 成人性爱在线播放| 国产香蕉视频在线观看| 老妇槡BBBB槡BBBB槡| 91久久国产综合久久91精品网站| 亚洲午夜久久久久久久久红桃| 亚洲AV无码乱码国产精品蜜芽| 亚洲AV无码乱码国产精品蜜芽| 18禁网站禁片免费观看| 狠狠天天| 俺来也AV| 中文字幕视频在线| 国产天堂| 欧洲天堂在线视频网站| 成人免费区一区二区三区| 深爱激情综合| 国产精品夜夜爽3000| 西西444WWW无码视频软件功能介绍 | 东京热综合影院| 亚洲国产精品自在自线| 狠狠艹| 午夜AV在线播放| 西西4444www大胆无吗| 日韩国产成人在线| 亚洲精品麻豆| 熟女人妻一区二区| 三级片无码麻豆视频| 日本老熟妇| 日韩高清无码人妻| 麻豆国产精品| 99热这里只有精品99| 黄色大片免费看| 特级艺体西西444WWw| 天堂俺去俺来也www久久婷婷| 91久久精品视频| 日韩高清无码不卡| 丁香五月天视频| 黄色片网站| 久艹| 日本黄A级A片国产免费| 久久精品性爱| 亚洲国产成人视频| 天天拍天天射| 黄色在线免费观看网站| 熊猫成人网| 欧美性爱91| 婷婷天堂站| 黄色视频在线观看18| 五月天色色婷婷| 豆花无码视频一区二区| 日韩欧美国产一区二区| 26uuu国产| 久热在线精品视频| 四季AV一区二区夜夜嗨| 亚洲性爱中文字幕| 肏屄视频在线观看| 91视频成人版一区二区| 国产黄色片免费| 国产精品国产三级囯产普通话2 | 色婷久久| 九九综合网| 国产精品成人在线| 狠狠狠狠操| 日韩一级片在线观看| 无码精品黄色片| 亚洲色综合网| 水蜜桃成人网| 成人无码电影在线观看| 国产一级a一级a免费视频| 欧美午夜福利| 亚洲日韩一区| 欧美偷拍一区二区| 91中文无码| 亚洲AV无码成人精品区在线欢看 | 欧美BBWBBWBBWBBWBBwBBW| 内射免费网站| 影音先锋国产资源| 国产人人干| 欧美激情视频在线| 三级毛片网站| 亚洲无码av在线观看| 日韩三级片无码| www.国产精品| 亚洲日本中文字幕| 人人操人人摸人人射| 免费在线观看黄色网址| 高清无码免费不卡| 99在线精品视频免费观看20| 亚欧毛片| 国产欧美高清在线| 亚洲成人在线视频免费观看| 97色色婷婷| 久草香蕉视频| 亚洲免费观看视频| 三级片在线观看网站| 伊大香蕉在线| 插插插菊花综合网| 骚逼综合| 97视频在线| 久久久精品无码| 欧美午夜激情视频| 欧美老熟女18| 泄火熟妇2-ThePorn| 尤物Av| 1204手机看片| 色天使视频| 久久免费视频精品| 国产骚逼| 久久久aaa| 精品成人在线视频| 强伦轩一区二区三区四区| 中文字幕伊人| 亚洲成人a片| 天堂va欧美ⅴa亚洲va一夜| 国产精品久久久久无码| 国产在线不卡| 最美孕交vivoestv另类| 久久久久亚洲精品| 激情五月天黄色| 成人三级在线观看| 国产精品欧美综合在线| 日韩在线国产| 蜜臀网在线| 深夜无码| 好吊视频一区二区三区红桃视频you | 免费激情| AV无码精品| 精品一区二区三区四区五区六区七区八区九区 | 日本一区二区三| 91社区成人影院| 国产精品大全| 一道本在线观看| 国产免费激情视频| 国产一区二区三区免费观看| 丝袜人妻| 好吊视频一区二区三区| sm视频网站| 男人天堂大香蕉| 先锋影音AV资源网| 日韩欧美视频一区国产欧美在线| TheAV精尽人亡av| 欧美偷拍精品| 天天操天天看| 国产狼友| 日日骚av一区二区三区| 麻豆啪啪| 一区二区高清无码视频| 无码二区三区| 国产无码激情| 亚洲国产成人自拍| AⅤ在线观看| 中文字幕人妻互换av久久| 日韩精品| 日韩精品无码一区二区三区| 日本高清无码视频| 人妻日韩精品中文字幕| 激情五月天开心网| 亚洲国产天堂| 黄色视频| 亚洲毛片亚洲毛片亚洲毛片| 91人妻人人澡人人爽人人精| 一区二区三区国产| 久久久久久久久久久国产精品| 91AV无码| 在线看片a| 日韩在线国产| 臭小子晚上让你爽个够视频| 国产主播在线观看| 午夜精品久久久久久久91蜜桃| www.久久久久| 露脸偷拍AV2025| 午夜精品久久久久久久91蜜桃| 久久99嫩草熟妇人妻蜜臀| 欧美撒色逼撒| 亚洲AV秘无码不卡在线观看| 欧美AAA视频| 999久久精品| 亚洲日韩欧美一厂二区入| 操逼网站免费观看| 婷婷激情五月天丁香| 久久久久99精品成人片三人毛片 | 91精品久久久久久久久久久久| 黄色影片在线观看| 日本高清视频www| 欧美性猛交XXXX乱大交HD| 国产又爽又黄免费网站在线观看| 香蕉国产在线视频| 波多野结衣亚洲视频| 北条麻妃久久| 俺去也在线播放| 中文字幕2018第一页| 天天日毛片| 99视频在线精品| 午夜福利欧美| 大香蕉亚洲网| 久久99久久99久久99| 伊人网在线视频| 国产高清第一页| 亚洲黄色在线视频| 日韩中文字幕成人| 亚洲视频黄色| 91乱伦| 91精品婷婷国产综合久久蝌蚪 | 褒姒AV无玛| 中文无码观看| 日韩AV无码电影| 日韩日逼网站| www.6969成人片亚洲| 激情日逼| 成人免费A片喷| 久久免费精品| 手机AV在线观看| www.色五月| 级婬片AAAAAAA免费| 综合AV| 91最新在线播放| 黄色免费在线网站| v天堂在线| 免费看黄色片视频| 日韩性爱无码| 欧洲在线观看| 人人艹人人干| 中国最大成人网站| 五月婷婷六月香| 午夜无码鲁丝片午夜精品| 精品动漫3D一区二区三区免费版| 国产一区二区三区四区视频| 91插插插插| 国产人妖在线观看| 二级黄色视频| 亚洲一区二区三| 超碰91免费在线观看| 97久久人人| 欧美美女日逼视频| 亚洲最大福利视频| 暗呦罗莉精品一区二区| 牛牛精品一区| 精品无码一区二区| 午夜综合在线| 亚洲综合区| 亚洲成人AAAAA| 成人三级电影| 狼人色综合| 超碰乱伦| 一区二区经典| 亚洲视频一区二区三区| 成年人视频免费| 成人精品在线视频| 九一无码| 久草99| 欧美操逼在线观看| 91精品久久久久久| 在线亚洲一区| 久久国产精品波多野结衣AV| 男人V天堂| 精品无码视频| 在线www| 亚洲骚妇| 五月天三级片| 天天久久| 中国免费XXXX18| av福利电影在线| 日本久久精品18| 日本A片一级| 蜜臀网在线| 91精品国产闺蜜国产在线闺蜜| 特大妓女BBwBBWBBw| 四虎在线视频观看96| www.俺也去| 国产女同在线观看| 亚洲无码专区在线观看| 日韩最新高清无码| 你懂的在线观看| 色婷婷狠| 四川少妇搡bbbbb搡多人| 国产精品国产精品国产专区不52 | 色男人色天堂| 日韩中文AV| 黄色福利| 不卡日韩| 手机看片福利永久| 在线日韩av| 国产三级成人| 99久久久久久久| 久久综合大香蕉| 国产视频中文字幕| 亚洲高清视频免费| 柠檬福利第一导航| 亚洲美女操| 久久精品视频在线免费观看| 91视频18| 人妻无码中文字幕蜜桃| 亚洲高清免费视频| 波多野结衣一级| 国产激情艹逼| 黑人狂躁女人高潮视频| 亚洲色成人网站www永久四虎| 中文字幕乱码中文字乱码影响大吗| 国产激情网址| 91视频播放| 黄片免费高清| 无码av亚洲一区二区毛片公司| 中文字幕av一区二区| 老司机午夜电影| 91大鸡巴| 男女AV网站| 国产香蕉视频| 亚洲av动漫| 国产日韩在线播放| 99久久婷婷国产综合精品草原| 日韩欧美国产综合| 亚洲美女喷水视频| 丁香五月婷婷在线| 一区二区三区高清不卡| 成人日韩无码| 日韩无码少妇| 91国产视频在线播放| 俺也去网| 日韩三级精品| 91精品在线播放| 欧一美一色一伦一A片| 欧美日韩一区二区三区视频| 黄色视频网站免费在线观看| 亚洲www视频| 免费岛国av大片| 亚洲国产精品成人久久蜜臀| 水蜜桃视频在线播放| 欧美成人精品欧美一级乱黄| 日韩欧美成人网站| 自拍视频在线| 黄色一级录像| 91精品国产一区二区| 91成人篇| 强辱丰满人妻HD中文字幕| 欧美一区免费| 亚洲日本三级片| 无码操逼视频| 久久人体视频| 婷婷色777777| 中文字幕不卡在线| 精品成人在线观看| 91免费在线视频| 日韩激情网站| 你懂得视频在线观看| 午夜性爱福利| 中文字幕永久在线观看| 蝌蚪窝在线视频观看| 成人在线网| 亚洲国产毛片| 天天日天天噜| 久久精品免费| 国产成人精品123区免费视频 | 国产精品久久久久无码| 亚洲日韩在线a成| 国产成人三级片| 国产精品久久久久久婷婷天堂| 麻豆精东一区二区欧美国产| 五月天伊人| 一级A片黄色| 激情日逼| 五月开心婷婷| 高清无码专区| 99久久综合国产精品二区| www黄色视频| 亚洲国产精品成人综合| 视频一二三区| 日韩精品| 三级麻豆| 韩国一区二区三区| 亚洲小电影在线观看| 西西人体WW大胆无码| 日韩中文字幕无码中字字幕| 国产高清无码网站| 麻豆久久| 日本性爱无码| 青青色综合| 欧美偷拍一区二区| 成人三级片网站| 国产麻豆精品成人毛片| 俺去也俺去啦| 北条麻妃在线观看香蕉| 淫香淫色综合网| 日产电影一区二区三区| 亚洲操逼图片| 蜜臀久久99精品久久久巴士| 靠逼免费视频| 粉嫩av在线| 中文字幕无码乱伦| 波多野结衣不卡| 国产插逼视频| 男女AV网站| 亚洲中文视频免费| 黄色网页在线观看| 无码一区二| 精品国产毛片| 久操香蕉| 北条麻妃无码| 亚洲无码一二三区| 亚洲色无码| 久久成人影音先锋| 极品美女扒开粉嫩小泬高潮一| 色婷婷一区二区| 欧亚免费视频| 91丨PORN首页| 欧美大吊在线| 高清国产AV| 九色PORNY蝌蚪自拍视频 | 久久XXX| 日韩三级片在线播放| 久久久无码精品亚洲日韩男男| 欧美日韩小视频| 成年人免费视频网站| 麻豆AV免费看| 日本中文字幕乱伦| 日韩一级无码| 亚洲无码视频一区| 中国黄色学生妹一级片| 久久精彩免费视频| 西西www444无码大胆| 99爱视频| 日韩高清中文字幕| 91色秘乱码一区二区| 精品无码AV一区二区三区| 国产无码一| 91在线无码精品秘国产色多多| 69AV在线播放| 亚洲无码免费播放| 伊大香蕉在线| 亚洲去干网| 刘玥91精品一区二区三区| www.99视频| 俺来俺也去| 一级a片在线播放| 日韩免费A| www.久草| 91在线超碰| 国产精品s色| www.日韩av| 51成人精品午夜福利| b逼一区| 六月激情| 大香蕉伊| 亚洲日韩欧美一区二区天天天| 日本黄色三级| 91艹艹| 中文字幕午夜福利| 亚洲黄色成人网站| 中文字幕亚洲综合| 男女国产网站| 欧美性猛交XXXX乱大交HD| 996热re视频精品视频| 可以免费观看的AV| 伊人网视频| 无码高清一区二区| 亚洲日韩影院| 香蕉AV777XXX色综合一区| 欧美h在线观看| 久久影院av| 久久久电影| 超碰免费在线| 夜夜骚av一区二区三区| 人妻无码专区| 国产AV日韩AV| 草榴在线视频| 无码视频中文字幕| 人成视频免费观看| 午夜av在线播放| 亚洲免费无码| 亚洲AV成人无码久久精品麻豆| 亚洲AV电影网| 无码人妻在线| 九九综合网| 囯产精品一区二区三区AV做线| 91色人妻| 91亚洲在线观看| 欧美日逼小视频| 久久精品在线视频| 婷婷丁香激情五月天| 日韩无码黄片| 看黄片网站| 成人免费视频网| 久草视频这里只有精品| 成人性爱视频在线| 亚洲第一a| 夜夜狠狠躁日日| 性感欧美美女| 国产综合久久| 亚洲精品在线视频| 亚洲高清AV| 免费A网站| 久久新视频| 天天天天天天天干| 水蜜桃成人网| 污视频网站免费观看| 日韩最新无码发布| 婷婷五月天在线电影| 插插插插网| 日韩在线中文字幕| 搡BBBB搡BBB搡Bb| 伊人成色| 亚洲天堂视频在线| 黄片在线免费观看视频| 色婷婷AV一区二区三区之e本道| 亚洲无码入口| 国产三级免费观看| 欧美精品乱码99久久蜜桃| 色草视频| 围内精品久久久久久久久白丝制服 | 丁香AV| 中文字幕婷婷五月天| 国产在线观看mv免费全集电视剧大全| 日本麻豆| 免费的黄色录像| 中文字幕在线看成人电影| 人人操超碰| 黄色福利| 国产精品一区在线观看| 国产成人精品免费视频| 波多野成人无码精品69| 大香蕉伊人影院| 久久穴| 精品乱子伦一区二区三区免费播成| 成人亚洲在线| 国产免费高清视频| 怡春院AV| 亚洲成人内射| 91人妻人人澡人人爽人妻| 色小说在线| 99国产精品久久久久久久成人| 超碰国产97| 婷婷五月天电影网| 另类TS人妖一区二区三区| 亚洲熟妇在线观看| 偷拍亚洲综合| 日韩视频在线观看一区| 天天夜夜有| 五月丁香激情四射| 影音先锋婷婷| 在线观看亚洲无码视频| 宅男噜| 三级无码视频在线观看| 免费A级| 伊人综合干| 国产精品操逼网站| 日韩AV免费在线播放| 亚洲精品系列| 成人免费高清| 欧美日韩色视频| 国产精品人妻无码一区牛牛影视| 中国操逼电影| 香蕉91| 欧美一区二区三区系列电影| 影音先锋蜜桃| 久久久久久少妇| 精品视频在线看| 日本熟妇无码一区二区| 韩国无码视频在线观看| 国产免费性爱| 日欧内射| 亚洲一级二级三级片| 无码欧美成人AAAA三区在线| 天堂色色| 91丨PORNY丨对白| 日产电影一区二区三区| 在线看一区| 亚洲午夜在线| 中文字幕不卡无码| 免费av在线播放| 午夜性爱剧场| 五十路无码| 中文字幕日韩在线观看| 国产人妻| 中文字幕+乱码+中文乱码91| a片在线免费观看| 日韩欧美国产| 女人的天堂AV在线观看| 一级黄片学生妹| 三级国产网站| 十八禁网站在线观看| 男女免费av| yjizz国产| 一级a一级a免费观看免免黄‘/| 骚骚网| 日韩一区二区三区精品| 日韩人妻精品中文字幕专区不卡| 十八禁视频在线观看网站.www | 日本一级黄色电影网| 黄色网页免费观看| 2026AV天堂网| 色婷婷国产精品视频| 午夜91| 97超碰伊人| 91无码人妻一区二区成人AⅤ| 超碰A片| 蜜桃AV一区二区三区| 伊人狠狠| 精品国产乱子伦一区二区三区最新章 | 成人美女视频| 成人黄色性爱视频| 中文无码一区二区三区| 欧美午夜福利电影| 91亚洲视频在线观看| 日韩爱爱| 久久久久久久| 欧美在线国产| 成人影片在线观看18| 精品一区二区三区四区学生| 日欧内射| 亚洲第一视频在线观看| 欧美在线成人视频| 91精品国产麻豆国产自产在线| 99成人国产精品视频| 玉米地一级婬片A片| 亚洲无码自拍| 国产喷水ThePorn| 日韩精彩视频| 亚洲videos| 亚洲A∨无码无在线观看| 日韩免费黄色视频| 国产av不卡| 在线色片| 一级免费视频| 电影豹妹香港版| 佐山爱人妻无码蜜桃| 狠狠se| 成人黄色录像| 亚洲无码91| 自拍三级片| 久久波多野结衣| 99热超碰| 国产麻豆电影在线观看| 国产青青操| 大香蕉伊人色| 乌克兰性爱视频| 97福利在线| 亚洲精品内射| 色色97| 在线成人视频网站大香蕉在线网站| 亚洲激情国产| 秋霞福利影院| 99Re66精品免费视频| 中出在线| 青青草手机视频| 最新精品视频| 手机在线一区| 1000部毛片A片免费视频| 国产理论片在线观看| 国产黄色免费乱伦片| 在线视频你懂得| 亚洲图片激情乱伦小说| 巨乳一区二区三区| 国产精品久久久久久99| R四虎18| 亚洲欧美日韩综合| 特级黄色毛片| 国产操逼免费| 黄色搞逼视频| 大香蕉五月丁香| 骚五月| www欧美| 丁香六月婷婷| 北条麻妃二区三区| 草草影院CCYYCOM屁屁影院合集限制影院 | 欧美footjob| 日韩蜜桃视频| 国产91视频在线观看| 中文字幕日韩高清| 国产无码激情视频| 四川少妇BBB凸凸凸BBB安慰我 | 日韩一级片视频| 懂色AV| 黑人粗大无码| 最近日本中文字幕中文翻译歌词 | 中文字幕第5页| 欧美日韩国产不卡视频| 综合黄色| 成人免费精品视频| 天堂在线无码| 天堂综合网| 国产91综合一区在线观看| 国产18禁网站| 久久久久久久免费| 一区二区三区四区免费| 日韩免费在线视频| 91人妻一区二区三区| 中文字幕av久久爽爽| 四虎在线观看一区网址| 麻豆av在线| 丰满人妻一区二区免费看| 美女网站永久免费观看| 蜜臀AV在线播放| 波多野结衣在线无码| 亚洲天堂在线视频| 久久亚洲AV无码午夜麻豆| 性爱视频久久| 日本在线小视频| 水蜜桃在线视频| 大香蕉在线99| 性综合网| 国产精品成人无码a无码| 伊人网在线播放| 狼友视频免费观看| 国产午夜精品一区二区三区牛牛 | 高清无码毛片| 日韩成人无码特集| 99亚洲天堂| 日韩欧美中文| 好吊一区二区三区| 黄色视频在线观看大全| 上海熟妇搡BBBB搡BBBB| 特黄aaaaaaaa真人毛片| 阿宾MD0165麻豆沈娜娜| 欧一美一婬一伦一区二区三区| 91亚洲国产成人久久精品麻豆| 91网站在线看| 国产欧美综合视频| 中文字幕AV在线免费观看| 闺蜜av| 亚洲黄色影院| 中文字幕69| 内射熟妇| 欧美黑吊大战白妞欧美大片| 日韩视频免费观看高清完整版在线观| 天天干,天天日| 99精品热视频| 日韩无码国产精品| 调教人妻视频| 中文字幕高清无码在线| 黃色一级A一片人与| 影音先锋中文字幕资源| 无码熟妇人妻无码AV在线天堂| 午夜成人鲁丝片午夜精品| 国产视频导航| 日韩中文字幕区| 91视频在线网站| 国产成人免费在线观看| www99国产| 国产免费内射| 中文字幕AV播放| 网络自拍亚洲激情| 日韩在线91| 国产又爽又黄免费网站在线看| 国产xxxx| 久久久久黄| 男人天堂视频在线观看| 欧美福利导航| 综合网在线| 久久国产99| 婷婷色色五月天| 国产AV无码影院| 十八毛片| 国产在线观看免费视频| 大鸡巴操小逼视频| 97国产高清| 亚洲黄色成人网站| 中文字幕成人免费视频| 成年人视频在线免费观看| 天堂视频在线| 黄色片一级片| 99热3| 香蕉久久国产AV一区二区| 久久久久亚洲AV无码成人片 | 欧美日韩一区二区三区视频| 日韩乱伦中文字幕| 毛片在线视频| 国产操逼图片| 西西444WWW无码精品| 国产你懂的| 人人操狠狠操| 成人性爱av| 九九九成人视频| 天天色天| 91爱爱视频| 成人黄色导航| 在线观看黄色网| 人妻无码中文字幕免费视频蜜桃| 色综合99久久久无码国产精品| 性饥渴熟妇乱子伦| 久久久国产探花视频| 亚洲AV成人电影| 亚洲成人性爱网| 精品视频久| 日产精品久久久久| 国产一级在线|