Raspberry PiでレトロゲームをLet's Play (1)

どうもー、Reveです。
最近、スマホゲームじゃないレトロなゲームがやりたいと思っていたのですが、どうやら

【準備する機材】
今回は必要な機材を主に紹介していきます。
まずは箇条書きで紹介していきます。

[必須]
・Raspberry Pi (2か3がおすすめ)
・micro SDカード (32GB, Class10, UHS-1程度の性能で十分)
・ゲームコントローラー
・USBマウス、キーボード
・HDMI端子付きモニター (テレビでも可)
・USB電源プラグ (スマートフォンの充電用アダプタでも代用可)
・ネットワーク環境
・PC
[推奨]
・Raspberry Pi用ケース (できればファン付き)
・Raspberry Pi用ヒートシンク
・スイッチングハブ

では、いくつかの項目について解説を加えていこうと思います。

【解説】
Raspberry Pi
Raspberry Pi2 はPi3と違い、BluetoothもWifiもUSBドングルをつけないと使えませんが、消費電力は少なくて済みます。
ですが、実はRaspberry Pi3のほうが安く買えたり...。
逆に、Raspberry Pi3のほうが単体でネットに接続できたり、性能も若干Pi2より高いのですが、逆に消費電力が高いためUSB電源プラグも注意する必要があります(5V, 2.1A以上)
なお、Raspberry Piは初代のModel AやModel B+、zeroなどもありますが、ゲームをエミュレートするのは負荷が高いため、なるべく性能の高いモデルを選んでおいたほうがよいでしょう。
 

micro SDカード
Raspberry Piを動かすOSやソフトウェアのデータは、すべてマイクロSDカードにインストールすることになります。
そのため、容量はある程度大きいほうがいいのですが、あまり大容量のゲームや数多くのゲームを保存する限りでなければ32GBで十分かと思います。
あと、SDカードは秋葉原で安く買えるので、余裕のある方は秋葉原で探すのをお勧めします。


Raspberry PI用ケース / ヒートシンク
ケースは好きなものでよいのですが、ゲームを動かすのはそれなりに負荷がかかるので、ファンやヒートシンクも用意しておくとよいでしょう。この商品であればまとめて購入できます。


スイッチングハブ
また、Wifiモバイルルーターしか持っていない方も、スイッチングハブがあればRaspberry PiでWifiの設定をしなくてもLANケーブルをつなげるだけでネットと接続できたり、PCからゲームのファイルを転送するのも簡単になります。
接続は Wifiルーター <--> スイッチングハブ <--> PC / Raspberry Pi という形になります。


コントローラー
ゲームをプレイするには、もちろんコントローラーが必要となるわけですが、実はPCのゲームをプレイするのにもXBOXのコントローラーが使いやすいともっぱら評判なので、ここではUSBのXBOX360コントローラーを載せています。
ちなみに、当方では右のエレコムのコントローラーが安く買えたのでそっちを使っています。


PC / ネットワーク環境
この2つに関してはもうこのブログを見ている方はお持ちかと思いますが(笑)、Raspberry PiのOSは基本的に配布先のウェブサイトからダウンロードしてSDカードに書き込むのが前提となっているため、ネットワーク環境とSDにOSのファイルを書き込むためのPCが必要となってきます。

ここまででおおよそ必要な機材はそろったので、次回はOSのインストールから起動まで入っていきます。

MMD4MecanimとUnity最新版について

MMDのモデルをUnityで動かす際に使われるのが
MMD4Mecanim
http://stereoarts.jp/

なのですが、現行バージョンのUnityではエラーが出てしまいます。
というのも、SkeletonBoneクラスのtransformModifiedというプロパティが廃止されてしまったので、その部分でエラーが発生します。

(最新バージョンのMMD4Mecanimは確認していないのですが)
すでに前のバージョンを入れた状態でUnityを最新版に引き上げた場合は、skeletonBone.transformModifiedの記述がある部分だけコメントアウトすれば動かせます。
幸い、ツールの中ではMMD4MecanimImporterRig.csの2か所しか無かったため、その2つだけコメントアウトすればOKでした。

以上、雑記でした。

AndroidでFragmentの遷移時にパラメータを渡すときは

newInstanceメソッドでFragmentのインスタンスを作って引数を与えて、Fragment画面に移動するようにしましょう。
(忘れていたので備忘録も兼ねてます...)

Androidアプリで別の画面に切り替える処理を実装する際、Fragmentを使った画面遷移がよくつかわれると思うのですが、前の画面での設定などを取り込む必要がある時は、newInstanceメソッドでFragmentのインスタンスを作る必要があります。

newInstanceメソッドは、Android StudioでFragmentを自動生成した際のサンプルコードにあらかじめ作られるメソッドで、基本的な処理も一緒に実装されます。ただ、引数の型などを自分の受け渡したいものに合わせて変えていく必要があります。
なお、デフォルトでは引数はstring型が2つとなっています。

public static YourFragment newInstance(...){
// pre-coded...
}


newInstanceの使い方は、呼び出し側のActivityでFragmentの切り替えを行う際に使います。
定義したFragmentのインスタンスを作る際、"new (フラグメント名)"のようにnewを使うのではなく、(クラス名).newInstanceという形でFragmentのインスタンスを生成します。
あとは、生成したインスタンスをFragmentManagerに入れて(addまたはreplace)、commitを呼び出せばOK。

//create setting fragment
//YourFragment fragment = new YourFragment(); // doesn't work
//fragment.newInstance(...); // doesn't work
YourFragment fragment = YourFragment.newInstance(...); // assign your preferable parameters

//call the fragment
//(...write your own cord)


【参考】
Tech Sheets: Fragmentで、パラメータ付きの画面遷移を実装する
https://tech.mokelab.com/android/Fragment/argument.html

テーマ : Android
ジャンル : 携帯電話・PHS

AndroidのAlertDialogで躓いた話

どうも、Reveです。
最近Androidアプリの開発をする機会がぐんと増えたのですが、なんか初歩的なところで躓いていたので備忘録でも残します。

【ボタンを押すとエラーで落ちる】
エラーが発生したのは、ボタン(FAB)を押すとAlertDialogが呼ばれる機能を実装していた時のことで、
ビルドは通るものの実行してボタンを押すと、以下のエラーが発生して強制終了してしまいました。

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity

どうやらTheme.AppCompatというテーマを適用する必要があると...
かれこれ色々悩んでいたのですが、原因はAlertDialog.Builderを作る際のContextにありました。

【原因】
AlertDialog.Builderのインスタンスを作る際、引数にContextを指定する必要があるのですが、
これが曲者で、間違えてApplicationContextを指定していたのが原因でした。

//AlertDialog.Builder builder = new AlertDialog.Builder(getApplicationContext()); //エラー発生
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); //正解

正確には、builder.show()を実行してダイアログを表示するときにエラーが発生するのですが、getApplicationContextで得られるContextでは、どうやらテーマのリソースにアクセスできないようでした。

そのため、ActivityのContextを取得することでエラーが解消された、とみています。
いや、Androidアプリの開発はめんどくさい...(ただの勉強不足)

【参考】
ちなみに、対処法はここで見つけました。
(stack overflow: Android AlertDialog error during showing)
https://stackoverflow.com/questions/41894944/android-alertdialog-error-during-showing

どうやら、以前にも、同じような現象に陥っていた方がいらっしゃいました。
(SLUMBERS: Android で アラートダイアログを表示しようとすると落ちる)
http://slumbers99.blogspot.jp/2012/01/android.html

また、こちらの記事では各Contextの違いについてまとめられており、記事の内容からテーマのリソースなどを参照するにはActivityのContextが必要で、getApplicationContextでは取得できないと推察しているのですが、果たして正解なのか...
(Yukiの枝折: Android:引数はthisか?getApplicationContextか?ActivityとApplicationの違い)
http://yuki312.blogspot.jp/2012/02/thisgetapplicationcontextactivityapplic.html

テーマ : Android
ジャンル : 携帯電話・PHS

UnityでOpenCV その4

さて、なんやかんや2か月以上も過ぎていましたがorz
いよいよ前回の実装に入ります。
OpenCVUnity_findcontour1.png

抽出処理については、基本的にこちらの記事を参考にしました。
OpenCVで輪郭抽出から隣接領域の切り出し(その1)輪郭抽出まで

では、抽出処理の流れに沿って、それぞれの処理を見てみましょう。
なお、ここから先は画像処理に関係する処理を主に抜粋して載せています。
追記に当方の作成した全ソースコードを載せているため、先にそちらをご覧いただいてもかまいません。

【前処理】
まずは、元画像、および画像処理とマスク用のMat型インスタンスをそれぞれ用意しておきます。
画像処理用のMatには、元画像のグレースケールを格納します。

//元画像を、リソースフォルダからTexture2Dとして読み込む
Texture2D texture_src = Resources.Load(texturePath) as Texture2D;

//元画像のMatを用意
Mat imgMat= new Mat(src.height, src.width, CvType.CV_8UC4);
//Texture2DをMatに変換(OpenCVUnityのUtilクラス内メソッド)
Utils.texture2DToMat(src, imgMat);

//画像処理用Mat画像
Mat mat_proc = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);
//元画像のグレースケールを入れておく
Imgproc.cvtColor(imgMat, mat_proc, Imgproc.COLOR_RGBA2GRAY);
//マスク用Mat画像
Mat maskMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);


また、二値化処理をかけて輪郭抽出に適した画像(モノクロ画像)にしておきます。
二値化処理はいくつか方法があり、まず手動で調整するとこのようになります。

//mat_procは画像処理の対象(Mat)、thresは二値化の閾値(0~255)
//Imgproc.THRESH_BINARY_INV: 閾値より大きい値は0に,それ以外はmaxValに
Imgproc.threshold(mat_proc, mat_proc, thres, 255, Imgproc.THRESH_BINARY_INV);


ただ、閾値は画像の状態によって大きく左右されるため、いちいち手動で値を設定するのは面倒です。
そこで、画像に適した閾値を自動で計算してくれるアルゴリズムを用いた方法もあるため、下で紹介していきます。
まずは大津の手法による二値化処理です。

//大津の手法による、単純2値化処理
Imgproc.threshold(mat_proc, mat_proc, 0, 255, Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);


ただし、先ほどの手法でも画素内の明るさが大きく異なる部分があると、処理の結果が芳しくないことがあります。
そこで、それぞれの場所に応じた閾値を設定できる、適応的閾値処理を使うことも可能です(引数の詳しい説明などは省きます)。

//GaussianCによる適応的閾値処理
//adaptiveMethod: Imgproc.ADAPTIVE_THRESH_MEAN_C と分けて使う
Imgproc.adaptiveThreshold(mat_proc, mat_proc, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY_INV, 201, 11);


【輪郭抽出】
ここまで、輪郭抽出に必要な二値化画像が手に入った段階です。
いよいよ輪郭の抽出に入っていきます。

まずは全体の流れから見ていきましょう。

//procMode: 輪郭抽出の手法を決める列挙型ExtractMode
//ThresArea: 輪郭を抽出したい領域の最小面積

//輪郭
List<MatOfPoint> contours = new List<MatOfPoint>();

//処理状態によってマスク処理に使うMat画像を切り替える
//2値化画像を直接使う
if (procMode == (int)ExtractMode.BINARY)
{
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//コピー
mat_proc.copyTo(maskMat);
}
//2値化画像の輪郭を切り取って使う
else if (procMode == (int)ExtractMode.CONTOUR)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE); //輪郭内にある輪郭もツリー構造で抽出

//領域(面積)の割合が一定以上の輪郭を探す: getCertainAreaContour
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
contours.Add(point);
}

//マスク領域の生成
//Imgproc.drawContours の線の太さを負の値にすると、内部の領域も塗りつぶす
Imgproc.drawContours(maskMat, contours, -1, new Scalar(255), -1);
}
//2値化画像の輪郭を直線近似して領域を求める
else if (procMode == (int)ExtractMode.POLYLINE)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)の割合が一定以上の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint approxf = getLineApproxContour(point, 0.001);
contours.Add(approxf);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}
//2値化画像の輪郭を凸包して領域を求める
else if (procMode == (int)ExtractMode.HULL)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)の割合が一定以上の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint mopOut = getHullContour(point);
contours.Add(mopOut);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}


実は、上記のスクリプトでは一部、自作のメソッドを用いていたのですが、
各メソッドの実装内容は以下の通りです。

/// <summary>
/// 抽出した輪郭リストから一定以上の面積を持つ輪郭を返す
/// </summary>
/// <param name="contours">輪郭のリスト</param>
/// <param name="thresArea">面積の閾値</param>
/// <returns>面積が閾値以上の輪郭すべて</returns>
List<MatOfPoint> getCertainAreaContour(List<MatOfPoint> contours, double thresArea = 10)
{
List<MatOfPoint> result = new List<MatOfPoint>();

foreach (var each in contours)
{
//面積を求める
double area = Imgproc.contourArea(each);
if (area > thresArea) result.Add(each);
}

return result;
}

/// <summary>
/// 輪郭(MatOfPoint)から直線近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <param name="approxRate">カーブの細かさ</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getLineApproxContour(MatOfPoint mopIn, double approxRate = 0.001)
{
MatOfPoint approxf1 = new MatOfPoint();
MatOfPoint2f curveContour = new MatOfPoint2f(mopIn.toArray());
MatOfPoint2f approxContour = new MatOfPoint2f();
Imgproc.approxPolyDP(curveContour, approxContour, approxRate * Imgproc.arcLength(curveContour, true), true);

//直線近似した輪郭を変換
approxContour.convertTo(approxf1, CvType.CV_32S);

return approxf1;
}

/// <summary>
/// 輪郭(MatOfPoint)から凸包近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getHullContour(MatOfPoint mopIn)
{
MatOfPoint mopOut = new MatOfPoint();

//輪郭を凸包
MatOfInt hull = new MatOfInt();
Imgproc.convexHull(mopIn, hull);

//凸包した輪郭を変換
int size = (int)hull.size().height;
mopOut.create(size, 1, CvType.CV_32SC2);
for (int i = 0; i < size; i++)
{
int index = (int)hull.get(i, 0)[0];
double[] point = new double[] { mopIn.get(index, 0)[0], mopIn.get(index, 0)[1] };
mopOut.put(i, 0, point);
}

return mopOut;
}


【仕上げ】
ここまで、画像の特定の領域に対する輪郭抽出ができました。
ですが、わずかに残るノイズを処理するため、以下の処理を加えて不要な部分を除去します。

//ノイズ除去
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//膨張処理(dilate)
Imgproc.dilate(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//オープニング
Imgproc.morphologyEx(maskMat, maskMat, Imgproc.MORPH_OPEN, new Mat());


【マスク処理】
あとは抽出できた輪郭を使い、画像の一部分だけを取り出す処理を行ってみましょう。

//マスク処理
Mat imgResult = new Mat(src.height, src.width, CvType.CV_8UC4, new Scalar(255, 255, 255));
imgMat.copyTo(imgResult, maskMat); //元画像のマットにマスクを当てはめたうえで結果画像(Mat)にコピー


こうして、輪郭抽出の大まかな流れは終わりです。
以上の処理をもとに輪郭抽出した結果はこちらです。
OpenCVUnity_findcontour2.png

Unityで画像処理をする機会は少ないかもしれませんが、当方の記事が少しでも参考になれば幸いです。
ではまた。

続きを読む

プロフィール

Reveちゃん

Author:Reveちゃん
コンビでやってます。
夢担当と技術担当がいます。

大学院卒業 → ロボットベンチャー(漆黒)就職 → 1年で退職 → ベトナムで仕事中(今ここ) → メディアアーティスト(未来☆)

リンクフリーです。

最新記事
最新コメント
月別アーカイブ
カテゴリ
アクセス数
検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR