高级UI<第十八篇>:图像处理之颜色矩阵

图像的处理大致有两种:

  • ColorMatrix:颜色矩阵
  • Matrix:变换矩阵

本章主要讲解颜色矩阵。

Paint有个方法可以设置颜色滤镜

ColorFilter setColorFilter(ColorFilter filter)

传递一个ColorFilter参数,ColorFilter有三个子类:LightingColorFilterPorterDuffColorFilterColorMatrixColorFilter

LightingColorFilter: 用来模拟简单的光照效果。
PorterDuffColorFilter: 结合Xfermode将源对象和目标对象进行合成。
ColorMatrixColorFilter: 通利用矩阵的方式处理颜色,这个才是本章的重点。

(1)构造方法

ColorMatrixColorFilter有两个构造方法,分别是

ColorMatrixColorFilter(@NonNull float[] array)
ColorMatrixColorFilter(@NonNull ColorMatrix matrix)

前者传递一个float数组,这是一个怎样的数组我们暂且不说,下面说一下后者,后者传递一个ColorMatrix对象,那么ColorMatrix又是什么呢?

(2)ColorMatrix类的源码解析

研究一个类,我们只能从源码开始研究,首先不容错过的就是注释,下面贴出ColorMatrix类的注释

/**
 * 4x5 matrix for transforming the color and alpha components of a Bitmap.
 * The matrix can be passed as single array, and is treated as follows:
 *
 * <pre>
 *  [ a, b, c, d, e,
 *    f, g, h, i, j,
 *    k, l, m, n, o,
 *    p, q, r, s, t ]</pre>
 *
 * <p>
 * When applied to a color <code>[R, G, B, A]</code>, the resulting color
 * is computed as:
 * </p>
 *
 * <pre>
 *   R&rsquo; = a*R + b*G + c*B + d*A + e;
 *   G&rsquo; = f*R + g*G + h*B + i*A + j;
 *   B&rsquo; = k*R + l*G + m*B + n*A + o;
 *   A&rsquo; = p*R + q*G + r*B + s*A + t;</pre>
 *
 * <p>
 * That resulting color <code>[R&rsquo;, G&rsquo;, B&rsquo;, A&rsquo;]</code>
 * then has each channel clamped to the <code>0</code> to <code>255</code>
 * range.
 * </p>
 *
 * <p>
 * The sample ColorMatrix below inverts incoming colors by scaling each
 * channel by <code>-1</code>, and then shifting the result up by
 * <code>255</code> to remain in the standard color space.
 * </p>
 *
 * <pre>
 *   [ -1, 0, 0, 0, 255,
 *     0, -1, 0, 0, 255,
 *     0, 0, -1, 0, 255,
 *     0, 0, 0, 1, 0 ]</pre>
 */

以上注释告诉我们这么几点:

位图的颜色的可用4x5矩阵表示,用于转换位图的颜色和alpha分量。
矩阵可以表示为:

[ a, b, c, d, e,
   f, g, h, i, j,
   k, l, m, n, o,
   p, q, r, s, t ]

计算公式为:

R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;

要想理解这个公式,必须要知道图像的构成以及图像的颜色模式(下面会讲到)。

(3)颜色模式

RGB颜色模式: 虽然可见光的波长有一定的范围,但我们在处理颜色时并不需要将每一种波长的颜色都单独表示。因为自然界中所有的颜色都可以用红、绿、蓝(RGB)这三种颜色波长的不同强度组合而得,这就是人们常说的三基色原理。因此,这三种光常被人们称为三基色或三原色。有时候我们亦称这三种基色为添加色(Additive Colors),这是因为当我们把不同光的波长加到一起的时候,得到的将会是更加明亮的颜色。把三种基色交互重叠,就产生了次混合色:青(Cyan)、洋红(Magenta)、黄(Yellow)。这同时也引出了互补色(Complement Colors)的概念。基色和次混合色是彼此的互补色,即彼此之间最不一样的颜色。例如青色由蓝色和绿色构成,而红色是缺少的一种颜色,因此青色和红色构成了彼此的互补色。在数字视频中,对RGB三基色各进行8位编码就构成了大约1677万种颜色,这就是我们常说的真彩色。顺便提一句,电视机和计算机的监视器都是基于RGB颜色模式来创建其颜色的。

CMYK模式: CMYK颜色模式是一种印刷模式。其中四个字母分别指青(Cyan)、洋红(Magenta)、黄(Yellow)、黑(Black),在印刷中代表四种颜色的油墨。CMYK模式在本质上与RGB模式没有什么区别,只是产生色彩的原理不同,在RGB模式中由光源发出的色光混合生成颜色,而在CMYK模式中由光线照到有不同比例C、M、Y、K油墨的纸上,部分光谱被吸收后,反射到人眼的光产生颜色。由于C、M、Y、K在混合成色时,随着C、M、Y、K四种成分的增多,反射到人眼的光会越来越少,光线的亮度会越来越低,所有CMYK模式产生颜色的方法又被称为色光减色法。

HSB颜色模式:从心理学的角度来看,颜色有三个要素:色泽(Hue)、饱和度(Saturation)和亮度(Brightness)。HSB颜色模式便是基于人对颜色的心理感受的一种颜色模式。它是由RGB三基色转换为Lab模式,再在Lab模式的基础上考虑了人对颜色的心理感受这一因素而转换成的。因此这种颜色模式比较符合人的视觉感受,让人觉得更加直观一些。它可由底与底对接的两个圆锥体立体模型来表示,其中轴向表示亮度,自上而下由白变黑;径向表示色饱和度,自内向外逐渐变高;而圆周方向,则表示色调的变化,形成色环。

Lab颜色模式: Lab颜色是由RGB三基色转换而来的,它是由RGB模式转换为HSB模式和CMYK模式的桥梁。该颜色模式由一个发光率(Luminance)和两个颜色(a,b)轴组成。它由颜色轴所构成的平面上的环形线来表示色的变化,其中径向表示色饱和度的变化,自内向外,饱和度逐渐增高;圆周方向表示色调的变化,每个圆周形成一个色环;而不同的发光率表示不同的亮度并对应不同环形颜色变化线。它是一种具有“独立于设备”的颜色模式,即不论使用任何一种监视器或者打印机,Lab的颜色不变。其中a表示从洋红至绿色的范围,b表示黄色至蓝色的范围。

位图模式: 位图模式用两种颜色(黑和白)来表示图像中的像素。位图模式的图像也叫作黑白图像。因为其深度为1,也称为一位图像。由于位图模式只用黑白色来表示图像的像素,在将图像转换为位图模式时会丢失大量细节,因此Photoshop提供了几种算法来模拟图像中丢失的细节。 在宽度、高度和分辨率相同的情况下,位图模式的图像尺寸最小,约为灰度模式的1/7和RGB模式的1/22以下。

灰度模式: 灰度模式可以使用多达256级灰度来表现图像,使图像的过渡更平滑细腻。灰度图像的每个像素有一个0(黑色)到255(白色)之间的亮度值。灰度值也可以用黑色油墨覆盖的百分比来表示(0%等于白色,100%等于黑色)。使用黑折或灰度扫描仪产生的图像常以灰度显示。

索引颜色模式: 索引颜色模式是网上和动画中常用的图像模式,当彩色图像转换为索引颜色的图像后包含近256种颜色。索引颜色图像包含一个颜色表。如果原图像中颜色不能用256色表现,则Photoshop会从可使用的颜色中选出最相近颜色来模拟这些颜色,这样可以减小图像文件的尺寸。用来存放图像中的颜色并为这些颜色建立颜色索引,颜色表可在转换的过程中定义或在生成索引图像后修改。

双色调模式: 双色调模式采用2-4种彩色油墨来创建由双色调(2种颜色)、三色调(3种颜色)和四色调(4种颜色)混合其色阶来组成图像。在将图像转换为双色调模式的过程中,可以对色调进行编辑,产生特殊的效果。而使用双色调模式最主要的用途是使用尽量少的颜色表现尽量多的颜色层次,这对于减少印刷成本是很重要的,因为在印刷时,每增加一种色调都需要更大的成本。

多通道模式: 多通道模式对有特殊打印要求的图像非常有用。例如,如果图像中只使用了一两种或两三种颜色时,使用多通道模式可以减少印刷成本并保证图像颜色的正确输出。 6. 8位/16位通道模式 在灰度RGB或CMYK模式下,可以使用16位通道来代替默认的8位通道。根据默认情况,8位通道中包含256个色阶,如果增到16位,每个通道的色阶数量为65536个,这样能得到更多的色彩细节。Photoshop可以识别和输入16位通道的图像,但对于这种图像限制很多,所有的滤镜都不能使用,另外16位通道模式的图像不能被印刷。

以上几种颜色模式都是传统上的概念,但是Android的颜色模式有哪些呢?

ARGB8888: 四通道高精度(32位),每个
ARGB4444: 四通道低精度(16位)
RGB565: 屏幕默认模式(16位)
Alpha8: 仅有透明通道(8位)

其中,A代表alpha(透明度)通道,R代表红色通道,G代表绿色通道,B代码蓝色通道。

(4)颜色通道

Android的屏幕的颜色模式默认是RGB,分别对应了三种通道R(红色通道)、G(绿色通道)、B(蓝色通道),但是如果涉及到图像处理,我们通常所用的模式是ARGB模式,分别对应这饱和度通道、红色通道、绿色通道、蓝色通道。

(5)颜色矩阵

有关矩阵的基本运算可以参考这篇博客高级UI<第二十四篇>:Android中用到的矩阵常识

android中可以通过颜色矩阵 ColorMatrix 方便的操作颜色,颜色矩阵是一个5x4 的矩阵:

 [ a  b  c  d  e
   f  g  h  i  j
   k  l  m  n  o
   p  q  r  s  t ]

它是一个5行4列的矩阵,第一行代表红色通道,第二行代表绿色通道,第三行代表蓝色通道,第四行代表饱和度通道,其中每行的最后一位是通道的偏移量。如图:(假设是矩阵A)

图片.png

这个矩阵可以用一维数组表示

[ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t ]

修改数组中的值可以轻松对图像的颜色进行操作。

我们知道,这个数据我们自己任意设定的,然而图像本身有个默认的ARGB通道,假设他们的值分别是A、R、G、B,那么图像的分量矩阵是:(假设是矩阵B)

 [ R
   G
   B
   A
   1 ]

那么A和B的相乘的结果是:(C矩阵)

 [ a  b  c  d  e                 [ R            
   f  g  h  i  j          乘以      G         =  结果如下
   k  l  m  n  o                   B
   p  q  r  s  t ]                 A
                                   1 ]


 [ aR + bG + cB + dA + e
   fR + gG + hB + iA + j
   kR + lG + mB + nA + o
   pR + qG + rB + sA + t ]

上文说到,第一行代表红色通道,第二行代表绿色通道,第三行代表蓝色通道,第四行代表饱和度通道,所以最终的计算结果为:

R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;

颜色矩阵的初始值是:

 [ 1  0  0  0  0
   0  1  0  0  0
   0  0  1  0  0
   0  0  0  1  0 ]

我们也能看出来,这个矩阵的每个值都是代码以上线性表达式的系数,其中偏移量的取值范围是[0,255]表示颜色的偏移量,其他的都相当于倍数

如果还不能理解,那就举几个例子吧

例子一: 调整亮度

 [ 1  0  0  0  N
   0  1  0  0  N
   0  0  1  0  N
   0  0  0  1  0 ]

这里的N就是调整三通道的亮度,我们只需要设置一下RGB的色彩偏移就能调节其亮度。

例子二: 颜色反向(底片效果)

 [ -1  0  0  0  255
   0  -1  0  0  255
   0  0  -1  0  255
   0  0  0  -1  0 ]

把RGB通道的原通道乘数设为-1,然后再把色彩偏移量设为255。

例子三: 颜色去色

 [ 0.3086  0.6094  0.0820  0  0
   0.3086  0.6094  0.0820  0  0
   0.3086  0.6094  0.0820  0  0
   0  0  0  1  0 ]

原理:只要把RGB三通道的色彩信息设置成一样;即:R=G=B,那么图像就变成了灰色,并且,为了保证图像亮度不变,同一个通道中的R+G+B=1。

例子四: 色彩饱和度

0.213*(1-sat) + N  0.715*(1-sat)        0.072*(1-sat)         0  0
0.213*(1-sat)      0.715*(1-sat) + sat  0.072*(1-sat)         0  0
0.213*(1-sat)      0.715*(1-sat)        0.072*(1-sat) + sat   0  0
      0                   0                    0              1  0

其中sat的一般取值范围是[0,1]

分析:

  • 当sat为0时,色彩饱和度降到最,相当于给图像完全去色;当sat为1时,图像还原成初始状态。
  • 当sat的取值范围脱离[0,1]时,可以起到增强色度的作用。

例子五: 对比度

N取值为0到10

[N,0,0,0,128*(1-N)
0,N,0,0,128*(1-N)
0,0,N,0,128*(1-N)
0,0,0,1,0]

所谓对比度就是让红的更红,绿的更绿。

例子六: 阈值

所谓阈值,就是以一个色度值为基准对图像作非黑即白的处理(注意没有灰色),由于不去除了彩色属性,因此,也离不开0.3086, 0.6094, 0.0820这三组神奇的数字。

下面的256也可以改成255

0.3086*256,0.6094*256,0.0820*256,0,-256*N
0.3086*256,0.6094*256,0.0820*256,0,-256*N
0.3086*256,0.6094*256,0.0820*256,0,-256*N
0, 0, 0, 1, 0

例子七: 色彩旋转

所谓色彩旋转就是让某一个通道的色彩信息让另一个通道去显示;比如,R显示G的信息,G显示B的信息,B显示R的信息,也可以只拿出一部份信息让给别的通道去显示,至于参数的瓜分可以平分。不必太讲究,但是,始终要坚持的一个原则就是每一个通道中的RGB信息量之和一定要为1,不然将会生偏色,如果您要制作偏色效果又另当别论;

0,1,0,0,0
0,0,1,0,0
1,0,0,0,0
0,0,0,1,0

或者

0,0,1,0,0
1,0,0,0,0
0,1,0,0,0
0,0,0,1,0

比如:发色效果

        0,1,0,0,0,
        1, 0,0,0,0,
        0,0,1,0,0,
        0,0,0,1,0,

例子八: 只显示某个通道

1,0,0,0,0

例子九: 颜色增强

 [ 1.2  0  0  0  N
   0  1.4  0  0  N
   0  0  1.1  0  N
   0  0  0  1  0 ]

例子九: 复古效果

        1/2f,1/2f,1/2f,0,0,
        1/3f, 1/3f,1/3f,0,0,
        1/4f,1/4f,1/4f,0,0,
        0,0,0,1,0,

例子十: 颜色通道过滤

        1, 0,0,0,0,
        0,0,0,0,0,
        0,0,0,0,0,
        0,0,0,1,0,

这个比较简单,原本是1倍颜色,现在改成了:红色通道增强到1.2倍、红色通道增强到1.2倍、红色通道增强到1.2倍、绿色通道增强到1.4倍,蓝色通道增强到1.1倍。

(6)ColorMatrix具体方法
  • 设置饱和度setSaturation

首先看一下源码

/**
 * Set the matrix to affect the saturation of colors.
 *
 * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
 */
public void setSaturation(float sat) {
    reset();
    float[] m = mArray;

    final float invSat = 1 - sat;
    final float R = 0.213f * invSat;
    final float G = 0.715f * invSat;
    final float B = 0.072f * invSat;

    m[0] = R + sat; m[1] = G;       m[2] = B;
    m[5] = R;       m[6] = G + sat; m[7] = B;
    m[10] = R;      m[11] = G;      m[12] = B + sat;
}

源码中0.213、0.715、0.072这三个值怎么算出来的我们不得而知,总之,我们先按照源码的算法整理一下最终矩阵。

以下是颜色矩阵的初始值

1  0  0  0  0
0  1  0  0  0
0  0  1  0  0
0  0  0  1  0

现在根据源码中的算法,将值填入这个初始矩阵

0.213*(1-sat) + N  0.715*(1-sat)        0.072*(1-sat)         0  0
0.213*(1-sat)      0.715*(1-sat) + sat  0.072*(1-sat)         0  0
0.213*(1-sat)      0.715*(1-sat)        0.072*(1-sat) + sat   0  0
      0                   0                    0              1  0

其中sat的一般取值范围是[0,1]

分析:

  1. 当sat为0时,色彩饱和度降到最,相当于给图像完全去色;当sat为1时,图像还原成初始状态。
  2. 当sat的取值范围脱离[0,1]时,可以起到增强色度的作用。
  • RGB和YUV相互转换

      //现在的YUV是通常用于计算机领域用来表示使用YCbCr编码的文件。所以可以粗浅地视YUV为YCbCr。
      colorMatrix.setRGB2YUV();
      colorMatrix.setYUV2RGB();
    

RGB转YUV的矩阵如下:

0.299     0.587      0.114     0  0
-0.16874  -0.33126   0.5       0  0
0.5       -0.41869   -0.08131  0  0
0         0          0         1  0

YUV转RGB的矩阵如下:

1   0        1.402     0  0
1  -0.34414  -0.71414  0  0
1  1.772      0        0  0
0  0          0        1  0
  • 设置色调
//axis:0 表示对红色通道进行修改
//axis:1 表示对绿色通道进行修改
//axis:2 表示对蓝色通道进行修改
//degrees 角度
setRotate(int axis, float degrees)

源码如下:

/**
 * Set the rotation on a color axis by the specified values.
 * <p>
 * <code>axis=0</code> correspond to a rotation around the RED color
 * <code>axis=1</code> correspond to a rotation around the GREEN color
 * <code>axis=2</code> correspond to a rotation around the BLUE color
 * </p>
 */
public void setRotate(int axis, float degrees) {
    reset();
    double radians = degrees * Math.PI / 180d;
    float cosine = (float) Math.cos(radians);
    float sine = (float) Math.sin(radians);
    switch (axis) {
    // Rotation around the red color
    case 0:
        mArray[6] = mArray[12] = cosine;
        mArray[7] = sine;
        mArray[11] = -sine;
        break;
    // Rotation around the green color
    case 1:
        mArray[0] = mArray[12] = cosine;
        mArray[2] = -sine;
        mArray[10] = sine;
        break;
    // Rotation around the blue color
    case 2:
        mArray[0] = mArray[6] = cosine;
        mArray[1] = sine;
        mArray[5] = -sine;
        break;
    default:
        throw new RuntimeException();
    }

由此可以推敲出最终矩阵为:

修改红色色调

1                      0                                     0                              0  0
0  (float) Math.cos(degrees * Math.PI / 180d)  (float) Math.sin(degrees * Math.PI / 180d)   0  0
0  -(float) Math.sin(degrees * Math.PI / 180d) (float) Math.cos(degrees * Math.PI / 180d)   0  0
0                              0                         0                                  1  0

修改绿色色调

(float) Math.cos(radegrees * Math.PI / 180ddians)  0  -(float) Math.sin(degrees * Math.PI / 180d)  0  0
0                                                  1              0                                0  0
(float) Math.sin(degrees * Math.PI / 180d)         0  (float) Math.cos(degrees * Math.PI / 180d)   0  0
0                                                  0                 0                             1  0

修改蓝色色调

(float) Math.cos(degrees * Math.PI / 180d)  (float) Math.sin(degrees * Math.PI / 180d)   0  0  0
-(float) Math.sin(degrees * Math.PI / 180d)  (float) Math.cos(degrees * Math.PI / 180d)  0  0  0
0                                                       0                                1  0  0
0                                                       0                                0  1  0
  • 设置亮度
 public void setScale(float rScale, float gScale, float bScale,float aScale)

最终矩阵为

rScale  0       0       0     0
0     gScale    0       0     0
0       0     bScale    0     0
0       0       0     aScale  0
  • 两矩阵组合setContact
setConcat(ColorMatrix matA, ColorMatrix matB)

假设组合后的最终矩阵是C,那么C = matA* matB。

除了setConcat之外,还有另外两个方法也能组合矩阵:preConcat、postConcat,大家看一下源码就在知道了,其实和setConcat类似。

/**
 * Concat this colormatrix with the specified prematrix.
 * <p>
 * This is logically the same as calling setConcat(this, prematrix);
 * </p>
 */
public void preConcat(ColorMatrix prematrix) {
    setConcat(this, prematrix);
}

/**
 * Concat this colormatrix with the specified postmatrix.
 * <p>
 * This is logically the same as calling setConcat(postmatrix, this);
 * </p>
 */
public void postConcat(ColorMatrix postmatrix) {
    setConcat(postmatrix, this);
}
  • 重置
public void reset

可以将当前矩阵重置为初始值。

[本章完...]

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