2 * The $P+ Point-Cloud Recognizer (JavaScript version)
4 * Radu-Daniel Vatavu, Ph.D.
5 * University Stefan cel Mare of Suceava
6 * Suceava 720229, Romania
9 * The academic publication for the $P+ recognizer, and what should be
10 * used to cite it, is:
12 * Vatavu, R.-D. (2017). Improving gesture recognition accuracy on
13 * touch screens for users with low vision. Proceedings of the ACM
14 * Conference on Human Factors in Computing Systems (CHI '17). Denver,
15 * Colorado (May 6-11, 2017). New York: ACM Press, pp. 4667-4679.
16 * https://dl.acm.org/citation.cfm?id=3025941
18 * This software is distributed under the "New BSD License" agreement:
20 * Copyright (C) 2017-2018, Radu-Daniel Vatavu and Jacob O. Wobbrock. All
21 * rights reserved. Last updated July 14, 2018.
23 * Redistribution and use in source and binary forms, with or without
24 * modification, are permitted provided that the following conditions are met:
25 * * Redistributions of source code must retain the above copyright
26 * notice, this list of conditions and the following disclaimer.
27 * * Redistributions in binary form must reproduce the above copyright
28 * notice, this list of conditions and the following disclaimer in the
29 * documentation and/or other materials provided with the distribution.
30 * * Neither the name of the University Stefan cel Mare of Suceava, nor the
31 * names of its contributors may be used to endorse or promote products
32 * derived from this software without specific prior written permission.
34 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
35 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
36 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
37 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Radu-Daniel Vatavu OR Lisa Anthony
38 * OR Jacob O. Wobbrock BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
39 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
40 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
41 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
43 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
48 // use $P instead of $P+?
49 // doesn't work right yet (something is wrong with the weights)
50 // maybe this is due to the different resampler?
51 // or due to the bug in angle calculation?
52 version = use_older_dollar_p
;
59 // ////////////////////////////////////////////////////////////////////////// //
63 @property bool valid () const nothrow @safe @nogc { pragma(inline
, true); import std
.math
: isNaN
; return !score
.isNaN
; }
67 // ////////////////////////////////////////////////////////////////////////// //
70 uint id
= 0; // stroke ID to which this point belongs (1,2,...)
71 version(use_older_dollar_p
) {} else {
77 final class DPPointCloud
{
83 DPPoint
[NumPoints
] points
= DPPoint(0.0f, 0.0f);
89 this (string aname
, const(DPPoint
)[] pts
...) nothrow @nogc {
91 resample(points
, pts
);
93 translateTo(points
, DPPoint(0.0f, 0.0f));
94 version(use_older_dollar_p
) {} else computeNormalizedTurningAngles(points
);
97 void save (VFile fl
) const {
99 if (nn
.length
> 65535) nn
= nn
[0..65535]; // fuck you
100 if (nn
.length
> 254) {
101 fl
.writeNum
!ubyte(255);
102 fl
.writeNum
!ushort(cast(ushort)nn
.length
);
104 fl
.writeNum
!ubyte(cast(ubyte)nn
.length
);
106 fl
.rawWriteExact(nn
);
107 fl
.writeNum
!ubyte(NumPoints
);
108 foreach (const ref DPPoint pt
; points
[]) {
109 fl
.writeNum
!float(cast(float)pt
.x
);
110 fl
.writeNum
!float(cast(float)pt
.y
);
111 fl
.writeNum
!uint(pt
.id
);
115 void load (VFile fl
) {
117 ubyte len
= fl
.readNum
!ubyte;
119 ushort xlen
= fl
.readNum
!ushort;
125 name
= cast(string
)nn
; // it is safe to cast here
126 if (fl
.readNum
!ubyte != NumPoints
) throw new Exception("invalid number of points in cloud");
127 foreach (ref DPPoint pt
; points
[]) {
128 pt
.x
= cast(float)fl
.readNum
!float;
129 pt
.y
= cast(float)fl
.readNum
!float;
130 pt
.id
= fl
.readNum
!uint;
132 // perform last $P+ step
133 version(use_older_dollar_p
) {} else computeNormalizedTurningAngles(points
);
136 static DPPointCloud
loadNew (VFile fl
) {
137 auto res
= new DPPointCloud();
144 // ////////////////////////////////////////////////////////////////////////// //
145 final class DPGestureList
{
147 DPPointCloud
[] gestures
;
150 this () nothrow @nogc {}
156 string
[] knownGestureNames () {
158 mainloop
: foreach (DPPointCloud gst
; gestures
) {
159 foreach (string s
; res
) if (s
== gst
.name
) continue mainloop
;
165 void appendGesture (string aname
, const(DPPoint
)[] pts
...) {
166 appendGesture(new DPPointCloud(aname
, pts
));
169 // you can add as many gestures with same name as you want to
170 void appendGesture (DPPointCloud gg
) {
171 import core
.memory
: GC
;
172 if (gg
is null) return;
173 auto optr
= gestures
.ptr
;
175 if (gestures
.ptr
!is optr
) {
177 if (optr
is GC
.addrOf(optr
)) GC
.setAttr(optr
, GC
.BlkAttr
.NO_INTERIOR
);
181 // remove *ALL* gestures with the given name
182 void removeGesture (const(char)[] name
) {
184 while (idx
< gestures
.length
) {
185 if (gestures
[idx
].name
== name
) {
186 foreach (immutable c
; idx
+1..gestures
.length
) gestures
[c
-1] = gestures
[c
];
187 gestures
[$-1] = null;
188 gestures
.length
-= 1;
189 gestures
.assumeSafeAppend
;
196 // if you have more than one gesture with the same name, keep increasing `idx` to get more, until you'll get `null`
197 DPPointCloud
findGesture (const(char)[] name
, uint idx
=0) nothrow @nogc {
198 foreach (DPPointCloud g
; gestures
) {
199 if (g
.name
== name
) {
200 if (idx
-- == 0) return g
;
206 DPResult
recognize (const(DPPoint
)[] origpoints
) nothrow @nogc {
207 import std
.algorithm
: max
;
208 if (origpoints
.length
< 2) return DPResult
.init
;
209 DPPoint
[DPPointCloud
.NumPoints
] points
;
210 resample(points
, origpoints
);
212 translateTo(points
, DPPoint(0.0f, 0.0f));
213 version(use_older_dollar_p
) {} else computeNormalizedTurningAngles(points
);
214 float b
= float.infinity
;
216 foreach (immutable idx
, DPPointCloud gst
; gestures
) {
217 version(use_older_dollar_p
) {
218 immutable float d
= greedyCloudMatch(points
, gst
);
220 immutable float d0
= cloudDistance(points
, gst
.points
);
221 immutable float d1
= cloudDistance(gst
.points
, points
);
222 immutable float d
= (d0
< d1 ? d0
: d1
);
225 b
= d
; // best (least) distance
226 u
= cast(int)idx
; // point-cloud
229 if (u
== -1) return DPResult();
230 { import core
.stdc
.stdio
; printf("b=%f (%f) (%f)\n", b
, (b
-2.0)/-2.0, (b
> 1.0f ?
1.0f/b
: 1.0f)); }
231 version(use_older_dollar_p
) {
232 return (u
!= -1 ?
DPResult(gestures
[u
].name
, max((b
-2.0f)/-2.0f, 0.0f)) : DPResult
.init
);
234 //return (u != -1 ? DPResult(gestures[u].name, max((b-2.0f)/-2.0f, 0.0f)) : DPResult.init);
235 return (u
!= -1 ?
DPResult(gestures
[u
].name
, (b
> 1.0f ?
1.0f/b
: 1.0f)) : DPResult
.init
);
240 enum Signature
= "DOLP8LB0";
242 void save (VFile fl
) const {
243 fl
.rawWriteExact(Signature
);
244 fl
.writeNum
!uint(cast(uint)gestures
.length
);
245 foreach (const DPPointCloud gst
; gestures
) gst
.save(fl
);
248 void load (VFile fl
) {
250 char[Signature
.length
] sign
;
251 fl
.rawReadExact(sign
);
252 if (sign
[0..$-1] != Signature
[0..$-1]) throw new Exception("invalid $P library signature");
253 if (sign
[$-1] != Signature
[$-1]) throw new Exception("invalid $P library version");
254 uint count
= fl
.readNum
!uint;
255 if (count
> uint.max
/16) throw new Exception("too many gestures in library");
256 gestures
.reserve(count
);
257 foreach (immutable idx
; 0..count
) appendGesture(DPPointCloud
.loadNew(fl
));
262 // ////////////////////////////////////////////////////////////////////////// //
265 version(use_older_dollar_p
) {
266 float greedyCloudMatch(size_t n
) (in ref DPPoint
[n
] points
, in DPPointCloud P
) nothrow @nogc {
267 import std
.algorithm
: min
;
268 import std
.math
: floor
, pow
;
269 enum e
= cast(float)0.50;
270 int step
= cast(int)floor(pow(points
.length
, 1-e
));
272 float minv
= float.infinity
;
273 for (int i
= 0; i
< points
.length
; i
+= step
) {
274 auto d1
= cloudDistanceOld(points
, P
.points
, i
);
275 auto d2
= cloudDistanceOld(P
.points
, points
, i
);
276 minv
= min(minv
, d1
, d2
);
281 float cloudDistanceOld(size_t n
) (in ref DPPoint
[n
] pts1
, in ref DPPoint
[n
] pts2
, int start
) nothrow @nogc {
282 import std
.math
: sqrt
;
283 bool[n
] matched
= false;
288 float minv
= float.infinity
;
289 foreach (immutable j
, bool mtv
; matched
[]) {
291 auto d
= distanceSqr(pts1
[i
], pts2
[j
]);
292 if (d
< minv
) { minv
= d
; index
= cast(int)j
; }
296 matched
[index
] = true;
297 float weight
= cast(float)1-((i
-start
+cast(int)pts1
.length
)%cast(int)pts1
.length
)/cast(float)pts1
.length
;
298 sum
+= weight
*sqrt(minv
);
299 i
= (i
+1)%cast(int)pts1
.length
;
300 } while (i
!= start
);
305 float cloudDistance(size_t n
) (in ref DPPoint
[n
] pts1
, in ref DPPoint
[n
] pts2
) nothrow @nogc {
306 import std
.math
: sqrt
;
307 bool[n
] matched
= false;
311 foreach (immutable i
; 0..pts1
.length
) {
313 float minv
= float.infinity
;
314 foreach (immutable j
; 0..pts2
.length
) {
315 immutable float d
= distanceWithAngleSqr(pts1
[i
], pts2
[j
]);
321 if (!matched
[index
]) {
323 matched
[index
] = true;
325 sum
+= cast(float)sqrt(minv
);
328 if (mtcount
!= matched
.length
) {
329 foreach (immutable j
, bool mtv
; matched
[]) {
331 float minv
= float.infinity
;
332 foreach (immutable i
; 0..pts1
.length
) {
333 immutable float d
= distanceWithAngleSqr(pts1
[i
], pts2
[j
]);
334 if (d
< minv
) minv
= d
;
336 sum
+= cast(float)sqrt(minv
);
346 void resample(size_t n
) (ref DPPoint
[n
] newpoints
, in DPPoint
[] points
) nothrow @nogc {
347 import std
.algorithm
: max
, min
;
348 import std
.math
: isNaN
;
350 assert(points
.length
> 1);
351 immutable float I
= pathLength(points
)/(n
-1); // interval length
354 newpoints
[nppos
++] = points
[0];
355 foreach (immutable idx
; 1..points
.length
) {
356 if (points
[idx
].id
== points
[idx
-1].id
) {
357 auto d
= distance(points
[idx
-1], points
[idx
]);
359 DPPoint firstPoint
= points
[idx
-1];
361 // add interpolated point
362 float t
= min(max((I
-D
)/d
, 0.0f), 1.0f);
363 if (isNaN(t
)) t
= 0.5f;
364 newpoints
[nppos
++] = DPPoint((cast(float)1-t
)*firstPoint
.x
+t
*points
[idx
].x
,
365 (cast(float)1-t
)*firstPoint
.y
+t
*points
[idx
].y
, points
[idx
].id
);
366 // update partial length
369 firstPoint
= newpoints
[nppos
-1];
378 // sometimes we fall a rounding-error short of adding the last point, so add it if so
379 newpoints
[nppos
++] = DPPoint(points
[$-1].x
, points
[$-1].y
, points
[$-1].id
);
385 void scale(size_t n
) (ref DPPoint
[n
] points
) nothrow @nogc {
386 import std
.algorithm
: max
, min
;
387 float minX
= +float.infinity
;
388 float minY
= +float.infinity
;
389 float maxX
= -float.infinity
;
390 float maxY
= -float.infinity
;
391 foreach (ref DPPoint p
; points
[]) {
392 minX
= min(minX
, p
.x
);
393 minY
= min(minY
, p
.y
);
394 maxX
= max(maxX
, p
.x
);
395 maxY
= max(maxY
, p
.y
);
397 immutable float size
= max(maxX
-minX
, maxY
-minY
);
398 foreach (ref DPPoint p
; points
[]) {
399 p
.x
= (p
.x
-minX
)/size
;
400 p
.y
= (p
.y
-minY
)/size
;
405 // translates points' centroid
406 void translateTo(size_t n
) (ref DPPoint
[n
] points
, DPPoint pt
) nothrow @nogc {
407 auto c
= centroid(points
);
408 foreach (ref DPPoint p
; points
[]) {
415 DPPoint
centroid(size_t n
) (in ref DPPoint
[n
] points
) nothrow @nogc {
416 float x
= 0.0f, y
= 0.0f;
417 foreach (const ref DPPoint p
; points
[]) {
421 immutable float pl
= cast(float)1/cast(float)points
.length
;
422 return DPPoint(x
*pl
, y
*pl
);
426 // modifies `points` angles
427 void computeNormalizedTurningAngles(size_t n
) (ref DPPoint
[n
] points
) nothrow @nogc {
428 import std
.math
: acos
, PI
;
429 points
[0].angle
= 0.0f;
430 foreach (immutable idx
; 1..n
-1) {
431 immutable float dx
= (points
[idx
+1].x
-points
[idx
].x
)*(points
[idx
].x
-points
[idx
-1].x
);
432 immutable float dy
= (points
[idx
+1].y
-points
[idx
].y
)*(points
[idx
].y
-points
[idx
-1].y
);
433 immutable float dn
= distance(points
[idx
+1], points
[idx
])*distance(points
[idx
], points
[idx
-1]);
434 //float cosangle = Math.max(-1.0, Math.min(1.0, (dx + dy) / dn)); // ensure [-1,+1]
435 float cosangle
= (dx
+dy
)/dn
;
437 if (cosangle
< -1.0f) cosangle
= -1.0f; else if (cosangle
> 1.0f) cosangle
= 1.0f;
438 immutable float angle
= acos(cosangle
)/cast(float)PI
; // normalized angle
439 points
[idx
].angle
= angle
;
442 points
[n
-1].angle
= 0.0f;
446 // length traversed by a point path
447 float pathLength (const(DPPoint
)[] points
) nothrow @nogc {
449 foreach (immutable idx
; 1..points
.length
) {
450 if (points
[idx
].id
== points
[idx
-1].id
) d
+= distance(points
[idx
-1], points
[idx
]);
456 // Euclidean distance between two points
457 float distance() (in auto ref DPPoint p1
, in auto ref DPPoint p2
) nothrow @nogc {
458 import std
.math
: sqrt
;
459 immutable float dx
= p2
.x
-p1
.x
;
460 immutable float dy
= p2
.y
-p1
.y
;
461 return cast(float)sqrt(dx
*dx
+dy
*dy
);
465 version(use_older_dollar_p
) {
466 // Euclidean distance between two points
467 float distanceSqr() (in auto ref DPPoint p1
, in auto ref DPPoint p2
) nothrow @nogc {
468 immutable float dx
= p2
.x
-p1
.x
;
469 immutable float dy
= p2
.y
-p1
.y
;
470 return cast(float)(dx
*dx
+dy
*dy
);
473 float distanceWithAngleSqr() (in auto ref DPPoint p1
, in auto ref DPPoint p2
) nothrow @nogc {
474 import std
.math
: sqrt
;
475 immutable float dx
= p2
.x
-p1
.x
;
476 immutable float dy
= p2
.y
-p1
.y
;
477 immutable float da = p2
.angle
-p1
.angle
;
478 //return cast(float)sqrt(dx*dx+dy*dy+da*da);
479 return cast(float)(dx
*dx
+dy
*dy
+da*da);