2 * The $P Point-Cloud Recognizer
4 * Radu-Daniel Vatavu, Ph.D.
5 * University Stefan cel Mare of Suceava
6 * Suceava 720229, Romania
11 * Information Systems Department
16 * Jacob O. Wobbrock, Ph.D.
17 * The Information School
18 * University of Washington
19 * Seattle, WA 98195-2840
22 * The academic publication for the $P recognizer, and what should be
23 * used to cite it, is:
25 * Vatavu, R.-D., Anthony, L. and Wobbrock, J.O. (2012).
26 * Gestures as point clouds: A $P recognizer for user interface
27 * prototypes. Proceedings of the ACM Int'l Conference on
28 * Multimodal Interfaces (ICMI '12). Santa Monica, California
29 * (October 22-26, 2012). New York: ACM Press, pp. 273-280.
31 * This software is distributed under the "New BSD License" agreement:
33 * Copyright (c) 2012, Radu-Daniel Vatavu, Lisa Anthony, and
34 * Jacob O. Wobbrock. All rights reserved.
36 * Redistribution and use in source and binary forms, with or without
37 * modification, are permitted provided that the following conditions are met:
38 * * Redistributions of source code must retain the above copyright
39 * notice, this list of conditions and the following disclaimer.
40 * * Redistributions in binary form must reproduce the above copyright
41 * notice, this list of conditions and the following disclaimer in the
42 * documentation and/or other materials provided with the distribution.
43 * * Neither the names of the University Stefan cel Mare of Suceava,
44 * University of Washington, nor UMBC, nor the names of its contributors
45 * may be used to endorse or promote products derived from this software
46 * without specific prior written permission.
48 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
49 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
50 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
51 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Radu-Daniel Vatavu OR Lisa Anthony
52 * OR Jacob O. Wobbrock BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
53 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
54 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
55 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
56 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
57 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
67 // ////////////////////////////////////////////////////////////////////////// //
68 alias DPFloat
= float;
71 // ////////////////////////////////////////////////////////////////////////// //
75 @property bool valid () const nothrow @safe @nogc { pragma(inline
, true); import std
.math
: isNaN
; return !score
.isNaN
; }
79 // ////////////////////////////////////////////////////////////////////////// //
82 uint id
; // stroke ID to which this point belongs (1,2,...)
86 final class DPPointCloud
{
92 DPPoint
[NumPoints
] mPoints
= DPPoint(0, 0, 0);
98 this (string aname
, const(DPPoint
)[] pts
...) nothrow @nogc {
100 resample(mPoints
, pts
);
102 translateTo(mPoints
, DPPoint(0, 0));
105 final @property const(DPPoint
)[] points () const pure nothrow @safe @nogc { pragma(inline
, true); return mPoints
[]; }
106 final @property string
name () const pure nothrow @safe @nogc { pragma(inline
, true); return mName
; }
108 void save (VFile fl
) const {
110 if (nn
.length
> 65535) nn
= nn
[0..65535]; // fuck you
111 if (nn
.length
> 254) {
112 fl
.writeNum
!ubyte(255);
113 fl
.writeNum
!ushort(cast(ushort)nn
.length
);
115 fl
.writeNum
!ubyte(cast(ubyte)nn
.length
);
117 fl
.rawWriteExact(nn
);
118 fl
.writeNum
!ubyte(NumPoints
);
119 foreach (const ref DPPoint pt
; mPoints
[]) {
120 fl
.writeNum
!float(cast(float)pt
.x
);
121 fl
.writeNum
!float(cast(float)pt
.y
);
122 fl
.writeNum
!uint(pt
.id
);
126 void load (VFile fl
) {
128 ubyte len
= fl
.readNum
!ubyte;
130 ushort xlen
= fl
.readNum
!ushort;
136 mName
= cast(string
)nn
; // it is safe to cast here
137 if (fl
.readNum
!ubyte != NumPoints
) throw new Exception("invalid number of points in cloud");
138 foreach (ref DPPoint pt
; mPoints
[]) {
139 pt
.x
= cast(DPFloat
)fl
.readNum
!float;
140 pt
.y
= cast(DPFloat
)fl
.readNum
!float;
141 pt
.id
= fl
.readNum
!uint;
145 static DPPointCloud
loadNew (VFile fl
) {
146 auto res
= new DPPointCloud();
153 // ////////////////////////////////////////////////////////////////////////// //
154 final class DPGestureList
{
156 DPPointCloud
[] mGestures
;
159 this () nothrow @nogc {}
165 final @property const(DPPointCloud
)[] gestures () const pure nothrow @safe @nogc { pragma(inline
, true); return mGestures
; }
167 string
[] knownGestureNames () {
169 mainloop
: foreach (DPPointCloud gst
; mGestures
) {
170 foreach (string s
; res
) if (s
== gst
.mName
) continue mainloop
;
176 void appendGesture (string aname
, const(DPPoint
)[] pts
...) {
177 appendGesture(new DPPointCloud(aname
, pts
));
180 // you can add as many gestures with same name as you want to
181 void appendGesture (DPPointCloud gg
) {
182 import core
.memory
: GC
;
183 if (gg
is null) return;
184 auto optr
= mGestures
.ptr
;
186 if (mGestures
.ptr
!is optr
) {
187 optr
= mGestures
.ptr
;
188 if (optr
is GC
.addrOf(optr
)) GC
.setAttr(optr
, GC
.BlkAttr
.NO_INTERIOR
);
192 // remove *ALL* gestures with the given name
193 void removeGesture (const(char)[] name
) {
195 while (idx
< mGestures
.length
) {
196 if (mGestures
[idx
].mName
== name
) {
197 foreach (immutable c
; idx
+1..mGestures
.length
) mGestures
[c
-1] = mGestures
[c
];
198 mGestures
[$-1] = null;
199 mGestures
.length
-= 1;
200 mGestures
.assumeSafeAppend
;
207 // if you have more than one gesture with the same name, keep increasing `idx` to get more, until you'll get `null`
208 DPPointCloud
findGesture (const(char)[] name
, uint idx
=0) nothrow @nogc {
209 foreach (DPPointCloud g
; mGestures
) {
210 if (g
.mName
== name
) {
211 if (idx
-- == 0) return g
;
217 DPResult
recognize (const(DPPoint
)[] origpoints
) nothrow @nogc {
218 import std
.algorithm
: max
;
219 if (origpoints
.length
< 2) return DPResult
.init
;
220 DPPoint
[DPPointCloud
.NumPoints
] points
;
221 resample(points
, origpoints
);
223 translateTo(points
, DPPoint(0, 0));
224 DPFloat b
= DPFloat
.infinity
;
226 foreach (immutable idx
, DPPointCloud gst
; mGestures
) {
227 auto d
= greedyCloudMatch(points
, gst
);
229 b
= d
; // best (least) distance
230 u
= cast(int)idx
; // point-cloud
233 if (u
== -1) return DPResult();
234 //{ import core.stdc.stdio; printf("b=%f (%f)\n", b, (b-2.0)/-2.0); }
235 return (u
!= -1 ?
DPResult(mGestures
[u
].mName
, max((b
-2.0)/-2.0, 0.0)) : DPResult
.init
);
239 enum Signature
= "DOLP8LB0";
241 void save (VFile fl
) const {
242 fl
.rawWriteExact(Signature
);
243 fl
.writeNum
!uint(cast(uint)mGestures
.length
);
244 foreach (const DPPointCloud gst
; mGestures
) gst
.save(fl
);
247 void load (VFile fl
) {
249 char[Signature
.length
] sign
;
250 fl
.rawReadExact(sign
);
251 if (sign
[0..$-1] != Signature
[0..$-1]) throw new Exception("invalid $P library signature");
252 if (sign
[$-1] != Signature
[$-1]) throw new Exception("invalid $P library version");
253 uint count
= fl
.readNum
!uint;
254 if (count
> uint.max
/16) throw new Exception("too many gestures in library");
255 mGestures
.reserve(count
);
256 foreach (immutable idx
; 0..count
) appendGesture(DPPointCloud
.loadNew(fl
));
261 // ////////////////////////////////////////////////////////////////////////// //
264 DPFloat
greedyCloudMatch(usize n
) (in ref DPPoint
[n
] points
, in DPPointCloud P
) nothrow @nogc {
265 import std
.algorithm
: min
;
266 import std
.math
: floor
, pow
;
267 enum e
= cast(DPFloat
)0.50;
268 int step
= cast(int)floor(pow(points
.length
, 1-e
));
270 DPFloat minv
= DPFloat
.infinity
;
271 for (int i
= 0; i
< points
.length
; i
+= step
) {
272 auto d1
= cloudDistance(points
, P
.mPoints
, i
);
273 auto d2
= cloudDistance(P
.mPoints
, points
, i
);
274 minv
= min(minv
, d1
, d2
);
280 DPFloat
cloudDistance(usize n
) (in ref DPPoint
[n
] pts1
, in ref DPPoint
[n
] pts2
, int start
) nothrow @nogc {
281 import std
.math
: sqrt
;
282 bool[n
] matched
= false;
287 DPFloat minv
= DPFloat
.infinity
;
288 foreach (immutable j
, bool mtv
; matched
[]) {
290 auto d
= distanceSqr(pts1
[i
], pts2
[j
]);
291 if (d
< minv
) { minv
= d
; index
= cast(int)j
; }
295 matched
[index
] = true;
296 DPFloat weight
= cast(DPFloat
)1-((i
-start
+cast(int)pts1
.length
)%cast(int)pts1
.length
)/cast(DPFloat
)pts1
.length
;
297 sum
+= weight
*sqrt(minv
);
298 i
= (i
+1)%cast(int)pts1
.length
;
299 } while (i
!= start
);
304 void resample(usize n
) (ref DPPoint
[n
] newpoints
, in DPPoint
[] points
) nothrow @nogc {
305 import std
.algorithm
: max
, min
;
306 import std
.math
: isNaN
;
308 assert(points
.length
> 1);
309 immutable DPFloat I
= pathLength(points
)/(n
-1); // interval length
312 newpoints
[nppos
++] = points
[0];
313 foreach (immutable idx
; 1..points
.length
) {
314 if (points
[idx
].id
== points
[idx
-1].id
) {
315 auto d
= distance(points
[idx
-1], points
[idx
]);
317 DPPoint firstPoint
= points
[idx
-1];
319 // add interpolated point
320 DPFloat t
= min(max((I
-D
)/d
, cast(DPFloat
)0), cast(DPFloat
)1);
321 if (isNaN(t
)) t
= cast(DPFloat
)0.5;
322 newpoints
[nppos
++] = DPPoint((cast(DPFloat
)1-t
)*firstPoint
.x
+t
*points
[idx
].x
,
323 (cast(DPFloat
)1-t
)*firstPoint
.y
+t
*points
[idx
].y
, points
[idx
].id
);
324 // update partial length
327 firstPoint
= newpoints
[nppos
-1];
336 // sometimes we fall a rounding-error short of adding the last point, so add it if so
337 newpoints
[nppos
++] = DPPoint(points
[$-1].x
, points
[$-1].y
, points
[$-1].id
);
343 void scale(usize n
) (ref DPPoint
[n
] points
) nothrow @nogc {
344 import std
.algorithm
: max
, min
;
345 DPFloat minX
= DPFloat
.infinity
;
346 DPFloat minY
= DPFloat
.infinity
;
347 DPFloat maxX
= -DPFloat
.infinity
;
348 DPFloat maxY
= -DPFloat
.infinity
;
349 foreach (ref DPPoint p
; points
[]) {
350 minX
= min(minX
, p
.x
);
351 minY
= min(minY
, p
.y
);
352 maxX
= max(maxX
, p
.x
);
353 maxY
= max(maxY
, p
.y
);
355 DPFloat size
= max(maxX
-minX
, maxY
-minY
);
356 foreach (ref DPPoint p
; points
[]) {
357 p
.x
= (p
.x
-minX
)/size
;
358 p
.y
= (p
.y
-minY
)/size
;
363 // translates points' centroid
364 void translateTo(usize n
) (ref DPPoint
[n
] points
, DPPoint pt
) nothrow @nogc {
365 auto c
= centroid(points
);
366 foreach (ref DPPoint p
; points
[]) {
373 DPPoint
centroid(usize n
) (in ref DPPoint
[n
] points
) nothrow @nogc {
374 DPFloat x
= 0, y
= 0;
375 foreach (const ref DPPoint p
; points
[]) {
379 immutable DPFloat pl
= cast(DPFloat
)1/cast(DPFloat
)points
.length
;
380 return DPPoint(x
*pl
, y
*pl
);
384 // length traversed by a point path
385 DPFloat
pathLength (const(DPPoint
)[] points
) nothrow @nogc {
387 foreach (immutable idx
; 1..points
.length
) {
388 if (points
[idx
].id
== points
[idx
-1].id
) d
+= distance(points
[idx
-1], points
[idx
]);
394 // Euclidean distance between two points
395 DPFloat
distance() (in auto ref DPPoint p1
, in auto ref DPPoint p2
) nothrow @nogc {
396 import std
.math
: sqrt
;
397 immutable DPFloat dx
= p2
.x
-p1
.x
;
398 immutable DPFloat dy
= p2
.y
-p1
.y
;
399 return cast(DPFloat
)sqrt(dx
*dx
+dy
*dy
);
403 // Euclidean distance between two points
404 DPFloat
distanceSqr() (in auto ref DPPoint p1
, in auto ref DPPoint p2
) nothrow @nogc {
405 immutable DPFloat dx
= p2
.x
-p1
.x
;
406 immutable DPFloat dy
= p2
.y
-p1
.y
;
407 return cast(DPFloat
)(dx
*dx
+dy
*dy
);