dolla-one-recognizer-py 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ The $1 Unistroke Recognizer (Python 3 port)
2
+
3
+ Jacob O. Wobbrock, Ph.D.
4
+ The Information School
5
+ University of Washington
6
+ wobbrock@uw.edu
7
+
8
+ Andrew D. Wilson, Ph.D.
9
+ Microsoft Research
10
+ awilson@microsoft.com
11
+
12
+ Yang Li, Ph.D.
13
+ Department of Computer Science and Engineering
14
+ University of Washington
15
+ yangli@cs.washington.edu
16
+
17
+ The academic publication for the $1 recognizer, and what should be
18
+ used to cite it, is:
19
+
20
+ Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without
21
+ libraries, toolkits or training: A $1 recognizer for user interface
22
+ prototypes. Proceedings of the ACM Symposium on User Interface
23
+ Software and Technology (UIST '07). Newport, Rhode Island (October
24
+ 7-10, 2007). New York: ACM Press, pp. 159-168.
25
+ https://dl.acm.org/citation.cfm?id=1294238
26
+
27
+ The Protractor enhancement was separately published by Yang Li and programmed
28
+ here by Jacob O. Wobbrock:
29
+
30
+ Li, Y. (2010). Protractor: A fast and accurate gesture
31
+ recognizer. Proceedings of the ACM Conference on Human
32
+ Factors in Computing Systems (CHI '10). Atlanta, Georgia
33
+ (April 10-15, 2010). New York: ACM Press, pp. 2169-2172.
34
+ https://dl.acm.org/citation.cfm?id=1753654
35
+
36
+ This software is distributed under the "New BSD License" agreement:
37
+
38
+ Copyright (C) 2007-2012, Jacob O. Wobbrock, Andrew D. Wilson and Yang Li.
39
+ All rights reserved.
40
+
41
+ Python 3 port Copyright (C) 2026, yuzujelly2222.
42
+ All rights reserved.
43
+
44
+ Redistribution and use in source and binary forms, with or without
45
+ modification, are permitted provided that the following conditions are met:
46
+ * Redistributions of source code must retain the above copyright
47
+ notice, this list of conditions and the following disclaimer.
48
+ * Redistributions in binary form must reproduce the above copyright
49
+ notice, this list of conditions and the following disclaimer in the
50
+ documentation and/or other materials provided with the distribution.
51
+ * Neither the names of the University of Washington nor Microsoft,
52
+ nor the names of its contributors may be used to endorse or promote
53
+ products derived from this software without specific prior written
54
+ permission.
55
+
56
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
57
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
58
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
59
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Jacob O. Wobbrock OR Andrew D. Wilson
60
+ OR Yang Li OR yuzujelly2222 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
61
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
62
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
63
+ OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
64
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
65
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
66
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: dolla_one_recognizer_py
3
+ Version: 0.1.2
4
+ Summary: A Python 3 implementation of the $1 Unistroke Recognizer
5
+ Home-page: https://github.com/yuzujelly2222/dolla_one_recognizer_python
6
+ Author: yuzujelly2222
7
+ License: BSD-3-Clause
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ [English](README.md) | [日本語](README.ja.md)
31
+
32
+ # dolla_one_recognizer
33
+
34
+ A Python 3 implementation of the **$1 Unistroke Recognizer**.
35
+
36
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
37
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
38
+
39
+ > 詳しい日本語の解説は [README.ja.md](README.ja.md) にあります。
40
+
41
+ ## What is this
42
+
43
+ The **$1 Unistroke Recognizer** is a lightweight single-stroke gesture recognition
44
+ algorithm by Wobbrock, Wilson, and Li (UIST '07). It needs no machine-learning
45
+ training: register just one template per shape and it works. It resamples the input
46
+ stroke to a fixed number of points, normalizes rotation, scale, and position, then
47
+ picks the nearest registered template.
48
+
49
+ Input is a plain list of points `[[x, y], ...]`, so the coordinates can come from a
50
+ mouse, touch input, or a tracker such as OpenCV optical flow / color / fingertip
51
+ tracking — just append each frame's `(x, y)` and pass the list in.
52
+
53
+ This repository contains the recognizer itself, packaged as `dolla_one_recognizer_py/`,
54
+ plus two small Tkinter demo apps: `export_gesture_app.py` (draw and save gesture
55
+ templates) and `recognize_app.py` (match a drawn stroke against saved templates).
56
+
57
+ ![Demo of recognize_app.py matching hand-drawn gestures against registered templates](assets/demo.gif)
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ git clone https://github.com/yuzujelly2222/dolla_one_recognizer_python.git
63
+ cd dolla_one_recognizer
64
+ pip install -r requirements.txt
65
+ ```
66
+
67
+ Dependencies are `numpy` (geometry) and `pillow` (PNG I/O for the demo apps).
68
+
69
+ ## Usage
70
+
71
+ ```python
72
+ from dolla_one_recognizer_py import dolla_one_recognizer
73
+
74
+ # Register templates — one example stroke per shape is enough
75
+ line_template = [[x, x] for x in range(0, 101, 5)]
76
+ check_template = [[0, 30], [30, 0], [90, 60]]
77
+
78
+ recognizer = dolla_one_recognizer(
79
+ size=250,
80
+ templates=[line_template, check_template],
81
+ templates_name=["line", "checkmark"],
82
+ n=64,
83
+ )
84
+
85
+ # Raw points to recognize (e.g. collected from a mouse drag)
86
+ query = [[x, x * 0.9 + 2] for x in range(0, 101, 4)]
87
+
88
+ best_points, best_name, score = recognizer.recognize(query)
89
+ print(best_name, score) # -> line 0.96...
90
+ ```
91
+
92
+ Try the GUI demos:
93
+
94
+ ```bash
95
+ python export_gesture_app.py # draw and save to gestures/ as CSV + PNG
96
+ python recognize_app.py # auto-load gestures/ and test recognition
97
+ ```
98
+
99
+ ## How it works
100
+
101
+ $1 applies four steps to the input points and returns the nearest template:
102
+
103
+ 1. **Resample** (`_resample`) to `n` (default 64) evenly spaced points, absorbing
104
+ differences in drawing speed.
105
+ 2. **Rotate** (`_rotate_to_zero`) so the angle from the centroid to the first point is zero.
106
+ 3. **Scale & translate** (`_scale_to_square` -> `_translate_to_origin`) into a
107
+ `size x size` square (default 250) centered at the origin.
108
+ 4. **Match** (`recognize`) against each template, searching +/-45 degrees for the
109
+ rotation that minimizes the average point-to-point distance (Golden Section Search),
110
+ and return the closest. The distance is converted to a 0-1 score.
111
+
112
+ The Protractor enhancement (a closed-form optimal angle instead of the search) is not
113
+ included in this implementation. See [README.ja.md](README.ja.md) for a fuller walkthrough.
114
+
115
+ ## API
116
+
117
+ Only the members below are part of the public API; the geometry helpers are prefixed
118
+ with `_` and are internal.
119
+
120
+ | Member | Description |
121
+ |---|---|
122
+ | `__init__(size, templates, templates_name, n)` | Initialize with a list of template strokes and their names |
123
+ | `add_template(points, name)` | Add one more template |
124
+ | `recognize(points)` | Normalize a raw stroke, match all templates, return `(best_points, best_name, score)` |
125
+
126
+ ## License & citation
127
+
128
+ Distributed under the **New BSD License** — see [LICENSE](LICENSE).
129
+
130
+ This is an independent implementation based on the pseudocode from the paper below.
131
+ The original authors and copyright holders exist separately from this port.
132
+
133
+ > Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without libraries, toolkits
134
+ > or training: A $1 recognizer for user interface prototypes. *Proceedings of the ACM
135
+ > Symposium on User Interface Software and Technology (UIST '07)*, pp. 159-168.
136
+
137
+ Official site (algorithm, pseudocode, reference implementation):
138
+ <https://depts.washington.edu/acelab/proj/dollar/index.html>
@@ -0,0 +1,109 @@
1
+ [English](README.md) | [日本語](README.ja.md)
2
+
3
+ # dolla_one_recognizer
4
+
5
+ A Python 3 implementation of the **$1 Unistroke Recognizer**.
6
+
7
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
8
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
9
+
10
+ > 詳しい日本語の解説は [README.ja.md](README.ja.md) にあります。
11
+
12
+ ## What is this
13
+
14
+ The **$1 Unistroke Recognizer** is a lightweight single-stroke gesture recognition
15
+ algorithm by Wobbrock, Wilson, and Li (UIST '07). It needs no machine-learning
16
+ training: register just one template per shape and it works. It resamples the input
17
+ stroke to a fixed number of points, normalizes rotation, scale, and position, then
18
+ picks the nearest registered template.
19
+
20
+ Input is a plain list of points `[[x, y], ...]`, so the coordinates can come from a
21
+ mouse, touch input, or a tracker such as OpenCV optical flow / color / fingertip
22
+ tracking — just append each frame's `(x, y)` and pass the list in.
23
+
24
+ This repository contains the recognizer itself, packaged as `dolla_one_recognizer_py/`,
25
+ plus two small Tkinter demo apps: `export_gesture_app.py` (draw and save gesture
26
+ templates) and `recognize_app.py` (match a drawn stroke against saved templates).
27
+
28
+ ![Demo of recognize_app.py matching hand-drawn gestures against registered templates](assets/demo.gif)
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ git clone https://github.com/yuzujelly2222/dolla_one_recognizer_python.git
34
+ cd dolla_one_recognizer
35
+ pip install -r requirements.txt
36
+ ```
37
+
38
+ Dependencies are `numpy` (geometry) and `pillow` (PNG I/O for the demo apps).
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from dolla_one_recognizer_py import dolla_one_recognizer
44
+
45
+ # Register templates — one example stroke per shape is enough
46
+ line_template = [[x, x] for x in range(0, 101, 5)]
47
+ check_template = [[0, 30], [30, 0], [90, 60]]
48
+
49
+ recognizer = dolla_one_recognizer(
50
+ size=250,
51
+ templates=[line_template, check_template],
52
+ templates_name=["line", "checkmark"],
53
+ n=64,
54
+ )
55
+
56
+ # Raw points to recognize (e.g. collected from a mouse drag)
57
+ query = [[x, x * 0.9 + 2] for x in range(0, 101, 4)]
58
+
59
+ best_points, best_name, score = recognizer.recognize(query)
60
+ print(best_name, score) # -> line 0.96...
61
+ ```
62
+
63
+ Try the GUI demos:
64
+
65
+ ```bash
66
+ python export_gesture_app.py # draw and save to gestures/ as CSV + PNG
67
+ python recognize_app.py # auto-load gestures/ and test recognition
68
+ ```
69
+
70
+ ## How it works
71
+
72
+ $1 applies four steps to the input points and returns the nearest template:
73
+
74
+ 1. **Resample** (`_resample`) to `n` (default 64) evenly spaced points, absorbing
75
+ differences in drawing speed.
76
+ 2. **Rotate** (`_rotate_to_zero`) so the angle from the centroid to the first point is zero.
77
+ 3. **Scale & translate** (`_scale_to_square` -> `_translate_to_origin`) into a
78
+ `size x size` square (default 250) centered at the origin.
79
+ 4. **Match** (`recognize`) against each template, searching +/-45 degrees for the
80
+ rotation that minimizes the average point-to-point distance (Golden Section Search),
81
+ and return the closest. The distance is converted to a 0-1 score.
82
+
83
+ The Protractor enhancement (a closed-form optimal angle instead of the search) is not
84
+ included in this implementation. See [README.ja.md](README.ja.md) for a fuller walkthrough.
85
+
86
+ ## API
87
+
88
+ Only the members below are part of the public API; the geometry helpers are prefixed
89
+ with `_` and are internal.
90
+
91
+ | Member | Description |
92
+ |---|---|
93
+ | `__init__(size, templates, templates_name, n)` | Initialize with a list of template strokes and their names |
94
+ | `add_template(points, name)` | Add one more template |
95
+ | `recognize(points)` | Normalize a raw stroke, match all templates, return `(best_points, best_name, score)` |
96
+
97
+ ## License & citation
98
+
99
+ Distributed under the **New BSD License** — see [LICENSE](LICENSE).
100
+
101
+ This is an independent implementation based on the pseudocode from the paper below.
102
+ The original authors and copyright holders exist separately from this port.
103
+
104
+ > Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without libraries, toolkits
105
+ > or training: A $1 recognizer for user interface prototypes. *Proceedings of the ACM
106
+ > Symposium on User Interface Software and Technology (UIST '07)*, pp. 159-168.
107
+
108
+ Official site (algorithm, pseudocode, reference implementation):
109
+ <https://depts.washington.edu/acelab/proj/dollar/index.html>
@@ -0,0 +1,2 @@
1
+ from .recognizer import dolla_one_recognizer
2
+ __version__ = '0.1.2'
@@ -0,0 +1,236 @@
1
+
2
+ # The $1 Unistroke Recognizer (Python 3 version)
3
+ #
4
+ # Jacob O. Wobbrock, Ph.D.
5
+ # The Information School
6
+ # University of Washington
7
+ # wobbrock@uw.edu
8
+ #
9
+ # Andrew D. Wilson, Ph.D.
10
+ # Microsoft Research
11
+ # awilson@microsoft.com
12
+ #
13
+ # Yang Li, Ph.D.
14
+ # Department of Computer Science and Engineering
15
+ # University of Washington
16
+ # yangli@cs.washington.edu
17
+ #
18
+ # The academic publication for the $1 recognizer, and what should be
19
+ # used to cite it, is:
20
+ #
21
+ # Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without
22
+ # libraries, toolkits or training: A $1 recognizer for user interface
23
+ # prototypes. Proceedings of the ACM Symposium on User Interface
24
+ # Software and Technology (UIST '07). Newport, Rhode Island (October
25
+ # 7-10, 2007). New York: ACM Press, pp. 159-168.
26
+ # https://dl.acm.org/citation.cfm?id=1294238
27
+ #
28
+ # The Protractor enhancement was separately published by Yang Li and programmed
29
+ # here by Jacob O. Wobbrock:
30
+ #
31
+ # Li, Y. (2010). Protractor: A fast and accurate gesture
32
+ # recognizer. Proceedings of the ACM Conference on Human
33
+ # Factors in Computing Systems (CHI '10). Atlanta, Georgia
34
+ # (April 10-15, 2010). New York: ACM Press, pp. 2169-2172.
35
+ # https://dl.acm.org/citation.cfm?id=1753654
36
+ #
37
+ # This software is distributed under the "New BSD License" agreement:
38
+ #
39
+ # Copyright (C) 2007-2012, Jacob O. Wobbrock, Andrew D. Wilson and Yang Li.
40
+ # All rights reserved.
41
+ #
42
+ # Python 3 port Copyright (C) 2026, yuzujelly2222.
43
+ #
44
+ # Redistribution and use in source and binary forms, with or without
45
+ # modification, are permitted provided that the following conditions are met:
46
+ # * Redistributions of source code must retain the above copyright
47
+ # notice, this list of conditions and the following disclaimer.
48
+ # * Redistributions in binary form must reproduce the above copyright
49
+ # notice, this list of conditions and the following disclaimer in the
50
+ # documentation and/or other materials provided with the distribution.
51
+ # * Neither the names of the University of Washington nor Microsoft,
52
+ # nor the names of its contributors may be used to endorse or promote
53
+ # products derived from this software without specific prior written
54
+ # permission.
55
+ #
56
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
57
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
58
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
59
+ # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Jacob O. Wobbrock OR Andrew D. Wilson
60
+ # OR Yang Li OR yuzujelly2222 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
61
+ # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
62
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
63
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
64
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
65
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66
+
67
+ from typing import Sequence
68
+
69
+ import numpy as np
70
+
71
+ Point = Sequence[float]
72
+ Points = Sequence[Point]
73
+
74
+
75
+ class templates_template:
76
+ points: np.ndarray = np.empty((0, 2))
77
+ name: str = ""
78
+
79
+
80
+ class _deg:
81
+ sin = staticmethod(lambda x: np.sin(np.deg2rad(x)))
82
+ cos = staticmethod(lambda x: np.cos(np.deg2rad(x)))
83
+ tan = staticmethod(lambda x: np.tan(np.deg2rad(x)))
84
+ arcsin = staticmethod(lambda x: np.rad2deg(np.arcsin(x)))
85
+ arccos = staticmethod(lambda x: np.rad2deg(np.arccos(x)))
86
+ arctan = staticmethod(lambda x: np.rad2deg(np.arctan(x)))
87
+ arctan2 = staticmethod(lambda y, x: np.rad2deg(np.arctan2(y, x)))
88
+
89
+
90
+ class dolla_one_recognizer:
91
+ def __init__(self, size: float, templates: Sequence[Points], templates_name: Sequence[str], n: int) -> None:
92
+ self.size = size
93
+ self.n = n
94
+ self.templates: list[templates_template] = []
95
+ for points, name in zip(templates, templates_name):
96
+ template = templates_template()
97
+ template.points = self._normalize(points)
98
+ template.name = name
99
+ self.templates.append(template)
100
+
101
+ def add_template(self, points: Points, name: str) -> None:
102
+ template = templates_template()
103
+ template.points = self._normalize(points)
104
+ template.name = name
105
+ self.templates.append(template)
106
+
107
+ def _get_distance(self, p1: Point, p2: Point) -> float:
108
+ return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5
109
+
110
+ def _get_length(self, points: Points) -> float:
111
+ return sum(self._get_distance(points[i-1], points[i]) for i in range(1, len(points)))
112
+
113
+ def _get_centroid(self, points: Points) -> list[float]:
114
+ xs = [p[0] for p in points]
115
+ ys = [p[1] for p in points]
116
+ return [sum(xs)/len(points), sum(ys)/len(points)]
117
+
118
+ def _get_bounding_box(self, points: Points) -> tuple[float, float]:
119
+ xs = [p[0] for p in points]
120
+ ys = [p[1] for p in points]
121
+ x_max = max(xs)
122
+ x_min = min(xs)
123
+ y_max = max(ys)
124
+ y_min = min(ys)
125
+ width = abs(x_max - x_min)
126
+ height = abs(y_max - y_min)
127
+ return width, height
128
+
129
+ def _all_point_rotate(self, points: Points, theta: float) -> np.ndarray:
130
+ c = self._get_centroid(points)
131
+ new_points = list()
132
+ for point in points:
133
+ qx = (point[0]-c[0])*_deg.cos(theta) - (point[1]-c[1])*_deg.sin(theta) + c[0]
134
+ qy = (point[0]-c[0])*_deg.sin(theta) + (point[1]-c[1])*_deg.cos(theta) + c[1]
135
+ new_points.append([qx,qy])
136
+ return np.array(new_points)
137
+
138
+ def _resample(self, points: Points) -> np.ndarray:
139
+ points = [list(p) for p in points]
140
+ ideal = self._get_length(points) / (self.n - 1)
141
+ new_points = [points[0][:]]
142
+ all_distance = 0.0
143
+ i = 1
144
+ while i < len(points):
145
+ distance = self._get_distance(points[i-1], points[i])
146
+ if all_distance + distance >= ideal:
147
+ t = (ideal - all_distance) / distance
148
+ qx = points[i-1][0] + t * (points[i][0] - points[i-1][0])
149
+ qy = points[i-1][1] + t * (points[i][1] - points[i-1][1])
150
+ q = [qx, qy]
151
+ new_points.append(q)
152
+ points.insert(i, q)
153
+ all_distance = 0.0
154
+ else:
155
+ all_distance += distance
156
+ i += 1
157
+ if len(new_points) == self.n - 1:
158
+ new_points.append(points[-1][:])
159
+ return np.array(new_points)
160
+
161
+ def _rotate_to_zero(self, points: Points) -> np.ndarray:
162
+ c = self._get_centroid(points)
163
+ theta = _deg.arctan2(c[1]-points[0][1],c[0]-points[0][0])
164
+ return self._all_point_rotate(points,-theta)
165
+
166
+ def _scale_to_square(self, points: Points) -> np.ndarray:
167
+ bounding_box = self._get_bounding_box(points)
168
+ new_points = list()
169
+ for point in points:
170
+ qx = point[0] * (self.size/bounding_box[0])
171
+ qy = point[1] * (self.size/bounding_box[1])
172
+ new_points.append([qx,qy])
173
+ return np.array(new_points)
174
+
175
+ def _translate_to_origin(self, points: Points) -> np.ndarray:
176
+ c = self._get_centroid(points)
177
+ new_points = list()
178
+ for point in points:
179
+ qx = point[0] - c[0]
180
+ qy = point[1] - c[1]
181
+ new_points.append([qx,qy])
182
+ return np.array(new_points)
183
+
184
+ def _normalize(self, points: Points) -> np.ndarray:
185
+ points = self._resample(points)
186
+ points = self._rotate_to_zero(points)
187
+ points = self._scale_to_square(points)
188
+ points = self._translate_to_origin(points)
189
+ return points
190
+
191
+ def recognize(self, points: Points) -> tuple[np.ndarray, str, float]:
192
+ points = self._normalize(points)
193
+ best_point = float('inf')
194
+ for template in self.templates:
195
+ temp_point = self._distance_at_best_angle(points, template.points, -45,45,2)
196
+ if temp_point < best_point:
197
+ best_point = temp_point
198
+ best_template = template.points
199
+ best_template_name = template.name
200
+ score = 1 - best_point / (0.5*((self.size**2+self.size**2)**0.5))
201
+ return best_template, best_template_name, score
202
+
203
+ def _distance_at_best_angle(self, points: Points, template: Points, theta_a: float, theta_b: float, theta_d: float) -> float:
204
+ GOLDEN_RATIO = 0.5*(-1+(5)**0.5)
205
+ x_1 = GOLDEN_RATIO*theta_a + (1-GOLDEN_RATIO)*theta_b
206
+ f_1 = self._distance_at_angle(points, template, x_1)
207
+ x_2 = GOLDEN_RATIO*theta_b + (1-GOLDEN_RATIO)*theta_a
208
+ f_2 = self._distance_at_angle(points, template, x_2)
209
+ while abs(theta_b-theta_a) > theta_d:
210
+ if f_1 < f_2:
211
+ theta_b = x_2
212
+ x_2 = x_1
213
+ f_2 = f_1
214
+ x_1 = GOLDEN_RATIO*theta_a +(1-GOLDEN_RATIO)*theta_b
215
+ f_1 = self._distance_at_angle(points, template,x_1)
216
+ else:
217
+ theta_a = x_1
218
+ x_1 = x_2
219
+ f_1 = f_2
220
+ x_2 = GOLDEN_RATIO*theta_b + (1-GOLDEN_RATIO)*theta_a
221
+ f_2 = self._distance_at_angle(points, template, x_2)
222
+
223
+ return min(f_1,f_2)
224
+
225
+ def _distance_at_angle(self, points: Points, template: Points, theta: float) -> float:
226
+ new_points = self._all_point_rotate(points,theta)
227
+ return self._path_distance(new_points,template)
228
+
229
+ def _path_distance(self, a: Points, b: Points) -> float:
230
+ d = 0
231
+ for i in range(len(a)):
232
+ d = d + self._get_distance(a[i],b[i])
233
+ return d / len(a)
234
+
235
+ def main(self, points: Points) -> tuple[np.ndarray, str, float]:
236
+ return self.recognize(points)
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: dolla_one_recognizer_py
3
+ Version: 0.1.2
4
+ Summary: A Python 3 implementation of the $1 Unistroke Recognizer
5
+ Home-page: https://github.com/yuzujelly2222/dolla_one_recognizer_python
6
+ Author: yuzujelly2222
7
+ License: BSD-3-Clause
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ [English](README.md) | [日本語](README.ja.md)
31
+
32
+ # dolla_one_recognizer
33
+
34
+ A Python 3 implementation of the **$1 Unistroke Recognizer**.
35
+
36
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
37
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
38
+
39
+ > 詳しい日本語の解説は [README.ja.md](README.ja.md) にあります。
40
+
41
+ ## What is this
42
+
43
+ The **$1 Unistroke Recognizer** is a lightweight single-stroke gesture recognition
44
+ algorithm by Wobbrock, Wilson, and Li (UIST '07). It needs no machine-learning
45
+ training: register just one template per shape and it works. It resamples the input
46
+ stroke to a fixed number of points, normalizes rotation, scale, and position, then
47
+ picks the nearest registered template.
48
+
49
+ Input is a plain list of points `[[x, y], ...]`, so the coordinates can come from a
50
+ mouse, touch input, or a tracker such as OpenCV optical flow / color / fingertip
51
+ tracking — just append each frame's `(x, y)` and pass the list in.
52
+
53
+ This repository contains the recognizer itself, packaged as `dolla_one_recognizer_py/`,
54
+ plus two small Tkinter demo apps: `export_gesture_app.py` (draw and save gesture
55
+ templates) and `recognize_app.py` (match a drawn stroke against saved templates).
56
+
57
+ ![Demo of recognize_app.py matching hand-drawn gestures against registered templates](assets/demo.gif)
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ git clone https://github.com/yuzujelly2222/dolla_one_recognizer_python.git
63
+ cd dolla_one_recognizer
64
+ pip install -r requirements.txt
65
+ ```
66
+
67
+ Dependencies are `numpy` (geometry) and `pillow` (PNG I/O for the demo apps).
68
+
69
+ ## Usage
70
+
71
+ ```python
72
+ from dolla_one_recognizer_py import dolla_one_recognizer
73
+
74
+ # Register templates — one example stroke per shape is enough
75
+ line_template = [[x, x] for x in range(0, 101, 5)]
76
+ check_template = [[0, 30], [30, 0], [90, 60]]
77
+
78
+ recognizer = dolla_one_recognizer(
79
+ size=250,
80
+ templates=[line_template, check_template],
81
+ templates_name=["line", "checkmark"],
82
+ n=64,
83
+ )
84
+
85
+ # Raw points to recognize (e.g. collected from a mouse drag)
86
+ query = [[x, x * 0.9 + 2] for x in range(0, 101, 4)]
87
+
88
+ best_points, best_name, score = recognizer.recognize(query)
89
+ print(best_name, score) # -> line 0.96...
90
+ ```
91
+
92
+ Try the GUI demos:
93
+
94
+ ```bash
95
+ python export_gesture_app.py # draw and save to gestures/ as CSV + PNG
96
+ python recognize_app.py # auto-load gestures/ and test recognition
97
+ ```
98
+
99
+ ## How it works
100
+
101
+ $1 applies four steps to the input points and returns the nearest template:
102
+
103
+ 1. **Resample** (`_resample`) to `n` (default 64) evenly spaced points, absorbing
104
+ differences in drawing speed.
105
+ 2. **Rotate** (`_rotate_to_zero`) so the angle from the centroid to the first point is zero.
106
+ 3. **Scale & translate** (`_scale_to_square` -> `_translate_to_origin`) into a
107
+ `size x size` square (default 250) centered at the origin.
108
+ 4. **Match** (`recognize`) against each template, searching +/-45 degrees for the
109
+ rotation that minimizes the average point-to-point distance (Golden Section Search),
110
+ and return the closest. The distance is converted to a 0-1 score.
111
+
112
+ The Protractor enhancement (a closed-form optimal angle instead of the search) is not
113
+ included in this implementation. See [README.ja.md](README.ja.md) for a fuller walkthrough.
114
+
115
+ ## API
116
+
117
+ Only the members below are part of the public API; the geometry helpers are prefixed
118
+ with `_` and are internal.
119
+
120
+ | Member | Description |
121
+ |---|---|
122
+ | `__init__(size, templates, templates_name, n)` | Initialize with a list of template strokes and their names |
123
+ | `add_template(points, name)` | Add one more template |
124
+ | `recognize(points)` | Normalize a raw stroke, match all templates, return `(best_points, best_name, score)` |
125
+
126
+ ## License & citation
127
+
128
+ Distributed under the **New BSD License** — see [LICENSE](LICENSE).
129
+
130
+ This is an independent implementation based on the pseudocode from the paper below.
131
+ The original authors and copyright holders exist separately from this port.
132
+
133
+ > Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without libraries, toolkits
134
+ > or training: A $1 recognizer for user interface prototypes. *Proceedings of the ACM
135
+ > Symposium on User Interface Software and Technology (UIST '07)*, pp. 159-168.
136
+
137
+ Official site (algorithm, pseudocode, reference implementation):
138
+ <https://depts.washington.edu/acelab/proj/dollar/index.html>
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ dolla_one_recognizer_py/__init__.py
5
+ dolla_one_recognizer_py/recognizer.py
6
+ dolla_one_recognizer_py.egg-info/PKG-INFO
7
+ dolla_one_recognizer_py.egg-info/SOURCES.txt
8
+ dolla_one_recognizer_py.egg-info/dependency_links.txt
9
+ dolla_one_recognizer_py.egg-info/requires.txt
10
+ dolla_one_recognizer_py.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ dolla_one_recognizer_py
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ from pathlib import Path
2
+ import dolla_one_recognizer_py
3
+ from setuptools import setup
4
+
5
+ long_description = Path(__file__).parent.joinpath("README.md").read_text(encoding="utf-8")
6
+
7
+ setup(
8
+ name="dolla_one_recognizer_py",
9
+ version=dolla_one_recognizer_py.__version__,
10
+ description="A Python 3 implementation of the $1 Unistroke Recognizer",
11
+ long_description=long_description,
12
+ long_description_content_type="text/markdown",
13
+ author="yuzujelly2222",
14
+ url="https://github.com/yuzujelly2222/dolla_one_recognizer_python",
15
+ license="BSD-3-Clause",
16
+ packages=["dolla_one_recognizer_py"],
17
+ install_requires=["numpy"],
18
+ python_requires=">=3.9",
19
+ classifiers=[
20
+ "License :: OSI Approved :: BSD License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ ],
28
+ )