2020年度卒研セミナー(2020/07/23)

関連サイトと資料

卒研関連

2020年8月6日(木)の卒研セミナーの時間に予定していた「各自の関心のあることについてのプレゼン」は、翌週8月13日(木)に延期したいと思います。よろしいでしょうか?

その他

4.Analyzing Video Streams on the Raspberry Pi

この章では、関数型プログラミングの概念を使用してビデオストリームを分析する方法を学びます。 具体的には、Filterインターフェースを使用してそれをPipelineオブジェクトと組み合わせ、それらをビデオストリームに適用します。

フィルターの概要から始めます。 次に、さまざまな基本的な楽しいフィルターを見ていきます。 次に、さまざまな視覚技術を使用したオブジェクトの検出に移ります。 最後に、ニューラルネットワークについて説明します。

Overview of Applying Filters

Clojure言語では、追加のボイラープレートコードを使用せずに、ビデオストリームのMatオブジェクトに一連の変換を直接適用できます。 READMEで入手できる最も基本的なorigamiの例を確認することを強くお勧めします。

https://github.com/hellonico/origami/blob/master/README.md#support-for-opencv-412-is-in

リスト4-1は、画像をロードして灰色に変更し、1つのパイプラインですべての関数を適用する方法を示しています。 これは、OpenCVのClojureバージョンを試してみたいと思うかもしれません。

リスト4-1
(require
  '[opencv4.utils :as u]
  '[opencv4.core :refer :all])
(->
  (imread "doc/cat_in_bowl.jpeg")
  (cvt-color! COLOR_RGB2GRAY)
  (canny! 300.0 100.0 3 true)
  (bitwise-not!)
  (u/resize-by 0.5)
  (imwrite "doc/canny-cat.jpg"))
    

ただし、これはJavaの本なので、Javaで同じ概念を適用する方法を見てみましょう。

ここでは、各フィルターがMatオブジェクトに対して1つの操作を実行するフィルターのパイプラインの概念を紹介します。

フィルターでできることの例をいくつか示します。

以下は、これらの概念を実装するために導入する2つのJavaタイプです。

リスト4-2は、(単純な)Filterインターフェースを示しています。

リスト4-2
import org.opencv.core.Mat;
  
public interface Filter {
  public Mat apply(Mat in);
}
    

リスト4-3は、Pipelineクラスの実装例を示しています。ここでは、さまざまなフィルターを1つずつ呼び出すことで、それらを組み合わせるだけです。

リスト4-3
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
  
import org.opencv.core.Mat;
  
public class Pipeline implements Filter {
  List filters;
  
  public Pipeline(Class... __filters) {
    List<Class<Filter>> _filters = (List) Arrays.asList(__filters);
    this.filters = _filters.stream().map(i -> {
      try {
        return (Filter) Class.forName(i.getName()).newInstance();
      } catch (Exception e) {
        return null;
      }
    }).collect(Collectors.toList());
  }
   
  public Pipeline(Filter... __filters) {
    this.filters = (List) Arrays.asList(__filters);
  }
   
  @Override
  public Mat apply(Mat in) {
    Mat dst = in.clone();
    for (Filter f : filters) {
      dst = f.apply(dst);
    }
    return dst;
  }
}
    

ここでは、クラスで直接使用されるnewInstance関数の非推奨バージョンの使用法に注意してください。 これは、あなたが本当に望んでいるコンストラクタを呼び出すわけではないかもしれませんが、この本の例では十分に機能します。

もちろん、これまでのところ、フィルターとパイプラインはそれほど多くは行っていません。 次のセクションでいくつかの基本的な例を確認しましょう。

Applying Basic Filters

このセクションでは、基本的なフィルターの例をいくつか見ていきます。

Gray Filter

フィルターの最も明らかな使用法は、Matオブジェクトを色から灰色に変えることです。 OpenCVでは、これはImgprocクラスの関数cvtColorを使用して行われます。

パッケージ定義を省略して、数ページ前のWebcamコードを、 最近導入されたFilterインターフェイスを実装するクラスの標準cvtColorラッパーと組み合わせます(リスト4-4)。

リスト4-4
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
  
import origami.ImShow;
import origami.Origami;
   
public class WebcamWithFilters {
  
  public static void main(final String[] args) {
    Origami.init();
  
    final VideoCapture cap = new VideoCapture(0);
    final ImShow ims = new ImShow("Camera", 800, 600);
    final Mat buffer = new Mat();
    Filter gray = new Gray();
    while (cap.read(buffer)) {
      ims.showImage(gray.apply(buffer));
    }
    cap.release();
  }
}
   
class Gray implements Filter {
  
  public Mat apply(final Mat img) {
    final Mat mat1 = new Mat();
    Imgproc.cvtColor(img, mat1, Imgproc.COLOR_RGB2GRAY);
    return mat1;
  }
}
    

これをPiで直接実行できます。Webカメラの前に立っていると、図4-1のようになります。

Edge Preserving Filter

同様に、OpenCVのPhotoクラスのEdgePreserving関数のラッパーを実装できます。 これは多くの異なるアプリケーションで使用され、画像内の不要な線を滑らかにして削除します。 たとえば、リスト4-5は実際には関数edgePreservingFilterの基本的な呼び出しにすぎません。

リスト4-5
import org.opencv.photo.Photo;
   
class EdgePreserving implements Filter {
  public int flags = Photo.RECURS_FILTER;
  // int flags = NORMCONV_FILTER;
  public float sigma_s = 60;
  public float sigma_r = 0.4f;
  
  public Mat apply(Mat in) {
    Mat dst = new Mat();
    Photo.edgePreservingFilter(in, dst, flags, sigma_s, sigma_r);
    return dst;
  }
}
    

リスト4-6に示すように、WebcamWithFiltersのメインメソッドを変更することにより、この新しいフィルターを使用できます。

リスト4-6
Filter filter = new EdgePreserving();
while (cap.read(buffer)) {
  ims.showImage(filter.apply(buffer));
}
    

次に、新しいコードの実行に移りましょう。 繰り返しますが、ウェブカメラに直面している場合は、図4-2のようになります。

Canny

OpenCVの世界でのもう1つの便利なフィルターは、Cannyエフェクトを適用することです。 これは、Matオブジェクトの輪郭と形状をすばやく効果的に見つける方法です。 フィルターとしてのCannyの簡単な実装をリスト4-7に示します。

リスト4-7
class Canny implements Filter {
  public boolean inverted = true;
  public int threshold1 = 100;
  public int threshold2 = 200;

  @Override
  public Mat apply(Mat in) {
    Mat dst = new Mat();
    Imgproc.Canny(in, dst, threshold1, threshold2);
    if (inverted) {
      Core.bitwise_not(dst, dst, new Mat());
    }
    Imgproc.cvtColor(dst, dst, Imgproc.COLOR_GRAY2RGB);
    return dst;
  }
}
    

図4-3は、メインループにキャニーフィルターを適用した結果を示しています。

Debugging (Again)

ここで、デバッグに関するいくつかの注意事項を再度示します。 WebcamWithFiltersのメインキャプチャループにブレークポイントを追加すると、フィルターのさまざまなフィールドすべてにアクセスできます。 図4-4に示すように、反転したブール値の値をfalseに変更してみましょう。

次に、ブレークポイントを削除して、コード実行を通常どおり再開します。 図4-5は、フィルターの値をすぐに変更すると、 実行中のコードと画面に表示されているMatオブジェクトの色がどのように変化したかを示しています。

独自のフィルターを実装するときは、最も影響力のある変数をクラスのフィールドとして保持することをお勧めします。 これにより、値が殺到することなく、重要な変数にアクセスできるようになります。

Combining Filters

前のセクションで、各フィルターのパフォーマンスを確認する必要があることに気付いたと思います。

実際にパフォーマンスは、ビデオファイルから、または直接Webカメラデバイスから読み取るときに処理できる1秒あたりのフレーム数に起因します。

ここでは、次のことを行います。

グレイフィルターのコードは既にあるので、フレームレート/秒(FPS)を表示するコードに直接移動します。

VideoCaptureのOpenCVの一連のプロパティを使用して、フレームレートの値にアクセスできるといつも思っていました。 残念ながら、ほとんどの場合、画面に実際に表示されている値ではなく、ハードコードされた値で止まっています。

したがって、リスト4-8のFPSの実装は、フィルターの有効期間の開始以降に表示されたフレームの数に基づいていくつかの単純な計算を行う小さな回避策です。

最後に、putTextを使用してテキストを直接フレームに適用します。 単純なユースケースでは十分に機能します。

リスト4-8
class FPS implements Filter {
  
  long start = System.currentTimeMillis();
  int count = 0;
  
  Point org = new Point(50, 50);
  int fontFace = Imgproc.FONT_HERSHEY_PLAIN;
  double fontScale = 4.0;
  Scalar color = new Scalar(0, 0, 0);
  int thickness = 3;
  
  public Mat apply(Mat in) {
    count++;
    String text = "FPS: " + count / (1 + ((System.
    currentTimeMillis() - start) / 1000));
    Imgproc.putText(in, text, org, fontFace, fontScale, color, thickness);
    return in;
  }
}
    

次に、ストリームに表示されているFPSとグレイフィルターを組み合わせることに戻ります。

以下に示すように、WebcamWithFiltersのmain()関数で更新する必要があるのは、フィルターがインスタンス化される行だけであることを知って満足します。

Raspberry Piからサンプルを再度実行すると、図4-6に示すようなものが表示されます。 2つのフィルターを一緒に適用すると、Raspberry Piでは通常、毎秒約15フレームが得られます。

Applying Instagram-like Filters

今のところ十分な真剣な仕事。 少し休憩して、Instagramのようなフィルタを少し楽しんでみましょう。

Color Map

ImgProcのOpenCVのカラーマップ関数を使用して、この楽しいセクションを始めましょう。 リスト4-9に示すように、カラーマップのパラメーターをコンストラクターに移動して、デバッグ画面から更新できるようにします。

リスト4-9
class Color implements Filter {
  
  int colormap = 0;
  
  public Color(int colormap) {
    this.colormap = colormap;
  }
  
  public Color() {
    this.colormap = Imgproc.COLORMAP_INFERNO;
  }
  
  public Mat apply(Mat img) {
    Mat threshed = new Mat();
    Imgproc.applyColorMap(img, threshed, colormap);
    return threshed;
  }
}
    

フィルターをインスタンス化するには、使用するカラーマップを渡す必要があるため、これはコンストラクターで直接行われます。 ここでは、クラスだけでなくインスタンス化されたフィルターが必要なので、 インスタンス化されたFilterオブジェクトをコンストラクターに渡して、 Pipelineクラスの2番目のコンストラクターを使用します。

これを実行すると、図4-7のようになります。

Thresh

Threshは、Imgprocのしきい値関数を適用して作成されたもう1つの楽しいフィルターです。 固定レベルのしきい値をMatオブジェクトの各配列要素に適用します。

スレッシュフィルターの本来の目的は、不要な要素を削除して画像のノイズを除去するなど、 画像の要素をセグメント化することでした。 これは通常、Instagramのフィルタリングには使用されませんが、 見栄えがよく、クリエイティブなアイデアを得ることができます。

リスト4-10は、Threshフィルターの実装方法を示しています。

リスト4-10
class Thresh implements Filter {
  
  int sensitivity = 100;
  int maxVal = 255;
  
  public Thresh() {
  }
  
  public Thresh(int _sensitivity) {
    this.sensitivity = _sensitivity;
  }
  
  public Mat apply(Mat img) {
    Mat threshed = new Mat();
    Imgproc.threshold(img, threshed, sensitivity, maxVal, Imgproc.THRESH_BINARY);
    return threshed;
  }
}
    

図4-8に結果を示します。

Sepia

リスト4-11で実装されているように、ここでも古い(しゃれた)セピア効果を使用します。

リスト4-11
class Sepia implements Filter {
  public Mat apply(Mat source) {
    Mat kernel = new Mat(3, 3, CvType.CV_32F);
    kernel.put(0, 0,
      0.272, 0.534, 0.131,
      0.349, 0.686, 0.168,
      0.393, 0.769, 0.189);
    Mat destination = new Mat();
    Core.transform(source, destination, kernel);
    return destination;
  }
}
    

ビデオストリームで使用すると、セピア効果は図4-9のような出力を提供します。

Cartoon

このカートゥーンエフェクトの素朴な実装は、ベースピクチャの重要な機能を定義する線を取り、 スムージングエフェクトとブラーエフェクトを適用した後、各ピクセル値にしきい値を適用します。 次に、リスト4-12に示すように、これらの操作の結果を結合します。

リスト4-12
class Cartoon implements Filter {
  
  public int d = 17;
  public int sigmaColor = d;
  public int sigmaSpace = 7;
  public int ksize = 7;
  
  public double maxValue = 255;
  public int blockSize = 19;
  public int C = 2;
  
  public Mat apply(Mat inputFrame) {
    Mat gray = new Mat();
    Mat co = new Mat();
    Mat m = new Mat();
    Mat mOutputFrame = new Mat();
  
    Imgproc.cvtColor(inputFrame, gray, Imgproc. COLOR_BGR2GRAY);
    Imgproc.bilateralFilter(gray, co, d, sigmaColor, sigmaSpace);
    Mat blurred = new Mat();
    Imgproc.blur(co, blurred, new Size(ksize, ksize));
    Imgproc.adaptiveThreshold(blurred, blurred, maxValue, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, blockSize, C);
    Imgproc.cvtColor(blurred, m, Imgproc.COLOR_GRAY2BGR);
    Core.bitwise_and(inputFrame, m, mOutputFrame);
    return mOutputFrame;
  }
}
    

ビデオストリームにカートゥーンフィルターを適用すると、図4-10のような効果が得られます。

Pencil Effect

コアOpenCV PhotoクラスからpencilSketchメソッドを呼び出すことで得られる鉛筆効果が気に入っています。 残念ながら、Raspberry Piにリアルタイムで適用するには遅すぎる。 ただし、実装作業をほとんど行わなくても、かなりの結果が得られます。 リスト4-13をご覧ください。

リスト4-13
class PencilSketch implements Filter {
  float sigma_s = 60;
  float sigma_r = 0.07f;
  float shade_factor = 0.05f;
  boolean gray = false;
   
  @Override
  public Mat apply(Mat in) {
    Mat dst = new Mat();
    Mat dst2 = new Mat();
    pencilSketch(in, dst, dst2, sigma_s, sigma_r, shade_factor);
    return gray ? dst : dst2;
  }
}
    

この効果を適用すると、図4-11に示すような結果が得られます。

ウフー。 それは、すぐに使用してレジャーに適用できるかなりの数の効果でした。 折り紙リポジトリには他にもいくつか利用できますが、 もちろん自分で投稿することもできますが、ここでは深刻なオブジェクト検出に移りましょう。

Performing Object Detection

オブジェクト検出は、さまざまなプログラミングアルゴリズムを使用して画像内のオブジェクトを見つけるという概念です。 それは人間が長い間行ってきた仕事であり、それは脳のないコンピュータが行うことは非常に困難でした。 しかし、テクノロジーの進歩により、状況は最近変化しています。

この章のこのセクションでは、コンテンツに関する情報なしで、 画像内のオブジェクトを識別するためのさまざまなコンピュータービジョンテクニックを確認します。 具体的には、次の内容を確認します。

例は難易度のいくらか進歩的な順序で進んでいるので、このリストの順序で試してみることをお勧めします。

Removing the Background

背景の削除は、シーンから不要なアーティファクトを削除するために使用できる手法です。 検索しようとしているオブジェクトはおそらく静的ではなく、 一連の画像またはビデオストリームを移動している可能性があります。 アーティファクトを効率的に削除するには、アルゴリズムは2つのMatオブジェクトを区別し、 ある種の短期記憶を使用して(フォアグラウンドで)動いているものをバックグラウンドの標準のシーンオブジェクトから区別する必要があります。

OpenCVでは、使いやすい2つのBackgroundSubtractorクラスを使用できます。 彼らの紹介と完全な説明は、次のWebサイトにあります。 https://docs.opencv.org/master/d1/dc5/tutorial_background_subtraction.html

基本的には、背景減算器に与えるフレームを増やし、前景で動いているものと動いていないものを検出できます。

リスト4-14は簡単に理解できます。 減算クラスの適用関数を、フィルターインターフェイスにあるものと間違えないように注意してください。

リスト4-14
class BackgroundSubtractor implements Filter {
  boolean useMOG2 = true;
  BackgroundSubtractor backSub;
  double learningRate = 1.0;
  boolean showMask = true;
  
  public BackgroundSubtractor() {
    if (useMOG2) {
      backSub = Video.createBackgroundSubtractorMOG2();
    } else {
      backSub = Video.createBackgroundSubtractorKNN();
    }
  }
  
  @Override
  public Mat apply(Mat in) {
    Mat mask = new Mat();
    backSub.apply(in, mask);
    Mat result = new Mat();
    if (showMask) {
      Imgproc.cvtColor(mask, result, Imgproc. COLOR_GRAY2RGB);
      return result;
    } else {
      in.copyTo(result, mask);
      return result;
    }
  }
}
    

コンストラクターを呼び出すことでフィルターが読み込まれ、図4-12のような結果が得られます。

このフィルターを実行したら、KNNベースのBackgroundSubtractorに切り替えて、速度の違い(フレームレートを確認)と結果の精度を確認します。

Detecting by Contours

2番目に基本的なOpenCV機能は、画像内の輪郭を検出できるようにすることです。 輪郭抽出は、ImgprocクラスのfindContours関数を使用します。

通常、次の操作を最初に行うと、findContoursはより良い結果をもたらします。

これらの2つのステップは、リスト4-15に追加されています。 次に、関数zerosで作成された黒いMatオブジェクトに輪郭を描画します。

リスト4-15
class Contours implements Filter {
  private int threshold = 100;
  
  public Mat apply(Mat srcImage) {
    Mat cannyOutput = new Mat();
    Mat srcGray = new Mat();
    Imgproc.cvtColor(srcImage, srcGray, Imgproc.COLOR_BGR2GRAY);
    Imgproc.Canny(srcGray, cannyOutput, threshold, threshold * 2);
    List<MatOfPoint> contours = new ArrayList<>();
    Mat hierarchy = new Mat();
    Imgproc.findContours(cannyOutput, contours, hierarchy,
    Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);
  
    Mat drawing = Mat.zeros(cannyOutput.size(), CvType.CV_8UC3);
    for (int i = 0; i < contours.size(); i++) {
      Scalar color = new Scalar(256, 150, 0);
      Imgproc.drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, new Point());
    }
    return drawing;
  }
}
    

注意深い読者は、前のコードがPipelineクラスを使用しているため、次のように書かれた方が実際に優れていることに気付くでしょう。

Marcelのビデオに等高線フィルターを適用すると、芸術的な外観になります(図4-13)。

これがあなたのための演習です:灰色に変換し、キャニーフィルターを適用する2つのステップを削除してから、元の結果と比較してください。

Detecting by Color

OpenCVの画像またはマットオブジェクトは、通常、赤/緑/青(RGB)色空間にあります(正確には、OpenCVでは実際には青/緑/赤)。 これは、各ピクセルに各チャネルの値が割り当てられていると考えると簡単に理解できます。 これらのチャネルの可能な値を確認するには、次のサイトを確認してください。 https://www.rapidtables.com/web/color/RGB_Color.html

この色空間の問題は、明度とコントラストの量が色自体の量と混ざり合うことです。

Matオブジェクトで特定の色を探すとき、HSVという名前の色空間に切り替えます(色相、彩度、値)。 この色空間では、色は直接色相値に変換されます。

色相の値は通常、円柱の度数のように0〜360です。 OpenCVは、範囲を2で割った、わずかに異なるスキームを持っています(そのため、メモリ内のスペースが少なくて済みます)。 表4-1に、色相の値の範囲を示します。

リスト4-16は、色空間を変換し、inRangeを使用して目的の色の範囲のHue値をチェックします。 例の最後に、findContoursを使用して形状を適切に描画するための魔法を追加します。

リスト4-16
class ColorDetector implements Filter {
  
  Scalar minColor, maxColor;
  
  public ColorDetector(Scalar minColor, Scalar maxColor) {
    this.minColor = minColor;
    this.maxColor = maxColor;
  }
  
  @Override
  public Mat apply(Mat input) {
    Mat array255 = new Mat(input.height(), input.width(), CvType.CV_8UC1);
    array255.setTo(new Scalar(255));
    Mat distance = new Mat(input.height(), input.width(), CvType.CV_8UC1);
  
    List<Mat> lhsv = new ArrayList<Mat>(3);
    Mat circles = new Mat();
  
    Mat hsv_image = new Mat();
    Mat thresholded = new Mat();
    Mat thresholded2 = new Mat();
  
    Imgproc.cvtColor(input, hsv_image, Imgproc.COLOR_BGR2HSV);
    Core.inRange(hsv_image, minColor, maxColor, thresholded);
   
    Imgproc.erode(thresholded, thresholded, Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(8, 8)));
    Imgproc.dilate(thresholded, thresholded, Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(8, 8)));
   
    Core.split(hsv_image, lhsv);
    Mat S = lhsv.get(1);
    Mat V = lhsv.get(2);
    Core.subtract(array255, S, S);
    Core.subtract(array255, V, V);
    S.convertTo(S, CvType.CV_32F);
    V.convertTo(V, CvType.CV_32F);
    Core.magnitude(S, V, distance);
    Core.inRange(distance, new Scalar(0.0), new Scalar(200.0), thresholded2);
    Core.bitwise_and(thresholded, thresholded2, thresholded);
    Imgproc.GaussianBlur(thresholded, thresholded, new Size(9, 9), 0, 0);
    List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
    Imgproc.HoughCircles(thresholded, circles, Imgproc.CV_HOUGH_GRADIENT, 2, thresholded.height() / 8, 200, 100, 0, 0);
    Imgproc.findContours(thresholded, contours, thresholded2, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
    Imgproc.drawContours(input, contours, -2, new Scalar(10, 0, 0), 4);
  
    return input;
  }
}
  
class RedDetector extends ColorDetector {
  public RedDetector() {
    super(new Scalar(0, 100, 100), new Scalar(10, 255, 255));
  }
}
    

このフィルターをバラのビデオに適用した結果は、図4-14のようになります。

表4-1のHueの値を見ると、青色を検索するフィルターを実装することはそれほど難しくないことがわかります。 これはあなたのための演習として残されています。

Detecting by Haar

第1章で見たように、Haarベースの分類器を使用して、Matオブジェクト内のオブジェクトや人を識別できます。 コードは見たものとほとんど同じですが、探している図形の数とサイズにさらに重点が置かれています。

具体的には、次のコードは、2つのサイズをパラメーターとして使用して、 探しているオブジェクトの最小サイズと最大サイズを指定する方法を示しています。

そのため、リスト4-17に追加の主要なサンプル関数を使用して、 Haar分類器検出のパラメーターとしてさまざまなXMLファイルを使用する方法を示します。

リスト4-17
public class DetectWithHaar {
  public static void main(String[] args) {
    Origami.init();
    
    VideoCapture cap = new VideoCapture(0);
    
    Mat buffer = new Mat();
    ImShow ims = new ImShow("Camera", 800, 600);
    Filter filter = new Pipeline(new Haar("haarcascades/haarcascade_frontalface_default.xml"), new FPS());
    while (cap.grab()) {
      cap.retrieve(buffer);
      ims.showImage(filter.apply(buffer));
    }
    cap.release();
  }
}
   
class Haar implements Filter {
  
  private CascadeClassifier classifier;
  Scalar white = new Scalar(255, 255, 255);
  
  public Haar(String path) {
    classifier = new CascadeClassifier(path);
  }
  
  public Mat apply(Mat input) {
    MatOfRect faces = new MatOfRect();
    classifier.detectMultiScale(input, faces, 1.1, 2, -1, new Size(100, 100), new Size(500, 500));
    for (Rect rect : faces.toArray()) {
      Imgproc.putText(input, "Face", new Point(rect.x, rect.y - 5), 3, 5, white);
      Imgproc.rectangle(input, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), white, 5);
    }
    return input;
  }
}
    

家に猫がいる場合、または例の猫のビデオを使用している場合、 これをWebカメラストリームに適用すると、図4-15のような結果が得られます。

サンプルには、Haarカスケード用の他のXMLファイルがあります。 エクササイズとして、人、目、笑顔を検出するのに自由に使用してください。

Transparent Overlay on Detection

前の例で長方形を描くとき、 検出された形状に長方形以外のものを描くことが可能かどうか疑問に思ったかもしれません。

リスト4-18は、検出された形状の位置にオーバーレイされるマスクをロードすることによってこれを行う方法を示しています。 これは、スマートフォンアプリケーションで常に使用しているものとほぼ同じです。

ここで、drawTransparency関数の透明レイヤーにトリックがあることに注意してください。 オーバーレイのマスクは、読み込みフラグとしてIMREAD_UNCHANGEDを使用して読み込まれます。 これを使用しないと、透明レイヤーが失われます。

透明レイヤーを作成したら、それをマスクとして使用して、オーバーレイをコピーし、 必要なMatオブジェクトの正確なピクセルをコピーします。

リスト4-18
class FunWithHaar implements Filter {
   
  CascadeClassifier classifier;
  Mat mask;
  Scalar white = new Scalar(255, 255, 255);
  
  public FunWithHaar(String path) {
    classifier = new CascadeClassifier(path);
    mask = Imgcodecs.imread("masquerade_mask.png",
    Imgcodecs.IMREAD_UNCHANGED);
  }
  
  void drawTransparency(Mat frame, Mat transp, int xPos, int yPos) {
    List<Mat> layers = new ArrayList<Mat>();
    Core.split(transp, layers);
    Mat mask = layers.remove(3);
    Core.merge(layers, transp);
    Mat submat = frame.submat(yPos, yPos + transp.rows(), xPos, xPos + transp.cols());
    transp.copyTo(submat, mask);
  }
  
  public Mat apply(Mat input) {
    MatOfRect faces = new MatOfRect();
    classifier.detectMultiScale(input, faces);
    Mat maskResized = new Mat();
    for (Rect rect : faces.toArray()) {
      Imgproc.resize(mask, maskResized, new Size(rect.width, rect.height));
      int adjusty = (int) (rect.y - rect.width * 0.2);
      try {
        drawTransparency(input, maskResized, rect.x, adjusty);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    return input;
  }
}
    

使用する透明なMatオブジェクトによっては、位置を調整する必要がある場合がありますが、 それ以外の場合は、図4-16のようなものが表示されます。 最後に、ビデオストリームにヴェネツィアのカーニバルの雰囲気を加えることができます。

私は実際に試してみましたが、 ビデオストリームへのオーバーレイとして使用する適切なバットマンマスクを見つけることができませんでした。 たぶん、使用する適切なマットオーバーレイ付きのコードを送ってくれるでしょう。

Detecting by Template Matching

OpenCVとのテンプレートマッチングは非常に単純です。 これは非常に単純なので、検出方法の順序の早いほうにあるはずです。 テンプレートマッチングとは、別のマット内でマットを探すことを意味します。 OpenCVには、これを行うmatchTemplateという超強力な関数があります。

リスト4-19は主にmatchTemplateの使用を中心に展開しています。 matchTemplateから受け取った結果でCore.minMaxLocの使用方法を探します。 これは、最高のスコアのインデックスを見つけるために使用され、 ニューラルネットワークを実行するときに再び使用されます。

リスト4-19
class Template implements Filter {
  Mat template;
  
  public Template(String path) {
    this.template = Imgcodecs.imread(path);
  }
  
  @Override
  public Mat apply(Mat in) {
    Mat outputImage = new Mat();
    Imgproc.matchTemplate(in, template, outputImage, Imgproc.TM_CCOEFF);
   
    MinMaxLocResult mmr = Core.minMaxLoc(outputImage);
    Point matchLoc = mmr.maxLoc;
   
    Imgproc.rectangle(in, matchLoc, new Point(matchLoc.x + template.cols(), matchLoc.y + template.rows()), new Scalar(255, 255, 255), 3);
    return in;
  }
}
    

では、次の章でこのスピーカーが必要になるため、図4-17に示すようなReSpeakerが含まれているボックスを見つけましょう。 今は見つかりません。 OpenCVを使用して探してみましょう。

図4-18に示すように、OpenCVのテンプレートマッチングによる検出は驚くほど高速で正確です。

図4-18でフレームレートを確認するのは少し難しいですが、Raspberry Pi 4では実際には毎秒約10〜15フレームです。

Detecting by Yolo

これは、この章で説明する最終的な検出方法です。 訓練されたニューラルネットワークを適用して、ストリーム内のオブジェクトを識別したいとします。 コンピューティング能力がほとんどないハードウェアでいくつかのテストを行った後、Yolo / Darknetと、 Cocoデータセットでトレーニングされた無料で入手可能なDarknetネットワークを使用して、非常に迅速な結果が得られました。

ランダム入力でニューラルネットワークを使用する利点は、 トレーニングされたネットワークのほとんどが非常に弾力性があり、 ほぼリアルタイムのストリームで80〜90%の精度で良好な結果が得られることです。

トレーニングは、ニューラルネットワークを使用する上で最も難しい部分です。 この本では、トレーニングではなく、Raspberry Piで検出コードを実行することに限定します。 Darknet / Yolo Webサイトで、ネットワークを再トレーニングするために写真を整理する方法の手順を見つけることができます。

OpenCVでDarknetを使用してオブジェクトを検出する一連の手順は次のとおりです。

  1. 構成ファイルと重みファイルからネットワークをロードします。
  2. これが結果になる場所なので、そのネットワークの出力レイヤー/ノードを見つけます。 出力層は、それ以上の出力層に接続されていない層です。
  3. Matオブジェクトをネットワークのblobに変換します。 blobは、 サイズやチャネルの順序などの点でネットワークが期待するフォーマットに一致するように調整された画像または画像のセットです。
  4. 次に、ネットワークを実行します。 つまり、ブロブにフィードし、出力レイヤーとしてマークされたレイヤーの値を取得します。
  5. 結果の各線について、実際に予想される認識可能な特徴のそれぞれについて信頼値を取得します。 Cocoでは、ネットワークは、人、自転車、車など、80の異なるオブジェクトを認識できるようにトレーニングされています。
  6. 次に、MinMaxLocResultを再度使用して、 最も可能性の高い認識されたオブジェクトのインデックスを取得し、 そのインデックスの値が0より大きい場合は、それを保持します。
  7. 各結果行の最初の4つの値は、 実際には検出されたオブジェクトが見つかったボックスを表す4つの値なので、 これらの4つの値を抽出し、長方形とそのラベルのインデックスを保持します。
  8. すべてのボックスを描画する前に、通常は重複するボックスを削除するNMSBoxも使用します。 ほとんどの場合、重なり合うボックスは、同じオブジェクトに対する同じポジティブ検出の複数のバージョンです。
  9. 最後に、残りの長方形を描画し、認識されたオブジェクトのラベルを追加します。

リスト4-20はフィルターとして実装されたベースのYoloDetectorの完全なコードを示しています。

リスト4-20
class YoloDetector implements Filter {
  final static Size sz = new Size(416, 416);
  List<String> outBlobNames;
  Net net;
  List<String> layers;
  List<String> labels;
  
  List<String> getOutputsNames(Net net) {
    List<String> layersNames = net.getLayerNames();
    return net.getUnconnectedOutLayers().toList().stream().map(i -> i - 1).map(layersNames::get).collect(Collectors.toList());
  }
  
  public YoloDetector(String modelWeights, String modelConfiguration) {
    net = Dnn.readNetFromDarknet(modelConfiguration, modelWeights);
    layers = getOutputsNames(net);
    try {
      labels = Files.readAllLines(Paths.get(LABEL_FILE));
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  @Override
  public Mat apply(Mat in) {
    findShapes(in);
    return in;
  }
  
  final int IN_WIDTH = 416;
  final int IN_HEIGHT = 416;
  final double IN_SCALE_FACTOR = 0.00392157;
  final int MAX_RESULTS = 20;
  final boolean SWAP_RGB = true;
  final String LABEL_FILE = "yolov3/coco.names";
  
  void findShapes(Mat frame) {
    Mat blob = Dnn.blobFromImage(frame, IN_SCALE_FACTOR, new Size(IN_WIDTH, IN_HEIGHT), new Scalar(0, 0, 0), SWAP_RGB);
    net.setInput(blob);
    List<Mat> outputs = new ArrayList<>();
  
    for (int i = 0; i < layers.size(); i++)
      outputs.add(new Mat());
    
    net.forward(outputs, layers);
    postprocess(frame, outputs);
  }
     
  private void postprocess(Mat frame, List<Mat> outs) {
    List<Rect> tmpLocations = new ArrayList<>();
    List<Integer> tmpClasses = new ArrayList<>();
    List<Float> tmpConfidences = new ArrayList<>();
  
    int w = frame.width();
    int h = frame.height();
   
    for (Mat out : outs) {
      final float[] data = new float[(int) out.total()];
      out.get(0, 0, data);
   
      int k = 0;
      for (int j = 0; j < out.height(); j++) {
        Mat scores = out.row(j).colRange(5, out.width());
        Core.MinMaxLocResult result = Core.minMaxLoc(scores);
        if (result.maxVal > 0) {
          float center_x = data[k + 0] * w;
          float center_y = data[k + 1] * h;
          float width = data[k + 2] * w;
          float height = data[k + 3] * h;
          float left = center_x - width / 2;
          float top = center_y - height / 2;
    
          tmpClasses.add((int) result.maxLoc.x);
          tmpConfidences.add((float) result.maxVal);
          tmpLocations.add(new Rect((int) left, (int)top, (int) width, (int) height));
        }
        k += out.width();
      }
    }
   
    annotateFrame(frame, tmpLocations, tmpClasses, tmpConfidences);
  }
  
  private void annotateFrame(Mat frame, List<Rect> tmpLocations, List<Integer> tmpClasses, List<Float> tmpConfidences) {
    MatOfRect locMat = new MatOfRect();
    MatOfFloat confidenceMat = new MatOfFloat();
    MatOfInt indexMat = new MatOfInt();
    locMat.fromList(tmpLocations);
    confidenceMat.fromList(tmpConfidences);
    Dnn.NMSBoxes(locMat, confidenceMat, 0.1f, 0.1f, indexMat);
   
    for (int i = 0; i < indexMat.total() && i < MAX_RESULTS; ++i) {
      int idx = (int) indexMat.get(i, 0)[0];
      int labelId = tmpClasses.get(idx);
      Rect box = tmpLocations.get(idx);
      String label = labels.get(labelId);
      annotateOne(frame, box, label);
    }
  }
    
  private void annotateOne(Mat frame, Rect box, String label) {
    Imgproc.rectangle(frame, box, new Scalar(0, 0, 0), 2);
  
    Imgproc.putText(frame, label, new Point(box.x, box.y),
    Imgproc.FONT_HERSHEY_PLAIN, 4.0, new Scalar(0, 0, 0), 3);
  }
}
    

これで、利用可能なさまざまなネットワークを使用して、独自のオブジェクト検出と実験のセットを実行できます。 リスト4-21は、主なYoloベースのネットワークのそれぞれをロードする方法を示しています。

リスト4-21
class Yolov2 extends YoloDetector {
  public Yolov2() {
    super("yolov2/yolov2.weights", "yolov2/yolov2.cfg");
  }
}
   
class TinyYolov2 extends YoloDetector {
  public TinyYolov2() {
    super("yolov2-tiny/yolov2-tiny.weights", "yolov2-tiny/yolov2-tiny.cfg");
  }
}
   
class Yolov3 extends YoloDetector {
  public Yolov3() {
    super("yolov3/yolov3.weights", "yolov3/yolov3.cfg");
  }
}
   
class TinyYolov3 extends YoloDetector {
  public TinyYolov3() {
    super("yolov3-tiny/yolov3-tiny.weights", "yolov3-tiny/yolov3-tiny.cfg");
  }
}
    

Raspberry Piで実行するYolo v3は、リスボンのにぎやかな通り(図4-19)やその他の猫(図4-20)で車や人を検出できます。


ご覧のとおり、標準のYolo v3のフレームレートは実際には非常に低かったです。 Yolo v3 Tinyでこの実験を試みると、実際には毎秒5〜6フレームに近くなる可能性があります。 これは、依然としてリアルタイムよりわずかに低いですが、それでも非常に高い精度で結果が得られます。 図4-21を参照してください。

これで、この章の最後の数行になりました。 RaspberryPiでJavaのOpenCVを使用したオブジェクト検出の概念のほとんどを見てきたのは、長いことです。

つまり、次のことを学びました。

次の章では、音声認識システムであるRhasspyを紹介し、 この章で説明する概念を結び付けて、 家庭、オフィス、または猫の家のオートメーションに適用します。