修改證件照背景顏色
近期我們的某個需求中有一個需求是需要替換證件照背景顏色,且我們不希望使用三方框架,且需要使用原生來實現,之前並未接觸過此類圖片處理相關的任務,因此需要仔細調研下各類實現方式,確認是否滿足產品要求。
方案
為證件照替換背景顏色,我們可以想到的無非就是這幾種方案
- 利用coreImage方法直接替換圖片中的某個顏色為另一個顏色
- 利用VisionKit邊緣檢測摳圖
具體實現
替換圖片中某個指定的顏色
這個方案我們的思路就是找到圖片中對應要替換的顏色的畫素點 然後替換
首先我們需要一個對比顏色是否相同的方法,這裡我們通過對比圖片的RGB來判斷
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 }
然後我們需要一個從圖片某個點取色的方法
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) } } }
下面是顏色替換
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
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) }
下面我們要找到對應顏色,然後設定透明
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; }
VNGenerateObjectnessBasedSaliencyImageRequest+VNDetectContoursRequest
VisionKit中的這兩個類VNGenerateObjectnessBasedSaliencyImageRequest可獲取圖片顯著性區域,VNDetectContoursRequest可進行邊緣檢測通過這兩部來摳出識別出顯著區域的圖片
直接使用系統的方法,我們這裡也不多廢話 直接看程式碼
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 呼叫很簡單 不需要藉助其他環境
缺點:
- 系統API需要iOS13+
- 效果比鋸齒更加明顯
OpenCV
在調研過程中,我們發現使用C++實際上有很多現有的方法,但是使用iOS目前可用的比較少,因此我們決定先配置一個C++的OpenCV的環境,先使用C++進行嘗試,如果可行在使用Swift進行翻譯,這樣會快一點
OpenCV在MacOS上的環境配置大家可以參考我的這篇文章 Mac 配置C++ OpenCV環境
待續……