목차
논문 중요 내용
구현에 필요할 것 같은 내용만 정리
https://faculty.washington.edu/wobbrock/pubs/uist-07.01.pdf
Abstract
모바일, 타블렛, 대형 디스플레이, 그리고 테이블 컴퓨터들은 유저 인터페이스에서 펜, 손가락, 막대기 등을 사용할 수 있게 되었지만, 제스쳐 인식을 구현하는 것은 패턴 인식 전문가의 특권이었고 유터 인터페이스를 만드는 사람들에게는 아니었다. 몇몇의 유저 인터페이스 라이브러리와 도구들은 제스쳐 인식기를 제공하지만, 그런 기술들은 Flash 같은 디자인 환경이나 Javascript, 또는 새로운 환경에서는 사용할 수 없다. 초심자 프로그래머도 UI 프로토타입에 gesture를 사용하도록 하기 위해 우리는 $1 Recognizer를 소개한다. 이것은 약 100줄의 코드만으로 쉽고 저렴하고 유용하며 어디든 적용가능하다. 본 연구에서는 우리의 $1 Recognizer와 Dynamic Time Warping, Rubine classifier를 유저 제공 제스쳐에 대해 비교하였고 $1 Recognizer가 1개의 template만으로 97% 이상의 정확도를, 3개 이상의 template은 99%의 정확도를 가지는 것을 확인했다. 이 결과들은 DTW와 거의 비슷하고 Rubine보다 뛰어나다. 또한 3가지 알고리즘에 대해서 느리거나 빠른 제스쳐보다 중간 속도의 제스쳐가 더 잘 인식되었다. 우리는 template의 개수가 인식, N-best list의 점수 하락, 그리고 각 제스쳐에 대해 미치는 영향을 관찰했다. 우리는 개발, 점검, 확장, 그리고 테스트를 돕기 위해 $1 Recognizer의 의사코드를 첨부하였다.
...
THE $1 GESTURE REOCGNIZER
Characterizing the Challenge
사용자의 제스쳐는 candidate points $C$ 로 주어지고 우리는 해당 제스쳐가 이전의 저장된 template points $T_i$ 중 가장 가까운 것이 무엇인지 판단해야 한다. 이 때 Candidate와 template points는 position-sensing 분야에서 사용되는 여러 path-making 방법을 사용할 것이며 사람마다 그리는 속도 등이 다를 수 있기 때문에 이 둘은 완벽히 같을 수 없다. Figure 2를 참고하자.
주어진 "pigtail"과 "x"의 짝을 보면 다른 크기와 다른 개수의 점을 가지고 있다. 이 차이점은 인식기에게 어려운 문제가 된다. 또한 pigtail은 시계방향으로 90도 돌리면 "x"와 거의 비슷하다. 이러한 문제들을 반영하고 간단하게 만들기 위해 우리는 아래와 같이 $1 Recognizer의 기준을 정립했다.
- 이동속도나 센서에 따른 샘플링의 다양성에 대처 가능
- 회전, 크기, 위치의 불변성 지원
- 복잡한 수학적 기술이 필요없음 (e.g. 역행렬, 미분, 적분)
- 적은 코드로 쉽게 사용가능
- 렉 없이 충분히 빠른 속도
- 하나의 예시만으로 새로운 제스쳐를 가르칠 수 있음
- 입력된 점의 개수와는 상관없이 0...1의 점수를 가지는 N-best list를 반환
- 이전에 쓰이던 복잡한 알고리즘에 비교될 정도의 인식률
A Simple Four-Step Algorithm
입력 점들은 templates이든 인식되어야할 candidates이든 처음에는 똑같이 처리된다; resampled, rotated once, scaled, and translated. Candidate points $C$는 각각의 template points $T_i$에 대해 점수가 매겨지고 $C$는 $T_i$와 맞춰지는 최적의 각도를 찾는다.
Step 1: Resample the Point Path
제스쳐는 입력되는 하드웨어/소프트웨어에 따라 다른 sampling rate를 가질 수 있다. 따라서 이동속도는 입력되는 점들의 개수에 명확한 영향을 미친다. (Figure 3 참조)
다른 이동속도로 입력된 제스쳐들을 직접적으로 비교하기 위해서 우리는 제스쳐들의 original $M$ points를 $N$이라는 같은 거리의 점들로 resample하였다.(Figure 4) 너무 낮은 N은 정확도가 낮고 너무 높은 N은 비교하는 시간이 많이 걸렸다. 실험 결과, 32≤N≤256 중에서는 N=64가 가장 적합하다는 것을 확인했다.
resample하기 위해 우리는 먼저 M-point path의 총 길이를 계산한다. 이 길이를 (N-1)로 나누면 $N$에서의 각 점 사이의 거리, $I$가 나온다. 또한 $I$를 초과하는 길이도 처리하기 위해 새로운 점들은 선형 보간을 통해 추가된다. 이 과정이 끝나면 candidate gesture나 loaded template은 모두 정확하게 $N$ 개의 점을 가진다. 이에 따라 $k=1...N$에 대해 $C[k]$와 $T_i[k] 사이의 거리를 비교할 수 있다.
Step 2: Roate Once Based on the "Indicative Angle"
2개의 정렬된 점들에 대해서 하나가 다른 하나에 가장 잘 맞는 각도를 찾는 제대로 된 방법이 없다. moment라는 걸 이용한 복잡한 방법들이 있긴 하지만 이것들은 정렬된 점을 다루기 위한 것이 아니다. $1 algorithm은 두 점들이 가장 잘 맞춰지는 각도를 탐색한다. 많은 복잡한 인식 알고리즘들의 반복문은 계산비용이 비싸기 때문에 $1은 이 반복문을 쓸만할 정도로 빠르게 만들어야 한다. 그냥 1도 단위로 360도를 돌리는 것은 30개의 template이 있을 땐 효과적이었지만 이런 무작정 알고리즘보다 더 좋은 rotation trick을 사용하여 더 빠르게 만들었다.
먼저 제스쳐의 indicative angle를 찾는다. 이것은 제스쳐의 무게중심$(\bar{x},\bar{y})$과 제스쳐의 첫번째 점으로 이루어진다. 이 indicative angle이 0도가 되도록 제스쳐를 회전시킨다. (Figure 5)
Step 3: Scale and Translate
회전 이후, 젯쳐는 reference square로 크기조정된다. 정사각형으로 크기를 조정함으로써 우리는 non-uniformly를 다룰 수 있다. 이 작업은 candidate를 무게중심을 기준으로 회전시킬 수 있도록 하고 $C$와 $T_i$의 점들 사이의 거리가 가로세로비율이 아니라 오직 회전으로만 이루어진다는 것을 보장해준다. 물론 non-uniform scaling은 한계가 존재한다.
크기 조정 이후, 제스쳐는 reference point로 이동된다. 간단하게 제스쳐의 무게중심을 $(0,0)$으로 옮겼다.
Step 4: Find the Optimal Angle for the Best Score
지금까지의 과정은 $C$와 $T_i$에 대해 모두 똑같이 적용된다; resampled, rotated once, scaled and translated. 이러한 과정들은 template의 점들을 읽을 때 적용되었다. candidate에 대해서 아래의 과정이 추가로 적용된다. Step 4가 실질적으로 인식을 하는 과정이다.
Equation 1을 사용하여 $C$와 $T_i$를 비교하여 평균 거리 $d_i$를 계산한다.
Equation 1은 path-distance, $d_i$를 정의한다. $T_i$ 중 $C$에 대해 가장 작은 path-distance를 가지는 것이 인식결과가 된다. 해당 path-distance $d_i$는 Equation 2를 사용하여 [0...1]의 점수로 변환된다.
Equation 2에서 size는 reference square의 한 변의 길이이다. 즉, 분모는 reference square의 대각선의 길이의 절반이다. 이를 통해 path-distance를 제한할 수 있다.
여기서 step 2에서 사용했던 indicative angle은 가장 좋은 것이 아닌 그것의 근사치이다. global minimum을 찾고 싶다면 "angular space"에서 탐색되어야 한다.
Limitation of the $1 Recognizer
간단한 기술은 한계를 가지고 있고 $1 recognzier도 예외는 아니다. $1 recognizer는 2D Euclidean space에서의 거리를 기반으로 하는 geometry matcher이다. 점들을 비교하기 위해 회전, 크기, 위치에 영향을 받지 않기 때문에 특정 방향을 가져야 하는 제스쳐는 구분할 수 없다. 예를 들어 원과 타원, 정사각형과 직사각형, 위쪽 화살표와 아래쪽 화살표는 알고리즘을 수정하지 않고서는 구분할 수 없다. 또한 수직선과 수평선은 non-uniform scaling에 의해 왜곡될 수 있다. 마지막으로 $1 algorithm은 시간을 사용하지 않기 때문에 시간을 기준으로 제스쳐를 구분할 수 없다. 이러한 문제들을 해결하고자 한다면 알고리즘을 수정하길 바란다. 예를 들어 scale과정을 생략하거나 rotation 과정을 생략할 수 있다.
구현
의사코드 분석
Step 1. Resample a points path into n evenly spaced points
RESAMPLE(points, n)
{
I = PATH-LENGTH(points)/(n-1)
D = 0
newPoints = points[0]
foreach point p[i] for i>=1 in points
{
d = DISTANCE(p[i-1], p[i]) //두 점 사이의 거리 계산
if(D+d) >= I //만약 누적된 두 점 사이의 거리가 평균거리보다 크면 보간된 새로운 점 생성
{
q.x = p[i-1].x + ((I-D)/d) * (p[i].x-p[i-1].x))
q.y = p[i-1].y + ((I-D)/d) * (p[i].y-p[i-1].y))
newPoints.Add(q)
points.Insert(q,i) //q가 다음에 고려할 점이 됨
D = 0
}
else
{
D = D+d
}
}
return newPoints
}
PATH-LENGTH(A) //제스쳐의 점들의 총 길이
{
d = 0
for i from 1 to |A| step 1
{
d = d + DISTANCE(A[i-1], A[i])
}
return d
}
Step 2. Rotate points so that thier indicatvie angle is at 0˚
ROTATE-TO-ZERO(points)
{
c = CENTRIOD(points)
angle = ATAN(c.y-points[0].y, c.x-points[0].x)
newPoints = ROTATE-BY(points, -angle)
return newPoints
}
ROTATE-BY(points, angle)
{
c = CENTROID(points)
foreach point p in points
{
q.x = (p.x-c.x)COS(angle) - (p.y-c.y)SIN(angle) + c.x
q.y = (p.x-c.x)SIN(angle) - (p.y-c.y)COS(angle) + c.x
newPoints.Add(q)
}
return newPoints
}
CENTRIOD(points)
{
for i from 0 to |points| step 1
{
q.x = q.x + points[i].x
q.y = q.y + points[i].y
}
return q
}
Step 3. Scale points, translate points
SCALE-TO-SQUARE(points, size)
{
B = BOUNDING-BOX(points)
foreach point p in points
{
q.x = p.x * (size/B.width)
q.y = p.y * (size/B.height)
newPoints.Add(q)
}
return newPoints
}
TRANSLATE-TO-ORIGIN(points)
{
c = CENTROID(points)
foreach point p in points
{
q.x = p.x-c.x
q.y = p.y-c.y
newPoints.Add(q)
}
return newPoints
}
BOUNDING-BOX(points)
{
min = (999,999)
max = (0,0)
foreach point p in points
{
if (p.x < min.x) min.x = p.x;
if (p.x > max.x) max.x = p.x;
if (p.y < min.y) min.y = p.y;
if (p.y > max.y) max.y = p.y;
}
return BOX(min, max)
}
Step 4. Match points against a set of templates
$ϕ = \frac{-1+\sqrt{5}}{2}$
$θ = \pm45^\circ$
$θ_\Delta = 2^\circ$
RECOGNIZE(points, templates)
{
b = INF
foreach template T in templates
{
d = DISTANCE-AT-BEST-ANGLE(points, T, -θ, θ, θ_△)
if d < b
{
b = d
_T = T
}
score = 1 - b/(0.5*sqrt(size^2+size^2))
return <_T, Score>
}
DISTANCE-AT-BEST-ANGLE(points, T, θ_a, θ_b, θ_△) //황금분할탐색 (Golden section search)
{
x1 = ϕ*θ_a + (1-ϕ)*θ_b
f1 = DISTANCE-AT-ANGLE(points, T, x1)
x2 = (1-ϕ)*θ_a + ϕ*θ_b
f2 = DISTANCE-AT-ANGLE(points, T, x2)
while(|θ_b-θ_a| > θ_△)
{
if f1 < f2
{
θ_b = x2
x2 = x1
f2 = f1
x1 = ϕ*θ_a + (1-ϕ)*θ_b
f1 = DISTANCE-AT-ANGLE(points, T, x1)
}
else
{
θ_a = x1
x1 = x2
f1 = f2
x2 = (1-ϕ)*θ_a + ϕ*θ_b
f2 = DISTANCE-AT-ANGLE(points, T, x2)
}
return MIN(f1,f2)
}
DISTANCE-AT-ANGLE(points, T, θ) //candidtate와 template 비교
{
newPoints = ROTATE-BY(points, θ)
d = PATH-DISTANCE(newPoints, T.points)
return d
}
PATH-DISTANCE(A,B)
{
d = 0
for i from 0 to |A| step 1
{
d = d+DISTANCE(A[i],B[i])
}
return d/|A|
}
Unity
Dollar1Recognizer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Dollar1Recognizer : Recognizer, IRecognizer
{
private float m_size = 250;
public Dollar1Recognizer(string name) : base(name)
{
}
public string GetName() => name;
public (string, float) DoRecognition(DollarPoint[] points, int n, List<RecognitionManager.GestureTemplate> gestureTemplates)
{
DollarPoint[] preparedPoints = Normalize(points, n);
return Recognize(preparedPoints, gestureTemplates);
}
public DollarPoint[] Normalize(DollarPoint[] points, int n)
{
DollarPoint[] copyPoints = new DollarPoint[points.Length];
points.CopyTo(copyPoints, 0);
DollarPoint[] resampled = ResamplePoints(copyPoints, n);
DollarPoint[] rotated = RotateToZero(resampled);
DollarPoint[] scaled = SclaeToSquare(rotated, m_size);
DollarPoint[] translated = TranslateToOrigin(scaled);
return translated;
}
private DollarPoint[] ResamplePoints(DollarPoint[] points, int n)
{
List<DollarPoint> _points = points.ToList();
float increament = PathLength(points) / (n - 1);
float proceedDistance = 0;
DollarPoint[] newPoints = new DollarPoint[n];
newPoints[0] = points[0];
int _index = 1;
for (int i = 1; i < _points.Count; i++)
{
DollarPoint prevPoint = _points[i - 1];
DollarPoint currPoint = _points[i];
float distance = Vector2.Distance(prevPoint.point, currPoint.point);
if (proceedDistance + distance >= increament)
{
float t = (increament - proceedDistance) / distance;
float approximatedX = prevPoint.point.x + t * (currPoint.point.x - prevPoint.point.x);
float approximatedY = prevPoint.point.y + t * (currPoint.point.y - prevPoint.point.y);
DollarPoint approximatedPoint = new DollarPoint(approximatedX, approximatedY);
newPoints[_index] = approximatedPoint;
_index++;
_points.Insert(i, approximatedPoint);
proceedDistance = 0;
}
else
{
proceedDistance += distance;
}
}
if(proceedDistance > 0.1f)
{
newPoints[newPoints.Length - 1] = _points[_points.Count - 1];
_index++;
}
return newPoints;
}
private float PathLength(DollarPoint[] points)
{
float length = 0f;
for(int i = 1; i < points.Length; i++)
{
length += Vector2.Distance(points[i - 1].point, points[i].point);
}
return length;
}
private DollarPoint[] RotateToZero(DollarPoint[] points)
{
Vector2 centeroid = GetCenteroid(points);
float angle = Mathf.Atan2(centeroid.y - points[0].point.y, centeroid.x - points[0].point.x);
DollarPoint[] newPoints = RotateBy(points, -angle);
return newPoints;
}
private DollarPoint[] RotateBy(DollarPoint[] points, float angle)
{
DollarPoint[] newPoints = new DollarPoint[points.Length];
int _index = 0;
Vector2 centeroid = GetCenteroid(points);
foreach(DollarPoint point in points)
{
float rotatedX = (point.point.x - centeroid.x) * Mathf.Cos(angle)
- (point.point.y - centeroid.y) * Mathf.Sin(angle)
+ centeroid.x;
float rotatedY = (point.point.x - centeroid.x) * Mathf.Sin(angle)
+ (point.point.y - centeroid.y) * Mathf.Cos(angle)
+ centeroid.x;
newPoints[_index] = new DollarPoint(rotatedX, rotatedY);
_index++;
}
return newPoints;
}
private Vector2 GetCenteroid(DollarPoint[] points)
{
float centerX = points.Sum(point => point.point.x) / points.Length;
float centerY = points.Sum(point => point.point.y) / points.Length;
return new Vector2(centerX, centerY);
}
private DollarPoint[] SclaeToSquare(DollarPoint[] points, float size)
{
DollarPoint[] newPoints = new DollarPoint[points.Length];
int _index = 0;
Rect bbox = GetBoundingBox(points);
foreach(DollarPoint point in points)
{
float scaledX = point.point.x * size / bbox.width;
float scaledY = point.point.y * size / bbox.height;
newPoints[_index] = new DollarPoint(scaledX, scaledY);
_index++;
}
return newPoints;
}
private DollarPoint[] TranslateToOrigin(DollarPoint[] points)
{
DollarPoint[] newPoints = new DollarPoint[points.Length];
int _index = 0;
Vector2 centeroid = GetCenteroid(points);
foreach(DollarPoint point in points)
{
float translatedX = point.point.x - centeroid.x;
float translatedY = point.point.y - centeroid.y;
newPoints[_index] = new DollarPoint(translatedX, translatedY);
_index++;
}
return newPoints;
}
private Rect GetBoundingBox(DollarPoint[] points)
{
float minX = points.Select(point => point.point.x).Min();
float maxX = points.Select(point => point.point.x).Max();
float minY = points.Select(point => point.point.y).Min();
float maxY = points.Select(point => point.point.y).Max();
return new Rect(minX, minY, maxX - minX, maxY - minY);
}
private (string, float) Recognize(DollarPoint[] points, List<RecognitionManager.GestureTemplate> templates)
{
float theta = 45;
float deltaTheta = 2;
float angle = 0.5f * (-1 + Mathf.Sqrt(5));
float best = float.PositiveInfinity;
RecognitionManager.GestureTemplate bestTemplate = new RecognitionManager.GestureTemplate();
foreach(RecognitionManager.GestureTemplate template in templates)
{
float distance = DistanceAtBestAngle(points, template, -theta, theta, deltaTheta, angle);
if(distance < best)
{
best = distance;
bestTemplate = template;
}
}
double score = 1 - (best / (0.5f * Math.Sqrt(2 * m_size * m_size)));
return ((string, float))(bestTemplate.name, score);
}
private float DistanceAtBestAngle(DollarPoint[] points, RecognitionManager.GestureTemplate template,
float thetaA, float thetaB, float deltaTheta, float angle)
{
float x1 = angle * thetaA + (1 - angle) * thetaB;
float x2 = (1 - angle) * thetaA + angle * thetaB;
float d1 = DistanceAtAngle(points, template, x1);
float d2 = DistanceAtAngle(points, template, x2);
while(Mathf.Abs(thetaB - thetaA) > deltaTheta)
{
if(d1 < d2)
{
thetaB = x2;
x2 = x1;
d2 = d1;
x1 = angle * thetaA + (1 - angle) * thetaB;
d1 = DistanceAtAngle(points, template, x1);
}
else
{
thetaA = x1;
x1 = x2;
d1 = d2;
x2 = (1 - angle) * thetaA + angle * thetaB;
d2 = DistanceAtAngle(points, template, x2);
}
}
return Mathf.Min(d1, d2);
}
private float DistanceAtAngle(DollarPoint[] points, RecognitionManager.GestureTemplate template, float angle)
{
DollarPoint[] newPoints = RotateBy(points, angle);
float distance = PathDistance(newPoints, Normalize(template.points, 64));
return distance;
}
private float PathDistance(DollarPoint[] A, DollarPoint[] B)
{
float distance = 0f;
for(int i = 0; i < A.Length; i++)
{
distance += Vector2.Distance(A[i].point, B[i].point);
}
return distance / A.Length;
}
}
IRecognizer.cs
UI에 이름을 표시하기 위한 GetName() 함수 추가
using System.Collections.Generic;
public interface IRecognizer
{
public string GetName();
public DollarPoint[] Normalize(DollarPoint[] points, int n);
public (string, float) DoRecognition(DollarPoint[] points, int n,
List<RecognitionManager.GestureTemplate> gestureTemplates);
}
Recognizer.cs
UI에 이름을 표기하기 위한 변수 및 생성자 추가
public class Recognizer
{
protected string name;
protected Recognizer(string name)
{
this.name = name;
}
}
RecognizePanel.cs
알고리즘 선택 및 초기화 코드 추가
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
public class RecognitionPanel : MonoBehaviour
{
[SerializeField] private TMP_Dropdown dd_algorithmList;
public void Initialize(Action<int> onAlgorithmChoose, List<IRecognizer> recognizers)
{
dd_algorithmList.onValueChanged.AddListener(onAlgorithmChoose.Invoke);
foreach(IRecognizer recognizer in recognizers)
{
dd_algorithmList.options = recognizers.Select(name => new TMP_Dropdown.OptionData(name.GetName())).ToList();
}
onAlgorithmChoose.Invoke(0);
}
}
RecognitionManager.cs
RecognizerList 선언
Dolla1Recognizer 선언
recognizer 종류에 따른 resultText 변경
using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
public class RecognitionManager : MonoBehaviour
{
[SerializeField] private Drawable m_drawable;
[SerializeField] private RecognitionPanel m_recogntionPanel;
[SerializeField] private TemplateReviewer m_templateReviewer;
private GestureTemplates m_templates => GestureTemplates.Get();
private static Dollar1Recognizer m_dollar1Recognizer = new Dollar1Recognizer("$1 Recognizer");
private IRecognizer m_currentRecognizer;
[SerializeField] private RecognizerMode m_mode = RecognizerMode.TEMPLATE;
public static List<IRecognizer> m_recognizerList = new List<IRecognizer>();
private string _templateName => input_templateName.text;
[SerializeField] private Button btn_recognizeMode;
[SerializeField] private Button btn_templateMode;
[SerializeField] private Button btn_reviewMode;
[SerializeField] private TextMeshProUGUI txt_recognitionResult;
[SerializeField] private TMP_InputField input_templateName;
public enum RecognizerMode
{
RECOGNITION,
TEMPLATE,
REVIEW,
}
[Serializable]
public struct GestureTemplate
{
public string name;
public DollarPoint[] points;
public GestureTemplate(string templateName, DollarPoint[] preparedPoint)
{
name = templateName;
points = preparedPoint;
}
}
private void Awake()
{
m_recognizerList.Add(m_dollar1Recognizer);
btn_recognizeMode.onClick.AddListener(() => SetMode(RecognizerMode.RECOGNITION));
btn_templateMode.onClick.AddListener(() => SetMode(RecognizerMode.TEMPLATE));
btn_reviewMode.onClick.AddListener(() => SetMode(RecognizerMode.REVIEW));
m_recogntionPanel.Initialize(SwitchRecognitionAlgorithm, m_recognizerList);
m_drawable.OnDrawFinished += OnDrawFinished;
SetMode(m_mode);
}
private void SwitchRecognitionAlgorithm(int algorithm)
{
m_currentRecognizer = m_recognizerList[algorithm];
}
private void SetMode(RecognizerMode mode)
{
m_mode = mode;
m_drawable.ClearDrawing();
input_templateName.gameObject.SetActive(mode == RecognizerMode.TEMPLATE);
txt_recognitionResult.gameObject.SetActive(mode == RecognizerMode.RECOGNITION);
m_drawable.gameObject.SetActive(mode != RecognizerMode.REVIEW);
m_templateReviewer.SetVisibility(mode == RecognizerMode.REVIEW);
m_recogntionPanel.gameObject.SetActive(mode == RecognizerMode.RECOGNITION);
}
private void OnDrawFinished(DollarPoint[] points)
{
if(m_mode == RecognizerMode.TEMPLATE)
{
//Save Template
GestureTemplate newTemplate = new GestureTemplate(_templateName, points);
m_templates.RawTemplates.Add(newTemplate);
//GestureTemplate preparedTemplate = new GestureTemplate(_templateName, m_currentRecognizer.Normalize(points, 64));
//m_templates.ProceedTemplates.Add(preparedTemplate);
}
else if(m_mode == RecognizerMode.RECOGNITION)
{
//Do Recognition
if (m_currentRecognizer == null)
{
Debug.LogError("currentRecognizer is null");
return;
}
(string, float) result = m_currentRecognizer.DoRecognition(points, 64, m_templates.RawTemplates);
string resultText = "";
if(m_currentRecognizer is Dollar1Recognizer)
{
resultText = $"Recognized: {result.Item1}, Score: {result.Item2}";
}
txt_recognitionResult.text = resultText;
Debug.Log(resultText);
}
}
private void OnApplicationQuit()
{
m_templates.Save();
}
}
전체 코드
https://github.com/richardlee-kr/DrawRecognition/tree/features/Recognition/Dollar1
'프로젝트 > Unity Gesture Recognizer' 카테고리의 다른 글
Unity Gesture Recognizer #1 - Base Code (0) | 2024.07.13 |
---|---|
Unity Gesture Recognizer #0 - $-family (0) | 2024.07.03 |