图像缩放操作中,输出图像像素点坐标有可能对应于输入图像上几个像素点之间的位置,需要通过灰度插值处理来计算出该输出点的灰度值。不同的插值算法有不同的精度,直接影响着图像的失真程度。最常用的插值算法有三种:最近邻插值、双线性插值、双三次插值,其中双三次插值的效果是最佳的。
由于图像像素的灰度值是离散的, 因此一般的处理方法是对原来在整数点坐标上的像素值进行插值生成连续的曲面, 然后在插值曲面上重新采样以获得缩放图像像素的灰度值。缩放处理从输出图像出发,采用逆向映射方法,即在输出图像中找到与之对应的输入图像中的某个或某几个像素,采用这种方法能够保证输出图像中的每个像素都有一个确定值,否则,如果从输入图像出发来推算输出图像,输出图像的像素点可能出现无灰度值的情况。因为,对图像进行缩放处理时输出图像像素和输入图像之间可能不再存在着一一对应关系。
双三次插值不仅考虑到周围四个直接相邻像素点灰度值的影响,还考虑到它们灰度值变化率的影响。在这种方法中,函数 f 在点(x, y) 的值可以通过矩形网格中最近的十六个采样点的加权平均得到,在这里需要使用两个多项式插值三次函数,每个方向使用一个。通过双三次插值可以得到一个连续的插值函数,它的一阶偏导数连续,并且交叉导数处处连续。
双三次插值核如下:
这里参考AForge.NET实现了双三次插值算法,并将实验结果与GDI+及EmguCV自带的双三次插值算法效率进行比较(缩放同一幅图像到相同尺寸,计算平均耗时,同时参考缩放效果)。
(1)双三次插值算法实现代码:
- /// <summary>
- /// 双三次插值缩放灰度图像。
- /// </summary>
- /// <param name="image">源图像。</param>
- /// <param name="newWidth">新宽度。</param>
- /// <param name="newHeight">新高度。</param>
- /// <returns>缩放后的图像。</returns>
- public static Bitmap ResizeGrayscaleImage(Bitmap image, int newWidth, int newHeight)
- {
- // 检查源图像格式
- CheckSourceFormat(image);
- // 锁定源图像内存
- BitmapData srcData = image.LockBits(
- new Rectangle(0, 0, image.Width, image.Height),
- ImageLockMode.ReadOnly, image.PixelFormat);
- // 新建目标图像
- Bitmap dstImage = CreateGrayscaleImage(newWidth, newHeight);
- // 锁定目标图像内存
- BitmapData dstData = dstImage.LockBits(
- new Rectangle(0, 0, newWidth, newHeight),
- ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
- try
- {
- Resize(srcData, ref dstData);
- }
- finally
- {
- // 解锁图像内存
- dstImage.UnlockBits(dstData);
- image.UnlockBits(srcData);
- }
- return dstImage;
- }
- /// <summary>
- /// 双三次插值处理缩放。
- /// </summary>
- /// <param name="srcData">源图像数据。</param>
- /// <param name="dstData">目标图像数据。</param>
- private static void ResizeProcess(BitmapData srcData, ref BitmapData dstData)
- {
- // 获取源图像数据
- int srcWidth = srcData.Width;
- int srcHeight = srcData.Height;
- int srcStride = srcData.Stride;
- IntPtr srcPtr = srcData.Scan0;
- // 获取目标图像数据
- int dstWidth = dstData.Width;
- int dstHeight = dstData.Height;
- int dstStride = dstData.Stride;
- int dstOffset = dstStride - dstWidth;
- IntPtr dstPtr = dstData.Scan0;
- // 计算比例系数
- double xFactor = (double)srcWidth / dstWidth;
- double yFactor = (double)srcHeight / dstHeight;
- // 将源图像数据复制到托管内存中
- int srcBytes = srcStride * srcHeight;
- byte[] srcGrayData = new byte[srcBytes];
- Marshal.Copy(srcPtr, srcGrayData, 0, srcBytes);
- // 保存目标图像数据
- int dstBytes = dstStride * dstHeight;
- byte[] dstGrayData = new byte[dstBytes];
- int dst = 0; // 下标
- // 源图像坐标点及系数
- double ox, oy, dx, dy, k1, k2;
- int ox1, oy1, ox2, oy2;
- // 目标图像像素值
- double grayValue;
- // 边界
- int ymax = srcHeight - 1;
- int xmax = srcWidth - 1;
- #region 插值
- for (int y = 0; y < dstHeight; y++)
- {
- // Y坐标
- oy = (double)y * yFactor - 0.5;
- oy1 = (int)oy;
- dy = oy - (double)oy1;
- for (int x = 0; x < dstWidth; x++, dst++)
- {
- // X坐标
- ox = (double)x * xFactor - 0.5f;
- ox1 = (int)ox;
- dx = ox - (double)ox1;
- // 像素值归零
- grayValue = 0;
- for (int n = -1; n < 3; n++)
- {
- // Y系数
- k1 = BiCubicInterpolator(dy - (double)n);
- oy2 = oy1 + n;
- if (oy2 < 0)
- oy2 = 0;
- if (oy2 > ymax)
- oy2 = ymax;
- for (int m = -1; m < 3; m++)
- {
- // X系数
- k2 = k1 * BiCubicInterpolator((double)m - dx);
- ox2 = ox1 + m;
- if (ox2 < 0)
- ox2 = 0;
- if (ox2 > xmax)
- ox2 = xmax;
- grayValue += k2 * srcGrayData[oy2 * srcStride + ox2];
- }
- }
- dstGrayData[dst] = (byte)Math.Max(0, Math.Min(255, grayValue));
- }
- dst += dstOffset;
- }
- Marshal.Copy(dstGrayData, 0, dstPtr, dstBytes);
- #endregion
- }
- /// <summary>
- /// 双三次插值器。
- /// coefficient is set to -0.5.
- /// </summary>
- /// <param name="x">X Value.</param>
- /// <returns>Bicubic cooefficient.</returns>
- private static double BiCubicInterpolator(double x)
- {
- if (x < 0)
- {
- x = -x;
- }
- double biCoef = 0;
- if (x <= 1)
- {
- biCoef = (1.5 * x - 2.5) * x * x + 1;
- }
- else if (x < 2)
- {
- biCoef = ((-0.5 * x + 2.5) * x - 4) * x + 2;
- }
- return biCoef;
- }
- /// <summary>
- /// 检查格式。
- /// </summary>
- /// <param name="original">图像。</param>
- private static void CheckSourceFormat(Bitmap original)
- {
- if ((original.PixelFormat != PixelFormat.Format8bppIndexed) ||
- (IsGrayscale(original) == false))
- {
- throw new Exception("Source pixel format is not supported.");
- }
- }
- /// <summary>
- /// 判断位图是不是8位灰度。
- /// </summary>
- /// <param name="original">位图。</param>
- /// <returns>判断结果。</returns>
- public static bool IsGrayscale(Bitmap original)
- {
- bool ret = false;
- // 检查像素格式
- if (original.PixelFormat == PixelFormat.Format8bppIndexed)
- {
- ret = true;
- // 检查调色板
- ColorPalette palette = original.Palette;
- Color color;
- for (int i = 0; i < 256; i++)
- {
- color = palette.Entries[i];
- if ((color.R != i) || (color.G != i) || (color.B != i))
- {
- ret = false;
- break;
- }
- }
- }
- return ret;
- }
- /// <summary>
- /// 新建8位灰度位图。
- /// </summary>
- /// <param name="width">长。</param>
- /// <param name="height">宽。</param>
- /// <returns>新建8位灰度位图。</returns>
- public static Bitmap CreateGrayscaleImage(int width, int height)
- {
- // 新建图像
- Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
- // 设置灰度图像的调色板
- SetGrayscalePalette(bitmap);
- return bitmap;
- }
- /// <summary>
- /// 设置灰度位图调色板。
- /// </summary>
- /// <param name="original">灰度位图。</param>
- public static void SetGrayscalePalette(Bitmap original)
- {
- // 检查像素格式
- if (original.PixelFormat != PixelFormat.Format8bppIndexed)
- throw new Exception("Source image is not 8 bpp image.");
- // 获取调色板
- ColorPalette palette = original.Palette;
- // init palette
- for (int i = 0; i < 256; i++)
- {
- palette.Entries[i] = Color.FromArgb(i, i, i);
- }
- // 设置调色板
- original.Palette = palette;
- }
算法效率:
图1.源图像
图2.ResizeGrayscaleImage方法耗时
(2)用GDI+的双三次插值算法实现代码:
- /// <summary>
- /// 使用GDI+缩放图像。
- /// </summary>
- /// <param name="original">要缩放的图像。</param>
- /// <param name="newWidth">新宽度。</param>
- /// <param name="newHeight">新高度。</param>
- /// <returns>缩放后的图像。</returns>
- public static Bitmap ResizeUsingGDIPlus(Bitmap original, int newWidth, int newHeight)
- {
- try
- {
- Bitmap bitmap = new Bitmap(newWidth, newHeight);
- Graphics graphics = Graphics.FromImage(bitmap);
- // 插值算法的质量
- graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
- graphics.SmoothingMode = SmoothingMode.HighQuality;
- graphics.DrawImage(original, new Rectangle(0, 0, newWidth, newHeight),
- new Rectangle(0, 0, original.Width, original.Height), GraphicsUnit.Pixel);
- graphics.Dispose();
- return bitmap;
- }
- catch
- {
- return null;
- }
- }
算法效率:
图3.GDI+方法耗时
(3)用EmguCV的双三次插值算法实现代码:
- /// <summary>
- /// 使用EmguCV缩放图像。
- /// </summary>
- /// <param name="original">要缩放的图像。</param>
- /// <param name="newWidth">新宽度。</param>
- /// <param name="newHeight">新高度。</param>
- /// <returns>缩放后的图像。</returns>
- public static Bitmap ResizeUsingEmguCV(Bitmap original, int newWidth, int newHeight)
- {
- try
- {
- Emgu.CV.Image<Emgu.CV.Structure.Gray, byte> image =
- new Emgu.CV.Image<Emgu.CV.Structure.Gray, byte>(original);
- Emgu.CV.Image<Emgu.CV.Structure.Gray, byte> newImage = image.Resize(
- newWidth, newHeight, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
- return newImage.Bitmap;
- }
- catch
- {
- return null;
- }
- }
算法效率:
(以上代码为封装在静态类中,直接使用EmguCV要快的多,eg.这个实验耗时为封装后的三分之一)