移动端视频进阶(三):OpenCV的集成及视频帧转cv::Mat的相关操作

前言

在前两篇文章总结了移动端iOS和Android摄像头数据的回调预览,及向RGB的转换,还有对YUV420数据格式做了详细的了解!
对于计算机图形图像的处理,有一个强大的开源库OpenCV,对应的也有移动端的release版本,可以很方便的帮助我们对摄像头的一帧数据进行各种处理,下边就来了解一下移动端和OpenCV的结合!

集成

首先我们要去OpenCV的官网,找到release版块!也就是下边的网址:
https://opencv.org/releases/

PS:今天,也就是2019年4月18日,OpenCV上线了全新的官网!同样的今天也发布了最新的V4.1.0版本!哇,对比前一天的官网,简直就是一个大变身!好看了很多!现在才像一个9012的官网,之前的简直是上个世纪的产物!

好了,言归正传,到了这个页面之后,会发现有好多release的版本,我们只需要选择所学需的版本和平台即可,当然我们这里需要iOS packAndroid pack。选中点击下载即可!

iOS

iOS的集成非常简单!
将下载好的opencv-x.x.x-ios-framework拖入项目中,然后在Build Phase->Link Binary With Libraries中添加这个framework即可!

Android

Android的集成稍微有点复杂。首先将文件文件解压后,有sdk、apk、samples三个文件夹,分别对应中opencv的java实现,opencv manager的安装包和示例项目!集成的话,只需要sdk这个目录即可!
①打开Android Studio,选择File->New->Import Module,然后在弹出的Source Directory中选择填入你下载的sdk的路径xxx/xxx/opencv-android-sdk/sdk/java,这里选择到java目录,点击Finish!module name会根据下载版本号自动填入,比如你下载的是V3.4.3,module name就是openCVLibrary343。

②打开Project Structure,选择Dependencies,点击+号添加选择Module Dependency,选择上一步添加的module即可。

③将sdk/native/libs文件夹下so文件全部拷入项目libs目录下,之后在build.gradle中指定:

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs'];
        }
    }

④在使用之前要先加载:

    static {
        boolean load = OpenCVLoader.initDebug();
        if (!load) {
            Log.i("CV", "Open CV Libraries has failed to load...");
        }
    }

视频帧数据转Mat

iOS(sampleBuffer转Mat)

opencv在iOS中的使用,需要注意两个地方:
①因为它是用c++写的,所以在使用到opencv的地方,要将文件格式后缀由.m改为.mm
②所有的opencv的导包,必须在apple的包的前边,否则会报错!

看一下转换代码:

    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CFRetain(imageBuffer);
    const int imageWidth = (int)CVPixelBufferGetWidth(imageBuffer);//480
    const int imageHeight = (int)CVPixelBufferGetHeight(imageBuffer);//640
    CVPixelBufferLockFlags unlockFlags = kNilOptions;
    CVPixelBufferLockBaseAddress(imageBuffer, unlockFlags);
    unsigned char* sourceBaseAddr = (unsigned char*)(CVPixelBufferGetBaseAddress(imageBuffer));
    cv::Mat image(imageHeight, imageWidth, CV_8UC4, sourceBaseAddr, 0);

	...//对mat进行操作

	CVPixelBufferUnlockBaseAddress(imageBuffer, unlockFlags);
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    CFRelease(imageBuffer);

上边转换是对应的32BRGA格式,那么如果NV12格式的数据,要改成下边的:

	cv::Mat image(imageHeight * 3 / 2, imageWidth, CV_8UC1, sourceBaseAddr, 0);

注意这里,取imageHeight * 3 / 2,关于yuv所占用的存储空间,在之前介绍过,这里就不再赘述了!

Android(NV21 frame转Mat)

对于CameraAPI回调的NV21格式的数据,同样的:

Mat mat  = new Mat(height*3/2, width, CV_8UC1);
mat.put(0, 0, bytes);

...//对mat进行操作

//再把处理过的mat重新拷回
byte[] data_result = new byte[bytes.length];
mat.get(0, 0, data_result);
System.arraycopy(data_result, 0, bytes, 0, data_result.length);

Mat的相关操作

数据格式的转换

在实际应用中,对数据格式的转换是避免不了的问题。比如TensorFlow中就只接收RGB格式的数据,所以需要对mat进行格式的转换!

iOS

对于NV12格式的数据转换为RGB,进行处理之后,再转换NV12:

cv::cvtColor(mat, mat, YUV2RGB_NV12);

...//相关处理

cv::cvtColor(mat, mat, RGB2YUV_NV12);

Android

对于NV21格式数据转换RGB,我们在之前讲了java的实现,需要了解的可以参考前边的文章!这里说一下mat:

cvtColor(mat,mat,COLOR_YUV2RGB_NV21);

对于再次转回,不知道这里为什么opencv没有提供RGB到NV21的转换接口?如果有知道的烦请告知!
那么,这里只有将mat数据再提取到byte[]数组中,自己用java写转换了!

还有就是NV21转不回来,I420可以用:

cvtColor(mat,mat,COLOR_YUV2RGB_I420);
...//相关处理
cvtColor(mat,mat,COLOR_RGB2YUV_I420);

Mat和UIImage/Bitmap的转换

opencv在两个平台都提供对应的工具类,可以非常方便的完成Mat和对应图像表示的转换!

iOS

UIImage* img = MatToUIImage(mat);
cv::Mat mat = UIImageToMat(image)

Android

Utils.matToBitmap(mat,rgbBitmap);
Utils.bitmapToMat(rgbBitmap,mat)

其他常用操作

这里有一个问题需要注意一下,OpenCV对于所有图像的读取,操作都是基于BGR的格式,尽管现在大多数采集到的数据都是RGB格式。这应该是一个历史遗留问题,我们需要注意,也是常说的“红蓝颠倒”。

iOS

cv::Mat dstMat;
cv::transpose(mat, dstMat);//转置
cv::flip(dstMat, dstMat, 1);//绕纵轴翻转, 0表示绕横轴翻转,-1表示绕两个坐标轴都翻转

上边两步操作可以模拟mat顺时针旋转90度。

然后:

//这里的255指的B通道,蓝色,也就是说绘制的矩形框是蓝色的
rectangle(mat, cv::Point(100, 100),cv::Point(200, 200) ,cv::Scalar(255 ,0 ,0));  //绘制矩形

//设置ROI
cv::Mat subMat = mat(cv::Rect(0,0,200,200));
GaussianBlur(subMat, subMat, cv::Size(51,51), 30, 30);//高斯模糊

Android

Mat flipmat = new Mat();
Core.transpose(mat, flipmat);
Core.flip(flipmat, flipmat, 1);

//绘制矩形
Imgproc.rectangle(mat, new Point(100, 100), new Point(200, 200),new Scalar(255, 0, 0));

//设置ROI
Mat dstSubMat = mat.submat(rect); //获取子矩阵
Imgproc.GaussianBlur(dstSubMat, dstSubMat, new Size(51, 51), 30, 30);  //高斯模糊

缩放

对于TensorFlow,所有的输入一般都指定的有格式大小,比如300x300等,就需要对mat进行缩放。

iOS

    cv::Mat cloneMat = mat.clone();
    cv::Mat resizeMat;
    double fx = 300.0/mat.cols;
    double fy = 300.0/mat.rows;
    cv::resize( cloneMat, resizeMat, cv::Size(), fx, fy, cv::INTER_LINEAR_EXACT );

    uint8_t* in = resizeMat.data;//得到缩放后的mat的指针

Android

    Mat cloneMat = mat.clone();
    Mat resizeMat = new Mat();
    double fx = 300.0/mat.cols();
    double fy = 300.0/mat.rows();
    Imgproc.resize( cloneMat, resizeMat, new Size(), fx, fy, Imgproc.INTER_LINEAR_EXACT );

仿射变换(旋转)

在旋转这块,在后边的版本有rotate()方法可以用,其中第三个参数RotateFlags

enum RotateFlags {
    ROTATE_90_CLOCKWISE = 0, //!
    ROTATE_180 = 1, //!
    ROTATE_90_COUNTERCLOCKWISE = 2, //!
};

这里不多说了!很简单!

我们这里了解一下使用仿射变换使mat可以任意角度的旋转:

iOS

-(cv::Mat)rotateDegree:(double) degree srcMat:(cv::Mat) src{    
    double thera = degree * M_PI / 180.0;
    double a = sin(thera);
    double b = cos(thera);
    
    int wsrc = src.cols;
    int hsrc = src.rows;
    
    int wdst = (int) (hsrc * abs(a) + wsrc * abs(b));
    int hdst = (int) (wsrc * abs(a) + hsrc * abs(b));
    cv::Mat dst(hdst,wdst,src.type());
    
    //指定旋转中心
    cv::Point2f center(src.cols / 2., src.rows / 2.);
    
    //获取旋转矩阵(2x3矩阵)
    cv::Mat rot_mat = cv::getRotationMatrix2D(center, degree, 1.0);//正值意味着逆时针旋转。坐标原点被假定为左上角)。
    //cv::Rect bbox = cv::RotatedRect(center, src.size(), degree).boundingRect();
    
    rot_mat.at<double>(0, 2) += (wdst - wsrc) / 2.0;
    rot_mat.at<double>(1, 2) += (hdst - hsrc) / 2.0;
    
    //根据旋转矩阵进行仿射变换
    cv::warpAffine(src, dst, rot_mat, dst.size());
    return dst;
}

其中cv::warpAffine(src, dst, rot_mat, dst.size());dst.size()可以使用注释掉那段代码bbox代替!
这里就是说一个矩阵旋转后,所占的空间大小会发生变化,如果还按原mat的size旋转,就很有可能发生裁剪,造成一部分像素的丢失。而cv::RotatedRectboundingRect()方法从字面就很容易理解,得到一个矩形rect的边缘所在的矩形

Android

public static Mat rotateMat(Mat splitImage, double angle) {
        double thera = angle * Math.PI / 180;
        double a = Math.sin(thera);
        double b = Math.cos(thera);

        int wsrc = splitImage.width();
        int hsrc = splitImage.height();

        int wdst = (int) (hsrc * Math.abs(a) + wsrc * Math.abs(b));
        int hdst = (int) (wsrc * Math.abs(a) + hsrc * Math.abs(b));
        Mat imgDst = new Mat(hdst, wdst, splitImage.type());

        Point pt = new Point(splitImage.cols() / 2.0, splitImage.rows() / 2.0);
        // 获取仿射变换矩阵
        Mat affineTrans = Imgproc.getRotationMatrix2D(pt, angle, 1.0);
        // 改变变换矩阵第三列的值
        affineTrans.put(0, 2, affineTrans.get(0, 2)[0] + (wdst - wsrc) / 2.0);
        affineTrans.put(1, 2, affineTrans.get(1, 2)[0] + (hdst - hsrc) / 2.0);

        warpAffine(splitImage, imgDst, affineTrans, imgDst.size(),
                Imgproc.INTER_CUBIC | Imgproc.WARP_FILL_OUTLIERS);
        return imgDst;
    }

好了,关于仿射变换更多的内容,可以参考:
opencv学习(三十五)之仿射变换warpAffine

结语

以上就是关于移动端OpenCV的集成和视频的mat转换,以及常用的关于的mat的相关操作的总结!
撒花完结!

你可能感兴趣的:(Android学习笔记,iOS学习笔记,图像处理,opencv,android,ios,视频回调,mat)