You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

state.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /*
  2. * MARTINS.js
  3. * GPU-accelerated Augmented Reality for the web
  4. * Copyright (C) 2022 Alexandre Martins <alemartf(at)gmail.com>
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Lesser General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. *
  19. * state.ts
  20. * Abstract state of the Image Tracker
  21. */
  22. import Speedy from 'speedy-vision';
  23. import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
  24. import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
  25. import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
  26. import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
  27. import { SpeedyPoint2 } from 'speedy-vision/types/core/speedy-point';
  28. import { SpeedyVector2 } from 'speedy-vision/types/core/speedy-vector';
  29. import { SpeedyPipeline, SpeedyPipelineOutput } from 'speedy-vision/types/core/pipeline/pipeline';
  30. import { SpeedyPipelineNodeImageSource } from 'speedy-vision/types/core/pipeline/nodes/images/source';
  31. import { SpeedyPipelineNodeResize } from 'speedy-vision/types/core/pipeline/nodes/transforms/resize';
  32. import { SpeedyPipelineNodeKeypointTransformer } from 'speedy-vision/types/core/pipeline/nodes/keypoints/transformer';
  33. import { SpeedyKeypoint } from 'speedy-vision/types/core/speedy-keypoint';
  34. import { ImageTracker, ImageTrackerOutput, ImageTrackerStateName } from '../image-tracker';
  35. import { TrackerOutput } from '../../tracker';
  36. import { Nullable } from '../../../utils/utils';
  37. import { IllegalOperationError } from '../../../utils/errors';
  38. import { TRACK_RECTIFIED_BORDER } from '../settings';
  39. /** State output */
  40. export interface ImageTrackerStateOutput
  41. {
  42. readonly trackerOutput: ImageTrackerOutput;
  43. readonly nextState: ImageTrackerStateName;
  44. readonly nextStateSettings?: Record<string,any>;
  45. }
  46. /**
  47. * Abstract state of the Image Tracker
  48. */
  49. export abstract class ImageTrackerState
  50. {
  51. /** image tracker */
  52. protected readonly _imageTracker: ImageTracker;
  53. /** state name */
  54. protected readonly _name: ImageTrackerStateName;
  55. /** pipeline */
  56. protected _pipeline: SpeedyPipeline;
  57. /**
  58. * Constructor
  59. * @param name
  60. * @param imageTracker
  61. */
  62. constructor(name: ImageTrackerStateName, imageTracker: ImageTracker)
  63. {
  64. this._name = name;
  65. this._imageTracker = imageTracker;
  66. this._pipeline = this._createPipeline();
  67. }
  68. /**
  69. * State name
  70. */
  71. get name(): ImageTrackerStateName
  72. {
  73. return this._name;
  74. }
  75. /**
  76. * AR screen size
  77. */
  78. get screenSize(): SpeedySize
  79. {
  80. const screen = this._pipeline.node('screen') as Nullable<SpeedyPipelineNodeResize>;
  81. if(!screen)
  82. throw new IllegalOperationError();
  83. // this is available once this state has run at least once
  84. return screen.size;
  85. }
  86. /**
  87. * Initialize the state
  88. */
  89. init(): void
  90. {
  91. }
  92. /**
  93. * Release resources
  94. */
  95. release(): null
  96. {
  97. return this._pipeline.release();
  98. }
  99. /**
  100. * Update the state
  101. * @param media user media
  102. * @param screenSize AR screen size for image processing
  103. * @param state all states
  104. * @returns promise
  105. */
  106. update(media: SpeedyMedia, screenSize: SpeedySize): SpeedyPromise<ImageTrackerStateOutput>
  107. {
  108. const source = this._pipeline.node('source') as Nullable<SpeedyPipelineNodeImageSource>;
  109. const screen = this._pipeline.node('screen') as Nullable<SpeedyPipelineNodeResize>;
  110. // validate the pipeline
  111. if(!source || !screen)
  112. throw new IllegalOperationError();
  113. // prepare the pipeline
  114. source.media = media;
  115. screen.size = screenSize;
  116. // run the pipeline
  117. return this._beforeUpdate().then(() =>
  118. this._gpuUpdate()
  119. ).then(result =>
  120. this._afterUpdate(result)
  121. );
  122. }
  123. /**
  124. * Called as soon as this becomes the active state, just before update() runs for the first time
  125. * @param settings
  126. */
  127. onEnterState(settings: Record<string,any>): void
  128. {
  129. }
  130. /**
  131. * Called when leaving the state, after update()
  132. */
  133. onLeaveState(): void
  134. {
  135. }
  136. /**
  137. * Called just before the GPU processing
  138. * @returns promise
  139. */
  140. protected _beforeUpdate(): SpeedyPromise<void>
  141. {
  142. return Speedy.Promise.resolve();
  143. }
  144. /**
  145. * GPU processing
  146. * @returns promise with the pipeline results
  147. */
  148. protected _gpuUpdate(): SpeedyPromise<SpeedyPipelineOutput>
  149. {
  150. return this._pipeline.run();
  151. }
  152. /**
  153. * Post processing that takes place just after the GPU processing
  154. * @param result pipeline results
  155. * @returns state output
  156. */
  157. protected abstract _afterUpdate(result: SpeedyPipelineOutput): SpeedyPromise<ImageTrackerStateOutput>;
  158. /**
  159. * Create & setup the pipeline
  160. * @returns pipeline
  161. */
  162. protected abstract _createPipeline(): SpeedyPipeline;
  163. //
  164. // Some utility methods common to various states
  165. //
  166. /**
  167. * Find the coordinates of a polyline surrounding the target image
  168. * @param homography maps the target image to the AR screen
  169. * @param targetSize size of the target space
  170. * @returns promise that resolves to 4 points in AR screen space
  171. */
  172. protected _findPolylineCoordinates(homography: SpeedyMatrix, targetSize: SpeedySize): SpeedyPromise<SpeedyMatrix>
  173. {
  174. const w = targetSize.width, h = targetSize.height;
  175. const referenceImageCoordinates = Speedy.Matrix(2, 4, [
  176. 0, 0,
  177. w, 0,
  178. w, h,
  179. 0, h,
  180. ]);
  181. const polylineCoordinates = Speedy.Matrix.Zeros(2, 4);
  182. return Speedy.Matrix.applyPerspectiveTransform(
  183. polylineCoordinates,
  184. referenceImageCoordinates,
  185. homography
  186. );
  187. }
  188. /**
  189. * Find a polyline surrounding the target image
  190. * @param homography maps the target image to the AR screen
  191. * @param targetSize size of the target space
  192. * @returns promise that resolves to 4 points in AR screen space
  193. */
  194. protected _findPolyline(homography: SpeedyMatrix, targetSize: SpeedySize): SpeedyPromise<SpeedyPoint2[]>
  195. {
  196. return this._findPolylineCoordinates(homography, targetSize).then(polylineCoordinates => {
  197. const polydata = polylineCoordinates.read();
  198. const polyline = Array.from({ length: 4 }, (_, i) => Speedy.Point2(polydata[2*i], polydata[2*i+1]));
  199. return polyline;
  200. });
  201. }
  202. /**
  203. * Whether or not to rotate the warped image in order to best fit the AR screen
  204. * @param media media associated with the reference image
  205. * @param screenSize AR screen
  206. * @returns boolean
  207. */
  208. protected _mustRotateWarpedImage(media: SpeedyMedia, screenSize: SpeedySize): boolean
  209. {
  210. const screenAspectRatio = screenSize.width / screenSize.height;
  211. const mediaAspectRatio = media.width / media.height;
  212. const eps = 0.1;
  213. return (mediaAspectRatio >= 1+eps && screenAspectRatio < 1-eps) || (mediaAspectRatio < 1-eps && screenAspectRatio >= 1+eps);
  214. }
  215. /**
  216. * Find a rectification matrix to be applied to an image fitting the entire AR screen
  217. * @param media media associated with the reference image
  218. * @param screenSize AR screen
  219. * @returns promise that resolves to a rectification matrix
  220. */
  221. protected _findRectificationMatrixOfFullscreenImage(media: SpeedyMedia, screenSize: SpeedySize): SpeedyPromise<SpeedyMatrix>
  222. {
  223. const b = TRACK_RECTIFIED_BORDER;
  224. const sw = screenSize.width, sh = screenSize.height;
  225. const mediaAspectRatio = media.width / media.height;
  226. const mustRotate = this._mustRotateWarpedImage(media, screenSize);
  227. // compute the vertices of the target in screen space
  228. // we suppose portrait or landscape mode for both screen & media
  229. const c = mustRotate ? 1 / mediaAspectRatio : mediaAspectRatio;
  230. const top = sw >= sh ? b * sh : (sh - sw * (1-2*b) / c) / 2;
  231. const left = sw >= sh ? (sw - sh * (1-2*b) * c) / 2 : b * sw;
  232. const right = sw - left;
  233. const bottom = sh - top;
  234. const targetVertices = Speedy.Matrix(2, 4, [
  235. left, top,
  236. right, top,
  237. right, bottom,
  238. left, bottom,
  239. ]);
  240. const screenVertices = Speedy.Matrix(2, 4, [
  241. 0, 0,
  242. sw, 0,
  243. sw, sh,
  244. 0, sh
  245. ]);
  246. const preRectificationMatrix = Speedy.Matrix.Eye(3);
  247. const alignmentMatrix = Speedy.Matrix.Zeros(3);
  248. const rectificationMatrix = Speedy.Matrix.Zeros(3);
  249. return (mustRotate ? Speedy.Matrix.perspective(
  250. // pre-rectifation: rotate by 90 degrees counterclockwise and scale to screenSize
  251. preRectificationMatrix,
  252. screenVertices,
  253. Speedy.Matrix(2, 4, [ 0,sh , 0,0 , sw,0 , sw,sh ])
  254. ) : Speedy.Promise.resolve(preRectificationMatrix)).then(_ =>
  255. // alignment: align the target to the center of the screen
  256. Speedy.Matrix.perspective(
  257. alignmentMatrix,
  258. screenVertices,
  259. targetVertices
  260. )
  261. ).then(_ =>
  262. // pre-rectify and then align
  263. rectificationMatrix.setTo(alignmentMatrix.times(preRectificationMatrix))
  264. );
  265. }
  266. /**
  267. * Find a rectification matrix to be applied to the target image
  268. * @param homography maps a reference image to the AR screen
  269. * @param targetSize size of the target space
  270. * @param media media associated with the reference image
  271. * @param screenSize AR screen
  272. * @returns promise that resolves to a rectification matrix
  273. */
  274. protected _findRectificationMatrixOfCameraImage(homography: SpeedyMatrix, targetSize: SpeedySize, media: SpeedyMedia, screenSize: SpeedySize): SpeedyPromise<SpeedyMatrix>
  275. {
  276. const sw = screenSize.width, sh = screenSize.height;
  277. const screen = Speedy.Matrix(2, 4, [ 0, 0, sw, 0, sw, sh, 0, sh ]);
  278. const rectificationMatrix = Speedy.Matrix.Zeros(3);
  279. return this._findPolylineCoordinates(homography, targetSize).then(polyline =>
  280. // from target space to (full)screen
  281. Speedy.Matrix.perspective(rectificationMatrix, polyline, screen)
  282. ).then(_ =>
  283. // from (full)screen to rectified coordinates
  284. this._findRectificationMatrixOfFullscreenImage(media, screenSize)
  285. ).then(mat =>
  286. // function composition
  287. rectificationMatrix.setTo(mat.times(rectificationMatrix))
  288. );
  289. }
  290. }