超详细解读Faster R-CNN-FPN

0. 一些啰嗦

2021年了,竟然还有人写关于Faster R-CNN的文章?我的原因主要有两点:

  1. 根据我们在实际项目和比赛中的经验,基于RoIAlign和FPN的Faster R-CNN(后面简称Faster R-CNN-FPN)是一个表现很强的基线,有必要充分了解它的思想和细节;

  2. 客观来说,相比单阶段、anchor free和基于transformer的检测方法,Faster R-CNN-FPN是一个细节很繁琐的方法,即使复现过一遍,时间长了很多细节也会忘记,而网上详细介绍该方法的文章较少,要了解方法的每个细节则必须阅读涉及Faster R-CNN-FPN演进的多篇论文或者源码。因此,很有必要用文字的方式记录Faster R-CNN-FPN的关键思想和细节,便于日后翻阅。

1. 关键思想

1.1 概述

我们先从全局上了解Faster R-CNN-FPN,然后再关注其中涉及的细节。下面是Faster R-CNN-FPN的网络框架图(或称为tensor流动图)。

众所周知,Faster R-CNN-FPN(主要是Faster R-CNN)是个两阶段的对象检测方法,主要由两部分网络组成,RPN和Fast R-CNN。

RPN的作用是以bouding box(后简称为box)的方式预测出图片中对象可能的位置,并过滤掉图片中绝大部分的背景区域,目标是达到尽量召回图像中感兴趣的对象,预测box尽量能够与实际对象的box贴合,并且保证一定的预测精度(Precision)。另外,RPN并不需要指出预测的box中对象具体的类别,RPN预测的box称为RoI(Region of Interest),由于是以box的方式输出,所以后面我们统一将其称为proposal box。

Fast R-CNN则是在FPN预测的proposal box基础上进一步预测box中对象具体的类别,并对proposal box进行微调,使得最终预测的box尽量贴合目标对象。大致的做法是根据RPN预测的proposal box,从原图backbone的feature map上通过RoIPooling或RoIAlign(Faster R-CNN-FPN使用RoIAlign)提取每个proposal box对应区域的feature map,在这些区域feature map上进一步预测box的类别和相对proposal box的偏移量(微调)。另外,RPN和Fast R-CNN共用同一个backbone网络提取图像的feature map,大大减少了推理耗时。

从上面的介绍可以看出,RPN和Fast R-CNN的配合作用其实可以理解为一种注意力机制,先大致确定目标在视野中的位置,然后再锁定目标仔细观察,确定目标的类别和更加精确的位置,简单来说就是look twice,相比单阶段的look once,当然是比较耗时的,但也换来了更好的效果(虽然很多单阶段方法号称已经获得相当或好于两阶段的效果)。

下面以Faster R-CNN-FPN发展顺序的汇总介绍每个改进的核心思想。

1.1.1 Fast R-CNN

在R-CNN中,CNN只被用来作为特征抽取,后接SVM和线性回归模型分别用于分类和box修正回归。在此基础上,Fast R-CNN直接对原输入图进行特征抽取,然后在整张图片的特征图上分别对每个RoI使用RoIPooling提取(后面会介绍RoIPooling的原理)特定长度的特征向量(论文中空降尺寸为7*7),去掉SVM和线性回归模型,在特征向量上直接使用若干FC层进行回归,然后分别使用两个FC分支预测RoI相关的类别和box,从而显著提升速度和预测效果。 整体框架图如下:

1.1.2 RPN

在Fast RCNN的基础上进一步优化,用CNN网络代替Fast R-CNN中的region proposal模块(使用传统Selective Search方法),从而实现了全神经网络的检测方法,在召回和速度上都超过了传统的Selective Search。作者将提供proposal region的网络称为RPN(Region Proposal Network),与检测网络Fast RCNN共享同一backbone,大大缩减了推理速度。

RPN在backbone产生的feature map(图中的conv feature map)之上执行n*n的滑窗操作,每个滑窗范围内的feature map会被映射为多个proposal box(图中的reg layer分支)以及每个box对应是否存在对象的类别信息(图中的cls layer分支)。由于CNN天然就是滑窗操作,所以RPN使用CNN作为窗口内特征的提取器(对应图中的intermediate layer,后面简称为“新增CNN层”),窗口大小n=3,将feature map映射为较低维的feature map以节省计算量(论文中为256)。虽然只使用了3*3的卷积,但是在原图上的有效的感受野还是很大的,感受野大小不等于网络的降采样率,对于VGG网络,降采样率为16,但是感受野为228像素。类似于Fast-RCNN,为了分别得到box和box对应的类别(此处类别只是表示有没有目标,不识别具体类别),CNN操作之后会分为两个子网络,它们的输入都是新增CNN层输出的feature map,一个子网络负责box回归,一个负责类别回归。由于新增CNN层产生的feature map的每个空间位置的特征(包括通道方向,shape为1*1*256)都被用来预测映射前窗口对应位置是否存在对象(类别)和对象的box,那么使用1*1的CNN进行计算正合适(等效于FC层),这便是RPN的做法。综上所述,所有滑窗位置共享一个新增CNN层和后续的分类和box回归分支网络。下图是RPN在一个窗口位置上执行计算的原理示意。

由于滑窗操作是通过正方形的CNN卷积实现的,为了训练网络适应不同长宽比和尺寸的对象,RPN引入了anchor box的概念。每个滑窗位置会预置k个anchor box,每个anchor box的位置便是滑窗的中心点,k个anchor box的长宽比和尺寸不同,作者使用了9种,分别是长宽比为1:21:12:1,尺寸为128*128, 256*256512*512的9种不同组合。分类分支和box回归分支会将新增CNN层输出的feature map的每个空间位置的tensor(shape为1*1*256)映射为k个box和与之对应的类别,假设每个位置的anchor box数量为k(如前所述,k=9),则分类分支输出的特征向量为2k(两个类别),box回归分支输出为4k(4为box信息,box中心点x坐标、box中心点y坐标、box宽w和box高h)。box分支预测的位置(x,y,w,h)都是相对anchor box的偏移量。从功能上来看,anchor box的作用有点类似于提供给Fast RCNN的propsal box的作用,也表示目标可能出现的位置box,但是anchor box是均匀采样的,而proposal box是通过特征抽取(或包含训练)回归得到的。由此可以看出,anchor box与预测的box是一一对应的。从后文将会了解到,通过anchor box与gt box的IoU的关系,可以确定每个预测box的正负样本类别。通过监督的方式让特定的box负责特定位置、特定尺寸和特定长宽比的对象,模型就学会了拟合不同尺寸和大小的对象。另外,由于预测的box是相对anchor box的偏移量,而anchor box是均匀分布在feature map上的,只有距离和尺寸与gt box接近(IoU较大)的anchor box对应的预测box才会与gt box计算损失,这大大简化了训练,不然会有大量的预测box与gt box计算损失,尤其是在训练初始阶段,当一切都是瞎猜的时候。

1.1.3 Faster R-CNN-ResNet

在Faster RCNN基础上,将backbone替换为ResNet50或ResNet101,涉及部分细节的改动,我们放在本文的细节部分进行描述。

1.1.4 FPN

在Faster RCNN-ResNet基础上,引入FPN(特征金字塔网络)模块,利用CNN网络天然的特征金字塔特点,模拟图像金字塔功能,使得RPN和Fast RCNN可以在多个尺度级别(scale level)的feature map上分别预测不同尺寸的对象,大大提高了Faster RCNN的检测能力。相比图像金字塔大大节省了推理时间。原理如下图所示:

从上图中可以看出,FPN并不是简单地使用backbone的多个CNN层输出的feature map进行box回归和分类,而是将不同层的feature map进行了top-down和lateral connection形式的融合后使用。这样便将CNN网络前向传播(bottom-up)产生的深层语义低分辨率特征与浅层的浅语义高分辨率的特征进行融合,从而弥补低层特征语义抽象不足的问题,类似增加上下文信息。其中,top-down过程只是简单地使用最近邻插值将低分辨率的feature map上采样到即将与之融合的下层feature map相同的尺寸(尺寸上采样到2倍),lateral connection则是先将低层的feature map使用1*1的卷积缩放为即将与之融合的上层feature map相同的通道数(减少计算量),然后执行像素级相加。融合后的feature map不仅会用于预测,还会继续沿着top-down方向向下传播用于下层的特征融合,直到最后一层。

1.1.5 RoIAlign

mask R-CNN提出的RoI Align缓解了RoIPooling的缺陷,能够显著提升小目标物体的检测能力。网上介绍RoIPooling和RoIAlign的文章很多,此处不再赘述,推荐阅读个人觉得比较好的两篇博客:RoIPoolingRoIAlign

此处稍微啰嗦下个人对RoIPooling的思考: 为什么RoIPooling不使用自适应的池化操作,即根据输入的feature map的尺寸和希望输出的feature map尺寸,自动调整池化窗口的大小和步长以计算想要尺寸的feature map,类似于自适应池化操作,而不是将输入的feature map划分成均匀的小区域(bins,论文中划分为7*7个bins),然后每个小区域中分别计算MaxPooling。不管计算上是否高效,至少这种做法在输入的feature map尺寸(比如2*2)小于期望的输出feature map尺寸(比如7*7)时会失效,因为在3*3的feature map上如果不使用padding的话是无法得到7*7的特征的,而使用padding又是很低效的操作,因为要扩展局部feature map的尺寸,而使用划分bins的方法,即使输出的feature map尺寸远小于要输出的feature map尺寸,也仅仅是在同一位置采样多次而已。

1.2 anchor的思想

本人之前介绍YOLOv3的文章也介绍过anchor box的作用,再加上本文1.1.2节中的介绍应该比较全面了,不再赘述。

2.实现细节

此处的绝大部分细节来自论文,论文中未提及的部分,主要参考了mmdetection中的实现

2.1 网络结构

整个模型的网络结构可以划分为四个部分,分别为backbone、FPN、RPN head和Fast RCNN head。

  1. backbone与Faster RCNN-ResNet相同,为ResNet系列(论文中实验时的网络);RPN和Fast RCNN使用的feature map原始多尺度feature map分别来自ResNet中的C2、C3、C4、C5阶段,即分别来自conv2_x、conv3_x、conv4_x和conv5_x阶段的输出。ResNet系列的结构如下表所示。
  1. FPN的结构如1.1.4节的描述,次上层feature map(对应为P5,见2.2中的FPN部分描述)的产生是通过在C5上执行一个1*1*256的卷积得到,lateral connection的卷积核大小为1*1*256。输出的feature map是在融合后(逐像素相加)的基础上再执行3*3卷积得到。具体的示例如下图,其中Up表示最近邻上采样,Down为MaxPooling操作 降采样 ,图中给出了每个feature map的维度,B为batch size,H和W分别为网络输入的图片的高和宽。
  1. RPN head操作是在多个尺寸不同的feature map上执行滑窗操作,滑窗由3*3的卷积核实现,之后直接连接两个1*1卷积分支),所有feature map共用同一RPN head。具体如下图所示,图中也给出了每个feature map的维度,B为batch size,k为每个滑窗位置的anchor box数量,H和W分别为网络输入的图片的高和宽。
  1. Fast RCNN head不同于Faster RCNN-ResNet的Fast RCNN head,由于FPN直接使用了ResNet backbone的C5 feature map,所以conv5_x被挪回backbone中,Fast RCNN head直接在不同层的feature map上执行RoIPooling或RoIAlign抽取7*7的feature map,依次输入给两个FC层(后接ReLU),最后两个FC分支(后接ReLU),然后输入给类别和box回归的输出层。具体如下图所示,图中也给出了每个feature map的维度,B为batch size,C为类别数量,H和W分别为网络输入的图片的高和宽。

2.2 输入/输出

1.backbone: 原图短边被resize到800像素,这里值得注意的是,如此resize后一个batch内的每张图片的大小很有可能并不一致,所以还无法合并为一个输入矩阵,普遍的做法是将batch内的每张图片的左上角对齐,然后计算resize后batch内所有图片的最大宽和高,最后按照最大宽或高分别对每张图片的宽或高进行0值padding;输出为4个不同尺寸的feature map(C2、C3、C4、C5)。

2.FPN: ResNet backbone产生的4个不同尺寸的feature map(C2、C3、C4、C5)作为输入,输出5个不同尺寸的feature map(P2、P3、P4、P5、P6),P6是对P5进行2倍降采样得到,每个feature map的通道数为固定的256;使用P6的原因是为了预测更大尺寸的对象。

3.RPN:输入为FPN产生的feature map(P2、P3、P4、P5、P6);由于RPN是在5个输入feature map上进行独立的预测,则每个feature map都会输出 proposal box,因此不可能将所有的proposal box都提供给Fast R-CNN,这里的做法是对每个feature map上产生的proposal box按类别概率进行排序(每个feature map上的proposal box独立进行),然后选择前k个proposal box, 5个feature map一共会 产生5*k个proposal box,训练时k=2000,推理时k=1000。最后,将所有的5*k个proposal box合并后统一进行NMS(IoU threshold=0.7)去掉冗余的box,最后选择前m个输出给Fast R-CNN,训练和测试时m都取1000。

训练时将gt box通过下面的公式转换为相对anchor box的偏移值,与网络的预测计算loss,至于将每个gt与具体的哪个anchor box计算偏移,则需要根据2.3.1节中的正负样本方法来确定。测试时将预测的box通过该公式中的逆运算计算出当前box相对原图的位置和大小,x^*y^*, w^*h^*指相对全图的box中心点坐标以及宽和高,x_ay_a, w_ah_a指每个anchor相对全图的box中心点坐标以及宽和高。由此可以看出,box回归分支直接预测的便是相对anchor的偏移值,即公式中的t^*_xt^*_yt^*_wt^*_h

以上提到的2000和1000是作为Fast R-CNN的输入proposal box,在训练时参与RPN loss计算的anchor boxs数量为256个,正负样本数量为1:1,正样本不足128的用负样本补足。这里的256是从所有feature map中的anchor box中选择的,并非每个feature map都独立取得256个正负样本。这也是合理的,因为每个gt box由于尺寸的原因,几乎不可能与所有feature map上的anchor box的IoU都大于一定的阈值(原因参考2.3.1节)。注意选择前并未进行NMS处理,而是直接根据2.3.1节中确定正负样本的方式确定每个预测box正负类别,然后分别在正样本中随机选择128个正样本,在负样本中随机选择128个负样本。

4.Fast R-CNN:输入为FPN产生的前4个feature map和RPN输出的proposal box,4个feature map为P2、P3、P4、P5,与backbone对应,不使用P6。那么,如何确定在哪个feature map上执行每个proposal box对应的RoIAlign操作并得到7*7大大小的feature map呢?论文中的做法是通过下面的公式将特定尺寸的proposal box与FPN产生的4个feature map中尺寸最适合的对应起来,即让感受野更接近对象尺寸的feature map预测该对象 ,其中224为backbone在ImageNet上预训练的尺寸,w和h为proposal box的长和宽,k表示适合尺寸为w和h的propsal box的feature map的位置,即4个feature map为P2、P3、P4、P5的下标,k_0为proposal box大致为224*224时对应feature map位置值(k_0=4),表示proposal box大致为224*224时在P4上执行RoIAlign,小于224*224时,在P2或P3上执行,大于则在P5上。

网络都会针对每个RoI会输出一个类别概率分布(包括背景类别)和一个相对RoI box的长度为4的box偏移向量。概率分支由softmax激活函数得到。与RPN的类似,训练时,如2.4.2节loss计算中所述,会将gt box通过下面的公式转换为相对proposal box(前提是该RoI是正样本)的偏移量,然后使用loss计算公式直接与预测的相对偏移量进行loss计算;测试时,会通过下列公式的逆运算将偏移值换算回相对原图的位置box,然后使用NMS去掉冗余的box,最终输出。

训练时,通过2.3.2中的方式确定每个proposal box属于正样本或负样本后,随机选择512个样本,其中正负比例为1:3进行loss计算,正样本不足的由负样本补足。

2.3 正负样本

2.3.1 RPN的正负样本

在RPN中,由于每个feature map的每个滑窗位置上的张量(3*3*C维张量,C为feature map的通道数)会被用来预测k个box和每个box对应的类别概率,那么具体哪个box才能参与gt box的损失计算(包括类别和box回归损失)?这便需要在所有预测的box中确定正负样本,因为一个anchor对应一个预测的box和类别,那么确定预测的box是正例还是负例等价于确定anchor box的是正例还是反例。为了便于训练,RPN中使用双IoU阈值的方式确定正负样本,与gt box的IoU为最大或者大于0.7的anchor box被设置为正样本,这会导致一个gt box与多个预测box计算损失,即允许多个box预测同一对象,与gt box的IoU小于0.3的anchor box被设置为负样本,其余的忽略掉,即不参与loss计算。在此基础上,如2.2节中所述,会对正负样本进行随机采样,总数为256,其他不参与损失函数计算。

2.3.2 Fast RCNN的正负样本

与gt box的IoU大于0.5的proposal box作为正样本,注意,是将proposal box与gt box计算IoU,Fast-RCNN中的proposal box的作用与anchor box有些类似,即确定正负样本和预测的box 都是针对它们的偏移值 ,其余IoU在[0.1, 0.5)之间的作为负样本,低于0.1的作为难例挖掘时的启发式样本(mmdetection中的做法是单阈值方式,与gt box的IoU大于0.5的proposal box作为正样本,小于的都是负样本)。

2.4 损失函数

Faster R-CNN中是以分步的方式联合训练RPN和Fast R-CNN,大致的过程为:

  1. backbone使用ImageNet预训练模型初始化后训练RPN,新增的Head 卷积层随机初始化;

  2. 使用第1步RPN产生的proposal box, backbone使用ImageNet预训练模型初始化后训练Fast R-CNN,新增的Head 全连接层随机初始化;

  3. 使用第2步训练的模型初始化RPN的backbone,并且固定backbone参数不进行训练,对RPN新增的Head 卷积层进行微调,这一步开始RPN和Fast R-CNN共享同一backbone参数;

  4. 最后也固定Fast R-CNN的backbone,微调Fast R-CNN新增的Head全连接层。

但在mmdetection中,已经将RPN和Fast R-CNN的loss进行权重加和,从而进行联合训练,训练流程简化很多,且能够达到相同的效果。

2.4.1 RPN的损失函数

确定了每个预测box或anchor box的正负类别后,便可以计算损失函数了,类似于Fast RCNN的做法,只有正样本的box才会参与box损失计算,损失函数如下:

L_{cls}为类别损失为类别损失函数,使用交叉熵损失,L_{reg}为box回归损失,使用smooth L1损失,论文中平衡因子lambda为10。p_i^*表示第i个anchor box对应的gt 类别(背景为0,对象为1),t_i^*为gt box相对anchor box的偏移量(如果该anchor box被确定为正样本),通过下面的公式计算得到,p_i^*L_{reg}(t_i, t_i^*)即表示只有p_i=1,即为正样本时才会计算box的损失。

2.4.2 Fast R-CNN的损失函数

Fast R-CNN的loss类似于RPN,只有proposal box为非背景类别(正样本)时才计算box损失,L_{cls}为类别损失,L_{loc}为box损失,[u>=1]表示proposal box的类别>=1类别=0时表示背景(通过2.3.2的方式确定proposal box的类别)。\lambda为平衡因子,作者所有实验中\lambda=1。为了防止box回归的L2 loss放大噪声(异常loss)从而影响训练,作者将L2 loss修改为smooth_{L1} loss,当box尺寸的差异较大时使用L1 loss,抑制异常值对梯度的贡献。

其中v是通过下面的公式将gt box(G_x, G_y, G_w, G_h)转换得到,其中,(P_x, P_y, P_w, P_h)为proposal box的在原图中的中心点坐标和宽与高。

2.5 Anchor box设计

在Faster R-CNN和Faster R-CNN-ResNet中,由于RPN只是在单尺寸的feature map上进行滑窗,为了缓解多尺寸的问题,每个滑窗位置会设计多个尺寸的anchor,但是在Faster R-CNN-FPN中使用了FPN,则天然就具有了适应对象多尺寸的问题,因此不用再为每个滑窗设计多个尺寸的anchor。即在Faster RCNN-FPN中,为每种尺寸feature map上的滑窗只设计了单一尺寸多种长宽比的anchor,长宽比有1:21:12:1,不同feature map上anchor的尺寸为:32*32, 64*64, 128*128, 256*256512*512,依次对应P2、P3、P4、P5和P6。

2.6 训练细节

COCO上的训练细节:RPN的weight decay为0.0001,SGD的mometum=0.9,初始学习率为0.002,学习率调整使用step decay方式。

3. 参考文献

  1. RoIPooling: https://towardsdatascience.com/understanding-region-of-interest-part-1-roi-pooling-e4f5dd65bb44

  2. RoIAlign: https://towardsdatascience.com/understanding-region-of-interest-part-2-roi-align-and-roi-warp-f795196fc193

  3. Fast R-CNN

  4. Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

  5. Feature Pyramid Networks for Object Detection

  6. Mask R-CNN

  7. Deep residual learning for image recognition

  8. mmdetection: https://github.com/open-mmlab/mmdetection

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容