ShapeContext.java

00001 package edu.stanford.hci.r3.pen.gesture;
00002 
00003 import java.io.IOException;
00004 import java.io.Writer;
00005 import java.text.DecimalFormat;
00006 import java.util.ArrayList;
00007 
00008 import edu.stanford.hci.r3.pen.PenSample;
00009 
00020 public class ShapeContext {
00021 
00022         public static int bands = 3;
00023 
00024         public static double[] logPolarAndTime(PenSample first, PenSample second, double distanceScaling,
00025                         double baseRotation) {
00026                 // normalize the times. actually, probably ought to normalize all of them -
00027                 // scale-invariance? not theta, though.
00028                 double dx = second.x - first.x;
00029                 double dy = second.y - first.y;
00030                 double[] results = new double[3];
00031                 if (dx == 0 && dy == 0) {
00032                         results[0] = distanceScaling; // hack
00033                         results[1] = 0;
00034                 } else {
00035                         results[0] = Math.sqrt(dx * dx + dy * dy) / distanceScaling;
00036                         // results[0] = .5 * Math.log((dx*dx + dy*dy)/* / distanceScaling */);
00037                         results[1] = renormalize(Math.atan2(dy, dx) - baseRotation);
00038                 }
00039                 results[2] = second.timestamp - first.timestamp;
00040                 return results;
00041         }
00042 
00043         public static double renormalize(double theta) { // cast into -PI to PI
00044                 if (theta < -Math.PI)
00045                         return theta + 2 * Math.PI;
00046                 if (theta > Math.PI)
00047                         return theta - 2 * Math.PI;
00048                 return theta;
00049 
00050         }
00051 
00052         String authorName;
00053 
00054         ArrayList<PenSample> controlPoints = new ArrayList<PenSample>();
00055 
00056         public ShapeContext(ArrayList<PenSample> controlPointsInput, String authorName) {
00057                 this.authorName = authorName;
00058                 // filter this for dupes
00059                 double[] times = new double[controlPointsInput.size()];
00060                 for (int i = 0; i < controlPointsInput.size(); i++) {
00061                         PenSample sample = controlPointsInput.get(i);
00062                         boolean duplicate = false;
00063                         for (PenSample old_sample : controlPoints)
00064                                 if (sample.x == old_sample.x && sample.y == old_sample.y) { // can't duplicate
00065                                         // samples,
00066                                         // unfortunately
00067                                         duplicate = true;
00068                                         break;
00069                                 }
00070                         if (duplicate)
00071                                 continue;
00072                         times[controlPoints.size()] = sample.timestamp;
00073                         controlPoints.add(sample);
00074                 }
00075                 // smooth time values, cuz the times from the pen streaming stink:
00076                 for (int i = 1; i < controlPoints.size() - 1; i++) {
00077                         controlPoints.get(i).timestamp = (long) ((times[i - 1] + times[i] + times[i + 1]) / 3);
00078                 }
00079                 // there's me ANN done
00080                 // ANN ann = new ANN();
00081         }
00082 
00083         double blend(int i, double t) {
00084                 switch (i) {
00085                 case 0:
00086                         return ((-t + 2) * t - 1) * t / 2;
00087                 case 1:
00088                         return (((3 * t - 5) * t) * t + 2) / 2;
00089                 case 2:
00090                         return ((-3 * t + 4) * t + 1) * t / 2;
00091                 case 3:
00092                         return ((t - 1) * t * t) / 2;
00093                 }
00094                 return 0; // we only get here if an invalid i is specified
00095         }
00096 
00097         // Catmull-Rom FTW
00098         PenSample blendHelper(int i, double t) {
00099                 PenSample[] samples = new PenSample[4];
00100                 PenSample blendedSample = new PenSample(0, 0, (int) 0, (long) 0);
00101                 if (i == 0) // double up the first
00102                         samples[0] = controlPoints.get(0);
00103                 else
00104                         samples[0] = controlPoints.get(i - 1);
00105                 samples[1] = controlPoints.get(i);
00106                 samples[2] = controlPoints.get(i + 1);
00107                 if (i + 2 >= controlPoints.size())
00108                         samples[3] = controlPoints.get(controlPoints.size() - 1);
00109                 else
00110                         samples[3] = controlPoints.get(i + 2);
00111                 for (int ii = 0; ii < 4; ii++) {
00112                         double b = blend(ii, t);
00113                         blendedSample.x += samples[ii].x * b;
00114                         blendedSample.y += samples[ii].y * b;
00115                         blendedSample.timestamp += samples[ii].timestamp * b;
00116                 }
00117                 return blendedSample;
00118         }
00119 
00120         double dblend_du(int i, double t) {
00121                 switch (i) {
00122                 case 0:
00123                         return (4 * t - 1 - 3 * t * t) / 2;
00124                 case 1:
00125                         return (9 * t * t - 10 * t) / 2;
00126                 case 2:
00127                         return (8 * t - 9 * t * t + 1) / 2;
00128                 case 3:
00129                         return (3 * t * t - 2 * t) / 2;
00130                 }
00131                 return 0; // we only get here if an invalid i is specified
00132         }
00133 
00134         public ArrayList<ShapeHistogram> generateShapeHistogram(int points, int dummy_padding,
00135                         boolean rotationInvariant, boolean timeSensitive) {
00136                 // histogram for each point
00137                 int dummy_points = points - dummy_padding;// size();
00138                 ArrayList<ShapeHistogram> histograms = new ArrayList<ShapeHistogram>();
00139                 ArrayList<PenSample> samples = resample(dummy_points);
00140                 ArrayList<PenSample> tangents = new ArrayList<PenSample>();// tangents(points);
00141                 for (int i = 0; i < samples.size(); i++) {
00142                         PenSample last, next;
00143                         if (i == 0)
00144                                 last = samples.get(0);
00145                         else
00146                                 last = samples.get(i - 1);
00147                         if (i == dummy_points - 1)
00148                                 next = samples.get(dummy_points - 1);
00149                         else
00150                                 next = samples.get(i + 1);
00151                         tangents.add(new PenSample(next.x - last.x, next.y - last.y, 0, 0)); // crude
00152                 }
00153                 // int bands = timeSensitive?3:2;
00154                 double[][] bins = new double[3][];
00155                 int[] bin_counts = new int[3];
00156                 boolean[] explicit_binning = new boolean[3];
00157                 explicit_binning[0] = true; // log scaled distances
00158                 bin_counts[0] = 5; // log r
00159                 bin_counts[1] = 12; // theta
00160                 bin_counts[2] = 2; // t (for the moment, only before/after)
00161                 // want a bin for "component" - how to discretize? connected ought to be fine. this may
00162                 // be redundant with time...somewhat
00163                 double[] mins = new double[3];
00164                 mins[0] = Double.MAX_VALUE; // not; calc this based on actual min
00165                 mins[1] = -Math.PI; // yup
00166                 mins[2] = Double.MAX_VALUE; // not; calc on actual mins
00167                 double[] maxes = new double[3];
00168                 maxes[0] = -Double.MAX_VALUE;
00169                 maxes[1] = Math.PI;
00170                 maxes[2] = -Double.MAX_VALUE;
00171                 double distance_min = Double.MAX_VALUE;
00172                 double distance_max = -Double.MAX_VALUE;
00173                 double timestamp_min = Double.MAX_VALUE;
00174                 double timestamp_max = -Double.MAX_VALUE;
00175                 double mean_distance = 0;
00176                 for (PenSample sample : samples) {
00177                         // wrong, should be considering deltas.
00178                         // Alt, normalize all the times ahead.
00179                         if (sample.timestamp < timestamp_min)
00180                                 timestamp_min = sample.timestamp;
00181                         if (sample.timestamp > timestamp_max)
00182                                 timestamp_max = sample.timestamp;
00183                         for (PenSample secondSample : samples) {
00184                                 if (sample.equals(secondSample))
00185                                         continue;
00186                                 double distance = Math.sqrt(Math.pow(sample.x - secondSample.x, 2)
00187                                                 + Math.pow(sample.y - secondSample.y, 2));
00188                                 mean_distance += distance;
00189                                 if (distance > 0) {
00190                                         distance_min = Math.min(distance_min, distance);
00191                                         distance_max = Math.max(distance_max, distance);
00192                                 }
00193                         }
00194                 }
00195                 mean_distance /= (dummy_points * (dummy_points - 1)) / 2;
00196                 double inner_threshold = Math.log10(mean_distance / 8);
00197                 double outer_threshold = Math.log10(2 * mean_distance);
00198                 double logscale = (outer_threshold - inner_threshold) / (bin_counts[0] - 1);
00199                 // log scale it
00200                 bins[0] = new double[bin_counts[0]];
00201                 for (int i = 0; i < bin_counts[0]; i++) {
00202                         bins[0][i] = Math.pow(10, inner_threshold + i * logscale);
00203                 }
00204                 mins[0] = Math.log(distance_min) - .01;
00205                 maxes[0] = Math.log(distance_max) + .01;
00206                 mins[2] = timestamp_min - timestamp_max;
00207                 maxes[2] = -mins[2];
00208                 // mins[0] = Math.log(distance_min);
00209                 // maxes[0] = Math.log(distance_max);
00210                 for (PenSample sample : samples) {
00211                         ShapeHistogram histogram = new ShapeHistogram(bin_counts, mins, maxes, explicit_binning, bins,
00212                                         bands);
00213                         PenSample tangent = tangents.get(samples.indexOf(sample)); // sue me, I'm lax
00214                         double theta = rotationInvariant ? Math.atan2(tangent.y, tangent.x) : 0;
00215                         for (PenSample secondSample : samples) {
00216                                 if (sample.equals(secondSample))
00217                                         continue;
00218                                 // for rotation invariance, adjust angles by setting normal to curve to some
00219                                 // axis. slightly tricky, but not too bad.
00220                                 // I guess I'll do this the easy way (via spline lookups and calculation). Is
00221                                 // there an analytic way? Probably. But I'm lazy.
00222                                 histogram.addPoint(logPolarAndTime(sample, secondSample, mins[0]/* sum * sum */, theta));
00223                         }
00224                         histograms.add(histogram);
00225                 }
00226                 for (int i = 0; i < points - samples.size(); i++) {
00227                         histograms.add(new ShapeHistogram(bin_counts, mins, maxes, explicit_binning, bins, bands)); // blank
00228                         // histograms
00229                 }
00230                 return histograms;
00231         }
00232 
00233         public double[][] points() {
00234                 return points(size());
00235         }
00236 
00237         public double[][] points(int N) {
00238                 ArrayList<PenSample> controlPoints = resample(N);
00239                 double[][] pts = new double[N][2];
00240                 for (int i = 0; i < N; i++) {
00241                         pts[i][0] = controlPoints.get(i).x;
00242                         pts[i][1] = controlPoints.get(i).y;
00243                 }
00244                 return pts;
00245         }
00246 
00247         public void quillWrite(Writer writer) throws IOException {
00248                 final DecimalFormat df = new DecimalFormat("#");
00249                 boolean normalized = false;
00250                 writer.write("normalized\t" + normalized + "\n");
00251                 writer.write("points\t" + controlPoints.size() + "\n");
00252                 int x = Integer.MAX_VALUE;
00253                 int y = Integer.MAX_VALUE;
00254                 for (PenSample sample : controlPoints) {
00255                         x = (int) Math.min(x, sample.x);
00256                         y = (int) Math.min(y, sample.y);
00257                 }
00258                 for (int i = 0; i < controlPoints.size(); i++) {
00259                         PenSample sample = controlPoints.get(i);
00260                         writer.write("\t" + df.format(sample.x - x) + "\t" + df.format(sample.y - y) + "\t"
00261                                         + df.format(sample.timestamp) + "\n");
00262                 }
00263                 writer.write("endgesture\n");
00264         }
00265 
00266         // pseudocode
00267         /*
00268          * 
00269          * constructor(points[]) {create shapehistogram for each point}
00270          * 
00271          * double[] logpolar(point1,point2) given input points, gets logpolar offset
00272          * 
00273          * distance(ShapeContext) need to be able to sample a pointset to get the right number of points to match.
00274          * do this via spline interpolation
00275          * 
00276          * 
00277          * bipartite matching
00278          */
00279 
00280         public ArrayList<PenSample> resample(int samples) {
00281                 // special case
00282                 if (samples == controlPoints.size())
00283                         return (ArrayList<PenSample>) controlPoints.clone();
00284                 // want to return something with time information
00285                 ArrayList<PenSample> sampledPoints = new ArrayList<PenSample>();
00286                 assert (controlPoints.size() > 1);
00287                 // sampling in time is a little weird; I'll sample in "space"
00288                 /*
00289                  * double fraction = (controlPoints.size()-1)/(double)(samples); for(int i=0; i < samples; i++) {
00290                  * double position = fraction * i; int truncated = (int)position; double remainder = position -
00291                  * truncated; sampledPoints.add(blendHelper(truncated, remainder)); }
00292                  */
00293                 // resample for even spacing in time
00294                 int points = controlPoints.size();
00295                 for (int i = 1; i < points; i++) {
00296                         while (controlPoints.get(i).timestamp <= controlPoints.get(i - 1).timestamp)
00297                                 controlPoints.get(i).timestamp++;
00298                 }
00299                 double t0 = controlPoints.get(0).timestamp;
00300                 double t1 = controlPoints.get(points - 1).timestamp;
00301                 double fraction = (t1 - t0) / (samples - 1);
00302                 for (int i = 0; i < samples; i++) {
00303                         double t = t0 + fraction * i;
00304                         int truncated = 0;
00305                         while (truncated + 2 < points && controlPoints.get(truncated + 1).timestamp < t)
00306                                 truncated++;
00307                         double start = controlPoints.get(truncated).timestamp;
00308                         double end = controlPoints.get(truncated + 1).timestamp;
00309                         double remainder = (t - start) / (end - start);
00310                         sampledPoints.add(blendHelper(truncated, remainder));
00311                 }
00312                 return sampledPoints;
00313         }
00314 
00315         public int size() {
00316                 return 2 * controlPoints.size();
00317         }
00318 
00319         // all I actually need is x and y, or even angle. but this will do
00320         PenSample tangent(int i, double t) {
00321                 PenSample[] samples = new PenSample[4];
00322                 PenSample blendedSample = new PenSample(0, 0, (int) 0, (long) 0);
00323                 if (i == 0) // double up the first
00324                         samples[0] = controlPoints.get(0);
00325                 else
00326                         samples[0] = controlPoints.get(i - 1);
00327                 samples[1] = controlPoints.get(i);
00328                 samples[2] = controlPoints.get(i + 1);
00329                 if (i + 2 >= controlPoints.size())
00330                         samples[3] = controlPoints.get(controlPoints.size() - 1);
00331                 else
00332                         samples[3] = controlPoints.get(i + 2);
00333                 for (int ii = 0; ii < 4; ii++) {
00334                         double b = dblend_du(ii, t);
00335                         blendedSample.x += samples[ii].x * b;
00336                         blendedSample.y += samples[ii].y * b;
00337                         blendedSample.timestamp += samples[ii].timestamp * b;
00338                 }
00339                 return blendedSample;
00340         }
00341 
00342         public ArrayList<PenSample> tangents(int samples) {
00343                 // want to return something with time information
00344                 ArrayList<PenSample> sampledPoints = new ArrayList<PenSample>();
00345                 assert (controlPoints.size() > 1);
00346                 // sampling in time is a little weird; I'll sample in "space"
00347                 double fraction = (controlPoints.size() - 1) / (double) (samples);
00348                 for (int i = 0; i < samples; i++) {
00349                         double position = fraction * i;
00350                         int truncated = (int) position;
00351                         double remainder = position - truncated;
00352                         sampledPoints.add(tangent(truncated, remainder));
00353                 }
00354                 return sampledPoints;
00355         }
00356 
00357 }

Generated on Sat Apr 14 18:21:38 2007 for R3 Paper Toolkit by  doxygen 1.4.7