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

就活関連

関連サイトと資料

2.Object Detection in Video Streams

JavaでOpenCVを操作するための利用可能なハウツーガイドのほとんどは、 始める前に非常に多くの知識が必要です。 あなたにとって良いニュースは、この本でこれまでに学んだことで、 JavaでOpenCVを数秒で始めることができるということです。

Going Sepia: OpenCV Java Primer

セピアを選択することはすべて、イメージをロマンチックで理想的な外観にしようとすることと関係があります。 それは一種のプロパガンダのソフトバージョンです。 / マーティン・パー

このセクションでは、いくつかの基本的なOpenCVの概念を紹介します。 JavaでOpenCVを使用するために必要なファイルを追加する方法を学び、 画像のスムージング、ぼかし、または実際にセピア画像に変換するなど、 いくつかの単純なOpenCVアプリケーションで作業します。

A Few Files to Make Things Easier…

Visual Studio Codeは賢いですが、OpenCVライブラリに関連するコードを理解するには、 いくつかの指示が必要です。 これらの手順は、コードを実行するために含めるライブラリとバージョンを指定するプロジェクトのメタデータファイルに含まれています。

OpenCV / Javaのテンプレートが用意されているので、 開始するためには、ここにあるプロジェクトテンプレートを複製するだけです。

ここにあるzipファイルを使用できます。 https://github.com/hellonico/opencv-java-template/archive/master.zip

これにより、次に示すように、必要な最小限のファイルセットが得られます。

7つのディレクトリと3つのファイルがあります。 この設定は自動生成できるため、ここにリストされているテンプレートのコンテンツに焦点を当てましょう。

図2-1に示すように、Visual Studio Code内のプロジェクトテンプレートから最上位のフォルダーを開くことができます。

図2-1は2つのJavaファイルを示しているため、それぞれの内容を確認できます。 さらに、プロジェクトのレイアウトが拡張され、すべてのファイルが左側にリストされます。

このビューは、前の章でおなじみのはずです。 メイン関数の上部にある[実行]または[デバッグ]リンクを使用して、 HelloCv.javaコードの実行をすぐに試すことができます。

端末の出力は、OpenCV Matオブジェクトである次のコードのようになります。 簡単に言うと、これはサイズ3×3のマトリックスで、左上と右下の対角線に1が付いています。

[ 1, 0, 0;
0, 1, 0;
0, 0, 1]
    

リスト2-1 HelloCv.java
package hello;
  
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.scijava.nativelib.NativeLoader;
  
public class HelloCv {
  public static void main(String[] args) throws Exception {
    NativeLoader.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    Mat hello = Mat.eye(3, 3, CvType.CV_8UC1);
    System.out.println(hello.dump());
  }
}
    

コードを1行ずつ見て、舞台裏で何が起こっているかを理解しましょう。

最初に、次に示すように、NativeLoaderクラスを使用してネイティブライブラリをロードします。

NativeLoader.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    

OpenCVはJavaライブラリではないため、この手順が必要です。 これは、特に環境に合わせてコンパイルされたバイナリです。

通常、Javaでは、System.loadLibraryを使用してネイティブライブラリをロードしますが、これには次の2つが必要です。

この本では、ライブラリが自動的にダウンロードおよびロードされ、 途中でNativeLoaderが処理する場所にライブラリが配置される、いくつかのパッケージングマジックに依存します。 したがって、OpenCVプログラムのそれぞれの先頭にその1行を追加する以外、ここで行うことは何もありません。 メイン関数の一番上に置くのが最善です。

次に、プログラムの2行目に進みます。

Mat hello = Mat.eye(3, 3, CvType.CV_8UC1);
    

その2行目はMatオブジェクトを作成します。 数秒前に説明したように、Matオブジェクトは行列です。 すべての画像操作、すべてのビデオ処理、およびすべてのネットワーキングは、 そのMatオブジェクトを使用して行われます。 OpenCVがプログラムで実行している主なことは、最適化された行列、 つまりこのMatオブジェクトを操作するための優れたプログラミングインターフェイスを提案していると言っても、 それほど大きくはありません。

Visual Studio Codeでは、図2-2に示すように、オートコンプリート機能を使用して操作に直接アクセスできます。

ワンライナー内で、3×3マトリックスを作成し、マトリックス内の各要素の内部タイプはタイプCV_8UC1です。 8Uは8ビットの符号なしと考えることができ、C1はチャネル1と考えることができます。 これは、基本的に行列のセルごとに1つの整数を意味します。

タイプの命名体系は次のとおりです。

CV_<bit-depth>{U|S|F}C<number_of_channels>
    

Matで使用されるタイプを理解することは重要なので、表2-1に示されている最も一般的なタイプを必ず確認してください。

画像の各ピクセルは複数の値の組み合わせで記述できるため、マットのチャネル数も重要です。 たとえば、RGB(画像で最も一般的なチャネルの組み合わせ)では、1ピクセルあたり0〜256の3つの整数値があります。 1つの値は赤、1つは緑、もう1つは青です。

リスト2-2でご確認ください。50×50の青いマットが表示されています。

リスト2-2 HelloCv2.java
package hello;
   
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
//import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.scijava.nativelib.NativeLoader;
   
public class HelloCv2 {
  public static void main(String[] args) throws Exception {
    NativeLoader.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    Mat hello = Mat.eye(50, 50, CvType.CV_8UC3);
    hello.setTo(new Scalar(190, 119, 0));
   
    Imgcodecs.imwrite("sea_blue.png", hello);
  
    //HighGui.imshow("rgb", hello);
    //HighGui.waitKey();
    //System.exit(0);
  }
}
    

前のコードを実行すると、この50×50マットが得られます。 各ピクセルは3つのチャネル、つまり3つの値で構成されていますが、 OpenCVでは、RGB値が設計によって反転していることに注意してください(理由は、なぜですか?)。 実際にBGR。 青の値はコードの最初にあります。

したがって、関数setToを使用して設定されたすべてのピクセルの値は、B:190、G:119、Red:0です。

Matオブジェクトの操作は通常、org.opencv.core.Coreクラスを使用して行われます。 たとえば、2つのMatオブジェクトを加算するには、Core.add関数、2つの入力オブジェクト、すなわち2つのMatオブジェクト、 および加算結果を受け取るオブジェクト(別のMat)を使用します。

これを理解するには、1×1マットオブジェクトを使用できます。 2つのMatオブジェクトを加算すると、最初のMatと2番目のMatの値を加算したMatが得られることがわかります。 最後に、リスト2-3に示すように、結果はdestに格納されます。

リスト2-3
Mat hello = Mat.eye(1, 1, CvType.CV_8UC3);
hello.setTo(new Scalar(190, 119, 0));
   
Mat hello2 = Mat.eye(1, 1, CvType.CV_8UC3);
hello2.setTo(new Scalar(0, 0, 100));
   
Mat dest = new Mat();
Core.add(hello, hello2, dest);
   
System.out.println(dest.dump());
    

結果は、次の1×1行列destに示されています。 混乱しないでください。 これは、チャネルごとに1つ、3つの値を持つ1ピクセルのみです。

[190, 119, 100]
    

OpenCV Primer 2: Loading, Resizing, and Adding Pictures

非常に小さなマットオブジェクトを加算する方法を確認しましたが、 2つの画像、具体的には2つの大きなマットオブジェクトを加算する場合の動作を見てみましょう。

私の妹はMarcelという名前の美しい猫を飼っています。 彼女はこの本で使用するMarcelの写真をいくつか提供することに非常にうまく同意しました。 図2-5は、昼寝をしているマルセルを示しています。

私はビーチでマルセルを見たことがありませんが、海の近くで彼を撮影したいと思います。

OpenCVでは、マルセルの写真を撮って、ビーチの写真に追加することで、これを実現できます(図2-6)。

これらの2つの画像を適切に加算すると、最初は少しだけ作業が必要になりますが、OpenCVのしくみを理解するのに役立ちます。

Simple Addition

画像の読み込みは、Imgcodecクラスのimread関数を使用して行われます。 パスを指定してimreadを呼び出すと、これまで操作してきたのと同じ種類のオブジェクトであるMatオブジェクトが返されます。 リスト2-4に示すように、2つの行列(marcel行列とbeacg行列)の加算は、 前に使用したのと同じCore.add関数を使用するのと同じくらい簡単です。

リスト2-4
Mat marcel = Imgcodecs.imread("marcel.jpg");
Mat beach = Imgcodecs.imread("beach.jpeg");
Mat dest = new Mat();
Core.add(marcel, beach, dest);
    

ただし、コードを初めて実行すると、次のように不明瞭なエラーメッセージが返されます。

怖がらないでください。 何が起こっているのかをデバッガーですばやく確認し、適切な場所にブレークポイントを追加しましょう。 図2-7に示すように、[変数]タブのMatオブジェクトを参照してください。

ブレークポイントを追加する前に、marcel行列が実際に2304×1728であるのに対し、 beach行列は333×500と小さいため、2番目の行列のサイズをmarcelの行列と一致するように変更する必要があります。 このサイズ変更手順を実行しない場合、OpenCVはadd関数の結果を計算する方法を認識せず、前に示したエラーメッセージを表示します。

2回目の試行では、リスト2-5のコードを生成します。 ここでは、クラスImgprocのサイズ変更関数を使用して、beach行列をmarcel行列と同じサイズに変更します。

リスト2-5
Mat marcel = Imgcodecs.imread("marcel.jpg");
Mat beach = Imgcodecs.imread("beach.jpeg");
Mat dest = new Mat();
Imgproc.resize(beach, dest, marcel.size());
Core.add(marcel, dest, dest);
Imgcodecs.imwrite("marcelOnTheBeach.jpg", dest);
    

このコードを実行すると、出力画像は図2-8のようになります。

うーん。 プログラムはエラーなしで最後まで実行されるため、確かに問題はありませんが、何かが正しくありません。 出力画像は露出オーバーの印象を与え、明るすぎるように見えます。 実際、見てみると、ほとんどの出力イメージピクセルの最大RGB値は255、255、255であり、これは白のRGB値です。

各行列の感触を維持しながら、最大値である255,255,255を超えないように加算を行う必要があります。

Weighted Addition

Matオブジェクトの意味のある値を保持することは、確かにOpenCVができることです。 Coreクラスには、便利なaddWeightedという名前の加重バージョンのadd関数が付属しています。 addWeightedが行うことは、各Matオブジェクトの値に、それぞれ異なるスケーリング係数を乗算することです。 ガンマと呼ばれるパラメータで結果値を調整することも可能です。

関数addWeightedは6つ以上のパラメーターを取ります。 1つずつ確認してみましょう。

したがって、結果のMatオブジェクトの各ピクセルを計算するために、addWeightedは次のことを行います。

\( image1 \times alpha + image2 \times beta + gamma = dest \)

Core.addをCore.addWeightedで置き換え、いくつかの意味のあるパラメーター値を使用して、リスト2-6に示すコードを取得します。

リスト2-6
Mat marcel = Imgcodecs.imread("marcel.jpg");
Mat beach = Imgcodecs.imread("beach.jpeg");
Mat dest = new Mat();
Imgproc.resize(beach, dest, marcel.size());
Core.addWeighted(marcel, 0.8, dest, 0.2, 0.5, dest);
    

図2-9に示すように、プログラム実行の出力は、はるかに有用なものになります。

Back to Sepia

この段階では、OpenCVでのMat計算について十分理解しているため、数ページ前に示したセピアの例に戻ることができます。

セピアのサンプルでは、Core.transform関数で使用する別のMatオブジェクトであるカーネルを作成していました。

Core.transformは、入力画像に変換を適用します。

この仕組みの例については、表2-2を参照してください。


OpenCVのCore.transform関数は多くの状況で使用でき、カラー画像をセピア色に変換する根本にもあります。

マルセルをセピアにできるかどうか見てみましょう。 3×3カーネルを使用した単純な変換を適用します。ここで、各値は先ほど説明したように計算されます。

最も単純で最も有名なセピアトランスフォームは、次の値を持つカーネルを使用します。

[ 0.272 0.534 0.131
0.349 0.686 0.168
0.393 0.769 0.189]
    

したがって、結果のピクセルごとに、青の値は次のようになります。0.272xソース青+ 0.534 xソース緑+ 0.131 xソース赤

緑の目標値は次のとおりです。0.349xソースブルー+ 0.686 xソースグリーン+ 0.168 xソースレッド

最後に、赤の値は次のとおりです。0.393xソース青+ 0.769 xソース緑+ 0.189 xソース赤。

ご覧のとおり、赤の値はほぼ固定されており、各乗数は約0.1です。緑は、結果のMatオブジェクトのピクセル値に最も影響を与えます。

今回のマルセルの入力画像を図2-10に示します。

マルセルをセピア色にするには、まずリスト2-7に示すように、imreadを使用して画像を読み取り、次にセピア色のカーネルを適用します。

リスト2-7
Mat marcel = Imgcodecs.imread("marcel.jpg");
Mat sepiaKernel = new Mat(3, 3, CvType.CV_32F);
sepiaKernel.put(0, 0,
  // bgr -> blue
  0.272, 0.534, 0.131,
  // bgr -> green
  0.349, 0.686, 0.168,
  // bgr -> red
  0.393, 0.769, 0.189);
Mat destination = new Mat();
Core.transform(marcel, destination, sepiaKernel);
Imgcodecs.imwrite("sepia.jpg", destination);
    

ご覧のとおり、各ピクセルのBGR出力は、入力内の同じピクセルの各チャネルの値から計算されます。

コードを実行すると、図2-11に示すように、Visual Studio Codeはsepia.jpgという名前のファイルに画像を出力します。

それは少し黄色がかったセピアでした。 さらに赤が必要な場合はどうなりますか? さあ、やってみてください。

赤を増やすことは、カーネル行列の3行目であるRチャネルの値を増やすことを意味します。

kernel [3,3]の値を0.189から0.589に上げると、入力の赤から出力の赤への出力が増加します。 したがって、次のカーネルを使用すると、Marcelは図2-12に示すように、何かより赤いものになります。

// bgr -> blue
0.272, 0.534, 0.131,
// bgr -> green
0.349, 0.686, 0.168,
// bgr -> red
0.393, 0.769, 0.589
    

遊んで、カーネルの他のいくつかの値を試して、 セピア色を青または青に変えることができます。 もちろん、猫がいる場合はそれを使ってみてください。 しかし、あなたはマルセルの可愛らしさを打ち負かすことができますか?

Finding Marcel: Detecting Objects Primer

猫のマルセルを使って物体を検出する方法について話しましょう。

Finding Cat Faces in Pictures Using a Classifier

処理速度が速くなり、ニューラルネットワークがすべてのIT雑誌や本の表紙を作成する前に、 OpenCVは分類器を使用して画像内のオブジェクトを検出する方法を実装しました。

分類器は、学習用画像および分類器に検出フェーズで検出させたい特徴量を与える方法によって、 数枚の写真のみで学習させることができます。

OpenCVの分類器の3つの主要なタイプは、 トレーニングフェーズ中に入力画像から抽出される特徴のタイプに応じて名前が付けられ、 次のとおりです。

カスケード分類器に関するOpenCVのドキュメント( https://docs.opencv.org/4.1.1/db/d28/tutorial_cascade_classifier.html )には、 さらに詳しい背景情報が必要な場合の詳細が満載です。 現時点では、ここでの目標は、この無料で入手できるドキュメントを繰り返すことではありません。

What Is a Feature?

特徴は、トレーニングに使用される一連のデジタル画像から抽出された重要なポイントであり、 まったく新しいデジタル入力でのマッチングに再利用できます。

たとえば、「ORBによるオブジェクト認識とFPGAでの実装」 (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.405.9932&rep=rep1&type=pdf)でうまく説明されているORB分類器は、BRIEFに基づく非常に高速なバイナリ記述子です。 他の有名な機能ベースのアルゴリズムは、回転・拡大縮小に不変な特徴量(SIFT)とスピードアップロバスト特徴量(SURF)です。 これらはすべて、新しい入力に対して一連の既知の画像(私たちが探しているもの)の特徴に一致するように探しています。

リスト2-8は、特徴抽出がどのように行われるかの非常に簡単な例を示しています。 ここでは、画像の重要なポイントを見つけるためにORB検出器を使用しています。

リスト2-8
Mat src = Imgcodecs.imread("marcel2.jpg", Imgcodecs.IMREAD_GRAYSCALE);
  
ORB detector = ORB.create();
MatOfKeyPoint keypoints = new MatOfKeyPoint();
detector.detect(src, keypoints);
  
Mat target = src.clone();
target.setTo(new Scalar(255, 255, 255));
  
Features2d.drawKeypoints(target, keypoints, target);
Imgcodecs.imwrite("orb.png", target);
    

基本的に、特徴は点の集合として抽出されます。 通常はそうしませんが、ここで重要な点を描くと、図2-13のようになります。

カスケード分類器と呼ばれるのは、内部にさまざまな分類器のセットがあり、 速度を犠牲にして、それぞれがより深く、より詳細に一致する可能性があるためです。 したがって、最初の分類器は非常に高速で正または負の値を取得します。 正の場合、タスクは次の分類器に渡され、さらに高度な処理が行われます。

ポールヴィオラとマイケルジョーンズによって提案されたように、 Haarベースの分類器は、図2-14に示すように、4つの主要な特徴の分析に基づいています。

特徴は簡単に計算できるため、Haarベースのオブジェクト検出のトレーニングに必要な画像の数は非常に少なくなります。 最も重要なのは、最近まで、組み込みシステムのCPU速度が低いため、これらの分類器には非常に高速であるという利点がありました。

Where in the World Is Marcel?

ですから、研究論文について十分に話し、読んでください。 つまり、Haarベースのカスケード分類器は、人間や動物の顔の特徴を見つけるのに優れており、 目、鼻、笑顔だけでなく、全身の特徴にも焦点を当てることができます。

分類器を使用して、ビデオストリームの動く頭の数を数え、 就寝後に冷蔵庫から物を盗んでいるかどうかを調べることもできます。

OpenCVを使用すると、カスケード分類器を使用してすぐに満足感を得られます。 カスケード分類子を実行する方法は次のとおりです。

  1. 探索対象の特徴量を記述する数値を含むXML定義ファイルから、分類器をロードします。
  2. 入力画像およびMatOfRectと呼ばれるMatオブジェクトをパラメータとして与えて、detectMultiScaleを呼び出します。 MatOfRectオブジェクトは、長方形のリストを適切に処理するように設計された特定のOpenCVオブジェクトです。
  3. 前の呼び出しが完了すると、MatOfRectはいくつかの長方形で塗りつぶされます。 それぞれの長方形は、ポジティブが見つかった入力画像のゾーンを表します。
  4. 元の画像に芸術的な描画を行って、分類器が見つけたものを強調します。
  5. 出力を保存します。

このためのJavaコードは実際にはかなり単純で、同等のPythonコードよりもやや長くなっています。 リスト2-9を参照してください。

リスト2-9
String classifier = "haarcascade_frontalcatface.xml";
   
CascadeClassifier cascadeFaceClassifier = new CascadeClassifier(classifier);
   
Mat cat = Imgcodecs.imread("marcel.jpg");
   
MatOfRect faces = new MatOfRect();
cascadeFaceClassifier.detectMultiScale(cat, faces);
  
for (Rect rect : faces.toArray()) {
  Imgproc.putText(cat, "Chat", new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_PLAIN, 10, new Scalar(255, 0, 0), 5);
  Imgproc.rectangle(cat, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Scalar(255, 0, 0), 5);
}
   
Imgcodecs.imwrite("marcel_blunt_haar.jpg", cat);
    

リスト2-9のコードを実行すると、わずかにナイーブなアプローチで問題が何であるかがすぐにわかります。 図2-15に示すように、分類器は多くの追加のポジティブを見つけています。

検出されたオブジェクトの数を減らすには、2つの方法があります。

フルバージョンには、表2-3に示すすべてのパラメーターがあります。

表2-3の情報に基づいて、次に示すように、いくつかの賢明なパラメータをdetectMutiScaleに適用してみましょう。

リスト2-9のコードに基づいて、detectMultiScaleを含む行を次の更新されたものに置き換えます。

cascadeFaceClassifier.detectMultiScale(cat, faces, 2, 3, -1, new Size(300, 300));
    

新しいパラメーターを適用し、(なぜでしょうか)新しいコードでデバッグセッションを実行すると、 図2-16に示すレイアウトが得られます。

出力は既にレイアウトに含まれていますが、 見つかった長方形は1つの長方形のみに制限され、出力 1つの画像のみを提供します(そして、はい、Marcelは1つしか存在できません)。

Finding Cat Faces in Pictures Using the Yolo Neural Network

カスケード分類器をトレーニングして、認識してもらいたいものを認識する方法についてはまだ確認していません(この本の範囲を超えているため)。 問題は、ほとんどの分類器は、車、カメ、標識よりも人など、他のものよりもいくつかのことをよりよく認識する傾向があることです。

これらの分類器に基づく検出システムは、複数の場所とスケールで画像にモデルを適用します。 画像の高スコア領域は検出と見なされます。

ニューラルネットワークYoloは、まったく異なるアプローチを使用しています。 ニューラルネットワークを画像全体に適用します。 このネットワークは、画像を領域に分割し、各領域の境界ボックスと確率を予測します。 これらの境界ボックスは、予測される確率によって重み付けされます。

Yoloは、リアルタイムのオブジェクト検出で高速であることが証明されており、第3章のRaspberry Piでリアルタイムで実行するために選択するニューラルネットワークになります。

後で、探している新しいオブジェクトを認識するようにカスタムYoloベースのモデルをトレーニングする方法を確認しますが、 この章をすばらしいエキサイティングな終わりに導くために、 COCO画像セットで学習済みの提供されているデフォルトのYoloネットワークの1つをすばやく実行しましょう。 猫、自転車、車など80個のオブジェクトを他のオブジェクトの中から検出できます。

私たちに関しては、マルセルが現代のニューラルネットワークの目から見ても猫として検出される任務を果たしているかどうかを見てみましょう。

最後のサンプルでは、ディープニューラルネットワークに関するいくつかのOpenCVの概念を紹介し、この章を締めくくります。

ニューラルネットワークとは何かすでにご存じでしょう。 これは、脳内の接続がどのように機能するかをモデルにしており、 入ってくる電気信号によってトリガーされるしきい値ベースのニューロンをシミュレートします。 ニューロンが非常に多い脳が描画されると、図2-17のようになります。

私は実際には現実が少し異なっていて、私の脳がはるかにカラフルであることを願っています。

図2-17に一目でわかるのは、ネットワークの構成です。 図2-17は、入力層と出力層の間にある1つの非表示層のみを示していますが、 標準のディープニューラルネットワークには約60から100の非表示層があり、 もちろん入力と出力のドット数が多くなっています。

多くの場合、ネットワークは、そのネットワークに固有のハードコードされたサイズで画像を1ピクセルから1ドットにマッピングします。 出力は実際には少し複雑で、とりわけ確率と名前、または少なくともリスト内の名前のインデックスが含まれています。

トレーニングフェーズ中、図の各矢印、またはネットワークの各ニューロン接続は、徐々に重みを取得します。 これは、次の非表示または出力の次のニューロン(円)の値に影響を与えるものです。

コードサンプルを実行するときは、ネットワークのグラフィック表現用の構成ファイル(非表示のレイヤーと出力レイヤーの数)と、 サークル間の各接続の数を含む重み用のファイルの両方が必要です。

いくつかのJavaコーディングを使って、理論から実践に移りましょう。 このYoloベースのネットワークへの入力としていくつかのファイルを提供し、 ご想像どおり、私たちのお気に入りの猫を認識したいと思います。

Yoloの例は、以下に示すいくつかのステップに分かれています。

  1. 2つのファイルを使用してOpenCVオブジェクトとしてネットワークをロードします。 ご覧のとおり、これにはウェイトファイルと構成ファイルの両方が必要です。
  2. ロードされたネットワークで、接続されていないレイヤー、つまり出力を持たないレイヤーを見つけます。 それらは出力レイヤー自体になります。 ネットワークを実行した後、これらのレイヤーに含まれる値に関心があります。
  3. オブジェクトを検出する画像をblobに変換します。これはネットワークが理解できるものです。 これはOpenCV関数blobFromImageを使用して行われます。 これには多くのパラメーターがありますが、非常に簡単に理解できます。
  4. この美しいblobを読み込まれたネットワークにフィードし、関数forwardを使用してネットワークを実行するようOpenCVに依頼します。 また、値を取得したいノードは、以前に計算した出力レイヤーです。
  5. Yoloモデルによって返される各出力は、次のセットです。
  6. ネットワークから返された出力から一連のボックスと信頼度を抽出して構築する後処理ステップに移動します。
  7. Yoloは同じ結果を得るために多くのボックスを送信する傾向があります。 別のOpenCV関数であるDnn.NMSBoxesを使用します。これは、信頼スコアが最も高いボックスを維持することで重複を削除します。 これは非最大抑制(NMS)と呼ばれます。
  8. 多くのオブジェクト検出サンプルの場合と同様に、注釈付きの長方形とテキストを使用して、これらすべてを画像に表示します。

完全なコードについては、リスト2-10を参照してください。 Javaは冗長であるため、かなりの数の行になりますが、これはもう心配する必要はありません。 よろしいですか?

リスト2-10 Yolo.java
package hello;
   
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
        
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfFloat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.scijava.nativelib.NativeLoader;
         
public class Yolo {
  static final String OUTFOLDER = "out/";
  static final Scalar BLACK = new Scalar(0, 0, 0);
      
  static {
    new File(OUTFOLDER).mkdirs();
  }
      
  public static void main(String[] args) throws Exception {
    NativeLoader.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    runDarknet(new String[] { "marcel.jpg", "marcel2.jpg", "cats.jpg", });
  }
      
  private static void runDarknet(String[] sourceImageFile) throws IOException {
    List<String> labels = Files.readAllLines(Paths.get("yolov3/coco.names"));
    Net net = Dnn.readNetFromDarknet("yolov2-tiny/yolov2-tiny.cfg", "yolov2-tiny/yolov2-tiny.weights");
    List<String> layersNames = net.getLayerNames();
    List<String> outLayers = net.getUnconnectedOutLayers().toList().stream().map(i -> i - 1).map(layersNames::get).collect(Collectors.toList());
    
    for (String image : sourceImageFile) {
      runInference(net, outLayers, labels, image);
    }
  }
      
  private static void runInference(Net net, List<String> layers, List<String> labels, String filename) {
    final Size BLOB_SIZE = new Size(416, 416);
    final double IN_SCALE_FACTOR = 0.00392157;
    final int MAX_RESULTS = 20;
      
    Mat frame = Imgcodecs.imread(filename, Imgcodecs.IMREAD_COLOR);
    Mat blob = Dnn.blobFromImage(frame, IN_SCALE_FACTOR, BLOB_SIZE, new Scalar(0, 0, 0), false);
    net.setInput(blob);
      
    List<Mat> outputs = layers.stream().map(s -> {return new Mat();}).collect(Collectors.toList());
      
    net.forward(outputs, layers);
      
    List<Integer> labelIDs = new ArrayList<>();
    List<Float> probabilities = new ArrayList<>();
    List<String> locations = new ArrayList<>();
      
    postprocess(filename, frame, labels, outputs, labelIDs, probabilities, locations, MAX_RESULTS);
  }
      
  private static void postprocess(String filename, Mat frame, List<String> labels, List<Mat> outs,
                  List<Integer> classIds, List<Float> confidences, List<String> locations, int nResults) {
    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) {
      // Scan through all the bounding boxes output from the network and keep only the
      // ones with high confidence scores. Assign the box's class label as the class
      // with the highest score for the box.
      final float[] data = new float[(int) out.total()];
      out.get(0, 0, data);
      
      int k = 0;
      for (int j = 0; j < out.height(); j++) {
      
        // Each row of data has 4 values for location, followed by N confidence values
        // which correspond to the labels
        Mat scores = out.row(j).colRange(5, out.width());
        // Get the value and location of the maximum score
        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, labels, classIds, confidences, nResults, tmpLocations, tmpClasses, tmpConfidences);
    Imgcodecs.imwrite(OUTFOLDER + new File(filename).getName(), frame);
  }
      
  private static void annotateFrame(Mat frame, List<String> labels, List<Integer> classIds, List<Float> confidences,
                  int nResults, List<Rect> tmpLocations, List<Integer> tmpClasses, List<Float> tmpConfidences) {
    // Perform non maximum suppression to eliminate redundant overlapping boxes with
    // lower confidences and sort by confidence
      
    // many overlapping results coming from yolo so have to use it
    MatOfRect locMat = new MatOfRect();
    locMat.fromList(tmpLocations);
      
    MatOfFloat confidenceMat = new MatOfFloat();
    confidenceMat.fromList(tmpConfidences);
      
    MatOfInt indexMat = new MatOfInt();
    Dnn.NMSBoxes(locMat, confidenceMat, 0.1f, 0.1f, indexMat);
      
    for (int i = 0; i < indexMat.total() && i < nResults; ++i) {
      int idx = (int) indexMat.get(i, 0)[0];
      classIds.add(tmpClasses.get(idx));
      confidences.add(tmpConfidences.get(idx));
      Rect box = tmpLocations.get(idx);
      String label = String.format("%s [%.0f%%]", labels.get(classIds.get(i)), 100 * tmpConfidences.get(idx));
      Imgproc.rectangle(frame, box, BLACK, 2);
      Imgproc.putText(frame, label, new Point(box.x, box.y), Imgproc.FONT_HERSHEY_PLAIN, 2.0, BLACK, 3);
    }
  }
}      
    

図2-18に示すように、Marcelの画像で例を実行すると、実際には完全な場所に近い非常に高い信頼スコアが得られます。

多くの猫で推論を実行しても素晴らしい結果が得られますが、 2回目の実行では、検出された別の猫との位置が近いため、 および一致するボックスの重複を削除する後処理ステップが導入されたため、 猫の1つが見つかりません。 図2-19を参照してください。

この時点での練習は、Dnnのパラメーターを変更することです。 2つのボックスを同時に表示できるかどうかを確認するNMSBoxes関数呼び出し。

静止画像の問題は、私たちが実際に持っている余分なコンテキストを取得することが難しいことです。 これは、ライブビデオストリームからの入力画像のセットを処理するときになくなる欠点です。

したがって、第2章を終えると、画像のオブジェクト検出を実行できるようになります。 第3章では、ここからそれを取り上げ、Raspberry Piを使用してデバイス上の検出について説明します。