lufei's Studio.

iOS OpenCV 初体验

字数统计: 1k阅读时长: 5 min
2024/03/01 Share

前言

出于兴趣,开始学习 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

CATALOG
  1. 1. 前言
  2. 2. 过程
  3. 3. 结论
  4. 4. 参考