🔲

Visual C# マウスによるアナログ入力の実装(全コード記載)

2025/02/05に公開

今回は、アナログ入力を実装した。
アナログ入力とは、マウスによって文字を書き、それを判定するものだ。
これを、ショートカット機能として使えたら面白いと思い実装した。

キーボードは、場所をとるので、簡単なショートカットであれば、マウスだけで実行できるようにしたいと考え、実装した。

技術的なところで行くと、マウス座標を、前座標との差分をとって、ベクトル化し、それを配列にし、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