PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/Recognizer/GeometricRecognizer.cs

#
C# | 629 lines | 485 code | 71 blank | 73 comment | 46 complexity | dceabd5a9a01083943dee5a00073b581 MD5 | raw file
  1. using System;
  2. using System.Collections;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Reflection;
  6. using System.Text;
  7. using System.Xml;
  8. namespace Recognizer.NIH
  9. {
  10. public class GeometricRecognizer
  11. {
  12. #region Members
  13. public const int NumResamplePoints = 64;
  14. private const double DX = 250.0;
  15. public static readonly SizeR ResampleScale = new SizeR(DX, DX);
  16. public static readonly double Diagonal = Math.Sqrt(DX * DX + DX * DX);
  17. public static readonly double HalfDiagonal = 0.5 * Diagonal;
  18. public static readonly PointR ResampleOrigin = new PointR(0, 0);
  19. private static readonly double Phi = 0.5 * (-1 + Math.Sqrt(5)); // Golden Ratio
  20. // batch testing
  21. private const int NumRandomTests = 100;
  22. public event ProgressEventHandler ProgressChangedEvent;
  23. private Hashtable _gestures;
  24. #endregion
  25. #region Constructor
  26. public GeometricRecognizer()
  27. {
  28. _gestures = new Hashtable(256);
  29. }
  30. #endregion
  31. #region Recognition
  32. public NBestList Recognize(ArrayList points) // candidate points
  33. {
  34. // resample to a common number of points
  35. points = Utils.Resample(points, NumResamplePoints);
  36. // rotate so that the centroid-to-1st-point is at zero degrees
  37. double radians = Utils.AngleInRadians(Utils.Centroid(points), (PointR) points[0], false); // indicative angle
  38. points = Utils.RotateByRadians(points, -radians); // undo angle
  39. // scale to a common (square) dimension
  40. points = Utils.ScaleTo(points, ResampleScale);
  41. // translate to a common origin
  42. points = Utils.TranslateCentroidTo(points, ResampleOrigin);
  43. NBestList nbest = new NBestList();
  44. foreach (Gesture p in _gestures.Values)
  45. {
  46. double[] best = GoldenSectionSearch(
  47. points, // to rotate
  48. p.Points, // to match
  49. Utils.Deg2Rad(-45.0), // lbound
  50. Utils.Deg2Rad(+45.0), // ubound
  51. Utils.Deg2Rad(2.0)); // threshold
  52. double score = 1d - best[0] / HalfDiagonal;
  53. nbest.AddResult(p.Name, score, best[0], best[1]); // name, score, distance, angle
  54. }
  55. nbest.SortDescending(); // sort so that nbest[0] is best result
  56. return nbest;
  57. }
  58. // From http://www.math.uic.edu/~jan/mcs471/Lec9/gss.pdf
  59. private double[] GoldenSectionSearch(ArrayList pts1, ArrayList pts2, double a, double b, double threshold)
  60. {
  61. double x1 = Phi * a + (1 - Phi) * b;
  62. ArrayList newPoints = Utils.RotateByRadians(pts1, x1);
  63. double fx1 = Utils.PathDistance(newPoints, pts2);
  64. double x2 = (1 - Phi) * a + Phi * b;
  65. newPoints = Utils.RotateByRadians(pts1, x2);
  66. double fx2 = Utils.PathDistance(newPoints, pts2);
  67. double i = 2.0; // calls
  68. while (Math.Abs(b - a) > threshold)
  69. {
  70. if (fx1 < fx2)
  71. {
  72. b = x2;
  73. x2 = x1;
  74. fx2 = fx1;
  75. x1 = Phi * a + (1 - Phi) * b;
  76. newPoints = Utils.RotateByRadians(pts1, x1);
  77. fx1 = Utils.PathDistance(newPoints, pts2);
  78. }
  79. else
  80. {
  81. a = x1;
  82. x1 = x2;
  83. fx1 = fx2;
  84. x2 = (1 - Phi) * a + Phi * b;
  85. newPoints = Utils.RotateByRadians(pts1, x2);
  86. fx2 = Utils.PathDistance(newPoints, pts2);
  87. }
  88. i++;
  89. }
  90. return new double[3] { Math.Min(fx1, fx2), Utils.Rad2Deg((b + a) / 2.0), i }; // distance, angle, calls to pathdist
  91. }
  92. // continues to rotate 'pts1' by 'step' degrees as long as points become ever-closer
  93. // in path-distance to pts2. the initial distance is given by D. the best distance
  94. // is returned in array[0], while the angle at which it was achieved is in array[1].
  95. // array[3] contains the number of calls to PathDistance.
  96. private double[] HillClimbSearch(ArrayList pts1, ArrayList pts2, double D, double step)
  97. {
  98. double i = 0.0;
  99. double theta = 0.0;
  100. double d = D;
  101. do
  102. {
  103. D = d; // the last angle tried was better still
  104. theta += step;
  105. ArrayList newPoints = Utils.RotateByDegrees(pts1, theta);
  106. d = Utils.PathDistance(newPoints, pts2);
  107. i++;
  108. }
  109. while (d <= D);
  110. return new double[3] { D, theta - step, i }; // distance, angle, calls to pathdist
  111. }
  112. private double[] FullSearch(ArrayList pts1, ArrayList pts2, StreamWriter writer)
  113. {
  114. double bestA = 0d;
  115. double bestD = Utils.PathDistance(pts1, pts2);
  116. for (int i = -180; i <= +180; i++)
  117. {
  118. ArrayList newPoints = Utils.RotateByDegrees(pts1, i);
  119. double d = Utils.PathDistance(newPoints, pts2);
  120. if (writer != null)
  121. {
  122. writer.WriteLine("{0}\t{1:F3}", i, Math.Round(d, 3));
  123. }
  124. if (d < bestD)
  125. {
  126. bestD = d;
  127. bestA = i;
  128. }
  129. }
  130. writer.WriteLine("\nFull Search (360 rotations)\n{0:F2}{1}\t{2:F3} px", Math.Round(bestA, 2), (char) 176, Math.Round(bestD, 3)); // calls, angle, distance
  131. return new double[3] { bestD, bestA, 360.0 }; // distance, angle, calls to pathdist
  132. }
  133. #endregion
  134. #region Gestures & Xml
  135. public int NumGestures
  136. {
  137. get
  138. {
  139. return _gestures.Count;
  140. }
  141. }
  142. public ArrayList Gestures
  143. {
  144. get
  145. {
  146. ArrayList list = new ArrayList(_gestures.Values);
  147. list.Sort();
  148. return list;
  149. }
  150. }
  151. public void ClearGestures()
  152. {
  153. _gestures.Clear();
  154. }
  155. public bool SaveGesture(string filename, ArrayList points)
  156. {
  157. // add the new prototype with the name extracted from the filename.
  158. string name = Gesture.ParseName(filename);
  159. if (_gestures.ContainsKey(name))
  160. _gestures.Remove(name);
  161. Gesture newPrototype = new Gesture(name, points);
  162. _gestures.Add(name, newPrototype);
  163. // figure out the duration of the gesture
  164. PointR p0 = (PointR) points[0];
  165. PointR pn = (PointR) points[points.Count - 1];
  166. // do the xml writing
  167. bool success = true;
  168. XmlTextWriter writer = null;
  169. try
  170. {
  171. // save the prototype as an Xml file
  172. writer = new XmlTextWriter(filename, Encoding.UTF8);
  173. writer.Formatting = Formatting.Indented;
  174. writer.WriteStartDocument(true);
  175. writer.WriteStartElement("Gesture");
  176. writer.WriteAttributeString("Name", name);
  177. writer.WriteAttributeString("NumPts", XmlConvert.ToString(points.Count));
  178. writer.WriteAttributeString("Millseconds", XmlConvert.ToString(pn.T - p0.T));
  179. writer.WriteAttributeString("AppName", Assembly.GetExecutingAssembly().GetName().Name);
  180. writer.WriteAttributeString("AppVer", Assembly.GetExecutingAssembly().GetName().Version.ToString());
  181. writer.WriteAttributeString("Date", DateTime.Now.ToLongDateString());
  182. writer.WriteAttributeString("TimeOfDay", DateTime.Now.ToLongTimeString());
  183. // write out the raw individual points
  184. foreach (PointR p in points)
  185. {
  186. writer.WriteStartElement("Point");
  187. writer.WriteAttributeString("X", XmlConvert.ToString(p.X));
  188. writer.WriteAttributeString("Y", XmlConvert.ToString(p.Y));
  189. writer.WriteAttributeString("T", XmlConvert.ToString(p.T));
  190. writer.WriteEndElement(); // <Point />
  191. }
  192. writer.WriteEndDocument(); // </Gesture>
  193. }
  194. catch (XmlException xex)
  195. {
  196. Console.Write(xex.Message);
  197. success = false;
  198. }
  199. catch (Exception ex)
  200. {
  201. Console.Write(ex.Message);
  202. success = false;
  203. }
  204. finally
  205. {
  206. if (writer != null)
  207. writer.Close();
  208. }
  209. return success; // Xml file successfully written (or not)
  210. }
  211. public bool LoadGesture(string filename)
  212. {
  213. bool success = true;
  214. XmlTextReader reader = null;
  215. try
  216. {
  217. reader = new XmlTextReader(filename);
  218. reader.WhitespaceHandling = WhitespaceHandling.None;
  219. reader.MoveToContent();
  220. Gesture p = ReadGesture(reader);
  221. // remove any with the same name and add the prototype gesture
  222. if (_gestures.ContainsKey(p.Name))
  223. _gestures.Remove(p.Name);
  224. _gestures.Add(p.Name, p);
  225. }
  226. catch (XmlException xex)
  227. {
  228. Console.Write(xex.Message);
  229. success = false;
  230. }
  231. catch (Exception ex)
  232. {
  233. Console.Write(ex.Message);
  234. success = false;
  235. }
  236. finally
  237. {
  238. if (reader != null)
  239. reader.Close();
  240. }
  241. return success;
  242. }
  243. // assumes the reader has been just moved to the head of the content.
  244. private Gesture ReadGesture(XmlTextReader reader)
  245. {
  246. Debug.Assert(reader.LocalName == "Gesture");
  247. string name = reader.GetAttribute("Name");
  248. ArrayList points = new ArrayList(XmlConvert.ToInt32(reader.GetAttribute("NumPts")));
  249. reader.Read(); // advance to the first Point
  250. Debug.Assert(reader.LocalName == "Point");
  251. while (reader.NodeType != XmlNodeType.EndElement)
  252. {
  253. PointR p = PointR.Empty;
  254. p.X = XmlConvert.ToDouble(reader.GetAttribute("X"));
  255. p.Y = XmlConvert.ToDouble(reader.GetAttribute("Y"));
  256. p.T = XmlConvert.ToInt32(reader.GetAttribute("T"));
  257. points.Add(p);
  258. reader.ReadStartElement("Point");
  259. }
  260. return new Gesture(name, points);
  261. }
  262. #endregion
  263. #region Batch Processing
  264. /// <summary>
  265. /// Assemble the gesture filenames into categories that contain
  266. /// potentially multiple examples of the same gesture.
  267. /// </summary>
  268. /// <param name="filenames"></param>
  269. /// <returns>A 1D arraylist of category instances that each
  270. /// contain the same number of examples, or <b>null</b> if an
  271. /// error occurs.</returns>
  272. /// <remarks>
  273. /// See the comments above MainForm.BatchProcess_Click.
  274. /// </remarks>
  275. public ArrayList AssembleBatch(string[] filenames)
  276. {
  277. Hashtable categories = new Hashtable();
  278. for (int i = 0; i < filenames.Length; i++)
  279. {
  280. string filename = filenames[i];
  281. XmlTextReader reader = null;
  282. try
  283. {
  284. reader = new XmlTextReader(filename);
  285. reader.WhitespaceHandling = WhitespaceHandling.None;
  286. reader.MoveToContent();
  287. Gesture p = ReadGesture(reader);
  288. string catName = Category.ParseName(p.Name);
  289. if (categories.ContainsKey(catName))
  290. {
  291. Category cat = (Category) categories[catName];
  292. cat.AddExample(p); // if the category has been made before, just add to it
  293. }
  294. else // create new category
  295. {
  296. categories.Add(catName, new Category(catName, p));
  297. }
  298. }
  299. catch (XmlException xex)
  300. {
  301. Console.Write(xex.Message);
  302. categories.Clear();
  303. categories = null;
  304. }
  305. catch (Exception ex)
  306. {
  307. Console.Write(ex.Message);
  308. categories.Clear();
  309. categories = null;
  310. }
  311. finally
  312. {
  313. if (reader != null)
  314. reader.Close();
  315. }
  316. }
  317. // now make sure that each category has the same number of elements in it
  318. ArrayList list = null;
  319. if (categories != null)
  320. {
  321. list = new ArrayList(categories.Values);
  322. int numExamples = ((Category) list[0]).NumExamples;
  323. foreach (Category c in list)
  324. {
  325. if (c.NumExamples != numExamples)
  326. {
  327. Console.WriteLine("Different number of examples in gesture categories.");
  328. list.Clear();
  329. list = null;
  330. break;
  331. }
  332. }
  333. }
  334. return list;
  335. }
  336. /// <summary>
  337. /// Tests an entire batch of files. See comments atop MainForm.TestBatch_Click().
  338. /// </summary>
  339. /// <param name="subject">Subject number.</param>
  340. /// <param name="speed">"fast", "medium", or "slow"</param>
  341. /// <param name="categories">A list of gesture categories that each contain lists of
  342. /// prototypes (examples) within that gesture category.</param>
  343. /// <param name="dir">The directory into which to write the output files.</param>
  344. /// <returns>True if successful; false otherwise.</returns>
  345. public bool TestBatch(int subject, string speed, ArrayList categories, string dir)
  346. {
  347. bool success = true;
  348. StreamWriter mainWriter = null;
  349. StreamWriter recWriter = null;
  350. try
  351. {
  352. //
  353. // set up a main results file and detailed recognition results file
  354. //
  355. int start = Environment.TickCount;
  356. string mainFile = String.Format("{0}\\geometric_main_{1}.txt", dir, start);
  357. string recFile = String.Format("{0}\\geometric_data_{1}.txt", dir, start);
  358. mainWriter = new StreamWriter(mainFile, false, Encoding.UTF8);
  359. mainWriter.WriteLine("Subject = {0}, TrainerProgram = geometric, Speed = {1}, StartTime(ms) = {2}", subject, speed, start);
  360. mainWriter.WriteLine("Subject TrainerProgram Speed NumTraining GestureType RecognitionRate\n");
  361. recWriter = new StreamWriter(recFile, false, Encoding.UTF8);
  362. recWriter.WriteLine("Subject = {0}, TrainerProgram = geometric, Speed = {1}, StartTime(ms) = {2}", subject, speed, start);
  363. recWriter.WriteLine("Correct? NumTrain Tested 1stCorrect Pts Ms Angle : (NBestNames) [NBestScores]\n");
  364. //
  365. // determine the number of gesture categories and the number of examples in each one
  366. //
  367. int numCategories = categories.Count;
  368. int numExamples = ((Category) categories[0]).NumExamples;
  369. double totalTests = (numExamples - 1) * NumRandomTests;
  370. //
  371. // outermost loop: trains on N=1..9, tests on 10-N (for e.g., numExamples = 10)
  372. //
  373. for (int n = 1; n <= numExamples - 1; n++)
  374. {
  375. // storage for the final avg results for each category for this N
  376. double[] results = new double[numCategories];
  377. //
  378. // run a number of tests at this particular N number of training examples
  379. //
  380. for (int r = 0; r < NumRandomTests; r++)
  381. {
  382. _gestures.Clear(); // clear any (old) loaded prototypes
  383. // load (train on) N randomly selected gestures in each category
  384. for (int i = 0; i < numCategories; i++)
  385. {
  386. Category c = (Category) categories[i]; // the category to load N examples for
  387. int[] chosen = Utils.Random(0, numExamples - 1, n); // select N unique indices
  388. for (int j = 0; j < chosen.Length; j++)
  389. {
  390. Gesture p = c[chosen[j]]; // get the prototype from this category at chosen[j]
  391. _gestures.Add(p.Name, p); // load the randomly selected test gestures into the recognizer
  392. }
  393. }
  394. //
  395. // testing loop on all unloaded gestures in each category. creates a recognition
  396. // rate (%) by averaging the binary outcomes (correct, incorrect) for each test.
  397. //
  398. for (int i = 0; i < numCategories; i++)
  399. {
  400. // pick a random unloaded gesture in this category for testing
  401. // instead of dumbly picking, first find out what indices aren't
  402. // loaded, and then randomly pick from those.
  403. Category c = (Category) categories[i];
  404. int[] notLoaded = new int[numExamples - n];
  405. for (int j = 0, k = 0; j < numExamples; j++)
  406. {
  407. Gesture g = c[j];
  408. if (!_gestures.ContainsKey(g.Name))
  409. notLoaded[k++] = j; // jth gesture in c is not loaded
  410. }
  411. int chosen = Utils.Random(0, notLoaded.Length - 1); // index
  412. Gesture p = c[notLoaded[chosen]]; // gesture to test
  413. Debug.Assert(!_gestures.ContainsKey(p.Name));
  414. // do the recognition!
  415. ArrayList testPts = Utils.RotateByDegrees(p.RawPoints, Utils.Random(0, 359));
  416. NBestList result = this.Recognize(testPts);
  417. string category = Category.ParseName(result.Name);
  418. int correct = (c.Name == category) ? 1 : 0;
  419. recWriter.WriteLine("{0} {1} {2} {3} {4} {5} {6:F1}{7} : ({8}) [{9}]",
  420. correct, // Correct?
  421. n, // NumTrain
  422. p.Name, // Tested
  423. FirstCorrect(p.Name, result.Names), // 1stCorrect
  424. p.RawPoints.Count, // Pts
  425. p.Duration, // Ms
  426. Math.Round(result.Angle, 1), (char) 176, // Angle tweaking :
  427. result.NamesString, // (NBestNames)
  428. result.ScoresString); // [NBestScores]
  429. results[i] += correct;
  430. }
  431. // provide feedback as to how many tests have been performed thus far.
  432. double testsSoFar = ((n - 1) * NumRandomTests) + r;
  433. ProgressChangedEvent(this, new ProgressEventArgs(testsSoFar / totalTests)); // callback
  434. }
  435. //
  436. // now create the final results for this N and write them to a file
  437. //
  438. for (int i = 0; i < numCategories; i++)
  439. {
  440. results[i] /= (double) NumRandomTests; // normalize by the number of tests at this N
  441. Category c = (Category) categories[i];
  442. // Subject TrainerProgram Speed NumTraining GestureType RecognitionRate
  443. mainWriter.WriteLine("{0} geometric {1} {2} {3} {4:F3}", subject, speed, n, c.Name, Math.Round(results[i], 3));
  444. }
  445. }
  446. // time-stamp the end of the processing
  447. int end = Environment.TickCount;
  448. mainWriter.WriteLine("\nEndTime(ms) = {0}, Minutes = {1:F2}", end, Math.Round((end - start) / 60000.0, 2));
  449. recWriter.WriteLine("\nEndTime(ms) = {0}, Minutes = {1:F2}", end, Math.Round((end - start) / 60000.0, 2));
  450. }
  451. catch (Exception ex)
  452. {
  453. Console.WriteLine(ex.Message);
  454. success = false;
  455. }
  456. finally
  457. {
  458. if (mainWriter != null)
  459. mainWriter.Close();
  460. if (recWriter != null)
  461. recWriter.Close();
  462. }
  463. return success;
  464. }
  465. private int FirstCorrect(string name, string[] names)
  466. {
  467. string category = Category.ParseName(name);
  468. for (int i = 0; i < names.Length; i++)
  469. {
  470. string c = Category.ParseName(names[i]);
  471. if (category == c)
  472. {
  473. return i + 1;
  474. }
  475. }
  476. return -1;
  477. }
  478. #endregion
  479. #region Rotation Graph
  480. public bool CreateRotationGraph(string file1, string file2, string dir, bool similar)
  481. {
  482. bool success = true;
  483. StreamWriter writer = null;
  484. XmlTextReader reader = null;
  485. try
  486. {
  487. // read gesture file #1
  488. reader = new XmlTextReader(file1);
  489. reader.WhitespaceHandling = WhitespaceHandling.None;
  490. reader.MoveToContent();
  491. Gesture g1 = ReadGesture(reader);
  492. reader.Close();
  493. // read gesture file #2
  494. reader = new XmlTextReader(file2);
  495. reader.WhitespaceHandling = WhitespaceHandling.None;
  496. reader.MoveToContent();
  497. Gesture g2 = ReadGesture(reader);
  498. // create output file for results
  499. string outfile = String.Format("{0}\\{1}({2}, {3})_{4}.txt", dir, similar ? "o" : "x", g1.Name, g2.Name, Environment.TickCount);
  500. writer = new StreamWriter(outfile, false, Encoding.UTF8);
  501. writer.WriteLine("Rotated: {0} --> {1}. {2}, {3}\n", g1.Name, g2.Name, DateTime.Now.ToLongDateString(), DateTime.Now.ToLongTimeString());
  502. // do the full 360 degree rotations
  503. double[] full = FullSearch(g1.Points, g2.Points, writer);
  504. // use bidirectional hill climbing to do it again
  505. double init = Utils.PathDistance(g1.Points, g2.Points); // initial distance
  506. double[] pos = HillClimbSearch(g1.Points, g2.Points, init, 1d);
  507. double[] neg = HillClimbSearch(g1.Points, g2.Points, init, -1d);
  508. double[] best = new double[3];
  509. best = (neg[0] < pos[0]) ? neg : pos; // min distance
  510. writer.WriteLine("\nHill Climb Search ({0} rotations)\n{1:F2}{2}\t{3:F3} px", pos[2] + neg[2] + 1, Math.Round(best[1], 2), (char) 176, Math.Round(best[0], 3)); // calls, angle, distance
  511. // use golden section search to do it yet again
  512. double[] gold = GoldenSectionSearch(
  513. g1.Points, // to rotate
  514. g2.Points, // to match
  515. Utils.Deg2Rad(-45.0), // lbound
  516. Utils.Deg2Rad(+45.0), // ubound
  517. Utils.Deg2Rad(2.0)); // threshold
  518. writer.WriteLine("\nGolden Section Search ({0} rotations)\n{1:F2}{2}\t{3:F3} px", gold[2], Math.Round(gold[1], 2), (char) 176, Math.Round(gold[0], 3)); // calls, angle, distance
  519. // for pasting into Excel
  520. writer.WriteLine("\n{0} {1} {2:F2} {3:F2} {4:F3} {5:F3} {6} {7:F2} {8:F2} {9:F3} {10} {11:F2} {12:F2} {13:F3} {14}",
  521. g1.Name, // rotated
  522. g2.Name, // into
  523. Math.Abs(Math.Round(full[1], 2)), // |angle|
  524. Math.Round(full[1], 2), // Full Search angle
  525. Math.Round(full[0], 3), // Full Search distance
  526. Math.Round(init, 3), // Initial distance w/o any search
  527. full[2], // Full Search iterations
  528. Math.Abs(Math.Round(best[1], 2)), // |angle|
  529. Math.Round(best[1], 2), // Bidirectional Hill Climb Search angle
  530. Math.Round(best[0], 3), // Bidirectional Hill Climb Search distance
  531. pos[2] + neg[2] + 1, // Bidirectional Hill Climb Search iterations
  532. Math.Abs(Math.Round(gold[1], 2)), // |angle|
  533. Math.Round(gold[1], 2), // Golden Section Search angle
  534. Math.Round(gold[0], 3), // Golden Section Search distance
  535. gold[2]); // Golden Section Search iterations
  536. }
  537. catch (XmlException xml)
  538. {
  539. Console.Write(xml.Message);
  540. success = false;
  541. }
  542. catch (Exception ex)
  543. {
  544. Console.Write(ex.Message);
  545. success = false;
  546. }
  547. finally
  548. {
  549. if (reader != null)
  550. reader.Close();
  551. if (writer != null)
  552. writer.Close();
  553. }
  554. return success;
  555. }
  556. #endregion
  557. }
  558. }