🔲
Visual C# マウスによるアナログ入力の実装(全コード記載)
今回は、アナログ入力を実装した。
アナログ入力とは、マウスによって文字を書き、それを判定するものだ。
これを、ショートカット機能として使えたら面白いと思い実装した。
キーボードは、場所をとるので、簡単なショートカットであれば、マウスだけで実行できるようにしたいと考え、実装した。
技術的なところで行くと、マウス座標を、前座標との差分をとって、ベクトル化し、それを配列にし、cos類似度をとって判定している。
しかし、COS類似度は、致命的な欠点があり、それは、ノイズにすこぶる弱いということだ。少しのスパイクがあっただけで、すぐに使えなくなるという特性がある。
そのため、200サンプルぐらい取った後、それを30に圧縮している。これによって、変化量をなだらかにすることができる。AIでいうところの畳み込みの意味合いもある。
以下コード
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// ジェスチャデータの保存と管理クラス
/// </summary>
public class GestureDataStore
{
private readonly List<Gesture> _gestures = new List<Gesture>();
public void AddGesture(string name, IEnumerable<PointF> vectors)
{
_gestures.Add(new Gesture
{
Name = name,
Vectors = SerializeVectors(vectors)
});
}
public void RemoveGesture(int index)
{
if (index >= 0 && index < _gestures.Count)
_gestures.RemoveAt(index);
}
public IReadOnlyList<Gesture> GetGestures() => _gestures.AsReadOnly();
public static string SerializeVectors(IEnumerable<PointF> vectors)
{
return string.Join(";", vectors.Select(v => $"{v.X},{v.Y}"));
}
public class Gesture
{
public string Name { get; set; }
public string Vectors { get; set; }
public IEnumerable<PointF> ParseVectors()
{
return Vectors.Split(';')
.Select(v => v.Split(','))
.Select(parts => new PointF(
float.Parse(parts[0]),
float.Parse(parts[1])
));
}
}
}
/// <summary>
/// ジェスチャキャプチャ制御クラス
/// </summary>
public class GestureCaptureController
{
private readonly List<PointF> _currentVectors = new List<PointF>();
private PointF _lastPosition;
private bool _isCapturing;
public void StartCapture(PointF initialPosition)
{
_isCapturing = true;
_currentVectors.Clear();
_lastPosition = initialPosition;
}
public void UpdatePosition(PointF currentPosition)
{
if (!_isCapturing) return;
var delta = new PointF(
currentPosition.X - _lastPosition.X,
currentPosition.Y - _lastPosition.Y
);
_currentVectors.Add(delta);
_lastPosition = currentPosition;
}
public IEnumerable<PointF> EndCapture()
{
_isCapturing = false;
return GestureNormalizer.Normalize(_currentVectors, 30);
}
}
/// <summary>
/// ジェスチャ認識クラス
/// </summary>
public class GestureRecognizer
{
private readonly GestureDataStore _dataStore;
public GestureRecognizer(GestureDataStore dataStore)
{
_dataStore = dataStore;
}
public int FindBestMatchIndex(IEnumerable<PointF> input)
{
var normalizedInput = GestureNormalizer.Normalize(input.ToList(), 30);
var maxSimilarity = float.MinValue;
var bestIndex = -1;
foreach (var (gesture, index) in _dataStore.GetGestures().Select((g, i) => (g, i)))
{
var similarity = CalculateSimilarity(
normalizedInput,
GestureNormalizer.Normalize(gesture.ParseVectors().ToList(), 30)
);
if (similarity > maxSimilarity)
{
maxSimilarity = similarity;
bestIndex = index;
}
}
return bestIndex;
}
private static float CalculateSimilarity(IList<PointF> a, IList<PointF> b)
{
return a.Zip(b, CosineSimilarity).Average();
}
private static float CosineSimilarity(PointF a, PointF b)
{
var dot = a.X * b.X + a.Y * b.Y;
var magA = Math.Sqrt(a.X * a.X + a.Y * a.Y);
var magB = Math.Sqrt(b.X * b.X + b.Y * b.Y);
return (float)(magA * magB == 0 ? 0 : dot / (magA * magB));
}
}
/// <summary>
/// ジェスチャ正規化ユーティリティ
/// </summary>
public static class GestureNormalizer
{
public static List<PointF> Normalize(List<PointF> vectors, int targetLength)
{
if (vectors.Count == 0) return new List<PointF>(new PointF[targetLength]);
if (vectors.Count == targetLength) return new List<PointF>(vectors);
var normalized = new List<PointF>();
var step = (vectors.Count - 1) / (float)(targetLength - 1);
for (var i = 0; i < targetLength; i++)
{
var pos = i * step;
var index = (int)Math.Floor(pos);
var fraction = pos - index;
normalized.Add(index >= vectors.Count - 1
? vectors.Last()
: Interpolate(vectors[index], vectors[index + 1], fraction));
}
return normalized;
}
private static PointF Interpolate(PointF a, PointF b, float t)
{
return new PointF(
a.X + (b.X - a.X) * t,
a.Y + (b.Y - a.Y) * t
);
}
}
使用方法
using System.Diagnostics;
using System.Windows.Forms;
using Microsoft.VisualBasic.Devices;
using System.Timers;
using System.Threading;
using System;
namespace cos_ruzido
{
public partial class Form1 : Form
{
GestureDataStore dataStore = new GestureDataStore();
GestureCaptureController captureController = new GestureCaptureController();
GestureCaptureController capture = new GestureCaptureController();
Label label1 = new Label();
bool capture_state = false;
bool sampleing_state = false;
System.Timers.Timer timer = new System.Timers.Timer();
public Form1()
{
InitializeComponent();
// データストアの初期化
label1.Location = new Point(16, 16);
label1.Text = "label1";
label1.Size = new Size(500, 500);
label1.Font = new Font("MS ゴシック", 30F);
label1.MouseDown += Form1_Click;
this.Controls.Add(label1);
this.MouseDown += Form1_Click;
// タイマー生成
var timer = new System.Windows.Forms.Timer();
timer.Tick += new EventHandler(this.OnTick_FormsTimer);//tickはwindowstimerしか使えないのか??
timer.Interval = 10;
// タイマーを開始
timer.Start();
// タイマーを停止
//timer.Stop();
// ジェスチャ登録例
var sampleVectors = new[] { new PointF(1, 1), new PointF(2, 3) };
//dataStore.AddGesture("Sample", sampleVectors);
// キャプチャコントローラの使用
captureController.StartCapture(new PointF(0, 0));
// マウス移動データ入力(実際のアプリケーションでは入力デバイスから取得)
captureController.UpdatePosition(new PointF(1, 1));
captureController.UpdatePosition(new PointF(3, 4));
// キャプチャ終了とデータ取得
var capturedData = captureController.EndCapture().ToList();
// ジェスチャ認識
var recognizer = new GestureRecognizer(dataStore);
var matchedIndex = recognizer.FindBestMatchIndex(capturedData);
Debug.WriteLine($"Best match index: {matchedIndex}");
}
public void OnTick_FormsTimer(object sender, EventArgs e)
{
if (!capture_state && !sampleing_state) { return; }
var iX = System.Windows.Forms.Cursor.Position.X;
var iY = System.Windows.Forms.Cursor.Position.Y;
capture.UpdatePosition(new PointF(iX, iY));
Debug.WriteLine("X = " + iX + "Y = " + iY);
}
private void Form1_Click(object? sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Debug.WriteLine("マウスの左ボタンが押されています。");
if (!capture_state)
{
var iX = System.Windows.Forms.Cursor.Position.X;
var iY = System.Windows.Forms.Cursor.Position.Y;
capture = new GestureCaptureController();
capture.StartCapture(new PointF(iX, iY));
capture_state = true;
label1.Text = "登録用データのサンプリング中";
}
else
{
capture_state = false;
// キャプチャ終了とデータ取得
var capturedData = capture.EndCapture().ToList();
dataStore.AddGesture("Sample", capturedData);
Debug.WriteLine("データ保存しました");
label1.Text = "登録用データの保存完了";
}
}
if (e.Button == MouseButtons.Right)
{
Debug.WriteLine("マウスの右ボタンが押されています。");
if (!sampleing_state)
{
var iX = System.Windows.Forms.Cursor.Position.X;
var iY = System.Windows.Forms.Cursor.Position.Y;
capture = new GestureCaptureController();
capture.StartCapture(new PointF(iX, iY));
sampleing_state = true;
label1.Text = "ジェスチャを記録中";
}
else
{
sampleing_state = false;
// キャプチャ終了とデータ取得
var capturedData = capture.EndCapture().ToList();
// ジェスチャ認識
var recognizer = new GestureRecognizer(dataStore);
var matchedIndex = recognizer.FindBestMatchIndex(capturedData);
label1.Text = "判定 : " + matchedIndex;
}
}
var X = System.Windows.Forms.Cursor.Position.X;
var Y = System.Windows.Forms.Cursor.Position.Y;
Debug.WriteLine("X = " + X + "Y = " + Y);
}
}
}
Discussion