首先从这里下载已经训练好的模型文件:
这个模型是使用Caffe实现的Google MobileNet SSD检测模型。有个Caffe Zoo项目,收集了很多已经训练好的模型,有兴趣的可以看一下。下载好模型之后,在app/src/main/
下面创建一个assets
目录,把两个模型文件放进去。至此,模型的准备工作就完成了。
布局文件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/imageSelect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
android:text="@string/image_select"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/recognize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:text="@string/recognize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageSelect"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="387dp"
android:layout_height="259dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="22dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:contentDescription="images"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageSelect" />
</androidx.constraintlayout.widget.ConstraintLayout>
刚接触安卓开发没几天,布局是瞎写的,仅考虑了功能。
MainActivity.java代码:
package com.niyanchun.demo;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.imgproc.Imgproc;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (OpenCVLoader.initDebug()) {
Log.i("CV", "load OpenCV Library Successful.");
} else {
Log.i("CV", "load OpenCV Library Failed.");
}
imageView = findViewById(R.id.imageView);
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
Button selectBtn = findViewById(R.id.imageSelect);
selectBtn.setOnClickListener(v -> {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent, "选择图片"), PICK_IMAGE_REQUEST);
});
Button recognizeBtn = findViewById(R.id.recognize);
recognizeBtn.setOnClickListener(v -> {
// 确保加载完成
if (net == null) {
Toast.makeText(this, "正在加载模型,请稍后...", Toast.LENGTH_LONG).show();
while (net == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
recognize();
});
}
@Override
protected void onResume() {
super.onResume();
loadModel();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK
&& data != null && data.getData() != null) {
Uri uri = data.getData();
try {
Log.d("image-decode", "start to decode selected image now...");
InputStream input = getContentResolver().openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(input, null, options);
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int max = Math.max(rawWidth, rawHeight);
int newWidth, newHeight;
float inSampleSize = 1.0f;
if (max > MAX_SIZE) {
newWidth = rawWidth / 2;
newHeight = rawHeight / 2;
while ((newWidth / inSampleSize) > MAX_SIZE || (newHeight / inSampleSize) > MAX_SIZE) {
inSampleSize *= 2;
}
}
options.inSampleSize = (int) inSampleSize;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
image = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options);
imageView.setImageBitmap(image);
} catch (Exception e) {
Log.e("image-decode", "decode image error", e);
}
}
}
/**
* 加载模型
*/
private void loadModel() {
if (net == null) {
Toast.makeText(this, "开始加载模型...", Toast.LENGTH_LONG).show();
String proto = getPath("MobileNetSSD_deploy.prototxt", this);
String weights = getPath("mobilenet_iter_73000.caffemodel", this);
net = Dnn.readNetFromCaffe(proto, weights);
Log.i("model", "load model successfully.");
Toast.makeText(this, "模型加载成功!", Toast.LENGTH_LONG).show();
}
}
/**
* 识别
*/
private void recognize() {
// 该网络的输入层要求的图片尺寸为 300*300
final int IN_WIDTH = 300;
final int IN_HEIGHT = 300;
final float WH_RATIO = (float) IN_WIDTH / IN_HEIGHT;
final double IN_SCALE_FACTOR = 0.007843;
final double MEAN_VAL = 127.5;
final double THRESHOLD = 0.2;
Mat imageMat = new Mat();
Utils.bitmapToMat(image, imageMat);
Imgproc.cvtColor(imageMat, imageMat, Imgproc.COLOR_RGBA2RGB);
Mat blob = Dnn.blobFromImage(imageMat, IN_SCALE_FACTOR,
new Size(IN_WIDTH, IN_HEIGHT),
new Scalar(MEAN_VAL, MEAN_VAL, MEAN_VAL),
false, false);
net.setInput(blob);
Mat detections = net.forward();
int cols = imageMat.cols();
int rows = imageMat.rows();
detections = detections.reshape(1, (int) detections.total() / 7);
boolean detected = false;
for (int i = 0; i < detections.rows(); ++i) {
double confidenceTmp = detections.get(i, 2)[0];
if (confidenceTmp > THRESHOLD) {
detected = true;
int classId = (int) detections.get(i, 1)[0];
int left = (int) (detections.get(i, 3)[0] * cols);
int top = (int) (detections.get(i, 4)[0] * rows);
int right = (int) (detections.get(i, 5)[0] * cols);
int bottom = (int) (detections.get(i, 6)[0] * rows);
// Draw rectangle around detected object.
Imgproc.rectangle(imageMat, new Point(left, top), new Point(right, bottom),
new Scalar(0, 255, 0), 4);
String label = classNames[classId] + ": " + confidenceTmp;
int[] baseLine = new int[1];
Size labelSize = Imgproc.getTextSize(label, Core.FONT_HERSHEY_COMPLEX, 0.5, 5, baseLine);
// Draw background for label.
Imgproc.rectangle(imageMat, new Point(left, top - labelSize.height),
new Point(left + labelSize.width, top + baseLine[0]),
new Scalar(255, 255, 255), Core.FILLED);
// Write class name and confidence.
Imgproc.putText(imageMat, label, new Point(left, top),
Core.FONT_HERSHEY_COMPLEX, 0.5, new Scalar(0, 0, 0));
}
}
if (!detected) {
Toast.makeText(this, "没有检测到目标!", Toast.LENGTH_LONG).show();
return;
}
Utils.matToBitmap(imageMat, image);
imageView.setImageBitmap(image);
}
// Upload file to storage and return a path.
private static String getPath(String file, Context context) {
Log.i("getPath", "start upload file " + file);
AssetManager assetManager = context.getAssets();
BufferedInputStream inputStream = null;
try {
// Read data from assets.
inputStream = new BufferedInputStream(assetManager.open(file));
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
// Create copy file in storage.
File outFile = new File(context.getFilesDir(), file);
FileOutputStream os = new FileOutputStream(outFile);
os.write(data);
os.close();
Log.i("getPath", "upload file " + file + "done");
// Return a path to file which may be read in common way.
return outFile.getAbsolutePath();
} catch (IOException ex) {
Log.e("getPath", "Failed to upload a file");
}
return "";
}
private static final int MAX_SIZE = 1024;
private ImageView imageView;
private Bitmap image;
private Net net = null;
private int PICK_IMAGE_REQUEST = 1;
private static final String[] classNames = {"background",
"aeroplane", "bicycle", "bird", "boat",
"bottle", "bus", "car", "cat", "chair",
"cow", "diningtable", "dog", "horse",
"motorbike", "person", "pottedplant",
"sheep", "sofa", "train", "tvmonitor"};
}
代码中的一些关键点说明如下:
loadModel
:实现了模型的加载,OpenCV提供了readNetFromCaffe
方法用于加载Caffe训练的模型,其输入就是两个模型文件。onActivityResult
:实现了选择图片后的图片处理和展示。recognize
:实现利用加载的模型进行目标检测,并根据检测结果用框画出目标的位置。和之前的基于HOG特征的目标检测类似。然后点击运行,效果如下:
可以看到,检测到了显示器、盆栽、猫、人等。对安卓还不太熟,后面有时间了弄一从摄像头视频中实时检测的App玩玩。
Reference:
]]>我使用的环境是:
Android Studio的安装就不说了非常简单(只要你网络畅通),OpenCV Android SDK下载后直接解压即可。关于OpenCV Android SDK目前最新的4.x版本是4.2.0,我刚开始使用的是这个版本,但发现问题比较多,所以最后使用了3.x的最新版本3.4.9。也强烈建议你使用3.x版本。
OpenCV底层的算法都是使用C/C++实现的,并且编译成了.so/.a,所以安卓(Java)要使用这些库必须通过JNI,安卓提供了一个开发工具包NDK(Native Development Kit),可以更方便的帮助开发人员通过JNI访问本地代码。这个包默认是没有安装的,所以第一步就是安装NDK及其一些依赖包。
直接上图吧:
安装NDK和CMake。
先创建一个Android的工程,按照下面的图来就行(重点地方已标记):
项目创建成功后在build的时候可能会出现如下错误:
A problem occurred configuring project ':app'.
> NDK not configured. Download it with SDK manager. Preferred NDK version is '20.0.5594570'. Log: /Volumes/Files/Study/android/opencv/Demo/app/.cxx/ndk_locator_record.json
这个时候打开“Project Structure”设置一下NDK的路径即可(NDK安装完成后再Android SDK目录下):
然后再Build,应该就没有什么错误了。接下来就是导入OpenCV SDK了。
右键工程,选择新增module:
module type选择Eclipse ADT Project:
然后就是选择模块的Source directory了,选择OpenCV Android SDK解压目录下面的sdk里面的java目录,如图所示:
选择以后,会自动生成一个模块名,自己也可以修改,建议使用默认值,因为随sdk的那些samples里面都用的是这个名字。4.x版本就没有这个贴心了。然后Next,后面都保持默认,直到Finish。
导入以后,修改导入的OpenCV模块的build.gradle
文件。主要有这么几个修改点:
apply plugin: 'com.android.application'
为apply plugin: 'com.android.library'
,这样才能将导入的OpenCV作为模块使用。compileSdkVersion
、buildToolsVersion
、 minSdkVersion
、 targetSdkVersion
值替换掉OpenCV build.gradle里面对应的值。然后编译。有可能会提示“AndroidManifest.xml”文件里面不应该有版本之类的错误,直接根据提示移除即可,如下图:修改完之后,确保build成功。
然后就是将导入的OpenCV添加为app的依赖:
确定即可。然后确保编译没有错误。到这里openCV依赖就算添加完了,只要编译没有错误,就可以在项目中使用OpenCV了。
但工作并没有完,OpenCV为了减小应用的体积,单独提供了一个OpenCV Manager的apk文件,这个文件包含了OpenCV的一些库。也就是要使用OpenCV,你的手机上面除了要安装你自己的app,还要安装OpenCV Manager才可以。如果没有装,你自己的app启动的时候,就会提示去Google Play下载。这里有几个坑:
本来这个OpenCV Manager的apk文件是随sdk一起的,但4.2的版本里面没有这个文件了(我估计4.x的都没有),但3.x的sdk包里面却有,就在解压目录里面有个apk目录,里面有各个硬件架构的apk:
当然很多时候,让别人为了使用你的App,再去安装另外一个app不是很OK,所以也有一种方式可以将需要的OpenCV的库直接打到自己的这个App里面。操作方式如下(这里有很多坑,网上绝大多数都没有说清楚):
将工程显示方式从Android切换为Project:
然后在app目录下面创建一个"jniLibs"目录,将OpenCV Android SDK的sdk->native->libs下面的目录拷贝到这个目录下(只拷贝需要的架构即可,一般手机都是arm64-v8a,模拟器一般都是x86、x86_64),如下图:
然后在app目录下面的build.gradle文件的android里面添加如下内容(网上绝大多数教程都漏了这一步):
文字版:
sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['jniLibs']
}
}
最后就是验证环境是否完全部署好了:修改MainActivity.java文件,在onCreate里面增加一些内容,如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
// tv.setText(stringFromJNI());
if (OpenCVLoader.initDebug()) {
Log.i("CV", "load OpenCV Library Successful.");
tv.setText("load OpenCV Library successfully.");
} else {
Log.i("CV", "load OpenCV Library Failed.");
tv.setText("load OpenCV Library failed.");;
}
}
然后连上你的手机或者模拟机,运行一下,如果提示加载OpenCV库成功,那整个环境搭建就算完成了:
至此,环境搭建就算完成了,总体来说,网上的文章有很多问题,包括官方那古老的文档。祝你好运!下篇文章介绍一下如何基于整个环境构建一个手机端的目标检测程序。
]]>注:图片来自这里.
提取出特征之后,就可以使用一些分类算法进行模型训练了。当时论文作者使用线性SVM进行了模型的训练,所以现在HOG特征也都基本是和SVM一起使用的(记得以前有统计称普通机器学习算法中最受欢迎的就是SVM和随机森林了)。完整的流程如下(图片来自HOG的原始论文):
OpenCV也支持基于HOG特征的目标检测,并且预先训练了一些模型,下面我们通过一个例子进行介绍:
import cv2
// 初始化HOG及SVM分类器
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
vs = cv2.VideoCapture("/Users/allan/Downloads/TownCentreXVID.avi")
threshold = 0.7
while True:
grabbed, frame = vs.read()
if grabbed:
frame = cv2.resize(frame, (1280, 720))
gray_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
rects, weights = hog.detectMultiScale(gray_frame)
for i, (x, y, w, h) in enumerate(rects):
if weights[i] < 0.7:
continue
else:
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imshow("frame", frame)
k = cv2.waitKey(1) & 0xFF
if k == ord("q"):
break
vs.release()
cv2.destroyAllWindows()
代码整体逻辑比较简单,和上篇文章代码非常相似。刚开始初始化HOG Descriptor,并设置SVM检测器为默认的行人检测器。然后从视频读取一帧帧的图片进行处理(文中的测试视频可在公众号回复"机器视觉实战3"获取)。
代码效果如下:
这里对检测函数detectMultiScale
的返回值稍作说明。我们知道分类算法的结果一般返回的是label,也就是告诉你目标属于哪个类别。而检测类算法要更进一步,不仅要解决图片中有没有目标出现,如果有,还要给出在哪里。所以不论是上节的Haar Cascades,还是这节的HOG,检测函数的返回值都很相似,HOG返回的信息更多,我们以HOG为例介绍。HOG返回了两个列表:rects和weights。检测到多少个目标,列表中就会有多少个值,即列表的大小回答了图片中有没有目标的问题。rects中的每个值是包含四个元素的元组,比如(952, 3, 77, 82),这四个值限定了图片中一个矩形,前两个值是矩形的一个顶点的坐标,后面两个值则是矩形的宽和高。而这个矩形就是检测出来的目标的位置,这些从代码中的cv2.rectangle
也能看出来。相比于Haar,HOG还多返回了一个weights列表,这个列表的行和rects是一致的,它指的是识别出来的目标的权重,可以理解为可信度,DNN模型里面一般称为confidence。
对于目标检测来说,还有一个非常重要的知识点,就是NMS(Non Maximum Suppression),一般翻译为非极大值抑制。我们进行检测的时候,是用一个个滑框去获取特征值的,所以会产生大量的重复区域,效果就是最终返回的矩形有很多会产生重复。而NMS就是用来消除这些局部区域的极大值,最终获得最大值,从而消除重复,下面是一张效果图:
左侧是没有经过NMS处理的,右侧是经过NMS处理。这里推荐一篇文章:非极大值抑制(Non-Maximum Suppression). NMS的算法原理相对简单,需要的时候可以自己实现,也可以使用一些已有实现。这里推荐一个Python的:
# pip install imutils
from imutils.object_detection import non_max_suppression
rects = np.array([[x, y, x + w, y + h] for (x, y, w, h) in rects])
pick = non_max_suppression(rects, probs=None, overlapThresh=0.65)
Haar Cascades和Hog特征检测是DNN没有出来之前主要的目标检测算法,即使现在有很多基于DNN的模型,Haar Cascades和Hog在工业界依旧有很多应用场景。
References
]]>然后用这些长方形去划过图片(非常类似于CNN里面的卷积核),每划一次,用白色框中的所有的像素值减去黑色框中所有像素的值,得到一个该特征的值。这样有了数据,有了特征,就可以训练分类器了,比如使用AdaBoost分类器对数据进行训练。目前这个算法已经非常成熟,本文演示一下如何借助OpenCV使用Haar Cascades进行目标检测。OpenCV已经集成了Haar Cascades,而且内置了一些训练好的模型,可以从这里获取和查看。
下面代码实现了通过Haar特征进行人脸和眼睛检测:
import cv2
from imutils.video import VideoStream
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_eye_tree_eyeglasses.xml")
vs = VideoStream(src=0).start()
while True:
frame = vs.read()
frame = cv2.resize(frame, (640, 480))
# Haar Cascades必须使用灰度图
gray_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
rects_face = face_cascade.detectMultiScale(gray_frame)
for (x, y, w, h) in rects_face:
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
y = y - 10 if y - 10 > 10 else y + 10
cv2.putText(frame, "face", (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 255, 0), 1)
rects_eye = eye_cascade.detectMultiScale(gray_frame)
for (x, y, w, h) in rects_eye:
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
y = y - 10 if y - 10 > 10 else y + 10
cv2.putText(frame, "eye", (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 255), 1)
# 滤镜不能少...
frame = cv2.bilateralFilter(frame, 0, 20, 5)
cv2.imshow("preview", frame)
k = cv2.waitKey(1) & 0xFF
if k == ord("q"):
break
代码见GitHub.
下面是检测效果(原谅我厚颜无耻的加了一点滤镜...):
关于Haar Cascades算法需要知道以下几点:
虽然Haar Cascades一般没有一些基于深度学习的目标检测模型强大,但他也有自己的一些优势,比如容易训练、资源占用少等,所以仍旧有一定的使用场景。本文介绍的比较简单,一些细节会放在下篇文章中和另外一种目标检测算法一起讲解。
]]>原文出处:Ball Tracking with OpenCV.
我使用的环境是:MacOS+Python3.7.4+OpenCV 4.2.0.
相比于原文,我对代码做了一些小修改,并且加了一些中文注释,方便你理解。整个项目代码如下:
import argparse
import time
from collections import deque
import cv2
import imutils
import numpy as np
from imutils.video import VideoStream
# 命令行参数
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video", help="path to video")
ap.add_argument("-b", "--buffer", type=int, default=64, help="max buffer size")
args = vars(ap.parse_args())
# 绿色球的HSV色域空间范围
greenLower = (29, 86, 6)
greenUpper = (64, 255, 255)
pts = deque(maxlen=args["buffer"])
# 判断是读入的视频文件,还是摄像头实时采集的,这里作区分是因为两种情况下后面的有些操作是有区别的
if args.get("video", None) is None:
useCamera = True
print("video is none, use camera...")
vs = VideoStream(src=0).start()
else:
useCamera = False
vs = cv2.VideoCapture(args["video"])
time.sleep(2.0)
while True:
frame = vs.read()
# 摄像头返回的数据格式为(帧数据),而从视频抓取的格式为(grabbed, 帧数据),grabbed表示是否读到了数据
frame = frame if useCamera else frame[1]
# 对于从视频读取的情况,frame为None表示数据读完了
if frame is None:
break
# resize the frame(become small) to process faster(increase FPS)
frame = imutils.resize(frame, width=600)
# blur the frame to reduce high frequency noise, and allow
# us to focus on the structural objects inside the frame
# 通过高斯滤波去除掉一些高频噪声,使得重要的数据更加突出
blurred = cv2.GaussianBlur(frame, (11, 11), 0)
# convert frame to HSV color space
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
# handles the actual localization of the green ball in the frame
# inRange的作用是根据阈值进行二值化:阈值内的像素设置为白色(255),阈值外的设置为黑色(0)
mask = cv2.inRange(hsv, greenLower, greenUpper)
# A series of erosions and dilations remove any small blobs that may be left on the mask
# 腐蚀(erode)和膨胀(dilate)的作用:
# 1. 消除噪声;
# 2. 分割(isolate)独立的图像元素,以及连接(join)相邻的元素;
# 3. 寻找图像中的明显的极大值区域或极小值区域
mask = cv2.erode(mask, None, iterations=2)
mask = cv2.dilate(mask, None, iterations=2)
# 寻找轮廓,不同opencv的版本cv2.findContours返回格式有区别,所以调用了一下imutils.grab_contours做了一些兼容性处理
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
center = None
# only proceed if at least one contour was found
if len(cnts) > 0:
# find the largest contour in the mask, then use it to compute the minimum enclosing circle
# and centroid
c = max(cnts, key=cv2.contourArea)
((x, y), radius) = cv2.minEnclosingCircle(c)
M = cv2.moments(c)
# 对于01二值化的图像,m00即为轮廓的面积, 一下公式用于计算中心距
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
# only proceed if the radius meets a minimum size
if radius > 10:
# draw the circle and centroid on the frame, then update the list of tracked points
cv2.circle(frame, (int(x), int(y)), int(radius), (0, 255, 255), 2)
cv2.circle(frame, center, 5, (0, 0, 255), -1)
pts.appendleft(center)
for i in range(1, len(pts)):
# if either of the tracked points are None, ignore them
if pts[i - 1] is None or pts[i] is None:
continue
# compute the thickness of the line and draw the connecting line
thickness = int(np.sqrt(args["buffer"] / float(i + 1)) * 2.5)
cv2.line(frame, pts[i - 1], pts[i], (0, 0, 255), thickness)
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
break
if useCamera:
vs.stop()
else:
vs.release()
cv2.destroyAllWindows()
该项目主要使用OpenCV实现,里面重要的地方我都已经加了注释说明,这里再理一下整个流程:
循环处理视频中的帧:
对于没有图像处理基础的人来说,可能不了解其中一些处理的作用,这里我把一些关键步骤处理的效果图放上来,方便你理解(以其中一帧图片为例):
resize后的效果:
这一步除了尺寸有变化,其它都和原图一样。
高斯滤波后的效果:
滤掉了高频噪音,看着变模糊了。
转换到HSV色域后的效果:
二值化后的效果:
腐蚀和膨胀后的效果:
先简单介绍下HSV颜色空间吧(其实还有个HSL,和HSV类似,但有些不同)。RGB是在笛卡尔坐标系来表示颜色的,而HSV则是在圆柱坐标系中表示颜色的,所以它能表示更多的信息。下图是一个HSV的图(图片来自维基百科):
简单说就是HSV使用圆柱坐标系表示颜色,分三个维度(如上图中的e):
圆柱的中心轴底部为黑色,顶部为白色,中间为灰色。更详细的信息请参加维基百科:HSV。
那为什么我们要将图像从RGB转到HSV再做颜色识别呢?因为虽然RGB通道对人眼来说比较友好,但却不能很好地反映出物体具体的颜色信息。相反,HSV空间能够非常直观的表达色彩的明暗,色调,以及鲜艳程度,方便进行颜色之间的对比。所以在通过颜色识别对象时,都是采用HSV颜色空间的。
最后说一下两个实战时需要注意的事项:
一个颜色在HSV中没有特别明确的值或者范围,实际使用的时候往往需要我们自己去观察测试进行确定。下面是一个常见颜色H值大致的范围参考(注意范围是0\~360):
通过这个项目我们可以学到如何通过颜色来做物体识别。
]]>项目来源?
有没有GitHub项目?
涉及了哪些知识?
其它说明