SSE图像算法优化体系三

  自从何凯明提议导向滤波后,因为其算法的简单性和有效性,该算法获得了常见的运用,以至于新版的matlab都将其用作标准自带的函数之一了,利用他得以化解的持有的保边滤波器的能解决的问题,比如细节增强、HDR压缩、细节羽化、去雾、风格化,而且由于其保边特性,倘使过多传统函数中运用高斯滤波或者均值滤波的地方用他取代,能很好解决一部分强边缘的连结不自然问题,比如retinex、Highlight/shadow等利用中,由此,神速的完毕该算法具有很强的适用意义。

  自从何凯明提出导向滤波后,因为其算法的简单性和管事,该算法获得了大面积的选择,以至于新版的matlab都将其看作正式自带的函数之一了,利用她可以解决的具备的保边滤波器的能缓解的题目,比如细节增强、HDR压缩、细节羽化、去雾、风格化,而且由于其保边特性,如若过多传统函数中运用高斯滤波或者均值滤波的地点用她取代,能很好解决一部分强边缘的连接不自然问题,比如retinex、Highlight/shadow等应用中,由此,疾速的完毕该算法具有很强的适用意义。

  本文简要的记录了本人在优化导向滤波落成的长河中所适用的优化措施和部分细节,以免时间久了后自己都不记得了,可是请不要向自身直接索取源代码。

  本文简要的笔录了自家在优化导向滤波达成的历程中所适用的优化措施和一部分细节,以免时间久了后自己都不记得了,可是请不要向自家平素索取源代码。

     
自以为当下本人优化的快慢在CPU版本中很难有人能领先了(仅仅使用CPU、不用三多线程,下采样率0.2),若是什么人有更快的算法,在第三方公证的意况下,我愿意提供1000元奖励^_^。

     
自以为眼前自家优化的进程在CPU版本中很难有人能当先了(仅仅使用CPU、不用三八线程,下采样率0.2),要是何人有更快的算法,在第三方公证的场地下,我甘愿提供1000元奖励^_^。

     
何凯明在导向滤波一文的相关材料中提供了其matlab代码,或者用上面的流水线也可以清晰的公布:

     
何凯明在导向滤波一文的相干材料中提供了其matlab代码,或者用上边的流水线也可以清晰的公布:

图片 1

图片 2

  大家看看了地方的6次取mean总计的历程,也就是浮点数的boxfilter,那几个事物已经是老掉牙的一个算法了,我在几年前研讨过opencv内部的那一个算法,并且指出了一种比opencv落成更快的法门,详见解析opencv中Box
Filter的落成并提议更进一步加速的方案(源码共享)
 一文。不过那里的处理时针对字节数据的,其内部用到了一部分整形数据的SSE优化,借使原本数据是浮点数,那反而就越是简易了,因为SSE指令生来就是为浮点数服务的。

  大家看看了地点的6次取mean统计的历程,也就是浮点数的boxfilter,这一个事物已经是老掉牙的一个算法了,我在几年前商量过opencv内部的那几个算法,并且提出了一种比opencv达成更快的艺术,详见解析opencv中Box
Filter的完毕并提议越来越加快的方案(源码共享)
 一文。可是那里的处理时针对字节数据的,其内部用到了有些整形数据的SSE优化,借使原本数据是浮点数,这反而就越来越简单了,因为SSE指令生来就是为浮点数服务的。

     
不过即使是那样,由于6次总结以及中间的别的部分浮点运算,仍旧给整个算法带来了很大的演算用度和内存费用,在很多场地仍旧不能满足要求的,比如实时去雾等场景。在先前时期我的长足去雾完毕中,都是先选用下采样图的导向滤波结果,然后再双线性插值放大得到大图的透射率图,固然在视觉效果上能解决去雾算法的速度问题,可是即使是其他场景的导向滤波必要,依旧会看出不少瑕疵的。

     
不过即便是那般,由于6次统计以及中等的其他部分浮点运算,仍然给所有算法带来了很大的演算开支和内存开支,在诸多场子照旧无法满意急需的,比如实时去雾等景观。在初期我的马上去雾已毕中,都是先利用下采样图的导向滤波结果,然后再双线性插值放大得到大图的透射率图,即使在视觉效果上能缓解去雾算法的快慢问题,不过只如若任何场景的导向滤波须要,依旧会看到众多通病的。

      何凯明在2015又公布了一篇《法斯特(Fast)Guided Filter》的稿子,演讲了一种很实用的更敏捷的导向滤波流程:

      何凯明在2015又刊出了一篇《法斯特(Fast)Guided Filter》的篇章,演说了一种很实用的更高速的导向滤波流程:

图片 3

图片 4

 
   我正好提的在去雾中本身实用的小Trick实际上就是第六步及第七步差别,我的措施可发挥如下:

 
   我刚刚提的在去雾中自我实用的小Trick实际上就是第六步及第七步分歧,我的办法可发挥如下:

       6: q = meana. * +
meanb

       6: q = meana. * +
meanb

       7:   q = fupsample(q,
s)

       7:   q = fupsample(q,
s)

     
很明显,由于I的插足总括,何的做法能更大程度上保证结果和原汁原味的类似,而自己的措施则会时有爆发较大的疙瘩相似,所以住户大神就是大神。

     
很明显,由于I的到场计算,何的做法能更大程度上保持结果和原汁原味的好像,而自我的点子则会暴发较大的疙瘩相似,所以住户大神就是大神。

      在何的舆论中已经证实下采样比例 s
取4时,计算的结果和高精度结果也依然不行贴近的,我在我的落到实处里s
取到了5。

      在何的舆论中曾经表明下采样比例 s
取4时,总计的结果和高精度结果也如故不行贴近的,我在自己的贯彻里s
取到了5。

     
那样改动后,所有的boxfilter均是对下取样后的数量进行拍卖,当s=4时,总计量收缩到原来的1/16,而s=5,则缩减到了原有的1/25,当然这些时候多了2个下取样和2个上取样的算法,下取样由于是裁减,总计量很小,无需关怀,而上采样,总结量和原图大小有关,依照本人的评测,这些上采样的耗时可能占总体经过的形似时间左右的,是极度值得注意优化的。

     
那样改动后,所有的boxfilter均是对下取样后的数额进行拍卖,当s=4时,统计量收缩到原有的1/16,而s=5,则减弱到了土生土长的1/25,当然那几个时候多了2个下取样和2个上取样的算法,下取样由于是收缩,计算量很小,无需关怀,而上采样,总结量和原图大小有关,按照我的估测,那些上采样的耗时可能占全体经过的形似时间左右的,是老大值得注意优化的。

   
 首先,第一,步骤6中的四个采样进度不要分开写,直接写到同一个for循环内部,那样可以省去见怪不怪坐标的总计进度,第二,这里一般的上采样经常选用双线性插值就OK了,网络上有很多关于双线性插值的SSE优化的代码,可是那个基本都是指向32位的图像做的优化,搬到24位和8位中是不适用的,而我辈会在50%上述的票房价值中蒙受24位图像,所以说啊,网络上的事物虽多,但精华太少。

   
 首先,第一,步骤6中的多个采样进程不要分开写,直接写到同一个for循环内部,那样可以省去成千上万坐标的乘除进度,第二,那里一般的上采样平时选用双线性插值就OK了,网络上有很多关于双线性插值的SSE优化的代码,可是那个基本都是本着32位的图像做的优化,搬到24位和8位中是不适用的,而我辈会在50%上述的票房价值中相见24位图像,所以说啊,网络上的东西虽多,但精华太少。

     
我动用的一个优化措施时,先举行水平方向的上采样到一个缓冲区中(Width  *
SmallH),然后在从那么些缓冲区中沿着高度方向缓冲到(Width *
Height),如下图所示:

     
我使用的一个优化措施时,先举行水平方向的上采样到一个缓冲区中(Width  *
SmallH),然后在从那么些缓冲区中沿着中度方向缓冲到(Width *
Height),如下图所示:

       
 图片 5 
 ———–——>   图片 6 
 ———–——>  图片 7

       
 图片 8 
 ———–——>   图片 9 
 ———–——>  图片 10

     
 由于那些上采样是针对浮点型的数量,所以中级的精度损失问题得以毫无考虑,而如若是图像的字节数据,则要慎重了。

     
 由于这几个上采样是针对性浮点型的数目,所以中间的精度损失问题可以不要考虑,而只要是图像的字节数据,则要慎重了。

     
 由地点的第三个图到第四个图的光景代码如下:

     
 由地点的首先个图到第三个图的大约代码如下:

 for (int Y = 0; Y < SmallH; Y++)
 {
     float PosX = 0;
     float AddX = (SmallW - 1.0f) / Width;        //  主要是为了减少下面插值向右增1的指针超过范围,但这样做其实是和精确的算法有一点点差异的
     float *LinePDA = TempA + Y * Width * Channel;    //  TempA和TempB为临时分配的大小为(SmallH * Width * Channel * sizeof(float)大小的内存
     float *LinePDB = TempB + Y * Width * Channel;
     float *LinePA = MeanA + Y * SmallW * Channel;
     float *LinePB = MeanB + Y * SmallW * Channel;
     if (Channel == 1)
     {
         for (int X = 0; X < Width; X++)
         {
             int XX = (int)PosX;
             float PartXX = PosX - XX;
             float InvertXX = 1 - PartXX;
             float *PtLeftA = LinePA + XX;
             float *PtLeftB = LinePB + XX;
             LinePDA[X] = PtLeftA[0] * InvertXX + PtLeftA[1] * PartXX;
             LinePDB[X] = PtLeftB[0] * InvertXX + PtLeftB[1] * PartXX;
             PosX += AddX;
         }
     }
   //  ...................
 }
 for (int Y = 0; Y < SmallH; Y++)
 {
     float PosX = 0;
     float AddX = (SmallW - 1.0f) / Width;        //  主要是为了减少下面插值向右增1的指针超过范围,但这样做其实是和精确的算法有一点点差异的
     float *LinePDA = TempA + Y * Width * Channel;    //  TempA和TempB为临时分配的大小为(SmallH * Width * Channel * sizeof(float)大小的内存
     float *LinePDB = TempB + Y * Width * Channel;
     float *LinePA = MeanA + Y * SmallW * Channel;
     float *LinePB = MeanB + Y * SmallW * Channel;
     if (Channel == 1)
     {
         for (int X = 0; X < Width; X++)
         {
             int XX = (int)PosX;
             float PartXX = PosX - XX;
             float InvertXX = 1 - PartXX;
             float *PtLeftA = LinePA + XX;
             float *PtLeftB = LinePB + XX;
             LinePDA[X] = PtLeftA[0] * InvertXX + PtLeftA[1] * PartXX;
             LinePDB[X] = PtLeftB[0] * InvertXX + PtLeftB[1] * PartXX;
             PosX += AddX;
         }
     }
   //  ...................
 }

  这段代码用SSE去优化的有害的脑细胞有点多,而且由于其计算量不是太大,意义恐怕有限。

  那段代码用SSE去优化的妨害的脑细胞有点多,而且由于其计算量不是太大,意义或许有限。

  而由第一个图到第五个图的进度大约可有效下边的代码表述:

  而由第一个图到第八个图的进程大概可使得上面的代码表述:

 for (int Y = 0; Y < Height; Y++)
 {
     float PosY = Y * (SmallH - 1.0f) / Height;
     int YY = (int)PosY;
     float PartYY = PosY - YY;
     float InvertYY = 1 - PartYY;
     byte *LinePS = Guide + Y * Stride;
     byte *LinePD = Dest + Y * Stride;
     float *PtTopA = TempA + YY * Width * Channel;
     float *PtBottomA = PtTopA + Width * Channel;
     float *PtTopB = TempB + YY * Width * Channel;
     float *PtBottomB = PtTopB + Width * Channel;
     for (int X = 0;; X < Width * Channel; X++)
     {
         float ValueA = PtTopA[X] * InvertYY + PtBottomA[X] * PartYY;
         float ValueB = PtTopB[X] * InvertYY + PtBottomB[X] * PartYY;
         LinePD[X] = IM_ClampFHtoByte(ValueA * LinePS[X] + ValueB * 255);
     }
 }
 for (int Y = 0; Y < Height; Y++)
 {
     float PosY = Y * (SmallH - 1.0f) / Height;
     int YY = (int)PosY;
     float PartYY = PosY - YY;
     float InvertYY = 1 - PartYY;
     byte *LinePS = Guide + Y * Stride;
     byte *LinePD = Dest + Y * Stride;
     float *PtTopA = TempA + YY * Width * Channel;
     float *PtBottomA = PtTopA + Width * Channel;
     float *PtTopB = TempB + YY * Width * Channel;
     float *PtBottomB = PtTopB + Width * Channel;
     for (int X = 0;; X < Width * Channel; X++)
     {
         float ValueA = PtTopA[X] * InvertYY + PtBottomA[X] * PartYY;
         float ValueB = PtTopB[X] * InvertYY + PtBottomB[X] * PartYY;
         LinePD[X] = IM_ClampFHtoByte(ValueA * LinePS[X] + ValueB * 255);
     }
 }

  注意最终的IM_ClampFHtoByte函数是将括号内的值限制在0和255里头的。

  注意最终的IM_ClampFHtoByte函数是将括号内的值限制在0和255之间的。

     
有不少爱人可能不理解,固然把下边的IM_ClampFHtoByte那几个函数去掉,直接使用括号内的代码,VS的编译器能够很好的对上边代码举办向量化编译(VS编译只要您没有把代码生成–》启用增强指令集设置成无增强指令/arch:IA32,哪怕设置为未设置,都会把浮点的代码编译为SIMD相关指令的),而如果大家对两样的Channel,比如3通道4通道在循环里展开后,很懊恼,根据大家的拓展循环的反驳,速度相应加紧,但实况却反而了。所以大家须要充足了解编译器的向量化特性,就能写成更高效的代码。

     
有好多仇人或者不通晓,假使把地点的IM_ClampFHtoByte那么些函数去掉,间接动用括号内的代码,VS的编译器可以很好的对上边代码进行向量化编译(VS编译只要您未曾把代码生成–》启用增强指令集设置成无增强指令/arch:IA32,哪怕设置为未设置,都会把浮点的代码编译为SIMD相关指令的),而一旦大家对两样的Channel,比如3通道4通路在循环里举行后,很不佳,按照大家的进行循环的答辩,速度相应加紧,但实际却反而了。所以大家需要足够掌握编译器的向量化特性,就能写成更敏捷的代码。

   
 由于在测算进程中确确实实存在部分结实大于了0和255的限制,因此一旦把IM_ClampFHtoByte函数去除,对有些图像会出现噪点,因而,大家不能一心依靠编译器的向量化优化了,那就不可能不团结写SIMD指令,由于SIMD自带了饱和处理的相关函数,而上述内部的X
的for循环是很不难用SSE处理的,唯一需求小心的就是亟需把LinePS对应的字节数据转换为浮点数据,那里我几乎的提醒能够用如下指令将8个字节数据转换为8个浮点数:

   
 由于在测算进程中确确实实存在部分结果超越了0和255的限制,由此一旦把IM_ClampFHtoByte函数去除,对有些图像会油但是生噪点,因而,大家不可能一心依赖编译器的向量化优化了,那就无法不协调写SIMD指令,由于SIMD自带了饱和处理的相关函数,而上述内部的X
的for循环是很简单用SSE处理的,唯一需求小心的就是亟需把LinePS对应的字节数据转换为浮点数据,那里我概括的擢升可以用如下指令将8个字节数据转换为8个浮点数:

__m128i SrcI = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i const *)(LinePS + X)), Zero);        //    Load the lower 64 bits of the value pointed to by p into the lower 64 bits of the result, zeroing the upper 64 bits of the result.
__m128 SrcFL = _mm_cvtepi32_ps(_mm_unpacklo_epi16(SrcI, Zero));                                //    转换为浮点
__m128 SrcFH = _mm_cvtepi32_ps(_mm_unpackhi_epi16(SrcI, Zero));
__m128i SrcI = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i const *)(LinePS + X)), Zero);        //    Load the lower 64 bits of the value pointed to by p into the lower 64 bits of the result, zeroing the upper 64 bits of the result.
__m128 SrcFL = _mm_cvtepi32_ps(_mm_unpacklo_epi16(SrcI, Zero));                                //    转换为浮点
__m128 SrcFH = _mm_cvtepi32_ps(_mm_unpackhi_epi16(SrcI, Zero));

     
里面的浮点总计的长河的SSE代码就和一般的函数调用没什么却别,最终的写到LinePD那个字节数据的进度可以用_mm_storel_epi64以及有关活动搞定。

     
里面的浮点计算的进程的SSE代码就和一般的函数调用没什么却别,最终的写到LinePD这些字节数据的历程可以用_mm_storel_epi64以及关于活动搞定。

     
那里如此做的其余一个功利是在Y循环中统计是独自的,因而都足以应用OPENMP增加速度。

     
那里如此做的别的一个便宜是在Y循环中统计是单独的,因而都可以动用OPENMP加快。

     
使用SSE优化能将上述进程提速2倍以上。

     
使用SSE优化能将上述进程提速2倍以上。

     
其余一个题目,在上头的流程2的第一步中,对boxfilter的半径r也是拓展了同比例的缩短的,注意到boxfilter的半径寻常状态下我们都是用的整数,如果减弱后的r’也开展取整的话,举例来说,对于s
=4的情景下,半径为8、9、10、11那四种情景最后收获的导向滤波结果就完全相同了,似乎那不符合我们对算法严酷性的渴求,所以大家要支持一种浮点半径的boxfilter。

     
此外一个题目,在上头的流程2的率先步中,对boxfilter的半径r也是进展了同比例的紧缩的,注意到boxfilter的半径平日景况下大家都是用的平头,假诺裁减后的r’也展开取整的话,举例来说,对于s
=4的动静下,半径为8、9、10、11那四种情况最终获得的导向滤波结果就完全一致了,就像这不符合我们对算法严酷性的需求,所以我们要匡助一种浮点半径的boxfilter。

   
 普通意义的boxfilter肯定是无法支撑浮点半径的(那差别于高斯模糊),一种转移的法子就是取浮点半径前后的八个整形半径值做模糊,然后再线性插值,举个例子,假使下取样后的半径为4.4,则分级统计R1
= boxfilter(4)以及R2 = boxfilter(5),最终合成得到结果R:

   
 普通意义的boxfilter肯定是无能为力支撑浮点半径的(这差距于高斯模糊),一种转移的法子就是取浮点半径前后的三个整形半径值做模糊,然后再线性插值,举个例证,如果下取样后的半径为4.4,则分别统计R1
= boxfilter(4)以及R2 = boxfilter(5),最后合成得到结果R:

               R = R1 * (1 – 0.4) + R2
* 0.4;

               R = R1 * (1 – 0.4) + R2
* 0.4;

   
 如此处理后,在一大半景色下(除了下取样后的半径为整数,比如原本半径为12,s=4,那是r’=3),统计量又会微微伸张一些,需求总结小图的12次boxfilter了,不过何必纠结这么些了呢。

   
 如此处理后,在超过半数景况下(除了下取样后的半径为整数,比如原本半径为12,s=4,那是r’=3),计算量又会有点扩张某些,必要统计小图的12次boxfilter了,可是何必纠结这么些了啊。

   
 关于上述浮点版本的Boxfilter,其实还有一种更好的落到实处格局。我在13行代码完结最高效最高效的积分图像算法中也提供了一段落成方框模糊的代码,当然万分代码还不是最优的,因为中间的pixlecount须要种种像素都重新总结,其实当半径较时辰中间部分的像素的pixlecount为固定值,因而得以把边缘部分的像素特殊处理,对于本例,是急需举行的浮点版本的算法,那对于中等有些的
/ pixlecount操作就应该能够改为 *Invpixlecount,其中Invpixlecount =
1.0f/pixlecount,变除法为乘法,而且这一部分盘算仍可以很不难的用SSE已毕。我测试过,创新后的兑现和解析opencv中Box
Filter的完结并提议越来越加快的方案(源码共享)
 
那篇文好章的进程方驾齐驱,但此间有个优势就是可以互相的。此外,最重大的某些时,当要统计上述浮点版半径版本的boxfilter时,积分图是不要求再行重复总结的,而积分图的计算所占的耗时最少有一半左右。由此,那一个场馆使用积分图版本的盒子滤波会更有优势。

   
 关于上述浮点版本的Boxfilter,其实还有一种更好的完成格局。我在13行代码已毕最快捷最神速的积分图像算法中也提供了一段已毕方框模糊的代码,当然非常代码还不是最优的,因为中间的pixlecount须要各种像素都重新总计,其实当半径较小时中间有些的像素的pixlecount为固定值,由此得以把边缘部分的像素特殊处理,对于本例,是内需展开的浮点版本的算法,那对于中等部分的
/ pixlecount操作就应该可以变成 *Invpixlecount,其中Invpixlecount =
1.0f/pixlecount,变除法为乘法,而且那部分总括还能很不难的用SSE落成。我测试过,革新后的贯彻和解析opencv中Box
Filter的落成并提议进一步加速的方案(源码共享)
 
那篇文好章的速度连镳并轸,但此间有个优势就是可以互相的。此外,最重大的某些时,当要统计上述浮点版半径版本的boxfilter时,积分图是不必要重新重复总结的,而积分图的统计所占的耗时起码有一半左右。由此,那个场地使用积分图版本的盒子滤波会更有优势。

   
 在内存占用方面,也足以做多量的优化办事,哪怕是对下取样图举行拍卖,第一,导向前必须把图像的字节数据归一化为0到1中间的浮点数据,很肯定,大家假若下采样大小的归一化数据,那么那个进度就活该很当然的直接由原来大小图像投射到下取样的浮点数据,而不用再在中间转来转去, 那些下采样的内存占用大小为(W
* H )/(S * S) * channel * sizeof(float)
.第二,导向的中档的各进程用到了汪洋的中级变量,像原作者使用matlab的代码为了参数算法清楚,就是为各种中间数据分配内存,可是实际操作中,为节约资源,必须加以优化,大家注意观看,就会发觉有点变量用完就不会再度利用了,当导向图和原图不一致时,我计算了只必要4
* (W * H )/(S * S) * channel *
sizeof(float)大小的内存,借使导向图和原图相同,则只必要2 * (W * H )/(S
* S) * channel *
sizeof(float),那些数量或者含有下采样图的内存占用的吗。考虑在均值滤波里还亟需一份附加大小的内存,以及尾声混合时的为了涨价的
2 * (H / S) * W * channel *
sizeof(float)的内存,当S=4时加起来也就是原图多一点点的内存。

   
 在内存占用方面,也得以做大量的优化工作,哪怕是对下取样图进行处理,第一,导向前必须把图像的字节数据归一化为0到1之间的浮点数据,很引人注目,大家只要下采样大小的归一化数据,那么这么些历程就活该很自然的一贯由原本大小图像投射到下取样的浮点数据,而并非再在中间转来转去, 这几个下采样的内存占用大小为(W
* H )/(S * S) * channel * sizeof(float)
.第二,导向的中等的各进程用到了大批量的中等变量,像原小编使用matlab的代码为了参数算法清楚,就是为各类中间数据分配内存,不过实际操作中,为节约资源,必须加以优化,咱们注意观望,就会发现有点变量用完就不会另行行使了,当导向图和原图不一致时,我总计了只需要4
* (W * H )/(S * S) * channel *
sizeof(float)大小的内存,即使导向图和原图相同,则只必要2 * (W * H )/(S
* S) * channel *
sizeof(float),那些数量或者含有下采样图的内存占用的呢。考虑在均值滤波里还需求一份附加大小的内存,以及最终混合时的为了涨价的
2 * (H / S) * W * channel *
sizeof(float)的内存,当S=4时加起来也就是原图多一点点的内存。

   

   

   
 在一台I5的记录簿上,选择默许设置,以自己为导向图处理3000*2000的24位图像需求约55ms,要是是灰度图大约是20ms,那个和优化后的
boxblur速度基本一致,如若开启动二十四线程,比如开四个线程,还是可以提速25%左右,再多也无协助了。

   
 在一台I5的记录本上,接纳默许设置,以本人为导向图处理3000*2000的24位图像须求约55ms,如若是灰度图大概是20ms,那一个和优化后的
boxblur速度基本一致,如果开启动八线程,比如开八个线程,还可以提速25%左右,再多也无扶助了。

     

     

   
 共享下一个C#做的Demo,以便供有趣味的情西洋参考比较: http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar

   
 共享下一个C#做的Demo,以便供有趣味的爱丹参考比较: http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar

 

 

图片 11

图片 12

 

 

 

 

   
 本文纯属计流水账,未做详细分析。

   
 本文纯属计流水账,未做详细分析。

图片 13

图片 14