近期我们的某个需求中有一个需求是需要替换证件照背景颜色,且我们不希望使用三方框架,且需要使用原生来实现,之前并未接触过此类图片处理相关的任务,因此需要仔细调研下各类实现方式,确认是否满足产品要求。
方案 为证件照替换背景颜色,我们可以想到的无非就是这几种方案
利用coreImage方法直接替换图片中的某个颜色为另一个颜色
利用VisionKit边缘检测抠图
具体实现 替换图片中某个指定的颜色 这个方案我们的思路就是找到图片中对应要替换的颜色的像素点 然后替换 首先我们需要一个对比颜色是否相同的方法,这里我们通过对比图片的RGB来判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private func compareColor ( firstColor: UIColor, secondColor: UIColor, tolerance: CGFloat ) -> Bool { var r1: CGFloat = 0.0 , g1: CGFloat = 0.0 , b1: CGFloat = 0.0 , a1: CGFloat = 0.0 ; var r2: CGFloat = 0.0 , g2: CGFloat = 0.0 , b2: CGFloat = 0.0 , a2: CGFloat = 0.0 ; firstColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) secondColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) return abs (r1 - r2) <= tolerance && abs (g1 - g2) <= tolerance && abs (b1 - b2) <= tolerance && abs (a1 - a2) <= tolerance }
然后我们需要一个从图片某个点取色的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 extension UIImage { func getPointColor (at point: CGPoint) -> UIColor? { guard CGRect (origin: CGPoint (x: 0 , y: 0 ), size: size).contains (point) else { return nil } let pointX = trunc(point.x); let pointY = trunc(point.y); let width = size.width; let height = size.height; let colorSpace = CGColorSpaceCreateDeviceRGB (); var pixelData: [UInt8 ] = [0 , 0 , 0 , 0 ] pixelData.withUnsafeMutableBytes { pointer in if let context = CGContext (data: pointer.baseAddress, width: 1 , height: 1 , bitsPerComponent: 8 , bytesPerRow: 4 , space: colorSpace, bitmapInfo: CGImageAlphaInfo .premultipliedLast.rawValue), let cgImage = cgImage { context.setBlendMode(.copy) context.translateBy(x: -pointX, y: pointY - height) context.draw(cgImage, in : CGRect (x: 0 , y: 0 , width: width, height: height)) } } let red = CGFloat (pixelData[0 ]) / CGFloat (255.0 ) let green = CGFloat (pixelData[1 ]) / CGFloat (255.0 ) let blue = CGFloat (pixelData[2 ]) / CGFloat (255.0 ) let alpha = CGFloat (pixelData[3 ]) / CGFloat (255.0 ) if #available(iOS 10.0 , *) { return UIColor (displayP3Red: red, green: green, blue: blue, alpha: alpha) } else { return UIColor (red: red, green: green, blue: blue, alpha: alpha) } } }
下面是颜色替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 func replaceColor(_ color: UIColor, with: UIColor, tolerance: CGFloat = 0.5) -> UIImage { guard let imageRef = self.cgImage else { return self } // 获取要替换颜色的RGBA信息方便后续判断 let withColorComponents = with.cgColor.components let newRed = UInt8(withColorComponents![0] * 255) let newGreen = UInt8(withColorComponents![1] * 255) let newBlue = UInt8(withColorComponents![2] * 255) let newAlpha = UInt8(withColorComponents![3] * 255) let width = imageRef.width let height = imageRef.height let bytesPerPixel = 4 let bytesPerRow = bytesPerPixel * width let bitmapByteCount = bytesPerRow * height // 申请bitmap要对应的空间 let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount) defer { rawData.deallocate() } guard let colorSpace = CGColorSpace(name: CGColorSpace.genericRGBLinear) else { return self } // 根据上述信息创建一个context guard let context = CGContext( data: rawData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue ) else { return self } let rc = CGRect(x: 0, y: 0, width: width, height: height) // 绘制图片信息 context.draw(imageRef, in: rc) var byteIndex = 0 // 依次遍历每个像素 while byteIndex < bitmapByteCount { // 获取图片当前位置对应的像素RGBA信息 let red = CGFloat(rawData[byteIndex + 0]) / 255 let green = CGFloat(rawData[byteIndex + 1]) / 255 let blue = CGFloat(rawData[byteIndex + 2]) / 255 let alpha = CGFloat(rawData[byteIndex + 3]) / 255 let currentColor = UIColor(red: red, green: green, blue: blue, alpha: alpha) // 比较当前颜色的RGBA信息与要被替换的图片的RGBA信息 如果在允许范围内 则替换成新的 if compareColor(firstColor: color, secondColor: currentColor, tolerance: tolerance) { rawData[byteIndex + 0] = newRed rawData[byteIndex + 1] = newGreen rawData[byteIndex + 2] = newBlue rawData[byteIndex + 3] = newAlpha } byteIndex += 4 } // 替换完颜色生成对应图片 guard let image = context.makeImage() else { return self } let result = UIImage(cgImage: image) return result }
下面我们来找个照片看下替换效果
优点:
简单,直接,不用集成其他库,可实现颜色替换 缺点:
无法很好的适应图片,其中tolerance的设置不同图片可能需要设置不同,无法取一个定值
适合传入图片背景颜色固定的 不够灵活
而且相同色值的就进行替换可能会有误伤
HSV 颜色透明 在查找方案的时候,网上有讨论要将RGB转换为HSV然后判断对应HSV的颜色,将对应HSV中的透明度设置为0。即将想要删除掉颜色的部分设置为透明,这样的话图片就会从一个有背景颜色的图变换为一个透明背景的图,这样就可以随意更换颜色了。
在开始之前,我们需要一个方法来将RGBA转换为HSV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var hsba: (hue: CGFloat , saturation: CGFloat , brightness: CGFloat , alpha: CGFloat ) { var h: CGFloat = 0 var s: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 self .getHue(&h, saturation: &s, brightness: &b, alpha: &a) return (h * 360 , s, b, a) }
下面我们要找到对应颜色,然后设置透明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 struct CubeMap createCubeMap (float h , float s , float v ) { const unsigned int size = 64 ; struct CubeMap map ; map .length = size * size * size * sizeof (float ) * 4; map .dimension = size ; float *cubeData = (float *)malloc (map .length ); float rgb [3], hsv [3], *c = cubeData ; for (int z = 0; z < size ; z ++) { rgb[2 ] = ((double)z)/(size-1 ); for (int y = 0 ; y < size; y++){ rgb[1 ] = ((double)y)/(size-1 ); for (int x = 0 ; x < size; x ++){ rgb[0 ] = ((double)x)/(size-1 ); rgbToHSV(rgb,hsv); float alpha = (hsv[2 ] == 1 && hsv[1 ] == 0 ) ? 0 .0f: 1 .0f; c [0 ] = rgb[0 ] * alpha; c [1 ] = rgb[1 ] * alpha; c [2 ] = rgb[2 ] * alpha; c [3 ] = alpha; c += 4 ; } } } map .data = cubeData; return map ; }
VNGenerateObjectnessBasedSaliencyImageRequest+VNDetectContoursRequest
VisionKit中的这两个类VNGenerateObjectnessBasedSaliencyImageRequest可获取图片显著性区域,VNDetectContoursRequest可进行边缘检测通过这两部来抠出识别出显著区域的图片
直接使用系统的方法,我们这里也不多废话 直接看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 func detectPhoto (photo: UIImage) -> UIImage { let ciOriginImage = CIImage (cgImage: photo.cgImage!) let imageHandler = VNImageRequestHandler (ciImage: ciOriginImage, options: [:]) let attensionRequest = VNGenerateObjectnessBasedSaliencyImageRequest { [weak self ] request, error in if let err = error { print ("发生了错误 \(err.localizedDescription)" ) return } if let result = request.results, result.count > 0 , let observation = result.first as ? VNSaliencyImageObservation { self ?.heatMapProcess(pixelBuffer: observation.pixelBuffer, ciImage: ciOriginImage) } } do { try imageHandler.perform([attensionRequest]) } catch { print (error.localizedDescription) } return photo } private func heatMapProcess (pixelBuffer: CVPixelBuffer, ciImage: CIImage) { let heatImge = CIImage (cvPixelBuffer: pixelBuffer) let contourRequest = VNDetectContoursRequest { [weak self ] request, error in if let err = error { print ("发生了错误 \(err.localizedDescription)" ) return } if let result = request.results, result.count > 0 , let observation = result.first as ? VNContoursObservation { let cxt = CIContext () let origin = cxt.createCGImage(ciImage, from: ciImage.extent) let _ = self ?.drawContour(contourObv: observation, cgImage: nil , originImg: origin) } } contourRequest.revision = VNDetectContourRequestRevision1 contourRequest.contrastAdjustment = 1.0 contourRequest.detectsDarkOnLight = false contourRequest.maximumImageDimension = 512 let handler = VNImageRequestHandler (ciImage: heatImge, options: [:]) do { try handler.perform([contourRequest]) } catch { print ("\(error.localizedDescription)" ) } }
效果如下:
鉴于实现思路与第一个方案是类似的,所以其优缺点也基本是一致的。这里我们不在赘述
我们再来看下结果:
还是会有锯齿的出现,效果不太满意, 我们在来看下这个方案的优缺点
优点:
采用的是边缘检测的思路,不会出现颜色替换时范围不可控的问题
使用系统API 调用很简单 不需要借助其他环境
缺点:
OpenCV 在调研过程中,我们发现使用C++实际上有很多现有的方法,但是使用iOS目前可用的比较少,因此我们决定先配置一个C++的OpenCV的环境,先使用C++进行尝试,如果可行在使用Swift进行翻译,这样会快一点
OpenCV在MacOS上的环境配置大家可以参考我的这篇文章Mac 配置C++ OpenCV环境
待续……