zl程序教程

您现在的位置是:首页 >  APP

当前栏目

Android + OpenCV 入门教程笔记(保姆级)

2023-04-18 14:28:37 时间

笔记基于Android+openCV培训进行记录
源码:github
记录不易,喜欢的可以给个三连,感谢感谢!!!


OpenCV概述

什么是OpenCV

OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉机器学习软件库,可以运行在LinuxWindowsAndroid和Mac OS操作系统上。 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。

OpenCV用C++语言编写,它具有C ++,PythonJavaMATLAB接口,并支持Windows,Linux,Android和Mac OS,OpenCV主要倾向于实时视觉应用,并在可用时利用MMX和SSE指令, 如今也提供对于C#、Ch、Ruby,GO的支持

OpenCV环境配置

  1. 下载OpenCV Android SDK(下面以3.4.6版本演示)3.4.6官方下载

  2. 目录结构
    在这里插入图片描述

    目录文件
    samplesOpenCV运行案例
    sdkOpenCV API以及依赖库
    sdk/etcHaar和LBP级联分类器
    sdk/javaOpenCV Java Api
    sdk/libcxx_helperbring libc++_shared.so into packages
    sdk/nativeOpenCV静态库、动态库以及JNI文件
  3. Android Studio配置

    需要配置NDK环境和CMake
    在这里插入图片描述

  4. 导入OpenCV前须知
    有两种方式

    1. 把sdk/java目录导入项目,但是设备上要安装OpenCV Manager这个第三方软件,这种方式不适合开发通用App
    2. 把整个sdk目录导入项目,这种方式无需安装第三方软件,一般都是以这种方式进行开发,缺点是这样打包的Apk会比较大,但无伤大雅。
  5. 新建项目Android 空白项目
    在这里插入图片描述

    在这里插入图片描述

  6. 导入OpenCV SDK

    1. 创建空白项目后,点击 File-New-import Module,然后选择openCV sdk这个目录
      在这里插入图片描述

      在这里插入图片描述

  7. 导入SDK后,需要将opencv的build.gradle里面的版本号与主项目版本号统一
    需要将下面三个版本号与主项目统一

    1. compileSdkVersion
    2. minSdkVersion
    3. targetSdkVersion
  8. 在app中的build.gradle导入opencv的依赖

    dependencies {
        	......
            implementation project(path: ':opencv')
    }
    
    
  9. 需要在project structure 中配置ndk版本
    在这里插入图片描述

  10. 通过代码查看opencv模块是否加载成功

    public void initLoadOpenCV() {
        boolean success = OpenCVLoader.initDebug();
        if (success) {
            Log.d("init", "initLoadOpenCV: openCV load success");
        } else {
            Log.e("init", "initLoadOpenCV: openCV load failed");
        }
    }
    

    至此,Android OpenCV已近配置完成

OpenCV 简单案例

OpenCV Java Api会经常用到这几个类:

  • Mat类,主要用来定义Mat对象,切割Mat对象。常规的Bitmap位图在OpenCV中都要转换为Mat
  • Core类,主要用于Mat的运算,提供了很多运算功能的静态函数
  • ImgProc类,主要用于图像处理,也提供了很多处理功能的静态函数。
  • Utils类,主要用于Mat和Bitmap之间的转换

OpenCV java API参考手册

中文

官方文档

简单理解

在这里插入图片描述

Mat

Mat就是一个图像的矩阵。Mat是由头部与数据部分组成,其中头部还包含了一个指向数据的指针

Bitmap和Mat的转换

bitmap转mat

通过org.opencv.android.Utils来实现相互转换

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.bg_color);
Mat mat = new Mat();
Utils.bitmapToMat(bitmap, mat);

mat转mat

Bitmap newBitmap = Bitmap.createBitmap(mat.width(),mat.height(), 		Bitmap.Config.ARGB_8888);
Utils.matToBitmap(mat,newBitmap);

也可以直接读取源文件获得mat

try {
    Mat mat = Utils.loadResource(this,R.mipmap.bg_color);
} catch (IOException e) {
    e.printStackTrace();
}

Mat的位运算和算数运算

Mat格式的图像可以直接进行位运算和算数运算。位运算主要支持按位非、按位与、或、异或。算数支持加减乘除。Api如下,下面方法都是Core类的。

方法操作
bitwise_not(Mat src,Mat dst)
bitwise_and(Mat src1,Mat src2,Mat dst)
bitwise_or(Mat src1,Mat src2,Mat dst)
bitwise_xor(Mat src1,Mat src2,Mat dst)异或
add(Mat src1,Mat src2,Mat dst)矩阵加法
subtract(Mat src1,Mat src2,Mat dst)矩阵减法
multiplf(Mat src1,Mat src2,Mat dst)矩阵乘法
divide(Mat src1,Mat src2,Mat dst)矩阵除法

Mat的释放release

定义的Mat一般都要在程序结束前release()释放掉。

protected void onDestroy(){
	super.onDestory();
	mat.release();
}

例子演示

要注意的是两张源图片像素大小需要是一致的,因为是点对点运算的

使用Utils.loadResource读取源图片

新建一个dst的Mat作为目标Mat

处理完图片后,转换为Bitmap进行UI显示

最后释放掉mat对象

public void xorMat() {
		Mat mat1 = null;
		Mat mat2 = null;
		try {
			mat1 = Utils.loadResource(this, R.mipmap.bg_color);
			mat2 = Utils.loadResource(this, R.mipmap.bg_1);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		Mat dst = new Mat();
    	//运算方法
		Core.bitwise_and(mat1, mat2, dst);
		//转换回Bitmap
		Bitmap newBitmap = Bitmap.createBitmap(dst.width(), dst.height(), Bitmap.Config.ARGB_8888);
		Utils.matToBitmap(dst, newBitmap);
		imageView.setImageBitmap(newBitmap);
		mat1.release();
		mat2.release();
		dst.release();
	}

颜色转换

图像色彩模式

**位图模式(二值图):**是图像中最基本的格式,图像只有黑色和白色像素,是色彩模式中占有空间最小的,同样也叫做黑白图,它包含的信息量最少,无法包含图像中的细节,相当于只有0或者1,所以也叫二值图。一副彩色图如果要转換成黑白模式,则一般不能直接转換,需要首先将图像转換成灰度模式

**灰度模式:**即使用单一色调来表示图像,与位图模式不同,不像位图只有0和1,使用256级的灰度来表示图像,一个像素相当于占用8为一个字节,每个像素值使用0到255的亮度值代表,其中0为黑色, 255为白色,相当于从黑->灰->白的过度,通常我们所说的黑白照片就是这种模式,与位图模式相比,能表现出一定的细节,占用空间也比位图模式较大。

**RGB模式:**为我们经常见到的,被称为真色彩。RGB模式的图像有3个颜色通道,分布为红(Red) ,绿(Green)和蓝(Bule) ,每个都占用8位一个字节来表示颜色信息,这样每个颜色的取值范围为0-255,那么就三种颜色就可以有多种组合,当三种基色的值相等时表现出为灰色,三种颜色都为255即为白色,三种颜色都为0,即为黑色.RGB模式的图像占用空间要比位图,灰度图都要大,但表现出的细节更加明显。

**HSV模式:**是根据日常生活中人眼的视觉对色彩的观察得而制定的一套色彩模式,最接近与人类对色彩的辨认的思考方式,所有的颜色都是用色彩三属性来描述

  • H(色相):是指从物体反射或透过物体传播的颜色
  • S(饱和度):是指颜色的强度或纯度,表示色相中灰色成分所占的比例
  • V(亮度):是指颜色相对明暗程度,通常100%定义为白色;0%定位为黑色

cvtColor()颜色转换函数

OpenCV主要使用cvtColor()进行颜色的转换操作

ImgProc.cvtColor(Mat src,Mat dest,Imgproc.COLOR_CODE)

COLOR_CORE提供了丰富的颜色转换模式

颜色码功能
Imgproc.COLOR_BGR2RGB颜色空间转换
Imgproc.COLOR_BGR2GRAYBGR转换到灰度空间
Imgproc.COLOR_GRAY2RGB灰度转换到RGB
Imgproc.COLOR_RGB2HSVRGB转换到HSV
Imgproc.COLOR_RGB2RGBA添加alpha通道

灰度图例子

public void cvtColor() {
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.bg_1);
    Mat src = new Mat();
    Mat dst = new Mat();
    Utils.bitmapToMat(bitmap,src);
    //转换为灰度图模式
    Imgproc.cvtColor(src,dst,Imgproc.COLOR_RGB2GRAY);
    //把mat转换回bitmap
    Utils.matToBitmap(dst,bitmap);
    imageView.setImageBitmap(bitmap);
    src.release();
    dst.release();
}

二值图例子

也就是位图模式,相当于只有0(即黑色)或者255(即白色),所以也叫二值图。转换二值图关键是确定一个阈值,如果像素点高于阈值,都设置为255,低于则设置为0。就可以呈现出明显黑白效果。

阈值确定有两种方法,一是手动设置,二是自动阈值

手动设置阈值方法

Imgproc.threshold(Mat src,Mat dst,double thresh,double maxval,int type)

src Mat源文件**,仅支持灰度图输入**

dst 输出文件

thresh double第一阈值

maxval double第二阈值

type 阈值类型。

​ THRESH_BINARY:当像素点灰度值 大于 thresh,像素点值为maxval,反之0

​ THRESH_BINARY_INV:当像素点灰度值 大于 thresh,像素点值为0,反之maxval

​ THRESH_TRUNC:当像素点灰度值 大于 thresh,像素点值为thresh,反之不变

​ THRESH_TOZERO:当像素点灰度值 大于 thresh,像素点值不变,反之为0

​ THRESH_TOZERO_INV:当像素点灰度值 大于 thresh,像素点值为0,反之不变

Imgproc.threshold(src,dst,125,255,Imgproc.THRESH_BINARY)

自动设置阈值

OpenCV中也可以使用算法来自动计算阈值。OpenCV支持均值算法和高斯均值算法。不是计算全局图像的阈值,而是根据图像不同区域亮度分布,计算其局部阈值,所以对于不同区域,能够自适应计算不同的阈值,所以被叫做自适应阈值法。

如果下面图片因为有暗角的原因,使用手动阈值设置的话会出现这种情况

在这里插入图片描述

而这种情况就应该使用自适应阈值法了,适用于文档扫描

在这里插入图片描述

自适应阈值方法

Imgporc.adaptiveThreshold(Mat src,Mat dst,double maxValue,int adaptiveMethod,int thresholdType,int blockSize,double c)
  • src – 灰度图输入
  • dst – 输出
  • maxValue – 分配给满足条件的像素的非零值
  • adaptiveMethod – 要使用的自适应阈值算法,ADAPTIVE_THRESH_MEAN_C(平均计算)或ADAPTIVE_THRESH_GAUSSIAN_C(高斯计算)。请参阅以下详细信息。
  • thresholdType – 必须THRESH_BINARYTHRESH_BINARY_INV的阈值类型。
  • blockSize – 用于计算像素阈值的像素邻域的大小:3、5、7 等。
  • C – 从平均值或加权平均值中减去的常量。通常,它是正数,但也可以是零或负数。

演示

Imgproc.adaptiveThreshold(src,dst,255,Imgproc.ADAPTIVE_THRESH_MEAN_C,Imgproc.THRESH_BINARY,13,5);

例子演示(根据手动设置阈值)

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.SeekBar;
import androidx.appcompat.app.AppCompatActivity;
import org.opencv.android.Utils;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
public class ThresholdActivity extends AppCompatActivity {
	private ImageView imageView;
	private SeekBar seekBar;
	private Bitmap bitmap;
	private Mat src;
	private Mat dst;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_threshold);
		imageView = findViewById(R.id.image);
		seekBar = findViewById(R.id.seekBar);

		bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.bg_1);
		src = new Mat();
		dst = new Mat();
		Utils.bitmapToMat(bitmap, src);
		//先把源文转为灰度图
		Imgproc.cvtColor(src, src, Imgproc.COLOR_RGB2GRAY);
		threshold(125);

		//滑杆监听
		seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
			@Override
			public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
				threshold(progress);
			}

			@Override
			public void onStartTrackingTouch(SeekBar seekBar) {
			}

			@Override
			public void onStopTrackingTouch(SeekBar seekBar) {
			}
		});
	}
	public void threshold(double threshold) {
		Imgproc.threshold(src, dst, threshold, 255, Imgproc.THRESH_BINARY);
		Utils.matToBitmap(dst, bitmap);
		imageView.setImageBitmap(bitmap);
	}
}

几何图形绘制

几何图形绘制主要使用Imgproc类里的line(直线)、rectangle(矩形)、polylines(多边形)、circle(圆形)、ellipse(椭圆形)等函数

直线绘制

line(Mat img,Point pt1,Point pt2,Scalar color,int thickness)

img 需要绘制的图像Mat

pt1 直线起点坐标

pt2 直线重点坐标

color 直线颜色

thickness 直线的宽度

演示

Imgproc.line(src,new Point(0,src.height),new Point(0,src.width),new Scalar(255,0,0),4)

矩形绘制

rectangle(Mat img,Point pt1,Point pt2,Scalar color,int thickness)

img 需要绘制的图像Mat

pt1 矩形左上角

pt2 矩形右下角

color 直线颜色

thickness 直线的宽度,如果为负值,表示填充。

多边形绘制

polyines(Mat img,List<MatOfPoint> pts,boolean isClosed,Scalar color,int thickness)

img 需要绘制的图像Mat

pts 多边形端点坐标列表

isClosed 是否闭合

color 直线颜色

thickness 直线宽度,如果是封闭的图像,负值也可以填充颜色

圆形绘制

circle(Mat img,Point center,int radius,Scalar color,int thickness)

img 需要绘制的图像Mat

center 圆心坐标

radius 圆半径

color 直线颜色

thickness 直线宽度,如果是封闭的图像,负值也可以填充颜色

文字绘制

putText(Mat img,String text,Point org,int fontFace,double fontScale,Scalar color,int thickness)

img 需要绘制的图像Mat

text 文字内容

org 文本字符串左下角位置

fontFace 字体类型,可取值

在这里插入图片描述

fontScale 字体大小

color 直线颜色

thickness 直线宽度,如果是封闭的图像,负值也可以填充颜色

演示

Imgproc.putText(src,"Hello world",new Point(src.width/2.src.height/3),2,5,new Scalar(0,255,0),3)

OpenCV对中文支持不太行,官方建议是将中文转换为图片再显示

图像切割

可以将原始图片部分识别区域切割出来,去除干扰的画面

切割图片可以自动切割和手动切割

在这里插入图片描述

自动切割

自动切割的原理是对整个图进行轮廓搜索,将搜索出来的结果按轮廓面积进行分析,将面积最合适的那个作为识别区与进行切割。这种方法的有点在于不用手动去确定切割点坐标,缺点是由于画面内容比较复杂,找到的轮廓可能会很多,分析轮廓的运算较大对Android设备带来较大的运算负担,造成处理速度慢。

ROI(region of interest)——感兴趣区域。

这个区域是图像分析所关注的重点。圈定这个区域,以便进行进一步的处理。而且,使用ROI指定想读入的目标,可以减少处理时间,增加精度,给图像处理带来不小的便利。

下面会讲到轮廓识别,运算轮廓识别可以实现自动切割。

手动切割

手动切割人工确定切割点。优点在于运算快,速度快。

手动切割步骤:

  1. 定义一个org.opencv.core.Rect类,分别输入x,y,width,height进行矩形的定位
    其中x,y表示矩形左上角的点坐标,width和height分别表示矩形的长宽

    Rect(int x,int y,int width,int height)
    
  2. 然后把创建好的rect放进mat的构建中

    Rect rect = new Rect(200,150,200,200);
    dstmat = new Mat(srcmat,rect);
    
  3. 切割后发现显示出来的图片不一致,是因为OpenCV默认会把图片颜色模式转为BGR,我们需要转回RGB模式,就是Mat接收到图片时,图片模式已近转为BGR了。

    //使用这行代码把BGR颜色转换为RGB模式
    Imgproc.cvtColor(dst,dst,Imgproc.COLOR_BGR2RGB);
    
    resultBitmap = Bitmap.createBitmap(dstmat.width(),dstmat.height(),Bitmap.Config.ARGB_8888);
    imgview.setImageBitmap(resultBitmap);
    

演示代码

public void cut() {
    Mat src = null;
    try {
        src = Utils.loadResource(ManualCutActivity.this,R.mipmap.bg_3);
    } catch (IOException e) {
        e.printStackTrace();
        return;
    }
    //手动指定切割区域
    Rect rect = new Rect(55,108,365,269);
    Mat dst = new Mat(src,rect);
    //转换会RGB颜色
    Imgproc.cvtColor(dst,dst,Imgproc.COLOR_BGR2RGB);

    Bitmap resultBitmap = Bitmap.createBitmap(dst.width(),dst.height(),Bitmap.Config.ARGB_8888);
    Utils.matToBitmap(dst,resultBitmap);
    imageView.setImageBitmap(resultBitmap);
}

颜色识别

数字图像处理中常用采用模型是RGB(红,绿,蓝)和HSV(色调,饱和度,亮度)。

HSV模型

H(色调):用角度度量,取值范围0-360,红色为0,绿色为120,蓝色240。

S(饱和度):取值范围0.0-1.0,0为低饱和度,1为高饱和度

V(亮度):取值范围0.0-1.0,0为黑色,1为白色

在OpenCV中,范围成了H(0,180),S和V(0,255)

H: 0— 180

S: 0— 255

V: 0— 255

常用颜色值

颜色最小值最大值
橘黄色022
黄色2238
绿色3875
蓝色75130
紫色130160
红色160179

使用cvtColor(Mat src,Mat dst,Imgproc.COlOR_RGB2HSV)进行颜色模式转换

颜色检测

Core.inRange(imgHSV,new Scalar(lowH,lowS,lowV),new Scalar(heighH.heighS,heighV),imgThresholded);

imgHSV HSV的Mat格式原图

Scalar lowHSV HSV范围下限

Scalar heigHSV HSV范围上线

imgThresholded 输出Mat格式图片,

检测imgHSV图像的每一个像素是不是在lowHSV和heigHSV之间,如果是就设置为255,否则为0,保存在imgThresholded中

演示

Core.inRange(src,new Scalar(0,255,144),new Scalar(0,255,255),dst);

当有时候我们在得到的图片上会看到一些白色的噪点,或者轮廓不连续有断开。这种情况我们需要执行下开运算闭运算

开运算

开运算的原理是通过先进行腐蚀操作,再进行膨胀操作得到。我们在移除小的对象时候很有用(假设物品是亮色,前景色是黑色),开运算可以去除噪声,消除小物体;在纤细点处分离物体;平滑较大物体的边界的同时并不明显改变其面积。比如在二值化图像没处理好的时候会有一些白色的噪点,可以通过开运算进行消除。

在这里插入图片描述

闭运算

闭运算是开运算的一个相反的操作,具体是先进行膨胀然后进行腐蚀操作。通常是被用来填充前景物体中的小洞,或者抹去前景物体上的小黑点。因为可以想象,其就是先将白色部分变大,把小的黑色部分挤掉,然后再将一些大的黑色的部分还原回来,整体得到的效果就是:抹去前景物体上的小黑点了

在这里插入图片描述

这样就可以得到一张二值比较好的图。

我们在优化图象时,可以先执行开运算消除背景上的白色噪点,在执行闭运算消除前景色上的黑色杂色。

在执行开运算和闭运算之前我们要确定一个运算核,这个运算核是一个小矩阵。腐蚀运算就是在整张图像上计算给定内核区域的局部最小值,用最小值替换对应的像素值,而膨胀运算就是在整张图像上计算给定内核区域的局部最大值,用最大值替换对应的像索值。

//这就是一个运算核,一个3x3的矩阵
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,new Size(3,3));
//进行开运算
Imgproc.morphologyEx(hsvmat,hsvmat,Imgproc.MORPH_OPEN,kernel);
//进行闭运算
Imgproc.morphologyEx(hsvmat,hsvmat,Imgproc.MORPH_CLOSE,kernel);
Utils.maToBitmap(hsvmat,resultBitmap);
imgView.setImageBitmap(resultBitmap);

轮廓识别

在OpenCV中,轮廓对应着一些列的点的集合

public static void findContours(Mat image,
                                List<MatOfPoint> contours,
                                Mat hierarchy,
                                int mode,
                                int method)
  • image 一张二值图(即位图)。如果使用的mode是RETR_CCOMP或者RETR_FLOODFILL,那么输入的图像类型也可以是32位单通道整型,即CV_32SC1

  • contours 检测到的轮廓。一个MatOfPoint保存一个轮廓,所有轮廓放在List中。

  • hierarchy 可选的输出。包含轮廓之间的联系。4通道矩阵,元素个数为轮廓数量。通道 [ 0 ] ~通道 [ 3 ]对应保存:后个轮廓下标,前一个轮廓下标,父轮廓下标,内嵌轮廓下标。如 果没有后一个,前一个,父轮廓,内嵌轮廓,那么该通道的值为-1.

mode 轮廓检索模式。

标识符 含义

RETR_EXTERNAL 只检测最外围的轮廓

RETR_LIST 检测所有轮廓,不建立等级关系,彼此独立

RETR_CCOMP 检测所有轮廓,但所有轮廓只建立两个等级关系

RETR_TREE 检测所有轮廓,并且所有轮廓建立一个树结构,层级完整

  • method 轮廓近似法

标识符 含义

CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点

CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需要4个点来保存轮廓信息

CV_CHAIN_APPROX_TC89_L1 使用Teh-Chin链近似算法

CV_CHAIN_APPROX_TC89_KCOS 使用Teh-Chin链近似算法

调用findContours()函数对二值图进行轮廓识别

//用来接收轮廓坐标信息
List<MatOfPoint> contours = new ArrayList();
Mat outmat = new Mat();
Imgproc.findContours(hsvmat,
                     contours,
                     outmat,
                     Imgproc.RETR_EXTERNAL,
                     Improc.CHAIN_APPROX_SIMPLE);
sout("轮廓数量:"+contours.size())

轮廓绘制

public static void drawContours(Mat src,
                               List<MatOfPoint> contours,
                               int contourIdx,
                               Scalar & color,
                               int thickness) 

src 目标图像

contours 所有轮廓信息,用findContours来得到轮廓信息

contoursIdx 指定绘制轮廓的下标,如果为负数,则绘制所有轮廓

color 绘制轮廓颜色

thickness 绘制轮廓的线宽度,如果为负数,则填充

Imgproc.drawContours(dstmat,contours,-1,new Scalar(255,0,0),4);
Utils.maToBitmap(dstmat,resultBitmap);
imgView.setImageBitmap(resultBitmap);

形状识别

先要使用形状拟合来过滤一些突出的顶点,再用根据顶点识别出形状

形状的拟合

轮廓点集合找到以后我们可以通过多边形拟合的方式来寻找由轮廓点所组成的多边形的顶点。 approxPolyDP()函数功能是把·个连续光滑曲线折线化,对图像轮廓点进行多边形拟合。简单来说就是该函数是用1一条具有较少顶点的曲线/多边形去逼近另一条具有较多顶点的曲线或多边形。 approxPolypDP()函数的原理如下:

  • 在曲线首尾两点A, B之间连接一条直线AB,该直线为曲线的弦;
  • 得到曲线上离该直线段距离最大的点C,计算其与AB的距离b;
  • 比较该距离与预先给定的阈值threshold的大小,如果小于threshold,则该直线段作为曲线的近似,该段曲线处理完毕。
  • 如果距离大于阈值,则用C将曲线分为两段AC和BC,并分别对两段取信进行1-3的处理。
  • 当所有曲线都处理完毕时,依次连接各个分割点形成的折线,即可以作为曲线的近似。

在这里插入图片描述

public static void approxPolyDP(MatOfpoint2f curve,
                               MatOfPoint2f approxCurve,
                               double epsilon,
                               boolean closed)

curve 输入的轮廓点集合

approxCurve 输出的轮廓点集合。最小包容指定点集,保存的是多边形的顶点

epsilon 拟合的精度,原始曲线和拟合曲线间的最大值

closed 是否为封闭曲线,true为封闭,反之

其中逼近精度epsilon可以手动指定,也可以通过curve轮廓点的个数进行计算

epsilon = a * Imgproc.arcLength(curve,true);

其中arcLength是计算轮廓点的个数,也就是周长。a 可按不同的图像测试取得最佳值

演示代码

//获取某一个形状的顶点的集合
contour2f = new MatOfPoint2f(contours.get(i).toArray());
epsilon = 0.04 * Imgproc.arcLength(contour2f,true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contour2f,approxCurve,epsilon,true);

approxCurve中存放着多边形顶点坐标的列表。列表的行值就是顶点的个数。

sout("顶点数量:"+approxCurve.rows());

形状的识别

顶点判断法

形状识别的方法有很多种,本案例采用最简单的一种就是直接根据多边形顶点的个数进行判断。这种方法最简单但**精度不高,**只能识别差别较大的几种形状。

这种方式局限性太大

if(approxCurve.rows()==3){
    sout("三角形");
}
if(approxCurve.rows()==4){
    sout("矩形");
}
if(approxCurve.rows()>20){
    sout("圆形");
}

信号分析法

使用Moments ()函数计算多边形的重点,求绕多边形一周重心到多边形轮廓线的距离。把距离值形成信号曲线图,我们可以看到不同的形状信号曲线图区别很大。信号分析法可以识别多种类型的多边形形状。

在这里插入图片描述

通过不同形状的信号图来确定不同的形状种类。

实际开发中,在进行形状识别时,很可能会出现一些很小的瑕疵,经过开运算和闭运算也不能消除的杂质,我们可以通过计算面积来进行过滤,小于多少的面积直接过滤掉,这也是一种好办法。通过Imgproc.contourArea(Mat mat)来进行多边形面积计算。

for(int i =0;i<contours.size();i++){
    //过滤了面积小于10的形状
    if(Imgproc.contourArea(contours.get(i))>10){
        contour2f = new MatOfPoint2f(contours.get(i).toArray());
        epsilon = 0.04 * Imgproc.arcLength(contour2f,true);
        approxCurve = new MatOfPoint2f();
        Imgproc.approxPolyDP(contour2f,approxCurve,epsilon,true);
        sout("顶点:"+approxCurve.rows());
        if(approxCurve.rows()==3){
            sout("三角形");
        }
        if(approxCurve.rows()==4){
            sout("矩形");
        }
        if(approxCurve.rows()>20){
            sout("圆形");
        }
    }
}

Mat高级用法

图片滤镜

通过对RGB三个颜色分量的调整可以将照片处理成一种老照片的怀旧风格

调整公式:

​ R=0.393 * r + 0.769 * g + 0.189 * b

​ G=0.349 * r + 0.686 * g + 0.168 * b

​ B=0.272 * r + 0.534 * g + 0.131 * b

要完成对颜色分量的调整,可利用Mat的各种像素操作。

通过不同的公式可以达到不同滤镜的效果。

图片基本信息

Mat对象中除了存储图像的像素数据外,还包括了图像的其他属性。

  • int channels() 返回通道数
  • int cols() 返回矩阵列数(宽度)
  • int rows() 返回矩阵的函数(高度)
  • int dims() 返回矩阵的维度
  • int type() 返回矩阵的类型

什么是通道数?

通常表示每个点能存放多少个数,如RGB彩色图中的每个像素点有三个值,即三通道。常见的通道数有1、3、4,分别对应单通道、三通道、四通道,其中四通道中会有透明通道数据。

单通道——灰度图

三通道——RGB彩色图

四通道——到Alpha通道的RGB图

图片的深度表示每个值有多少位来存储,是一个精度问题,一般图片是8Bit(位),则深度是8。

什么是类型

图片深度与类型密切相关。U表示无符号整数、S表示符号整数、F表示浮点数。

OpenCV中使用bitmaptToMat()加载的图片,图片类型为CV_8UC4,通道顺序为BGRA。其中CV表示计算机视觉、8表示深度、U表示无符号整数、4表示通道数。这就解释了为什么Mat加载图片后颜色空间默认为BGR了!!

下面是类型

在这里插入图片描述

下面是图片深度对应JAVA的数据类型,像素操作的时候要用到

在这里插入图片描述

像素点操作,读取,修改,写入

CV_8UC4的Mat类型来说,对应的数据类型是byte;则先定义初始化byte数组p,用来存储每次读取出来的一个像素点所有通道值,数组的长度取决于图像通道数目。

从矩阵中读取像素数据采用get方法,写入像素数据采用put方法。put拥有相同的方方法

在这里插入图片描述

row , col 表示xy像素点位置
data表示该像素点的数据
根据图片深度来使用对应的数据类型,上面的知识点。

转换怀旧图片例子

处理图片应放在子线程处理,防止处理时间过长。

处理流程:

  1. 加载图片
  2. 读取基本信息
  3. 读取每个像素点
  4. 修改像素低数据
  5. 写入
/**
	 * 转换怀旧图片处理
	 * @param srcBitmap 源bitmap
	 * @return  处理后bitmap
	 */
	private Bitmap modify(Bitmap srcBitmap) {
		Mat mat = new Mat();
		Utils.bitmapToMat(srcBitmap, mat);

		//通道数
		int channels = mat.channels();
		//宽度
		int col = mat.cols();
		//高度 等同于srcMat.height();
		int row = mat.rows();
		//类型
		int type = mat.type();
		Log.d("TAG", "通道数: " + channels + " 宽度:" + col + " 高度:" + row + " 类型:" + type);


		//定义一个数组,用来存储每个像素点数据,数组长度对应图片的通道数
		//至于为什么用byte,那就要看文档的对照表,不同图片类型对应不同java数据类型,这里CV_8UC4,对应Java byte类型
		byte[] p = new byte[channels];

		int r = 0, g = 0, b = 0;

		//循环遍历每个像素点,对每个像素点进行操作
		for (int h = 0; h < row; h++) {
			for (int w = 0; w <= col; w++) {
				//通过像素点位置得到该像素带点的数据,并存入p数组中
				mat.get(h, w, p);

				//得到一个像素点的RGB值
				//这里为啥要 & 0xff ,下节有讲。
				r = p[0] & 0xff;
				g = p[1] & 0xff;
				b = p[2] & 0xff;


				//根据怀旧图片滤镜公式进行计算
				int AR = (int) (0.393 * r + 0.769 * g + 0.189 * b);
				int AG = (int) (0.349 * r + 0.686 * g + 0.168 * b);
				int AB = (int) (0.272 * r + 0.534 * g + 0.131 * b);

				//防越界判断,byte最大数值是255。
				AR = (AR > 255 ? 255 : (AR < 0 ? 0 : AR));
				AG = (AG > 255 ? 255 : (AG < 0 ? 0 : AG));
				AB = (AB > 255 ? 255 : (AB < 0 ? 0 : AB));

				//把修改后的数据重新写入数组
				p[0] = (byte) AR;
				p[1] = (byte) AG;
				p[2] = (byte) AB;

				//把数组写入像素点
				mat.put(h, w, p);
			}
		}

		Bitmap dstBitmap = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888);
		Utils.matToBitmap(mat, dstBitmap);
		mat.release();
		return dstBitmap;
	}

连续读取像素修改

连续读取就是读取一行像素进行操作,这样的优点是运算比上面的单个读取快

  • 上面的方法因为频繁访问JNI调用而效率嗲下,但是内存(局部变量数组p的长度)需求小;
  • 下面的方法每次读取一行,相比第一种方法速度有所提升,但是内存也根据图片像素而增加
  • 还有一种方法是一次性读取全部像素,在内存中修改像素速度最快,效率最高,但是内存消耗是最多的,用以内存溢出

演示

private Bitmap modifyV2(Bitmap srcBitmap) {
		Mat mat = new Mat();
		Utils.bitmapToMat(srcBitmap, mat);

		int channels = mat.channels();
		int col = mat.cols();
		int row = mat.rows();
		int type = mat.type();
		Log.d("TAG", "通道数: " + channels + " 宽度:" + col + " 高度:" + row + " 类型:" + type);


		//用于保存一行像素的数据,单个像素点的数据*一行的像素
		byte[] p = new byte[channels * col];

		int r = 0, g = 0, b = 0;

		for (int h = 0; h < row; h++) {
			//col = 0 表示这是一行的数据,把一行的像素点读取到p数组来
			mat.get(h, 0, p);
			for (int w = 0; w < col; w++) {
                //当前操作像素数组索引,通道数x当前像素位置 = 当前索引位置
				int index = channels * w;

				r = p[index] & 0xff;
				g = p[index + 1] & 0xff;
				b = p[index + 2] & 0xff;

				int AR = (int) (0.393 * r + 0.769 * g + 0.189 * b);
				int AG = (int) (0.349 * r + 0.686 * g + 0.168 * b);
				int AB = (int) (0.272 * r + 0.534 * g + 0.131 * b);
				AR = (AR > 255 ? 255 : (AR < 0 ? 0 : AR));
				AG = (AG > 255 ? 255 : (AG < 0 ? 0 : AG));
				AB = (AB > 255 ? 255 : (AB < 0 ? 0 : AB));
				p[index] = (byte) AR;
				p[index + 1] = (byte) AG;
				p[index + 2] = (byte) AB;

			}

			//同上,0表示是一行的数据,不再通过某个像素点写入
			mat.put(h, 0, p);
		}

		Bitmap dstBitmap = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888);
		Utils.matToBitmap(mat, dstBitmap);
		mat.release();
		return dstBitmap;
	}

为什么要使用 & 0xff

这是关于补码的事情。byte类型的数字要&0xff再赋值给int类型,byte是一字节,而int类型是4字节,如果不做补码操作,就会导致二进制数据的一致性丢失掉,这个问题的产生的原因和计算机存储数据的方式有关,负数,会取反然后+1存储

人脸检测案例

opencv中的人脸检测是基于训练好的LBP与HAAR的特征级联分类检测器完成的

LBP特征:Local Binary Pattern 局部二值模式。LBP的引用中,如纹理分类、人脸分析等,一般采用LBP特征谱的统计直方图作为特征向量用于分类识别。

HAAR特征:一种反映图像的灰度变化的,像素分模块求差值的一种特征。

级联分类器

基于LBP特征的分类器成为LBP级联分类器,基于HAAR特征的分类器成为HAAR级联分类器。

人脸检测模型

级联分类器是根据训练好的模型进行检测的。
opencv sdk中自带了许多训练好的模型。
sdk/etc/haarcascadessdk/etc/lbpcascades两个目录下的xml文件,
分别是HAAR特征和LBP特征的训练模型

把对应的模型放在Android工程下res/raw目录下进行使用

初始化级联分类器

把训练好的模型放在res/raw目录下,然后通过代码读取到资源文件,进行初始化

public void initClassifier() {
    try {
        //读取存放在raw的文件
        InputStream is = getResources()
            .openRawResource(R.raw.lbpcascade_frontalface_improved);
        File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
        File cascadeFile = new File(cascadeDir,"lbpcascade_frontalface_improved.xml");
        FileOutputStream os = new FileOutputStream(cascadeFile);
        byte[] buffer = new byte[4096];
        int bytesRead;
        while((bytesRead = is.read(buffer))!=-1){
            os.write(buffer,0,bytesRead);
        }
        is.close();
        os.close();
        //通过classifier来操作人脸检测, 在外部定义一个CascadeClassifier classifier,做全局变量使用
        classifier = new CascadeClassifier(cascadeFile.getAbsolutePath());
        cascadeFile.delete();
        cascadeDir.delete();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

图片人脸检测

初始化之后,就可以通过classifier.detectMultiScale()方法进行人脸检测

detectMultiScale(Mat img,
                 MatOfRect objects,
                 double scaleFactor,
                 int minNeighborsm,
                 int flags,
                 Size minSize,
                 Size maxSize)

参数:

  • img:输入的图像,需要灰度图
  • objects:表示检测到的对象个数,返回每个对象的矩形BOX坐标
  • scaleFactor:尺度变化的比率,基本在1.05~1.2之间比较好
  • minNeighbors:领域范围内符合条件的对象个数,它是输出检测最终BOX的重要阈值,太大,则条件比较苛刻,用以丢失检测对象,太小,则容易导致错误检测。
  • flags:设置0即可
  • minSize:对象检测的最小范围
  • maxSize:对象检测的最大范围

示例

classifier.detectMultiScale(natGray,faces,1.05,3,0,new Size(30,30),new Size())

例子

public Bitmap face(Bitmap bitmap) {
    Mat mat = new Mat();
    Mat matdst = new Mat();
    Utils.bitmapToMat(bitmap, mat);
    //把当前数据复制一份给matdst
    mat.copyTo(matdst);

    //1.把图片转为灰度图 BGR2GRAY,注意是BGR
    Imgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2GRAY);

    //2.定义MatOfRect用于接收人脸位置
    MatOfRect faces = new MatOfRect();

    //3.开始人脸检测,把检测到的人脸数据存放在faces中
    classifier.detectMultiScale(mat, faces, 1.05, 3, 0, new Size(30, 30), new Size());
    List<Rect> faceList = faces.toList();

    //4.判断是否存在人脸
    if (faceList.size() > 0) {
        for (Rect rect : faceList) {
            //5.根据得到的人脸位置绘制矩形框
            //rect.tl() 左上角
            //rect.br() 右下角
            Imgproc.rectangle(matdst, rect.tl(), rect.br(), new Scalar(255, 0, 0,255), 4);
        }
    }

    Bitmap resultBitmap = Bitmap.createBitmap(matdst.width(), matdst.height(), Bitmap.Config.ARGB_8888);
    Utils.matToBitmap(matdst, resultBitmap);
    mat.release();
    matdst.release();
    return resultBitmap;
}

实时人脸检测

OpenCV把原来c++的部分和本地的Android SDK进行了整合,通过桥接的方式调用Android摄像头。JavaCameraView类是OpenCV中调用Android手机设想头的接口类,支持代码方式和XML配置,该类可以在Android设备中使用摄像头完成拍照和预览。

JavaCameraView

在布局文件中添加JavaCameraView,作为实时预览的控件

<org.opencv.android.JavaCameraView
        android:id="@+id/camera"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

setCameraIndex() 设置摄像头

disableView() 关闭预览

enableView() 启动预览

JavaCameraView默认都是LANDSCAPE横屏显示模式,当改成PORTRAIT竖屏显示时,就会发现预览发生逆时针方向90度的旋转,要正确显示图像,需要对预览帧进行实时处理,进行翻转,这样会导致帧率有所降低。

所以直接把该页面横屏显示。
在加载布局前执行。

private void initWindowSettings() {
    //隐藏ActionBar
    getSupportActionBar().hide();
    //全屏显示
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
    //屏幕常量
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    //强制横屏显示,注意这里会重启活动,导致执行两次onCreate(),
    //可以在Manifest.xml中配置android:screenOrientation="landscape"
    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}

Android权限配置

需要android.permission.CAMERA权限

<uses-permission android:name="android.permission.CAMERA" />
private void initPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1234);
    }
}

人脸检测

通过实现CameraBridgeViewBase.CvCameraViewListener2接口,获取视频的帧数据

在onCameraFrame中做人脸检测处理

对视频每一帧进行处理,这样运算会很大,从而导致卡顿

/*
			实时接收摄像头数据
			然后调用classifier人脸检测
			对视频每一帧进行处理
		 */
@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
    mRgba = inputFrame.rgba();
    float mRelativeFaceSize = 0.2f;
    if (mAbsoluteFaceSize == 0) {
        int height = mRgba.rows();
        if (Math.round(height * mRelativeFaceSize) > 0) {
            mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
        }
    }
    MatOfRect faces = new MatOfRect();
    if (classifier != null) {
        classifier.detectMultiScale(mRgba, faces, 1.05, 2, 2,
                                    new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
    }
    Rect[] facesArray = faces.toArray();
    for (int i = 0; i < facesArray.length; i++) {
        Imgproc.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), faceRectColor, 2);
    }

    return mRgba;
}

对于运算大,优化方法也是有的

就是隔帧处理,具体隔多少帧由你决定。
对于视频每一帧都要处理,对于高像素拍摄的时候显得非常吃力。

不过缺点也是有的,就是矩形框绘制没有那么流畅

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
    mRgba = inputFrame.rgba();
    //隔3帧进行一次人脸检测
    if (fps == 3) {
        float mRelativeFaceSize = 0.2f;
        if (mAbsoluteFaceSize == 0) {
            int height = mRgba.rows();
            if (Math.round(height * mRelativeFaceSize) > 0) {
                mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
            }
        }
        MatOfRect faces = new MatOfRect();
        if (classifier != null) {
            classifier.detectMultiScale(mRgba, faces, 1.05, 2, 2,
                                        new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
        }
        //把检测到的人脸坐标存在全局变量,从而实现连续稳定的跟踪
        facesCache = faces.toList();
        //标识归0
        fps = 0;
    }

    //使用缓存的人脸坐标信息进行绘制
    for (Rect rect : facesCache) {
        Imgproc.rectangle(mRgba, rect.tl(), rect.br(), faceRectColor, 4);
    }
    //标识进1
    fps++;
    return mRgba;
}

眼睛检测

需要加载训练好的模型,然后加载检测器

在这里插入图片描述

/**
	 * 眼睛
	 * 初始化级联分类器
	 */
public void initClassifierEye() {
    try {
        //读取存放在raw的文件
        InputStream is = getResources()
            .openRawResource(R.raw.haarcascade_eye_tree_eyeglasses);
        File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
        File cascadeFile = new File(cascadeDir, "haarcascade_eye_tree_eyeglasses.xml");
        FileOutputStream os = new FileOutputStream(cascadeFile);
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        is.close();
        os.close();
        classifierEye = new CascadeClassifier(cascadeFile.getAbsolutePath());
        cascadeFile.delete();
        cascadeDir.delete();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

检测流程

在这里插入图片描述

先要检测出人脸位置,根据人脸位置取上半部分作为识别区域,这样符合人体构造,同时运算也会小很多


笔记基于Android+openCV培训进行记录
源码:github
记录不易,喜欢的可以给个三连,感谢感谢!!!