近期我们的某个需求中有一个需求是需要替换证件照背景颜色,且我们不希望使用三方框架,且需要使用原生来实现,之前并未接触过此类图片处理相关的任务,因此需要仔细调研下各类实现方式,确认是否满足产品要求。
为证件照替换背景颜色,我们可以想到的无非就是这几种方案
这个方案我们的思路就是找到图片中对应要替换的颜色的像素点 然后替换 首先我们需要一个对比颜色是否相同的方法,这里我们通过对比图片的RGB来判断
12345678910111213141516
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}
然后我们需要一个从图片某个点取色的方法
1234567891011121314151617181920212223242526272829303132333435
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) } }}
下面是颜色替换
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
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 }
下面我们来找个照片看下替换效果
在查找方案的时候,网上有讨论要将RGB转换为HSV然后判断对应HSV的颜色,将对应HSV中的透明度设置为0。即将想要删除掉颜色的部分设置为透明,这样的话图片就会从一个有背景颜色的图变换为一个透明背景的图,这样就可以随意更换颜色了。
在开始之前,我们需要一个方法来将RGBA转换为HSV
1234567891011121314
var hsba: (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat) { /** hue:色相 saturation:饱和度 brightness:亮度 alpha:透明度 */ 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)}
下面我们要找到对应颜色,然后设置透明
1234567891011121314151617181920212223242526272829303132
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); // Blue value for (int y = 0; y < size; y++){ rgb[1] = ((double)y)/(size-1); // Green value for (int x = 0; x < size; x ++){ rgb[0] = ((double)x)/(size-1); // Red value rgbToHSV(rgb,hsv); // Use the hue value to determine which to make transparent // The minimum and maximum hue angle depends on // the color you want to remove float alpha = (hsv[2] == 1 && hsv[1] == 0) ? 0.0f: 1.0f; // Calculate premultiplied alpha values for the cube c[0] = rgb[0] * alpha; c[1] = rgb[1] * alpha; c[2] = rgb[2] * alpha; c[3] = alpha; c += 4; // advance our pointer into memory for the next color value } } } map.data = cubeData; return map;}
VisionKit中的这两个类VNGenerateObjectnessBasedSaliencyImageRequest可获取图片显著性区域,VNDetectContoursRequest可进行边缘检测通过这两部来抠出识别出显著区域的图片
直接使用系统的方法,我们这里也不多废话 直接看代码
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
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)") } }
效果如下:
鉴于实现思路与第一个方案是类似的,所以其优缺点也基本是一致的。这里我们不在赘述
我们再来看下结果:
还是会有锯齿的出现,效果不太满意, 我们在来看下这个方案的优缺点
在调研过程中,我们发现使用C++实际上有很多现有的方法,但是使用iOS目前可用的比较少,因此我们决定先配置一个C++的OpenCV的环境,先使用C++进行尝试,如果可行在使用Swift进行翻译,这样会快一点
OpenCV在MacOS上的环境配置大家可以参考我的这篇文章 Mac 配置C++ OpenCV环境