影像重取樣演算法之最鄰近插值演算法

大雄愛折騰發表於2020-01-30

概述

最近鄰插值是最簡單的插值方法。該方法不是根據某些加權標準來計算平均值,也不是根據複雜的規則生成中間值,而是根據目標影像的寬(高)與源影像的寬(高)比值,取源影像相對位置的畫素點作為目標畫素點的值。新的畫素值一定是原圖的某個畫素值。

假設我們要把一個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

參考連結

相關文章