前言

阅读了博主的三次卷积插值的进一步SSE优化文章后,对这个算法产生了很大的兴趣,然而复现代码的过程是艰难的,好在作者了提供了部分代码,经过1周的努力,我实现了完整的支持1通道,3通道,4通道的三次卷积插值算法的SSE优化代码。现在准备分享一下这个算法的实现过程。

算法原理

之前我在我的另外一个工程: https://github.com/BBuf/Image-processing-algorithm/tree/master/Image%20Interpolation 实现过3次卷积插值,百度百科提供的原理如下:https://baike.baidu.com/item/%E5%8F%8C%E4%B8%89%E6%AC%A1%E6%8F%92%E5%80%BC/11055947?fr=aladdin 。这个过程可以用另外一套理论来解释,也就是:

SSE优化系列十一:SSE优化三次卷积插值算法_插值图片截图自ImageShop的文章。先贴出使用三次卷积插值的简单代码:

void Bicubic_Original(unsigned char *Src, int Width, int Height, int Stride, unsigned char *Pixel, float X, float Y)
{
int Channel = Stride / Width;
int PosX = floor(X), PosY = floor(Y);
float PartXX = X - PosX, PartYY = Y - PosY;

unsigned char *Pixel00 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX - 1, PosY - 1);
unsigned char *Pixel01 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 0, PosY - 1);
unsigned char *Pixel02 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 1, PosY - 1);
unsigned char *Pixel03 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 2, PosY - 1);
unsigned char *Pixel10 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX - 1, PosY + 0);
unsigned char *Pixel11 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 0, PosY + 0);
unsigned char *Pixel12 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 1, PosY + 0);
unsigned char *Pixel13 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 2, PosY + 0);
unsigned char *Pixel20 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX - 1, PosY + 1);
unsigned char *Pixel21 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 0, PosY + 1);
unsigned char *Pixel22 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 1, PosY + 1);
unsigned char *Pixel23 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 2, PosY + 1);
unsigned char *Pixel30 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX - 1, PosY + 2);
unsigned char *Pixel31 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 0, PosY + 2);
unsigned char *Pixel32 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 1, PosY + 2);
unsigned char *Pixel33 = GetCheckedPixel(Src, Width, Height, Stride, Channel, PosX + 2, PosY + 2);

float U0 = SinXDivX(1 + PartXX), U1 = SinXDivX(PartXX);
float U2 = SinXDivX(1 - PartXX), U3 = SinXDivX(2 - PartXX);
float V0 = SinXDivX(1 + PartYY), V1 = SinXDivX(PartYY);
float V2 = SinXDivX(1 - PartYY), V3 = SinXDivX(2 - PartYY);

for (int I = 0; I < Channel; I++)
{
float Sum1 = (Pixel00[I] * U0 + Pixel01[I] * U1 + Pixel02[I] * U2 + Pixel03[I] * U3) * V0;
//printf("%.5f\n", Sum1);
float Sum2 = (Pixel10[I] * U0 + Pixel11[I] * U1 + Pixel12[I] * U2 + Pixel13[I] * U3) * V1;
//printf("%.5f\n", Sum2);
float Sum3 = (Pixel20[I] * U0 + Pixel21[I] * U1 + Pixel22[I] * U2 + Pixel23[I] * U3) * V2;
//printf("%.5f\n", Sum3);
float Sum4 = (Pixel30[I] * U0 + Pixel31[I] * U1 + Pixel22[I] * U2 + Pixel33[I] * U3) * V3;
//printf("%.5f\n", Sum4);
// printf("%d %.5f %.5f %.5f %.5f\n", I, Sum1, Sum2, Sum3, Sum4);
Pixel[I] = ClampToByte(Sum1 + Sum2 + Sum3 + Sum4 + 0.5f);
}
}

从这里可以看到,我们引入上面那2条曲线正是为了计算每个像素点在三次卷积插值时的系数。而提出一个拟合得表达式来近似Sin曲线的原因是什么呢?实际上当我们将X取值为0.3,然后按照这2个公式计算的结果如下:

​SinXDivX_Standard(1 + X) + SinXDivX_Standard(X) + SinXDivX_Standard(1 - X) + SinXDivX_Standard(2 - X) = 0.8767​

​SinXDivX(1 + X) + SinXDivX(X) + SinXDivX(1 - X) + SinXDivX(2 - X) =1​​, 可以看到如果我们使用拟合得表达式,权重系数就不需要再进行归一化处理了。这两个函数的代码如下:

// 该函数计算插值曲线sin(x * PI) / (x * PI)的值,下面是它的近似拟合表达式
float SinXDivX(float X) {
const float a = -1; //a还可以取 a=-2,-1,-0.75,-0.5等等,起到调节锐化或模糊程度的作用
X = abs(X);
float X2 = X * X, X3 = X2 * X;
if (X <= 1)
return (a + 2) * X3 - (a + 3) * X2 + 1;
else if (X <= 2)
return a * X3 - (5 * a) * X2 + (8 * a) * X - (4 * a);
else
return 0;
}

// 精确计算插值曲线sin(x * PI) / (x * PI)
float SinXDivX_Standard(float X) {
if (abs(X) < 0.000001f)
return 1;
else
return sin(X * 3.1415926f) / (X * 3.1415926f);
}

三次卷积插值的原始实现

// 原始的插值算法
void IM_Resize_Cubic_Origin(unsigned char *Src, unsigned char *Dest, int SrcW, int SrcH, int StrideS, int DstW, int DstH, int StrideD) {
int Channel = StrideS / SrcW;
if ((SrcW == DstW) && (SrcH == DstH)) {
memcpy(Dest, Src, SrcW * SrcH * Channel * sizeof(unsigned char));
return;
}
printf("%d\n", Channel);
for (int Y = 0; Y < DstH; Y++)
{
unsigned char *LinePD = Dest + Y * StrideD;
float SrcY = (Y + 0.4999999f) * SrcH / DstH - 0.5f;
for (int X = 0; X < DstW; X++)
{
float SrcX = (X + 0.4999999f) * SrcW / DstW - 0.5f;
Bicubic_Original(Src, SrcW, SrcH, StrideS, LinePD, SrcX, SrcY);
LinePD += Channel;
}
}
}

优化1:边界特殊处理+查表法

对原始实现一个显然的优化点在于,对于每个像素都要计算领域每个像素的系数,显然这个可以用定点化查表来实现。同时,如果我们把边界特殊处理,我们就可以省去很多GetCheckedPixel函数的调用,这个加速也是非常明显的。最后在使用了边界特殊处理+定点化查表后,速度比原始实现大概提升了2倍。

// C语言实现的查表+插值算法
void IM_Resize_Cubic_Table(unsigned char *Src, unsigned char *Dest, int SrcW, int SrcH, int StrideS, int DstW, int DstH, int StrideD) {
int Channel = StrideS / SrcW;
if ((SrcW == DstW) && (SrcH == DstH)) {
memcpy(Dest, Src, SrcW * SrcH * Channel * sizeof(unsigned char));
return;
}
short *SinXDivX_Table = (short *)malloc(513 * sizeof(short));
for (int I = 0; I < 513; I++)
SinXDivX_Table[I] = int(0.5 + 256 * SinXDivX(I / 256.0f)); // 建立查找表,定点化
int AddX = (SrcW << 16) / DstW, AddY = (SrcH << 16) / DstH;
int ErrorX = -(1 << 15) + (AddX >> 1), ErrorY = -(1 << 15) + (AddY >> 1);

int StartX = ((1 << 16) - ErrorX) / AddX + 1; // 计算出需要特殊处理的边界
int StartY = ((1 << 16) - ErrorY) / AddY + 1; // y0+y*yr>=1; y0=ErrorY => y>=(1-ErrorY)/yr
int EndX = (((SrcW - 3) << 16) - ErrorX) / AddX + 1;
int EndY = (((SrcH - 3) << 16) - ErrorY) / AddY + 1; // y0+y*yr<=(height-3) => y<=(height-3-ErrorY)/yr
if (StartY >= DstH) StartY = DstH;
if (StartX >= DstW) StartX = DstW;
if (EndX < StartX) EndX = StartX;
if (EndY < StartY) EndY = StartY;
// 输出边界
//printf("%d %d %d %d\n", StartX, StartY, EndX, EndY);
int SrcY = ErrorY;
for (int Y = 0; Y < StartY; Y++, SrcY += AddY) // 前面的不是都有效的取样部分数据
{
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0, SrcX = ErrorX; X < DstW; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
for (int Y = StartY; Y < EndY; Y++, SrcY += AddY)
{
int SrcX = ErrorX;
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0; X < StartX; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
for (int X = StartX; X < EndX; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Center(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
for (int X = EndX; X < DstW; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
for (int Y = EndY; Y < DstH; Y++, SrcY += AddY)
{
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0, SrcX = ErrorX; X < DstW; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
free(SinXDivX_Table);
}

优化3:SSE优化

如果是一个通道和三个通道的图像,是很容易把上面的过程翻译为SSE代码的。比较复杂的是对于24位图像的处理,我这里偷了个懒,将RGB图像直接为了RGBA图像,在RGBA图像操作之后将结果图像又转为了RGB图像,这个转化过程也使用了SSE优化。在SSE优化系列2-高斯滤波中,留下了一个坑,就是BGR和BGRA相互转化的SSE优化,我这里填了这个坑点。实现的代码如下:

void ConvertBGR8U2BGRAF_SSE(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) {
const int BlockSize = 4;
int Block = (Width - 2) / BlockSize;
__m128i Mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
__m128i Mask2 = _mm_setr_epi8(0, 2, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1);
__m128i Zero = _mm_setzero_si128();
for (int Y = 0; Y < Height; Y++) {
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Width * 4;
int X = 0;
for (; X < Block * BlockSize; X += BlockSize, LinePS += BlockSize * 3, LinePD += BlockSize * 4) {
__m128i SrcV = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)LinePS), Mask);
__m128i Src16L = _mm_unpacklo_epi8(SrcV, Zero);
__m128i Src16H = _mm_unpackhi_epi8(SrcV, Zero);

_mm_storeu_si128((__m128i *)(LinePD + 0), _mm_shuffle_epi8(_mm_unpacklo_epi32(Src16L, Zero), Mask2));
_mm_storeu_si128((__m128i *)(LinePD + 4), _mm_shuffle_epi8(_mm_unpackhi_epi32(Src16L, Zero), Mask2));
_mm_storeu_si128((__m128i *)(LinePD + 8), _mm_shuffle_epi8(_mm_unpacklo_epi32(Src16H, Zero), Mask2));
_mm_storeu_si128((__m128i *)(LinePD + 12), _mm_shuffle_epi8(_mm_unpackhi_epi32(Src16H, Zero), Mask2));
}
for (; X < Width; X++, LinePS += 3, LinePD += 4) {
LinePD[0] = LinePS[0]; LinePD[1] = LinePS[1]; LinePD[2] = LinePS[2]; LinePD[3] = 0;
}
}
}

void ConvertBGRAF2BGR8U_SSE(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) {
const int BlockSize = 4;
int Block = (Width - 2) / BlockSize;
//__m128i Mask = _mm_setr_epi8(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
__m128i MaskB = _mm_setr_epi8(0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1);
__m128i MaskG = _mm_setr_epi8(1, 5, 9, 13, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1);
__m128i MaskR = _mm_setr_epi8(2, 6, 10, 14, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1);
__m128i Zero = _mm_setzero_si128();
for (int Y = 0; Y < Height; Y++) {
unsigned char *LinePS = Src + Y * Width * 4;
unsigned char *LinePD = Dest + Y * Stride;
int X = 0;
for (; X < Block * BlockSize; X += BlockSize, LinePS += BlockSize * 4, LinePD += BlockSize * 3) {
__m128i SrcV = _mm_loadu_si128((const __m128i*)LinePS);
__m128i B = _mm_shuffle_epi8(SrcV, MaskB);
__m128i G = _mm_shuffle_epi8(SrcV, MaskG);
__m128i R = _mm_shuffle_epi8(SrcV, MaskR);
__m128i Ans1 = Zero, Ans2 = Zero, Ans3 = Zero;
Ans1 = _mm_or_si128(Ans1, _mm_shuffle_epi8(B, _mm_setr_epi8(0, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans1 = _mm_or_si128(Ans1, _mm_shuffle_epi8(G, _mm_setr_epi8(-1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans1 = _mm_or_si128(Ans1, _mm_shuffle_epi8(R, _mm_setr_epi8(-1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));

Ans2 = _mm_or_si128(Ans2, _mm_shuffle_epi8(B, _mm_setr_epi8(-1, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans2 = _mm_or_si128(Ans2, _mm_shuffle_epi8(G, _mm_setr_epi8(1, -1, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans2 = _mm_or_si128(Ans2, _mm_shuffle_epi8(R, _mm_setr_epi8(-1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));

Ans3 = _mm_or_si128(Ans3, _mm_shuffle_epi8(B, _mm_setr_epi8(-1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans3 = _mm_or_si128(Ans3, _mm_shuffle_epi8(G, _mm_setr_epi8(-1, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));
Ans3 = _mm_or_si128(Ans3, _mm_shuffle_epi8(R, _mm_setr_epi8(2, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)));

_mm_storeu_si128((__m128i*)(LinePD + 0), Ans1);
_mm_storeu_si128((__m128i*)(LinePD + 4), Ans2);
_mm_storeu_si128((__m128i*)(LinePD + 8), Ans3);
}
for (; X < Width; X++, LinePS += 4, LinePD += 3) {
LinePD[0] = LinePS[0]; LinePD[1] = LinePS[1]; LinePD[2] = LinePS[2];
}
}
}

都是一些常规操作,可以看着数据模拟一下寄存器变量变化过程。这里再提供一个输出寄存器变量值的函数:

void debug(__m128i var) {
uint8_t *val = (uint8_t*)&var;//can also use uint32_t instead of 16_t
printf("Numerical: %i %i %i %i %i %i %i %i %i %i %i %i %i %i %i %i\n",
val[0], val[1], val[2], val[3], val[4], val[5],
val[6], val[7], val[8], val[9], val[10], val[11], val[12], val[13],
val[14], val[15]);
}

最后对单通道和四通道的图像进行三次卷积插值的SSE优化代码为:

// 使用SSE优化立方插值算法
// 最大支持图像大小为: 32767*32767
void IM_Resize_SSE(unsigned char *Src, unsigned char *Dest, int SrcW, int SrcH, int StrideS, int DstW, int DstH, int StrideD) {
int Channel = StrideS / SrcW;
if ((SrcW == DstW) && (SrcH == DstH)) {
memcpy(Dest, Src, SrcW * SrcH * Channel * sizeof(unsigned char));
return;
}
short *SinXDivX_Table = (short *)malloc(513 * sizeof(short));
short *Table = (short *)malloc(DstW * 4 * sizeof(short));
for (int I = 0; I < 513; I++)
SinXDivX_Table[I] = int(0.5 + 256 * SinXDivX(I / 256.0f)); // 建立查找表,定点化
int AddX = (SrcW << 16) / DstW, AddY = (SrcH << 16) / DstH;
int ErrorX = -(1 << 15) + (AddX >> 1), ErrorY = -(1 << 15) + (AddY >> 1);

int StartX = ((1 << 16) - ErrorX) / AddX + 1; // 计算出需要特殊处理的边界
int StartY = ((1 << 16) - ErrorY) / AddY + 1; // y0+y*yr>=1; y0=ErrorY => y>=(1-ErrorY)/yr
int EndX = (((SrcW - 3) << 16) - ErrorX) / AddX + 1;
int EndY = (((SrcH - 3) << 16) - ErrorY) / AddY + 1; // y0+y*yr<=(height-3) => y<=(height-3-ErrorY)/yr
if (StartY >= DstH) StartY = DstH;
if (StartX >= DstW) StartX = DstW;
if (EndX < StartX) EndX = StartX;
if (EndY < StartY) EndY = StartY;
for (int X = StartX, SrcX = ErrorX + StartX * AddX; X < EndY; X++, SrcX += AddX) {
int U = (unsigned char)(SrcX >> 8);
Table[X * 4 + 0] = SinXDivX_Table[256 + U]; //建立一个新表便于SSE操作
Table[X * 4 + 1] = SinXDivX_Table[U];
Table[X * 4 + 2] = SinXDivX_Table[256 - U];
Table[X * 4 + 3] = SinXDivX_Table[512 - U];
}
int SrcY = ErrorY;
for (int Y = 0; Y < StartY; Y++, SrcY += AddY) { // 同IM_Resize_Cubic_Table函数
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0, SrcX = ErrorX; X < DstW; X++, SrcX += AddX, LinePD += Channel) {
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
for (int Y = StartY; Y < EndY; Y++, SrcY += AddY) {
int SrcX = ErrorX;
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0; X < StartX; X++, SrcX += AddX, LinePD += Channel) {
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
int V = (unsigned char)(SrcY >> 8);
unsigned char *LineY = Src + ((SrcY >> 16) - 1) * StrideS;
__m128i PartY = _mm_setr_epi32(SinXDivX_Table[256 + V], SinXDivX_Table[V], SinXDivX_Table[256 - V], SinXDivX_Table[512 - V]);
for (int X = StartX; X < EndX; X++, SrcX += AddX, LinePD += Channel) {
__m128i PartX = _mm_loadl_epi64((__m128i *)(Table + X * 4));
//PartX: U0 U1 U2 U3 U0 U1 U2 U3
PartX = _mm_unpacklo_epi64(PartX, PartX);
unsigned char *Pixel0 = LineY + ((SrcX >> 16) - 1) * Channel;
unsigned char *Pixel1 = Pixel0 + StrideS;
unsigned char *Pixel2 = Pixel1 + StrideS;
unsigned char *Pixel3 = Pixel2 + StrideS;
if (Channel == 1) {
__m128i P01 = _mm_cvtepu8_epi16(_mm_unpacklo_epi32(_mm_cvtsi32_si128(*((int *)Pixel0)), _mm_cvtsi32_si128(*((int *)Pixel1)))); // P00 P01 P02 P03 P10 P11 P12 P13
__m128i P23 = _mm_cvtepu8_epi16(_mm_unpacklo_epi32(_mm_cvtsi32_si128(*((int *)Pixel2)), _mm_cvtsi32_si128(*((int *)Pixel3)))); // P20 P21 P22 P23 P30 P31 P32 P33
__m128i Sum01 = _mm_madd_epi16(P01, PartX); // P00 * U0 + P01 * U1 P02 * U2 + P03 * U3 P10 * U0 + P11 * U1 P12 * U2 + P13 * U3
__m128i Sum23 = _mm_madd_epi16(P23, PartX); // P20 * U0 + P21 * U1 P22 * U2 + P23 * U3 P30 * U0 + P31 * U1 P32 * U2 + P33 * U3
__m128i Sum = _mm_hadd_epi32(Sum01, Sum23); // P00 * U0 + P01 * U1 + P02 * U2 + P03 * U3 P10 * U0 + P11 * U1 + P12 * U2 + P13 * U3 P20 * U0 + P21 * U1 + P22 * U2 + P23 * U3 P30 * U0 + P31 * U1 + P32 * U2 + P33 * U3
LinePD[0] = ClampToByte(_mm_hsum_epi32(_mm_mullo_epi32(Sum, PartY)) >> 16);
}
else if (Channel == 4) {
__m128i P0 = _mm_loadu_si128((__m128i *)Pixel0), P1 = _mm_loadu_si128((__m128i *)Pixel1);
__m128i P2 = _mm_loadu_si128((__m128i *)Pixel2), P3 = _mm_loadu_si128((__m128i *)Pixel3);
P0 = _mm_shuffle_epi8(P0, _mm_setr_epi8(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15)); // B0 G0 R0 A0
P1 = _mm_shuffle_epi8(P1, _mm_setr_epi8(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15)); // B1 G1 R1 A1
P2 = _mm_shuffle_epi8(P2, _mm_setr_epi8(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15)); // B2 G2 R2 A2
P3 = _mm_shuffle_epi8(P3, _mm_setr_epi8(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15)); // B3 G3 R3 A3

__m128i BG01 = _mm_unpacklo_epi32(P0, P1); // B0 B1 G0 G1
__m128i RA01 = _mm_unpackhi_epi32(P0, P1); // R0 R1 A0 A1
__m128i BG23 = _mm_unpacklo_epi32(P2, P3); // B2 B3 G2 G3
__m128i RA23 = _mm_unpackhi_epi32(P2, P3); // R2 R3 A2 A3

__m128i B01 = _mm_unpacklo_epi8(BG01, _mm_setzero_si128());
__m128i B23 = _mm_unpacklo_epi8(BG23, _mm_setzero_si128());
__m128i SumB = _mm_hadd_epi32(_mm_madd_epi16(B01, PartX), _mm_madd_epi16(B23, PartX));

__m128i G01 = _mm_unpackhi_epi8(BG01, _mm_setzero_si128());
__m128i G23 = _mm_unpackhi_epi8(BG23, _mm_setzero_si128());
__m128i SumG = _mm_hadd_epi32(_mm_madd_epi16(G01, PartX), _mm_madd_epi16(G23, PartX));

__m128i R01 = _mm_unpacklo_epi8(RA01, _mm_setzero_si128());
__m128i R23 = _mm_unpacklo_epi8(RA23, _mm_setzero_si128());
__m128i SumR = _mm_hadd_epi32(_mm_madd_epi16(R01, PartX), _mm_madd_epi16(R23, PartX));

__m128i A01 = _mm_unpackhi_epi8(RA01, _mm_setzero_si128());
__m128i A23 = _mm_unpackhi_epi8(RA23, _mm_setzero_si128());
__m128i SumA = _mm_hadd_epi32(_mm_madd_epi16(A01, PartX), _mm_madd_epi16(A23, PartX));

__m128i Result = _mm_setr_epi32(_mm_hsum_epi32(_mm_mullo_epi32(SumB, PartY)), _mm_hsum_epi32(_mm_mullo_epi32(SumG, PartY)), _mm_hsum_epi32(_mm_mullo_epi32(SumR, PartY)), _mm_hsum_epi32(_mm_mullo_epi32(SumA, PartY)));
Result = _mm_srai_epi32(Result, 16);
// *((int *)LinePD) = _mm_cvtsi128_si32(_mm_packus_epi16(_mm_packus_epi32(Result, Result), Result));
_mm_stream_si32((int *)LinePD, _mm_cvtsi128_si32(_mm_packus_epi16(_mm_packus_epi32(Result, Result), Result)));

//LinePD[0] = IM_ClampToByte(_mm_hsum_epi32(_mm_mullo_epi32(SumB, PartY)) >> 16); // 确实有部分存在超出unsigned char范围的,因为定点化的缘故
//LinePD[1] = IM_ClampToByte(_mm_hsum_epi32(_mm_mullo_epi32(SumG, PartY)) >> 16);
//LinePD[2] = IM_ClampToByte(_mm_hsum_epi32(_mm_mullo_epi32(SumR, PartY)) >> 16);
//LinePD[3] = IM_ClampToByte(_mm_hsum_epi32(_mm_mullo_epi32(SumA, PartY)) >> 16);
}
}
for (int X = EndX; X < DstW; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
for (int Y = EndY; Y < DstH; Y++, SrcY += AddY)
{
unsigned char *LinePD = Dest + Y * StrideD;
for (int X = 0, SrcX = ErrorX; X < DstW; X++, SrcX += AddX, LinePD += Channel)
{
Bicubic_Border(Src, SrcW, SrcH, StrideS, LinePD, SinXDivX_Table, SrcX, SrcY);
}
}
free(Table);
free(SinXDivX_Table);
}

这个优化过程技巧性比较强,这里需要仔细说明一下。我们先来看Channel为1的情况,观察这句代码:​​Pixel00[I] * U0 + Pixel01[I] * U1 + Pixel02[I] * U2 + Pixel03[I] * U3​​​,我们发现​​Pixel00,Pixel01,Pixel02,Pixel03​​​在内存中是连续的,且这个变量都是在0-256的值域中,显然我们可以使用SSE寄存器读取这几个变量。同时SSE中有一个​​_mm_madd_epi16​​,实现的功能是:

__m128i _mm_madd_epi16 (__m128i a, __m128i b); 

Return Value
  Adds the signed 32-bit integer results pairwise and packs the 4 signed 32-bit integer results.

  r0 := (a0 * b0) + (a1 * b1)
  r1 := (a2 * b2) + (a3 * b3)
  r2 := (a4 * b4) + (a5 * b5)
  r3 := (a6 * b6) + (a7 * b7)

考虑我们函数中,行1到行4每行的代码都只有4次乘法和3次加法,不能直接使用,但是我们可以考虑把两行整合在一起,一次性计算,这样就需要调用2次​​_mm_madd_epi16​​​ ,然后2次的结果在调用​​_mm_hadd_epi32​​这个水平方向的累加函数就能得到新的结果, _mm_hsum_epi32的实现如下:

// 4个有符号的32位的数据相加的和
inline int _mm_hsum_epi32(__m128i V) { //V3 V2 V1 V0
__m128i T = _mm_add_epi32(V, _mm_srli_si128(V, 8)); //V3+V1 V2+V0 V1 V0
T = _mm_add_epi32(T, _mm_srli_si128(T, 4)); //V3+V1+V2+V0 V2+V0+V1 V1+V0 V0
return _mm_cvtsi128_si32(T); //提取低位
}

实现过程中还有很多以前重复介绍过的sse指令,可以在MSDN或我的资源中: https://github.com/BBuf/Image-processing-algorithm-Speed/tree/master/resources 查看。算法的具体过程请看代码, 一些重要地方我都注释好了。完整代码请在github查看。

速度测试

SSE优化系列十一:SSE优化三次卷积插值算法_卷积_02可以看到我们的SSE优化版本代码还是比不过OpenCV3.1.0自带的函数,猜测OpenCV内部对BGR图像应该是直接在3个通道上操作,且优化做得更多更好,所以比我SSE优化的速度快了近3倍。

效果

原图:

SSE优化系列十一:SSE优化三次卷积插值算法_插值_03

结果图,扩大了1.5倍:

SSE优化系列十一:SSE优化三次卷积插值算法_插值_04