图像重采样算法之最邻近插值算法

4,286 阅读4分钟

概述

最近邻插值是最简单的插值方法。该方法不是根据某些加权标准来计算平均值,也不是根据复杂的规则生成中间值,而是根据目标图像的宽(高)与源图像的宽(高)比值,取源图像相对位置的像素点作为目标像素点的值。新的像素值一定是原图的某个像素值。

假设我们要把一个2X2的小图片拉伸到4X4的尺寸,如下所示

假设P1的坐标为(Dx, Dy),原图的宽、高分别为Sw、Sh,拉伸后的图像宽、高分别为Dw、Dh,我们需要求P1在原图的坐标(Sx, Sy)。则

由 
    Dx / Dw = Sx / Sw
    Dy / Dh = Sy / Sh
得 
    Sx = Dx * (Sh / Dh)
    Sy = Dy * (Sw / Dw)
代入
    Dx = 0
    Dy = 0
    Sh = 2
    Sw = 2
    Dh = 4
    Dw = 4
得P1对应原图的坐标
    Sx = 0 * (2 / 4) = 0
    Sy = 0 * (2 / 4) = 0
同理P2的坐标为(0, 1),代入
得P2对应原图的坐标
    Sx = 0 * (2 / 4) = 0
    Sy = 1 * (2 / 4) = 0.5 // 去掉小数部分 为 0

因此P1点对应原图的坐标为(0, 0),也就是P1的值为10,P2对应原图的坐标为(0, 0),因此P2的值也为10,以此类推

iOS平台原生API实现

  1. Core Graphics框架

当我们用Core Graphics框架来重采样图片时,可以设置CGContextinterpolationQuality(插值质量级别)为None。这样就会使用最近邻插值算法

class func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
    else {
        return nil
    }

    let context = CGContext(data: nil,
                            width: Int(size.width),
                            height: Int(size.height),
                            bitsPerComponent: image.bitsPerComponent,
                            bytesPerRow: image.bytesPerRow,
                            space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
                            bitmapInfo: image.bitmapInfo.rawValue)
    // 设置插值质量为 none,也就是使用最近邻插值算法
    context?.interpolationQuality = .none
    context?.draw(image, in: CGRect(origin: .zero, size: size))

    guard let scaledImage = context?.makeImage() else { return nil }

    return UIImage(cgImage: scaledImage)
}

Objective-C 里面的用法则是

CGContextSetInterpolationQuality(context, kCGInterpolationNone);
  1. CALayer提供的API

在CALayer中,提供了minificationFilter(缩小图片时使用的过滤器类型)和magnificationFilter(放大图片时使用的过滤器类型)两个属性,指定渲染CALayer中的content属性的内容时使用的过滤器类型。当需要缩小图像数据时使用minificationFilter指定的过滤器,当需要拉大图像数据时使用magnificationFilter指定的过滤器。默认两个属性的值都为 kCAFilterLinear。其中kCAFilterNearest就是使用最近邻插值算法的过滤器。

  • kCAFilterLinear
  • kCAFilterNearest
  • kCAFilterTrilinear

我们可以如下设置UIImageView中的layer的对应属性,当所设置的图像数据过大或者过小导致需要拉伸时,就会使用你指定的过滤器类型来重采样图片。但需要注意的是,图像大小和显示的大小不一致,是会有性能隐忧的,如果在一个列表中需要显示很多图像,而这些图像大小又和实际显示大小不一致时,在滑动时可能会引起卡顿。一般都是在后台手动重采样图像后才设置到UIImageView的,而下面这样的设置重采样图像都是在主线程做的。

imageView.layer.magnificationFilter = kCAFilterNearest;

算法实现

我在iOS平台上,使用Objective-C语言实现这个算法。首先需要获取到图片的RGBA数据,也叫RAW数据,这是一个一维数组,但是在实际处理中我们需要以二维的思路来遍历。

  1. 获取图片RGBA数据
UInt32* pixelData = (UInt32 *)calloc(width * height, sizeof(UInt32));
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pixelData,
                                                width,
                                                height,
                                                bitsPerComponent,
                                                bytesPerRow,
                                                colorSpace,
                                                kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgOriginalImage);
  1. 简单地循环遍历目标/输出图像中的所有像素,通过将宽、高坐标按rowRatiocolRatio缩放,并去掉小数得到缩放索引值,从而来寻址要复制的源像素
static UInt32* scaleImageWithNearesNeighborInterpolation(UInt32* pixelData, int sourceWidth, int sourceHeight, int desWidth, int desHeight) {
    
    // 宽和高的缩放常数
    float rowRatio = ((float)sourceHeight) / ((float)desHeight);
    float colRatio = ((float)sourceWidth) / ((float)desWidth);
    UInt32* rgba = (UInt32 *)calloc(desWidth * desHeight, sizeof(UInt32));
    int offset=0;
    for(int i = 0; i < desHeight; ++i) {
        // 如果用round则四舍五入,round(0.5) = 1
        // 这里用floor,floor(0.5) = 0
        int srcRow = floor(((float)i)*rowRatio);
        if(srcRow >= sourceHeight) {
            srcRow = sourceHeight - 1;
        }
        
        for (int j = 0; j < desWidth; j++) {
            
            int srcCol = floor(((float)j)*colRatio);
            if(srcCol >= sourceWidth) {
                srcCol = sourceWidth - 1;
            }
            
            rgba[offset]   = pixelData[(srcRow * sourceWidth + srcCol)];
            offset++;
        }
    }
    
    return rgba;
}
  1. 把新的rgba数据,转换回UIImage
CGContextRef bitmapContext = CGBitmapContextCreate(
                                                    rgba,
                                                    width,
                                                    height,
                                                    bitsPerComponent,
                                                    bytesPerRow,
                                                    colorSpace,
                                                    kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

CGImageRef cgImage = CGBitmapContextCreateImage(bitmapContext);
UIImage *newUIImage = [UIImage imageWithCGImage:cgImage];

最终得到下面的效果对比图

Source Code

GitHub

参考链接