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


filter-logo

概述

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

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

CoreImage

core_image

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)
            }
        }

原圖

CIFilter_Demo_Origin

應用高斯模糊

CIFilter_Demo_Gauss

濾鏡鏈
所謂濾鏡鏈就是將一個濾鏡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框架也提供了這個濾鏡:CIConvolution3X3CIConvolution5X5。這兩個濾鏡支持開發者自定義算子實現一個濾鏡操作,下面是使用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算子后的效果:

CIFilter_Demo_Sobel

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

sobel_demo

除了上面的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)
        }

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

CIFilter_Demo_Flip

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

由於篇幅原因關於編寫CIKL的具體細節這里不再贅述,感興趣可以參考Writing KernelsCore 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-MetalShader

如果你編寫過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
    }()
}

運行效果:

Metal-Shader-Demo

可以看到,拖動滑動條可以實時預覽濾鏡效果而沒有絲毫卡頓,前面也提到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_GaussianBlur_Demo

不過可以對比之前的效果,發現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);
}

濾鏡后的圖片

GPUImage_Vignette_Demo

和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++代碼。

下面是羽化后的效果:

CIFilter_Demo_EdgeBlur

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM