おもちゃラボ

Unityで遊びを作っていきます

OpenCV plus Unityサンプル集 20選

UnityでOpenCVを使いたい場合には「OpenCV for Unity」と「OpenCV plus Unity」の2種類のアセットがあります。OpenCV for Unityは有償($104)でメンテナンスもこまめに行われている一方、APIが独特でpythonやC++のOpenCVに慣れている人には少し使いにくいイメージです。

一方、OpenCV plus Unityは オープンソースのOpenCVSharpをUnity用にカスタムした無償のアセットです。APIの形式も他言語のものと似ていて比較的使いやすいため、この記事ではOpenCV plus Unityを使ったサンプルを紹介します。

ここで紹介するOpenCVのサンプルは以下のとおりです。

下準備

Asset StoreからUnity plus OpenCVをダウンロードして、プロジェクトにimportします。

assetstore.unity.com

インポート直後はエラーが表示されます。メニューバーからEdit→Project Settingsを選択して、「Allow unsafe code」のチェックボックスにチェックを入れて下さい。

f:id:nn_hokuson:20210422222024p:plain:w450

OpenCVで処理した画像を表示するため、UIのRawImageを使います。ヒエラルキーウインドウで「+」→「UI」→「Raw Image」を選択して下さい。表示する画像を画面サイズに合わせるため、RawImageにCameraScalerスクリプトをアタッチしておきます。
CameraScalerスクリプトはAssets/OpenCV+Unity/Demo/Scriptsの中にあります。

f:id:nn_hokuson:20210422222411p:plain:w300

これで下準備は完了です。次に新規でスクリプトを作成して、OpenCVで画像を表示していきましょう。

画像の読み込み

まずは画像をTexture2D型として読み込み、それを一旦OpenCVのMatに変換して、最後にもう一度Texture2D型に戻して表示するサンプルです。

次のTestスクリプトを作成して、上で作成したRawImageのオブジェクトにアタッチして下さい。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using OpenCvSharp.Demo;
using OpenCvSharp;
using OpenCvSharp.Aruco;

public class Test : MonoBehaviour
{
    public Texture2D texture;

    void Start()
    {
        // 画像読み込み
        Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

        // 画像書き出し
        Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
        OpenCvSharp.Unity.MatToTexture(mat, outTexture);

        // 表示
        GetComponent<RawImage>().texture = outTexture;
    }
}

スクリプトをRaw Imageにアタッチできたら、インスペクタからtexture変数に表示したい画像を指定します。このときOpenCVが画像のピクセルにアクセスできるように事前に設定する必要があります。表示したい画像を選択してインスペクタから「Read/Write Enable」にチェックを入れて下さい。

f:id:nn_hokuson:20210422223640p:plain:w350

表示したい画像をインスペクタからTestスクリプトのtexture変数にセットしたら実行してみて下さい。ゲームビューに画像が表示されればOKです。

f:id:nn_hokuson:20210422224515j:plain:w500

グレースケール化

カラー画像を読み込んで、グレースケール化するサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// 画像書き出し
Texture2D outTexture = new Texture2D(gray.Width, gray.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(gray, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

画像をグレースケールに変換するにはCvtColorメソッドを使います。第三引数に指定する値によって変換する形式を選択できます。よく使う値は次のとおりです。

意味
RGB2GRAY RGB画像をグレースケールに変換
RGB2HSV RGBをHSV形式に変換
RGBA2RGB 透明チャネルを削除する
RGB2GBR RとBチャネルを入れ替える

実行結果は次のようになります。
f:id:nn_hokuson:20210423080015j:plain:w500

画像を2値化する

カラー画像を読み込んで、しきい値よりも明るいピクセルは白、暗いピクセルは黒にする2値化のサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// 2値化
Mat bin = new Mat();
Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary);

// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(bin, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

画像を2値化するにはThresholdメソッドを使います。第3引数にしきい値、第5引数には変換形式を設定します。第5引数でよく使う値には次のものがあります。

意味
Binary 白黒に変換
Binary 白黒反転して変換
Otsu 大津の2値化アルゴリズムで変換

実行結果は次のようになります。
f:id:nn_hokuson:20210423080900j:plain:w500

画像から輪郭検出する

2値化した画像から輪郭を抽出するサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// 2値化
Mat bin = new Mat();
Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary);

// 輪郭抽出
Point[][] contours;
HierarchyIndex[] hierarchy;
Cv2.FindContours(bin, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxNone, null);

foreach (Point[] contour in contours)
{
    // 面積
	double area = Cv2.ContourArea(contour);

	// 重心
	Moments m = Cv2.Moments(contour);
	int cx = (int)(m.M10 / m.M00);
	int cy = (int)(m.M01 / m.M00);

	Cv2.DrawContours(mat, new Point[][] { contour }, 0, new Scalar(0, 0, 255), 8);
}

// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture; 

輪郭抽出をするには、画像を2値化してからFindContoursメソッドを使用します。第5引数にExternalを指定した場合は最外周の輪郭のみ検出します。一方、Treeを指定した場合は入れ子になった輪郭も抽出します。

意味
External 最外周の輪郭のみを検出
Tree 内側の輪郭も検出

実行結果は次のようになります。
f:id:nn_hokuson:20210423081500j:plain:w500

画像を射影変換する

画像を射影変換するサンプルです。射影変換とは四角形の頂点をつまんで好きな形に変形するイメージです。例えば撮影した画像に含まれるQRコード部分だけを取り出して、正方形に変換する場合などに使用します。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

Vector2[] srcData = new Vector2[]
{
	new Vector2(0, 0),
	new Vector2(mat.Width, 0),
    new Vector2(mat.Width, mat.Height),
    new Vector2(0, mat.Height)
};
Vector2[] dstData = new Vector2[]
{
	new Vector2(100, 50),
	new Vector2(400, 200),
	new Vector2(440, 350),
	new Vector2(0, 500)
};
Mat srcMat = new Mat(4, 1, MatType.CV_32FC2, srcData);
Mat dstMat = new Mat(4, 1, MatType.CV_32FC2, dstData);
Mat T = Cv2.GetPerspectiveTransform(srcMat, dstMat);

Mat dst = new Mat();
Cv2.WarpPerspective(mat, dst, T, new Size(mat.Width, mat.Height));

// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(dst, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

射影変換するにはGetPerspectiveTransformメソッドを使い、入力座標を出力座標に射影するための行列を求めます。次にWarpPerspectiveメソッドに計算した射影行列を指定して画像を変換します。

実行結果は次のようになります。
f:id:nn_hokuson:20210423082435j:plain:w500

座標の射影変換

画像の射影変換では出力結果は射影変換した後の画像でした。座標自体を変換したい場合にはこちらのスクリプトを使用します。

Vector2[] srcData = new Vector2[]
{
	new Vector2(100, 100),
	new Vector2(300, 120),
	new Vector2(300, 280),
	new Vector2(120, 300),
};
Vector2[] dstData = new Vector2[]
{
	new Vector2(0, 0),
	new Vector2(1, 0),
	new Vector2(1, 1),
	new Vector2(0, 1),
};
Mat srcMat = new Mat(4, 1, MatType.CV_32FC2, srcData);
Mat dstMat = new Mat(4, 1, MatType.CV_32FC2, dstData);

// 射影変換行列
Mat T = Cv2.GetPerspectiveTransform(srcMat, dstMat);

Point2f[] srcPos = { new Point2f(300, 280) };
Point2f[] dstPos = Cv2.PerspectiveTransform(srcPos, T);
Debug.Log(dstPos[0].X + "," + dstPos[0].Y);

GetPerspectiveTransformメソッドを使って射影変換行列を求めるところまでは同じです。座標自体を変換するにはPerspectiveTransformメソッドを使います。PerspectiveTransformには入力座標の配列と射影変換行列を指定し、戻り値として出力座標の配列を受け取ります。

ガウシアンブラーでぼかす

ガウシアンブラーを使って画像をぼかすサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// ぼかし
Mat blur = new Mat();
Cv2.GaussianBlur(mat, blur, new Size(11, 11), 0);

// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(blur, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture; 

OpenCVでガウシアンブラーをかけるにはGaussianBlurメソッドを使います。第3引数に渡すフィルタのサイズを大きくすればするほど、よりボケた画像になります。

実行結果は次のようになります。
f:id:nn_hokuson:20210423082929j:plain:w500

画像の膨張処理

白色のピクセルを周囲8方向に膨張させる処理を行うサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// 2値化
Mat bin = new Mat();
Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary);

// 膨張処理
Mat dst = new Mat();
Mat k = new Mat();
Cv2.Dilate(bin, dst, k, null, 3);

// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(dst, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

膨張処理を行うにはDilateメソッドを使用します。第5引数には何回膨張処理を繰り返すかを指定します。この値が大きければ大きいほど白色ピクセルの膨張量が大きくなります。また、逆に白色のピクセルを縮小させたい場合はCv2.Erodeメソッドを使います。引数の指定方法はDilateとほぼ同じです。

実行結果は次のようになります。
f:id:nn_hokuson:20210423201247j:plain:w500

Sovelフィルタを使う

縦方向や横方向のエッジを検出できるSovelフィルタを使うサンプルです。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// Sovelフィルタ
Mat sobel = new Mat();
Cv2.Sobel(gray, sobel, MatType.CV_8UC1, 0, 1);

// 書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(sobel, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVでSobelフィルタを使うにはSobelメソッドを使います。第3引数には入力画像の形式を指定します。第4と第5引数で検出するエッジの方向を指定します。(1, 0)であれば縦方向、(0, 1)であれば横方向のエッジを検出します。

下図(左)は縦方向のエッジを検出、下図(右)は横方向のエッジを検出したものです。
f:id:nn_hokuson:20210425073130j:plain:w320 f:id:nn_hokuson:20210425073153j:plain:w320

ハイパスフィルタを使う

ハイパスフィルタを使って画像中から輝度値の変化が大きい領域のみを抽出するサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);
		
// ハイパスフィルタのカーネル
double[] data = { -1, -1, -1,
                    -1,  8, -1,
                    -1, -1, -1 };
Mat kernel = new Mat(3, 3, MatType.CV_64FC1, data);

// ハイパス
Mat dst = new Mat();
Cv2.Filter2D(gray, dst, MatType.CV_8UC1, kernel);
  
// 画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(dst, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVにはハイパスフィルタに使えるメソッドは用意されていません。そこでハイパスフィルタのカーネルを定義して、Filter2Dメソッドを使って畳み込み演算を行います。カーネルの値はマイナスの値も含まれるので、MatTypeにはCV_64FC1を指定していることに注意して下さい。

実行結果は次のようになります。
f:id:nn_hokuson:20210423201627j:plain:w500

論理演算を使ってマスクする

OpenCVで論理演算をするサンプルです。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// 黒色に白丸のマスク(カラー画像の場合はmaskも3chぶん用意する)
Mat mask = new Mat(mat.Width, mat.Height, MatType.CV_8UC3, new Scalar(0,0,0));
Cv2.Circle(mask, new Point(mat.Width / 2, mat.Height / 2), 200, new Scalar(255,255,255), -1);

// 論理演算
Mat dst = new Mat();
Cv2.BitwiseAnd(mat, mask, dst);

// 書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(dst, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVの論理演算にはBitwiseAnd、BitwiseOr、BitwiseNot、BitwiseXOrが用意されています。1chのグレースケール画像に対して論理演算をする場合はマスクも1chで大丈夫ですが、3chのカラー画像に対して論理演算をする場合はマスク画像も3chぶん用意する必要があります。カラー画像に対して1chのマスクを使うとRレイヤのみマスクされてしまいます。

実行結果は次のようになります。
f:id:nn_hokuson:20210424063000j:plain:w500

画像のレイヤを分ける

複数チャネルを持つ画像をレイヤごとに分割するサンプルです。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// 各レイヤを取り出す
Mat[] rgba = Cv2.Split(mat);

// Rレイヤを書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(rgba[2], outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture; 

複数レイヤを分割するにはSplitメソッドを使用します。戻り値はMatの配列になります。また、逆に各レイヤを1つの画像に合成する場合はMergeメソッドを使います。

実行結果は次のようになります。
f:id:nn_hokuson:20210425072655j:plain:w500

画像を左右反転する

OpenCVで読み込んだ画像を左右反転するサンプルです。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// 左右反転
Mat dst = new Mat();
Cv2.Flip(mat, dst, FlipMode.Y);

//画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(dst, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

画像を左右反転するにはFlipメソッドを使います。第3引数で縦方向に反転するか、横方向に反転するかを選ぶことができます。

実行結果は次のようになります。
f:id:nn_hokuson:20210424063128j:plain:w500

図形を描画する

OpenCVで円や四角形を描画するサンプルです。

Mat mat = new Mat(512, 512, MatType.CV_8UC3, new Scalar(30, 30, 30));

// 円
Cv2.Circle(mat, new Point(100, 100), 50, new Scalar(255, 0, 0), 3, LineTypes.AntiAlias);

// 円塗りつぶし
Cv2.Circle(mat, new Point(300, 100), 50, new Scalar(255, 0, 0), -1, LineTypes.AntiAlias);

// 四角形
Cv2.Rectangle(mat, new OpenCvSharp.Rect(50, 180, 300, 100), new Scalar(0, 0, 255), 3, LineTypes.AntiAlias);

//画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVで円を描画するにはCircleメソッドを使用します。第2引数に円の中心座標、第3引数に半径、第5引数に線の太さを指定します。線の太さをマイナスの値にすると図形の内部が塗りつぶされます。また、第6引数のLineTypesにAntiAliasを指定することで、ギザギザのないなめらかな図形を描画できます。

OpenCVで四角形を描画するにはRectangleメソッドを使います。第2引数のRectメソッドで左上座標と幅、高さを指定します。塗りつぶしやLineTypesはCircleメソッドと同様です。

実行結果は次のようになります。
f:id:nn_hokuson:20210423202108j:plain:w500

Arucoマーカーを作成する

OpenCVにはARマーカーを検出するためのArucoライブラリが付属しています(実際にはOpenCV Contribモジュールに含まれているものです)。次のスクリプトはArucoマーカーを作成するためのサンプルです。

Dictionary dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict6X6_250);

int size = 150;
Mat mat = new Mat();
CvAruco.DrawMarker(dictionary, 5, size, mat);

Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

作成するARマーカーはGetPredefinedDictionaryの引数で指定します。ここではDict6X6_250を指定しているので6セルx6セルのサイズのマーカー(外側の黒枠も入れると8セルx8セル)を250個まで生成できます。実際に生成するにはDrawMarkerメソッドを使用します。このDrawMarkerメソッドの第2引数で250個のうち何番目のマーカーを生成するかを指定します。

作成したマーカーは次のようになります。
f:id:nn_hokuson:20210423202957p:plain:w200

Arucoマーカーを検出する

上で生成したArucoマーカーを撮影した画像中から検出するサンプルです。

DetectorParameters detectorParameters = DetectorParameters.Create();

Dictionary dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict6X6_250);

Point2f[][] corners;
int[] ids;
Point2f[][] rejectedImgPoints;

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール化
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// Arucoマーカーの検出
CvAruco.DetectMarkers(gray, dictionary, out corners, out ids, detectorParameters, out rejectedImgPoints);
CvAruco.DrawDetectedMarkers(mat, corners, ids);

//画像書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

Arucoマーカーを検出するにはDetectMarkersメソッドを使用します。出力結果として、マーカーの四隅の座標とマーカーのIDを受け取ることができます。この情報を使ってマーカーの姿勢を推定することもできます。姿勢推定の手法はコチラの記事が詳しいです。

qiita.com

OpenCVでピクセルにアクセスする

ピクセルにアクセスして画像の左上だけ白色に塗りつぶすサンプルです。

Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

unsafe
{
	byte* data = mat.DataPointer;
	int width = mat.Width;
	int height = mat.Height;
	int channel = mat.Channels();

	for (int y = 0; y < height/4; y++)
	{
		for (int x = 0; x < width/4 ; x++)
		{
			int idx = (x + y * width) * channel;
			data[idx+0] = 255;
			data[idx+1] = 255;
			data[idx+2] = 255;
		}
	}
}

// 書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVのMatクラスにはAtメソッドが用意されていて、これでもピクセル(画素)にアクセスすることはできます。ただ、このメソッドは呼び出しに時間がかかるため、全ピクセルを処理するには向いていません。高速にピクセルにアクセスするためには、DataPointerで取得できるポインタを使います。ポインタを使う箇所はunsafeブロックで囲む必要があります。

ピクセルの座標を(x,y)、画像の幅と高さを(width, height)、チャネル数をchannelとすると、そのピクセルのインデックスは (x + y * width) * channelで調べられます。

実行結果は次のとおりです。
f:id:nn_hokuson:20210425074450j:plain:w500

顔検出をする

画像中から顔を検出して、その位置を表示するサンプルです。

// 画像読み込み
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture);

// グレースケール
Mat gray = new Mat();
Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// カスケード分類器の準備
CascadeClassifier haarCascade = new CascadeClassifier("Assets/OpenCV+Unity/Demo/Face_Detector/haarcascade_frontalface_default.xml");

// 顔検出
OpenCvSharp.Rect[] faces =  haarCascade.DetectMultiScale(gray);

// 顔の位置を描画
foreach (OpenCvSharp.Rect face in faces)
{
	Cv2.Rectangle(mat, face, new Scalar(0, 0, 255), 3);
}

// 書き出し
Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false);
OpenCvSharp.Unity.MatToTexture(mat, outTexture);

// 表示
GetComponent<RawImage>().texture = outTexture;

OpenCVで顔検出をするにはカスケード分類器を使います。カスケード分類器のパラメータはOpenCV plus Unityの/OpenCV+Unity/Demo/Face_Detector/に含まれているので、CascadeClassifierのコンストラクタでパスを指定して読み込みます。顔検出はDetectMultiScaleメソッドを使います。検出した顔の領域は、DetectMultiScaleメソッドの戻り値としてRect型の配列で受け取ります。

実行結果は次のようになります。
f:id:nn_hokuson:20210425074942j:plain:w500

【Arduino】赤外線を使って通信する(後編)

f:id:nn_hokuson:20210406214650j:plain

赤外線通信をする(前編)では赤外線通信に欠かせない搬送波の作成までを作りました。この記事では赤外線送信機と受信機を作っていきます。

nn-hokuson.hatenablog.com

目次は次のとおりです。

赤外線の送信機をつくる

それでは実際に赤外線通信の送信機を作ってみましょう。送信機といっても赤外線LEDとスイッチだけで構成された簡易的なものです。ハードウエアの構成は次のようになります。
f:id:nn_hokuson:20210414204200j:plain:w350

PWM出力をする10番ポートに赤外線LEDを接続します。また、4番ポートにタクトスイッチを接続します。このスイッチを押している間だけ赤外線通信が行われるようにします。今回は次の赤外線LEDを使用しました。

赤外線通信プログラム(Arduino版)

次にArduinoで赤外線の送信プログラムを作成しましょう。データのフォーマットは前回の記事で紹介したとおりです。このフォーマットに合わせてPWM出力をON/OFFします。次のプログラムをArduinoに書き込んでみましょう。

int top = 0;
const int onTime = 480; 
const int offTime = 380;

const unsigned char data[] = {0,1,0,0,1,0,1,1};

void setup() {
   pinMode(10, OUTPUT );    
   pinMode(4, INPUT_PULLUP);

  TCCR1A = B00100011;
  TCCR1B = B00011010;
  top = F_CPU / 38000 / 8 - 1;
  OCR1AL = top; 
  OCR1BL = OCR1AL / 3;
}

void sendData()
{
    //リーダ部
    OCR1A = top;
    _delay_us(3400);
    OCR1A = 0; 
    _delay_us(1700);

    //データ部
    for(int i = 0; i < 8; i++){
       OCR1A = top; 
       _delay_us(onTime);
       OCR1A = 0; 
       if(data[i]){ 
          _delay_us(offTime*3); 
       }
       else{   
          _delay_us(offTime);  
       }
    }

    //ストップ部
    OCR1A = top; 
    _delay_us(onTime);
    OCR1A = 0; 
    _delay_ms(65);
}

void loop() 
{
  if(digitalRead(4) == LOW){
    sendData();
  }
}

このプログラムではsetup関数の中でPWMを使って38kHzの搬送波を出力するように、レジスタの設定を行っています。レジスタの設定値については前回の記事を参照下さい。

nn-hokuson.hatenablog.com

sendData関数の中では赤外線通信のフォーマットに従って、ヘッダ部、データ部、ストップビット部の波形を作っています。OCR1A(PWM用のカウンタの上限値)を0設定するとPWMが停止することを利用して、ON/OFFを行っています。

赤外線通信プログラム(AVR版)

赤外線の送信機は、小型化したり、電池駆動にしたい場合も多いと思います。その場合はArduinoを使うのではなく、直接AVRを使うことになります。そこでAVR用のプログラムも参考程度に乗せておきます。下記はATTiny85を使用した場合のプログラムです。

//ATTiny85用のプログラム
#define F_CPU 1000000UL
#define F_IR  38000L  
#include <util/delay.h>

int top = 0;
const int onTime = 480; 
const int offTime = 380;
int Limit = 1000;

const unsigned char data[] =  = {0,1,0,0,1,0,1,1};
int prevD = LOW;

void setup() 
{
  pinMode(1, OUTPUT );
  pinMode(4, INPUT_PULLUP);

  // AVRだとdelayにTCCR0A/Bのタイマーを使うのでTCCR1を使う
  TCCR1 = B11010001;
  top = F_CPU / 38000  -  1; 
  OCR1C = top; 
  OCR1A = OCR1C / 3;  
}

void sendData()
{
    //リーダ部
    OCR1C = top; 
    _delay_us(3400); //AVRでdelayMicrosecondsはx8倍されるバグあり
    OCR1C = 0;
    _delay_us(1700);

    //データ部
    for(int i = 0; i < 8; i++){
       OCR1C = top; 
       _delay_us(onTime);
       OCR1C = 0; 
       if(data[i]){
          _delay_us(offTime*3);
       }
       else{  
          _delay_us(offTime);
       }
    }

    //ストップ部
    OCR1C = top;       
    _delay_us(onTime);
    OCR1C = 0;  
    _delay_ms(65);
}

void loop() 
{
  if( digitalRead(4) == LOW){
    sendData();
  }
}

Arduinoを使った赤外線送信プログラムとほぼ同じですが、タイマーのレジスタにはTCCR1A/Bの代わりにTCCR1 を使っています。また、タイマーの上限値はOCR1Cレジスタに、デューティー比はOCR1Aレジスタに設定します。

AVRにプログラムを書き込むには、専用のライターを使うのが簡単です。Arduino IDEを使ってAVRにプログラムを書き込む方法は次の記事を参考にして下さい。

nn-hokuson.hatenablog.com

赤外線の受信機をつくる

最後に赤外線の受信機を作成しましょう。使用した赤外線受信モジュールは下記のものです。赤外線受信モジュール自体は一般的なものなので、秋月電子などで購入したものでも同じような構成で動くはずです。

受信機のハードウエアは次のとおりです。赤外線持っジュールのスペックシートによると、正面から見て一番右の足がVcc、真ん中がGND、左がデータになっています。そこで、赤外線受信モジュールの左のピンをArduinoの7番ポートに接続しています。

f:id:nn_hokuson:20210414211415j:plain:w350

赤外線を受信するArduinoのプログラムは次のとおりです。

int input_pin = 7;

void setup()
{
    Serial.begin(9600);   
    pinMode(input_pin, INPUT); 
}

void loop()
{
  unsigned long dt; 
  int i , cnt;
  int receiveData[8];
  dt = 0;
  
  if (digitalRead(input_pin) == LOW) { 
    dt = micros();                             
    while (digitalRead(input_pin) == LOW) ;
    dt = micros() - dt;  
  }

  if (dt >= 3400) {  
    i = 0 ;
    while(digitalRead(input_pin) == HIGH) ;   
    while (1) {
      while(digitalRead(input_pin) == LOW) ; 
      dt = micros(); 
      cnt = 0 ;
      while(digitalRead(input_pin) == HIGH) { 
        delayMicroseconds(10) ; 
        cnt++ ;
        if (cnt >= 1200){ 
          break ;
        }
      }
      dt = micros() - dt;  
      if(dt >= 10000){ 
        break;
      }
      receiveData[i] = (dt >= 800) ? 1 : 0; 
      i++;
    }

    // 受信データの表示
    if (i != 0) {
      for(int j=0; j<i; j++){
        Serial.print(",");
        Serial.print(data[j]);
     }
    }
  }
}

この記事の最初にArduinoで作った赤外線送信機のボタンを押して、受信機でちゃんと正しいデータが送られてくることを確認しましょう!

まとめ

Arduinoで赤外線通信をする(前編)では赤外線通信に必要なフォーマットや、搬送波の作り方を説明しました。またArduinoで赤外線通信をする(後編)では赤外線の送信機と受信機を作りながらArduinoで赤外線通信をする方法を紹介しました。

【Arduino】赤外線を使って通信する(前編)

f:id:nn_hokuson:20210406214650j:plain

電子工作をしていると、作った機器を無線化したい場合が時々あります。方法としてはBLEや315Mhzの無線、Wifiを使うなどの方法があります。ただ、これらの方法は少し大掛かりになってしまうため、簡単に作れて取り回しの良い、赤外線通信の方法を紹介したいと思います。

この記事では、前編で赤外線通信で使用する38kHzの搬送波を作ります。また、後編では赤外線の送信機と、赤外線の受信機をそれぞれ作ることにします。目次は次のとおりです。

赤外線通信の概要

赤外線通信はTVのリモコンや、Nintendo DS、ガラケーなどの通信に使われている無線通信技術です。赤外線LEDを一定のパターンで光らせることでデータを送信します。

f:id:nn_hokuson:20210406192901p:plain:w400

このとき、単に赤外線LEDをON/OFFしてデータを送ることもできますが、通常手法は取られていません。これは太陽光やシーリングライトにも赤外線が含まれており、これらの外光がノイズになって機器が誤動作してしまうためです。

そのため、赤外線通信する場合は38kHzの搬送波で変調してデータを送信します。変調というと難しそうですが、ONのとき赤外線LEDを38kHzで発振させ、OFFのときは赤外線を消灯します。

f:id:nn_hokuson:20210406202700p:plain:w450

赤外線ONのときをデータの「1」、OFFのときをデータの「0」に割り当てることもできますが、これだと受信側でのデコードがシビアになります。どういうことかというと、次のようにデータのサンプリングのタイミングを厳密に合わせないと、データのデコードにミスってしまうのです。

そこで、少し冗長ではありますが、次のように最初にON、その後にOFFの期間を設けます。ONに続くOFFの時間が800us未満であればデータ「1」、800us以上であればデータ「0」とします。このようにすることで、受信側では確実にデータをデコードすることができるようになります。

f:id:nn_hokuson:20210406202746p:plain

最後にデータの最初と最後を定義するパターンも用意してやります。ここでは、ONの時間が3400us以上続くパターンをヘッダ、OFFの時間が10000us以上のパターンをストップビットとして使用します。

f:id:nn_hokuson:20210406204220p:plain:w500

Arduinoで赤外線通信をするために必要な基礎知識はこれでOKです。次は実際にArduinoで38kHzの搬送波を作っていくことにします。

38kHzの搬送波を作る

まずは最初に搬送波を作ります。ここで作る搬送波は38kHzでONとOFFを繰り返す矩形波になります。Arduinoを使ってこの矩形波を作るにはPWM(Pulse Width Modulation)という機能を使います。

PWMを使うには次の4つの手順に従って、それぞれのレジスタにパラメータを設定する必要があります。

  1. PWMに使うポート(出力ポート)を指定する
  2. TCCR1AとTCCR1Bのレジスタにパラメータをセットする
  3. OCR1Aのレジスタで周波数を決める
  4. OCR1Bでデューティ比を決める

それぞれの手順について、詳細に確認していきましょう。

PWMに使うポートを指定する

ArduinoのPWMで使用できるポートは9番と10番ポートです。ここでは10番ポートからPWMを出力して赤外線LEDを光らせるので、次のようにして10番ポートを出力に設定します。

pinMode(10, OUTPUT );    

TCCR1AとTCCR1Bのレジスタにパラメータをセットする

ここがPWM出力のキモになる設定です。TCCRx系のレジスタは、タイマーに使用するレジスタです。タイマーに使えるレジスタはTCCR0A/BとTCCR1A/Bがあるのですが、TCCR0系はArduinoの内部処理(delay関数など)に使われているので、プログラマが使えるのはTCCR1系になります。

TCCR1AとTCCR1Bの設定項目をまとめると下図のようになります。

f:id:nn_hokuson:20210406205437p:plain:w500

詳しくは公式のドキュメントを参照してください。
https://avr.jp/user/DS/PDF/megaXX8PA.pdf:image=https://avr.jp/user/DS/PDF/megaXX8PA.pdf

■ 使用ポートの設定
TCCR1Aの4〜7ビット目で使用するポートを宣言します。9番ポートを使う場合は6~7ビットを10に、使用しない場合は00に設定します。10番ポートを使う場合は4~5ビットを10に、使用しない場合は00に設定します。

■ タイマーモードの設定
TCCR1Aの0〜1ビット目とTCCR1Bの3〜4ビット目を使ってタイマーモードを設定します。主な設定項目には次のようなものがあります。ここではPWM出力をするため全ビットを1に設定します。

出力モード 設定値
標準 0000
CTC 0100
高速PWM 1111

■ 分周の設定
TCCR1Bの0〜1ビット目でシステムクロックの分周率を決めます。分周とは、システムクロック周波数を1/n倍して、より周波数の低いクロックを作成する仕組みです。

分周を使うことで、タイマーにシステムクロックよりも低い周波数を使うことができます。クロックを設定値は次の通りで、ここではArduino Unoのシステムクロック16Mhzを8分周して使いたいので「010」に設定します。

分周率 設定値
分周なし 000
分周1/8 010
分周1/64 011
分周1/256 100
分周1/1024 101

OCR1AとOCR1Bのレジスタにパラメータをセットする

OCR1Aのレジスタにはタイマーの上限値(=リセットする時間)を設定します。この上限値によって、下図のように生成するPWMの周波数が決まります。OCR1Bではデューティー比(PWMがONになっている期間とOFFになっている時間の比)を決めます。タイマーの値がOCR1Bで設定した値よりも小さい場合はON、大きい場合はOFFになります。

f:id:nn_hokuson:20210406210949p:plain:w500

赤外線通信では38kHzの搬送波を作りたいので、PWMで作成する波形の周期は1/38kHz=0.00002631578947秒となります。クロックの周期はシステムクロックを8分周しているため、1/(16Mhz/8)=1/2Mhz=0.00000005秒です。したがって、0.00002631578947÷0.00000005=51カウントしたときに、リセットがかかるようにOCR1Aには51を設定します。

f:id:nn_hokuson:20210406212052p:plain:w300

また、赤外線通信では電力消費を考えてデューティー比を3:1にします。そのためOCR1BにはOCR1Aで設定した値の1/3(=17)を設定します。

搬送波のプログラム

ここまでで38kHzの搬送波を作る方法がわかりました。これをふまえて、プログラムを作ってみましょう。

void setup() {
  pinMode(10, OUTPUT );    
  TCCR1A = B00100011;
  TCCR1B = B00011010;
  OCR1AL = F_CPU / 38000L / 8 - 1;  
  OCR1BL = OCR1AL / 3; 
}

上のプログラムでは10番ポートを出力に設定し、TCCR1A/BのレジスタでPWM出力を行うように設定しています。OCR1Aにはタイマの上限値を設定しています。F_CPUはArduinoで定義されている定数でシステムクロック周波数(Arduino Unoでは16Mhz)が格納されています。また、OCR1Bにはデューティー比が1/3になるように設定しています。

実行して、10番ポートにオシロスコープをあてて出力波形を見てみてください(ここでは赤外線LEDは接続しなくても大丈夫です)。次のように38kHzの矩形波が出力されていれば成功です。

f:id:nn_hokuson:20210406214949j:plain:w330

後編では、ここで作成した38kHzの搬送波を使って赤外線の送信機と、赤外線の受信機をそれぞれ作ることにします。後編の記事はコチラ!
nn-hokuson.hatenablog.com

使用したパーツなど

今回のようにPWM出力を確認したいときにはオシロスコープがあると便利です。今回のように、観測したい周波数が100kHz程度でよければ安価な製品もあるので、チェックしてみてください!