前言
写了两篇基础的,有点水,但会继续写下去,毕竟基础得扎实不是么,在继续水之前先来一篇干货,基于OpenCV 的形状识别。
正文
先上图
这张图的的背景色是白色,在上面有着颜色不一的图形,拿红色的矩形举例,但现在我们先假装不知道它是个矩形,只知道他是红色的一个图形,要想识别形状先要找到图形,欧克现在的问题是如何在这个白色为背景的图片中找到这个红色的图形呢?
既然我们已经说了是个红色的图形那么我们就找红色不就行了嘛?
理论存在,实践开始
注意:由于这一片是案例篇,将不会对方法进行具体的介绍,不过不用担心,在后续的基础篇中会陆续介绍的!
- 按照惯例先从导入图片开始
try {
srcMat = Utils.loadResource(this, R.drawable.shape);
} catch (IOException e) {
e.printStackTrace();
}
颜色空间
接下来我们要对图片做一些处理了,先简单引入一下颜色空间的概念,后面会单独出一篇来详细说明这里只简单概括:
RGB
RGB:是我们最常听到也是最常用的颜色空间,以R(Red:红)、G(Green:绿)、B(Blue:蓝)三种基本色为基础,进行不同程度的叠加,产生丰富而广泛的颜色,所以俗称三基色模式。RGB颜色空间最大的优点就是直观,容易理解。缺点是R,G,B这3个分量是高度相关的,即如果一个颜色的某一个分量发生了一定程度的改变,那么这个颜色很可能要发生改变;人眼对于常见的红绿蓝三色的敏感程度是不一样的,因此RGB颜色空间的均匀性非常差,且两种颜色之间的知觉差异色差不能表示为该颜色空间中两点间的距离。(ps:拆抄自百度)
由此看出RGB并不适合用来表述一个广义上的颜色范围,由此我们就要引出我们的HSV颜色空间了。
HSV
HSV:HSV(Hue, Saturation, Value)是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。
H(色调): 用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,紫色为300°;
S(饱和度): 饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。
V(明度): 明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。
由此看见我们很容易就能用HSV中的H表示一个颜色,下面是一个在网上找到的一个HSV颜色分量范围图:
在代码中我们也要使用HSV颜色空间
顺便提一下,Android中的Bitmap颜色空间并不是RGB,而是BGR这里需要注意
- 我们通过
cvtColor(),将源图srcMat的BGR颜色空间转化成为HSV颜色空间存入hsvMat中
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
inRange(),会将指定颜色阈值内的区域变为白色,其余部分变为黑色的二值化图片,例如下面代码就是将hsvMat中红色部分变为白色,其余部分变为黑色并存到binaryMat中。
Core.inRange(hsvMat, new Scalar(156, 43, 46), new Scalar(180, 255, 255), binaryMat);
ok先到这里,我们将上面这些写到一个按钮然并输出看看是什么结果
果不其然找到了我们要找的红色区域
findContours():顾名思义该方法可以帮助我们找到轮廓,我们需要将二值化的图binaryMat传入,方法会将轮廓存入到一个MatofPoint列表contours中,RETR_EXTERNAL表示监测所有轮廓,CV_CHAIN_APPROX_SIMPLE表示仅保存轮廓的拐点信息,此外还有一个保存轮廓层级的hierarchy:
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
- 有了轮廓之后,为了方便我们先将轮廓给画出来,这里的
resultMat其实就是srcMat,为了区分用了Mat resultMat = srcMat.clone();,下面代码的意思是,把contours轮廓列表中的所有轮廓(-1表示所有轮廓)在,resultMat上画用黑色(new Scalar(0, 0, 0))粗细为10的线条画出来。
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
效果下图:
- 找到轮廓后我们需要利用
approxPolyDP()对轮廓进行拟合:
什么是拟合?
这一步的操作就是将一个圆滑的曲线变成多边形,通过调整阈值可以控制拟合的程度,简单来说域值越大,边数越少;当然不是越少越好,这里我们用了一个公式,具体原理会在后续的基础篇中介绍,现在你只需要知道epsilon就是我们的阈值,我们可以通过改变后面的0.04来调整;
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
contours.get(1)由于我们这里只有一个轮廓所以直接输入0即可,通过这段操作,拟合后的多边形的顶点的集合就被我们保存到approxCurve中了,approxCurve事实上是一个二维数组,那么这个数组的“行数”就等于多边形的顶点数
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
- 得到顶点数量之后我们就可以直接通过顶点数量来判断形状了,三角形就是三个顶点,矩形就是四个顶点,五边形就是五个,星形就是10个,圆形就是大于5且不等于十;于是乎就有了以下代码:
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
让我们来看一下效果如何:
欧克到这里形状的识别就大功告成了,其他颜色的形状以此类推。这时候可能你会说能不能一次性识别所有的形状呢?当然是可以的,一开始我们识别的是红色的形状,要想识别所有的形状只需要将红色改为白色,然后将二值图反色即可,如下:
完整代码
MainActivity.java
public class MainActivity extends AppCompatActivity {
private String TAG = "MainActivity";
private Button redBtn, blueBtn, greenBtn, yellowBtn, cyanBtn, allBtn;
private ImageView iv_dst;
private Bitmap resultBitmap;
private Mat srcMat, hsvMat;
private List<MatOfPoint> contours;
private MatOfPoint2f contours2f, approxCurve;
private int contoursSize;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
OpenCVInit();
redBtn = findViewById(R.id.btn_red);
blueBtn = findViewById(R.id.btn_blue);
greenBtn = findViewById(R.id.btn_green);
yellowBtn = findViewById(R.id.btn_yellow);
cyanBtn = findViewById(R.id.btn_cyan);
allBtn = findViewById(R.id.btn_all);
iv_dst = findViewById(R.id.iv_dst);
try {
srcMat = Utils.loadResource(this, R.drawable.shape);
} catch (IOException e) {
e.printStackTrace();
}
redBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
Mat hierarchy = new Mat();
Mat binaryMat = new Mat();
Mat resultMat = srcMat.clone();
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(156, 43, 46), new Scalar(180, 255, 255), binaryMat);
resultBitmap = Bitmap.createBitmap(hsvMat.width(), hsvMat.height(), Bitmap.Config.ARGB_8888);
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
double epsilon;
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
Core.inRange(hsvMat, new Scalar(100, 43, 46), new Scalar(124, 255, 255), hsvMat);
}
});
greenBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
Mat hierarchy = new Mat();
Mat binaryMat = new Mat();
Mat resultMat = srcMat.clone();
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(35, 43, 46), new Scalar(77, 255, 255), binaryMat);
resultBitmap = Bitmap.createBitmap(hsvMat.width(), hsvMat.height(), Bitmap.Config.ARGB_8888);
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
double epsilon;
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
}
});
blueBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
Mat hierarchy = new Mat();
Mat binaryMat = new Mat();
Mat resultMat = srcMat.clone();
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(100, 43, 46), new Scalar(124, 255, 255), binaryMat);
resultBitmap = Bitmap.createBitmap(hsvMat.width(), hsvMat.height(), Bitmap.Config.ARGB_8888);
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
double epsilon;
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
}
});
yellowBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
Mat hierarchy = new Mat();
Mat binaryMat = new Mat();
Mat resultMat = srcMat.clone();
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(26, 43, 46), new Scalar(34, 255, 255), binaryMat);
resultBitmap = Bitmap.createBitmap(hsvMat.width(), hsvMat.height(), Bitmap.Config.ARGB_8888);
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
double epsilon;
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
}
});
cyanBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
Mat hierarchy = new Mat();
Mat binaryMat = new Mat();
Mat resultMat = srcMat.clone();
hsvMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(78, 43, 46), new Scalar(99, 255, 255), binaryMat);
resultBitmap = Bitmap.createBitmap(hsvMat.width(), hsvMat.height(), Bitmap.Config.ARGB_8888);
Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
double epsilon;
contours2f = new MatOfPoint2f(contours.get(0).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
}
});
allBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int tri = 0;
int rect = 0;
int circle = 0;
int star = 0;
int pentagon = 0;
contours = new ArrayList<>();
hsvMat = new Mat();
Mat resultMat = srcMat.clone();
Mat binaryMat = new Mat();
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_BGR2HSV);
Core.inRange(hsvMat, new Scalar(0, 0, 221), new Scalar(180, 30, 255), binaryMat);
Core.bitwise_not(binaryMat, binaryMat);
resultBitmap = Bitmap.createBitmap(binaryMat.width(), binaryMat.height(), Bitmap.Config.ARGB_8888);
Mat outMat = new Mat();
Imgproc.findContours(binaryMat, contours, outMat, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
contoursSize = contours.size();
double epsilon;
Imgproc.drawContours(resultMat, contours, -1, new Scalar(0, 0, 0), 10);
for (int i = 0; i < contoursSize; i++) {
contours2f = new MatOfPoint2f(contours.get(i).toArray());
epsilon = 0.04 * Imgproc.arcLength(contours2f, true);
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contours2f, approxCurve, epsilon, true);
if (approxCurve.rows() == 3) {
tri++;
}
if (approxCurve.rows() == 4) {
rect++;
}
if (approxCurve.rows() == 5) {
pentagon++;
}
if (approxCurve.rows() == 10) {
star++;
}
if (approxCurve.rows() > 5 && approxCurve.rows() != 10) {
circle++;
}
}
Log.d(TAG, "三角形:" + tri + "\t" + "矩形:" + rect + "\t" + "五边形:" + pentagon + "\t" + "星形:" + star + "\t" + "圆形:" + circle + "\t");
Imgproc.cvtColor(resultMat, resultMat, Imgproc.COLOR_RGB2BGR);
Utils.matToBitmap(resultMat, resultBitmap);
iv_dst.setImageBitmap(resultBitmap);
}
});
}
private void OpenCVInit() {
boolean success = OpenCVLoader.initDebug();
if (success) {
Log.d(TAG, "OpenCV Lode success");
} else {
Log.d(TAG, "OpenCV Lode failed ");
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_red"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RED"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_green"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.584" />
<ImageView
android:id="@+id/iv_dst"
android:layout_width="320dp"
android:layout_height="174dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.194"
app:srcCompat="@drawable/shape" />
<Button
android:id="@+id/btn_blue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BLUE"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_green"
app:layout_constraintTop_toTopOf="@+id/btn_red" />
<Button
android:id="@+id/btn_green"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="green"
app:layout_constraintTop_toTopOf="@+id/btn_red"
app:layout_constraintStart_toEndOf="@id/btn_red"
app:layout_constraintEnd_toStartOf="@id/btn_blue"
tools:layout_editor_absoluteX="190dp" />
<Button
android:id="@+id/btn_yellow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yellow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/btn_red"
app:layout_constraintTop_toBottomOf="@+id/btn_red"
app:layout_constraintVertical_bias="0.1" />
<Button
android:id="@+id/btn_cyan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cyan"
app:layout_constraintStart_toStartOf="@+id/btn_green"
app:layout_constraintTop_toTopOf="@+id/btn_yellow" />
<Button
android:id="@+id/btn_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="all"
app:layout_constraintStart_toStartOf="@+id/btn_blue"
app:layout_constraintTop_toTopOf="@+id/btn_cyan" />
</androidx.constraintlayout.widget.ConstraintLayout>
例图