NYC's Blog - 机器视觉 http://niyanchun.com/tag/machine-vision/ 机器视觉实战5:安卓端目标检测App开发 http://niyanchun.com/object-detection-on-android.html 2020-03-08T15:55:00+08:00 上篇文章《机器视觉实战4:OpenCV Android环境搭建(喂饭版)》中介绍了如何使用Android Studio搭建OpenCV开发环境,本节基于之前搭建好的环境开发一个基于神经网络的目标检测App。准备模型首先从这里下载已经训练好的模型文件:deploy.prototxt:神经网络结构的描述文件mobilenet_iter_73000.caffemodel:神经网络的参数信息这个模型是使用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:https://docs.opencv.org/4.2.0/d0/d6c/tutorial_dnn_android.html 机器视觉实战4:OpenCV Android环境搭建(喂饭版) http://niyanchun.com/android-opencv-dev-env.html 2020-03-08T07:25:00+08:00 本文介绍如何构建OpenCV Android开发环境,在OpenCV与Android Studio集成的过程中,看了很多文章,但实际操作的时候都还是多多少少碰到了一些问题,所以写一篇文章,总结记录一下,也希望能够帮助到他人。我使用的环境是:MacOS 10.15.3Android Studio 3.6.1(下载,包含MacOS Android SDK)OpenCV Android SDK 3.4.9(下载)环境准备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直接上图吧:安装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了。导入OpenCV右键工程,选择新增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作为模块使用。用app的build.gradle里面的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下载。这里有几个坑:我试了一下,即使能访问Google Play,也搜索不到这个app。本来这个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库成功,那整个环境搭建就算完成了:至此,环境搭建就算完成了,总体来说,网上的文章有很多问题,包括官方那古老的文档。祝你好运!下篇文章介绍一下如何基于整个环境构建一个手机端的目标检测程序。 机器视觉实战3:基于Hog特征的目标检测 http://niyanchun.com/hog-object-detection.html 2020-03-07T19:23:00+08:00 上篇文章《机器视觉实战2:基于Haar特征的目标检测》中介绍了如何使用Haar特征进行目标检测,本文介绍另外一种目标检测算法:基于HOG特征的目标检测。该算法是在Dalal和Triggs于2005年发表的论文 Histogram of Oriented Gradients for Human Detection 中提出的,他们当时正在研究行人检测。HOG特征和Haar特征类似,都是一种提取特征的算法,其原理都是选择一个窗口,然后使用这个窗口去滑过图片的所有区域(如下图),每滑动一次就会产生一个特征值,相比于Haar,HOG的特征值计算更加复杂一些,要进行投影、计算梯度等操作,细节参见 Wikipedia HOG。注:图片来自这里.提取出特征之后,就可以使用一些分类算法进行模型训练了。当时论文作者使用线性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在工业界依旧有很多应用场景。ReferencesPedestrian Detection OpenCV 机器视觉实战2:基于Haar特征的目标检测 http://niyanchun.com/mvia-2-haar-cascades-object-detection.html 2020-03-06T21:13:00+08:00 Haar Cascades是Paul Viola和Michael Jones在2001年发表的论文"Rapid Object Detection using a Boosted Cascade of Simple Features"中提出的一种目标检测算法。论文中写的非常细致,网上相关的文章也很多,作为一个非学院派、业余AI爱好者就只简单说一下其原理。我们知道一个好的分类模型一般至少需要两个先决条件:高质量的训练数据+高质量的特征,Haar Cascades也不例外 。他需要通过一大批正样本(包含目标)和负样本(不包含目标)来训练分类器,这是对数据的要求。论文还提出了如何提取特征:定义一些长方形,每一个长方形就代表一个特征,这些长方形内部会划分为多个黑白色区域。比如下图,定义了A、B、C、D四个长方形,即四个特征:然后用这些长方形去划过图片(非常类似于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分类器的输入需要灰度图。Haar特征是一种目标检测算法,可以用来训练各种目标的检测模型。但一个模型只能用来检测一种目标,比如上面代码中,检测人脸和眼睛是两个模型(其实光眼睛这一个也区分了两个模型,一个戴眼镜和一个不戴眼睛)。这和基于深度学习的目标检测模型是有区别的,后者一般一个模型可以检测多个目标。虽然Haar Cascades一般没有一些基于深度学习的目标检测模型强大,但他也有自己的一些优势,比如容易训练、资源占用少等,所以仍旧有一定的使用场景。本文介绍的比较简单,一些细节会放在下篇文章中和另外一种目标检测算法一起讲解。 机器视觉实战1:Ball Tracking With OpenCV http://niyanchun.com/mvia-1-ball-tracking-with-opencv.html 2020-02-23T20:11:00+08:00 本文是机器视觉实战系列第1篇,实现的一个通过颜色来小球的检测和运动轨迹跟踪,效果如下:原文出处: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实现,里面重要的地方我都已经加了注释说明,这里再理一下整个流程:处理命令行参数。定义绿色球在HSV色域空间的颜色范围,后面会根据这个范围进行二值化,从而实现轮廓检测。区分一下是直接从摄像头读取的数据还是处理的已经拍好的视频,这里作区分是因为两种情况下,后面有些代码要做不同处理。循环处理视频中的帧:获取1帧的图像。对于从已有视频读的情况,如果读到的帧为空,则表示处理视频处理完了。调整图片大小,主要是缩小一下图片,提高处理速度。通过高斯滤波去除掉一些高频噪声,使得重要的数据更加突出。将图像从BGR色域转换到HSV色域。根据绿色球在HSV色域的颜色范围进行二值化。这样处理之后,绿色就会变成白色,其它都会变成黑色,方便后面提取球的轮廓。后面进行的腐蚀和膨胀操作也是为了消除噪声点(毛刺),更准确的提取小球轮廓。提取轮廓。后面就是根据提取出来的轮廓,画了一些小球的边沿,以及运动轨迹,就不细述了,代码里面很清楚。图像处理效果展示对于没有图像处理基础的人来说,可能不了解其中一些处理的作用,这里我把一些关键步骤处理的效果图放上来,方便你理解(以其中一帧图片为例):resize后的效果:​ ​ 这一步除了尺寸有变化,其它都和原图一样。高斯滤波后的效果:滤掉了高频噪音,看着变模糊了。转换到HSV色域后的效果:二值化后的效果:腐蚀和膨胀后的效果:HSV颜色空间先简单介绍下HSV颜色空间吧(其实还有个HSL,和HSV类似,但有些不同)。RGB是在笛卡尔坐标系来表示颜色的,而HSV则是在圆柱坐标系中表示颜色的,所以它能表示更多的信息。下图是一个HSV的图(图片来自维基百科):简单说就是HSV使用圆柱坐标系表示颜色,分三个维度(如上图中的e):Hue:即色相,就是我们平时说的颜色名称,比如红色、黄色。H的取值为绕圆柱中心轴一圈的角度(即圆柱坐标系中的方位角),所以取值范围是0\~360。Saturation:即饱和度,是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。S为圆柱的半径值(即圆柱坐标系中的径向距离)。Value:即明度,也称亮度,取0-100%的数值。V为圆柱坐标系中的高度。圆柱的中心轴底部为黑色,顶部为白色,中间为灰色。更详细的信息请参加维基百科:HSV。那为什么我们要将图像从RGB转到HSV再做颜色识别呢?因为虽然RGB通道对人眼来说比较友好,但却不能很好地反映出物体具体的颜色信息。相反,HSV空间能够非常直观的表达色彩的明暗,色调,以及鲜艳程度,方便进行颜色之间的对比。所以在通过颜色识别对象时,都是采用HSV颜色空间的。最后说一下两个实战时需要注意的事项:HSV标准中,Hue是角度,所以取值范围是0\~360,但在OpenCV中做了精简,实际范围变成了0\~180;S和V的取值是0\~100%,但OpenCV中是0\~255.所以使用OpenCV的时候需要注意一下。一个颜色在HSV中没有特别明确的值或者范围,实际使用的时候往往需要我们自己去观察测试进行确定。下面是一个常见颜色H值大致的范围参考(注意范围是0\~360):红色(Red):0\~60黄色(Yellow):61\~120绿色(Green): 121\~180青色(Cyan): 181\~240蓝色(Blue): 241\~300品红(Magenta): 301\~360资源信息完整代码:code.涉及的视频资源:在公众号回复“机器视觉实战1”获取下载链接。通过这个项目我们可以学到如何通过颜色来做物体识别。 机器视觉实战0:开篇 http://niyanchun.com/machine-vision-in-action-0-preface.html 2020-02-22T22:21:00+08:00 本系列主要是分享一些网上的机器视觉实战项目,会长期更新,以下是本系列的一些信息,供参考。项目来源?项目主要收集自互联网上公开的教程(大多会是国外网站上的英文教程),仅做学习使用,每篇文章会附上原始出处。有兴趣的可阅读原文,就当是练习英语水平了。有没有GitHub项目?文章中会有完整的代码,我也建了一个对应的项目:machine-vision-in-action.涉及了哪些知识?机器视觉主要涉及传统机器学习、深度学习(主要是CNN)这些理论知识,同时会用到Numpy、Matplotlib、OpenCV、Tensorflow/Pytorch等库或者框架,主要语言是Python。其它说明项目里面可能会涉及一些测试用的图片、视频,这些数据比较大,有一些可能原作者归档到了国外的服务器,或者传到了YouTube上面,有的读者可能获取不到,所以为了方便,我会将涉及到的数据统一下载下来(除过涉及到版权的以及有一些限制的),并上传到百度网盘上面分享出来。本系列是实战系列,以应用为主,理论为辅,所以不适合于想对理论、算法做深入研究的同学。但文中会给出一些有用的链接,有需要的可参考。