TopView Transformation (鳥瞰図変換) 2

TopView Transformation 1 のエントリで論文まとめを書いた鳥瞰図変換ですが,ここでは実際に鳥瞰図変換にトライしていきます.

実験設定

 自宅実験室(笑) にて,以前紹介した USB カメラを使って写真を撮ります.鳥瞰図変換用にカメラ三脚と角度計を購入.

実験の様子(笑)

f:id:rkoichi2001:20160529025845j:plain f:id:rkoichi2001:20160529025842j:plain

結果

 結果ですが,カメラのキャリブレーションが甘いのか,カメラ設置があまいのか,直線が少し曲がってしまっているのですが,それでも鳥瞰図は出来上がりました.画像の端に行くに連れて対応するピクセルが疎になっている様子がわかります.線形補間等の処理を加えてあげる必要がありそうですが,それはまた今度....

変換前写真(歪み補正前)

f:id:rkoichi2001:20160529133210p:plain

変換前写真(歪み補正後)

f:id:rkoichi2001:20160529133136p:plain

変換後写真

f:id:rkoichi2001:20160529134517p:plain

実装

 下記,実験に使ったコードです.

#include <iostream>
#include <math.h>
#include "opencv2/opencv.hpp"
//#include "opencv2/calib3d.hpp"

using namespace std;
using namespace cv;

int main(int argc, char** argv) {

	// 1. Load Raw Iamge
	string imagepath = "/home/koichi/my_photo-1.jpg";
	Mat rawImage = imread(imagepath, CV_LOAD_IMAGE_COLOR);
	namedWindow("Raw Image", WINDOW_AUTOSIZE);
	imshow("Raw Image", rawImage);

	// 2. Undistort Image Based on Calibration Matrix.
	Mat undistortImage(rawImage.rows, rawImage.cols, CV_8UC3);
	Matx33d K(	627.235434, 0,			654.957574,
				0,			630.482585,	494.346943,
				0,			0,			1			);
	cv::Vec4d D(-0.210146, 0.030563, 0.001172, -0.001306);
	cv::undistort(rawImage, undistortImage, K, D);
	namedWindow("Undistorted Image", WINDOW_AUTOSIZE);
	//line(undistortImage, Point(640, 0), Point(640, 960), Scalar(255, 255, 255), 10);
	//line(undistortImage, Point(0, 460), Point(1280, 460), Scalar(255, 255, 255), 10);
	imshow("Undistorted Image", undistortImage);

	// 3. Convert Image to Gray
	Mat grayImage(rawImage.rows, rawImage.cols, CV_8UC1);
	cvtColor(undistortImage, grayImage, COLOR_RGB2GRAY);
	namedWindow("Gray Image", WINDOW_AUTOSIZE);
	imshow("Gray Image", grayImage);

	// 4. Top View Conversion
	Mat topImage(rawImage.rows, rawImage.cols, CV_8UC3);
	topImage.setTo(Scalar(0));

	Mat topImageGray(rawImage.rows, rawImage.cols, CV_8UC1);
	topImageGray.setTo(Scalar(0));
	{
		double Hvc = 2;
		double Hc = 0.7;
		double Dvc = 1.7;
		double f = 630;
		double fp = f;
		double theta = 30.0 / 180.0 * M_PI;
		double s = sin(theta);
		double c = cos(theta);
		int cx = 640;
		int cy = 480;
		int cxp = 640;
		int cyp = 480;

		for (int y = 0; y < topImage.rows; y++) {
			for (int x = 0; x < topImage.cols; x++) {

				int xOrg = x - cx;
				int yOrg = - y + cy;

				int newX = fp / Hvc * Hc * xOrg / (f * s - yOrg * c);
				int newY = fp / Hvc * (Hc * (f * c + yOrg * s) / (f * s - yOrg * c) - Dvc);

				newX = newX + cxp;
				newY = -newY + cyp;

				if (newX < 0 || topImage.cols - 1 < newX || newY < 0 || topImage.rows - 1 < newY ) {
					continue;
				}

				topImageGray.data[newY * topImageGray.cols + newX] = grayImage.data[y * grayImage.cols + x];

				topImage.data[(newY * topImage.cols + newX) * topImage.channels()] = undistortImage.data[(y * topImage.cols + x) * topImage.channels()];
				topImage.data[(newY * topImage.cols + newX) * topImage.channels() + 1] = undistortImage.data[(y * topImage.cols + x) * topImage.channels() + 1];
				topImage.data[(newY * topImage.cols + newX) * topImage.channels() + 2] = undistortImage.data[(y * topImage.cols + x) * topImage.channels() + 2];

			}
		}
	}

	namedWindow("Top Image", WINDOW_AUTOSIZE);
	imshow("Top Image", topImage);
	namedWindow("Top Image Gray", WINDOW_AUTOSIZE);
	imshow("Top Image Gray", topImageGray);

	while(true) {
		waitKey(0);
	}
	return 0;
}



TopView Transformation (鳥瞰図変換) 1

 いよいよつくばチャレンジに向けての具体的な”ロボット工作”的投稿です.突然ですが,最近の車についてるアラウンドビューモニタってご存じですか?駐車しようとしたときに,ナビに表示される車全体を上から見たような絵のことです.

アラウンドビューモニター | 日産|技術開発の取り組み

 自分は画像処理の経験が浅いからかもしれませんが,これって本当にすごいと思います.実際にはカメラは車両のバンパやサイドミラーに”斜め下向き”についているので,カメラから得られる画像そのものはもちろんトップビューではないんですが,カメラと車の正確な位置情報をつかってえられた画像を変換してやることで,まるで地面を上から見ているような映像に変えることができるわけです.

 で,これがつくばチャレンジのロボットとなんの関係があるの?という話なんですが,ロボットの自己位置推定にトップビュー変換した画像を使えないかと考えているわけです.流れとしては,,,,

①手動で,つくばチャレンジ2016のコース上をロボット走行させる.
②走行中,適切な間隔で写真を撮っていく.
③走行後,撮影した写真を Top View 変換する.

①~③のステップで,理想的な走行経路から撮影した Top View 画像が取得できます.自動で走らせるときには,この事前に撮りためた Top View 画像と今まさに取得している写真の Top View 画像の比較をして,自動走行時のロボットがどこにいるかを判断します.
(上手くいくかどうかわかりませんが...)

 ということで,まず Top View 変換のプログラムを作ることにしました.OpenCV の WarpAffine という関数を呼べば割と簡単に作れそうだったのですが,とりあえず肝になるところだと思ったので勉強がてら論文読み&サンプルコードを実装してみます.

文献

 参考にしたのは,下記の文献です.

  1. Vehicle Detection Based on Perspective Transformation Using Rear-View Camera
  2. A Vision Based Top-View Transformation Model for a Vehicle Parking Assistant
  3. 鳥瞰表示による駐車支援システム

技術概要

 Vehicle Detection Based on Perspective Transformation Using Rear-View Camera で用いられていた下記の図がわかりやすかったので,この図をベースに考えていきます.

f:id:rkoichi2001:20160526035336p:plain

座標変換

 視点変換をするにあたり,まずは座標変換を考えないといけません.いきなり撮像面間 (xy平面 - x'y'平面) の変換を考えるとこんがらがるので,カメラセンター座標間 (Oc座標 - Ovc座標) の変換を考えるところから始めます.元々のカメラ位置は Oc にあるわけですが,ここから見える風景を,あたかも Ovc から見たように変換する必要があります.つまり,「Oc から見た時の風景の各点の 3次元座標を,Ovcから見た時の 3次元座標に変換する」必要があるわけです.

 \vec{X_{O_{w}}} = (X_{O_{w}}, Y_{O_{w}}, Z_{O_{w}})^T

 上記は空間内の一点で,それを世界座標系  O_{w} から見た座標とします.カメラ座標系  O_{c} ,仮想カメラ座標系  O_{vc} から見たそれぞれの座標を下記のように定義します.

 \vec{X_{O_{c}}} = (X_{O_{c}}, Y_{O_{c}}, Z_{O_{c}})^T
 \vec{X_{O_{vc}}} = (X_{O_{vc}}, Y_{O_{vc}}, Z_{O_{vc}})^T

 また,仮想カメラ座標系から見たカメラ座標系の位置関係は下記のようになります.

回転関係 :  {}^{O_{vc}}R_{O_{c}}=\begin{bmatrix}1 &0 &0 \\ 0 &cos(90-\theta) &sin(90-\theta) \\ 0 &-sin(90-\theta) &cos(90-\theta) \end{bmatrix}
並進関係 :  \vec{{}^{O_{vc}}Q_{O_{c}}} = (0, -D_{vc}, H_{vc} - H_{c})^T

 上記の回転関係と並進関係を用いると,座標間の関係性は下記の変換行列で結ばれます.

 (X_{O_{vc}}, Y_{O_{vc}}, Z_{O_{vc}})^T\ =\ {}^{O_{vc}}T_{O_{c}}*(X_{O_{c}}, Y_{O_{c}}, Z_{O_{c}})^T
 {}^{O_{vc}}T_{O_{c}}\ = \ \begin{bmatrix} & & &  \\ &{}^{O_{vc}}R_{O_{c}} & &{}^{O_{vc}}Q_{O_{c}} \\ & & &  \\ 0 &0 &0 &1 \end{bmatrix}

 上記をそのまま計算すると,下記の 3 つの関係式が導かれます.

1.  X_{O_{vc}}\ =\ X_{O_{c}}
2.  Y_{O_{vc}}\ =\ Y_{O_{c}}*sin\theta\ +\ Z_{O_{c}}*cos\theta\ -\ D_{vc}
 Z_{O_{vc}}\ =\ -Y_{O_{c}}*cos\theta\ +\ Z_{O_{c}}*sin\theta\ +\ H_{vc}\ -\ H_{c}

※変換後のZ座標は常に  H_{vc} なので,上記の最後の式は最終的には下記のようになります.
3.  H_{vc}\ =\ -Y_{O_{c}}*cos\theta\ +\ Z_{O_{c}}*sin\theta\ +\ H_{vc}\ -\ H_{c}

 また,三次元座標と投影座標の関係より,下記の 4 式が導かれます.

Camera ピクセル座標系
4.  x\ =\ f \displaystyle \frac{X_{O_{c}}}{Z_{O_{c}}}
5.  y\ =\ f \displaystyle \frac{Y_{O_{c}}}{Z_{O_{c}}}

Virtual Camera ピクセル座標系
6.  x'\ =\ f' \displaystyle \frac{X_{O_{vc}}}{Z_{O_{vc}}}
7.  y'\ =\ f' \displaystyle \frac{Y_{O_{vc}}}{Z_{O_{vc}}}

上記の 7 つの式から, X_{O_{c}}\ Y_{O_{c}}\ Z_{O_{c}}\ X_{O_{vc}}\ Y_{O_{vc}}\ Z_{O_{vc}} を消去すると,論文に乗っている下記の式を得ることができます.

 x'\ =\ \displaystyle \frac{f'}{H_{vc}} \cdot \frac{H_{c}x}{fsin\theta-ycos\theta}
 y'\ =\ \displaystyle \frac{f'}{H_{vc}} \cdot \{\frac{H_{c}(fcos\theta+ysin\theta)}{fsin\theta-ycos\theta}-D_{vc}\}

ふー...やっとまとまりました.最後の 2 式の変換を実際に実装していきます.
TopView Transformation 2 を好ご期待!!

Windows Form と スレッド

C# のライブラリっていろいろと便利なものがそろってると思うんですが,どうも資料が不足してるような気がする...前回のエントリでも Bitmap を扱ったけど,いま Image とか Bitmap とかを扱うものを作ってて,一枚の画像を複数のスレッドからいじる必要があったんだけど,「別のスレッドがつかんでる」とか「Invalid Operation」とかの例外がたくさん上がってくるのでちょっと調べたのでメモ.

事前にちょっとググった結果,下記のようなコメント発見.

Windows フォームと複数スレッド
1. Windows フォームでスレッドを作成した場合,フォームやフォーム上のコントロールに対しては,そのスレッドからの操作は動作が保証されない.<- つまり,基本的には UI スレッドからしかいじるなということらしい.

ということで実験.下記,メインのコードです.
下記のコードではまず写真を GUI に表示して, 1 秒毎に写真を反転させます.

public partial class Form1 : Form
{
  private System.Timers.Timer myTimer;

  private void SetupTimer()
  {
    myTimer = new System.Timers.Timer();
    myTimer.Enabled = true;
    myTimer.AutoReset = true;
    myTimer.Interval = 1000;
    myTimer.Elapsed += new ElapsedEventHandler(OnTimerEvent);
  }

  delegate void FlipDelegate();

  internal void FlipImageByThread()
  {
    Image img = pictureBox1.Image;
    img.RotateFlip(RotateFlipType.Rotate180FlipX);
    pictureBox1.Refresh();
  }

  private void OnTimerEvent(object source, ElapsedEventArgs e)
  {
    FlipImageByThread();
    //Console.WriteLine("OnTimerEvent");
    //Invoke(new FlipDelegate(FlipImageByThread));        
  }

  private void DisplayBitmap()
  {
    Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
    Graphics g = Graphics.FromImage(canvas);
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    Bitmap pic = new Bitmap(@"C:\\Users\\Koichi\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication1\\OpenCVSharpSample\\img\\boeing777.jpg");
    g.DrawImage(pic, 0, 0, 500, 500);
    pic.Dispose();
    g.Dispose();
    pictureBox1.Image = canvas;           
  }

  public Form1()
  {
    InitializeComponent();
    DisplayBitmap();
    SetupTimer();            
  }
}

結果的に,上記のコードは例外がはかれてしまってうまく動きません.理由は最初に述べたとおり, UI スレッド以外から UI をいじろうとしたからみたいです.

f:id:rkoichi2001:20160528233945p:plain

で,それじゃ別のスレッドから UI をいじりたいとき(例えば,ユーザのマウスイベントを拾ってとか,上記の例みたいにタイマイベントをひろってとか...)にどうすればええんか?って話が出てきますが,Invoke というメソッドを使えば UI スレッドに処理を移譲できるみたい.

該当するコードは下記の部分です.

delegate void FlipDelegate();

internal void FlipImageByThread()
{
  Image img = pictureBox1.Image;
  img.RotateFlip(RotateFlipType.Rotate180FlipX);
  pictureBox1.Refresh();
}

private void OnTimerEvent(object source, ElapsedEventArgs e)
{
  // このラインをコメントアウト.
  // FlipImageByThread();

  // 下記の 2 行を有効化.
  Console.WriteLine("OnTimerEvent");
  Invoke(new FlipDelegate(FlipImageByThread));        
}

結果は,うまく写真が反転して出力されました.
まとめると,Windows フォームは UI スレッド以外からコントロールしてはいけない.コントロールが必要な場合は Invoke メソッドを用いて処理を UI スレッドに移譲する.


f:id:rkoichi2001:20160528234850p:plain

f:id:rkoichi2001:20160528234757p:plain

OpenCVSharp BitMap <-> IplImage 間の変換

 いま携わっているプロジェクトで,BitMap形式の画像を IplImage に変換して OpenCVSharp を使って画像処理をやる必要があったんだけど,うまい変換方法がわからなかったので調べてみました.C# 自体そんなに強くない状態から始めたので,いろいろと勉強になってます.

 まずは,BitMap を表示するところから.

1. Windows Forms Project を生成する.

 「ファイル」->「新規作成」->「新規作成」->「プロジェクト」->「Windows フォームアプリケーション」

f:id:rkoichi2001:20160525234600p:plain

2. OpenCVSharp をプロジェクトに導入し,C# のフォーム上に写真を表示します.

 「ツール」->「NuGet パッケージマネージャ」->「ソリューションの NuGet パッケージの管理」

f:id:rkoichi2001:20160525234844p:plain

3. Form に Picture Box を追加します.

3.1 GUI コンポーネントの編集

 選択している「PictureBox」のドラッグ&ドロップで完了.

f:id:rkoichi2001:20160525234935p:plain

3.2 コード編集

 ソリューションエクスプローラに作成したフォームアイテムが表示されるので,このアイテムをダブルクリックするとテキストベースのコードが現れます.このケースの場合 「Form1.cs」 が該当.

f:id:rkoichi2001:20160526000159p:plain


 このファイルにコードを記述して,外部から取り込んだ写真を表示.追加したコードは下記のとおり.もともと持っていた写真のサイズが少し大きかったので,それを縮小して表示する部分も加えました.詳細は下記のとおり.

1. Picture Box に追加する Bitmap オブジェクトを生成する.
 Form に追加した Picture Box と同じサイズではまる Bitmap を生成します.実際に Picture Box に表示されるのはこの canvas オブジェクトになります.

Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);

2. canvas オブジェクト上の Image を操作するために,Graphics オブジェクトを取得します.

Graphics g = Graphics.FromImage(canvas);

3. canvas 上での Image 操作の際の補完方法を指定します.

g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

4. 写真から Bitmap オブジェクトを生成します.
 実際に加工されるイメージの元となる Bitmap を生成します.こいつが canvas 上にコピーされて,canvas 上にコピーされたものに対していろいろと操作を加えるという流れになります.

Bitmap pic = new Bitmap(@"C:\\Users\\Koichi\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication1\\OpenCVSharpSample\\img\\boeing777.jpg");

5. オリジナルの Bitmap を canvas 上にコピーすると同時に拡大・縮小も実施します(されます).

g.DrawImage(pic, 0, 0, 500, 500);

6. 元のイメージ,および Graphics オブジェクトは必要なくなったので,消去!

pic.Dispose();
g.Dispose();

7. 拡大・縮小したイメージを Picture Box に紐づける.

pictureBox1.Image = canvas;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace WindowsFormsApplicationExample
{
    public partial class Form1 : Form
    {

        private void DisplayBitmap()
        {
            // 1. Image Object Creation.
            Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            // 2. Generate Graphics Object from Image.
            Graphics g = Graphics.FromImage(canvas);
            // 3. Specify Interpolation Mode.
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
            // 4. Generate Bitmap Object from picture.
            Bitmap pic = new Bitmap(@"C:\\Users\\Koichi\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication1\\OpenCVSharpSample\\img\\boeing777.jpg");
            // 5. Reduce Size of Picture and paint it to canvas.
            g.DrawImage(pic, 0, 0, 500, 500);
            // 6. Dispose unnecessary "pic" and graphics.
            pic.Dispose();
            g.Dispose();
            // 7. Display image.         
            pictureBox1.Image = canvas;
            
        }

        public Form1()
        {
            InitializeComponent();
            DisplayBitmap();
        }
    }
}


実行結果は下記のとおり.
f:id:rkoichi2001:20160526003811p:plain

3. System.Drawing.Bitmap -> IplImage 間の変換を実施

 ここからがやっと本題です...2 のステップで,Bitmap をフォーム上に表示するところまでは行きました.今度は,こいつを OpenCVSharp の画像形式に変換して OpenCVSharp の Window でも表示します.

1. 表示したイメージを取得する.

Image img = pictureBox1.Image;

2. 表示したイメージと同じサイズ・フォーマットの IplImage を用意する.

IplImage convertedImg = new IplImage(new CvSize(img.Width, img.Height), BitDepth.U8, 3);

3. Bitmap フォーマットから IplImage にコピーする.

convertedImg.CopyFrom(img as Bitmap);

4. OpenCV 流に表示する.

Cv.ShowImage("CvSharpImage", convertedImg);

 なんか,3 のステップだけを書いたらええやんけ!っていう気もしますが,自分の練習・備忘も含まれてるのでご容赦を!ちなみに,ここで紹介しているのは Bitmap -> IplImage の変換ですが,下記のようにすれば逆の変換もできます.

System.Drawing.Image img = iplImage.ToBitmap();
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApplicationExample
{
    public partial class Form1 : Form
    {

        private void DisplayBitmap()
        {
            // 1. Image Object Creation.
            Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            // 2. Generate Graphics Object from Image.
            Graphics g = Graphics.FromImage(canvas);
            // 3. Specify Interpolation Mode.
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
            // 4. Generate Bitmap Object from picture.
            Bitmap pic = new Bitmap(@"C:\\Users\\Koichi\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication1\\OpenCVSharpSample\\img\\boeing777.jpg");
            // 5. Reduce Size of Picture and paint it to canvas.
            g.DrawImage(pic, 0, 0, 500, 500);
            // 6. Dispose unnecessary "pic" and graphics.
            pic.Dispose();
            g.Dispose();
            // 7. Display image.                        
            pictureBox1.Image = canvas;
            
        }

        private void DisplayPictureByOpenCVSharp()
        {
      // 1. Get Displayed Image.
            Image img = pictureBox1.Image;

      // 2. Instantiate OpenCV Image Class.
            IplImage convertedImg = new IplImage(new CvSize(img.Width, img.Height), BitDepth.U8, 3);

      // 3. Copy data from Bitmap.            
            convertedImg.CopyFrom(img as Bitmap);

      // 4. Display Image.
            Cv.ShowImage("CvSharpImage", convertedImg);

        }

        public Form1()
        {
            InitializeComponent();
            DisplayBitmap();
            DisplayPictureByOpenCVSharp();
        }
    }
}

実行結果は下記のとおり.
(若干端が黒くなっていてサイズが合ってないですが,,,とりあえず細かいことは置いといてということで...)

f:id:rkoichi2001:20160526010304p:plain

OpenCVSharp を使った画像の切り貼り

OpenCVSharp の導入と,画像の切り貼りについて,調べたのでメモ.

OpenCVSharp の導入

1. Visual Studio 2013 Community のインストール

 個人用なら完全タダで Professional と同等機能を提供!素晴らしい!インストールは素直に下記のリンクから.


2014-Nov 12 Release Notes | Visual Studio


Download Visual Studio Community 2013 というリンクに飛べば,インストーラがダウンロードされる.

2. C# プロジェクトの作成

 Visual Studio 起動後,「ファイル」->「新規作成」->「プロジェクト」->「Visual C#」->「コンソールアプリケーション」とたどり,適当な名前を入力して OK ボタンクリック.

3. OpenCVSharp の導入.

 「ツール」->「Nu Get パッケージマネージャー」->「ソリューションの Nu Get パッケージの管理」->「オンライン」とたどり,検索ボックスに "OpenCVSharp" と入力.OpenCVSharp のパッケージが出てくるので,インストール.ってか,これだけで導入できるなんて,すごい楽ちん.

f:id:rkoichi2001:20160525033507p:plain

OpenCVSharp を使った画像の切り貼り

 複数の画像を集めて,切り貼りして一枚の画像にするという作業が必要になったんだけど,直接画素をいじって編集するというのもなんだかスマートじゃないと思ったのでやり方を調べてみた.やり方としてはそんなに複雑でなく,SetROI 関数を使って対象となるエリアを限定してあげたあとに,Cv.Copy でコピーしてあげれば完了だった.ただ,注意点が一つあり,Cv.Copy やその他の ROI の設定が内部で用いられる関数を呼ぶときには二つの画像の ROI が完全に一致していないといけない.今回の場合だと, targetImage の ROI とコピー元となる whiteImage, grayImage が該当する.以下,簡単にコードの内容をチェック.

1. イメージの生成と画素情報の設定.

 恥ずかしいことに,画面全面で画素情報を設定する方法をしらず,危うく for ループで設定するところだった.こんな時には Cv.Set 関数が役立つ.

// Target Image.
targetImage = new IplImage(new CvSize(500, 500), BitDepth.U8, 1);
// Set the iamge completely black.
Cv.Set(targetImage, new CvScalar(0));

2. ROI の設定と,コピー

 コピーしたい領域を ROI として指定する.そのあとに,Cv.Copy を呼べば OK.SetROI した後の ResetROI のコールを忘れずに!

targetImage.SetROI(new CvRect(100, 100, 100, 100));
Cv.Copy(whiteImage, targetImage);
targetImage.ResetROI();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using OpenCvSharp;

namespace OpenCVSharpExample
{
    class Program
    {

        public static IplImage targetImage;

        public static IplImage whiteImage;

        public static IplImage grayImage;


        static void Main(string[] args)
        {

            // 1. Image Instance Generation.
            {
                // Target Image.
                targetImage = new IplImage(new CvSize(500, 500), BitDepth.U8, 1);
                // Set the iamge completely black.
                Cv.Set(targetImage, new CvScalar(0));

                whiteImage = new IplImage(new CvSize(100, 100), BitDepth.U8, 1);
                // Set the iamge completely black.
                Cv.Set(whiteImage, new CvScalar(255));

                grayImage = new IplImage(new CvSize(100, 100), BitDepth.U8, 1);
                // Set the iamge completely black.
                Cv.Set(grayImage, new CvScalar(80));
            }

            // 2. Set ROI and Copy
            {
                targetImage.SetROI(new CvRect(100, 100, 100, 100));
                Cv.Copy(whiteImage, targetImage);
                targetImage.ResetROI();

                targetImage.SetROI(new CvRect(300, 300, 100, 100));
                Cv.Copy(grayImage, targetImage);
                targetImage.ResetROI();
            }
            
            Cv.ShowImage("Target Image", targetImage);
            Cv.WaitKey(0);
        }
    }
}


以下,実行結果

f:id:rkoichi2001:20160525040232p:plain

OpenCV 2.4 を用いた USB カメラの取り込み2

前回のエントリで OpenCV2.4 を使って USB カメラの取り込みを実施したけど, Pixel Format がうまくコントロールできなかったのでこのエントリでは V4L2 の API からカメラの設定を変更するコードを作る.

参考にしたのは下記のホームページ
Capture images using V4L2 on Linux — Jay Rambhia

完成形のコードは最後に乗せておくとして, V4L2 でビデオからデータをキャプチャするまでには大きく分けて下記の 10 個くらいのステップをふまないといけなかった.

1. Video デバイスをオープンする.

 当然のことながら,まずはデバイスファイルをオープンする必要がある.

コード該当部

  // 1. Open Video Device.
  int fd;
  fd = open("/dev/videoLeftImgSrc", O_RDWR, 0);
  if (fd == -1)
  {
    std::cout << "Failed to open video device." << std::endl;
    return 1;
  }
2. デバイス情報をデバイスから抜き出す.

 次に,どうやらデバイス情報をクエリするのがお作法となっているみたい.確かに,素性の分からないカメラを使うこともあるだろうから,クエリして事前情報を掴んで置くことは必要なのかも.ただ,クエリ情報をその後のコードで使わないなら, v4l2-ctl コマンドで調べるほうが早いかも.

コード該当部

  // 2. Querying video capabilities.
  struct v4l2_capability caps;
  memset(&caps, 0, sizeof(caps));
  if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &caps))
  {
    std::cout << "Failed to query capabilities." << std::endl;
      return 1;
  }
  std::cout << "bus_info  : " << caps.bus_info << std::endl;
  std::cout << "card    : " << caps.card << std::endl;
  std::cout << "driver  : " << caps.driver << std::endl;
  std::cout << "version  : " << caps.version << std::endl;
3. Video に対して,フォーマットのリクエストをする.

 つぎに,やり取りしたいビデオのフォーマットをデバイスに対してリクエストする.今回使ったカメラだと,例えばピクセルフォーマットは下記の 3 つが対応可能であった.

  1. V4L2_PIX_FMT_SGRBG8
  2. V4L2_PIX_FMT_GREY
  3. V4L2_PIX_FMT_Y16

 今回はカラー画像を使いたかったので,ベイヤーパターンの V4L2_PIX_FMT_SGRBG8 を指定した.また,最大サイズ 1280 x 960 を指定.

コード該当部

  // 3. Format Specification.
  {
    struct v4l2_format fmt;
    memset(&(fmt), 0, sizeof(fmt));

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 1280;
    fmt.fmt.pix.height = 960;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_SGRBG8;
    //fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_GREY;
    fmt.fmt.pix.field = V4L2_FIELD_NONE;

    if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt))
    {
      std::cout << "Failed to set pixel format." << std::endl;
      return 1;
    }
  }
4. Video に対してバッファのリクエストをする.

 デバイス内部で保持されるバッファのリクエストをする.memory では,バッファの I/O を指定.count ではバッファ数, type は Stream/Buffer の種類を指定する...と書いてみたものの,このあたりの詳しい話は自分もちゃんとは理解できず...たぶん下記のままでそんなに問題無いかと.詳しい説明は下記をどーぞ!
ioctl VIDIOC_REQBUFS

コード該当部

  // 4. Request Buffer
  {
    struct v4l2_requestbuffers req;
    memset(&(req), 0, sizeof(req));
    req.count = 1;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req))
    {
      std::cout << "Failed to request buffer." << std::endl;
      return 1;
    }
  }
5. Video のバッファ情報を抜き出す.

 4 のステップでバッファリクエストをしたが,ここではそのリクエストに基づいて作成されたバッファの情報を抜き出す.今回のケースでは, 4 のステップで一つのバッファしかリクエストしなかったので index = 0 としているが,複数個のバッファをリクエストした場合,複数個のバッファに対して複数回クエリをする形になる.最後の mmap という関数がバッファのアドレスを返すので,そのアドレスを用意したアドレス変数 "buffer" に渡してやって,アプリの側ではこのポインタの先を見に行くことでキャプチャが見れるようになる.

コード該当部

  // 5. Query Buffer
  struct   v4l2_buffer buf;
  memset(&(buf), 0, sizeof(buf));
  {
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    //buf.index = bufferindex;
    buf.index = 0;
    if(-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf))
    {
      std::cout << "Failed to query buffer." << std::endl;
      return 1;
    }

    std::cout << "buf.length : " << buf.length << std::endl;
    std::cout << "buf.m.offset : " << buf.m.offset << std::endl;

    buffer = (unsigned char*)mmap (NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
  }
6. Streaming を開始する.

 ついに Streaming を開始できるとこまで来た!やった!

コード該当部

  // 6. Start Streaming
  {
    if(-1 == xioctl(fd, VIDIOC_STREAMON, &buf.type))
    {
      std::cout << "Start Capture" << std::endl;
      return 1;
    }
  }
7. Buffer をキューにつなぐ, 8. Buffer をキューから外す

 下記, while ループが閉じてない中途半端なコードの一部分になってしまったが,,, 6 のステップで Streaming を開始したが,実際にはバッファを渡してあげないとカメラからデータを取ることはできない.流れとしては下記のようになると思う.

  1. VIDIOC_QBUF で使用可能なバッファをカメラに伝えてあげる.(ストリームを開始した直後はカメラが使用可能なバッファがひとつもない状態なので, while 文の上で index=0 を指定してその後の VIDIOC_QBUF でこのバッファが使用可能であることを伝えている.複数個のバッファをリクエストした場合は,最初のステップとして利用可能なすべてのバッファをカメラに伝える必要がある.)
  2. VIDIOC_DQBUF でカメラがデータを詰めたバッファを取得する.この時,buf.index にはカメラによって該当の index 番号が渡されているので,これを辿ってデータを取得する.
  3. アプリ側でデータを吸い上げ完了したら,不要になったバッファ (index = buf.index) をカメラに返してあげる.これには再び VIDIOC_QBUF を用いる.

コード該当部

struct 	v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
while (true) {
  // 7. Capture Image
  {
    // Connect buffer to queue for next capture.
    if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) {
      std::cout << "VIDIOC_QBUF" << std::endl;
    }

    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    struct timeval tv = {0};
    tv.tv_sec = 2;
    int r = select(fd+1, &fds, NULL, NULL, &tv);

    if(-1 == r)
    {
      std::cout << "Waiting for Frame" << std::endl;
      return 1;
    }

    memset(&(buf), 0, sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    if(-1 == xioctl(fd, VIDIOC_DQBUF, &buf))
    {
      std::cout << "Retrieving Frame" << std::endl;
      return 1;
    }
  }

....
8. 適切なイメージフォーマットに変換する.

 デバイスからのデータは 8bit Bayer Pattern なので,OpenCV 提供の cvColor 関数を使って RGB 情報に変換する.

コード該当部

// 8. Store Image in OpenCV Data Type
{
  memcpy(bayerRaw.data, buffer, 1280 * 960);
  cv::cvtColor(bayerRaw, color, CV_BayerGB2BGR);
}
9. 表示!

 ここは説明不要かと.

コード該当部

// 9. Display Image
{
  cv::imshow("edges", color);
  if (-1 != cv::waitKey(1000)) {
    break;
  }
}
10. Streaming を終了する.

 最後にカメラにストリーミングの終了を宣言して,終了!

コード該当部

// 10. Turn off streaming.
if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &buf.type)) {
  std::cout << "VIDIOC_STREAMOFF" << std::endl;
}


とりあえず作成したコードは下記の通り.実行するところまではできたので,雛形としては使ってもらえるかも.

#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

#include <linux/videodev2.h>
#include "opencv2/opencv.hpp"

static int xioctl(int fd, int request, void *arg)
{
    int r;
	do {
		r = ioctl (fd, request, arg);
		if (request == VIDIOC_DQBUF) {
			std::cout << "r : " << r << std::endl;
		}
	} while (-1 == r && EINTR == errno);
	return r;
}

int main() {

	unsigned char *buffer;

	// 1. Open Video Device.
	int fd;
	fd = open("/dev/videoLeftImgSrc", O_RDWR, 0);
	if (fd == -1)
	{
	    std::cout << "Failed to open video device." << std::endl;
	    return 1;
	}

	// 2. Querying video capabilities.
	struct v4l2_capability caps;
	memset(&caps, 0, sizeof(caps));
	if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &caps))
	{
		std::cout << "Failed to query capabilities." << std::endl;
	    return 1;
	}
	std::cout << "bus_info	: " << caps.bus_info << std::endl;
	std::cout << "card		: " << caps.card << std::endl;
	std::cout << "driver	: " << caps.driver << std::endl;
	std::cout << "version	: " << caps.version << std::endl;

	// 3. Format Specification.
	{
		struct v4l2_format fmt;
		memset(&(fmt), 0, sizeof(fmt));

		fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		fmt.fmt.pix.width = 1280;
		fmt.fmt.pix.height = 960;
		fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_SGRBG8;
		//fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_GREY;
		fmt.fmt.pix.field = V4L2_FIELD_NONE;

		if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt))
		{
			std::cout << "Failed to set pixel format." << std::endl;
			return 1;
		}
	}

	// 4. Request Buffer
	{
		struct v4l2_requestbuffers req;
		memset(&(req), 0, sizeof(req));
		req.count = 1;
		req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		req.memory = V4L2_MEMORY_MMAP;

		if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req))
		{
			std::cout << "Failed to request buffer." << std::endl;
			return 1;
		}
	}

	// 5. Query Buffer
	{
		struct 	v4l2_buffer buf;
		memset(&(buf), 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		//buf.index = bufferindex;
		buf.index = 0;
		if(-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf))
		{
			std::cout << "Failed to query buffer." << std::endl;
			return 1;
		}

		std::cout << "buf.length : " << buf.length << std::endl;
		std::cout << "buf.m.offset : " << buf.m.offset << std::endl;

		buffer = (unsigned char*)mmap (NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
	}

	// 6. Start Streaming
	{
		struct 	v4l2_buffer buf;
		memset(&(buf), 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		if(-1 == xioctl(fd, VIDIOC_STREAMON, &buf.type))
		{
			std::cout << "Start Capture" << std::endl;
			return 1;
		}
	}

	cv::namedWindow("edges",1);
	cv::Mat bayerRaw(960, 1280, CV_8UC1);
	cv::Mat color(960, 1, CV_8UC3);
	struct 	v4l2_buffer buf;
	buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buf.memory = V4L2_MEMORY_MMAP;
	buf.index = 0;
	while (true) {
		// 7. Capture Image
		{
			// Connect buffer to queue for next capture.
			if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) {
				std::cout << "VIDIOC_QBUF" << std::endl;
			}

			fd_set fds;
			FD_ZERO(&fds);
			FD_SET(fd, &fds);
			struct timeval tv = {0};
			tv.tv_sec = 2;
			int r = select(fd+1, &fds, NULL, NULL, &tv);

			if(-1 == r)
			{
				std::cout << "Waiting for Frame" << std::endl;
				return 1;
			}

			memset(&(buf), 0, sizeof(buf));
			buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			buf.memory = V4L2_MEMORY_MMAP;

			if(-1 == xioctl(fd, VIDIOC_DQBUF, &buf))
			{
				std::cout << "Retrieving Frame" << std::endl;
				return 1;
			}

		}

		// 8. Store Image in OpenCV Data Type
		{
			memcpy(bayerRaw.data, buffer, 1280 * 960);
			cv::cvtColor(bayerRaw, color, CV_BayerGB2BGR);
		}

		// 9. Display Image
		{
			cv::imshow("edges", color);
			if (-1 != cv::waitKey(1000)) {
				break;
			}
		}

	}

	// 10. Turn off streaming.
	if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &buf.type)) {
		std::cout << "VIDIOC_STREAMOFF" << std::endl;
	}

	return 0;
}

下記の写真が実際に上記プログラムを実行してとった写真.(洗濯物とか写ってて,生活感出ててすみません(笑))
とりあえず,ようやくカメラとOpenCVがつながったということで,バンザイ!
f:id:rkoichi2001:20160525025235p:plain

OpenCV 2.4 を用いた USB カメラの取り込み1

実装前にいろいろと試す用途で,EclipseOpenCV 用プロジェクトを作成する.ここはそのメモ.

EclipseC++ プロジェクトを作成する.

Eclipse を開いてからの...

  • "Project Exploer 右クリック" -> "New" -> "C/C++" を選択 -> "C++ Project" を選択.

f:id:rkoichi2001:20160522215514p:plain

  • "Project Name" を指定 -> "Location" を決定 -> "Executable" を選択 -> "Hello World C++ Project" を選択 -> "Linux GCC" を選択

f:id:rkoichi2001:20160522215949p:plain

OpenCV のインクルードパス,ライブラリパスを通す.

作成したプロジェクトを選択してからの...

  • "Project" 右クリック -> "Properties" -> "C/C++ General" を選択 -> "Paths and Symbols" を選択.

f:id:rkoichi2001:20160522223315p:plain

  • インクルードパスの追加

環境設定で構築した OpenCV の include フォルダのパスを追加.

f:id:rkoichi2001:20160522223532p:plain

  • ライブラリパスの追加

環境設定で構築した OpenCV の lib フォルダのパスを追加

f:id:rkoichi2001:20160522230051p:plain

  • ライブラリの追加

環境設定で構築した OpenCV の lib フォルダ内にあるライブラリを追加.今回はどれを使うかはっきりしなかったので,とりあえず全部追加.ここで,ライブラリ名は先頭の "lib" と末尾の ".so" を除いた形で追加する.

f:id:rkoichi2001:20160522230123p:plain

以上, "Apply" を押下して, "OK" で再ビルドが走る.

OpenCV を使っての USB カメラの取り込み.

 下記のサンプルコードを参考に作成した下記のコードをコンパイルして,カメラ画像が表示されることを確認.

Reading and Writing Images and Video — OpenCV 2.4.13.0 documentation

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main(int, char**)
{
	const string fileName = "/dev/videoLeftImgSrc";
    //VideoCapture cap(fileName); // open the default camera
	VideoCapture cap(0); // open the default camera
    if(!cap.isOpened()) {  // check if we succeeded
    	cout << "Can not open device." << endl;
    	return -1;
    }

    Mat edges;
    namedWindow("edges",1);
    for(;;)
    {
        Mat frame;
        cap >> frame; // get a new frame from camera
        cvtColor(frame, edges, CV_BGR2GRAY);
        //cvtColor(frame, edges, CV_YUV2RGB_YUYV);
        //cvtColor(frame, edges, CV_BayerBG2BGR);
        //GaussianBlur(edges, edges, Size(7,7), 1.5, 1.5);
        //Canny(edges, edges, 0, 30, 3);
        imshow("edges", edges);
        if(waitKey(30) >= 0) break;
    }
    // the camera will be deinitialized automatically in VideoCapture destructor
    return 0;
}

下記実行結果.どうやら Pixel Format がおかしいみたいなので,V4L2 の API を使って調整する必要があるみたい.とりあえず開発環境が整ってバンザイということで,それは次回に!

f:id:rkoichi2001:20160522234459p:plain