1. 程式人生 > >iOS濾鏡系列-濾鏡開發概覽

iOS濾鏡系列-濾鏡開發概覽

概述

濾鏡最早的出現應該是應用在相機鏡頭前實現自然光過濾和調色的鏡片,然而在軟體開發中更多的指的是軟體濾鏡,是對鏡頭濾鏡的模擬實現。當然這種方式更加方便快捷,缺點自然就是無法還原拍攝時的真實場景,例如無法實現偏光鏡和紫外線濾色鏡的效果。今天簡單介紹一下iOS濾鏡開發中的正確姿勢,讓剛剛接觸濾鏡開發的朋友少走彎路。

在iOS開發中常見的濾鏡開發方式大概包括:CIFilter、GPUImage、OpenCV等。

CoreImage

CIFilter

CIFilter存在於CoreImage框架中,它基於OpenGL著色器來處理影象(最新的已經基於Metal實現),優點當然是快,因為它可以充分利用GPU加速來處理影象渲染,同時它自身支援濾鏡鏈,多個濾鏡同時使用時迅速高效。

CIFilter目前已經支援21個分類(如下程式碼)196種濾鏡:

public let kCICategoryDistortionEffect: String
public let kCICategoryGeometryAdjustment: String
public let kCICategoryCompositeOperation: String
public let kCICategoryHalftoneEffect: String
public let kCICategoryColorAdjustment: String
public let kCICategoryColorEffect: String
public let kCICategoryTransition: String
public let kCICategoryTileEffect: String
public let kCICategoryGenerator: String
@available(iOS 5.0, *)
public let kCICategoryReduction: String
public let kCICategoryGradient: String
public let kCICategoryStylize: String
public let kCICategorySharpen: String
public let kCICategoryBlur: String
public let kCICategoryVideo: String
public let kCICategoryStillImage: String
public let kCICategoryInterlaced: String
public let kCICategoryNonSquarePixels: String
public let kCICategoryHighDynamicRange: String
public let kCICategoryBuiltIn: String
@available(iOS 9.0, *)
public let kCICategoryFilterGenerator: String

使用open class func filterNames(inCategory category: String?) -> [String]可以檢視每個分類的濾鏡名稱。而每個濾鏡的屬性設定通過CIFilter的attributes就可以檢視。而應用一個CIFilter濾鏡也僅僅需要:建立濾鏡->設定屬性(KVC)->讀取輸入圖片(下面演示了高斯模糊濾鏡的簡單實現):

guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        let filter = CIFilter(name: "CIGaussianBlur")
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
        filter?.setValue(5.0, forKey: "inputRadius")
        
        if let outputImage = filter?.value(forKeyPath: kCIOutputImageKey) as? CIImage {
            let context = CIContext()
            if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                let image = UIImage(cgImage: cgImage)
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
        }

原圖

應用高斯模糊

濾鏡鏈
所謂濾鏡鏈就是將一個濾鏡A的輸出作為另一個濾鏡B的輸入形成有向圖,使用這種方式Core Image並非一步步執行結果應用到B濾鏡,而是將多個濾鏡的著色器合併操作,從而提高效能。
例如在上面的高斯模糊濾鏡基礎上應用畫素化濾鏡:

guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        let blurFilter = CIFilter(name: "CIGaussianBlur")
        blurFilter?.setValue(ciImage, forKey: kCIInputImageKey)
        blurFilter?.setValue(5.0, forKey: "inputRadius")
        
        let pixelFilter = CIFilter(name: "CIPixellate", parameters: [kCIInputImageKey:blurFilter!.outputImage!])
        pixelFilter?.setDefaults()
        
        if let outputImage = pixelFilter?.outputImage {
            let context = CIContext()
            if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                let image = UIImage(cgImage: cgImage)
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
        }

另外新的API(iOS 11)如果使用濾鏡建議使用更加直觀的表達以簡化書寫:let outputImage = ciImage.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey:5.0]).applyingFilter("CIPixellate")
此外說到CoreImage的高斯模糊時直接使用是有一個問題的,那就是radius越大越會產生一個明顯的空白邊緣,當然這個問題是因為濾鏡的卷積操作通常從中心點開始應用造成的,這樣就會致使邊緣上的畫素值不能得到有效應用,類似於OpenCV會自己處理這個問題,但是Core Image並沒有處理這個邊緣問題,通常的處理方法就是放大圖片,然後剪下到原來的圖片大小即可(其實就是在濾鏡前後分別呼叫clampedToExtend()獲取一個邊緣擴充套件的影象,應用濾鏡之後呼叫croped()獲取一個裁剪邊緣的影象即可)。

guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        let outputImage = ciImage.clampedToExtent().applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey:5.0]).cropped(to: ciImage.extent)
        let context = CIContext()
        if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) {
            let image = UIImage(cgImage: cgImage)
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        }

自定義運算元

儘管Core Image提供了不少濾鏡可以使用,不過實際開發中還並不能夠滿足需求,比如說描繪邊緣這個操作在Core Image中應該就沒有提供直接的濾鏡。而有不少濾鏡是通過卷積操作完成的,只要提供一個運算元就可以形成一個新的濾鏡效果,事實上Core Image框架也提供了這個濾鏡:CIConvolution3X3和CIConvolution5X5。這兩個濾鏡支援開發者自定義運算元實現一個濾鏡操作,下面是使用CIConvolution3X3實現的sobel運算元提取邊緣的濾鏡:

guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        let sobel:[CGFloat] = [-1,0,1,-2,0,2,-1,0,1]
        let weight = CIVector(values: sobel, count: 9)
        let outputImage = ciImage.applyingFilter("CIConvolution3X3", parameters: [kCIInputWeightsKey:weight,kCIInputBiasKey:0.5])
        
        let context = CIContext()
        if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) {
            let image = UIImage(cgImage: cgImage)
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        }

前面的圖應用Sobel運算元後的效果:

可以看出來邊緣已經被提取出來,其實無論是CIConvolution3X3還是CIConvolution5X5都只是進行一個卷積操作,本質就是對應的畫素分別乘以對應運算元上的值最後相加等於產生一個新的值作為當前畫素的值(這個值通常是待處理影象區塊中心)如下圖:

除了上面的Sobel運算元,常見的運算元還有銳化運算元{0,-1,0,-1,5,-1,0,-1,0}、浮雕運算元{1,0,0,0,0,0,0,0,-1}、拉普拉斯運算元(邊緣檢測){0,1,0,1,-4,1,0,1,0}等等。

自定義濾鏡

如果僅僅是自定義運算元恐怕還不能體現出CIFilter的強大之處,畢竟不少濾鏡通過特定運算元還是無法滿足的,CIFilter支援自定義片段著色器實現自己的濾鏡效果。
自定義的 Filter 和系統內建的各種 CIFilter,使用起來方式是一樣的。我們唯一要做的,就是實現一個符合規範的 CIFilter 的子類。過程大家就是:編寫 kernel->載入 kernel->設定引數。假設現在編寫一個圖片翻轉的效果大概過程如下:

1.編寫kernel指令碼,儲存為Flip.kernel

kernel vec2 mirrorX ( float imageWidth ) 
{
    vec2 currentVec = destCoord();
    return vec2 ( imageWidth - currentVec.x , currentVec.y ); 
}

2.載入kernel

class FlipFilterGenerator:NSObject, CIFilterConstructor {
    func filter(withName name: String) -> CIFilter? {
        if name == "\(FlipFilter.self)" {
            return FlipFilter()
        }
        return nil
    }
}
private let flipKernel:CIWarpKernel? = CIWarpKernel(source:try! String(contentsOf:Bundle.main.url(forResource: "Flip", withExtension: "cikernel")!))
class FlipFilter: CIFilter {
    
    
    
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    static func register() {
        CIFilter.registerName("\(FlipFilter.self)", constructor: FlipFilterGenerator(), classAttributes: [kCIAttributeFilterName:"\(FlipFilter.self)"])
    }
    
    override func setDefaults() {
        
    }
    
    @objc var inputImage: CIImage?
    
    override var outputImage: CIImage? {
        guard let width = self.inputImage?.extent.size.width else { return nil }
        let result = flipKernel?.apply(extent: inputImage!.extent, roiCallback: { (index, rect) -> CGRect in
            return rect
        }, image: self.inputImage!, arguments: [width])
        return result
    }
    
    override var name: String {
        get {
            return "\(FlipFilter.self)"
        }
        set {
            
        }
    }
    
}

使用CIFilter的source建構函式傳入著色器程式碼,然後通過apply()方法傳入引數即可執行著色。當然使用之前記得進行註冊,這樣在使用的時候就可以像使用內建濾鏡一樣使用了。

但是這裡必須著重看一下apply()方法的幾個引數
extent:要處理的輸入圖片的區域(稱之為DOD ( domain of definition ) ),一般處理的都是原圖,並不會改變影象尺寸所以上面傳的是inputImage.extent
roiCallback:感興趣的處理區域(ROI ( region of interest ),可以理解為當前處理區域對應的原圖區域)處理完後的回撥,回撥引數index代表圖片索引順序,回撥引數rect代表輸出圖片的區域DOD,但是需要注意在Core Image處理中這個回撥會多次呼叫。這個值通常只要不發生旋轉就是當前圖片的座標(如果旋轉90°,則返回為CGRect(x: rect.origin.y, y: rect.origin.x, width: rect.size.height, height: rect.size.width))
arguments:著色器函式中需要的引數,按順序傳入。

自定義濾鏡呼叫:

FlipFilter.register()
        guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        let outputImage = ciImage.applyingFilter("FlipFilter")
        
        let context = CIContext()
        if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) {
            let image = UIImage(cgImage: cgImage)
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        }

下面是上圖使用翻轉濾鏡後的效果:

其實準確的來說實現一個自定義濾鏡就是實現一個自定義的CIKernel類,當然這個類本身包括兩個子類CIColorKernel和CIWarpKernel,前者用於影象顏色轉化濾鏡,而後者用於形變濾鏡,如前面的翻轉很明顯不是一個顏色值的修改就能解決的,必須依賴於形變操作所以繼承自CIWarpKernel要簡單些。當然如果你的濾鏡綜合了二者的特點那麼直接選擇使用CIKernel是正確的。至於著色器程式碼編寫使用的是Core Image Kernel Language (CIKL),它是OpenGL Shading Language (GLSL) 的子集。CIKL 集成了 GLSL 絕大部分的引數型別和內建函式,另外它還添加了一些適應 Core Image 的引數類似和函式。另外編寫CIKL需要注意座標系,它的座標系從左下角開始而不是UIKit的左上角。

由於篇幅原因關於編寫CIKL的具體細節這裡不再贅述,感興趣可以參考Writing Kernels和Core Image Kernel Language Reference,而編寫CIKL的工具自然推薦官方的Quartz Composer。

從前面的演示也可以看到圖片在UIImage、CGImage和CIImage之間不停的轉化,那麼三者之間有什麼區別呢?
UIImage存在於UIKit中,CGImage存在於Core Graphics中,CIImage存在於Core Image中。前者負責展示和管理圖片資料,例如可以使用UIImageView展示、或者繪製到UIView、layer上等,主要在CPU上操作;CGImage表示影象的畫素矩陣,每個點都對應了圖片的畫素資訊,主要執行在GPU上;而CIImage包含了建立圖片的必要資料,自身並不會渲染成圖片,代表了影象的資料或者操作影象的流程(如濾鏡),主要執行在GPU上。換句話說對於CIImage的操作並不會進行大量的圖片運算,只有要輸出圖片時才需要轉化成圖片資料(推薦這一步儘量放到非同步執行緒中操作)。
注意:獲取一個圖片的CIImage型別時請使用CIImage()構造方法建立,請勿直接訪問uiImage.ciImage,因為如果一個UIImage不是從CIImage建立是無法獲取ciImage的(uiImage.cgImage類似,上面之所以可以直接使用UIImage.cgImage屬性是因為它並非從ciImage建立)。反之,如果從ciImage建立UIImage就不推薦使用UIImage的構造方法了,因為這種方式會丟失資訊,例如使用UIViewImage顯示時會丟失contentMode設定,如果使用上面的程式碼儲存會出現儲存失敗的情況,推薦的方式則是使用UIContext先生成CGImage,然後從CGImage建立UIImage(總結起來就是UIImage到CGImage明確的情況下可以直接訪問cgImage屬性,但是cgImage為空則訪問ciImage屬性再從ciImage建立cgImage,從CGImage轉化為UIImage使用建構函式;UIImage到CIImage推薦使用建構函式,也可以使用CGImage從中間過渡,而從CIImage轉化為UIImage只能通過CGImage過渡再用建構函式建立)。

Metal Shader

如果你編寫過CIKL你會發現這種開發方式很古老,Quartz Composer儘管作為目前開發CIKL最合適的工具但在Xcode7之後幾乎沒有更新過,儘管有語法高亮但是沒有錯誤除錯,更不用說執行時出錯的問題(儘管可以使用+(id)kernelsWithString:(id)arg1 messageLog:(id)arg2這個私有方法列印kernel中的錯誤,但是除錯依然很麻煩),自身以字串傳入CIKernel類的方式讓它天然失去了語法檢查。更重要的是這種方式最終要將CIKL片段變成CIKernel必須經過CIKL->GLSL->CIKernel->IL->GPU識別碼->Render到GPU,如果遇到濾鏡鏈還必須在中間連結Kernel,而這些操作全部在執行時進行。所以首次使用會比較慢(後面使用會快取),而2017年Metal支援CIKernel則將Kernel的編譯提前到了App編譯階段,從而支援了語法檢查,大大提高了開發效率和執行效率。

例如前面的濾鏡鏈中使用了一個馬賽克風格的濾鏡,這裡不妨先看一下使用CIKL編寫這個濾鏡(注意這是一個CIWrapKernel,返回值是變化後的座標位置):

kernel vec2 pixellateKernel(float radius)
{
    vec2 positionOfDestPixel, centerPoint;
    positionOfDestPixel = destCoord();
    centerPoint.x = positionOfDestPixel.x - mod(positionOfDestPixel.x, radius * 2.0) + radius;
    centerPoint.y = positionOfDestPixel.y - mod(positionOfDestPixel.y, radius * 2.0) + radius;

    return centerPoint;
}

這個CIKL用Metal Shader書寫如下:

extern "C" {
    namespace coreimage {
        
        float2 pixellateMetal(float radius,destination dest) {
            float2 positionOfDestPixel, centerPoint;
            positionOfDestPixel = dest.coord();
            centerPoint.x = positionOfDestPixel.x - fmod(positionOfDestPixel.x, radius * 2.0) + radius;
            centerPoint.y = positionOfDestPixel.y - fmod(positionOfDestPixel.y, radius * 2.0) + radius;
            
            return centerPoint;
        }
        
    }
}

當然對應的自定義CIFilter需要做少許調整:

class PixellateFilterGenerator:NSObject, CIFilterConstructor {
    func filter(withName name: String) -> CIFilter? {
        if name == "\(PixellateFilter.self)" {
            return PixellateFilter()
        }
        return nil
    }
}

private var pixellateKernel:CIWarpKernel? = {
    guard let url = Bundle.main.url(forResource: "default", withExtension: "metallib") else { return nil }
    guard let data = try? Data(contentsOf: url) else { return nil }
    let kernel = try? CIWarpKernel(functionName: "pixellateMetal", fromMetalLibraryData: data)
    return kernel
}()
class PixellateFilter: CIFilter {
    
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    static func register() {
        CIFilter.registerName("\(PixellateFilter.self)", constructor: PixellateFilterGenerator(), classAttributes: [kCIAttributeFilterName:"\(PixellateFilter.self)"])
    }
    
    override func setDefaults() {
        
    }
    
    @objc var inputImage: CIImage?
    
    @objc var radius:CGFloat = 5.0
    
    override var outputImage: CIImage? {
        let result = pixellateKernel?.apply(extent: inputImage!.extent, roiCallback: { (index, rect) -> CGRect in
            return rect
        }, image: self.inputImage!, arguments: [radius])
        return result
    }
    
    override var name: String {
        get {
            return "\(PixellateFilter.self)"
        }
        set {
            
        }
    }
    
    override var attributes: [String : Any] {
        get {
            return [
                "radius":[
                    kCIAttributeMin:1,
                    kCIAttributeDefault:5.0,
                    kCIAttributeType:kCIAttributeTypeScalar
                ]
            ]
        }
    }
}

如果說只是像前面一樣簡單的使用這個濾鏡恐怕還無法體現Metal Shader的高效能,不妨把上面應用自定義濾鏡後直接儲存相簿的操作改成一個滑動條在UIImageView直接預覽:

class ViewController: UIViewController {

    var filter:CIFilter?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.imageView)
        self.view.addSubview(sliderBar)
        
        PixellateFilter.register()
        filter = CIFilter(name: "PixellateFilter")

        guard let cgImage = UIImage(named: "CIFilter_Demo_Origin")?.cgImage else { return }
        let ciImage = CIImage(cgImage: cgImage)
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
    }
    
    @objc func sliderValueChange(_ sender:UISlider) {
        filter?.setValue(sender.value, forKey: "radius")
        if let outputImage = filter?.outputImage {
            self.imageView.image = UIImage(ciImage: outputImage)
        }
    }
    
    private lazy var imageView:UIImageView = {
        let temp = UIImageView(frame: CGRect(x: 0.0, y: 0.0, width: Constants.screenSize.width, height: Constants.screenSize.height-60))
        temp.contentMode = .scaleAspectFill
        temp.image = UIImage(named: "CIFilter_Demo_Origin")
        return temp
    }()
    
    private lazy var sliderBar:UISlider = {
        let temp = UISlider(frame: CGRect(x: 0.0, y: Constants.screenSize.height-50, width: Constants.screenSize.width, height: 30))
        temp.minimumValue = 1
        temp.maximumValue = 20
        temp.addTarget(self, action: #selector(sliderValueChange(_:)), for: UIControl.Event.valueChanged)
        return temp
    }()
}

執行效果:

可以看到,拖動滑動條可以實時預覽濾鏡效果而沒有絲毫卡頓,前面也提到CIImage本身並不包含影象資料,當UIImageView顯示時會在GPU上執行Core Image操作,釋放了CPU的壓力(這也是UIImageView針對Core Image優化的結果)。

無論是通過CIKL還是通過Metal自定義CIFilter都不是萬能的,這是由於kernel本身的限制所造成的。kernel的原理簡單理解就是遍歷一個圖片的所有畫素點,然後通過kernel處理後返回新的畫素點作為新的圖片的畫素點。而類似於繪製直方圖、動漫風格等操作依賴於整個圖片的分佈或者依賴於機器學習的操作則很難使用kernel完成,當然這可以藉助於後面的OpenCV輕鬆做到。

GPUImage

GPUImage可以說是iOS濾鏡開發中多數app的首選,原因在於它不僅高效(從名字就可以看出它執行在GPU上),而且簡單(下面三行程式碼就實現了上面的高斯模糊效果),當然還有它強大的工具屬性。它不僅支援實時濾鏡預覽,還支援視訊實時濾鏡等。

下面是使用高斯模糊的演示:

GPUImageGaussianBlurFilter * blurFilter = [[GPUImageGaussianBlurFilter alloc] init];
 blurFilter.blurRadiusInPixels = 2.0;
 UIImage * image = [UIImage imageNamed:@"CIFilter_Demo_Origin"];
 UIImage *blurredImage = [blurFilter imageByFilteringImage:image];

濾鏡後的效果:

不過可以對比之前的效果,發現GPUImage對於高斯模糊的處理包括了邊緣的處理,並不需要針對邊緣進行重新裁剪。

當然如果不支援自定義那麼GPUImage也談不上強大,GPUImage 自定義濾鏡需要使用 OpenGL 著色語言( GLSL )編寫 Fragment Shader(片段著色器),這些其實和自定義Core Image是類似的。

下面演示了使用GPUImage自定義實現一個圖片暗角濾鏡:

#import <GPUImage/GPUImage.h>

@interface VignetteFilter : GPUImageFilter
    
    @property (nonatomic,assign) CGPoint center;
    @property (nonatomic,assign) CGFloat radius;
    @property (nonatomic,assign) CGFloat alpha;
    
@end
@implementation VignetteFilter {
    GLint centerXUniform,centerYUniform,alphaUniform,radiusUniform;
}

    - (instancetype)init
    {
        self = [super initWithFragmentShaderFromFile:@"VignetteFilter"];
        if (!self) {
            return nil;
        }
        
        centerXUniform = [filterProgram uniformIndex:@"centerX"];
        centerYUniform = [filterProgram uniformIndex:@"centerY"];
        alphaUniform = [filterProgram uniformIndex:@"alpha"];
        radiusUniform = [filterProgram uniformIndex:@"radius"];
        
        self.alpha = 0.5;
        self.radius = 100;
        return self;
    }

    - (void)setCenter:(CGPoint)center {
        [self setFloat:center.x forUniform:centerXUniform program:filterProgram];
        [self setFloat:center.y forUniform:centerYUniform program:filterProgram];
    }
    
    - (void)setAlpha:(CGFloat)alpha {
        [self setFloat:alpha forUniform:alphaUniform program:filterProgram];
    }
    
    - (void)setRadius:(CGFloat)radius {
        [self setFloat:radius forUniform:radiusUniform program:filterProgram];
    }

@end

片段著色器程式碼:

uniform highp float alpha;
uniform lowp float radius;
uniform lowp float centerX;
uniform lowp float centerY;
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
    highp vec2 centerPoint = vec2(centerX, centerY);
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    highp float distance = distance(gl_FragCoord.xy, centerPoint);
    highp float darken = 1.0 - (distance / (radius*0.5) * alpha);
    gl_FragColor = vec4(textureColor.rgb*darken,textureColor.a);
}

濾鏡後的圖片

和Core Image不同的是GPUImage使用的並非CIKL而是GLSL(二者均是類C語言)來編寫濾鏡,優點自然是瞭解片段著色器就可以無過渡編寫濾鏡著色程式碼,無需轉化,同時它也是跨平臺的。缺點就是iOS 12之後Core Image使用Metal引擎逐漸摒棄了OpenGL,效率則更高(當然GPUImage3已經支援Metal Shader,這樣二者就逐漸沒有了區別)。

OpenCV

既然前面提到了OpenGL,那麼就離不開另外一個庫OpenCV,前者主要用於顯示,後者用於運算處理,當然OpenCV預設編譯是不支援的GPU加速的,不過勝在它的演算法強大,演算法速度很快,而且令人興奮的是3.0以後使用CUDA是可以支援使用GPU運算的。

使用OpenCV實現濾鏡更像是使用vImage(存在於Accelerate.framework),不僅可以像上面一樣直接基於畫素進行處理,還能使用它提供的很多強大演算法,同時考慮到自定義運算元OpenCV甚至直接暴漏了Filter2D讓我們可以直接像編寫上面的著色器那樣方便的進行卷積操作。

下面使用OpenCV實現一個羽化操作:

#include <math.h>
#include <opencv/cv.h>
#include <opencv/highgui.h>
#define MAXSIZE (32768)
using namespace cv;
using namespace std;



float mSize = 0.5;

int main()
{
    Mat src = imread("/Users/Kenshin/Downloads/CIFilter_Demo_Origin.jpg",1);
    imshow("src",src);
    int width=src.cols;
    int heigh=src.rows;
    int centerX=width>>1;
    int centerY=heigh>>1;
    
    int maxV=centerX*centerX+centerY*centerY;
    int minV=(int)(maxV*(1-mSize));
    int diff= maxV -minV;
    float ratio = width >heigh ? (float)heigh/(float)width : (float)width/(float)heigh;
    
    Mat img;
    src.copyTo(img);
    
    Scalar avg=mean(src);
    Mat dst(img.size(),CV_8UC3);
    Mat mask1u[3];
    float tmp,r;
    for (int y=0;y<heigh;y++)
    {
        uchar* imgP=img.ptr<uchar>(y);
        uchar* dstP=dst.ptr<uchar>(y);
        for (int x=0;x<width;x++)
        {
            int b=imgP[3*x];
            int g=imgP[3*x+1];
            int r=imgP[3*x+2];
            
            float dx=centerX-x;
            float dy=centerY-y;
            
            if(width > heigh)
                dx= (dx*ratio);
            else
                dy = (dy*ratio);
            
            int dstSq = dx*dx + dy*dy;
            
            float v = ((float) dstSq / diff)*255;
            
            r = (int)(r +v);
            g = (int)(g +v);
            b = (int)(b +v);
            r = (r>255 ? 255 : (r<0? 0 : r));
            g = (g>255 ? 255 : (g<0? 0 : g));
            b = (b>255 ? 255 : (b<0? 0 : b));
            
            dstP[3*x] = (uchar)b;
            dstP[3*x+1] = (uchar)g;
            dstP[3*x+2] = (uchar)r;
        }
    }
    imshow("blur",dst);
    
    waitKey();
    imwrite("/Users/Kenshin/Downloads/blur.jpg",dst);
}

沒錯,這是一段c++程式碼,但在OC中可以很方便的使用,只要實現一個Wrapper類,將.m改為.mm就可以直接呼叫c++程式碼。

下面是羽化後的效果:

總結

從上面可以看到其實開發濾鏡選擇很多,普通的濾鏡使用GPUImage這種基於OpenGL的濾鏡效率比較高、可移植性強,缺點當然就是GLSL除錯比較難,遇到錯誤需要反覆試驗。如果你的App僅僅考慮iOS 11以上的執行環境,自然首推Metal Shading Language,除錯方便又高效,儘管GPUImage3已經支援了Metal Shader但是當前還不完善,很多GPUImage有的功能還在待開發階段當前不建議使用。而OpenCV自然是一把倚天劍,強大的演算法,天然的可移植性,但是由於過於強大,不是類似於人臉識別這種複雜的非著色濾鏡不推薦使用,當然換句話說一旦遇到機器學習相關(例如CARTOONGAN),高階特效一般非OpenCV莫屬