trajectoryClassifier.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. import { Matrix, Vector3 } from "../Maths/math.vector.js";
  2. // This implementation was based on the original MIT-licensed TRACE repository
  3. // from https://github.com/septagon/TRACE.
  4. /**
  5. * Generic implementation of Levenshtein distance.
  6. */
  7. var Levenshtein;
  8. (function (Levenshtein) {
  9. /**
  10. * Alphabet from which to construct sequences to be compared using Levenshtein
  11. * distance.
  12. */
  13. class Alphabet {
  14. /**
  15. * Serialize the Alphabet to JSON string.
  16. * @returns JSON serialization
  17. */
  18. serialize() {
  19. const jsonObject = {};
  20. const characters = new Array(this._characterToIdx.size);
  21. this._characterToIdx.forEach((v, k) => {
  22. characters[v] = k;
  23. });
  24. jsonObject["characters"] = characters;
  25. jsonObject["insertionCosts"] = this._insertionCosts;
  26. jsonObject["deletionCosts"] = this._deletionCosts;
  27. jsonObject["substitutionCosts"] = this._substitutionCosts;
  28. return JSON.stringify(jsonObject);
  29. }
  30. /**
  31. * Parse an Alphabet from a JSON serialization.
  32. * @param json JSON string to deserialize
  33. * @returns deserialized Alphabet
  34. */
  35. static Deserialize(json) {
  36. const jsonObject = JSON.parse(json);
  37. const alphabet = new Alphabet(jsonObject["characters"]);
  38. alphabet._insertionCosts = jsonObject["insertionCosts"];
  39. alphabet._deletionCosts = jsonObject["deletionCosts"];
  40. alphabet._substitutionCosts = jsonObject["substitutionCosts"];
  41. return alphabet;
  42. }
  43. /**
  44. * Create a new Alphabet.
  45. * @param characters characters of the alphabet
  46. * @param charToInsertionCost function mapping characters to insertion costs
  47. * @param charToDeletionCost function mapping characters to deletion costs
  48. * @param charsToSubstitutionCost function mapping character pairs to substitution costs
  49. */
  50. constructor(characters, charToInsertionCost = null, charToDeletionCost = null, charsToSubstitutionCost = null) {
  51. charToInsertionCost = charToInsertionCost ?? (() => 1);
  52. charToDeletionCost = charToDeletionCost ?? (() => 1);
  53. charsToSubstitutionCost = charsToSubstitutionCost ?? ((a, b) => (a === b ? 0 : 1));
  54. this._characterToIdx = new Map();
  55. this._insertionCosts = new Array(characters.length);
  56. this._deletionCosts = new Array(characters.length);
  57. this._substitutionCosts = new Array(characters.length);
  58. let c;
  59. for (let outerIdx = 0; outerIdx < characters.length; ++outerIdx) {
  60. c = characters[outerIdx];
  61. this._characterToIdx.set(c, outerIdx);
  62. this._insertionCosts[outerIdx] = charToInsertionCost(c);
  63. this._deletionCosts[outerIdx] = charToDeletionCost(c);
  64. this._substitutionCosts[outerIdx] = new Array(characters.length);
  65. for (let innerIdx = outerIdx; innerIdx < characters.length; ++innerIdx) {
  66. this._substitutionCosts[outerIdx][innerIdx] = charsToSubstitutionCost(c, characters[innerIdx]);
  67. }
  68. }
  69. }
  70. /**
  71. * Get the index (internally-assigned number) for a character.
  72. * @param char character
  73. * @returns index
  74. */
  75. getCharacterIdx(char) {
  76. return this._characterToIdx.get(char);
  77. }
  78. /**
  79. * Get the insertion cost of a character from its index.
  80. * @param idx character index
  81. * @returns insertion cost
  82. */
  83. getInsertionCost(idx) {
  84. return this._insertionCosts[idx];
  85. }
  86. /**
  87. * Get the deletion cost of a character from its index.
  88. * @param idx character index
  89. * @returns deletion cost
  90. */
  91. getDeletionCost(idx) {
  92. return this._deletionCosts[idx];
  93. }
  94. /**
  95. * Gets the cost to substitute two characters. NOTE: this cost is
  96. * required to be bi-directional, meaning it cannot matter which of
  97. * the provided characters is being removed and which is being inserted.
  98. * @param idx1 the first character index
  99. * @param idx2 the second character index
  100. * @returns substitution cost
  101. */
  102. getSubstitutionCost(idx1, idx2) {
  103. const min = Math.min(idx1, idx2);
  104. const max = Math.max(idx1, idx2);
  105. return this._substitutionCosts[min][max];
  106. }
  107. }
  108. Levenshtein.Alphabet = Alphabet;
  109. /**
  110. * Character sequence intended to be compared against other Sequences created
  111. * with the same Alphabet in order to compute Levenshtein distance.
  112. */
  113. class Sequence {
  114. /**
  115. * Serialize to JSON string. JSON representation does NOT include the Alphabet
  116. * from which this Sequence was created; Alphabet must be independently
  117. * serialized.
  118. * @returns JSON string
  119. */
  120. serialize() {
  121. return JSON.stringify(this._characters);
  122. }
  123. /**
  124. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  125. * from which the Sequence was originally created, which must be serialized and
  126. * deserialized independently so that it can be passed in here.
  127. * @param json JSON string representation of Sequence
  128. * @param alphabet Alphabet from which Sequence was originally created
  129. * @returns Sequence
  130. */
  131. static Deserialize(json, alphabet) {
  132. const sequence = new Sequence([], alphabet);
  133. sequence._characters = JSON.parse(json);
  134. return sequence;
  135. }
  136. /**
  137. * Create a new Sequence.
  138. * @param characters characters in the new Sequence
  139. * @param alphabet Alphabet, which must include all used characters
  140. */
  141. constructor(characters, alphabet) {
  142. if (characters.length > Sequence._MAX_SEQUENCE_LENGTH) {
  143. throw new Error("Sequences longer than " + Sequence._MAX_SEQUENCE_LENGTH + " not supported.");
  144. }
  145. this._alphabet = alphabet;
  146. this._characters = characters.map((c) => this._alphabet.getCharacterIdx(c));
  147. }
  148. /**
  149. * Get the distance between this Sequence and another.
  150. * @param other sequence to compare to
  151. * @returns Levenshtein distance
  152. */
  153. distance(other) {
  154. return Sequence._Distance(this, other);
  155. }
  156. /**
  157. * Compute the Levenshtein distance between two Sequences.
  158. * @param a first Sequence
  159. * @param b second Sequence
  160. * @returns Levenshtein distance
  161. */
  162. static _Distance(a, b) {
  163. const alphabet = a._alphabet;
  164. if (alphabet !== b._alphabet) {
  165. throw new Error("Cannot Levenshtein compare Sequences built from different alphabets.");
  166. }
  167. const aChars = a._characters;
  168. const bChars = b._characters;
  169. const aLength = aChars.length;
  170. const bLength = bChars.length;
  171. const costMatrix = Sequence._CostMatrix;
  172. costMatrix[0][0] = 0;
  173. for (let idx = 0; idx < aLength; ++idx) {
  174. costMatrix[idx + 1][0] = costMatrix[idx][0] + alphabet.getInsertionCost(aChars[idx]);
  175. }
  176. for (let idx = 0; idx < bLength; ++idx) {
  177. costMatrix[0][idx + 1] = costMatrix[0][idx] + alphabet.getInsertionCost(bChars[idx]);
  178. }
  179. for (let aIdx = 0; aIdx < aLength; ++aIdx) {
  180. for (let bIdx = 0; bIdx < bLength; ++bIdx) {
  181. Sequence._InsertionCost = costMatrix[aIdx + 1][bIdx] + alphabet.getInsertionCost(bChars[bIdx]);
  182. Sequence._DeletionCost = costMatrix[aIdx][bIdx + 1] + alphabet.getDeletionCost(aChars[aIdx]);
  183. Sequence._SubstitutionCost = costMatrix[aIdx][bIdx] + alphabet.getSubstitutionCost(aChars[aIdx], bChars[bIdx]);
  184. costMatrix[aIdx + 1][bIdx + 1] = Math.min(Sequence._InsertionCost, Sequence._DeletionCost, Sequence._SubstitutionCost);
  185. }
  186. }
  187. return costMatrix[aLength][bLength];
  188. }
  189. }
  190. // Scratch values
  191. Sequence._MAX_SEQUENCE_LENGTH = 256;
  192. Sequence._CostMatrix = [...Array(Sequence._MAX_SEQUENCE_LENGTH + 1)].map(() => new Array(Sequence._MAX_SEQUENCE_LENGTH + 1));
  193. Levenshtein.Sequence = Sequence;
  194. })(Levenshtein || (Levenshtein = {}));
  195. /**
  196. * A 3D trajectory consisting of an order list of vectors describing a
  197. * path of motion through 3D space.
  198. */
  199. export class Trajectory {
  200. /**
  201. * Serialize to JSON.
  202. * @returns serialized JSON string
  203. */
  204. serialize() {
  205. return JSON.stringify(this);
  206. }
  207. /**
  208. * Deserialize from JSON.
  209. * @param json serialized JSON string
  210. * @returns deserialized Trajectory
  211. */
  212. static Deserialize(json) {
  213. const jsonObject = JSON.parse(json);
  214. const trajectory = new Trajectory(jsonObject["_segmentLength"]);
  215. trajectory._points = jsonObject["_points"].map((pt) => {
  216. return new Vector3(pt["_x"], pt["_y"], pt["_z"]);
  217. });
  218. return trajectory;
  219. }
  220. /**
  221. * Create a new empty Trajectory.
  222. * @param segmentLength radius of discretization for Trajectory points
  223. */
  224. constructor(segmentLength = 0.01) {
  225. this._points = [];
  226. this._segmentLength = segmentLength;
  227. }
  228. /**
  229. * Get the length of the Trajectory.
  230. * @returns length of the Trajectory
  231. */
  232. getLength() {
  233. return this._points.length * this._segmentLength;
  234. }
  235. /**
  236. * Append a new point to the Trajectory.
  237. * NOTE: This implementation has many allocations.
  238. * @param point point to append to the Trajectory
  239. */
  240. add(point) {
  241. let numPoints = this._points.length;
  242. if (numPoints === 0) {
  243. this._points.push(point.clone());
  244. }
  245. else {
  246. const getT = () => this._segmentLength / Vector3.Distance(this._points[numPoints - 1], point);
  247. for (let t = getT(); t <= 1.0; t = getT()) {
  248. const newPoint = this._points[numPoints - 1].scale(1.0 - t);
  249. point.scaleAndAddToRef(t, newPoint);
  250. this._points.push(newPoint);
  251. ++numPoints;
  252. }
  253. }
  254. }
  255. /**
  256. * Create a new Trajectory with a segment length chosen to make it
  257. * probable that the new Trajectory will have a specified number of
  258. * segments. This operation is imprecise.
  259. * @param targetResolution number of segments desired
  260. * @returns new Trajectory with approximately the requested number of segments
  261. */
  262. resampleAtTargetResolution(targetResolution) {
  263. const resampled = new Trajectory(this.getLength() / targetResolution);
  264. this._points.forEach((pt) => {
  265. resampled.add(pt);
  266. });
  267. return resampled;
  268. }
  269. /**
  270. * Convert Trajectory segments into tokenized representation. This
  271. * representation is an array of numbers where each nth number is the
  272. * index of the token which is most similar to the nth segment of the
  273. * Trajectory.
  274. * @param tokens list of vectors which serve as discrete tokens
  275. * @returns list of indices of most similar token per segment
  276. */
  277. tokenize(tokens) {
  278. const tokenization = [];
  279. const segmentDir = new Vector3();
  280. for (let idx = 2; idx < this._points.length; ++idx) {
  281. if (Trajectory._TransformSegmentDirToRef(this._points[idx - 2], this._points[idx - 1], this._points[idx], segmentDir)) {
  282. tokenization.push(Trajectory._TokenizeSegment(segmentDir, tokens));
  283. }
  284. }
  285. return tokenization;
  286. }
  287. /**
  288. * Transform the rotation (i.e., direction) of a segment to isolate
  289. * the relative transformation represented by the segment. This operation
  290. * may or may not succeed due to singularities in the equations that define
  291. * motion relativity in this context.
  292. * @param priorVec the origin of the prior segment
  293. * @param fromVec the origin of the current segment
  294. * @param toVec the destination of the current segment
  295. * @param result reference to output variable
  296. * @returns whether or not transformation was successful
  297. */
  298. static _TransformSegmentDirToRef(priorVec, fromVec, toVec, result) {
  299. const DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD = 0.98;
  300. fromVec.subtractToRef(priorVec, Trajectory._ForwardDir);
  301. Trajectory._ForwardDir.normalize();
  302. fromVec.scaleToRef(-1, Trajectory._InverseFromVec);
  303. Trajectory._InverseFromVec.normalize();
  304. if (Math.abs(Vector3.Dot(Trajectory._ForwardDir, Trajectory._InverseFromVec)) > DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD) {
  305. return false;
  306. }
  307. Vector3.CrossToRef(Trajectory._ForwardDir, Trajectory._InverseFromVec, Trajectory._UpDir);
  308. Trajectory._UpDir.normalize();
  309. Matrix.LookAtLHToRef(priorVec, fromVec, Trajectory._UpDir, Trajectory._LookMatrix);
  310. toVec.subtractToRef(fromVec, Trajectory._FromToVec);
  311. Trajectory._FromToVec.normalize();
  312. Vector3.TransformNormalToRef(Trajectory._FromToVec, Trajectory._LookMatrix, result);
  313. return true;
  314. }
  315. /**
  316. * Determine which token vector is most similar to the
  317. * segment vector.
  318. * @param segment segment vector
  319. * @param tokens token vector list
  320. * @returns index of the most similar token to the segment
  321. */
  322. static _TokenizeSegment(segment, tokens) {
  323. Trajectory._BestMatch = 0;
  324. Trajectory._Score = Vector3.Dot(segment, tokens[0]);
  325. Trajectory._BestScore = Trajectory._Score;
  326. for (let idx = 1; idx < tokens.length; ++idx) {
  327. Trajectory._Score = Vector3.Dot(segment, tokens[idx]);
  328. if (Trajectory._Score > Trajectory._BestScore) {
  329. Trajectory._BestMatch = idx;
  330. Trajectory._BestScore = Trajectory._Score;
  331. }
  332. }
  333. return Trajectory._BestMatch;
  334. }
  335. }
  336. Trajectory._ForwardDir = new Vector3();
  337. Trajectory._InverseFromVec = new Vector3();
  338. Trajectory._UpDir = new Vector3();
  339. Trajectory._FromToVec = new Vector3();
  340. Trajectory._LookMatrix = new Matrix();
  341. /**
  342. * Collection of vectors intended to be used as the basis of Trajectory
  343. * tokenization for Levenshtein distance comparison. Canonically, a
  344. * Vector3Alphabet will resemble a "spikeball" of vectors distributed
  345. * roughly evenly over the surface of the unit sphere.
  346. */
  347. class Vector3Alphabet {
  348. /**
  349. * Helper method to create new "spikeball" Vector3Alphabets. Uses a naive
  350. * optimize-from-random strategy to space points around the unit sphere
  351. * surface as a simple alternative to really doing the math to tile the
  352. * sphere.
  353. * @param alphabetSize size of the desired alphabet
  354. * @param iterations number of iterations over which to optimize the "spikeball"
  355. * @param startingStepSize distance factor to move points in early optimization iterations
  356. * @param endingStepSize distance factor to move points in late optimization iterations
  357. * @param fixedValues alphabet "characters" that are required and cannot be moved by optimization
  358. * @returns a new randomly generated and optimized Vector3Alphabet of the specified size
  359. */
  360. static Generate(alphabetSize = 64, iterations = 256, startingStepSize = 0.1, endingStepSize = 0.001, fixedValues = []) {
  361. const EPSILON = 0.001;
  362. const EPSILON_SQUARED = EPSILON * EPSILON;
  363. const alphabet = new Vector3Alphabet(alphabetSize);
  364. for (let idx = 0; idx < alphabetSize; ++idx) {
  365. alphabet.chars[idx] = new Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
  366. alphabet.chars[idx].normalize();
  367. }
  368. for (let idx = 0; idx < fixedValues.length; ++idx) {
  369. alphabet.chars[idx].copyFrom(fixedValues[idx]);
  370. }
  371. let stepSize;
  372. let distSq;
  373. const force = new Vector3();
  374. const scratch = new Vector3();
  375. const lerp = (l, r, t) => (1.0 - t) * l + t * r;
  376. for (let iteration = 0; iteration < iterations; ++iteration) {
  377. stepSize = lerp(startingStepSize, endingStepSize, iteration / (iterations - 1));
  378. for (let idx = fixedValues.length; idx < alphabet.chars.length; ++idx) {
  379. force.copyFromFloats(0, 0, 0);
  380. alphabet.chars.forEach((pt) => {
  381. alphabet.chars[idx].subtractToRef(pt, scratch);
  382. distSq = scratch.lengthSquared();
  383. if (distSq > EPSILON_SQUARED) {
  384. scratch.scaleAndAddToRef(1 / (scratch.lengthSquared() * distSq), force);
  385. }
  386. });
  387. force.scaleInPlace(stepSize);
  388. alphabet.chars[idx].addInPlace(force);
  389. alphabet.chars[idx].normalize();
  390. }
  391. }
  392. return alphabet;
  393. }
  394. /**
  395. * Serialize to JSON.
  396. * @returns JSON serialization
  397. */
  398. serialize() {
  399. return JSON.stringify(this.chars);
  400. }
  401. /**
  402. * Deserialize from JSON.
  403. * @param json JSON serialization
  404. * @returns deserialized Vector3Alphabet
  405. */
  406. static Deserialize(json) {
  407. const jsonObject = JSON.parse(json);
  408. const alphabet = new Vector3Alphabet(jsonObject.length);
  409. for (let idx = 0; idx < jsonObject.length; ++idx) {
  410. alphabet.chars[idx] = new Vector3(jsonObject[idx]["_x"], jsonObject[idx]["_y"], jsonObject[idx]["_z"]);
  411. }
  412. return alphabet;
  413. }
  414. constructor(size) {
  415. this.chars = new Array(size);
  416. }
  417. }
  418. /**
  419. * Class which formalizes the manner in which a Vector3Alphabet is used to tokenize and
  420. * describe a Trajectory. This class houses the functionality which determines what
  421. * attributes of Trajectories are and are not considered important, such as scale.
  422. */
  423. class TrajectoryDescriptor {
  424. /**
  425. * Serialize to JSON.
  426. * @returns JSON serialization
  427. */
  428. serialize() {
  429. return JSON.stringify(this._sequences.map((sequence) => sequence.serialize()));
  430. }
  431. /**
  432. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  433. * from which the descriptor was originally created, which must be serialized and
  434. * deserialized independently so that it can be passed in here.
  435. * @param json JSON serialization
  436. * @param alphabet Alphabet from which descriptor was originally created
  437. * @returns deserialized TrajectoryDescriptor
  438. */
  439. static Deserialize(json, alphabet) {
  440. const descriptor = new TrajectoryDescriptor();
  441. descriptor._sequences = JSON.parse(json).map((s) => Levenshtein.Sequence.Deserialize(s, alphabet));
  442. return descriptor;
  443. }
  444. /**
  445. * Create a new TrajectoryDescriptor to describe a provided Trajectory according
  446. * to the provided alphabets.
  447. * @param trajectory Trajectory to be described
  448. * @param vector3Alphabet Vector3Alphabet to be used to tokenize the Trajectory
  449. * @param levenshteinAlphabet Levenshtein.Alphabet to be used as basis for comparison with other descriptors
  450. * @returns TrajectoryDescriptor describing provided Trajectory
  451. */
  452. static CreateFromTrajectory(trajectory, vector3Alphabet, levenshteinAlphabet) {
  453. return TrajectoryDescriptor.CreateFromTokenizationPyramid(TrajectoryDescriptor._GetTokenizationPyramid(trajectory, vector3Alphabet), levenshteinAlphabet);
  454. }
  455. /**
  456. * Create a new TrajectoryDescriptor from a pre-existing pyramid of tokens.
  457. * NOTE: This function exists to support an outdated serialization mechanism and should
  458. * be deleted if it is no longer useful.
  459. * @param pyramid tokenization pyramid
  460. * @param levenshteinAlphabet Levenshtein.Alphabet to be uses as basis for comparison with other descriptors
  461. * @returns TrajectoryDescriptor describing the Trajectory from which the pyramid was built
  462. */
  463. static CreateFromTokenizationPyramid(pyramid, levenshteinAlphabet) {
  464. const descriptor = new TrajectoryDescriptor();
  465. descriptor._sequences = pyramid.map((tokens) => new Levenshtein.Sequence(tokens, levenshteinAlphabet));
  466. return descriptor;
  467. }
  468. constructor() {
  469. this._sequences = [];
  470. }
  471. /**
  472. * Create the tokenization pyramid for the provided Trajectory according to the given
  473. * Vector3Alphabet.
  474. * @param trajectory Trajectory to be tokenized
  475. * @param alphabet Vector3Alphabet containing tokens
  476. * @param targetResolution finest resolution of descriptor
  477. * @returns tokenization pyramid for Trajectory
  478. */
  479. static _GetTokenizationPyramid(trajectory, alphabet, targetResolution = TrajectoryDescriptor._FINEST_DESCRIPTOR_RESOLUTION) {
  480. const pyramid = [];
  481. for (let res = targetResolution; res > 4; res = Math.floor(res / 2)) {
  482. pyramid.push(trajectory.resampleAtTargetResolution(res).tokenize(alphabet.chars));
  483. }
  484. return pyramid;
  485. }
  486. /**
  487. * Calculate a distance metric between this TrajectoryDescriptor and another. This is
  488. * essentially a similarity score and does not directly represent Euclidean distance,
  489. * edit distance, or any other formal distance metric.
  490. * @param other TrajectoryDescriptor from which to determine distance
  491. * @returns distance, a nonnegative similarity score where larger values indicate dissimilarity
  492. */
  493. distance(other) {
  494. let totalDistance = 0;
  495. let weight;
  496. for (let idx = 0; idx < this._sequences.length; ++idx) {
  497. weight = Math.pow(2, idx);
  498. totalDistance += weight * this._sequences[idx].distance(other._sequences[idx]);
  499. }
  500. return totalDistance;
  501. }
  502. }
  503. TrajectoryDescriptor._FINEST_DESCRIPTOR_RESOLUTION = 32;
  504. /**
  505. * A set of TrajectoryDescriptors defined to be "the same." This is essentially a helper
  506. * class to facilitate methods of Trajectory clustering.
  507. */
  508. class TrajectoryClass {
  509. /**
  510. * Serialize to JSON.
  511. * @returns JSON serialization
  512. */
  513. serialize() {
  514. const jsonObject = {};
  515. jsonObject.descriptors = this._descriptors.map((desc) => desc.serialize());
  516. jsonObject.centroidIdx = this._centroidIdx;
  517. jsonObject.averageDistance = this._averageDistance;
  518. return JSON.stringify(jsonObject);
  519. }
  520. /**
  521. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  522. * from which the descriptors were originally created, which must be serialized and
  523. * deserialized independently so that it can be passed in here.
  524. * @param json JSON string representation
  525. * @param alphabet Alphabet from which TrajectoryDescriptors were originally created
  526. * @returns deserialized TrajectoryDescriptor
  527. */
  528. static Deserialize(json, alphabet) {
  529. const jsonObject = JSON.parse(json);
  530. const described = new TrajectoryClass();
  531. described._descriptors = jsonObject.descriptors.map((s) => TrajectoryDescriptor.Deserialize(s, alphabet));
  532. described._centroidIdx = jsonObject.centroidIdx;
  533. described._averageDistance = jsonObject.averageDistance;
  534. return described;
  535. }
  536. /**
  537. * Create a new DescribedTrajectory.
  538. * @param descriptors currently-known TrajectoryDescriptors, if any
  539. */
  540. constructor(descriptors = []) {
  541. this._descriptors = descriptors;
  542. this._centroidIdx = -1;
  543. this._averageDistance = 0;
  544. this._refreshDescription();
  545. }
  546. /**
  547. * Add a new TrajectoryDescriptor to the list of descriptors known to describe
  548. * this same DescribedTrajectory.
  549. * @param descriptor descriptor to be added
  550. */
  551. add(descriptor) {
  552. this._descriptors.push(descriptor);
  553. this._refreshDescription();
  554. }
  555. /**
  556. * Compute the cost, which is inversely related to the likelihood that the provided
  557. * TrajectoryDescriptor describes a Trajectory that is considered to be the same as
  558. * the class represented by this DescribedTrajectory.
  559. * @param descriptor the descriptor to be costed
  560. * @returns cost of the match, which is a nonnegative similarity metric where larger values indicate dissimilarity
  561. */
  562. getMatchCost(descriptor) {
  563. return descriptor.distance(this._descriptors[this._centroidIdx]) / this._averageDistance;
  564. }
  565. /**
  566. * Compute the minimum distance between the queried TrajectoryDescriptor and a
  567. * descriptor which is a member of this collection. This is an alternative way of
  568. * conceptualizing match cost from getMatchCost(), and it serves a different function.
  569. * @param descriptor the descriptor to find the minimum distance to
  570. * @returns minimum descriptor distance to a member descriptor of this DescribedTrajectory
  571. */
  572. getMatchMinimumDistance(descriptor) {
  573. return Math.min(...this._descriptors.map((desc) => desc.distance(descriptor)));
  574. }
  575. /**
  576. * Refreshes the internal representation of this DescribedTrajectory.
  577. */
  578. _refreshDescription() {
  579. this._centroidIdx = -1;
  580. let sum;
  581. const distances = this._descriptors.map((a) => {
  582. sum = 0;
  583. this._descriptors.forEach((b) => {
  584. sum += a.distance(b);
  585. });
  586. return sum;
  587. });
  588. for (let idx = 0; idx < distances.length; ++idx) {
  589. if (this._centroidIdx < 0 || distances[idx] < distances[this._centroidIdx]) {
  590. this._centroidIdx = idx;
  591. }
  592. }
  593. this._averageDistance = 0;
  594. this._descriptors.forEach((desc) => {
  595. this._averageDistance += desc.distance(this._descriptors[this._centroidIdx]);
  596. });
  597. if (this._descriptors.length > 0) {
  598. this._averageDistance = Math.max(this._averageDistance / this._descriptors.length, TrajectoryClass._MIN_AVERAGE_DISTANCE);
  599. }
  600. }
  601. }
  602. TrajectoryClass._MIN_AVERAGE_DISTANCE = 1;
  603. /**
  604. * Class representing a set of known, named trajectories to which Trajectories can be
  605. * added and using which Trajectories can be recognized.
  606. */
  607. export class TrajectoryClassifier {
  608. /**
  609. * Serialize to JSON.
  610. * @returns JSON serialization
  611. */
  612. serialize() {
  613. const jsonObject = {};
  614. jsonObject.maximumAllowableMatchCost = this._maximumAllowableMatchCost;
  615. jsonObject.vector3Alphabet = this._vector3Alphabet.serialize();
  616. jsonObject.levenshteinAlphabet = this._levenshteinAlphabet.serialize();
  617. jsonObject.nameToDescribedTrajectory = [];
  618. this._nameToDescribedTrajectory.forEach((described, name) => {
  619. jsonObject.nameToDescribedTrajectory.push(name);
  620. jsonObject.nameToDescribedTrajectory.push(described.serialize());
  621. });
  622. return JSON.stringify(jsonObject);
  623. }
  624. /**
  625. * Deserialize from JSON.
  626. * @param json JSON serialization
  627. * @returns deserialized TrajectorySet
  628. */
  629. static Deserialize(json) {
  630. const jsonObject = JSON.parse(json);
  631. const classifier = new TrajectoryClassifier();
  632. classifier._maximumAllowableMatchCost = jsonObject.maximumAllowableMatchCost;
  633. classifier._vector3Alphabet = Vector3Alphabet.Deserialize(jsonObject.vector3Alphabet);
  634. classifier._levenshteinAlphabet = Levenshtein.Alphabet.Deserialize(jsonObject.levenshteinAlphabet);
  635. for (let idx = 0; idx < jsonObject.nameToDescribedTrajectory.length; idx += 2) {
  636. classifier._nameToDescribedTrajectory.set(jsonObject.nameToDescribedTrajectory[idx], TrajectoryClass.Deserialize(jsonObject.nameToDescribedTrajectory[idx + 1], classifier._levenshteinAlphabet));
  637. }
  638. return classifier;
  639. }
  640. /**
  641. * Initialize a new empty TrajectorySet with auto-generated Alphabets.
  642. * VERY naive, need to be generating these things from known
  643. * sets. Better version later, probably eliminating this one.
  644. * @returns auto-generated TrajectorySet
  645. */
  646. static Generate() {
  647. const vecs = Vector3Alphabet.Generate(64, 256, 0.1, 0.001, [Vector3.Forward()]);
  648. const charIdxs = new Array(vecs.chars.length);
  649. for (let idx = 0; idx < charIdxs.length; ++idx) {
  650. charIdxs[idx] = idx;
  651. }
  652. const alphabet = new Levenshtein.Alphabet(charIdxs, (idx) => (idx === 0 ? 0 : 1), (idx) => (idx === 0 ? 0 : 1), (a, b) => Math.min(1 - Vector3.Dot(vecs.chars[a], vecs.chars[b]), 1));
  653. const trajectorySet = new TrajectoryClassifier();
  654. trajectorySet._vector3Alphabet = vecs;
  655. trajectorySet._levenshteinAlphabet = alphabet;
  656. return trajectorySet;
  657. }
  658. constructor() {
  659. this._maximumAllowableMatchCost = 4;
  660. this._nameToDescribedTrajectory = new Map();
  661. }
  662. /**
  663. * Add a new Trajectory to the set with a given name.
  664. * @param trajectory new Trajectory to be added
  665. * @param classification name to which to add the Trajectory
  666. */
  667. addTrajectoryToClassification(trajectory, classification) {
  668. if (!this._nameToDescribedTrajectory.has(classification)) {
  669. this._nameToDescribedTrajectory.set(classification, new TrajectoryClass());
  670. }
  671. this._nameToDescribedTrajectory.get(classification).add(TrajectoryDescriptor.CreateFromTrajectory(trajectory, this._vector3Alphabet, this._levenshteinAlphabet));
  672. }
  673. /**
  674. * Remove a known named trajectory and all Trajectories associated with it.
  675. * @param classification name to remove
  676. * @returns whether anything was removed
  677. */
  678. deleteClassification(classification) {
  679. return this._nameToDescribedTrajectory.delete(classification);
  680. }
  681. /**
  682. * Attempt to recognize a Trajectory from among all the classifications
  683. * already known to the classifier.
  684. * @param trajectory Trajectory to be recognized
  685. * @returns classification of Trajectory if recognized, null otherwise
  686. */
  687. classifyTrajectory(trajectory) {
  688. const descriptor = TrajectoryDescriptor.CreateFromTrajectory(trajectory, this._vector3Alphabet, this._levenshteinAlphabet);
  689. const allowableMatches = [];
  690. this._nameToDescribedTrajectory.forEach((trajectoryClass, classification) => {
  691. if (trajectoryClass.getMatchCost(descriptor) < this._maximumAllowableMatchCost) {
  692. allowableMatches.push(classification);
  693. }
  694. });
  695. if (allowableMatches.length === 0) {
  696. return null;
  697. }
  698. let bestIdx = 0;
  699. let bestMatch = this._nameToDescribedTrajectory.get(allowableMatches[bestIdx]).getMatchMinimumDistance(descriptor);
  700. let match;
  701. for (let idx = 0; idx < allowableMatches.length; ++idx) {
  702. match = this._nameToDescribedTrajectory.get(allowableMatches[idx]).getMatchMinimumDistance(descriptor);
  703. if (match < bestMatch) {
  704. bestMatch = match;
  705. bestIdx = idx;
  706. }
  707. }
  708. return allowableMatches[bestIdx];
  709. }
  710. }
  711. //# sourceMappingURL=trajectoryClassifier.js.map