概述
最近邻插值是最简单的插值方法。该方法不是根据某些加权标准来计算平均值,也不是根据复杂的规则生成中间值,而是根据目标图像的宽(高)与源图像的宽(高)比值,取源图像相对位置的像素点作为目标像素点的值。新的像素值一定是原图的某个像素值。
假设我们要把一个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实现
- Core Graphics框架
当我们用Core Graphics框架来重采样图片时,可以设置CGContext
的interpolationQuality
(插值质量级别)为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);
- 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数据,这是一个一维数组,但是在实际处理中我们需要以二维的思路来遍历。
- 获取图片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);
- 简单地循环遍历目标/输出图像中的所有像素,通过将宽、高坐标按
rowRatio
和colRatio
缩放,并去掉小数得到缩放索引值,从而来寻址要复制的源像素
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;
}
- 把新的rgba数据,转换回UIImage
CGContextRef bitmapContext = CGBitmapContextCreate(
rgba,
width,
height,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGImageRef cgImage = CGBitmapContextCreateImage(bitmapContext);
UIImage *newUIImage = [UIImage imageWithCGImage:cgImage];
最终得到下面的效果对比图