前言 出于兴趣,开始学习 OpenCV (其实是领导安排QAQ,需要实现一个小需求),正好好久没更新博客,那就边写边学吧~
过程 安装的部分就跳过了,手动导入,或者 pod 导入,都可以,有一点要说的是目前来说,因为 OpenCV 的语言是 C++,所以不可避免的你需要一个支持和 C++ 混编的环境,如果你的项目的主要语言是 Swift,那么比较好的选择就是使用 OC 作为媒介来与 C++ 交互。
具体地,你需要创建一个 .mm 文件,在这个文件里面编写 OpenCV 相关代码,然后通过桥接文件暴露给 Swift 使用,大概是这样的过程。
当然 OC 里的 UIImage 和 OpenCV 的 Mat 你也是需要提供一个互相转换的方法的,如下所示:
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 + (Mat)_matFrom:(UIImage *)source { CGImageRef image = CGImageCreateCopy(source.CGImage); CGFloat cols = CGImageGetWidth(image); CGFloat rows = CGImageGetHeight(image); Mat result(rows, cols, CV_8UC4); CGBitmapInfo bitmapFlags = kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault; size_t bitsPerComponent = 8; size_t bytesPerRow = result.step[0]; CGColorSpaceRef colorSpace = CGImageGetColorSpace(image); CGContextRef context = CGBitmapContextCreate(result.data, cols, rows, bitsPerComponent, bytesPerRow, colorSpace, bitmapFlags); CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, cols, rows), image); CGContextRelease(context); return result; } + (UIImage *)_imageFrom:(Mat)source { NSData *data = [NSData dataWithBytes:source.data length:source.elemSize() * source.total()]; CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); CGBitmapInfo bitmapFlags = kCGImageAlphaNone | kCGBitmapByteOrderDefault; size_t bitsPerComponent = 8; size_t bytesPerRow = source.step[0]; CGColorSpaceRef colorSpace = (source.elemSize() == 1 ? CGColorSpaceCreateDeviceGray() : CGColorSpaceCreateDeviceRGB()); CGImageRef image = CGImageCreate(source.cols, source.rows, bitsPerComponent, bitsPerComponent * source.elemSize(), bytesPerRow, colorSpace, bitmapFlags, provider, NULL, false, kCGRenderingIntentDefault); UIImage *result = [UIImage imageWithCGImage:image]; CGImageRelease(image); CGDataProviderRelease(provider); CGColorSpaceRelease(colorSpace); return result; }
然后你就可以尝试用 OpenCV 实现一些简单的图片处理,我第一个想实现的就是利用 Vision 检测出当前图片的嘴唇的所有点位,并修改唇色,这个功能很多美妆 app 都可以实现,思考起来好像也不是很难,如果不用 OpenCV ,直接使用原生的 api 来做的,大概是这样:
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 func detectFaceLandmarks(in image: UIImage) -> VNFaceLandmarks2D? { let faceLandmarksRequest = VNDetectFaceLandmarksRequest() guard let cgImage = image.cgImage else { return nil } let handler = VNImageRequestHandler(cgImage: cgImage, orientation: .up, options: [:]) guard (try? handler.perform([faceLandmarksRequest])) != nil else { return nil } guard let observation = faceLandmarksRequest.results?.first as? VNFaceObservation, let landmarks = observation.landmarks else { return nil } return landmarks } func drawLandmarks(image: UIImage, landmarks: VNFaceLandmarks2D) -> UIImage? { guard let cgImage = image.cgImage else { return nil } let outerLips = landmarks.outerLips?.pointsInImage(imageSize: image.size) UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.translateBy(x: 0, y: image.size.height) context.scaleBy(x: 1.0, y: -1.0) context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: image.size)) context.setFillColor(UIColor.blue.cgColor) if let outerLips = outerLips { let outerLipsPath = UIBezierPath(cgPath: createPath(from: outerLips)) context.addPath(outerLipsPath.cgPath) context.fillPath() } let colorizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return colorizedImage } func createPath(from points: [CGPoint]) -> CGPath { let path = CGMutablePath() guard let firstPoint = points.first else { return path } path.move(to: firstPoint) for point in points.dropFirst() { path.addLine(to: point) } path.closeSubpath() return path } func changeLipColorToBlue(in image: UIImage) -> UIImage? { guard let landmarks = detectFaceLandmarks(in: image) else { return image } return drawLandmarks(image: image, landmarks: landmarks) }
参考这个过程,如果使用 OpenCV 的话,其实思路是类似的,特别注意的话就是,坐标系的转换,因为 UIKit 和 OpenCV 的原点坐标不同,所以在传入 OpenCV api 里前,需要提前转换一下坐标,查找到合适的 api 后,大概的实现如下:
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 + (UIImage *)changeLipColorInImage:(UIImage *)image lipPoints:(NSArray<NSValue *> *)points { Mat matImage = [OpenCVWrapper _matFrom:image]; std::vector<cv::Point2f> cvPoints; for (NSValue *pointValue in points) { CGPoint cgPoint = [pointValue CGPointValue]; cv::Point2f cvPoint = cv::Point2f(cgPoint.x, image.size.height - cgPoint.y); // 垂直翻转 cvPoints.push_back(cvPoint); } // 将点的vector转换为点的数组 std::vector<cv::Point> pts; for (const auto& pt2f : cvPoints) pts.push_back(cv::Point(pt2f.x, pt2f.y)); // 绘制不规则图形 cv::polylines(matImage, pts, true, cv::Scalar(255, 0, 0)); // 填充图形 cv::fillPoly(matImage, std::vector<std::vector<cv::Point>>{pts}, cv::Scalar(255, 0, 0)); UIImage *result = [OpenCVWrapper _imageFrom:matImage]; return result; }
结论 最终实现的效果还可以,成就感拉满QAQ~
不过想要深入,还是继续学习,不光是 OpenCV 的 api,C++ 的语法什么的,也都要继续学习,任重而道远
参考 iOS OpenCV
Vision
OpenCV Swift Wrapper