I've been trying to write an image scaler in C# with a focus on improved performance over the GDI+ scaler.
My current bicubic implementation when running with a single thread is slightly slower than the GDI+ HighQualityBicubic interpolation mode, and multi threaded performance is over 3 times faster. Note that this is running a 64bit .Net 4.6 release build with the optimise flag set. However, according to this post, GDI+ doesn't scale each axis separately, which means that it should be possible to significantly improve the performance of my scaler.
Horizontal Scale:
public static Bitmap BicubicHorizontalUpscale(Bitmap inBmp, int newWidth, int maxDegreeOfParallelism)
{
if (inBmp.PixelFormat != PixelFormat.Format24bppRgb)
{
throw new Exception("PixelFormat incorrect. Must be Format24bppRgb.");
}
if (inBmp.Width == newWidth)
{
return (Bitmap)inBmp.Clone();
}
Bitmap outBmp = new Bitmap(newWidth, inBmp.Height, PixelFormat.Format24bppRgb);
unsafe
{
BitmapData inData = inBmp.LockBits(new Rectangle(0, 0, inBmp.Width, inBmp.Height), ImageLockMode.ReadOnly, inBmp.PixelFormat);
BitmapData outData = outBmp.LockBits(new Rectangle(0, 0, outBmp.Width, outBmp.Height), ImageLockMode.WriteOnly, outBmp.PixelFormat);
int bpp = Bitmap.GetPixelFormatSize(PixelFormat.Format24bppRgb) / 8;
float xRatio = inBmp.Width / (float)outBmp.Width;
int lastInPixelX = inBmp.Width - 1;
int outBmpWidth = outBmp.Width;
byte* outScan0 = (byte*)outData.Scan0;
byte* inScan0 = (byte*)inData.Scan0;
int outDataStride = outData.Stride;
int intDataStride = inData.Stride;
float centerPreCalc = xRatio * 0.5f - 0.5f; //Precalculate part of x position. Originally intX = (outX + 0.5f) * xRatio - 0.5f.
//Removes one subtraction operation from the inner loop.
Parallel.For(0, outBmp.Height, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, y =>
{
byte* outRow = outScan0 + (y * outDataStride);
byte* inRow = inScan0 + (y * intDataStride);
for (int outX = 0; outX < outBmpWidth; outX++)
{
float center = outX * xRatio + centerPreCalc;
int inX2 = (int)center; //Center pixel
int inX1 = inX2 - 1;
int inX3 = inX2 + 1;
int inX4 = inX2 + 2;
float diff2 = center - inX2;
float diff3 = inX3 - center;
float diff4 = inX4 - center;
float diff4Min2 = diff4 - 2;
//Weights are using simplified catmull-rom formula.
float weight2 = (1.5f * diff2 - 2.5f) * (diff2 * diff2) + 1;
float weight3 = (1.5f * diff3 - 2.5f) * (diff3 * diff3) + 1;
float weight4 = -0.5f * (diff4Min2 * diff4Min2) * (diff4 - 1);
float weight1 = 1 - weight2 - weight3 - weight4; //Because the weights will add up to 1.
//Weight one is chosen to calculate this way as it is the most expensive
//due to originally containing an if statement.
int col1 = Math.Max(inX1, 0) * bpp;
int col2 = inX2 * bpp;
int col3 = Math.Min(inX3, lastInPixelX) * bpp;
int col4 = Math.Min(inX4, lastInPixelX) * bpp;
float inB1 = inRow[col1];
float inG1 = inRow[col1 + 1];
float inR1 = inRow[col1 + 2];
float inB2 = inRow[col2];
float inG2 = inRow[col2 + 1];
float inR2 = inRow[col2 + 2];
float inB3 = inRow[col3];
float inG3 = inRow[col3 + 1];
float inR3 = inRow[col3 + 2];
float inB4 = inRow[col4];
float inG4 = inRow[col4 + 1];
float inR4 = inRow[col4 + 2];
float outB = inB1 * weight1 + inB2 * weight2 + inB3 * weight3 + inB4 * weight4;
float outG = inG1 * weight1 + inG2 * weight2 + inG3 * weight3 + inG4 * weight4;
float outR = inR1 * weight1 + inR2 * weight2 + inR3 * weight3 + inR4 * weight4;
*outRow++ = clampFloat(outB);
*outRow++ = clampFloat(outG);
*outRow++ = clampFloat(outR);
}
});
outBmp.UnlockBits(outData);
inBmp.UnlockBits(inData);
}
return outBmp;
}
Vertical Scale:
public static Bitmap BicubicVerticalUpscale(Bitmap inBmp, int newHeight, int maxDegreeOfParallelism)
{
if (inBmp.PixelFormat != PixelFormat.Format24bppRgb)
{
throw new Exception("PixelFormat incorrect. Must be Format24bppRgb.");
}
if (inBmp.Height == newHeight)
{
return (Bitmap)inBmp.Clone();
}
Bitmap outBmp = new Bitmap(inBmp.Width, newHeight, PixelFormat.Format24bppRgb);
unsafe
{
BitmapData inData = inBmp.LockBits(new Rectangle(0, 0, inBmp.Width, inBmp.Height), ImageLockMode.ReadOnly, inBmp.PixelFormat);
BitmapData outData = outBmp.LockBits(new Rectangle(0, 0, outBmp.Width, outBmp.Height), ImageLockMode.ReadWrite, outBmp.PixelFormat);
int bpp = Bitmap.GetPixelFormatSize(PixelFormat.Format24bppRgb) / 8;
float yRatio = inBmp.Height / (float)outBmp.Height;
int lastInPixelY = inBmp.Height - 1;
int outBmpWidth = outBmp.Width;
byte* outBmpDataScan0 = (byte*)outData.Scan0;
byte* inBmpDataScan0 = (byte*)inData.Scan0;
int outDataStride = outData.Stride;
int intDataStride = inData.Stride;
Parallel.For(0, outBmp.Height, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, outY =>
{
byte* outRow = outBmpDataScan0 + (outY * outDataStride);
float center = (outY + 0.5f) * yRatio - 0.5f;
int inY2 = (int)center; //Center pixel
int inY1 = inY2 - 1;
int inY3 = inY2 + 1;
int inY4 = inY2 + 2;
byte* inRow1 = inBmpDataScan0 + (Math.Max(inY1, 0) * intDataStride);
byte* inRow2 = inBmpDataScan0 + (inY2 * intDataStride);
byte* inRow3 = inBmpDataScan0 + (Math.Min(inY3, lastInPixelY) * intDataStride);
byte* inRow4 = inBmpDataScan0 + (Math.Min(inY4, lastInPixelY) * intDataStride);
float diff2 = center - inY2;
float diff3 = inY3 - center;
float diff4 = inY4 - center;
float diff4Min2 = diff4 - 2;
//Weights are using simplified catmull-rom formulas.
float weight2 = (1.5f * diff2 - 2.5f) * (diff2 * diff2) + 1;
float weight3 = (1.5f * diff3 - 2.5f) * (diff3 * diff3) + 1;
float weight4 = -0.5f * (diff4Min2 * diff4Min2) * (diff4 - 1);
float weight1 = 1 - weight2 - weight3 - weight4; //Because the weights will add up to 1.
//Weight one is chosen to calculate this way as it is the most expensive
//due to originally containing an if statement.
for (int x = 0; x < outBmpWidth; x++)
{
int colB = x * bpp;
int colG = colB + 1;
int colR = colB + 2;
float inB1 = inRow1[colB];
float inG1 = inRow1[colG];
float inR1 = inRow1[colR];
float inB2 = inRow2[colB];
float inG2 = inRow2[colG];
float inR2 = inRow2[colR];
float inB3 = inRow3[colB];
float inG3 = inRow3[colG];
float inR3 = inRow3[colR];
float inB4 = inRow4[colB];
float inG4 = inRow4[colG];
float inR4 = inRow4[colR];
float outB = inB1 * weight1 + inB2 * weight2 + inB3 * weight3 + inB4 * weight4;
float outG = inG1 * weight1 + inG2 * weight2 + inG3 * weight3 + inG4 * weight4;
float outR = inR1 * weight1 + inR2 * weight2 + inR3 * weight3 + inR4 * weight4;
*outRow++ = clampFloat(outB);
*outRow++ = clampFloat(outG);
*outRow++ = clampFloat(outR);
}
});
outBmp.UnlockBits(outData);
inBmp.UnlockBits(inData);
}
return outBmp;
}
Calling and clamping methods:
public static Bitmap Bicubic(Bitmap inBmp, Size newSize, int maxDegreeOfParallelism)
{
Bitmap horizontalScaleBmp = BicubicHorizontalUpscale(inBmp, newSize.Width, maxDegreeOfParallelism);
Bitmap verticalScaleBmp = BicubicVerticalUpscale(horizontalScaleBmp, newSize.Height, maxDegreeOfParallelism);
horizontalScaleBmp.Dispose();
return verticalScaleBmp;
}
private static byte clampFloat(float val)
{
if (val < 0)
{
return 0;
}
if (val > 255)
{
return 255;
}
return (byte)val;
}
The only two obvious things that I can think of are integer math and using a simd library such as System.Numerics.Vector
. Although, on my pc integer math isn't faster than floating point math, and I couldn't manage to use the vector class without reducing performance.
I think that a Lookup Table would be slower than my current weight function calculations.