实现完整的图像平面几何变换

http://blog.csdn.net/maozefa/archive/2010/10/10/5931427.aspx

有关图像的平面几何变换,现有的教程、计算机图书以及网上的资料上介绍理论的偏多,即使有些编程实例,也只是介绍图像几何变换的某些特例,如旋转、缩放、平移等。GDI+倒是有个Matrix类,可完整地实现图像的几何变换,可惜没法得到源码。

    本文将完整的实现一个类似GDI+ Matrix的C++几何变换类TransformMatrix,关于几何变换的理论及原理请参考有关书籍或资料。

    下面是C++几何变换类TransformMatrix的代码:

  1. typedef   union   
  2. {  
  3.     float  Elements[6];  
  4.     struct   
  5.     {  
  6.         float  m11;  
  7.         float  m12;  
  8.         float  m21;  
  9.         float  m22;  
  10.         float  dx;  
  11.         float  dy;  
  12.     };  
  13. }MatrixElements, *PMatrixElements;  
  14.   
  15. class  TransformMatrix  
  16. {  
  17. private :  
  18.     MatrixElements elements;  
  19.   
  20.     VOID  ElementsInit(MatrixElements &e)  
  21.     {  
  22.         e.m11 = e.m22 = 1.0f;  
  23.         e.m12 = e.m21 = e.dx = e.dy = 0.0f;  
  24.     }  
  25.   
  26.     VOID  ElementsMultiply(MatrixElements &e)  
  27.     {  
  28.         float  m11 = elements.m11;  
  29.         float  m12 = elements.m12;  
  30.         elements.m11 = e.m11 * m11 + e.m12 * elements.m21;  
  31.         elements.m12 = e.m11 * m12 + e.m12 * elements.m22;  
  32.         elements.m21 = e.m21 * m11 + e.m22 * elements.m21;  
  33.         elements.m22 = e.m21 * m12 + e.m22 * elements.m22;  
  34.     }  
  35.   
  36. public :  
  37.     // 建立一个新实例,并初始化为单位矩阵 Elements = 1,0,0,1,0,0   
  38.     TransformMatrix(VOID )  
  39.     {  
  40.         Reset();  
  41.     }  
  42.   
  43.     // 建立一个新实例,并复制matrix的元素   
  44.     TransformMatrix(TransformMatrix *matrix)  
  45.     {  
  46.         SetElements(matrix->elements);  
  47.     }  
  48.   
  49.     TransformMatrix(TransformMatrix &matrix)  
  50.     {  
  51.         SetElements(matrix.elements);  
  52.     }  
  53.   
  54.     // 建立一个按指定的元素初始化的新实例   
  55.     TransformMatrix(float  m11,  float  m12,  float  m21,  float  m22,  float  dx,  float  dy)  
  56.     {  
  57.         SetElements(m11, m12, m21, m22, dx, dy);  
  58.     }  
  59.   
  60.     // 重置对象为单位矩阵   
  61.     VOID  Reset( VOID )  
  62.     {  
  63.         ElementsInit(elements);  
  64.     }  
  65.   
  66.     // 将对象与matrix相乘   
  67.     VOID  Multiply(TransformMatrix *matrix)  
  68.     {  
  69. //      float dx = elements.dx;   
  70.         elements.dx += (matrix->elements.dx * elements.m11 + matrix->elements.dy  * elements.m21);  
  71.         elements.dy += (matrix->elements.dx * elements.m12 + matrix->elements.dy  * elements.m22);  
  72.         ElementsMultiply(matrix->elements);  
  73.     }  
  74.   
  75.     VOID  Multiply(TransformMatrix &matrix)  
  76.     {  
  77.         Multiply(&matrix);  
  78.     }  
  79.   
  80.     // 设置平移   
  81.     VOID  Translate( float  offsetX,  float  offsetY)  
  82.     {  
  83.          elements.dx += (offsetX * elements.m11 + offsetY * elements.m21);  
  84.          elements.dy += (offsetX * elements.m12 + offsetY * elements.m22);  
  85.     }  
  86.   
  87.     // 设置缩放   
  88.     VOID  Scale( float  scaleX,  float  scaleY)  
  89.     {  
  90.         MatrixElements e;  
  91.         ElementsInit(e);  
  92.         e.m11 = scaleX;  
  93.         e.m22 = scaleY;  
  94.         ElementsMultiply(e);  
  95.     }  
  96.   
  97.     // 设置剪切   
  98.     VOID  Shear( float  shearX,  float  shearY)  
  99.     {  
  100.         MatrixElements e;  
  101.         ElementsInit(e);  
  102.         e.m21 = shearX;  
  103.         e.m12 = shearY;  
  104.         ElementsMultiply(e);  
  105.     }  
  106.   
  107.     // 设置按角度angle沿原点旋转   
  108.     VOID  Rotate( float  angle)  
  109.     {  
  110.         MatrixElements e;  
  111.         angle = angle * M_PI / 180.0f;  
  112.         e.m11 = e.m22 = cos(angle);  
  113.         e.m12 = sin(angle);  
  114.         e.m21 = -e.m12;  
  115.         e.dx = e.dy = 0.0f;  
  116.         ElementsMultiply(e);  
  117.     }  
  118.   
  119.     // 设置按角度angle沿中心点centerX, centerY旋转   
  120.     VOID  RotateAt( float  angle,  float  centerX,  float  centerY)  
  121.     {  
  122.         Translate(centerX, centerY);  
  123.         Rotate(angle);  
  124.         Translate(-centerX, -centerY);  
  125.     }  
  126.   
  127.     // 如果此对象是可逆转的,则逆转该对象,返回TRUE;否则返回FALSE   
  128.     BOOL  Invert( VOID )  
  129.     {  
  130.         double  tmp = elements.m11 * elements.m22 - elements.m12 * elements.m21;  
  131.         if  (( INT )(tmp * 1000.0f) == 0)  return  FALSE;  
  132.         tmp = 1.0f / tmp;  
  133.         float  m11 = elements.m11;  
  134.         float  dx = -elements.dx;  
  135.         elements.m11 = tmp * elements.m22;  
  136.         elements.m12 = tmp * -elements.m12;  
  137.         elements.m21 = tmp * -elements.m21;  
  138.         elements.m22 = tmp * m11;  
  139.         elements.dx = dx * elements.m11 - elements.dy * elements.m21;  
  140.         elements.dy = dx * elements.m12 - elements.dy * elements.m22;  
  141.         return  TRUE;  
  142.     }  
  143.   
  144.     // 按给定的大小计算并返回实施变换后的尺寸   
  145.     VOID  GetTransformSize( INT  width,  INT  height,  float  &fx,  float  &fy,  float  &fwidth,  float  &fheight)  
  146.     {  
  147.         float  fxs[3], fys[3], v;  
  148.         fxs[1] = fys[0] = 0.0f;  
  149.         fxs[0] = fxs[2] = width;  
  150.         fys[1] = fys[2] = height;  
  151.         fx = fy = fwidth = fheight = 0.0f;  
  152.         for  ( INT  i = 0; i < 3; i ++)  
  153.         {  
  154.             v = fxs[i] * elements.m11 + fys[i] * elements.m21;  
  155.             if  (v < fx) fx = v;  
  156.             else   if  (v > fwidth) fwidth = v;  
  157.             v = fxs[i] * elements.m12 + fys[i] * elements.m22;  
  158.             if  (v < fy) fy = v;  
  159.             else   if  (v > fheight) fheight = v;  
  160.         }  
  161.         fwidth -= fx;  
  162.         fheight -= fy;  
  163.         fx += elements.dx;  
  164.         fy += elements.dy;  
  165.     }  
  166.   
  167.     // 按给定的大小计算并返回实施变换后整型数矩形   
  168.     VOID  GetTransformRect( int  width,  int  height, RECT &r)  
  169.     {  
  170.         float  fx, fy, fwidth, fheight;  
  171.         GetTransformSize(width, height, fx, fy, fwidth, fheight);  
  172.         r.left = (INT )fx;  
  173.         r.top = (INT )fy;  
  174.         r.right = (INT )(fwidth + fx + 0.999999f);  
  175.         r.bottom = (INT )(fheight + fy + 0.999999f);  
  176.     }  
  177.   
  178.     // 判断此对象是否是单位矩阵   
  179.     BOOL  GetIdentity( VOID )  
  180.     {  
  181.         return  (elements.m11 == 1.0f &&  
  182.                 elements.m22 == 1.0f &&  
  183.                 elements.m12 == 0.0f &&  
  184.                 elements.m21 == 0.0f &&  
  185.                 elements.dx == 0.0f &&  
  186.                 elements.dy == 0.0f);  
  187.     }  
  188.   
  189.     // 获取对象的x偏移量   
  190.     float  GetOffsetX( VOID )  
  191.     {  
  192.         return  elements.dx;  
  193.     }  
  194.   
  195.     // 获取对象的y偏移量   
  196.     float  GetOffsetY( VOID )  
  197.     {  
  198.         return  elements.dy;  
  199.     }  
  200.   
  201.     // 判断对象是否是可逆转的。   
  202.     BOOL  GetInvertible( VOID )  
  203.     {  
  204.         return  ( INT )(1000.0f * (elements.m11 * elements.m22 - elements.m12 * elements.m21)) != 0;  
  205.     }  
  206.   
  207.     // 获取对象元素   
  208.     MatrixElements& GetElements(VOID )  
  209.     {  
  210.         return  elements;  
  211.     }  
  212.   
  213.     // 设置对象元素。注:设置元素是覆盖形式的   
  214.     VOID  SetElements(CONST MatrixElements &value)  
  215.     {  
  216.         SetElements(value.m11, value.m12, value.m21, value.m22, value.dx, value.dy);  
  217.     }  
  218.   
  219.     VOID  SetElements( float  m11,  float  m12,  float  m21,  float  m22,  float  dx,  float  dy)  
  220.     {  
  221.         elements.m11 = m11;  
  222.         elements.m12 = m12;  
  223.         elements.m21 = m21;  
  224.         elements.m22 = m22;  
  225.         elements.dx = dx;  
  226.         elements.dy = dy;  
  227.     }  
  228.   
  229. };  

    上面代码中定义了一个几何变换矩阵成员类型MatrixElements,便于实际编程时获取或设置几何变换矩阵成员,TransformMatrix只是简单的对其进行了封装,并通过计算实现有关的几何变换。

    TransformMatrix的核心代码是Multiply函数(或ElementsMultiply函数)和Invert函数。

    Multiply函数可完成各种复杂的几何变换计算,所有能够实现的具体几何变换都是可以通过其完成的(代码中的平移函数Translate也可以通过其 完成的,当然多了一些不必要的计算)。虽说本文标题是《实现完整的图像平面几何变换》,但TransformMatrix中的几种基础的变换函数并不代表 全部的几何变换,如对称几何变换(镜像),更不用说复杂的组合变换。这倒不是本人要做“标题党”,我所说的“实现完整的图像几何变换”,是指可以通过 Multiply函数或者更直接的变换矩阵成员设置去实现“完整的”图像几何变换,除非其不能使用平面几何变换矩阵进行描述(如梯形变换我就没想到怎么实 现,也许其超出了平面几何变换矩阵范畴?),或者不能进行实际的几何变换(不可逆);“实现完整的图像几何变换”的另一层含义是下面的图像变换执行函数可 实现TransformMatrix所能表示的任意图像几何变换,而不必去写一个个具体的,如缩放、旋转变换函数等。

    Invert函数实现了变换矩阵的逆矩阵,通过这个几何变换逆矩阵,可以很方便地实现图形图像几何变换的实际操作。为什么要靠几何变换矩阵的逆矩阵,而不 是直接依据变换矩阵来实现图形图像几何变换的实际操作呢?因为几何变换矩阵表示的意思是,把源图像的任意座标点通过几何变换后投影到目标图像。因为源图像 像素通过几何变换后与目标图像上的像素点有可能不能一一对应,如图像缩放变换后,不是多个源图像像素点对应同一个目标像素点(缩小),就是源图像像素点不 足以填充全部的目标像素点(放大),这就有可能造成目标图像像素点被重复绘制或者被遗漏的现象发生;而几何变换逆矩阵所表示的意思是,对于目标图像任意一 个像素点,如果在几何变换前有源图像像素点与其对应,则进行复制。遍历目标图像像素点就能保证目标图像像素点既不重复、也不遗漏的被复制。

    下面是一个图像几何变换函数代码:

  1. // 获取子图数据   
  2. BOOL  GetSubBitmapData(CONST BitmapData *data,  INT  x,  INT  y,  INT  width,  INT  height, BitmapData *sub)  
  3. {  
  4.     if  (x < 0)  
  5.     {  
  6.         width += x;  
  7.         x = 0;  
  8.     }  
  9.     if  (x + width > ( INT )data->Width)  
  10.         width = (INT )data->Width - x;  
  11.     if  (width <= 0)  return  FALSE;  
  12.     if  (y < 0)  
  13.     {  
  14.         height += y;  
  15.         y = 0;  
  16.     }  
  17.     if  (y + height > ( INT )data->Height)  
  18.         height = (INT )data->Height - y;  
  19.     if  (height <= 0)  return  FALSE;  
  20.     sub->Width = width;  
  21.     sub->Height = height;  
  22.     sub->Stride = data->Stride;  
  23.     sub->Scan0 = (CHAR *)data->Scan0 + y * data->Stride + (x << 2);  
  24.     return  TRUE;  
  25. }  
  26.   
  27. // 执行图像数据几何变换   
  28. VOID  Transform(BitmapData *dest,  INT  x,  INT  y, CONST BitmapData *source, TransformMatrix *matrix)  
  29. {  
  30.     // 复制几何变换矩阵对象   
  31.     TransformMatrix m(matrix);  
  32.     // 几何变换矩阵绝对增加平移量x, y   
  33.     m.GetElements().dx += x;  
  34.     m.GetElements().dy += y;  
  35.     // 按几何变换矩阵计算并获取目标图像数据子数据   
  36.     float  fx, fy, fwidth, fheight;  
  37.     m.GetTransformSize(source->Width, source->Height, fx, fy, fwidth, fheight);  
  38.     BitmapData dst;  
  39.     if  (!GetSubBitmapData(dest, ( INT )fx, ( INT )fy,  
  40.         (INT )(fwidth + 0.999999f), ( INT )(fheight + 0.999999f), &dst))  
  41.         return ;  
  42.     // 获取几何变换逆矩阵   
  43.     if  (!m.Invert())  return ;  
  44.     // 如果子图数据与目标图像原点不一致,几何变换矩阵相对增加平移量fx, fy   
  45.     if  (fx > 0.0f || fy > 0.0f)  
  46.     {  
  47.         if  (fx < 0.0f) fx = 0.0f;  
  48.         else   if  (fy < 0.0f) fy = 0.0f;  
  49.         m.Translate(fx, fy);  
  50.     }  
  51.     // 设置子图扫描线指针及行偏移宽度   
  52.     UINT  *pix = ( UINT *)dst.Scan0;  
  53.     INT  dstOffset = (dst.Stride >> 2) - dst.Width;  
  54.     // 几何变换逆矩阵的平移量为与子图原点对应的源图起始坐标点   
  55.     MatrixElements e = m.GetElements();  
  56.     float  xs = e.dx;  
  57.     float  ys = e.dy;  
  58.     // 逐点计算并复制源图几何变换后的数据到目标子图   
  59.     for  (y = 0; y < ( INT )dst.Height; y ++, pix += dstOffset, xs += e.m21, ys += e.m22)  
  60.     {  
  61.         float  xs0 = xs;  
  62.         float  ys0 = ys;  
  63.         for  (x = 0; x < ( INT )dst.Width; x ++, pix ++, xs0 += e.m11, ys0 += e.m12)  
  64.         {  
  65.             INT  x0 = xs0 < 0.0f? ( INT )(xs0 - 0.5f) : ( INT )(xs0 + 0.5f);  
  66.             INT  y0 = ys0 < 0.0f? ( INT )(ys0 - 0.5f) : ( INT )(ys0 + 0.5f);  
  67.             if  (y0 >= 0 && y0 < ( INT )source->Height && x0 >= 0 && x0 < ( INT )source->Width)  
  68.                 *pix = *(UINT *)(( CHAR *)source->Scan0 + y0 * source->Stride + (x0 << 2));  
  69.         }  
  70.     }  
  71. }  

    上面图像几何变换函数的几个特点:

    1、可以实现任意的图像几何变换(只要TransformMatrix能正确表达的,即变换矩阵可逆);

    2、采用了GDI+ 的BitmapData结构(转换为32位ARGB像素格式),而并非任何具体的图像格式,保证了其通用性;

    3、函数使用浮点数运算,但在计算像素点位置时避免了通常的浮点数乘除运算,既提高了一定的运算速度,也为以后修改为定点数运算奠定了基础;

    4、函数采用临近像素插值,且没有边界像素处理代码,像素复制质量较差。

    可以看出,Transform函数的着重点在于特点(1),在实际的实现代码中,可以把它作为一个框架进行扩充和修改。

    下面是一个利用Transform函数对GDI+位图进行旋转变换的例子(使用BCB2007):

  1. // 锁定GDI+位图扫描线到data   
  2. inline   VOID  LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)  
  3. {  
  4.     Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());  
  5.     bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite,  
  6.         PixelFormat32bppARGB, data);  
  7. }  
  8.   
  9. // GDI+位图扫描线解锁   
  10. inline   VOID  UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)  
  11. {  
  12.     bmp->UnlockBits(data);  
  13. }  
  14.   
  15. void  __fastcall TForm1::Button1Click(TObject *Sender)  
  16. {  
  17.     // 获取源图像扫描线数据   
  18.     Gdiplus::Bitmap *bmp =  new  Gdiplus::Bitmap(L "..//media//001-1.jpg" );  
  19.     BitmapData source, dest;  
  20.     LockBitmap(bmp, &source);  
  21.   
  22.     // 设置几何变换   
  23.     TransformMatrix matrix;  
  24.     matrix.Rotate(45);  
  25.   
  26.     // 建立目标位图并获取其扫描线数据   
  27.     // r.right和r.bottom分别为几何变换后可见部分的宽度和高度   
  28.     RECT r;  
  29.     matrix.GetTransformRect(source.Width, source.Height, r);  
  30.     if  (r.right > 0 && r.bottom > 0)  
  31.     {  
  32.         Gdiplus::Bitmap *newBmp = new  Gdiplus::Bitmap(r.right, r.bottom, PixelFormat32bppARGB);  
  33.         LockBitmap(newBmp, &dest);  
  34.         Transform(&dest, 0, 0, &source, &matrix);  
  35.         UnlockBitmap(newBmp, &dest);  
  36.         // 画几何变换后的图像   
  37.         Gdiplus::Graphics *g = new  Gdiplus::Graphics(Canvas->Handle);  
  38.         g->DrawImage(newBmp, 0, 0);  
  39.         delete  g;  
  40.         delete  newBmp;  
  41.     }  
  42.   
  43.     UnlockBitmap(bmp, &source);  
  44.     delete  bmp;  
  45. }  

    下面是图像旋转变换例子运行界面截图:

旋转45度

    由于图像几何变换是以源图原点(0,0)为变换原点,所以界面上只能看到原点右下边的图像。还有些几何变换,如旋转90度、180度等,可能会导致几何变 换后的图像完全不可见,为了直观的看到各种几何变换后的完整图像,可以修改一下例子代码,将 TransformMatrix::GetTransformRect函数返回矩形的左上边部分也包括进来:

  1. void  __fastcall TForm1::Button1Click(TObject *Sender)  
  2. {  
  3.     // 获取源图像扫描线数据   
  4.     Gdiplus::Bitmap *bmp =  new  Gdiplus::Bitmap(L "..//media//001-1.jpg" );  
  5.     BitmapData source, dest;  
  6.     LockBitmap(bmp, &source);  
  7.   
  8.     // 设置几何变换   
  9.     TransformMatrix matrix;  
  10.     matrix.Rotate(45);  
  11. //  matrix.RotateAt(45, source.Width / 2, source.Height / 2);   
  12. //  matrix.Scale(1.2, 1.2);   
  13. //  matrix.Shear(0.2, 0.3);   
  14. //  matrix.GetElements().m11 = -1.0f;       // 水平镜像   
  15. //  matrix.GetElements().m22 = -1.0f;       // 垂直镜像   
  16. //  matrix.SetElements(0, 1, 1, 0, 0, 0);   // x=y镜像   
  17.   
  18.     // 建立目标位图并获取其扫描线数据   
  19.     RECT r;  
  20.     matrix.GetTransformRect(source.Width, source.Height, r);  
  21.     Gdiplus::Bitmap *newBmp = new  Gdiplus::Bitmap(  
  22.         r.right - r.left, r.bottom - r.top, PixelFormat32bppARGB);  
  23.     LockBitmap(newBmp, &dest);  
  24.   
  25.     // 执行图像几何变换   
  26.     Transform(&dest, -r.left, -r.top, &source, &matrix);  
  27.   
  28.     // 释放图像扫描线数据(位图解锁)   
  29.     UnlockBitmap(newBmp, &dest);  
  30.     UnlockBitmap(bmp, &source);  
  31.   
  32.     // 画几何变换后的图像   
  33.     Gdiplus::Graphics *g = new  Gdiplus::Graphics(Canvas->Handle);  
  34.     g->DrawImage(newBmp, 0, 0);  
  35.   
  36.     delete  g;  
  37.     delete  newBmp;  
  38.     delete  bmp;  
  39. }  

    运行界面截图如下:

使几何变换后图像完全可见

    例子代码中,被注释掉的是一些图像常用几何变换,其运行界面就不一一贴图了。

    上面的例子使用的是GDI+ 位图,而我们通常使用更多的是GDI位图,下面以BCB2007的VCL位图为例作为本文结束部分:

  1. // 获取VCL位图扫描线到data   
  2. VOID  GetTBitmapData(::Graphics::TBitmap *bmp, BitmapData *data)  
  3. {  
  4.     bmp->PixelFormat = pf32bit;  
  5.     data->Width = bmp->Width;  
  6.     data->Height = bmp->Height;  
  7.     data->Scan0 = bmp->ScanLine[0];  
  8.     // 因为VCL位图扫描线是倒置的,所以其扫描线字节数设置为负数   
  9.     data->Stride = -(bmp->Width << 2);  
  10. }  
  11. void  __fastcall TForm1::Button3Click(TObject *Sender)  
  12. {  
  13.     TJPEGImage *jpg = new  TJPEGImage();  
  14.     jpg->LoadFromFile("..//media//001-1.jpg" );  
  15.     ::Graphics::TBitmap *bmp = new  ::Graphics::TBitmap();  
  16.     bmp->Assign(jpg);  
  17.     delete  jpg;  
  18.     BitmapData source, dest;  
  19.     GetTBitmapData(bmp, &source);  
  20.   
  21.     // 设置几何变换   
  22.     TransformMatrix matrix;  
  23. //  matrix.Rotate(45);   
  24. //  matrix.RotateAt(45, source.Width / 2, source.Height / 2);   
  25.     matrix.Scale(1.2, 1.2);  
  26.     matrix.Shear(0.5, 0.5);  
  27. //  matrix.GetElements().m11 = -1.0f;       // 水平镜像   
  28. //  matrix.GetElements().m22 = -1.0f;       // 垂直镜像   
  29. //  matrix.SetElements(0, 1, 1, 0, 0, 0);   // x=y镜像   
  30.   
  31.     // 建立目标位图并获取其扫描线数据   
  32.     RECT r;  
  33.     matrix.GetTransformRect(source.Width, source.Height, r);  
  34.     ::Graphics::TBitmap *newBmp = new  ::Graphics::TBitmap();  
  35.     newBmp->PixelFormat = pf32bit;  
  36.     newBmp->Width = r.right - r.left;  
  37.     newBmp->Height = r.bottom - r.top;  
  38.     GetTBitmapData(newBmp, &dest);  
  39.   
  40.     // 执行图像几何变换   
  41.     Transform(&dest, -r.left, -r.top, &source, &matrix);  
  42.   
  43.     // 画几何变换后的图像   
  44.     newBmp->Transparent = true ;  
  45.     newBmp->TransparentColor = clWhite;  
  46.     Canvas->Draw(0, 0, newBmp);  
  47.   
  48.     delete  newBmp;  
  49.     delete  bmp;  
  50. }  

    该例子实现了图像缩放与剪切组合变换,其运行界面截图如下:

图像缩放与剪切组合变换

    尽管我十分努力,但水平有限,错误在所难免,欢迎指正和指导。邮箱地址:

    maozefa@hotmail.com

 

 





怎样实现图像旋转 通过c++ builder

http://topic.csdn.net/u/20110510/18/8887b1a1-7f93-4d27-85c7-e63b053e6968.html?39315

 

 


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值