/**
* The $P Point-Cloud Recognizer (Java version)
*
* by David White
* Copyright (c) 2012, David White. All rights reserved.
*
* based entirely on the $P Point-Cloud Recognizer (Javascript version)
* found at http://depts.washington.edu/aimgroup/proj/dollar/pdollar.html
* who's original header follows:
*
*************************************************************************
* The $P Point-Cloud Recognizer (JavaScript version)
*
* Radu-Daniel Vatavu, Ph.D.
* University Stefan cel Mare of Suceava
* Suceava 720229, Romania
* vatavu@eed.usv.ro
*
* Lisa Anthony, Ph.D.
* UMBC
* Information Systems Department
* 1000 Hilltop Circle
* Baltimore, MD 21250
* lanthony@umbc.edu
*
* Jacob O. Wobbrock, Ph.D.
* The Information School
* University of Washington
* Seattle, WA 98195-2840
* wobbrock@uw.edu
*
* The academic publication for the $P recognizer, and what should be
* used to cite it, is:
*
* Vatavu, R.-D., Anthony, L. and Wobbrock, J.O. (2012).
* Gestures as point clouds: A $P recognizer for user interface
* prototypes. Proceedings of the ACM Int'l Conference on
* Multimodal Interfaces (ICMI '12). Santa Monica, California
* (October 22-26, 2012). New York: ACM Press, pp. 273-280.
*
* This software is distributed under the "New BSD License" agreement:
*
* Copyright (c) 2012, Radu-Daniel Vatavu, Lisa Anthony, and
* Jacob O. Wobbrock. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the names of the University Stefan cel Mare of Suceava,
* University of Washington, nor UMBC, nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Radu-Daniel Vatavu OR Lisa Anthony
* OR Jacob O. Wobbrock BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
**/
package nars.gui.input.image;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import static nars.gui.input.image.PointCloudPoint.pcp;
public class PointCloudLibrary
{
private static double CLOSE_ENOUGH = 0.3;
private static PointCloudLibrary demoLibrary = null;
private ArrayList<PointCloud> _pointClouds = new ArrayList<>();
public PointCloudLibrary()
{
}
public PointCloudLibrary(ArrayList<PointCloud> pointClouds)
{
if(null == pointClouds)
{
throw new IllegalArgumentException("Point clouds cannot be null");
}
_pointClouds = pointClouds;
}
// the following is NOT part of the originally published javascript implementation
// and has been added to support addition of directional testing for point clouds
// which represent unistroke gestures
public boolean containsOnlyUnistrokes()
{
for(int i = 0; i < _pointClouds.size(); i++)
{
if(!_pointClouds.get(i).isUnistroke())
{
return false;
}
}
return true;
}
Set<PointCloud> getPointCloud(String name)
{
HashSet<PointCloud> result = new HashSet<>();
for(int i = 0; i < _pointClouds.size(); i++)
{
if(_pointClouds.get(i).getName().equals(name))
{
result.add(_pointClouds.get(i));
}
}
return result;
}
public Set<String> getNames()
{
HashSet<String> result = new HashSet<>();
for(int i = 0; i < _pointClouds.size(); i++)
{
result.add(_pointClouds.get(i).getName());
}
return result;
}
public void addPointCloud(PointCloud cloud)
{
_pointClouds.add(cloud);
}
// removes one or more point clouds carrying the specified name (which
// is case sensitive) from the library. if no matches are found, null is
// returned, else those removed are returned
public ArrayList<PointCloud> removePointCloud(String name)
{
ArrayList<PointCloud> result = null;
if(null == name || "" == name)
{
throw new IllegalArgumentException("Name must be provided");
}
for(int i = 0; i < _pointClouds.size(); i++)
{
PointCloud p = _pointClouds.get(i);
if(name != p.getName())
{
continue;
}
if(result == null)
{
result = new ArrayList<>();
result.add(p);
_pointClouds.remove(i);
}
}
return result;
}
public void clear()
{
if(this == demoLibrary)
{
_pointClouds = new ArrayList<>();
populateDemoLibrary(this);
}
else
{
_pointClouds = new ArrayList<>();
}
}
public int getSize()
{
return _pointClouds.size();
}
// most closely matches published javascript implementation of $P,
// returns only the single, best match. note that the score member of
// the result contains the aggregate distances between the two gestures.
// for the original implementation see originalRecognize() below.
public PointCloudMatchResult recognize(PointCloud inputGesture)
{
return recognize(inputGesture, false);
}
// as with published javascript implementation of $P, this returns only
// the single, best match. however it permits use of directional testing
// and, as such, should be used only with unistrokes. note that the score
// member of the result contains the aggregate distances between the two
// gestures. for the original implementation see originalRecognize() below.
public PointCloudMatchResult recognize(PointCloud inputGesture, boolean testDirectionality)
{
return recognizeAll(inputGesture, testDirectionality)[0];
}
// unlike the published javascript implementation of $P, this returns an array
// of results - one for each point cloud in the library sorted in order of
// increasing aggregate distance between the point clouds. it also permits use of
// some simple directional testing which should be used only with unistrokes. note
// that the score member of the result contains the aggregate distance between the
// two gestures. for the original implementation see originalRecognize() below.
public PointCloudMatchResult[] recognizeAll(PointCloud inputGesture, boolean testDirectionality)
{
// the following is NOT part of the originally published javascript implementation
// and has been added to support addition of directional testing for point clouds
// which represent unistroke gestures
if(testDirectionality)
{
if(!inputGesture.isUnistroke())
{
throw new IllegalArgumentException("If testDirectionality is true, input gesture must contain a unistroke");
}
if(! containsOnlyUnistrokes())
{
throw new IllegalArgumentException("If testDirectionality is true, the point cloud library must contain only unistroke point clouds");
}
}
double b = Double.POSITIVE_INFINITY;
int u = -1;
PointCloudMatchResult[] results = new PointCloudMatchResult[_pointClouds.size()];
for(int i = 0; i < _pointClouds.size(); i++) // for each point-cloud template
{
PointCloud pointCloud = _pointClouds.get(i);
// the following is NOT part of the originally published javascript implementation
// and has been added to support addition of directional testing for point clouds
// which represent unistroke gestures
if(testDirectionality)
{
// test to see if the gestures match roughly in directionality
// if not, keep looking
PointCloudPoint refStart = pointCloud.getFirstPoint();
PointCloudPoint inStart = inputGesture.getFirstPoint();
PointCloudPoint refEnd = pointCloud.getLastPoint();
PointCloudPoint inEnd = inputGesture.getLastPoint();
if((PointCloudUtils.distance(refStart, inStart) > CLOSE_ENOUGH) ||
(PointCloudUtils.distance(refEnd, inEnd) > CLOSE_ENOUGH))
{
results[i] = new PointCloudMatchResult(pointCloud.getName(), Double.POSITIVE_INFINITY);
continue;
}
}
double d = pointCloud.greedyMatch(inputGesture);
results[i] = new PointCloudMatchResult(pointCloud.getName(), d);
}
Arrays.sort(results, new Comparator<PointCloudMatchResult>()
{
public int compare(PointCloudMatchResult obj1, PointCloudMatchResult obj2)
{
if(obj1.getScore() < obj2.getScore())
{
return -1;
}
if(obj1.getScore() > obj2.getScore())
{
return 1;
}
return 0;
}
});
return results;
}
/* this method implements the recognize routine as originally published in
* javascript. in this author's experience:
* (a) the score being normalized between 0 and 1 offered little meaning or
* relationship to the quality of the match as implied by the word "score".
* "correct" matches were found to result with scores at both extremes of the
* range.
*
* (b) on occasion it was helpful to have the results of matches for each of
* the point clouds in the library. the original implementation provides only
* the "best" match.
*
* (c) $P as published implements directional invariance and does so by design.
* the main rationale for this decision stems from the value of directional invariance
* when the recognizer is used with multi-stroke gestures. however, this attribute
* of the recognizer renders it incapable of discerning similar but semantically
* different unistroke gestures such as a single top->bottom or left->right
* from a single bottom->top or right->left.
*/
public PointCloudMatchResult originalRecognize(PointCloud inputGesture)
{
double b = Double.POSITIVE_INFINITY;
int u = -1;
for(int i = 0; i < _pointClouds.size(); i++) // for each point-cloud template
{
PointCloud pointCloud = _pointClouds.get(i);
double d = inputGesture.greedyMatch(pointCloud);
if (d < b)
{
b = d; // best (least) distance
u = i; // point-cloud
}
}
if(u == -1)
{
return new PointCloudMatchResult("No match", 0.0);
}
else
{
double r = Math.max((b - 2.0) / -2.0, 0.0);
return new PointCloudMatchResult(_pointClouds.get(u).getName(), r);
}
}
public static PointCloudLibrary getDemoLibrary()
{
if(null != demoLibrary)
{
return demoLibrary;
}
demoLibrary = new PointCloudLibrary();
populateDemoLibrary(demoLibrary);
return demoLibrary;
}
private static void populateDemoLibrary(PointCloudLibrary library)
{
ArrayList<PointCloudPoint> p = new ArrayList<>();
p.add(pcp(30,7,1));
p.add(pcp(103,7,1));
p.add(pcp(66,7,2));
p.add(pcp(66,87,2));
library.addPointCloud(new PointCloud("T", p));
p = new ArrayList<>();
p.add(pcp(177,92,1));
p.add(pcp(177,2,1));
p.add(pcp(182,1,2));
p.add(pcp(246,95,2));
p.add(pcp(247,87,3));
p.add(pcp(247,1,3));
library.addPointCloud(new PointCloud("N", p));
p = new ArrayList<>();
p.add(pcp(345,9,1));
p.add(pcp(345,87,1));
p.add(pcp(351,8,2));
p.add(pcp(363,8,2));
p.add(pcp(372,9,2));
p.add(pcp(380,11,2));
p.add(pcp(386,14,2));
p.add(pcp(391,17,2));
p.add(pcp(394,22,2));
p.add(pcp(397,28,2));
p.add(pcp(399,34,2));
p.add(pcp(400,42,2));
p.add(pcp(400,50,2));
p.add(pcp(400,56,2));
p.add(pcp(399,61,2));
p.add(pcp(397,66,2));
p.add(pcp(394,70,2));
p.add(pcp(391,74,2));
p.add(pcp(386,78,2));
p.add(pcp(382,81,2));
p.add(pcp(377,83,2));
p.add(pcp(372,85,2));
p.add(pcp(367,87,2));
p.add(pcp(360,87,2));
p.add(pcp(355,88,2));
p.add(pcp(349,87,2));
library.addPointCloud(new PointCloud("D", p));
p = new ArrayList<>();
p.add(pcp(507,8,1));
p.add(pcp(507,87,1));
p.add(pcp(513,7,2));
p.add(pcp(528,7,2));
p.add(pcp(537,8,2));
p.add(pcp(544,10,2));
p.add(pcp(550,12,2));
p.add(pcp(555,15,2));
p.add(pcp(558,18,2));
p.add(pcp(560,22,2));
p.add(pcp(561,27,2));
p.add(pcp(562,33,2));
p.add(pcp(561,37,2));
p.add(pcp(559,42,2));
p.add(pcp(556,45,2));
p.add(pcp(550,48,2));
p.add(pcp(544,51,2));
p.add(pcp(538,53,2));
p.add(pcp(532,54,2));
p.add(pcp(525,55,2));
p.add(pcp(519,55,2));
p.add(pcp(513,55,2));
p.add(pcp(510,55,2));
library.addPointCloud(new PointCloud("P", p));
p = new ArrayList<>();
p.add(pcp(30,146,1));
p.add(pcp(106,222,1));
p.add(pcp(30,225,2));
p.add(pcp(106,146,2));
library.addPointCloud(new PointCloud("X", p));
p = new ArrayList<>();
p.add(pcp(188,137,1));
p.add(pcp(188,225,1));
p.add(pcp(188,180,2));
p.add(pcp(241,180,2));
p.add(pcp(241,137,3));
p.add(pcp(241,225,3));
library.addPointCloud(new PointCloud("H", p));
p = new ArrayList<>();
p.add(pcp(371,149,1));
p.add(pcp(371,221,1));
p.add(pcp(341,149,2));
p.add(pcp(401,149,2));
p.add(pcp(341,221,3));
p.add(pcp(401,221,3));
library.addPointCloud(new PointCloud("I", p));
p = new ArrayList<>();
p.add(pcp(526,142,1));
p.add(pcp(526,204,1));
p.add(pcp(526,221,2));
library.addPointCloud(new PointCloud("exclamation", p));
p = new ArrayList<>();
p.add(pcp(12,347,1));
p.add(pcp(119,347,1));
library.addPointCloud(new PointCloud("line", p));
p = new ArrayList<>();
p.add(pcp(177,396,1));
p.add(pcp(223,299,1));
p.add(pcp(262,396,1));
p.add(pcp(168,332,1));
p.add(pcp(278,332,1));
p.add(pcp(184,397,1));
library.addPointCloud(new PointCloud("five-point star", p));
p = new ArrayList<>();
p.add(pcp(382,310,1));
p.add(pcp(377,308,1));
p.add(pcp(373,307,1));
p.add(pcp(366,307,1));
p.add(pcp(360,310,1));
p.add(pcp(356,313,1));
p.add(pcp(353,316,1));
p.add(pcp(349,321,1));
p.add(pcp(347,326,1));
p.add(pcp(344,331,1));
p.add(pcp(342,337,1));
p.add(pcp(341,343,1));
p.add(pcp(341,350,1));
p.add(pcp(341,358,1));
p.add(pcp(342,362,1));
p.add(pcp(344,366,1));
p.add(pcp(347,370,1));
p.add(pcp(351,374,1));
p.add(pcp(356,379,1));
p.add(pcp(361,382,1));
p.add(pcp(368,385,1));
p.add(pcp(374,387,1));
p.add(pcp(381,387,1));
p.add(pcp(390,387,1));
p.add(pcp(397,385,1));
p.add(pcp(404,382,1));
p.add(pcp(408,378,1));
p.add(pcp(412,373,1));
p.add(pcp(416,367,1));
p.add(pcp(418,361,1));
p.add(pcp(419,353,1));
p.add(pcp(418,346,1));
p.add(pcp(417,341,1));
p.add(pcp(416,336,1));
p.add(pcp(413,331,1));
p.add(pcp(410,326,1));
p.add(pcp(404,320,1));
p.add(pcp(400,317,1));
p.add(pcp(393,313,1));
p.add(pcp(392,312,1));
p.add(pcp(418,309,2));
p.add(pcp(337,390,2));
library.addPointCloud(new PointCloud("null", p));
p = new ArrayList<>();
p.add(pcp(506,349,1));
p.add(pcp(574,349,1));
p.add(pcp(525,306,2));
p.add(pcp(584,349,2));
p.add(pcp(525,388,2));
library.addPointCloud(new PointCloud("arrowhead", p));
p = new ArrayList<>();
p.add(pcp(38,470,1));
p.add(pcp(36,476,1));
p.add(pcp(36,482,1));
p.add(pcp(37,489,1));
p.add(pcp(39,496,1));
p.add(pcp(42,500,1));
p.add(pcp(46,503,1));
p.add(pcp(50,507,1));
p.add(pcp(56,509,1));
p.add(pcp(63,509,1));
p.add(pcp(70,508,1));
p.add(pcp(75,506,1));
p.add(pcp(79,503,1));
p.add(pcp(82,499,1));
p.add(pcp(85,493,1));
p.add(pcp(87,487,1));
p.add(pcp(88,480,1));
p.add(pcp(88,474,1));
p.add(pcp(87,468,1));
p.add(pcp(62,464,2));
p.add(pcp(62,571,2));
library.addPointCloud(new PointCloud("pitchfork", p));
p = new ArrayList<>();
p.add(pcp(177,554,1));
p.add(pcp(223,476,1));
p.add(pcp(268,554,1));
p.add(pcp(183,554,1));
p.add(pcp(177,490,2));
p.add(pcp(223,568,2));
p.add(pcp(268,490,2));
p.add(pcp(183,490,2));
library.addPointCloud(new PointCloud("six-point star", p));
p = new ArrayList<>();
p.add(pcp(325,499,1));
p.add(pcp(417,557,1));
p.add(pcp(417,499,2));
p.add(pcp(325,557,2));
p.add(pcp(371,486,3));
p.add(pcp(371,571,3));
library.addPointCloud(new PointCloud("asterisk", p));
p = new ArrayList<>();
p.add(pcp(546,465,1));
p.add(pcp(546,531,1));
p.add(pcp(540,530,2));
p.add(pcp(536,529,2));
p.add(pcp(533,528,2));
p.add(pcp(529,529,2));
p.add(pcp(524,530,2));
p.add(pcp(520,532,2));
p.add(pcp(515,535,2));
p.add(pcp(511,539,2));
p.add(pcp(508,545,2));
p.add(pcp(506,548,2));
p.add(pcp(506,554,2));
p.add(pcp(509,558,2));
p.add(pcp(512,561,2));
p.add(pcp(517,564,2));
p.add(pcp(521,564,2));
p.add(pcp(527,563,2));
p.add(pcp(531,560,2));
p.add(pcp(535,557,2));
p.add(pcp(538,553,2));
p.add(pcp(542,548,2));
p.add(pcp(544,544,2));
p.add(pcp(546,540,2));
p.add(pcp(546,536,2));
library.addPointCloud(new PointCloud("half-note", p));
}
}