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.
- dolla_one_recognizer_py-0.1.2/LICENSE +66 -0
- dolla_one_recognizer_py-0.1.2/PKG-INFO +138 -0
- dolla_one_recognizer_py-0.1.2/README.md +109 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py/__init__.py +2 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py/recognizer.py +236 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py.egg-info/PKG-INFO +138 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py.egg-info/SOURCES.txt +10 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py.egg-info/dependency_links.txt +1 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py.egg-info/requires.txt +1 -0
- dolla_one_recognizer_py-0.1.2/dolla_one_recognizer_py.egg-info/top_level.txt +1 -0
- dolla_one_recognizer_py-0.1.2/setup.cfg +4 -0
- dolla_one_recognizer_py-0.1.2/setup.py +28 -0
|
@@ -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)
|
|
37
|
+
[](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
|
+

|
|
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)
|
|
8
|
+
[](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
|
+

|
|
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,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)
|
|
37
|
+
[](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
|
+

|
|
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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dolla_one_recognizer_py
|
|
@@ -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
|
+
)
|