NYC's Blog - OpenCV http://niyanchun.com/tag/opencv/ zh-CN 开源机器视觉库。 Sun, 08 Mar 2020 15:55:00 +0800 Sun, 08 Mar 2020 15:55:00 +0800 机器视觉实战5:安卓端目标检测App开发 http://niyanchun.com/object-detection-on-android.html http://niyanchun.com/object-detection-on-android.html Sun, 08 Mar 2020 15:55:00 +0800 NYC 上篇文章《机器视觉实战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特征的目标检测类似。

然后点击运行,效果如下:

result

可以看到,检测到了显示器、盆栽、猫、人等。对安卓还不太熟,后面有时间了弄一从摄像头视频中实时检测的App玩玩。

Reference:

]]>
0 http://niyanchun.com/object-detection-on-android.html#comments http://niyanchun.com/feed/tag/opencv/
机器视觉实战4:OpenCV Android环境搭建(喂饭版) http://niyanchun.com/android-opencv-dev-env.html http://niyanchun.com/android-opencv-dev-env.html Sun, 08 Mar 2020 07:25:00 +0800 NYC 本文介绍如何构建OpenCV Android开发环境,在OpenCV与Android Studio集成的过程中,看了很多文章,但实际操作的时候都还是多多少少碰到了一些问题,所以写一篇文章,总结记录一下,也希望能够帮助到他人。

我使用的环境是:

  • MacOS 10.15.3
  • Android 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

直接上图吧:

3OxbyF.png
3OxqL4.md.png

安装NDK和CMake。

创建项目

先创建一个Android的工程,按照下面的图来就行(重点地方已标记):

3Ozr79.md.png
3OzykR.md.png
3OzD0J.md.png

项目创建成功后在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目录下):

NDK

然后再Build,应该就没有什么错误了。接下来就是导入OpenCV SDK了。

导入OpenCV

右键工程,选择新增module:

module

module type选择Eclipse ADT Project:

module type

然后就是选择模块的Source directory了,选择OpenCV Android SDK解压目录下面的sdk里面的java目录,如图所示:

source

java

选择以后,会自动生成一个模块名,自己也可以修改,建议使用默认值,因为随sdk的那些samples里面都用的是这个名字。4.x版本就没有这个贴心了。然后Next,后面都保持默认,直到Finish。

导入以后,修改导入的OpenCV模块的build.gradle文件。主要有这么几个修改点:

  • 修改第一行的apply plugin: 'com.android.application'apply plugin: 'com.android.library',这样才能将导入的OpenCV作为模块使用。
  • 用app的build.gradle里面的compileSdkVersionbuildToolsVersionminSdkVersiontargetSdkVersion值替换掉OpenCV build.gradle里面对应的值。然后编译。有可能会提示“AndroidManifest.xml”文件里面不应该有版本之类的错误,直接根据提示移除即可,如下图:

gradle

修改完之后,确保build成功。

然后就是将导入的OpenCV添加为app的依赖:

dependency

d2

确定即可。然后确保编译没有错误。到这里openCV依赖就算添加完了,只要编译没有错误,就可以在项目中使用OpenCV了。

但工作并没有完,OpenCV为了减小应用的体积,单独提供了一个OpenCV Manager的apk文件,这个文件包含了OpenCV的一些库。也就是要使用OpenCV,你的手机上面除了要安装你自己的app,还要安装OpenCV Manager才可以。如果没有装,你自己的app启动的时候,就会提示去Google Play下载。这里有几个坑:

  1. 我试了一下,即使能访问Google Play,也搜索不到这个app。
  2. 本来这个OpenCV Manager的apk文件是随sdk一起的,但4.2的版本里面没有这个文件了(我估计4.x的都没有),但3.x的sdk包里面却有,就在解压目录里面有个apk目录,里面有各个硬件架构的apk:

    apks

当然很多时候,让别人为了使用你的App,再去安装另外一个app不是很OK,所以也有一种方式可以将需要的OpenCV的库直接打到自己的这个App里面。操作方式如下(这里有很多坑,网上绝大多数都没有说清楚):

  • 将工程显示方式从Android切换为Project:

    project

  • 然后在app目录下面创建一个"jniLibs"目录,将OpenCV Android SDK的sdk->native->libs下面的目录拷贝到这个目录下(只拷贝需要的架构即可,一般手机都是arm64-v8a,模拟器一般都是x86、x86_64),如下图:

    so

    jnilibs

  • 然后在app目录下面的build.gradle文件的android里面添加如下内容(网上绝大多数教程都漏了这一步):

    srclib

    文字版:

        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库成功,那整个环境搭建就算完成了:

successfully

至此,环境搭建就算完成了,总体来说,网上的文章有很多问题,包括官方那古老的文档。祝你好运!下篇文章介绍一下如何基于整个环境构建一个手机端的目标检测程序。

]]>
0 http://niyanchun.com/android-opencv-dev-env.html#comments http://niyanchun.com/feed/tag/opencv/
机器视觉实战3:基于Hog特征的目标检测 http://niyanchun.com/hog-object-detection.html http://niyanchun.com/hog-object-detection.html Sat, 07 Mar 2020 19:23:00 +0800 NYC 上篇文章《机器视觉实战2:基于Haar特征的目标检测》中介绍了如何使用Haar特征进行目标检测,本文介绍另外一种目标检测算法:基于HOG特征的目标检测。该算法是在Dalal和Triggs于2005年发表的论文 Histogram of Oriented Gradients for Human Detection 中提出的,他们当时正在研究行人检测。HOG特征和Haar特征类似,都是一种提取特征的算法,其原理都是选择一个窗口,然后使用这个窗口去滑过图片的所有区域(如下图),每滑动一次就会产生一个特征值,相比于Haar,HOG的特征值计算更加复杂一些,要进行投影、计算梯度等操作,细节参见 Wikipedia HOG

sliding-window

注:图片来自这里.

提取出特征之后,就可以使用一些分类算法进行模型训练了。当时论文作者使用线性SVM进行了模型的训练,所以现在HOG特征也都基本是和SVM一起使用的(记得以前有统计称普通机器学习算法中最受欢迎的就是SVM和随机森林了)。完整的流程如下(图片来自HOG的原始论文):

hog-svm

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"获取)。

代码效果如下:

result

这里对检测函数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处理的,右侧是经过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

]]>
0 http://niyanchun.com/hog-object-detection.html#comments http://niyanchun.com/feed/tag/opencv/
机器视觉实战2:基于Haar特征的目标检测 http://niyanchun.com/mvia-2-haar-cascades-object-detection.html http://niyanchun.com/mvia-2-haar-cascades-object-detection.html Fri, 06 Mar 2020 21:13:00 +0800 NYC Haar Cascades是Paul Viola和Michael Jones在2001年发表的论文"Rapid Object Detection using a Boosted Cascade of Simple Features"中提出的一种目标检测算法。论文中写的非常细致,网上相关的文章也很多,作为一个非学院派、业余AI爱好者就只简单说一下其原理。我们知道一个好的分类模型一般至少需要两个先决条件:高质量的训练数据+高质量的特征,Haar Cascades也不例外 。他需要通过一大批正样本(包含目标)和负样本(不包含目标)来训练分类器,这是对数据的要求。论文还提出了如何提取特征:定义一些长方形,每一个长方形就代表一个特征,这些长方形内部会划分为多个黑白色区域。比如下图,定义了A、B、C、D四个长方形,即四个特征:

Haar特征

然后用这些长方形去划过图片(非常类似于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.

下面是检测效果(原谅我厚颜无耻的加了一点滤镜...):

detected

关于Haar Cascades算法需要知道以下几点:

  1. Haar Cascades分类器的输入需要灰度图。
  2. Haar特征是一种目标检测算法,可以用来训练各种目标的检测模型。但一个模型只能用来检测一种目标,比如上面代码中,检测人脸和眼睛是两个模型(其实光眼睛这一个也区分了两个模型,一个戴眼镜和一个不戴眼睛)。这和基于深度学习的目标检测模型是有区别的,后者一般一个模型可以检测多个目标。

虽然Haar Cascades一般没有一些基于深度学习的目标检测模型强大,但他也有自己的一些优势,比如容易训练、资源占用少等,所以仍旧有一定的使用场景。本文介绍的比较简单,一些细节会放在下篇文章中和另外一种目标检测算法一起讲解。

]]>
0 http://niyanchun.com/mvia-2-haar-cascades-object-detection.html#comments http://niyanchun.com/feed/tag/opencv/
机器视觉实战1:Ball Tracking With OpenCV http://niyanchun.com/mvia-1-ball-tracking-with-opencv.html http://niyanchun.com/mvia-1-ball-tracking-with-opencv.html Sun, 23 Feb 2020 20:11:00 +0800 NYC 本文是机器视觉实战系列第1篇,实现的一个通过颜色来小球的检测和运动轨迹跟踪,效果如下:

ball tracking

原文出处: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实现,里面重要的地方我都已经加了注释说明,这里再理一下整个流程:

  1. 处理命令行参数。
  2. 定义绿色球在HSV色域空间的颜色范围,后面会根据这个范围进行二值化,从而实现轮廓检测。
  3. 区分一下是直接从摄像头读取的数据还是处理的已经拍好的视频,这里作区分是因为两种情况下,后面有些代码要做不同处理。
  4. 循环处理视频中的帧:

    1. 获取1帧的图像。对于从已有视频读的情况,如果读到的帧为空,则表示处理视频处理完了。
    2. 调整图片大小,主要是缩小一下图片,提高处理速度。
    3. 通过高斯滤波去除掉一些高频噪声,使得重要的数据更加突出。
    4. 将图像从BGR色域转换到HSV色域。
    5. 根据绿色球在HSV色域的颜色范围进行二值化。这样处理之后,绿色就会变成白色,其它都会变成黑色,方便后面提取球的轮廓。后面进行的腐蚀和膨胀操作也是为了消除噪声点(毛刺),更准确的提取小球轮廓。
    6. 提取轮廓。
    7. 后面就是根据提取出来的轮廓,画了一些小球的边沿,以及运动轨迹,就不细述了,代码里面很清楚。

图像处理效果展示

对于没有图像处理基础的人来说,可能不了解其中一些处理的作用,这里我把一些关键步骤处理的效果图放上来,方便你理解(以其中一帧图片为例):

  • resize后的效果:

    31bY38.png

    ​ 这一步除了尺寸有变化,其它都和原图一样。

  • 高斯滤波后的效果:

    31b84P.png

滤掉了高频噪音,看着变模糊了。

  • 转换到HSV色域后的效果:

    31btgS.png

  • 二值化后的效果:

    31b3Nt.png

  • 腐蚀和膨胀后的效果:

    31b1AI.png

HSV颜色空间

先简单介绍下HSV颜色空间吧(其实还有个HSL,和HSV类似,但有些不同)。RGB是在笛卡尔坐标系来表示颜色的,而HSV则是在圆柱坐标系中表示颜色的,所以它能表示更多的信息。下图是一个HSV的图(图片来自维基百科):

33qP6U.png

简单说就是HSV使用圆柱坐标系表示颜色,分三个维度(如上图中的e):

  • Hue:即色相,就是我们平时说的颜色名称,比如红色、黄色。H的取值为绕圆柱中心轴一圈的角度(即圆柱坐标系中的方位角),所以取值范围是0\~360。
  • Saturation:即饱和度,是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。S为圆柱的半径值(即圆柱坐标系中的径向距离)。
  • Value:即明度,也称亮度,取0-100%的数值。V为圆柱坐标系中的高度

圆柱的中心轴底部为黑色,顶部为白色,中间为灰色。更详细的信息请参加维基百科:HSV

那为什么我们要将图像从RGB转到HSV再做颜色识别呢?因为虽然RGB通道对人眼来说比较友好,但却不能很好地反映出物体具体的颜色信息。相反,HSV空间能够非常直观的表达色彩的明暗,色调,以及鲜艳程度,方便进行颜色之间的对比。所以在通过颜色识别对象时,都是采用HSV颜色空间的。

最后说一下两个实战时需要注意的事项:

  1. HSV标准中,Hue是角度,所以取值范围是0\~360,但在OpenCV中做了精简,实际范围变成了0\~180;S和V的取值是0\~100%,但OpenCV中是0\~255.所以使用OpenCV的时候需要注意一下。
  2. 一个颜色在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/mvia-1-ball-tracking-with-opencv.html#comments http://niyanchun.com/feed/tag/opencv/
OpenCV学习笔记(7)——关于RGBA的存储 http://niyanchun.com/opencv-notes-7.html http://niyanchun.com/opencv-notes-7.html Fri, 11 Apr 2014 09:49:00 +0800 NYC 其实前面我已经说过OpenCV中关于RGBA的存储问题,但是为了强调,这里专门单独拿出来介绍一下。

在OpenCV中有分别有1通道图,2通道图,3通道图,4通道图。我们最常用的是3通道图,即RGB图,4通道图是在3通道图的基础上多了一个Alpha分量,用于表示透明这个特性。虽然我们一般说的时候都是RGBA的顺序,但其实在内存里面,图像的存储顺序是BGRA BGRA BGRA …。而我们知道在OpenCV里面使用CvScalar结构表示颜色,正好该结构包含4个双精度浮点型成员。用该结构表示颜色时,它的4个成员变量对应的颜色时B、G、R、A,即是颜色在内存里面存储的顺序,而不是我们通常说的颜色的顺序(RGBA)。所以,在设计颜色的函数调用中,我们就需要注意。比如,我们想设置蓝色,则应该是cvScalar(255, 0, 0),设置红色应该是cvScalar(0, 255, 0)。当然,为了满足人们平时的习惯,OpenCV里面有一个宏CV_RGB,其实是一个宏定义:

#define CV_RGB(r, g, b)	cvScalar((b), (g), (r), 0)

所以如果我们习惯了RGB的顺序,可以使用该宏来获得颜色,例如设置绿色:CV_RGB(0, 255, 0)。

]]>
0 http://niyanchun.com/opencv-notes-7.html#comments http://niyanchun.com/feed/tag/opencv/
OpenCV学习笔记(6)——绘图(包括字体和文字) http://niyanchun.com/opencv-notes-6.html http://niyanchun.com/opencv-notes-6.html Thu, 10 Apr 2014 22:03:00 +0800 NYC 绘图是OpenCV经常使用的一个地方,很多时候我们需要绘制图像或者在已有的图像上方绘制一些图像。

1. 绘制直线

函数原型:

void cvLine(
  CvArr*    array,
  CvPoint    pt1,
  CvPoint    pt2,
  CvScalar    color,
  int        thickness = 1,
  int        connectivity = 8
);

第一个参数 一般为一个图像类型的指针IplImage*。

第二个和第三个参数分别为直线的起点和终点,是CvPoint类型的,我们可以使用cvPoint(int x, int y)构造函数很方便的构造一个CvPoint类型的变量。

第四个参数是一个CvScalar类型的颜色变量,其结构前面已经介绍过,是一个包含四个双精度浮点型变量的集合。在这里。前三个分别代表红、绿、蓝通道(但其实因为RGB在内存里面的存储顺序是BGRA BGRA,所以当使用CVScalar类型表示颜色时,其实四个分量分别代表B、G、R、A。如果想按照RGBA的顺序,可以使用后面介绍的宏CV_RGB构造CvScalar类型,而不是cvScalar。关于这一点,最后面的例子中也会有体现);没有用到第四个(它只在适当的时候用于alpha通道)。一个常用的便捷宏指令是CV_RGB(r, g, b),该指令采用三个数字作为参数并将其封装到CvScalar。

最后两个参数是可选的,因为都有默认值。thickness是线的粗细(像素)。connectivity被设为反走样模式,默认值为“八连通”,这种是较为平滑不会走样的线型。也可以设置为“4连通”,这样的话,斜线会产生重叠以致看上去过于粗重,不过画起来速度很快。

2. 绘制矩形

函数原型:

void cvRectangle(
  CvArr*    array,
  CvPoint    pt1,
  CvPoint    pt2,
  CvScalar    color,
  int        thickness = 1
);

从函数原型就可以看出,这个函数和cvLine( )除了最后一个参数外,其他都一样。的确是,因为这个函数画的矩形总是平行于X和Y轴。利用这个函数画矩形,只需要给出两个对顶点就可以。

3. 绘制圆形和椭圆

函数原型:

void cvCircle (
  CvArr*    array,
  CvPoint    center,
  int        radius,
  CvScalar    color,
  int        thickness = 1,
  int        connectivity = 8
);

void cvEllipse(
  CvArr*    img,
  CvPoint    center,
  CvSize    axes,
  double    angle,
  double    start_angle,
  double    end_angle,
  CvScalar    color,
  int        thickness =1,
  int        line_type = 8
);

// 使用外接矩形绘制椭圆
void cvEllipseBox(
  CvArr*    img,
  CvBox2D    box,
  CvScalar    color,
  int        thickness = 1,
  int         line_type = 8,
  int        shift = 0
);

画圆也很简单,参数和前面介绍的基本相同,这里就不多介绍了。对于圆形和矩阵等很多封闭图形来说,thickness参数也可以设置为CV_FILL,其值是-1,;其结果是使用与边一样的颜色填充封闭空间。

椭圆函数比cvCircle( )略复杂一点。主要的新成员是axes属性,其类型为CvSize,其结构前面已经介绍过,是一个包含宽度和高度的简单结构。同样,可以利用其构造函数cvSize( )得到一个CvSize结构。在这种情况下,height和width参数分别代表椭圆的长短半轴长。  angle是指偏离主轴的角度,从X轴算起,逆时针方向为正。同样,start_angle和end_angle表示弧线开始和结束位置的角度。因此,一个完整的椭圆必须分别将这两个值分别设为0°和360°。

除了cvEllipse( )函数外,还可以使用外接矩形的方法绘制椭圆,函数是cvEllipseBox( )。该函数用到一个CvBox2D结构:

typedef struct {
  CvPoint2D32f    center,
  CvSize2D32f    size,
  float        angle
} CvBox2D;

4. 绘制多边形

绘制多边形有多个函数:

void cvFillPoly (
  CvArr*    img,
  CvPoint**    pts,
  int*        npts,
  int        contours,
  CvScalar    color
  int        line_type = 8
);

void cvFillConvexPoly (
  CvArr*    img,
  CvPoint*    pts,
  int        npts,
  CvScalar    color,
  int        line_tyoe = 8
);

void cvPolyLine (
  CvArr*    img,
  CvPoint**    pts,
  int*        npts,
  int        contours,
  int        is_closed,
  CvScalar    color,
  int        thickness = 1,
  int        line_type = 8
);

这三个函数都可以绘制多边形,思路也基本相同,主要区别是如何描述点:

在cvFillPolly( )中,点是由CvPoint数组提供的。它允许cvFillPoly( )在一次调用中绘制多个多边形。同样的,npts是由计数点构成的数组,与多边形对应。如果把变量is_closed设为ture,那么下一个多边形的第一个线段就会从上一多边形最后一个点开始。cvFillPoly很稳定,可以处理自相交多边形,有孔的多边形等复杂问题。然而缺点是函数运行起来相对缓慢。

cvFillConvexPoly( )和cvFillPoly( )类似。不同的是,它一次只能画一个多边形,而且只能画凸多边形。优点是函数运行速度快。

第三个cvPolyLine( ),七参数与cvFillPoly( )基本相同,但因为只需画出多边形的边,不需处理相交情况。因此,函数运行速度远远超过cvFillPoly( )。

5. 绘制文字

OpenCV有一个主要的函数,叫cvPutText( ),这个函数可以在图像上输出一些文本。函数原型:

void cvPutText (
  CvArr*    img,
  const        text,
  CvPoint    origin,
  const CvFont*    font,
  CvScalar    color
);

参数text所指向的文本将打印到图像上。

参数origin指定文本框左下角位置。

参数color指定文本颜色。

要获取CvFont*指针需要调用函数cvInitFont( )函数。函数原型:

void cvInitFont (
  CvFont*    font,
  int        font_face,
  double    hscale,
  double    vscale,
  double    shear = 0,
  int        thickness = 1,
  int        line_type = 8
);

该函数采用一组参数配置一些用于屏幕输出的基本特定字体。为了建立一个可以传值给cvPutText( )的cvFont,首先必须声明一个CvFont变量,然后把它传递给cvInitFont( )。

font_face是选择字体,且该标志可以和CV_FONT_ITALIC(字体标志)组合使用(使用与或)。以下是全部可用的字体:

标志名称 描述
CV_FONT_HERSHEY_SIMPLEX 正常尺寸的sanserif字体
CV_FONT_HERSHEY_PLAIN 小尺寸的sanserif字体
CV_FONT_HERSHEY_DUPLEX 正常尺寸的sanserif字体,但比CV_FONT_HERSHEY_SIMPLEX 复杂
CV_FONT_HERSHEY_COMPLEX 正常尺寸serif,比CV_FONT_HERSHEY_DUPLEX更复杂
CV_FONT_HERSHEY_TRIPLEX 正常尺寸serif,比CV_FONT_HERSHEY_COMPLEX更复杂
CV_FONT_HERSHEY_COMPLEX_SMALL 小尺寸的CV_FONT_HERSHEY_COMPLEX
CV_FONT_HERSHEY_SCRIPT_SIMPLEX 手写风格
CV_FONT_HERSHEY_SCRIPT_COMPLEX 比CV_FONT_HERSHEY_SCRIPT_SIMPLEX更复杂

hscale和vscale只能设定为1.0或0.5,。字体渲染时选择全高或半高(宽度同比缩放),绘制效果与指定字体的基本定义有关。

参数shear创建斜体字,如果设置为0.0,字体不倾斜。当设置为1.0时,字体倾斜范围接近45°。

其他两个参数与以前相同。

下面用一个例子来说明前面介绍的知识:

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{
    IplImage *image = cvCreateImage(cvSize(800, 600), 8, 3);

    // Draw Line
    cvLine(image, cvPoint(10, 10), cvPoint(500, 20),
            cvScalar(255), 2, 8, 1);

    // Draw Circle
    cvCircle(image, cvPoint(100, 100), 50, cvScalar(0, 255, 0), 2, 8, 0);

    // Draw Ellipse
    cvEllipse(image, cvPoint(400, 150),
        cvSize(100, 150), 45, 0, 360, cvScalar(0, 0, 255), 2);

    cvEllipse(image, cvPoint(600, 150),
        cvSize(100, 150), 0, 0, 360, CV_RGB(0, 0, 255), 2);

    // Draw Text
    CvFont  font1, font2, font3, font4, font5, font6, font7, font8;
    char* text = "Time Track";
    cvInitFont(&font1, CV_FONT_HERSHEY_COMPLEX, 1, 1, 0);
    cvPutText(image, text, cvPoint(100, 200), &font1, CV_RGB(0, 0, 0));

    cvInitFont(&font2, CV_FONT_HERSHEY_PLAIN, 1, 0.5, 0.2);
    cvPutText(image, text, cvPoint(100, 250), &font2, CV_RGB(0, 0, 0));

    cvInitFont(&font3, CV_FONT_HERSHEY_DUPLEX, 0.5, 1, 0.4);
    cvPutText(image, text, cvPoint(100, 300), &font3, CV_RGB(0, 0, 0));

    cvInitFont(&font4, CV_FONT_HERSHEY_COMPLEX, 0.5, 1, 0.6);
    cvPutText(image, text, cvPoint(100, 350), &font4, CV_RGB(0, 0, 0));

    cvInitFont(&font5, CV_FONT_HERSHEY_TRIPLEX | CV_FONT_ITALIC, 1, 1, 0.8);
    cvPutText(image, text, cvPoint(100, 400), &font5, CV_RGB(0, 0, 0));

    cvInitFont(&font6, CV_FONT_HERSHEY_COMPLEX_SMALL, 1, 1, 1);
    cvPutText(image, text, cvPoint(100, 450), &font6, CV_RGB(0, 0, 0));

    cvInitFont(&font7, CV_FONT_HERSHEY_SCRIPT_SIMPLEX, 1, 1, 2);
    cvPutText(image, text, cvPoint(100, 500), &font7, CV_RGB(0, 0, 0));

    cvInitFont(&font8, CV_FONT_HERSHEY_SCRIPT_COMPLEX, 1, 1, 3);
    cvPutText(image, text, cvPoint(200, 550), &font8, CV_RGB(0, 0, 0));

    cvNamedWindow("Display");
    cvShowImage("Display", image);

    cvWaitKey();
    return 0;
}
]]>
1 http://niyanchun.com/opencv-notes-6.html#comments http://niyanchun.com/feed/tag/opencv/
OpenCV学习笔记(5)——ROI http://niyanchun.com/opencv-notes-5.html http://niyanchun.com/opencv-notes-5.html Thu, 10 Apr 2014 18:58:00 +0800 NYC 在以前介绍IplImage结构的时候,有一个重要的参数——ROI。ROI全称是"Region Of Interest”,即感兴趣的区域。实际上,它是IPL/IPP(这两个是Inter的库)结构IplROI的实例。IplROI包含xOffset、yOffset、height、width和coi成员变量。其中COI代表channel of interest(感兴趣的通道)。ROI的思想是:一旦设定ROI,通常组用于整幅图像的函数便只会对ROI所表示的子图像进行操作。如果COI被设置非0值,则对该图像的操作就只作用于被指定的通道上了。这个COI变量可取的值是1、2、3、4通道,并且为了使COI无效而保留了0取值(有点像忽略)。但是,许多OpenCV函数都忽略参数COI。

ROI在实际工作中有很重要的作用,在许多情况下,使用它会提高计算机视觉代码的执行速度。这是因为他们允许对图像的某一部分进行操作,而不是对整个图像进行操作。在OpenCV中,普遍支持ROI,函数的操作被限于感兴趣的区域。要设置或取消ROI,就要使用 cvSetImageROI( )和cvResetImageROI( )函数。如果设置ROI,可以使用函数 cvSetImageROI( ),并为其传递一个图像指针和矩形。要取消ROI,只需要为函数 cvResetImageROI( )传递一个图像指针。

void cvSetImageROI(IplImage* image, CvRect rect);
void cvResetImageROI(IplImage* image);

下面用一个例子来来说明ROI的用法。例子中:我们加载一幅图像并修改一些区域——设置ROI的x,y,width和height的值,最后将ROI区域中像素都加上一个整数。代码如下:

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{
	IplImage	*src;

	src = cvLoadImage("E:OpenCVimagescat.jpg");

	int x = 90;
	int y = 110;
	int width = 180;
	int height = 180;
	int add = 150;

	cvSetImageROI(src, cvRect(x, y, width, height));
	cvAddS(src, cvScalar(add), src);
	cvResetImageROI(src);

	cvNamedWindow("Roi_Add", 1);
	cvShowImage("Roi_Add", src);

	cvWaitKey();
	return 0;
}

上述代码把ROI集中于猫的脸部,并将其蓝色通道增加150。

 

最后有两点需要说明:

1. cvAddS( )函数,该函数用于实现一个数组和一个标量的元素级的相加运算。例子中通过cvScalar( )构造函数将CvScalar的第一个分量设置为add代表的值,其他值为默认值0,然后和src里面的元素相加。那为什么是蓝色通道增加150,而不是红色通道呢?因为RGB图在内存中的排列顺序是:BGRA BGRA BGRA …。所以,实际上第一个通道是B(blue)。该函数其实还有第四个参数(默认为NULL),其代表的含义可参考OpenCV文档。

2. 显示图像之前的" cvResetImageROI(src); ”这一句代码千万不能少。如果没有这行代码,那接下去显示图像的时候,只会显示ROI区域。因为前面已经说过,设置了ROI区域后,OpenCV的函数只会对ROI区域有效。

 

]]>
0 http://niyanchun.com/opencv-notes-5.html#comments http://niyanchun.com/feed/tag/opencv/
OpenCV学习笔记(4)——滑动条 http://niyanchun.com/opencv-notes-4.html http://niyanchun.com/opencv-notes-4.html Thu, 10 Apr 2014 17:48:00 +0800 NYC 滑动条是我们在OpenCV中经常使用的一个控件,HighGUI提供了滑动条的实现,在OpenCV中滑动条称为trackbar。创建滑动条的函数(这里以C接口为例)cvCreateTrackbar( ),函数原型如下:

int cvCreateTrackbar(
  const char* trackbar_name,
  const char* window_name,
  int* value,
  int count,
  CvTrackbarCallback on_change
);

前两个参数分别指定了滑动条的名字以及滑动条附属窗口的名字。当滑动条被创建后,会位于窗口的顶部或者底部(由操作系统决定)。滑动条不会遮挡窗口中的图像。

第三个参数value是一个整数指针。当滑动条被拖动时,OpenCV会自动将当前位置所代表的值传递给指针指向的整数。第四个参数count是一个整数数值,为滑动条所能表示的最大值。

最后一个参数是一个指向回调函数的指针。当滑动条被拖动时,回调函数会自动被调用。回调函数必须为CvTrackbarCallback格式,即:

void (*callback) (int position)

不过,这个回调函数不是必须的,所以如果不需要一个回调函数,可以将参数设置为NULL(默认值就是NULL)。没有回调函数,当滑动条被拖动时,唯一的影响就是改变指针value所指向的整数值。

HighGUI还提供了两个函数分别用来读取和设置滚动条的value值,不过前提是必须知道滑动条的名称。

int cvGetTrackbarPos(
  const char* trackbar_name,
  const char* window_name
);

void cvSetTrackbarPos(
  const char* trackbar_name,
  const char* window_name,
  int	      pos
);

下面用对《学习OpenCV》书上一个简单的例子进行改进来说明滚动条的具体应用:用滑动条模拟按钮。

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>

using namespace cv;
using namespace std;

int g_switch_value = 0;

//callback function
void switch_callback(int postion);

void switch_off_funtion();
void switch_on_function();

int main(int argc, char** argv)
{
	cvNamedWindow("Demo Window", 1);
	cvCreateTrackbar(
			"Switch",
			"Demo Window",
			&g_switch_value,
			1,
			switch_callback
			);

	while (1)
	{
		if (cvWaitKey(15) == 27)
		{
			break;
		}
	}
}



void switch_callback(int postion)
{
	if (postion == 0)
	{
		switch_off_funtion();
	}
	else
	{
		switch_on_function();
	}
}

void switch_on_function()
{
	printf("Switch is On!n");
}

void switch_off_funtion()
{
	printf("Switch is off!n");
}
]]>
0 http://niyanchun.com/opencv-notes-4.html#comments http://niyanchun.com/feed/tag/opencv/
OpenCV学习笔记(3)——基本数据类型 http://niyanchun.com/opencv-notes-3.html http://niyanchun.com/opencv-notes-3.html Thu, 10 Apr 2014 16:49:00 +0800 NYC OpenCV提供了许多的基本数据类型,这里学习一些比较常用的数据类型。C接口的基本都是用结构体或联合体来实现基本数据类型,C++接口的话使用模板来实现,但对于使用者而言,其实都是一样的。这里以C接口的为例介绍。

1. CvPoint以及其变体

这单个结构都非常简单,用于表示二维或三维的点(确切的说是点的坐标,通常坐标都是以0为基点),既有表示整数也有表示浮点型的。他们的定义分别如下:

typedef struct CvPoint
{
  int x;    // X坐标
  int y;    // Y坐标
}
CvPoint;

typedef struct CvPoint2D32f
{
  float x;    // X坐标
  float y;    // Y坐标
}
CvPoint2D32f;

typedef struct CvPoint3D32f
{
  float x;    // X坐标
  float y;    // Y坐标
  float z;    // Z坐标
}
CvPoint3D32f;

所有的OpenCV的数据类型都具有以其名称来定义的构造函数,例如cvPoint( )。(通常构造函数具有和结构类型一样的名称,只是首字母不大写)。因为C中没有类的概念,所以构造函数其实都是由内联函数实现的。比如上面介绍的3种类型都有如下构造函数:

inline CvPoint cvPoint(int x, int y);
inline CvPoint2D32f cvPoint2D32f(double x, double y);
inline CvPoint3D32f cvPoint3D32f(double x, double y, double z);

/*
 * 除了基本的构造函数以外,往往还可以通过一些转换函数进行构造
 */
inline CvPoint cvPointFrom32f(CvPoint2D32f point);
inline CvPoint2D32f cvPointTo32f(CvPoint point);

2. CvSzie以及其变体

这三个结构都和矩形有关系,前两个结构都用来表示矩形框的大小,第一个以像素为精度,第二个以亚像素为精度。CvRect结构用于表示矩形框的偏移和大小。他们的定义及其构造函数如下:

typedef struct CvSize
{
  int width;    // 矩形宽
  int height;    // 矩形高
}
CvSize;

inline CvSize cvSize(int width, int height)
{
  CvSize s;

  s.width = width;
  s.height = height;

  return s;
}

typedef struct CvSize2D32f
{
  float width;    // 矩形宽
  float height; // 矩形高
}
CvSize2D32f;

inline CvSize2D32f cvSize2D32f(double width, double height)
{
  CvSize2D32f s;

  s.width = (float)width;
  s.height = (float)height;

  return s;
}

typedef struct CvRect
{
  int x;    // 方形的最左角的X坐标
  int y;    // 方形的最上或者最下角的Y坐标
  int width;    // 宽
  int height;    // 高
}
CvRect;

inline CvRect cvRect(int x, int y, int width, int height)
{
  CvRect os;

  os.x = x;
  os.y = y;
  os.width = width;
  os.height = heigth;

  return os;
}

3. CvScalar

这个结构也比较常用,它可以用来表示一个1、2、3、4元组(比如表示RGAB值)。定义如下:

typedef struct CvScalar
{
  double val[4];
}
CvScalar;

这个结构有3个构造函数:

/*
 * 第一个:cvScalar( )。他需要1~4个参数,如果参数不足4个,其他量默认为0。
 */
inline CvScalar cvScalar(double val0, double val1, double val2, double val3)
{
  CvScalar scalar;

  scalar.val[0] = val0;    scalar.val[1] = val1;
  scalar.val[2] = val2; scalar.val[3] = val3;

  return scalar;
}

/*
 * 第二个:cvRealScalar().它只需要一个参数,用于初始化val[0],其他值设为0.
 */
inline CvScalar cvRealScalar(double val0)
{
  CvScalar scalar;

  scalar.val[0] = val0;    scalar.val[1] = 0;
  scalar.val[2] = 0; scalar.val[3] = 0;

  return scalar;
}

/*
 * 第三个:cvScalarAll(). 他需要一个参数,并且将val[]数组的4个元素都设为此值。
 */
inline CvScalar cvRealScalar(double val)
{
  CvScalar scalar;

  scalar.val[0] = val; scalar.val[1] = val;
  scalar.val[2] = val; scalar.val[3] = val;

  return scalar;
}

更多的数据类型可以参考OpenCV官方文档:http://docs.opencv.org/modules/core/doc/core.html

]]>
0 http://niyanchun.com/opencv-notes-3.html#comments http://niyanchun.com/feed/tag/opencv/