SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

  拉普拉斯金字塔融合是多图融合相关算法里最简单和最容易实现的一种,我们在看网络上大部分的文章都是在拿那个苹果和橙子融合在一起,变成一个果橙的效果作为例子说明。在这方面确实融合的比较好。但是本文我们主要讲下这个在图像增强方面的运用。

首先我们还是来讲下这个融合的过程和算法优化。

算法第一步:输入两个相同大小,位深的图像,通过拉普拉斯分解得到各自的拉普拉斯金字塔数据A和B。

  算法第二步:选择下低频部分的融合规则,这里的低频部分,其实就是高斯金字塔最顶层那里的数据,这个数据相当于是原图像的一个高斯模糊的下采样版本,反应了基本的图像轮廓和信息。

通常情况下,融合规则有三种:

(1)选择A;

(2)选择B;

(3)选择A和B的平均值。

算法第三步:选择高频部分的融合规则。高频代表了图像的边缘和细节,当然也可能是噪音。高频部分的数据保存在各自拉普拉斯金字塔的除最顶层外的层中(最顶层和高斯金字塔的最顶层共享数据)。

这里的融合规则就有多种,常用的比如如下几种:

(1)选择A和B中绝对值最大的;

(2)选择A和B领域中绝对值最大的。

  第一种规则比较容易理解,绝对值大(拉普拉斯金字塔数据有可能是负值得),表示这里的边缘强度越高,细节越丰富。

第二种也好理解,通常我们选择3*3领域。A的3*3领域的绝对值最大值如果大于B的3*3领域最大值,则选择A,否则选择B,这种做法的道理就是用领域去除一定的噪音影响。通常伴随着该种方法的还有一个叫做一致性检测的过程,即如果中心位置的融合系数选自原图像A变换的系数,而其周围领域内的融合系数大部分都选取自原图像B变换的系数 ,则把中心位置的融合系数修改为图像B变换后的系数,反之亦然。

 那还有一种基于基于3X3窗口内相似性测度,获取拉普拉斯金字塔融合结果的方法,这个可以参考:https://wenku.baidu.com/view/c8ae11adf61fb7360b4c65c4.html ,这种融合规则由于考虑了与相邻像素间的相关性,降低了对边缘的敏感性,能够有效减少融合像素的错误选取,在一定程度上显著提高了融合算法的鲁棒性,从而提高了融合效果。 实测这种也还可以,但是代码比较麻烦,这里不描述。

  算法第四步:高频和低频都已经处理好后,则重构图像得到结果值。

    一个简单的描述过程如下:

int IM_LaplacePyramidFusion(unsigned char *SrcA, unsigned char *SrcB, unsigned char *Dest, int Width, int Height, int Stride, LowFrequencyFusionRule Low, HighFrequencyFusionRule High, int Level)
{
int Channel = Stride / Width;
if ((SrcA == NULL) || (SrcB == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; int Status = IM_STATUS_OK; Level = IM_ClampI(Level, , IM_GetMaxPyramidLevel(Width, Height, )); // 经过测试如果直接处理到最小为1个像素的金子塔,效果并不好 Pryamid *GaussPyramid = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 必须用calloc,不然在后面的释放函数中可能存在野指针释放问题,高斯金字塔可以用同一个内存
Pryamid *LaplacePyramidA = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 图A的拉普拉斯金字塔
Pryamid *LaplacePyramidB = (Pryamid *)calloc(Level, sizeof(Pryamid)); // 图B的拉普拉斯金字塔
Pryamid *GaussPyramidD, *LaplacePyramidD; if ((GaussPyramid == NULL) || (LaplacePyramidA == NULL) || (LaplacePyramidB == NULL))
{
Status = IM_STATUS_OUTOFMEMORY;
goto FreeMemory;
} Status = IM_AllocatePyramidMemory(GaussPyramid, Width, Height, Stride, Level, true, sizeof(unsigned char)); // 分配内存,高斯金字塔的塔底就是原数据
if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_AllocatePyramidMemory(LaplacePyramidA, Width, Height, Stride, Level, false, sizeof(unsigned char));
if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_AllocatePyramidMemory(LaplacePyramidB, Width, Height, Stride, Level, false, sizeof(unsigned char));
if (Status != IM_STATUS_OK) goto FreeMemory; GaussPyramid[].Data = SrcA;
for (int Y = ; Y < Level; Y++) // 各级高斯金字塔
{
Status = IM_DownSample8U((unsigned char *)GaussPyramid[Y - ].Data, (unsigned char *)GaussPyramid[Y].Data, GaussPyramid[Y - ].Width, GaussPyramid[Y - ].Height, GaussPyramid[Y - ].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
} // 拉普拉斯金子塔的最顶层和高斯金字塔的是一样的额
memcpy(LaplacePyramidA[Level - ].Data, GaussPyramid[Level - ].Data, LaplacePyramidA[Level - ].Height * LaplacePyramidA[Level - ].Stride); for (int Y = Level - ; Y >= ; Y--)
{
Status = IM_UpSampleSub8U((unsigned char *)GaussPyramid[Y + ].Data, (unsigned char *)GaussPyramid[Y].Data, (unsigned char *)LaplacePyramidA[Y].Data, GaussPyramid[Y + ].Width, GaussPyramid[Y + ].Height, GaussPyramid[Y + ].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
} GaussPyramid[].Data = SrcB;
for (int Y = ; Y < Level; Y++) // 各级高斯金字塔
{
Status = IM_DownSample8U((unsigned char *)GaussPyramid[Y - ].Data, (unsigned char *)GaussPyramid[Y].Data, GaussPyramid[Y - ].Width, GaussPyramid[Y - ].Height, GaussPyramid[Y - ].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
} // 拉普拉斯金子塔的最顶层和高斯金字塔的是一样的额
memcpy(LaplacePyramidB[Level - ].Data, GaussPyramid[Level - ].Data, LaplacePyramidB[Level - ].Height * LaplacePyramidB[Level - ].Stride); for (int Y = Level - ; Y >= ; Y--)
{
Status = IM_UpSampleSub8U((unsigned char *)GaussPyramid[Y + ].Data, (unsigned char *)GaussPyramid[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, GaussPyramid[Y + ].Width, GaussPyramid[Y + ].Height, GaussPyramid[Y + ].Stride, GaussPyramid[Y].Width, GaussPyramid[Y].Height, GaussPyramid[Y].Stride, Channel);
if (Status != IM_STATUS_OK) goto FreeMemory;
} LaplacePyramidD = LaplacePyramidA; // 低频部分的融合
PyramidLowFreqFusion((unsigned char *)LaplacePyramidA[Level - ].Data, (unsigned char *)LaplacePyramidB[Level - ].Data, (unsigned char *)LaplacePyramidD[Level - ].Data, LaplacePyramidA[Level - ].Width, LaplacePyramidA[Level - ].Height, LaplacePyramidA[Level - ].Stride, Low); for (int Y = ; Y < Level - ; Y++) // 高频部分的融合
{
if (High == SinglePixelAbsMax)
PyramidHighFreqFusion_AbsMax((unsigned char *)LaplacePyramidA[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, (unsigned char *)LaplacePyramidD[Y].Data, LaplacePyramidA[Y].Width, LaplacePyramidA[Y].Height, LaplacePyramidA[Y].Stride);
else if (High == LocalAbsMaxWithConsistencyCheck)
PyramidHighFreqFusion_3X3MaxAbsValue((unsigned char *)LaplacePyramidA[Y].Data, (unsigned char *)LaplacePyramidB[Y].Data, (unsigned char *)LaplacePyramidD[Y].Data, LaplacePyramidA[Y].Width, LaplacePyramidA[Y].Height, LaplacePyramidA[Y].Stride);
// else
//PyramidHighFreqFusion_3X3Similarity(LaplacePyramidA[Y], LaplacePyramidB[Y], LaplacePyramidD[Y], PryamidW[Y], PryamidH[Y], Channel);*/
} GaussPyramidD = GaussPyramid;
memcpy(GaussPyramidD[Level - ].Data, LaplacePyramidD[Level - ].Data, LaplacePyramidD[Level - ].Height * LaplacePyramidD[Level - ].Stride); for (int Y = Level - ; Y >= ; Y--) // 重构拉普拉斯金子塔
{
if (Y != )
IM_UpSampleAdd8U((unsigned char *)GaussPyramidD[Y + ].Data, (unsigned char *)LaplacePyramidD[Y].Data, (unsigned char *)GaussPyramidD[Y].Data, GaussPyramidD[Y + ].Width, GaussPyramidD[Y + ].Height, GaussPyramidD[Y + ].Stride, GaussPyramidD[Y].Width, GaussPyramidD[Y].Height, GaussPyramidD[Y].Stride, Channel);
else
IM_UpSampleAdd8U((unsigned char *)GaussPyramidD[Y + ].Data, (unsigned char *)LaplacePyramidD[Y].Data, Dest, GaussPyramidD[Y + ].Width, GaussPyramidD[Y + ].Height, GaussPyramidD[Y + ].Stride, GaussPyramidD[Y].Width, GaussPyramidD[Y].Height, GaussPyramidD[Y].Stride, Channel);
} FreeMemory:
IM_FreeGaussPyramid(GaussPyramid, Level, true);
IM_FreeLaplacePyramid(LaplacePyramidA, Level);
IM_FreeLaplacePyramid(LaplacePyramidB, Level);
if (GaussPyramid != NULL) free(GaussPyramid);
if (LaplacePyramidA != NULL) free(LaplacePyramidA);
if (LaplacePyramidB != NULL) free(LaplacePyramidB);
return Status;
}

  我们上面的所有的高斯或者拉普拉斯金字塔数据都是用unsigned char类型来描述的, 为什么可以这样做呢,做个简单的分析。第一,高斯金字塔用byte是毫无疑问的,第二,前面说了,严格的拉普拉斯金字塔是有负数的,但是我们考虑到一个这个负数大于-127的可能性是非常小的,这种情况可能会在二值图像中出现,而二值图的处理算法中能用到金字塔吗,我似乎没听说过,所以我们可以把拉普拉斯金字塔的数据加上127,让整体在0和255之间,这样有很多算法都直接调用了。

在我们的高频或者低频的选取过程中,因为都不存在新的数据出来,也就是没有啥几何乘积计算,因此,用byte保存也不存在啥大问题。

  PyramidLowFreqFusion低频部分的融合代码非常简单,如下所示:

///    低频部分的融合,一般有三种方式。1、取图像A的系数; 2、取图像B的系数;3、取图像A和个B系数的平均值。
/// 其实这里应该用高斯金字塔的最顶层数据,只是由于拉普拉斯和高斯金字塔共享这一层数据,所以也可以直接这样写
int PyramidLowFreqFusion(unsigned char *LaplacePyramidA, unsigned char *LaplacePyramidB, unsigned char *LaplacePyramidD, int Width, int Height, int Stride, LowFrequencyFusionRule Low)
{
int Channel = Stride / Width;
if ((LaplacePyramidA == NULL) || (LaplacePyramidB == NULL) || (LaplacePyramidD == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; if (Low == SrcA)
memcpy(LaplacePyramidD, LaplacePyramidA, Height * Stride);
else if (Low == SrcB)
memcpy(LaplacePyramidD, LaplacePyramidB, Height * Stride);
else
{
// 也可以考虑某一副图占比高一点
//int BlockSize = 16, Block = (Height * Stride) / BlockSize;
//for (int Y = 0; Y < Block * BlockSize; Y += BlockSize)
//{
// __m128i SrcA = _mm_loadu_si128((__m128i *)(LaplacePyramidA + Y));
// __m128i SrcB = _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y));
// __m128i Dst1 = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(_mm_cvtepu8_epi16(SrcA), _mm_set1_epi16(3)), _mm_cvtepu8_epi16(SrcA)), 2);
// __m128i Dst2 = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_srli_si128(SrcA, 8)), _mm_set1_epi16(3)), _mm_cvtepu8_epi16(_mm_srli_si128(SrcA, 8))), 2);
// _mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_packus_epi16(Dst1, Dst2));
//}
//for (int Y = Block * BlockSize; Y < Height * Stride; Y++)
//{
// LaplacePyramidD[Y] = (LaplacePyramidA[Y] * 3 + LaplacePyramidB[Y]) >> 2; // 最高层(低频)系数取平均
//} // **************************** 真正意义上的平均值 *************************************
int BlockSize = , Block = (Height * Stride) / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
_mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_avg_epu8(_mm_loadu_si128((__m128i *)(LaplacePyramidA + Y)), _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y))));
}
for (int Y = BlockSize * BlockSize; Y < Height * Stride; Y++)
{
LaplacePyramidD[Y] = (LaplacePyramidA[Y] + LaplacePyramidB[Y]) >> ; // 最高层(低频)系数取平均
}
}
return IM_STATUS_OK;
}

  取平均直接用_mm_avg_epu8就可以了。

  高频部分如果选择绝对值最大的方案代码也是很简单的:

///    基于系数绝对值取大的融合策略进行拉普拉斯金字塔图像融合。
int PyramidHighFreqFusion_AbsMax(unsigned char *LaplacePyramidA, unsigned char *LaplacePyramidB, unsigned char *LaplacePyramidD, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((LaplacePyramidA == NULL) || (LaplacePyramidB == NULL) || (LaplacePyramidD == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; int BlockSize = , Block = (Height * Stride) / BlockSize; __m128i C127 = _mm_set1_epi8();
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
__m128i SrcA = _mm_loadu_si128((__m128i *)(LaplacePyramidA + Y));
__m128i SrcB = _mm_loadu_si128((__m128i *)(LaplacePyramidB + Y));
__m128i Flag = _mm_cmpgt_epu8(_mm_absdiff_epu8(SrcA, C127), _mm_absdiff_epu8(SrcB, C127));
_mm_storeu_si128((__m128i *)(LaplacePyramidD + Y), _mm_blendv_epi8(SrcB, SrcA, Flag));
}
for (int Y = Block * BlockSize; Y < Height * Stride; Y++)
{
if (IM_Abs(LaplacePyramidA[Y] - ) > IM_Abs(LaplacePyramidB[Y] - ))
LaplacePyramidD[Y] = LaplacePyramidA[Y];
else
LaplacePyramidD[Y] = LaplacePyramidB[Y];
}
return IM_STATUS_OK;
}

  其中的_mm_absdiff_epu8函数如下所示:

// 返回8位字节数数据的差的绝对值
inline __m128i _mm_absdiff_epu8(__m128i a, __m128i b)
{
return _mm_or_si128(_mm_subs_epu8(a, b), _mm_subs_epu8(b, a));
}

  这里要减去127的主要原因是前面所说的再计算拉普拉斯金字塔时我们增加了127,而这里计算时我们需要真正的拉普拉斯金子塔数据。

用_mm_blendv_epi8可以方便的解决后续的抉择问题,有点相当于C语言的里的三目运算符。

  关于PyramidHighFreqFusion_3X3MaxAbsValue这个函数就要复杂很多了,首先这种3*3的领域计算,我还是推荐我在sobel边缘算子优化一文中提到的那种优化结构,可以支持In-Place操作,同时还可以完美处理边缘。算法的流程是标准化的。就是求出各自3*3领域的绝对值的最大值,然后进行比较,为了后续的一致性检测,比较的结果需要写入个临时内存,在实现时,我们做了如下处理:

_mm_storeu_si128((__m128i *)(LinePF + X), _mm_cmpgt_epu8(MaxA, MaxB));

  其中的MaxA和MaxB为领域的最大值,这里也就是说A>B,对应的Flag位置设置为255,否则设置为0(也是用的unsigned char内存保存的)。

  为什么这样做,有两个好处,第一,我们在后续的一致性检测里,可以充分利用这个数据的特殊性。在一致性检测里,我们要判断周边的是不是大部分都和中心点来自同一个数据源,一种处理方式就是把周边的8个点的数据都相加,如果中心点为0,周边的和大于255*4,则表明周边和中心不太一致,需要把中心的点改为255,如果中心点为255,而周边的点的和小于255*4,则中心点要改为0,用代码表示如下:

int Sum = First[X + 0] + First[X + 1] + First[X + 2] + Second[X + 0] + Second[X + 2] + Third[X + 0] + Third[X + 1] + Third[X + 2];
if (LinePD[X] == && Sum > * ) // 如果当前点为0,并且周边8个点中至少有5个点为1,则把当前点的值修改为1。
LinePD[X] = ;
else if (LinePD[X] == && Sum < * ) // 如果当前点为1,并且周边8个点中至少有5个点为0,则把当前点的值修改为0。
LinePD[X] = ;

  第二,在SSE优化时,这个特殊性是可以帮上大忙的,主要体现在两个方面,一时如上的Sum计算过程,我们如果直接按照255相加,则8个数会超出8位所表达的范围,这样就要转换到16位的空间进行计算了,但是如果我们把epu8看成epi8,这个时候255就编程了-1,此时的加法我使用_mm_add_epi8,则结果能在epi8的范围内,这样一次性就可以处理16个像素了。二是后续我们需要根据这个Flag对组中的输出结果做明示,这个时候我们就可以直接使用这个Flag做mask供_mm_blendv_epi8调用。

  我们在俩看看上面的判断部分如何用SSE处理,因为SSE不善于做分支,所以我们需要想办法,这样做,我们看看下面的代码是不是和上面的一个意思:

//    if ((LinePD[X] == 0 && Sum > 255 * 4) || ((LinePD[X] == 255 && Sum < 255 * 4)))
// LinePD[X] = 255 - LinePD[X];

  但是这里是有不同的,我们可以很方便的对上述代码SSE处理:

__m128i FlagA = _mm_and_si128(_mm_cmpeq_epi8(Current, _mm_setzero_si128()), _mm_cmplt_epi8(Sum, _mm_set1_epi8(-)));
__m128i FlagB = _mm_and_si128(_mm_cmpeq_epi8(Current, _mm_set1_epi8()), _mm_cmpgt_epi8(Sum, _mm_set1_epi8(-)));
__m128i FlagAB = _mm_or_si128(FlagA, FlagB);
_mm_storeu_si128((__m128i *)(LinePD + X), _mm_blendv_epi8(Current, _mm_subs_epu8(_mm_set1_epi8(), Current), FlagAB)); // 局部取反

  这样效率可以大大的提高。

  优化方面基本讲完了,当然,由于我没有共享代码,大部分其实是写给自己看的,因为我怕时间长了自己都不知道为什么要这样写。

下面我们测试下算法效果和性能:

SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)  SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)                  SrcA                                    SrcB

SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)  SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

低频=SrcA,高频=3*3领域, Level = 10                         低频=SrcB,高频=3*3领域, Level = 10

SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)  SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

            低频=(SrcA + SrcB) / 2,高频=3*3领域, Level = 10                     低频=(SrcA + SrcB) / 2,高频=3*3领域, Level =5

  上述原图B是某个增强算法处理后的结果,很明显,改算法对图像右下角的暗部的增强效果很好,但是同时图像上部的天空区域已经完全过曝了,天空的云消失不见了,这在很多增强算法中都会出现类似的情况,而在原图中天空的细节本身就已经比较好了,因此,我们尝试用不同的选项对这两幅图做拉普拉斯融合,如果高频选择SrcA,则整体融合后的图像暗部增强的不明显,选择SrcB,则天空恢复的不够好,选择(SrcA + SrcB) / 2则能对天空和暗部都有较好的恢复。

另外,金字塔的层数对结果也有一定的影响,在最后两张图中,我们可以看到Level=5时的效果要比为10时的稍微好一点,我们一般也不建议高斯金字塔的最顶层取得太小,通常,我们取5层金字塔应该能获得较为满意的效果。

  效率方面,一般1920*1080的彩色图像,这种混合大概需要20-30ms左右(取决于选择的参数),一半的时间用于了金字塔的构建。

其他说明:

1、在PyramidLowFreqFusion函数中,我们注释掉了一部分,这部分注释的代码的本意是低频的算法我们不一定一定要是取平均值,也可以根据实际的情况更加强调某一对象,比如假如SrcB是处理后的部分,我们可以把他的权重设置为75%,而SrcA的权重设置为25%。

2、对于彩色图像,如果三个通道独立写,则对每个像素,有可能每个通道的高频或低频部分会选自不同的来源,这样有可能导致结果出现异常的彩色,一种解决方案是采用高频或低频部分的灰度信号作为判断源。

3、金子塔融合的基本原理还是保留更多的细节,因此,如果对一个原始图像进行了类似锐化方面的处理后,这个图在和原图进行融合,那基本上不会有什么变化的,柔和的结果必然是靠近锐化后的结果图。这个大家可以自己做实验。

4、这个融合的过程基本不需要外接的参数接入,我们可以考虑把他作为某些算法的最后一个默认步骤。

5、对于任意两幅大小相同的图,这个算法融合的结果也是蛮有意思的,如下:

SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一) SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一) SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

 当然,这种融合还是有一定的限制的,下一节,我们将讨论基于蒙版的金字塔融合,那里可以更加智能的获取更好的融合效果。

提供一个DEMO供测试效果:极度优化版本工程:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,见MultiImage->LaplacePyramidFusion菜单。

SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

上一篇:如何改变Android标准键的颜色?


下一篇:django文件上传和序列化