Bigdra.Log

備忘録

Unityエディターが使用中のadbを取得する

はじめに

UnityでAndroid開発をしている際に、 開発に使用中のUnityエディタが使用しているadbをCLIから呼び出したいことがあります。

例えば、 wi-fi経由でadb接続して無線で Build And RunAndroid Logcat など使用したいときです。

https://qiita.com/niusounds/items/ceacb5291c80dcbb4041

このような場合には、Unityエディタが使用するadbでAndroidバイスと接続している必要があります。

そのためには、 Unityバージョン毎に適切なadbパスを指定して実行する必要がありますが、毎回長いadbのパスを指定するのも面倒です。

そこで、 カレントディレクトリのUnityバージョン用のadbを取得するシェル用の関数を用意しました。

本題

Unityで使用されるadbは、 Unityエディタの各バージョン毎に用意されています。

そして、 adbは次の場所にあります。

UnityEditorのインストール場所/20xx.x.xf1/Editor/Data/PlaybackEngines/AndroidPlayer/SDK/platform-tools/

UnityEditorのインストール場所はUnityHubの環境設定から確認できます。

Unityプロジェクトが使用しているエディタのバージョン情報は、 ProjectSettings/ProjectVersion.txt に記述されています。

m_EditorVersion: 2021.3.6f1
m_EditorVersionWithRevision: 2021.3.6f1 (7da38d85baf6)

ここからUnityバージョンのみ取り出します。

function unity-version {
    cat .\ProjectSettings\ProjectVersion.txt | Select-String "m_EditorVersion:" | %{$($_-split(" "))[1]}
}
function unity-version(){
    cat ./ProjectSettings/ProjectVersion.txt | grep "m_EditorVersion:" | awk -F" " '{print $2 }'
}

このコマンドをUnityプロジェクトのルートで実行すると、 2021.3.6f1 のようにそのプロジェクトのUnityエディタバージョンが出力されます。

次に、 adbパスを取得します。

function uadb {
    & "E:\archives/UnityEditor/$(unity-version)\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\platform-tools\adb.exe" $args
}
function uadb {
    eval "/Applications/Unity/Hub/Editor/${unity-version}/PlayBackEngines/AndroidPlayer/SDK/platform-tools/adb" $@
}

Unityエディタのインストール場所は適宜置き換えてください。

これで、 uadb というコマンドでUnityエディタが使用しているadbを呼び出せるようになりました。

$ uadb --version                                                                      ─╯
Android Debug Bridge version 1.0.41
Version 30.0.4-6686687
Installed as E:\archives\UnityEditor\2021.3.6f1\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\platform-tools\adb.exe

OpenCV for Unityで異なる画像上の顔を入れ替える

はじめに

OpenCV for UnityとDlib FaceLandmark Detectorは有料ですが、 FaceSwapperExampleは無料で公開されています。
FaceSwapperExampleに顔入れ替えのサンプルはありましたが、 1枚の画像内に写っている顔同士を入れ替えるというものでした。
この記事では、 そのサンプルを基に異なる画像内の顔同士を入れ替える例を紹介します。

検証環境

  • Windows10 Home
  • Unity2020.3.14f1
  • OpenCV for Unity v2.4.4
  • Dlib FaceLandmark Detector v1.3.2
  • FaceSwapperExample v1.1.0

ソースコード

OpenCV for UnityとDlib FaceLandmark Detector で異なる画…

f:id:bigdra50:20210731142609p:plain

流れ

初期化

顔検出用のクラスを初期化します。

テクスチャ読み込み

入れ替えたい顔が写っている2枚のテクスチャから、 OpenCVでの処理で使う行列Matを生成します。

画像の連結

元のサンプルは1枚の画像内の顔を入れ替えるものだったので、 ここで2枚のテクスチャを連結しました。
連結するテクスチャは同じサイズである必要があるので、 まずはImgproc.resizeでサイズを揃えます。
次に、 Core.vconcatで第1引数のリスト内の画像を連結し、 結果を第2引数へ格納します。

f:id:bigdra50:20210731144923p:plain

顔の位置を検出

ここから顔入れ替えまでは元のサンプル通りです。

DlibのFaceLandmarkDetector または、 HaarCascadeを使って顔の位置を検出します。

顔の形状を検出

前のステップでは顔の位置の矩形が取得できました。 ここでは顔の入れ替えのために顔の形状を検出します。
f:id:bigdra50:20210731150043p:plain

正面を向いていない顔を除外

検出した結果の中から、 正面をむいていないものを除外します。
どの程度まで許容するかを_frontalFaceRateLowerLimitで設定できます。

顔入れ替え

DlibFaceSwapperクラスを使って顔の入れ替えを実行します。
f:id:bigdra50:20210731150636p:plain

連結した部分を再分割

最後に、 連結していた1枚の画像を2枚に再分割してテクスチャへ変換して完了です。
Matクラスのコンストラクタに切り抜きたい領域をRect型で指定することでその領域のみのMatを生成できるため、 上半分と下半分の領域を指定すると分割できます。

NRSDK 1.6.0 Release Note 確認

概要

NRSDK v1.6.0が2021.06.30に公開されました。

新機能

ハンドトラッキングの追加(ベータ段階)

ついにNReal Lightでハンドトラッキングができるようになりました!
精度が高く、サンプルシーンではジェスチャの検出やUI・3Dオブジェクトとのインタラクションを試すことができます。

以下のように各手の23箇所のキーポイントをトラッキングします。

f:id:bigdra50:20210703221138p:plain

また、 6種類のジェスチャーを認識することができます。

一般的なジェスチャー

f:id:bigdra50:20210703221114p:plain

選択用のジェスチャー

f:id:bigdra50:20210703221119p:plain

  • 人差し指と親指をつまんでいれば、他の指に関わらずPinchと判定されます。

システムジェスチャー

f:id:bigdra50:20210703221123p:plain

  • このジェスチャーを1.2秒間続けるとホームメニューが呼び出されます。

ベータ版のため、 このバージョンのハンドトラッキング機能は2022.12.31までしか動作しないようです。

UnityXR Pluginへのアクセスの追加

https://github.com/nreal-ai/NRSDK-XR-Plugin.git をPackage Managerから追加すると使えるようになります。

f:id:bigdra50:20210703221126p:plain

検証中

NRSDK環境用のクイックセットアップツールの追加

NRSDK/Project Tips

f:id:bigdra50:20210703221131p:plain

ビルド前にやらないといけない設定がこのウィンドウから手軽にできます。

f:id:bigdra50:20210703221134p:plain

一人称視点のビデオ映像を撮影しながら音声を録音する機能の追加

以前のバージョンのMR録画機能では音声が入らなかったのですが、録音機能が追加されました。

この機能を使用するためにはマイクの権限が必要なので、Assets/Plugins/Android/AndroidManifest.xml に以下の行を追加します。

<uses-permission android:name="android.permission.RECORD_AUDIO" />

アプリ内の音ではなくマイクから入力された音を録音しているため、 ノイズが結構入ってしまっています。

警告メッセージとイベントの追加

Android 11に対応

Nebulaの設定がMRアプリに同期される(左手モード、省電力モードなど)

f:id:bigdra50:20210703221110j:plain

最適化

  • 空間コンピューティングのパフォーマンスを最適化
  • レンダリングパフォーマンスの最適化
  • 安静時の最適化された画像追跡
  • 6DoF / 3DoF / 0DoFコントローラートラッキングモード間の動的切り替えを最適化

修正点

  • 画面の記録中に発生する可能性があったクラッシュを修正
  • 無効なRGBCameraを呼び出すとAPIがクラッシュする問題を修正
  • 起こりうるメモリリークを修正
  • いくつかのインスタンスでのみ発生するウィンドウジッターを修正
  • 不安定な電話接続で使用中に発生したクラッシュを修正
  • 既存の問題を修正

参考

https://developer.nreal.ai/download

https://nrealsdkdoc.readthedocs.io/en/v1.6.0/index.html

非対応キャリアのスマホでNebula2を使う

概要

Nebula とは、NrealブランドのMRグラスのために設計された、3Dユーザーインターフェースシステムです。Nebulaは、2Dコンテンツをインタラクティブな仮想3D空間に投影するとともに、NrealのMRグラスを直感的に操作できるように、スマートフォンでおなじみのインターフェース機能を保持しています。

Nebulaの対応デバイスは以下の通りです。

キャリア専用Nebula
f:id:bigdra50:20210626011754p:plain

Nebula2スタンダード
f:id:bigdra50:20210626011818p:plain

Samsung Galaxy Note 20 Ultra 5Gはdocomoauから発売されていますが、現時点で対応しているのはau版のみのようです。

この記事はdocomo版のGalaxy Note 20 Ultra 5G(SC-53A)でNebula2を動かせたのでそのメモです。

手順

  1. こちらからNebula2のapkをインストールする。(NRealの開発者向けSlack)で配布されていたものです。)
  2. "対応していません"というメッセージを10回タップする。 f:id:bigdra50:20210626011910p:plain

  3. Developer Optionsが表示されるので、開発者モードを有効にする。

f:id:bigdra50:20210626011937p:plain

これでNebula2が使えるようになります。

さいごに

NReal関連の情報は公式Slackによく流れているのでぜひ参加してみてください!

OpenCV for Unityで画像処理100本ノック 11~20

f:id:bigdra50:20210215145821p:plain

1~10はこちら

bigdra50.hatenablog.com

前回は画像処理部分をなるべく自分で実装していましたが,今回はライブラリ使っています.

リポジトリ

github.com

ライブラリ使うコードはOneHubdredKnock.A, なるべく使わず実装してるのはOneHundredKnock.Bに置きます.

Q11 平滑化フィルタ

f:id:bigdra50:20210215145903p:plain
平滑化フィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// 平滑化フィルタ
    /// フィルタ内の画素の平均値を出力するフィルタ
    /// </summary>
    public class Q11 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat();
            var size = 3;
            Imgproc.blur(src, dst, new Size(size, size));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q12 モーションフィルタ

f:id:bigdra50:20210215145949p:plain
モーションフィルタ
Imgproc.filter2Dの引数でカーネルを指定すれば任意のフィルタをかけられるようなので, これ以降のOpenCV側で用意されていない(または見つけられなかった)フィルターに関しては, カーネルの行列を入れ替えただけのコードになります.

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// モーションフィルタ
    /// 対角方向の平均値を取るフィルタ
    ///     |1/3 0  0|
    /// k = |0  1/3 0|
    ///     |0  0 1/3|
    /// </summary>
    public class Q12 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat();
            var kernel = new MatOfFloat(
                1f / 3f, 0f, 0f,
                0f, 1f / 3f, 0f,
                0f, 0f, 1f / 3f);
            Imgproc.filter2D(src, dst, -1, kernel);
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q13 MAX-MINフィルタ

f:id:bigdra50:20210215150017p:plain
MAX-MINフィルタ

これはカーネルがわからなかったので自分で実装しました.

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// MAX-MINフィルタ
    /// フィルタ内の画素の最大最小の差を出力するフィルタで, エッジ検出のフィルタの一つ
    /// エッジ検出では多くの場合, グレースケール画像に対してフィルタリングを行う
    /// </summary>
    public class Q13 : MonoBehaviour
    {
        [SerializeField] private int _size = 3;

        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGB2GRAY);
            var edgeCols = new byte[dst.rows(), dst.cols()];

            for (var x = 0; x < dst.width(); x++)
            {
                for (var y = 0; y < dst.height(); y++)
                {
                    var max = byte.MinValue;
                    var min = byte.MaxValue;
                    for (var dx = -_size / 2; dx <= _size / 2; dx++)
                    {
                        for (var dy = -_size / 2; dy <= _size / 2; dy++)
                        {
                            if (x + dx >= 0 && x + dx < dst.width() && y + dy >= 0 && y + dy < dst.height())
                            {
                                var col = new byte[4];
                                dst.get(x + dx, y + dy, col);
                                if (col[0] > max) max = col[0];
                                if (col[0] < min) min = col[0];
                            }
                        }
                    }

                    edgeCols[x, y] = (byte) (max - min);
                }
            }

            for (var x = 0; x < dst.width(); x++)
            {
                for (var y = 0; y < dst.height(); y++)
                {
                    dst.put(x, y, new[]
                    {
                        edgeCols[x, y],
                        edgeCols[x, y],
                        edgeCols[x, y],
                        edgeCols[x, y],
                    });
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q14 微分フィルタ

f:id:bigdra50:20210215150112p:plain
微分フィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// 微分フィルタ
    /// 輝度の急激な変化が起こっている部分のエッジを取り出すフィルタ
    /// 隣り合う画素同士の差を取る
    ///          |0 -1  0|           | 0  0  0|
    /// 縦: K =  |0  1  0|   横: K = |-1  1  0|
    ///         |0  0  0|           | 0  0  0|
    /// </summary>
    public class Q14 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k_v = new[]
            {
                0f, -1f, 0f,
                0f, 1f, 0f,
                0f, 0f, 0f
            };
            var k_h = new[]
            {
                0f, 0f, 0f,
                -1f, 1f, 0f,
                0f, 0f, 0f
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k_v));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q15 Prewittフィルタ

f:id:bigdra50:20210215150203p:plain
Prewittフィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// Prewittフィルタ
    /// エッジ抽出フィルタの1種
    /// 微分フィルタを3x3に拡大したもの
    ///          | 1  1  1|          | 1  0 -1|
    /// 縦: K =  | 0  0  0|   横: K = | 1  0 -1|
    ///         |-1  -1 -1|          | 1  0 -1|
    /// </summary>
    public class Q15 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k_v = new[]
            {
                1f, 1f, 1f,
                0f, 0f, 0f,
                -1f, -1f, -1f
            };
            var k_h = new[]
            {
                1f, 0f, -1f,
                1f, 0f, -1f,
                1f, 0f, -1f
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k_v));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q16 Sobelフィルタ

f:id:bigdra50:20210215150237p:plain
Sobelフィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// Sobelフィルタ
    /// エッジ抽出フィルタの1種
    /// prewittフィルタの中心部分に重みを付けたフィルタ
    ///          | 1  2  1|          | 1  0 -1|
    /// 縦: K =  | 0  0  0|   横: K = | 2  0 -2|
    ///         |-1  -2 -1|          | 1  0 -1|
    /// </summary>
    public class Q16 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k_v = new[]
            {
                1f, 2f, 1f,
                0f, 0f, 0f,
                -1f, -2f, -1f
            };
            var k_h = new[]
            {
                1f, 0f, -1f,
                2f, 0f, -2f,
                1f, 0f, -1f
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k_v));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q17 Lapcacianフィルタ

f:id:bigdra50:20210215150308p:plain
Laplacianフィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// Laplacianフィルタ
    /// エッジ抽出フィルタの1種
    /// 輝度の2次微分
    ///      | 0  1  0|
    /// K =  | 1 -4  1| 
    ///      | 0  1  0|
    /// </summary>
    public class Q17 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k = new[]
            {
                0f, 1f, 0f,
                1f, -4f, 1f,
                0f, 1f, 0f
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q18 Embossフィルタ

f:id:bigdra50:20210215150343p:plain
Embossフィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// Embossフィルタ
    /// 輪郭部分を浮き出しにするフィルタ
    ///      |-2 -1  0|
    /// K =  |-1  1  1| 
    ///      | 0  1  2|
    /// </summary>
    public class Q18 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_256x256");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k = new[]
            {
                -2f, -1f, 0f,
                -1f, 1f, 1f,
                0f, 1f, 2f
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q19 LoGフィルタ

f:id:bigdra50:20210215150421p:plain
LoG(Laplacian of Gaussian)フィルタ

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using UnityEngine;

namespace OneHundredKnock.A
{
    /// <summary>
    /// LoG(Laplacian of Gaussian)フィルタ
    /// ガウシアンフィルタで平滑化した後にラプラシアンフィルタで輪郭を取り出すフィルタ
    /// Laplacianは2次微分をとるのでノイズが強調されるのを防ぐために, 予めGaussianでノイズを抑える
    /// 
    ///     |0  0  1  0  0|
    ///     |0  1  2  1  0|
    /// K = |1  2 -16 2  1|
    ///     |0  1  2  1  0|
    ///     |0  0  1  0  0|
    /// </summary>
    public class Q19 : MonoBehaviour
    {
        private void Start()
        {
            var src = Util.LoadTexture("imori_noise");
            var dst = new Mat(src.rows(), src.cols(), CvType.CV_8UC4);
            var k = new[]
            {
                0f, 0f, 1f, 0f, 0f,
                0f, 1f, 2f, 1f, 0f,
                1f, 2f, -16f, 2f, 1f,
                0f, 1f, 2f, 1f, 0f,
                0f, 0f, 1f, 0f, 0f,
            };
            Imgproc.cvtColor(src, dst, Imgproc.COLOR_RGBA2GRAY);
            //Imgproc.GaussianBlur(gray, gauss, new Size(5,5), 3d);
            //Imgproc.Laplacian(gauss, dst, -1, 5);
            Imgproc.filter2D(dst, dst, -1, new MatOfFloat(k));
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dst);
        }
    }
}

Q20 ヒストグラム表示

OpenCV for Unityでは多分できないし必要性を感じないので省略

参考記事

Gasyori100knock/Question_11_20 at master · yoyoyo-yo/Gasyori100knock · GitHub 【Python/OpenCV】空間フィルタリングで平滑化・輪郭検出 | 西住工房 【画像処理】LoGフィルタの原理・特徴・計算式 | 西住工房

OpenCV for Unityで画像処理100本ノック 1~10

f:id:bigdra50:20210214201613p:plain

経緯

OpenCV for Unity を使う機会があり、その練習がてら画像処理100本ノックをやってみます。

assetstore.unity.com

最初の10本はなるべくライブラリを使わずに画像処理の部分を実装します。

リポジトリはこちら

github.com

準備

OpenCVで画像の加工をする際には、

テクスチャを入力→Matという形式に変換→Matを加工→テクスチャへ変換

という流れになるようです。

この部分の処理は以下のクラスを用意して使いまわしました。

using OpenCVForUnity.CoreModule;
using OpenCVForUnity.UnityUtils;
using UnityEngine;

namespace OneHundredKnock
{
    public class Util
    {
        public static Mat LoadTexture(string path)
        {
            var srcTex = Resources.Load(path) as Texture2D;
            var srcMat = new Mat(srcTex.height, srcTex.width, CvType.CV_8UC4);
            Utils.texture2DToMat(srcTex, srcMat);
            return srcMat;
        }

        public static Texture2D MatToTexture2D(Mat imgMat)
        {
            var texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);
            Utils.matToTexture2D(imgMat, texture);
            return texture;
        }
    }
}

f:id:bigdra50:20210214201958p:plain
imori_256x256

Q1 チャネル入れ替え

f:id:bigdra50:20210214202101p:plain
チャネル入れ替え

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    public class Q1 : MonoBehaviour
    {
        private void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);
            var col = new byte[4];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    srcMat.get(x, y, col);
                    var tmp = col[0];
                    col[0] = col[2];
                    col[2] = tmp;
                    dstMat.put(x, y, col);
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q2 グレースケール化

f:id:bigdra50:20210214202133p:plain
グレースケール化

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// グレースケール化
    /// 画像の輝度表現方法の一種で下式で計算される
    /// Y = 0.2126R + 0.7151G + 0.0722B
    /// </summary>
    public class Q2 : MonoBehaviour
    {
        void Start()
        {
            var imgMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC4);
            //Imgproc.cvtColor(imgMat, dstMat, Imgproc.COLOR_RGBA2GRAY);

            var rgba = new byte[4];
            for (var x = 0; x < imgMat.width(); x++)
            {
                for (var y = 0; y < imgMat.height(); y++)
                {
                    imgMat.get(x, y, rgba);
                    var gr = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2];
                    rgba[0] = (byte)gr;
                    rgba[1] = (byte)gr;
                    rgba[2] = (byte)gr;
                    dstMat.put(x, y, rgba);
                }
            }
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q3 2値化

f:id:bigdra50:20210214202209p:plain
2値化

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// 2値化
    /// 画像を黒と白の2値で表現する方法.
    /// ここではグレースケールにおいて閾値を128に設定して2値化する
    /// </summary>
    public class Q3 : MonoBehaviour
    {
        private void Start()
        {
            var imgMat = Util.LoadTexture("imori_256x256");

            var dstMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC4);
            //Imgproc.cvtColor(imgMat, dstMat, Imgproc.COLOR_RGBA2GRAY);
            //Imgproc.threshold(dstMat, dstMat, 128, 255, Imgproc.THRESH_BINARY);
            var rgba = new byte[4];
            for (var x = 0; x < imgMat.width(); x++)
            {
                for (var y = 0; y < imgMat.height(); y++)
                {
                    imgMat.get(x, y, rgba);
                    var gray = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2];
                    var col = (byte)(gray < 128 ? 0 : 255);
                    rgba[0] = col;
                    rgba[1] = col;
                    rgba[2] = col;
                    dstMat.put(x, y, rgba);
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q4 大津の2値化

f:id:bigdra50:20210214202245p:plain
大津の2値化

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// 大津の2値化
    /// 2値化における分離の閾値を自動決定するアルゴリズム. クラス内分散とクラス間分散の比から計算される
    /// </summary>
    public class Q4 : MonoBehaviour
    {
        private void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = BinarizeByOtsu(srcMat);
            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }

        private float[,] ToGrayScale(Mat srcMat)
        {
            var rgba = new byte[4];
            var grs = new float[srcMat.rows(), srcMat.cols()];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    srcMat.get(x, y, rgba);
                    grs[x, y] = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2];
                }
            }

            return grs;
        }

        private int GetThreshByOtsu(float[,] grs, int pNum)
        {
            var (thresh, max) = (1, 0f);
            for (var t = 1; t < 255; t++)
            {
                var m0 = 0f; // 各クラス内の画素の輝度の平均
                var m1 = 0f;
                var p0 = 0; // 各クラスに含まれる画素数
                var p1 = 0;
                foreach (var gr in grs)
                {
                    if (gr < t)
                    {
                        // class 0
                        p0++;
                        m0 += gr;
                    }
                    else
                    {
                        // class 1
                        p1++;
                        m1 += gr;
                    }
                }

                m0 /= p0;
                m1 /= p1;
                var r0 = p0 / (float) pNum;
                var r1 = p1 / (float) pNum;
                var sbsb = r0 * r1 * (m0 - m1) * (m0 - m1); // クラス間分散

                if (sbsb < max) continue;
                thresh = t;
                max = sbsb;
            }

            return thresh;
        }

        private Mat BinarizeByOtsu(Mat srcMat)
        {
            var grs = ToGrayScale(srcMat);
            var pNum = srcMat.rows() * srcMat.cols();
            var thresh = GetThreshByOtsu(grs, pNum);

            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);
            var rgba = new byte[4];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    var col = (byte) (grs[x, y] < thresh ? 0 : 255);
                    rgba[0] = col;
                    rgba[1] = col;
                    rgba[2] = col;
                    dstMat.put(x, y, rgba);
                }
            }

            return dstMat;
        }
    }
}

Q5 HSV変換

f:id:bigdra50:20210214202328p:plain
HSV変換

using System;
using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// HSV変換
    /// 色相Hを反転(180を加算)してRGBになおす
    /// </summary>
    public class Q5 : MonoBehaviour
    {
        private void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);

            var rgba = new byte[4];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    srcMat.get(x, y, rgba);
                    var hsv = Rgb2Hsv(new Rgb(rgba[0], rgba[1], rgba[2]));
                    hsv.h += 180;
                    var rgb = Hsv2Rgb(hsv);
                    rgba[0] = (byte) rgb.r;
                    rgba[1] = (byte) rgb.g;
                    rgba[2] = (byte) rgb.b;
                    dstMat.put(x, y, rgba);
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }

        private Hsv Rgb2Hsv(Rgb rgb)
        {
            var r = rgb.r / 255f;
            var g = rgb.g / 255f;
            var b = rgb.b / 255f;

            var max = Math.Max(r, Math.Max(g, b));
            var min = Math.Min(r, Math.Min(g, b));

            var hsv = new Hsv();
            hsv.h = (int) (
                Math.Abs(min - max) < float.Epsilon ? 0f :
                Math.Abs(min - b) < float.Epsilon ? 60f * ((g - r) / (max - min)) + 60f :
                Math.Abs(min - r) < float.Epsilon ? 60f * ((b - g) / (max - min)) + 180f :
                60f * ((r - b) / (max - min)) + 300f);
            hsv.h %= 360;
            hsv.v = (int) (max * 100);
            hsv.s = (int) ((max - min) / max * 100);
            return hsv;
        }

        private Rgb Hsv2Rgb(Hsv hsv)
        {
            var v = hsv.v * .01f;
            if (hsv.s == 0) return new Rgb((int) (v * 255));
            hsv.h %= 360;
            var s = hsv.s * .01f;

            var hi = hsv.h / 60;
            var ha = hsv.h / 60f % 1;
            var a = (int) (v * 255f);
            var b = (int) (v * (1f - s) * 255f);
            var c = (int) (v * (1f - s * ha) * 255f);
            var d = (int) (v * (1f - s * (1f - ha)) * 255f);

            Rgb rgb;
            switch (hi)
            {
                case 0:
                    rgb = new Rgb(a, d, b);
                    break;
                case 1:
                    rgb = new Rgb(c, a, b);
                    break;
                case 2:
                    rgb = new Rgb(b, a, d);
                    break;
                case 3:
                    rgb = new Rgb(b, c, a);
                    break;
                case 4:
                    rgb = new Rgb(d, b, a);
                    break;
                case 5:
                    rgb = new Rgb(a, b, c);
                    break;
                default:
                    rgb = new Rgb(a);
                    break;
            }

            return rgb;
        }

        struct Rgb
        {
            public Rgb(byte col)
            {
                r = col;
                g = col;
                b = col;
            }

            public Rgb(int col)
            {
                r = col;
                g = col;
                b = col;
            }

            public Rgb(byte r, byte g, byte b)
            {
                this.r = r;
                this.g = g;
                this.b = b;
            }

            public Rgb(int r, int g, int b)
            {
                this.r = r;
                this.g = g;
                this.b = b;
            }

            public int r;
            public int g;
            public int b;
        }

        struct Hsv
        {
            public int h;
            public int s;
            public int v;
        }
    }
}

Q6 減色処理

f:id:bigdra50:20210214202434p:plain
減色処理

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// 画像の値を256^3から4^3, すなわちR,G,B in {32, 96, 160, 224}の各4値に減色する.(量子化操作)
    /// </summary>
    public class Q6 : MonoBehaviour
    {
        void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);
            var color = new byte[4];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    srcMat.get(x, y, color);
                    for (var i = 0; i < color.Length; i++)
                    {
                        switch (color[i] / 64)
                        {
                            case 0:
                                color[i] = 32;
                                break;
                            case 1:
                                color[i] = 96;
                                break;
                            case 2:
                                color[i] = 160;
                                break;
                            case 3:
                                color[i] = 224;
                                break;
                        }

                        dstMat.put(x, y, color);
                    }
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);

        }
    }
}

Q7 平均プーリング

f:id:bigdra50:20210214202520p:plain
平均プーリング

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// 平均プーリング
    /// 画像をグリッド分割し. 各領域内の平均値でその領域内の値を埋める.
    /// </summary>
    public class Q7 : MonoBehaviour
    {
        [SerializeField] private int _grid = 16;

        private void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);
            var color = new byte[4];
            for (var x = 0; x < srcMat.width(); x += _grid)
            {
                for (var y = 0; y < srcMat.height(); y += _grid)
                {
                    // グリッド内の領域の色の平均を求める
                    var additive = new int[4];
                    for (var dx = 0; dx < _grid; dx++)
                    {
                        for (var dy = 0; dy < _grid; dy++)
                        {
                            srcMat.get(x + dx, y + dy, color);
                            for (var i = 0; i < 3; i++)
                            {
                                additive[i] += color[i];
                            }
                        }
                    }
                    for (var i = 0; i < 3; i++)
                    {
                        color[i] = (byte) (additive[i] / (_grid * _grid));
                    }

                    for (var dx = 0; dx < _grid; dx++)
                    {
                        for (var dy = 0; dy < _grid; dy++)
                        {
                            dstMat.put(x + dx, y + dy, color);
                        }
                    }
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q8 Maxプーリング

f:id:bigdra50:20210214202550p:plain
Maxプーリング

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// Maxプーリング
    /// 平均値でなく最大値でプーリングする
    /// </summary>
    public class Q8 : MonoBehaviour
    {
        [SerializeField] private int _grid = 16;
        void Start()
        {
            var srcMat = Util.LoadTexture("imori_256x256");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);

            for (var x = 0; x < srcMat.width(); x += _grid)
            {
                for (var y = 0; y < srcMat.height(); y += _grid)
                {
                    var col = new byte[4];
                    var maxCol = new byte[4];
                    
                    for (var dx = 0; dx < _grid; dx++)
                    {
                        for (var dy = 0; dy < _grid; dy++)
                        {
                            srcMat.get(x+dx, y+dy, col);
                            for (var i = 0; i < col.Length; i++)
                            {
                                maxCol[i] = col[i] > maxCol[i] ? col[i] : maxCol[i];
                            }
                        }
                    }

                    for (var dx = 0; dx < _grid; dx++)
                    {
                        for (var dy = 0; dy < _grid; dy++)
                        {
                            dstMat.put(x+dx, y+dy, maxCol);
                        }
                    }
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q9 ガウシアンフィルタ

今度はこのノイズ混じりの画像を入力に使います.

f:id:bigdra50:20210214202829p:plain
imori_noise

f:id:bigdra50:20210214202627p:plain
ガウシアンフィルタ

using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// ガウシアンフィルタ
    /// 画像の平滑化を行うフィルタの一種であり, ノイズ除去にも使われる.
    /// ガウシアンフィルタは注目がその周辺画素を, ガウス分布による重み付けで平滑化する.
    ///
    /// </summary>
    public class Q9 : MonoBehaviour
    {
        private void Start()
        {
            var srcMat = Util.LoadTexture("imori_noise");
            var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4);
            var kernel = new double[,]
            {
                {1d / 16d, 2d / 16d, 1d / 16d,},
                {2d / 16d, 4d / 16d, 2d / 16d,},
                {1d / 16d, 2d / 16d, 1d / 16d,},
            };

            var col = new byte[4];
            var src = new byte[srcMat.width(), srcMat.height(), 3];
            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    srcMat.get(x, y, col);
                    for (var i = 0; i < src.GetLength(2); i++)
                    {
                        src[x, y, i] = col[i];
                    }
                }
            }

            for (var x = 0; x < srcMat.width(); x++)
            {
                for (var y = 0; y < srcMat.height(); y++)
                {
                    var additive = new byte[4];
                    for (var dx = -kernel.GetLength(0) / 2; dx <= kernel.GetLength(0) / 2; dx++)
                    {
                        for (var dy = -kernel.GetLength(0) / 2; dy <= kernel.GetLength(0) / 2; dy++)
                        {
                            if (x + dx < 0 || x + dx >= srcMat.width() || y + dy < 0 || y + dy >= srcMat.height())
                                continue;
                            for (var i = 0; i < additive.Length - 1; i++)
                            {
                                var tmp1 = src[x + dx, y + dy, i];
                                var tmp2 = kernel[dx + kernel.GetLength(0) / 2, dy + kernel.GetLength(0) / 2];
                                additive[i] += (byte) (tmp1 * tmp2);
                            }
                        }
                    }

                    dstMat.put(x, y, additive);
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat);
        }
    }
}

Q10 メディアンフィルタ

f:id:bigdra50:20210214202701p:plain
メディアンフィルタ

using System.Collections.Generic;
using OpenCVForUnity.CoreModule;
using UnityEngine;

namespace OneHundredKnock.B
{
    /// <summary>
    /// メディアンフィルター
        /// 注目画素の3x3の領域内の、メディアン値(中央値)を出力するフィルタ
    /// </summary>
    public class Q10 : MonoBehaviour
    {
        private void Start()
        {
            var srcImg = Util.LoadTexture("imori_noise");
            var dstImg = new Mat(srcImg.rows(), srcImg.cols(), CvType.CV_8UC4);
            var color = new byte[4];
            const int grid = 3;
            var srcRgb = new byte[srcImg.width(), srcImg.height(), 4];
            for (var x = 0; x < srcImg.width(); x++)
            {
                for (var y = 0; y < srcImg.height(); y++)
                {
                    srcImg.get(x, y, color);
                    for (var i = 0; i < 3; i++)
                    {
                        srcRgb[x, y, i] = color[i];
                    }
                }
            }

            for (var x = 0; x < srcImg.width(); x++)
            {
                for (var y = 0; y < srcImg.height(); y++)
                {
                    var medRgb = new List<List<byte>>
                    {
                        new List<byte>(),
                        new List<byte>(),
                        new List<byte>(),
                    };
                    for (var dx = -grid / 2; dx <= grid / 2; dx++)
                    {
                        for (var dy = -grid / 2; dy <= grid / 2; dy++)
                        {
                            if (x + dx < 0 || x + dx >= srcImg.width() || y + dy < 0 || y + dy >= srcImg.height())
                                continue;
                            for (var i = 0; i < medRgb.Count; i++)
                            {
                                medRgb[i].Add(srcRgb[x + dx, y + dy, i]);
                            }
                        }
                    }

                    for (var i = 0; i < medRgb.Count; i++)
                    {
                        medRgb[i].Sort();
                        color[i] = medRgb[i][medRgb[i].Count / 2];
                    }

                    dstImg.put(x, y, color);
                }
            }

            GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstImg);
        }
    }
}

Q11以降はOpenCVのライブラリをガンガン使っていきます

参考記事

Documentation – OpenCV for Unity Gasyori100knock/Question_01_10 at master · yoyoyo-yo/Gasyori100knock · GitHub 画像処理100本ノック!!(001 - 010)丁寧にじっくりと - Qiita OpenCV plus Unityを使ってみる(セットアップ、画像処理100本ノック1~10編) - Qiita 大津の二値化ってなんだ…ってなった. - Qiita HSV色空間 - Wikipedia ツール|色の変換(RGB・HSV・HSL) | 矢野ヒロタ 【C#】ガウシアンフィルタで画像のぼかし(ノイズ除去) | 西住工房 3x3のメディアンフィルタの高速化をC#で書いてみた - 午後わてんのブログ

Windowsの常駐アプリを作る

経緯

この記事でWindowsの常駐アプリが必要になったので、 作り方を調べてみます。

https://bigdra50.hatenablog.com/entry/2020/12/27/085509

本題

準備

WinFormアプリとして以下のような設定でプロジェクトを作成します。

タスクトレイに表示するアイコンが必要です。
icoファイルを用意し、 ソリュージョンエクスプローラーから用意したicoファイルを右クリック→プロパティ→Copy if newerとして、ビルド時に出力先のディレクトリへコピーされるようにしておきます。

常駐させる

NotifyIconクラスで通知領域へアイコンを作成します。

using System;
using System.Windows.Forms;

namespace WinFormsApp1
{
    static class Program
    {
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var taskTray = new TaskTray();
            Application.Run();
        }
    }
}
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace WinFormsApp1
{
    public class TaskTray
    {
        private NotifyIcon _notifyIcon;

        public TaskTray()
        {
            Initialize();
        }

        private void Initialize()
        {
            _notifyIcon = new NotifyIcon {Icon = new Icon(@"./favicon.ico"), Visible = true, Text = "NotifyIcon"};

            var contextMenuStrip = new ContextMenuStrip();
            contextMenuStrip.Items.AddRange(new ToolStripItem[]
            {
                new ToolStripMenuItem("&Exit", null, (_, _) => Exit(), "Exit")
            });
            _notifyIcon.ContextMenuStrip = contextMenuStrip;
        }

        private void Exit()
        {
            var e = new CancelEventArgs();
            Application.Exit(e);
            if (e.Cancel)
            {
                Console.WriteLine("Application.Exit is canceled!");
            }
        }
    }
}

Windows起動時にアプリを自動で起動させる

「スタートアップ」フォルダの中にexeのショートカットを作成します。

private static void CreateShortcut()
{
    // Application.ExecutablePathがなぜかdotnet.exeのパスを返すから, 代わりにdllのパスを取得してexeに変更する
    var path = Assembly.GetEntryAssembly()?.Location;
    if (path == null) return;
    var exePath =
        Path.ChangeExtension(Path.Combine(Application.StartupPath, Path.GetFileName(path)), "exe");
    var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup),
        $"{Path.GetFileNameWithoutExtension(path)}.lnk");

    // WshShellを作成
    var type = Type.GetTypeFromCLSID(new Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8"));
    dynamic shell = Activator.CreateInstance(type);

    //WshShortcutを作成
    var shortcut = shell.CreateShortcut(shortcutPath);

    shortcut.TargetPath = exePath;
    shortcut.WorkingDirectory = Application.StartupPath;
    shortcut.Description = $"場所: {exePath}";
    shortcut.IconLocation = $@"{Application.StartupPath}/favicon.ico";
    shortcut.Save();

    //後始末
    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shortcut);
    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shell);
}

参考

C#のタスクトレイ常駐アプリの作り方のご紹介!
C# - Windows常駐アプリ(タスクトレイ) - Form表示なし
OS起動時にプログラムを自動的に実行する OS起動時に一回だけプログラムを自動的に実行する
C# ショートカットを作成する