OpenCV在iOS端的集成及Mat和UIImage互相转化(附源码)

OpenCV是一个非常强大的图形处理框架,可以运行在Linux、Windows、Android和Mac OS操作系统上,在自动驾驶、智能家居、人脸识别、图片处理等方面提供了非常丰富且功能强大的api,在图片处理方便,基本上可以满足对图片处理的所有需求。近期项目中有使用opencv作为图片处理框架的需求,而且项目对图片处理的需求并不是最常用的8bit色深图片,而是16bit色深,所以在开发的过程中踩了很多坑,同时也对opencv的使用有了更深的理解,特此记录回顾,也希望能给正在研究OpenCV的小伙伴提供一点思路。

本文简单讲解OpenCV的集成及Mat和UIImage互相转化,下一篇文章会详细记录使用OpenCV对图片进行类似于美图秀秀的各种处理功能。

一 集成OpenCV

OpenCV的集成有两种方式

1.使用cocoapods进行集成,在Podfile文件中使用

pod 'OpenCV', '~> 4.7.0'

即可集成opencv的4.7.0版本

2.手动集成

需要去Opencv官网下载iOS端使用的框架,下载地址
https://opencv.org/releases/

d9e0ca17c4fb4496abfcc22d057678aa.png

选择iOS端的包下载就好,然后将下载下来的文件夹整个导入项目中


7616f7b1448146b384bd75a4a9c119a7.png

即可正常使用

二 Mat和UIImage的互相转化

Mat是OpenCV中提供的一个重要的类,Mat中包含了图片的很多信息,比如图片的像素宽高、通道数量,iOS端使用opencv框架对图片的处理基本上也都需要转化为Mat对象之后才可以正常进行。

注意:转化方法使用c++代码,所以在代码的编写文件以及使用该文件的地方,都需要将.m改为.mm,以告诉编译器以c++的形式来编译这些文件,否则会报错。

代码里特意标明了清晰的注释,帮助小伙伴理解。想深入梳理的务必阅读,伸手党直接复制粘贴就好,互相转化的方法对8位及16位的RGB及RGBA图片都做了兼容,可以愉快使用。其他特殊格式如16bpp的图片,请照葫芦画瓢,单独处理,思路和方法是一样的。

1.UIImage转Mat

+(cv::Mat)cvMatFromUIImage:(UIImage *)image
{
    //获取图片的CGImageRef结构体
    CGImageRef imageRef = CGImageCreateCopy([image CGImage]);
    //获取图片尺寸
    CGSize size = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    //获取图片宽度
    CGFloat cols = size.width;
    //获取图高度
    CGFloat rows = size.height;
    //获取图片颜色空间,创建图片对应Mat对象,需要使用同样的颜色空间
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
    
    //判断图片的通道位深及通道数 默认使用8位4通道格式
    int type = CV_16UC4;
    //获取bitmpa位数
    size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
    //获取通道位深
    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
    //获取通道数
    size_t channels = bitsPerPixel/bitsPerComponent;
    if(channels == 3 || channels == 4){  // 因为quartz框架只支持处理带有alpha通道的数据,所以3通道的图片采取跟4通道的图片一样的处理方式,转化的时候alpha默认会赋最大值,归一化的数值位1.0,这样即使给图片增加了alpha通道,也并不会影响图片的展示
        if(bitsPerComponent == 8){
            //8位3通道 因为iOS端只支持
            type = CV_8UC4;
        }else if(bitsPerComponent == 16){
            //16位3通道
            type = CV_16UC4;
        }else{
            printf("图片格式不支持");
            abort();
        }
    }else{
        printf("图片格式不支持");
        abort();
    }
    
    //创建位图信息  根据通道位深及通道数判断使用的位图信息
    CGBitmapInfo bitmapInfo;
    
    if(bitsPerComponent == 8){
        if(channels == 3){
            bitmapInfo = kCGImageAlphaNone | kCGImageByteOrderDefault;
        }else  if(channels == 4){
            bitmapInfo = kCGImageAlphaPremultipliedLast | kCGImageByteOrderDefault;
        }else{
            printf("图片格式不支持");
            abort();
        }
    }else if(bitsPerComponent == 16){
        if(channels == 3){  //虽然是三通道,但是iOS端的CGBitmapContextCreate方法不支持16位3通道的创建,所以仍然作为4通道处理
            bitmapInfo = kCGImageAlphaPremultipliedLast | kCGImageByteOrder16Little;
        }else  if(channels == 4){
            bitmapInfo = kCGImageAlphaPremultipliedLast | kCGImageByteOrder16Little;
        }else{
            printf("图片格式不支持");
            abort();
        }
    }else{
        printf("图片格式不支持");
        abort();
    }


    //使用获取到的宽高创建mat对象CV_16UC4 为传入的矩阵类型
    cv::Mat cvMat(rows, cols, type); // 每通道8bit 共有4通道(RGB + Alpha通道 RGBA格式)
    CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,                 // 数据源
                                                    cols,                       // 每行像素数
                                                    rows,                       // 列数(高度)
                                                    bitsPerComponent,                          // 每个通道bit数
                                                    cvMat.step[0],              // 每行字节数
                                                    colorSpace,                 // 颜色空间
                                                    bitmapInfo); // 位图信息(alpha通道信息,字节读取信息)
    //将图片绘制到上下文中mat对象中
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
    //释放imageRef对象
    CGImageRelease(imageRef);
    //释放颜色空间
    CGColorSpaceRelease(colorSpace);
    //释放上下文环境
    CGContextRelease(contextRef);
    return cvMat;
}

2.Mat转Image

+(UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
    //获取矩阵数据
    NSData *data = [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
    //判断矩阵使用的颜色空间
    CGColorSpaceRef colorSpace;
    if (cvMat.elemSize() == 1) {
        colorSpace = CGColorSpaceCreateDeviceGray();
    } else {
        colorSpace = CGColorSpaceCreateDeviceRGB();
    }
    //创建数据privder
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    
    //获取bitmpa位数
    size_t bitsPerPixel = cvMat.elemSize()*8;
    //获取通道数
    size_t channels = cvMat.channels();
    //获取通道位深
    size_t bitsPerComponent = bitsPerPixel/channels;
    
    //创建位图信息  根据通道位深及通道数判断使用的位图信息
    CGBitmapInfo bitmapInfo;
    if(bitsPerComponent == 8){
        if(channels == 3){
            bitmapInfo = kCGImageAlphaNone | kCGImageByteOrderDefault;
        }else if(channels == 4){
            bitmapInfo = kCGImageAlphaPremultipliedLast | kCGImageByteOrderDefault;
        }else{
            printf("图片格式不支持");
            abort();
        }
    }else if(bitsPerComponent == 16){
        if(channels == 3){
            bitmapInfo = kCGImageAlphaNone | kCGImageByteOrder16Little;
        }else if(channels == 4){
            bitmapInfo = kCGImageAlphaPremultipliedLast | kCGImageByteOrder16Little;
        }else{
            printf("图片格式不支持");
            abort();
        }
    }else{
        printf("图片格式不支持");
        abort();
    }
    
   

    //根据矩阵及相关信息创建CGImageRef结构体
    CGImageRef imageRef = CGImageCreate(cvMat.cols, //矩阵宽度
                                        cvMat.rows, //矩阵列数
                                        bitsPerComponent,        //通道位深
                                        8 * cvMat.elemSize(),  //每个像素位深
                                        cvMat.step[0],  //每行占用字节数
                                        colorSpace,    //使用的颜色空间
                                        bitmapInfo,//通道排序、大小端读取顺序信息
                                        provider, //数据源
                                        NULL,   //解码数组 一般传null
                                        true, //是否抗锯齿
                                        kCGRenderingIntentDefault   //使用默认的渲染方式
                                        );
    // 通过cgImage转化出来UIImage对象
    UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
    //释放imageRef
    CGImageRelease(imageRef);
    //释放provider
    CGDataProviderRelease(provider);
    //释放颜色空间
    CGColorSpaceRelease(colorSpace);
    return finalImage;
}

3.一个小工具,使用Mat打印图片详细信息,方便核对数据

//获取图片信息
+(void)readInfoWithImage:(UIImage*)inputImage{
    Mat inputMat = [CVTools matFromImage:inputImage];
    printf("图片宽度 = %d \n",inputMat.cols);
    printf("图片高度 = %d \n",inputMat.rows);
    printf("通道位深 = %zu \n",inputMat.elemSize()*8/inputMat.channels());
    printf("通道数 %d \n",inputMat.channels());
    printf("每个像素bit数 = %zu \n",inputMat.elemSize()*8);

    printf("每行元素的字节数 = %zu \n",inputMat.step[0]);
}

三 常见问题

1.提示不支持的参数组合

因为quarzt 2D框架对于图片的处理有着严格的规定,所以对于Bitmapinfo内的alpha通道和读取顺序组合有着明确的规则,报错如下


1b69d955b3cd467a93e4a07a5e2117a0.png

解决方法
第一种方式是通过官网查阅quartz允许的组合搭配,官网截图如下:

782f2d2601ba4f9182c70f04efd6dbd1.png

第二种方法是根据提示去设置环境变量在Log窗口打印支持的组合搭配,设置方式如下


939cdd79342b4fb885b71da73884c77d.png
50aebc2e6cd846e3881dd6ce3974e91e.png

增加“CGBITMAP_CONTEXT_LOG_ERRORS”位图环境错误log信息的打印,然后再运行Log窗口输出如下:


f1944feaead744f680cd4731dd78ad9d.png

可以看到,对于8Bit和16Bit通道位深的图片,quartz只支持带有alpha通道的,通道的读取方式也有明确规定,根据自己的图片格式采取相应的配置就可以了。
因为quartz框架只支持处理带有alpha通道的数据,所以3通道的图片采取跟4通道的图片一样的处理方式,转化的时候alpha默认会赋最大值,归一化的数值位1.0,这样即使给图片增加了alpha通道,也并不会影响图片的展示

这个地方很坑,以16位图片来说,即使明知图片是含有alpha通道的,而且alpha通道的位置在最后,也并不能使用kCGImageAlphaLast的图片通道信息,而是要使用kCGImageAlphaPremultipliedLast的枚举来约束,但是如果是8位的图片却并没有这个限制,而且字节读取顺序需要额外注明使用16位小端读取kCGImageByteOrder16Little,做16位图片处理的小伙伴一定要注意,深坑啊。

2.在导入头文件的时候,一定要将oencv用到的头文件放在所有OC的文件引用之前引用,否则会出现函数重定义冲突

以该测试工程里的文件为例,头文件引用方式为:

#import <opencv2/opencv.hpp>
#import <opencv2/imgcodecs/ios.h>
#include <math.h>
#include <iostream>
using namespace cv;
using namespace std;

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

使用的命名空间也需要额外声明。

有想法欢迎交流,等你。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容