Unityエディターが使用中のadbを取得する
はじめに
UnityでAndroid開発をしている際に、 開発に使用中のUnityエディタが使用しているadbをCLIから呼び出したいことがあります。
例えば、 wi-fi経由でadb接続して無線で Build And Run
や Android 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 で異なる画…
流れ
初期化
顔検出用のクラスを初期化します。
テクスチャ読み込み
入れ替えたい顔が写っている2枚のテクスチャから、 OpenCVでの処理で使う行列Mat
を生成します。
画像の連結
元のサンプルは1枚の画像内の顔を入れ替えるものだったので、 ここで2枚のテクスチャを連結しました。
連結するテクスチャは同じサイズである必要があるので、 まずはImgproc.resize
でサイズを揃えます。
次に、 Core.vconcat
で第1引数のリスト内の画像を連結し、 結果を第2引数へ格納します。
顔の位置を検出
ここから顔入れ替えまでは元のサンプル通りです。
DlibのFaceLandmarkDetector
または、 HaarCascade
を使って顔の位置を検出します。
顔の形状を検出
前のステップでは顔の位置の矩形が取得できました。 ここでは顔の入れ替えのために顔の形状を検出します。
正面を向いていない顔を除外
検出した結果の中から、 正面をむいていないものを除外します。
どの程度まで許容するかを_frontalFaceRateLowerLimit
で設定できます。
顔入れ替え
DlibFaceSwapper
クラスを使って顔の入れ替えを実行します。
連結した部分を再分割
最後に、 連結していた1枚の画像を2枚に再分割してテクスチャへ変換して完了です。
Mat
クラスのコンストラクタに切り抜きたい領域をRect
型で指定することでその領域のみのMat
を生成できるため、 上半分と下半分の領域を指定すると分割できます。
NRSDK 1.6.0 Release Note 確認
概要
NRSDK v1.6.0が2021.06.30に公開されました。
新機能
ハンドトラッキングの追加(ベータ段階)
ついにNReal Lightでハンドトラッキングができるようになりました!
精度が高く、サンプルシーンではジェスチャの検出やUI・3Dオブジェクトとのインタラクションを試すことができます。
— bigdra (@bigdra50) July 3, 2021
以下のように各手の23箇所のキーポイントをトラッキングします。
また、 6種類のジェスチャーを認識することができます。
ジェスチャーは6種類 pic.twitter.com/M5UMrBTcMh
— bigdra (@bigdra50) July 3, 2021
一般的なジェスチャー
選択用のジェスチャー
- 人差し指と親指をつまんでいれば、他の指に関わらずPinchと判定されます。
システムジェスチャー
- このジェスチャーを1.2秒間続けるとホームメニューが呼び出されます。
ベータ版のため、 このバージョンのハンドトラッキング機能は2022.12.31までしか動作しないようです。
UnityXR Pluginへのアクセスの追加
https://github.com/nreal-ai/NRSDK-XR-Plugin.git をPackage Managerから追加すると使えるようになります。
検証中
NRSDK環境用のクイックセットアップツールの追加
NRSDK/Project Tips
ビルド前にやらないといけない設定がこのウィンドウから手軽にできます。
一人称視点のビデオ映像を撮影しながら音声を録音する機能の追加
以前のバージョンのMR録画機能では音声が入らなかったのですが、録音機能が追加されました。
この機能を使用するためにはマイクの権限が必要なので、Assets/Plugins/Android/AndroidManifest.xml に以下の行を追加します。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
録音もできるようになったけど、アプリ内の音ではなくてマイクの音を使うからPCファンのノイズがひどいw
— bigdra (@bigdra50) July 3, 2021
あとなんかこもってる pic.twitter.com/aFD8HNhPf9
アプリ内の音ではなくマイクから入力された音を録音しているため、 ノイズが結構入ってしまっています。
警告メッセージとイベントの追加
Android 11に対応
Nebulaの設定がMRアプリに同期される(左手モード、省電力モードなど)
最適化
- 空間コンピューティングのパフォーマンスを最適化
- レンダリングパフォーマンスの最適化
- 安静時の最適化された画像追跡
- 6DoF / 3DoF / 0DoFコントローラートラッキングモード間の動的切り替えを最適化
修正点
- 画面の記録中に発生する可能性があったクラッシュを修正
- 無効なRGBCameraを呼び出すとAPIがクラッシュする問題を修正
- 起こりうるメモリリークを修正
- いくつかのインスタンスでのみ発生するウィンドウジッターを修正
- 不安定な電話接続で使用中に発生したクラッシュを修正
- 既存の問題を修正
参考
非対応キャリアのスマホでNebula2を使う
概要
Nebula とは、NrealブランドのMRグラスのために設計された、3Dユーザーインターフェースシステムです。Nebulaは、2Dコンテンツをインタラクティブな仮想3D空間に投影するとともに、NrealのMRグラスを直感的に操作できるように、スマートフォンでおなじみのインターフェース機能を保持しています。
Nebulaの対応デバイスは以下の通りです。
キャリア専用Nebula
Nebula2スタンダード
Samsung Galaxy Note 20 Ultra 5Gはdocomoとauから発売されていますが、現時点で対応しているのはau版のみのようです。
この記事はdocomo版のGalaxy Note 20 Ultra 5G(SC-53A)でNebula2を動かせたのでそのメモです。
手順
- こちらからNebula2のapkをインストールする。(NRealの開発者向けSlack)で配布されていたものです。)
"対応していません"というメッセージを10回タップする。
Developer Optionsが表示されるので、開発者モードを有効にする。
これでNebula2が使えるようになります。
さいごに
NReal関連の情報は公式Slackによく流れているのでぜひ参加してみてください!
OpenCV for Unityで画像処理100本ノック 11~20
1~10はこちら
前回は画像処理部分をなるべく自分で実装していましたが,今回はライブラリ使っています.
ライブラリ使うコードはOneHubdredKnock.A
, なるべく使わず実装してるのはOneHundredKnock.B
に置きます.
Q11 平滑化フィルタ
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 モーションフィルタ
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フィルタ
これはカーネルがわからなかったので自分で実装しました.
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 微分フィルタ
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フィルタ
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フィルタ
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フィルタ
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フィルタ
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フィルタ
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
経緯
OpenCV for Unity を使う機会があり、その練習がてら画像処理100本ノックをやってみます。
最初の10本はなるべくライブラリを使わずに画像処理の部分を実装します。
リポジトリはこちら
準備
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; } } }
Q1 チャネル入れ替え
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 グレースケール化
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値化
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値化
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変換
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 減色処理
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 平均プーリング
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プーリング
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 ガウシアンフィルタ
今度はこのノイズ混じりの画像を入力に使います.
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 メディアンフィルタ
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# ショートカットを作成する