├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cypress.json ├── jest.config.js ├── package.json ├── postcss.config.js ├── public ├── images │ └── bg.png └── index.html ├── src ├── App.vue ├── assets │ ├── asteroids │ │ ├── asteroids_1.json │ │ ├── asteroids_2.json │ │ ├── asteroids_3.json │ │ ├── asteroids_4.json │ │ └── asteroids_5.json │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── RobotoMono-Bold.ttf │ │ ├── RobotoMono-BoldItalic.ttf │ │ ├── RobotoMono-Light.ttf │ │ ├── RobotoMono-LightItalic.ttf │ │ ├── RobotoMono-Medium.ttf │ │ ├── RobotoMono-MediumItalic.ttf │ │ ├── RobotoMono-Regular.ttf │ │ ├── RobotoMono-RegularItalic.ttf │ │ ├── RobotoMono-Thin.ttf │ │ └── RobotoMono-ThinItalic.ttf │ ├── images │ │ ├── background-2x.png │ │ ├── badge-first.png │ │ ├── badge-fourth.png │ │ ├── badge-second.png │ │ ├── badge-third.png │ │ ├── bg.png │ │ └── countdown-background-2x.png │ ├── json │ │ ├── asteroid-map.json │ │ ├── circle-animation.json │ │ ├── collision-block.json │ │ ├── collision-hit.json │ │ └── flight-paths.json │ └── svgs │ │ ├── asteroid-warning.svg │ │ ├── asteroid.svg │ │ ├── asteroid1.svg │ │ ├── asteroid2.svg │ │ ├── generic-error-icon.svg │ │ ├── health-meter.svg │ │ ├── logo.svg │ │ ├── no-video-icon.svg │ │ ├── rank-bar.svg │ │ ├── score-card-bg.svg │ │ ├── shields-off.svg │ │ ├── ship-wireframe.svg │ │ ├── spinner.svg │ │ └── tensorflow-logo.svg ├── components │ ├── ZoneDetector.vue │ ├── layouts │ │ ├── bg.vue │ │ ├── gameCanvas.vue │ │ ├── gameCanvasScreens │ │ │ ├── asteroidScreens │ │ │ │ ├── ambientAsteroids.vue │ │ │ │ └── gameAsteroids.vue │ │ │ ├── asteroids.vue │ │ │ ├── branding.vue │ │ │ ├── gameCanvasAsteroidWarning.vue │ │ │ ├── gameCanvasBg.vue │ │ │ ├── gameCanvasCountdown.vue │ │ │ ├── gameCanvasHealthMeter.vue │ │ │ ├── gameCanvasMinimap.vue │ │ │ ├── gameCanvasSeconds.vue │ │ │ ├── gameCanvasShields.vue │ │ │ ├── gameCanvasShip.vue │ │ │ └── spinner.vue │ │ ├── rightPanel.vue │ │ └── rightPanelScreens │ │ │ ├── errorIcons.vue │ │ │ ├── gamePlayMessages │ │ │ ├── error.vue │ │ │ └── posenetData.vue │ │ │ ├── messageScreen.vue │ │ │ ├── sharePanel.vue │ │ │ ├── sharePanels │ │ │ ├── badge.vue │ │ │ └── shareLink.vue │ │ │ ├── skeletalTrackingCanvas.vue │ │ │ └── stateMessages │ │ │ ├── attract.vue │ │ │ ├── calibration.vue │ │ │ ├── genericError.vue │ │ │ ├── noVideo.vue │ │ │ └── share.vue │ └── screens │ │ └── debugBar.vue ├── i18n.js ├── lib │ ├── PoseNetAdapter.js │ └── posenet │ │ ├── checkpoints.ts │ │ ├── index.ts │ │ ├── keypoints.ts │ │ ├── mobilenet.ts │ │ ├── model_weights.ts │ │ ├── posenet_model.ts │ │ ├── posenet_test.ts │ │ ├── resnet.ts │ │ ├── single_pose │ │ ├── argmax2d.ts │ │ ├── argmax2d_test.ts │ │ ├── decode_single_pose.ts │ │ └── util.ts │ │ ├── types.ts │ │ ├── util.ts │ │ └── util_test.ts ├── locales │ └── en.json ├── lottie.vue ├── main.js ├── router.js ├── scss │ ├── colors.scss │ ├── fonts.scss │ └── global.scss ├── settings.js ├── store.js └── views │ └── mainLayout.vue ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ └── example.spec.js ├── vue.config.js └── yarn.lock /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Google Inc. All Rights Reserved. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pose Shield 2 | 3 | Defend your spacecraft from asteroids by striking a pose to control its 4 | forcefields! Pose Shield is an experimental web-based game powered by 5 | TensorFlow.js and PoseNet. 6 | 7 | You can learn more about this experiment or try it out online by heading to 8 | g.co/poseshield. 9 | 10 | ## Tech stack 11 | 12 | These are some of the tools which were used in the implementation of 13 | this project: 14 | * [TensorFlow.js](https://www.tensorflow.org/js) and [PoseNet](https://github.com/tensorflow/tfjs-models/tree/master/posenet) handle pose estimation 15 | * [Vue.js](https://vuejs.org/) was used to implement the user interface 16 | * [GSAP](https://github.com/greensock/GreenSock-JS) was used for animations 17 | 18 | ## Development 19 | 20 | ### Prerequisites 21 | * Yarn (tested on 1.17.3) 22 | * Node.js (tested on 12.8.0) 23 | 24 | If you are missing these dependencies, on OS X you can install them using 25 | [HomeBrew](https://brew.sh/). Once you've installed Brew: 26 | 27 | ``` 28 | brew install yarn 29 | ``` 30 | 31 | ### Running Pose Shield locally 32 | 33 | You can use the following Yarn commands to run Pose Shield on your 34 | own machine. When running it locally, you'll find the demo here: 35 | http://127.0.0.1:8080/play/ 36 | 37 | ``` 38 | # Install dependencies 39 | yarn install 40 | 41 | # Serve the demo locally with hot-reloading for development 42 | yarn serve 43 | 44 | # Build the demo out to static, minified files in "dist" 45 | yarn build 46 | ``` 47 | 48 | ### Where to start reading the code 49 | 50 | A lot of this codebase is related primarily to user interface, animating 51 | asteroids, and managing state. If you are mostly interested in checking out 52 | how the pose estimation works, you might want to start by looking at one of 53 | the following files: 54 | * `src/lib/PoseNetAdapter.js`: all of the interaction with PoseNet happens 55 | here. Handles getting pose estimations, detecting certain basic poses 56 | used by the game, and drawing the pose outline. 57 | * `src/components/ZoneDetector.vue`: this component is in charge of using 58 | PoseNetAdapter to perform game-specific logic (such as determining when 59 | specific forcefields are activated) and updating the app's state with this 60 | information. 61 | 62 | ### URL parameters 63 | 64 | Pose Shield implements several URL parameters that can be used to modify its 65 | settings. Many of these parameters are related to the 66 | [parameters of the PoseNet model] (https://github.com/tensorflow/tfjs-models/tree/master/posenet#config-params-in-posenetload). 67 | 68 | By default, Pose Shield is configured to use PoseNet settings which are 69 | suitable for slower CPUs and machines without higher end GPUs. If you have 70 | a faster machine, you might want to try changing some of these parameters 71 | to see how it impacts the performance of the model. 72 | 73 | First up, you might want to try changing to one of the predefined "modes", 74 | which are combinations of settings given a name. The valid modes are: 75 | 76 | * `default`: these are the base settings used when no mode is set 77 | * `kiosk`: this mode uses somewhat higher end settings 78 | * `kiosk-noqr`: the same as `kiosk`, but with the QR-code based social sharing 79 | turned off. 80 | 81 | You would set a mode by adding it to the URL like this: 82 | * Online: https://poseshield.withgoogle.com/play?mode=kiosk 83 | * Running locally: https://127.0.0.1:8080/play?mode=kiosk 84 | 85 | Parameters can also be set individually. If you set an individual parameter, it 86 | will override whatever settings came from any mode you may have already chosen. 87 | 88 | The available parameters are as follows: 89 | * `share-qr`: true (default) or false. This disables the QR Code that would otherwise be displayed on the share screen at the end of the game. 90 | * `share-link`: true (default) or false. This disables the poseshield.withgoogle.com url that would otherwise be displayed on the share screen at the end of the game. 91 | * `sharing`: true (default) or false. This adds the ability to disable `share-qr` and `share-link` at once. Will override individual settings. 92 | * `timeout`: number of seconds (default = 15, and must be between 10 and 120) the application will wait the timeout duration before resetting states after a game is completed or if no person is detected. 93 | * `asteroid-group`: value between 0 and 5 (default = 0). This selects a specific asteroid script for gameplay. If set to 0, it will choose randomly between the 5 available scripts. 94 | * `model-stride`: 8 or 16 (default). Sets the model's output stride. The smaller the number, the more accurate the model but the slower the application. 95 | * `multiplier`: 0.5 (default), 0.75 or 1.0. Sets the number of channels used for all convolution operations. The larger the number, the more accurate the model but the slower the application. 96 | * `input-res`: an integer which defaults to 257 and must be one of the following: 161|193|257|289|321|353|385|417|449|481|513|801|1217. Specifies the size the input image is scaled to before feeding it through the PoseNet model. The larger the number, the more accurate the model but the slower the application. 97 | * `min-part-conf` - a value between 0 and 1 (default = 0.5). It sets the threshold for body part detection. 98 | * `min-pose-conf` - a value between 0 and 1 (default = 0.5). It sets the threshold for pose detection. 99 | 100 | Here's an example of setting these parameters: 101 | * Online: https://poseshield.withgoogle.com/play?input-res=289&multiplier=.75 102 | * Running locally: http://127.0.0.1:8080/play?input-res=289&multiplier=.75 103 | 104 | ### Debugging in the browser 105 | 106 | There are some helpful shortcuts you can use to turn debugging features of 107 | the demo on and off, or skip around different modes of the game: 108 | 109 | * `v`: toggle video feed on and off 110 | * `z`: toggle video overlay of where the zones are detected. These are used to 111 | determine which forcefield to activate based on your arm positions 112 | * `d`: switch to debug mode. Turns on video and zones, as well as a 113 | framerate display. Once in debug mode, you can also use the following keys: 114 | * `space bar`: Turn off pose estimation completely (can be useful in 115 | development if your machine is struggling with performance) 116 | * `a` to jump to the attract screen 117 | * `p` to do the initial pose activation 118 | * `c` to jump to the calibration screen 119 | * `g` to jump to the game 120 | * `s` to jump to the share screen 121 | * `e` to jump to the error screen 122 | * use the numbers `1-7` to activate shields with the keyboard. 123 | 124 | ## License 125 | 126 | Copyright 2019 Google LLC 127 | 128 | Licensed under the Apache License, Version 2.0 (the "License"); 129 | you may not use this file except in compliance with the License. 130 | You may obtain a copy of the License at 131 | 132 | https://www.apache.org/licenses/LICENSE-2.0 133 | 134 | Unless required by applicable law or agreed to in writing, software 135 | distributed under the License is distributed on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 137 | See the License for the specific language governing permissions and 138 | limitations under the License. 139 | 140 | ## Final Thoughts 141 | 142 | This is not an official Google product. We will do our best to support and maintain this experiment but your mileage may vary. 143 | 144 | We encourage open sourcing projects as a way of learning from each other. Please respect our and other creators’ rights, including copyright and trademark rights when present, when sharing these works and creating derivative work. 145 | 146 | If you want more info on Google's policy, you can find that [here](https://policies.google.com/). 147 | 148 | 149 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | module.exports = { 19 | moduleFileExtensions: [ 20 | 'js', 21 | 'jsx', 22 | 'json', 23 | 'vue' 24 | ], 25 | transform: { 26 | '^.+\\.vue$': 'vue-jest', 27 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 28 | '^.+\\.jsx?$': 'babel-jest' 29 | }, 30 | transformIgnorePatterns: [ 31 | '/node_modules/' 32 | ], 33 | moduleNameMapper: { 34 | '^@/(.*)$': '/src/$1' 35 | }, 36 | snapshotSerializers: [ 37 | 'jest-serializer-vue' 38 | ], 39 | testMatch: [ 40 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 41 | ], 42 | testURL: 'http://localhost/', 43 | watchPlugins: [ 44 | 'jest-watch-typeahead/filename', 45 | 'jest-watch-typeahead/testname' 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poseshield", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'" 10 | }, 11 | "dependencies": { 12 | "@tensorflow-models/posenet": "^2.1.3", 13 | "@tensorflow/tfjs-converter": "^1.2.7", 14 | "@tensorflow/tfjs-core": "^1.2.8", 15 | "gsap": "^2.1.3", 16 | "qrcode": "^1.4.1", 17 | "stats.js": "^0.17.0", 18 | "vue": "^2.6.10", 19 | "vue-i18n": "^8.0.0", 20 | "vue-lottie": "^0.2.1", 21 | "vue-router": "^3.0.3", 22 | "vuex": "^3.0.1" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-eslint": "^3.10.0", 26 | "@vue/cli-service": "^3.10.0", 27 | "@vue/eslint-config-standard": "^4.0.0", 28 | "babel-eslint": "^10.0.1", 29 | "eslint": "^6.1.0", 30 | "eslint-config-google": "^0.13.0", 31 | "eslint-plugin-vue": "^5.0.0", 32 | "sass": "^1.18.0", 33 | "sass-lint-vue": "^0.4.0", 34 | "sass-loader": "^7.1.0", 35 | "vue-cli-plugin-i18n": "^0.6.0", 36 | "vue-svg-loader": "^0.12.0", 37 | "vue-template-compiler": "^2.6.10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | module.exports = { 19 | plugins: { 20 | autoprefixer: {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/public/images/bg.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pose Shield by TensorFlow 8 | 9 | 10 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-LightItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-MediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-RegularItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-RegularItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-Thin.ttf -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/fonts/RobotoMono-ThinItalic.ttf -------------------------------------------------------------------------------- /src/assets/images/background-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/background-2x.png -------------------------------------------------------------------------------- /src/assets/images/badge-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/badge-first.png -------------------------------------------------------------------------------- /src/assets/images/badge-fourth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/badge-fourth.png -------------------------------------------------------------------------------- /src/assets/images/badge-second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/badge-second.png -------------------------------------------------------------------------------- /src/assets/images/badge-third.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/badge-third.png -------------------------------------------------------------------------------- /src/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/bg.png -------------------------------------------------------------------------------- /src/assets/images/countdown-background-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/poseshield-tfjs/34d0f10479eb3b97b06821129c6309d184cf5f22/src/assets/images/countdown-background-2x.png -------------------------------------------------------------------------------- /src/assets/json/circle-animation.json: -------------------------------------------------------------------------------- 1 | {"v":"5.4.3","fr":60,"ip":0,"op":70,"w":100,"h":450,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 17","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,76.5,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[74,74,100],"ix":6}},"ao":0,"ip":0,"op":1200,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Isolation Mode 4","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":1,"s":[0],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":13.172,"s":[100],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":19.443,"s":[100],"e":[0]},{"t":34.84375}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.785,457.765,0],"ix":2},"a":{"a":0,"k":[-444.898,-204.491,0],"ix":1},"s":{"a":0,"k":[769.231,769.231,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,4.859],[4.859,0],[0,-4.859],[-4.859,0]],"o":[[0,-4.859],[-4.859,0],[0,4.859],[4.859,0]],"v":[[-436.099,-204.49],[-444.898,-213.289],[-453.696,-204.49],[-444.898,-195.692]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705884457,0.917647063732,0.952941179276,1],"ix":4},"o":{"a":0,"k":5,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":2},{"ddd":0,"ind":3,"ty":4,"nm":"Isolation Mode 3","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":5.695,"s":[0],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":17.869,"s":[100],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":23.994,"s":[100],"e":[0]},{"t":40.888671875}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.785,322.402,0],"ix":2},"a":{"a":0,"k":[-444.898,-222.088,0],"ix":1},"s":{"a":0,"k":[769.231,769.231,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,4.859],[4.859,0],[0,-4.859],[-4.859,0]],"o":[[0,-4.859],[-4.859,0],[0,4.859],[4.859,0]],"v":[[-436.099,-222.088],[-444.898,-230.886],[-453.696,-222.088],[-444.898,-213.289]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705884457,0.917647063732,0.952941179276,1],"ix":4},"o":{"a":0,"k":10,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":2},{"ddd":0,"ind":4,"ty":4,"nm":"Isolation Mode 2","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":11.172,"s":[0],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":23.346,"s":[100],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":30.256,"s":[100],"e":[0]},{"t":47.689453125}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.785,187.04,0],"ix":2},"a":{"a":0,"k":[-444.898,-239.685,0],"ix":1},"s":{"a":0,"k":[769.231,769.231,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,4.859],[4.859,0],[0,-4.859],[-4.859,0]],"o":[[0,-4.859],[-4.859,0],[0,4.859],[4.859,0]],"v":[[-436.099,-239.685],[-444.898,-248.483],[-453.696,-239.685],[-444.898,-230.886]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705884457,0.917647063732,0.952941179276,1],"ix":4},"o":{"a":0,"k":19,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":2},{"ddd":0,"ind":5,"ty":4,"nm":"Isolation Mode","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":15.707,"s":[0],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":30.385,"s":[100],"e":[100]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.35],"y":[0]},"n":["0p16_1_0p35_0"],"t":36.512,"s":[100],"e":[0]},{"t":56}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.785,51.677,0],"ix":2},"a":{"a":0,"k":[-444.898,-257.282,0],"ix":1},"s":{"a":0,"k":[769.231,769.231,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,4.859],[4.859,0],[0,-4.859],[-4.859,0]],"o":[[0,-4.859],[-4.859,0],[0,4.859],[4.859,0]],"v":[[-436.099,-257.282],[-444.898,-266.081],[-453.696,-257.282],[-444.898,-248.483]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705884457,0.917647063732,0.952941179276,1],"ix":4},"o":{"a":0,"k":32,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":2}],"markers":[]} -------------------------------------------------------------------------------- /src/assets/json/flight-paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "paths": [ 4 | { 5 | "start": { 6 | "left": -112, 7 | "top": 800 8 | }, 9 | "end": { 10 | "left": 354, 11 | "top": 800 12 | }, 13 | "hitzone": { 14 | "left": 310, 15 | "top": 800, 16 | "percent": 0.9055793991416309 17 | } 18 | } 19 | ] 20 | }, 21 | "2": { 22 | "paths": [ 23 | { 24 | "start": { 25 | "left": -112, 26 | "top": 525 27 | }, 28 | "end": { 29 | "left": 456, 30 | "top": 656 31 | }, 32 | "hitzone": { 33 | "left": 407, 34 | "top": 645, 35 | "percent": 0.9137323943661971 36 | } 37 | } 38 | ] 39 | }, 40 | "3": { 41 | "paths": [ 42 | { 43 | "start": { 44 | "left": -112, 45 | "top": 400 46 | }, 47 | "end": { 48 | "left": 484, 49 | "top": 504 50 | }, 51 | "hitzone": { 52 | "left": 441, 53 | "top": 497, 54 | "percent" : 0.9278523489932886 55 | } 56 | } 57 | ] 58 | }, 59 | "4": { 60 | "paths": [ 61 | { 62 | "start": { 63 | "left": 584, 64 | "top": -112 65 | }, 66 | "end": { 67 | "left": 584, 68 | "top": 348 69 | }, 70 | "hitzone": { 71 | "left": 584, 72 | "top": 280, 73 | "percent" : 0.8521739130434782 74 | } 75 | } 76 | ] 77 | }, 78 | "5": { 79 | "paths": [ 80 | { 81 | "start": { 82 | "left": 1280, 83 | "top": 400 84 | }, 85 | "end": { 86 | "left": 684, 87 | "top": 504 88 | }, 89 | "hitzone": { 90 | "left": 724.453125, 91 | "top": 497, 92 | "percent" : 0.9278523489932886 93 | } 94 | } 95 | ] 96 | }, 97 | "6": { 98 | "paths": [ 99 | { 100 | "start": { 101 | "left": 1280, 102 | "top": 525 103 | }, 104 | "end": { 105 | "left": 710, 106 | "top": 656 107 | }, 108 | "hitzone": { 109 | "left": 758, 110 | "top": 645, 111 | "percent": 0.9137323943661971 112 | } 113 | } 114 | ] 115 | }, 116 | "7": { 117 | "paths": [ 118 | { 119 | "start": { 120 | "left": 1280, 121 | "top": 800 122 | }, 123 | "end": { 124 | "left": 814, 125 | "top": 800 126 | }, 127 | "hitzone": { 128 | "left": 852, 129 | "top": 800, 130 | "percent": 0.9055793991416309 131 | } 132 | } 133 | ] 134 | } 135 | } -------------------------------------------------------------------------------- /src/assets/svgs/asteroid-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Asset 25 9 | 10 | 11 | 12 | 13 | ASTEROID WARNING 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/svgs/asteroid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | asteroid 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/assets/svgs/asteroid1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Asteroid 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/svgs/asteroid2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Asteroid 2 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/svgs/generic-error-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | generic-error-icon 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/svgs/health-meter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | health-meter 17 | 18 | 19 | 20 | 21 | 22 | 23 | 100% 24 | 25 | 26 | 27 | 28 | HEALTH 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/assets/svgs/no-video-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | no-video-icon 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/svgs/rank-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/svgs/score-card-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | score-card-bg 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/svgs/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | spinner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/svgs/tensorflow-logo.svg: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/components/layouts/bg.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvas.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 73 | 74 | 152 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/asteroidScreens/ambientAsteroids.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 146 | 147 | 167 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/asteroids.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | 25 | 62 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/branding.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 156 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasAsteroidWarning.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 52 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasBg.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 92 | 93 | 119 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasCountdown.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasHealthMeter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | 46 | 128 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasMinimap.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 62 | 63 | 75 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasSeconds.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | 32 | 60 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasShields.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 38 | 143 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/gameCanvasShip.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 146 | -------------------------------------------------------------------------------- /src/components/layouts/gameCanvasScreens/spinner.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 | 125 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | 42 | 91 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/errorIcons.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | 71 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/gamePlayMessages/error.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/gamePlayMessages/posenetData.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 125 | 126 | 201 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/messageScreen.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 57 | 207 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/sharePanel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 47 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/sharePanels/badge.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 53 | 54 | 216 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/sharePanels/shareLink.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 83 | 84 | 140 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/skeletalTrackingCanvas.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 64 | 65 | 145 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/stateMessages/attract.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/stateMessages/calibration.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/stateMessages/genericError.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/stateMessages/noVideo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/components/layouts/rightPanelScreens/stateMessages/share.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/components/screens/debugBar.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 88 | 89 | 90 | 120 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | import Vue from 'vue'; 20 | import VueI18n from 'vue-i18n'; 21 | 22 | Vue.use(VueI18n); 23 | 24 | /** 25 | * @return {messages} Messages/content from i18n file. 26 | */ 27 | function loadLocaleMessages() { 28 | // Uncomment to use json instead of YAML 29 | const locales = require. 30 | context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i); 31 | 32 | // Uncomment to using yaml file instead of json 33 | // const locales = require. 34 | // context('./locales', true, /[A-Za-z0-9-_,\s]+\.yaml$/i); 35 | const messages = {}; 36 | 37 | locales.keys().forEach( (key) => { 38 | const matched = key.match(/([A-Za-z0-9-_]+)\./i); 39 | if (matched && matched.length > 1) { 40 | const locale = matched[1]; 41 | messages[locale] = locales(key); 42 | } 43 | }); 44 | 45 | return messages; 46 | } 47 | 48 | export default new VueI18n({ 49 | locale: process.env.VUE_APP_I18N_LOCALE || 'en', 50 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en', 51 | messages: loadLocaleMessages(), 52 | }); 53 | -------------------------------------------------------------------------------- /src/lib/posenet/checkpoints.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | const MOBILENET_BASE_URL = 19 | 'https://storage.googleapis.com/tfjs-models/savedmodel/posenet/mobilenet/'; 20 | const RESNET50_BASE_URL = 21 | 'https://storage.googleapis.com/tfjs-models/savedmodel/posenet/resnet50/'; 22 | 23 | // The PoseNet 2.0 ResNet50 models use the latest TensorFlow.js 1.0 model 24 | // format. 25 | export function resNet50Checkpoint(stride: number, quantBytes: number): string { 26 | const graphJson = `model-stride${stride}.json`; 27 | // quantBytes=4 corresponding to the non-quantized full-precision checkpoints. 28 | if (quantBytes == 4) { 29 | return RESNET50_BASE_URL + `float/` + graphJson; 30 | } else { 31 | return RESNET50_BASE_URL + `quant${quantBytes}/` + graphJson; 32 | } 33 | }; 34 | 35 | // The PoseNet 2.0 MobileNetV1 models use the latest TensorFlow.js 1.0 model 36 | // format. 37 | export function mobileNetCheckpoint( 38 | stride: number, multiplier: number, quantBytes: number): string { 39 | const toStr: {[key: number]: string} = {1.0: '100', 0.75: '075', 0.50: '050'}; 40 | const graphJson = `model-stride${stride}.json`; 41 | // quantBytes=4 corresponding to the non-quantized full-precision checkpoints. 42 | if (quantBytes == 4) { 43 | return MOBILENET_BASE_URL + `float/${toStr[multiplier]}/` + graphJson; 44 | } else { 45 | return MOBILENET_BASE_URL + `quant${quantBytes}/${toStr[multiplier]}/` + 46 | graphJson; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/posenet/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import {MobileNet, MobileNetMultiplier} from './mobilenet'; 19 | import {decodeMultiplePoses} from './multi_pose/decode_multiple_poses'; 20 | import {decodeSinglePose} from './single_pose/decode_single_pose'; 21 | 22 | export {partChannels, partIds, partNames, poseChain} from './keypoints'; 23 | export {load, PoseNet, PoseNetOutputStride, VALID_INPUT_RESOLUTION} from './posenet_model'; 24 | export {Keypoint, Pose} from './types'; 25 | export {getAdjacentKeyPoints, getBoundingBox, getBoundingBoxPoints, scalePose} from './util'; 26 | export { 27 | decodeMultiplePoses, 28 | decodeSinglePose, 29 | MobileNet, 30 | MobileNetMultiplier, 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/posenet/keypoints.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | export type Tuple = [T, T]; 19 | export type StringTuple = Tuple; 20 | export type NumberTuple = Tuple; 21 | 22 | export const partNames = [ 23 | 'nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar', 'leftShoulder', 24 | 'rightShoulder', 'leftElbow', 'rightElbow', 'leftWrist', 'rightWrist', 25 | 'leftHip', 'rightHip', 'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle' 26 | ]; 27 | 28 | export const NUM_KEYPOINTS = partNames.length; 29 | 30 | export interface NumberDict { 31 | [jointName: string]: number; 32 | } 33 | 34 | export const partIds = 35 | partNames.reduce((result: NumberDict, jointName, i): NumberDict => { 36 | result[jointName] = i; 37 | return result; 38 | }, {}) as NumberDict; 39 | 40 | const connectedPartNames: StringTuple[] = [ 41 | ['leftHip', 'leftShoulder'], ['leftElbow', 'leftShoulder'], 42 | ['leftElbow', 'leftWrist'], ['leftHip', 'leftKnee'], 43 | ['leftKnee', 'leftAnkle'], ['rightHip', 'rightShoulder'], 44 | ['rightElbow', 'rightShoulder'], ['rightElbow', 'rightWrist'], 45 | ['rightHip', 'rightKnee'], ['rightKnee', 'rightAnkle'], 46 | ['leftShoulder', 'rightShoulder'], ['leftHip', 'rightHip'] 47 | ]; 48 | 49 | /* 50 | * Define the skeleton. This defines the parent->child relationships of our 51 | * tree. Arbitrarily this defines the nose as the root of the tree, however 52 | * since we will infer the displacement for both parent->child and 53 | * child->parent, we can define the tree root as any node. 54 | */ 55 | export const poseChain: StringTuple[] = [ 56 | ['nose', 'leftEye'], ['leftEye', 'leftEar'], ['nose', 'rightEye'], 57 | ['rightEye', 'rightEar'], ['nose', 'leftShoulder'], 58 | ['leftShoulder', 'leftElbow'], ['leftElbow', 'leftWrist'], 59 | ['leftShoulder', 'leftHip'], ['leftHip', 'leftKnee'], 60 | ['leftKnee', 'leftAnkle'], ['nose', 'rightShoulder'], 61 | ['rightShoulder', 'rightElbow'], ['rightElbow', 'rightWrist'], 62 | ['rightShoulder', 'rightHip'], ['rightHip', 'rightKnee'], 63 | ['rightKnee', 'rightAnkle'] 64 | ]; 65 | 66 | export const connectedPartIndices = connectedPartNames.map( 67 | ([jointNameA, jointNameB]) => ([partIds[jointNameA], partIds[jointNameB]])); 68 | 69 | export const partChannels: string[] = [ 70 | 'left_face', 71 | 'right_face', 72 | 'right_upper_leg_front', 73 | 'right_lower_leg_back', 74 | 'right_upper_leg_back', 75 | 'left_lower_leg_front', 76 | 'left_upper_leg_front', 77 | 'left_upper_leg_back', 78 | 'left_lower_leg_back', 79 | 'right_feet', 80 | 'right_lower_leg_front', 81 | 'left_feet', 82 | 'torso_front', 83 | 'torso_back', 84 | 'right_upper_arm_front', 85 | 'right_upper_arm_back', 86 | 'right_lower_arm_back', 87 | 'left_lower_arm_front', 88 | 'left_upper_arm_front', 89 | 'left_upper_arm_back', 90 | 'left_lower_arm_back', 91 | 'right_hand', 92 | 'right_lower_arm_front', 93 | 'left_hand' 94 | ]; 95 | -------------------------------------------------------------------------------- /src/lib/posenet/mobilenet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tfconv from '@tensorflow/tfjs-converter'; 19 | import * as tf from '@tensorflow/tfjs-core'; 20 | 21 | import {BaseModel, PoseNetOutputStride} from './posenet_model'; 22 | 23 | 24 | export type MobileNetMultiplier = 0.50|0.75|1.0; 25 | 26 | const VALID_OUTPUT_STRIDES = [8, 16, 32]; 27 | // tslint:disable-next-line:no-any 28 | export function assertValidOutputStride(outputStride: any) { 29 | tf.util.assert( 30 | typeof outputStride === 'number', () => 'outputStride is not a number'); 31 | tf.util.assert( 32 | VALID_OUTPUT_STRIDES.indexOf(outputStride) >= 0, 33 | () => `outputStride of ${outputStride} is invalid. ` + 34 | `It must be either 8, 16, or 32`); 35 | } 36 | 37 | // tslint:disable-next-line:no-any 38 | export function assertValidResolution(resolution: any, outputStride: number) { 39 | tf.util.assert( 40 | typeof resolution === 'number', () => 'resolution is not a number'); 41 | 42 | tf.util.assert( 43 | (resolution - 1) % outputStride === 0, 44 | () => `resolution of ${resolution} is invalid for output stride ` + 45 | `${outputStride}.`); 46 | } 47 | 48 | function toFloatIfInt(input: tf.Tensor3D): tf.Tensor3D { 49 | return tf.tidy(() => { 50 | if (input.dtype === 'int32') input = input.toFloat(); 51 | // Normalize the pixels [0, 255] to be between [-1, 1]. 52 | input = tf.div(input, 127.5); 53 | return tf.sub(input, 1.0); 54 | }) 55 | } 56 | 57 | export class MobileNet implements BaseModel { 58 | readonly model: tfconv.GraphModel 59 | readonly outputStride: PoseNetOutputStride 60 | 61 | constructor(model: tfconv.GraphModel, outputStride: PoseNetOutputStride) { 62 | this.model = model; 63 | const inputShape = 64 | this.model.inputs[0].shape as [number, number, number, number]; 65 | tf.util.assert( 66 | (inputShape[1] === -1) && (inputShape[2] === -1), 67 | () => `Input shape [${inputShape[1]}, ${inputShape[2]}] ` + 68 | `must both be -1`); 69 | this.outputStride = outputStride; 70 | } 71 | 72 | predict(input: tf.Tensor3D): {[key: string]: tf.Tensor3D} { 73 | return tf.tidy(() => { 74 | const asFloat = toFloatIfInt(input); 75 | const asBatch = asFloat.expandDims(0); 76 | const [offsets4d, heatmaps4d, displacementFwd4d, displacementBwd4d] = 77 | this.model.predict(asBatch) as tf.Tensor[]; 78 | 79 | const heatmaps = heatmaps4d.squeeze() as tf.Tensor3D; 80 | const heatmapScores = heatmaps.sigmoid(); 81 | const offsets = offsets4d.squeeze() as tf.Tensor3D; 82 | const displacementFwd = displacementFwd4d.squeeze() as tf.Tensor3D; 83 | const displacementBwd = displacementBwd4d.squeeze() as tf.Tensor3D; 84 | 85 | return { 86 | heatmapScores, offsets: offsets as tf.Tensor3D, 87 | displacementFwd: displacementFwd as tf.Tensor3D, 88 | displacementBwd: displacementBwd as tf.Tensor3D 89 | } 90 | }); 91 | } 92 | 93 | dispose() { 94 | this.model.dispose(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/posenet/model_weights.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | 20 | export class ModelWeights { 21 | private variables: {[varName: string]: tf.Tensor}; 22 | 23 | constructor(variables: {[varName: string]: tf.Tensor}) { 24 | this.variables = variables; 25 | } 26 | 27 | weights(layerName: string) { 28 | return this.variables[`MobilenetV1/${layerName}/weights`] as tf.Tensor4D; 29 | } 30 | 31 | depthwiseBias(layerName: string) { 32 | return this.variables[`MobilenetV1/${layerName}/biases`] as tf.Tensor1D; 33 | } 34 | 35 | convBias(layerName: string) { 36 | return this.depthwiseBias(layerName); 37 | } 38 | 39 | depthwiseWeights(layerName: string) { 40 | return this.variables[`MobilenetV1/${layerName}/depthwise_weights`] as 41 | tf.Tensor4D; 42 | } 43 | 44 | dispose() { 45 | for (const varName in this.variables) { 46 | this.variables[varName].dispose(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/posenet/posenet_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | ============================================================================= 17 | */ 18 | 19 | import * as tfconv from '@tensorflow/tfjs-converter'; 20 | import * as tf from '@tensorflow/tfjs-core'; 21 | import {describeWithFlags, NODE_ENVS} from '@tensorflow/tfjs-core/dist/jasmine_util'; 22 | 23 | import * as mobilenet from './mobilenet'; 24 | import * as posenetModel from './posenet_model'; 25 | import * as resnet from './resnet'; 26 | 27 | describeWithFlags('PoseNet', NODE_ENVS, () => { 28 | let mobileNet: posenetModel.PoseNet; 29 | let resNet: posenetModel.PoseNet; 30 | const inputResolution = 513; 31 | const outputStride = 32; 32 | const multiplier = 1.0; 33 | const quantBytes = 4; 34 | const outputResolution = (inputResolution - 1) / outputStride + 1; 35 | const numKeypoints = 17; 36 | 37 | beforeAll((done) => { 38 | // Mock out the actual load so we don't make network requests in the unit 39 | // test. 40 | const resNetConfig = { 41 | architecture: 'ResNet50', 42 | outputStride: outputStride, 43 | inputResolution, 44 | quantBytes: quantBytes 45 | } as posenetModel.ModelConfig; 46 | 47 | const mobileNetConfig = { 48 | architecture: 'MobileNetV1', 49 | outputStride: outputStride, 50 | inputResolution, 51 | multiplier: multiplier, 52 | quantBytes: quantBytes 53 | } as posenetModel.ModelConfig; 54 | 55 | spyOn(tfconv, 'loadGraphModel').and.callFake((): tfconv.GraphModel => { 56 | return null; 57 | }) 58 | 59 | spyOn(resnet, 'ResNet').and.callFake(() => { 60 | return { 61 | outputStride, 62 | predict: (input: tf.Tensor3D) => { 63 | return { 64 | inputResolution, 65 | heatmapScores: 66 | tf.zeros([outputResolution, outputResolution, numKeypoints]), 67 | offsets: tf.zeros( 68 | [outputResolution, outputResolution, 2 * numKeypoints]), 69 | displacementFwd: tf.zeros( 70 | [outputResolution, outputResolution, 2 * (numKeypoints - 1)]), 71 | displacementBwd: tf.zeros( 72 | [outputResolution, outputResolution, 2 * (numKeypoints - 1)]) 73 | }; 74 | }, 75 | dipose: () => {} 76 | }; 77 | }); 78 | 79 | spyOn(mobilenet, 'MobileNet').and.callFake(() => { 80 | return { 81 | outputStride, 82 | predict: (input: tf.Tensor3D) => { 83 | return { 84 | inputResolution, 85 | heatmapScores: 86 | tf.zeros([outputResolution, outputResolution, numKeypoints]), 87 | offsets: tf.zeros( 88 | [outputResolution, outputResolution, 2 * numKeypoints]), 89 | displacementFwd: tf.zeros( 90 | [outputResolution, outputResolution, 2 * (numKeypoints - 1)]), 91 | displacementBwd: tf.zeros( 92 | [outputResolution, outputResolution, 2 * (numKeypoints - 1)]) 93 | }; 94 | }, 95 | dipose: () => {} 96 | }; 97 | }); 98 | 99 | posenetModel.load(resNetConfig) 100 | .then((posenetInstance: posenetModel.PoseNet) => { 101 | resNet = posenetInstance; 102 | }) 103 | .then(() => posenetModel.load(mobileNetConfig)) 104 | .then((posenetInstance: posenetModel.PoseNet) => { 105 | mobileNet = posenetInstance; 106 | }) 107 | .then(done) 108 | .catch(done.fail); 109 | }); 110 | 111 | it('estimateSinglePose does not leak memory', done => { 112 | const input = 113 | tf.zeros([inputResolution, inputResolution, 3]) as tf.Tensor3D; 114 | 115 | const beforeTensors = tf.memory().numTensors; 116 | 117 | resNet.estimateSinglePose(input, {flipHorizontal: false}) 118 | .then(() => { 119 | return mobileNet.estimateSinglePose(input, {flipHorizontal: false}); 120 | }) 121 | .then(() => { 122 | expect(tf.memory().numTensors).toEqual(beforeTensors); 123 | }) 124 | .then(done) 125 | .catch(done.fail); 126 | }); 127 | 128 | it('estimateMultiplePoses does not leak memory', done => { 129 | const input = 130 | tf.zeros([inputResolution, inputResolution, 3]) as tf.Tensor3D; 131 | 132 | const beforeTensors = tf.memory().numTensors; 133 | resNet 134 | .estimateMultiplePoses(input, { 135 | flipHorizontal: false, 136 | maxDetections: 5, 137 | scoreThreshold: 0.5, 138 | nmsRadius: 20 139 | }) 140 | .then(() => { 141 | return mobileNet.estimateMultiplePoses(input, { 142 | flipHorizontal: false, 143 | maxDetections: 5, 144 | scoreThreshold: 0.5, 145 | nmsRadius: 20 146 | }); 147 | }) 148 | .then(() => { 149 | expect(tf.memory().numTensors).toEqual(beforeTensors); 150 | }) 151 | .then(done) 152 | .catch(done.fail); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/lib/posenet/resnet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tfconv from '@tensorflow/tfjs-converter'; 19 | import * as tf from '@tensorflow/tfjs-core'; 20 | 21 | import {BaseModel, PoseNetOutputStride} from './posenet_model'; 22 | 23 | function toFloatIfInt(input: tf.Tensor3D): tf.Tensor3D { 24 | return tf.tidy(() => { 25 | if (input.dtype === 'int32') { 26 | input = input.toFloat(); 27 | } 28 | const imageNetMean = tf.tensor([-123.15, -115.90, -103.06]); 29 | return input.add(imageNetMean); 30 | }); 31 | } 32 | 33 | export class ResNet implements BaseModel { 34 | readonly model: tfconv.GraphModel; 35 | readonly outputStride: PoseNetOutputStride; 36 | 37 | constructor(model: tfconv.GraphModel, outputStride: PoseNetOutputStride) { 38 | this.model = model; 39 | const inputShape = 40 | this.model.inputs[0].shape as [number, number, number, number]; 41 | tf.util.assert( 42 | (inputShape[1] === -1) && (inputShape[2] === -1), 43 | () => `Input shape [${inputShape[1]}, ${inputShape[2]}] ` + 44 | `must both be equal to or -1`); 45 | this.outputStride = outputStride; 46 | } 47 | 48 | predict(input: tf.Tensor3D): {[key: string]: tf.Tensor3D} { 49 | return tf.tidy(() => { 50 | const asFloat = toFloatIfInt(input); 51 | const asBatch = asFloat.expandDims(0); 52 | const [displacementFwd4d, displacementBwd4d, offsets4d, heatmaps4d] = 53 | this.model.predict(asBatch) as tf.Tensor[]; 54 | 55 | const heatmaps = heatmaps4d.squeeze() as tf.Tensor3D; 56 | const heatmapScores = heatmaps.sigmoid(); 57 | const offsets = offsets4d.squeeze() as tf.Tensor3D; 58 | const displacementFwd = displacementFwd4d.squeeze() as tf.Tensor3D; 59 | const displacementBwd = displacementBwd4d.squeeze() as tf.Tensor3D; 60 | 61 | return { 62 | heatmapScores, 63 | offsets: offsets as tf.Tensor3D, 64 | displacementFwd: displacementFwd as tf.Tensor3D, 65 | displacementBwd: displacementBwd as tf.Tensor3D 66 | }; 67 | }); 68 | } 69 | 70 | dispose() { 71 | this.model.dispose(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/posenet/single_pose/argmax2d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | 20 | function mod(a: tf.Tensor1D, b: number): tf.Tensor1D { 21 | return tf.tidy(() => { 22 | const floored = a.div(tf.scalar(b, 'int32')); 23 | 24 | return a.sub(floored.mul(tf.scalar(b, 'int32'))); 25 | }); 26 | } 27 | 28 | export function argmax2d(inputs: tf.Tensor3D): tf.Tensor2D { 29 | const [height, width, depth] = inputs.shape; 30 | 31 | return tf.tidy(() => { 32 | const reshaped = inputs.reshape([height * width, depth]); 33 | const coords = reshaped.argMax(0) as tf.Tensor1D; 34 | 35 | const yCoords = 36 | coords.div(tf.scalar(width, 'int32')).expandDims(1) as tf.Tensor2D; 37 | const xCoords = mod(coords, width).expandDims(1) as tf.Tensor2D; 38 | 39 | return tf.concat([yCoords, xCoords], 1); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/posenet/single_pose/argmax2d_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * ============================================================================= 17 | */ 18 | 19 | import * as tf from '@tensorflow/tfjs-core'; 20 | 21 | import {argmax2d} from './argmax2d'; 22 | 23 | describe('argmax2d', () => { 24 | it('x = [2, 2, 1]', () => { 25 | const input = tf.tensor3d([1, 2, 0, 3], [2, 2, 1]); 26 | 27 | const result = argmax2d(input); 28 | 29 | const expectedResult = tf.tensor2d([1, 1], [1, 2], 'int32'); 30 | 31 | tf.test_util.expectArraysClose( 32 | result.dataSync(), expectedResult.dataSync()); 33 | }); 34 | 35 | it('x = [3, 3, 1]', () => { 36 | const input1 = tf.tensor3d([1, 2, 0, 3, 4, -1, 2, 9, 6], [3, 3, 1]); 37 | const input2 = 38 | tf.tensor3d([.5, .2, .9, 4.3, .2, .7, .6, -0.11, 1.4], [3, 3, 1]); 39 | 40 | tf.test_util.expectArraysClose( 41 | argmax2d(input1).dataSync(), 42 | tf.tensor2d([2, 1], [1, 2], 'int32').dataSync()); 43 | 44 | tf.test_util.expectArraysClose( 45 | argmax2d(input2).dataSync(), 46 | tf.tensor2d([1, 0], [1, 2], 'int32').dataSync()); 47 | }); 48 | 49 | it('x = [3, 3, 3]', () => { 50 | const input1 = tf.tensor3d([1, 2, 0, 3, 4, -1, 2, 9, 6], [3, 3, 1]); 51 | const input2 = 52 | tf.tensor3d([.5, .2, .9, 4.3, .2, .7, .6, -.11, 1.4], [3, 3, 1]); 53 | const input3 = tf.tensor3d([4, .2, .8, .1, 6, .6, .3, 11, .6], [3, 3, 1]); 54 | const input = tf.concat([input1, input2, input3], 2); 55 | 56 | const result = argmax2d(input); 57 | 58 | const expectedResult = tf.tensor2d([2, 1, 1, 0, 2, 1], [3, 2], 'int32'); 59 | 60 | tf.test_util.expectArraysClose( 61 | result.dataSync(), expectedResult.dataSync()); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/lib/posenet/single_pose/decode_single_pose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | 20 | import {partNames} from '../keypoints'; 21 | import {PoseNetOutputStride} from '../posenet_model'; 22 | import {Keypoint, Pose} from '../types'; 23 | import {toTensorBuffer} from '../util'; 24 | 25 | import {argmax2d} from './argmax2d'; 26 | import {getOffsetPoints, getPointsConfidence} from './util'; 27 | 28 | /** 29 | * Detects a single pose and finds its parts from part scores and offset 30 | * vectors. It returns a single pose detection. It works as follows: 31 | * argmax2d is done on the scores to get the y and x index in the heatmap 32 | * with the highest score for each part, which is essentially where the 33 | * part is most likely to exist. This produces a tensor of size 17x2, with 34 | * each row being the y and x index in the heatmap for each keypoint. 35 | * The offset vector for each for each part is retrieved by getting the 36 | * y and x from the offsets corresponding to the y and x index in the 37 | * heatmap for that part. This produces a tensor of size 17x2, with each 38 | * row being the offset vector for the corresponding keypoint. 39 | * To get the keypoint, each part’s heatmap y and x are multiplied 40 | * by the output stride then added to their corresponding offset vector, 41 | * which is in the same scale as the original image.  42 | * 43 | * @param heatmapScores 3-D tensor with shape `[height, width, numParts]`. 44 | * The value of heatmapScores[y, x, k]` is the score of placing the `k`-th 45 | * object part at position `(y, x)`. 46 | * 47 | * @param offsets 3-D tensor with shape `[height, width, numParts * 2]`. 48 | * The value of [offsets[y, x, k], offsets[y, x, k + numParts]]` is the 49 | * short range offset vector of the `k`-th object part at heatmap 50 | * position `(y, x)`. 51 | * 52 | * @param outputStride The output stride that was used when feed-forwarding 53 | * through the PoseNet model. Must be 32, 16, or 8. 54 | * 55 | * @return A promise that resolves with single pose with a confidence score, 56 | * which contains an array of keypoints indexed by part id, each with a score 57 | * and position. 58 | */ 59 | export async function decodeSinglePose( 60 | heatmapScores: tf.Tensor3D, offsets: tf.Tensor3D, 61 | outputStride: PoseNetOutputStride): Promise { 62 | let totalScore = 0.0; 63 | 64 | const heatmapValues = argmax2d(heatmapScores); 65 | 66 | const [scoresBuffer, offsetsBuffer, heatmapValuesBuffer] = await Promise.all([ 67 | toTensorBuffer(heatmapScores), toTensorBuffer(offsets), 68 | toTensorBuffer(heatmapValues, 'int32') 69 | ]); 70 | 71 | const offsetPoints = 72 | getOffsetPoints(heatmapValuesBuffer, outputStride, offsetsBuffer); 73 | const offsetPointsBuffer = await toTensorBuffer(offsetPoints); 74 | 75 | const keypointConfidence = 76 | Array.from(getPointsConfidence(scoresBuffer, heatmapValuesBuffer)); 77 | 78 | const keypoints = keypointConfidence.map((score, keypointId): Keypoint => { 79 | totalScore += score; 80 | return { 81 | position: { 82 | y: offsetPointsBuffer.get(keypointId, 0), 83 | x: offsetPointsBuffer.get(keypointId, 1) 84 | }, 85 | part: partNames[keypointId], 86 | score 87 | }; 88 | }); 89 | 90 | heatmapValues.dispose(); 91 | offsetPoints.dispose(); 92 | 93 | console.log({keypoints, score: totalScore / keypoints.length}); 94 | return {keypoints, score: totalScore / keypoints.length}; 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/posenet/single_pose/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | import {NUM_KEYPOINTS} from '../keypoints'; 20 | import {Vector2D} from '../types'; 21 | 22 | export function getPointsConfidence( 23 | heatmapScores: tf.TensorBuffer, 24 | heatMapCoords: tf.TensorBuffer): Float32Array { 25 | const numKeypoints = heatMapCoords.shape[0]; 26 | const result = new Float32Array(numKeypoints); 27 | 28 | for (let keypoint = 0; keypoint < numKeypoints; keypoint++) { 29 | const y = heatMapCoords.get(keypoint, 0); 30 | const x = heatMapCoords.get(keypoint, 1); 31 | result[keypoint] = heatmapScores.get(y, x, keypoint); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | function getOffsetPoint( 38 | y: number, x: number, keypoint: number, 39 | offsetsBuffer: tf.TensorBuffer): Vector2D { 40 | return { 41 | y: offsetsBuffer.get(y, x, keypoint), 42 | x: offsetsBuffer.get(y, x, keypoint + NUM_KEYPOINTS) 43 | }; 44 | } 45 | 46 | export function getOffsetVectors( 47 | heatMapCoordsBuffer: tf.TensorBuffer, 48 | offsetsBuffer: tf.TensorBuffer): tf.Tensor2D { 49 | const result: number[] = []; 50 | 51 | for (let keypoint = 0; keypoint < NUM_KEYPOINTS; keypoint++) { 52 | const heatmapY = heatMapCoordsBuffer.get(keypoint, 0).valueOf(); 53 | const heatmapX = heatMapCoordsBuffer.get(keypoint, 1).valueOf(); 54 | 55 | const {x, y} = getOffsetPoint(heatmapY, heatmapX, keypoint, offsetsBuffer); 56 | 57 | result.push(y); 58 | result.push(x); 59 | } 60 | 61 | return tf.tensor2d(result, [NUM_KEYPOINTS, 2]); 62 | } 63 | 64 | export function getOffsetPoints( 65 | heatMapCoordsBuffer: tf.TensorBuffer, outputStride: number, 66 | offsetsBuffer: tf.TensorBuffer): tf.Tensor2D { 67 | return tf.tidy(() => { 68 | const offsetVectors = getOffsetVectors(heatMapCoordsBuffer, offsetsBuffer); 69 | 70 | return heatMapCoordsBuffer.toTensor() 71 | .mul(tf.scalar(outputStride, 'int32')) 72 | .toFloat() 73 | .add(offsetVectors); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/posenet/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | 20 | export declare type Vector2D = { 21 | y: number, 22 | x: number 23 | }; 24 | 25 | export declare type Part = { 26 | heatmapX: number, 27 | heatmapY: number, 28 | id: number 29 | }; 30 | 31 | export declare type PartWithScore = { 32 | score: number, 33 | part: Part 34 | }; 35 | 36 | export declare type Keypoint = { 37 | score: number, 38 | position: Vector2D, 39 | part: string 40 | }; 41 | 42 | export declare type Pose = { 43 | keypoints: Keypoint[], 44 | score: number, 45 | }; 46 | 47 | export type PosenetInput = 48 | ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|tf.Tensor3D; 49 | 50 | export type TensorBuffer3D = tf.TensorBuffer; 51 | 52 | export declare interface Padding { 53 | top: number, bottom: number, left: number, right: number 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/posenet/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import * as tf from '@tensorflow/tfjs-core'; 19 | 20 | import {connectedPartIndices} from './keypoints'; 21 | import {PoseNetOutputStride} from './posenet_model'; 22 | import {Keypoint, Padding, Pose, PosenetInput, TensorBuffer3D, Vector2D} from './types'; 23 | 24 | function eitherPointDoesntMeetConfidence( 25 | a: number, b: number, minConfidence: number): boolean { 26 | return (a < minConfidence || b < minConfidence); 27 | } 28 | 29 | export function getAdjacentKeyPoints( 30 | keypoints: Keypoint[], minConfidence: number): Keypoint[][] { 31 | return connectedPartIndices.reduce( 32 | (result: Keypoint[][], [leftJoint, rightJoint]): Keypoint[][] => { 33 | if (eitherPointDoesntMeetConfidence( 34 | keypoints[leftJoint].score, keypoints[rightJoint].score, 35 | minConfidence)) { 36 | return result; 37 | } 38 | 39 | result.push([keypoints[leftJoint], keypoints[rightJoint]]); 40 | 41 | return result; 42 | }, []); 43 | } 44 | 45 | const {NEGATIVE_INFINITY, POSITIVE_INFINITY} = Number; 46 | export function getBoundingBox(keypoints: Keypoint[]): 47 | {maxX: number, maxY: number, minX: number, minY: number} { 48 | return keypoints.reduce(({maxX, maxY, minX, minY}, {position: {x, y}}) => { 49 | return { 50 | maxX: Math.max(maxX, x), 51 | maxY: Math.max(maxY, y), 52 | minX: Math.min(minX, x), 53 | minY: Math.min(minY, y) 54 | }; 55 | }, { 56 | maxX: NEGATIVE_INFINITY, 57 | maxY: NEGATIVE_INFINITY, 58 | minX: POSITIVE_INFINITY, 59 | minY: POSITIVE_INFINITY 60 | }); 61 | } 62 | 63 | export function getBoundingBoxPoints(keypoints: Keypoint[]): Vector2D[] { 64 | const {minX, minY, maxX, maxY} = getBoundingBox(keypoints); 65 | return [ 66 | {x: minX, y: minY}, {x: maxX, y: minY}, {x: maxX, y: maxY}, 67 | {x: minX, y: maxY} 68 | ]; 69 | } 70 | 71 | export async function toTensorBuffer( 72 | tensor: tf.Tensor, 73 | type: 'float32'|'int32' = 'float32'): Promise> { 74 | const tensorData = await tensor.data(); 75 | 76 | return tf.buffer(tensor.shape, type, tensorData as Float32Array) as 77 | tf.TensorBuffer; 78 | } 79 | 80 | export async function toTensorBuffers3D(tensors: tf.Tensor3D[]): 81 | Promise { 82 | return Promise.all(tensors.map(tensor => toTensorBuffer(tensor, 'float32'))); 83 | } 84 | 85 | export function scalePose( 86 | pose: Pose, scaleY: number, scaleX: number, offsetY = 0, 87 | offsetX = 0): Pose { 88 | return { 89 | score: pose.score, 90 | keypoints: pose.keypoints.map(({score, part, position}) => ({ 91 | score, 92 | part, 93 | position: { 94 | x: position.x * scaleX + offsetX, 95 | y: position.y * scaleY + offsetY 96 | } 97 | })) 98 | }; 99 | } 100 | 101 | export function scalePoses( 102 | poses: Pose[], scaleY: number, scaleX: number, offsetY = 0, offsetX = 0) { 103 | if (scaleX === 1 && scaleY === 1 && offsetY === 0 && offsetX === 0) { 104 | return poses; 105 | } 106 | return poses.map(pose => scalePose(pose, scaleY, scaleX, offsetY, offsetX)); 107 | } 108 | 109 | export function flipPoseHorizontal(pose: Pose, imageWidth: number): Pose { 110 | return { 111 | score: pose.score, 112 | keypoints: pose.keypoints.map( 113 | ({score, part, position}) => ({ 114 | score, 115 | part, 116 | position: {x: imageWidth - 1 - position.x, y: position.y} 117 | })) 118 | }; 119 | } 120 | 121 | export function flipPosesHorizontal(poses: Pose[], imageWidth: number) { 122 | if (imageWidth <= 0) { 123 | return poses; 124 | } 125 | return poses.map(pose => flipPoseHorizontal(pose, imageWidth)); 126 | } 127 | 128 | export function getValidResolution( 129 | imageScaleFactor: number, inputDimension: number, 130 | outputStride: PoseNetOutputStride): number { 131 | const evenResolution = inputDimension * imageScaleFactor - 1; 132 | 133 | return evenResolution - (evenResolution % outputStride) + 1; 134 | } 135 | 136 | export function getInputTensorDimensions(input: PosenetInput): 137 | [number, number] { 138 | return input instanceof tf.Tensor ? [input.shape[0], input.shape[1]] : 139 | [input.height, input.width]; 140 | } 141 | 142 | export function toInputTensor(input: PosenetInput) { 143 | return input instanceof tf.Tensor ? input : tf.browser.fromPixels(input); 144 | } 145 | 146 | export function toResizedInputTensor( 147 | input: PosenetInput, resizeHeight: number, resizeWidth: number, 148 | flipHorizontal: boolean): tf.Tensor3D { 149 | return tf.tidy(() => { 150 | const imageTensor = toInputTensor(input); 151 | 152 | if (flipHorizontal) { 153 | return imageTensor.reverse(1).resizeBilinear([resizeHeight, resizeWidth]); 154 | } else { 155 | return imageTensor.resizeBilinear([resizeHeight, resizeWidth]); 156 | } 157 | }); 158 | } 159 | 160 | export function padAndResizeTo( 161 | input: PosenetInput, [targetH, targetW]: [number, number]): 162 | {resized: tf.Tensor3D, padding: Padding} { 163 | const [height, width] = getInputTensorDimensions(input); 164 | const targetAspect = targetW / targetH; 165 | const aspect = width / height; 166 | let [padT, padB, padL, padR] = [0, 0, 0, 0]; 167 | if (aspect < targetAspect) { 168 | // pads the width 169 | padT = 0; 170 | padB = 0; 171 | padL = Math.round(0.5 * (targetAspect * height - width)); 172 | padR = Math.round(0.5 * (targetAspect * height - width)); 173 | } else { 174 | // pads the height 175 | padT = Math.round(0.5 * ((1.0 / targetAspect) * width - height)); 176 | padB = Math.round(0.5 * ((1.0 / targetAspect) * width - height)); 177 | padL = 0; 178 | padR = 0; 179 | } 180 | 181 | const resized: tf.Tensor3D = tf.tidy(() => { 182 | let imageTensor = toInputTensor(input); 183 | imageTensor = tf.pad3d(imageTensor, [[padT, padB], [padL, padR], [0, 0]]); 184 | 185 | return imageTensor.resizeBilinear([targetH, targetW]); 186 | }) 187 | 188 | return { 189 | resized, padding: {top: padT, left: padL, right: padR, bottom: padB} 190 | } 191 | } 192 | 193 | export function scaleAndFlipPoses( 194 | poses: Pose[], [height, width]: [number, number], 195 | [inputResolutionHeight, inputResolutionWidth]: [number, number], 196 | padding: Padding, flipHorizontal: boolean): Pose[] { 197 | const scaleY = 198 | (height + padding.top + padding.bottom) / (inputResolutionHeight); 199 | const scaleX = 200 | (width + padding.left + padding.right) / (inputResolutionWidth); 201 | 202 | const scaledPoses = 203 | scalePoses(poses, scaleY, scaleX, -padding.top, -padding.left); 204 | 205 | if (flipHorizontal) { 206 | return flipPosesHorizontal(scaledPoses, width); 207 | } else { 208 | return scaledPoses; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/lib/posenet/util_test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google Inc. All Rights Reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * ============================================================================= 16 | */ 17 | 18 | import {getValidResolution} from './util'; 19 | 20 | describe('util.getValidResolution', () => { 21 | it('returns an odd value', () => { 22 | expect(getValidResolution(0.5, 545, 32) % 2).toEqual(1); 23 | expect(getValidResolution(0.5, 545, 16) % 2).toEqual(1); 24 | expect(getValidResolution(0.5, 545, 8) % 2).toEqual(1); 25 | expect(getValidResolution(0.845, 242, 8) % 2).toEqual(1); 26 | expect(getValidResolution(0.421, 546, 16) % 2).toEqual(1); 27 | }); 28 | 29 | it('returns a value that when 1 is subtracted by it is ' + 30 | 'divisible by the output stride', 31 | () => { 32 | const outputStride = 32; 33 | const imageSize = 562; 34 | 35 | const scaleFactor = 0.63; 36 | 37 | const resolution = 38 | getValidResolution(scaleFactor, imageSize, outputStride); 39 | 40 | expect((resolution - 1) % outputStride).toEqual(0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "attract": { 3 | "title": "Asteroid alert!", 4 | "body": "Position your body within the
frame. Raise your arms above
your head to take control of
the shields." 5 | }, 6 | "calibration": { 7 | "title": "Arm the shields", 8 | "body": "To continue,
activate all 7 shields
with your arms." 9 | }, 10 | "error": { 11 | "title": "Still there?", 12 | "body": "Make sure your whole
body is in the frame, and
no one else is visible." 13 | }, 14 | "no-video": { 15 | "title": "Video Unavailable", 16 | "body": "Camera access is required
to play this game. Please allow
camera access and refresh
this page." 17 | }, 18 | "generic-error": { 19 | "title": "Error Title", 20 | "body": "A brief and friendly
description of the problem, and suggestions for fixing it." 21 | }, 22 | "loading-pose-model" : "Loading pose
estimation model...", 23 | "share": { 24 | "title": "All clear!", 25 | "body": "The spacecraft
has reached a safe area.
Nice work.", 26 | "share-text": "Share the experience", 27 | "share-link": "g.co/PoseShield", 28 | "share-url": "https://g.co/PoseShield", 29 | "web-text": "I blocked asteroids and got ranked as playing Pose Shield! Check it out at g.co/PoseShield", 30 | "tweet-hashtag": "TFPoseShield,TensorFlow" 31 | }, 32 | "asteroids-blocked": "asteroids blocked", 33 | "ranks": { 34 | "level": "level", 35 | "first": "Expert", 36 | "second": "Pro", 37 | "third": "Apprentice", 38 | "fourth": "Novice" 39 | }, 40 | "links":{ 41 | "tensorflow": { 42 | "link": "TensorFlow.js", 43 | "url": "https://www.tensorflow.org/js" 44 | }, 45 | "posenet": { 46 | "link": "PoseNet", 47 | "url": "https://www.tensorflow.org/lite/models/pose_estimation/overview" 48 | } 49 | }, 50 | "gameplay-text":{ 51 | "tensorflow": "Tensorflow", 52 | "tensorflowjs": "Tensorflow.js", 53 | "posenet-model": "Posenet Model", 54 | "fps-title": "fps" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lottie.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Vue from 'vue'; 19 | import App from './App.vue'; 20 | import router from './router'; 21 | import store from './store'; 22 | import i18n from './i18n'; 23 | 24 | Vue.config.productionTip = false; 25 | 26 | new Vue({ 27 | router, 28 | store, 29 | i18n, 30 | render: function(h) { 31 | return h(App); 32 | }, 33 | }).$mount('#app'); 34 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Vue from 'vue'; 19 | import Router from 'vue-router'; 20 | 21 | Vue.use(Router); 22 | 23 | export default new Router({ 24 | routes: [ 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /src/scss/colors.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | $navy: #252e65; 3 | $share-navy: #0f193c; 4 | $dark-blue: #152760; 5 | $white: #fff; 6 | 7 | $svg-white-stroke: #fcfdff; 8 | $shields-light-orange: #ffa800; 9 | $shields-dark-orange: #ff6f00; 10 | $rgba-white-full: rgba(255, 255, 255, 1); 11 | $rgba-white-partial: rgba(255, 255, 255, 0.75); 12 | $rgba-white-transparent: rgba(255, 255, 255, 0); 13 | $rbga-navy-half: rgba(15, 25, 60, 0.5); 14 | $rbga-navy-mostly: rgba(15, 25, 60, 0.75); -------------------------------------------------------------------------------- /src/scss/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto Mono'; 3 | font-weight: 300; 4 | font-style: normal; 5 | src: url('~@/assets/fonts/RobotoMono-Light.ttf') format('truetype'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto Mono'; 10 | font-weight: 300; 11 | font-style: italic; 12 | src: url('~@/assets/fonts/RobotoMono-LightItalic.ttf') format('truetype'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto Mono'; 17 | font-weight: 400; 18 | font-style: normal; 19 | src: url('~@/assets/fonts/RobotoMono-Regular.ttf') format('truetype'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Roboto Mono'; 24 | font-weight: 400; 25 | font-style: italic; 26 | src: url('~@/assets/fonts/RobotoMono-RegularItalic.ttf') format('truetype'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Roboto Mono'; 31 | font-weight: 500; 32 | font-style: normal; 33 | src: url('~@/assets/fonts/RobotoMono-Medium.ttf') format('truetype'); 34 | } 35 | 36 | @font-face { 37 | font-family: 'Roboto Mono'; 38 | font-weight:500; 39 | font-style: italic; 40 | src: url('~@/assets/fonts/RobotoMono-MediumItalic.ttf') format('truetype'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Roboto Mono'; 45 | font-weight: 700; 46 | font-style: normal; 47 | src: url('~@/assets/fonts/RobotoMono-Bold.ttf') format('truetype'); 48 | } 49 | 50 | @font-face { 51 | font-family: 'Roboto Mono'; 52 | font-weight:700; 53 | font-style: italic; 54 | src: url('~@/assets/fonts/RobotoMono-BoldItalic.ttf') format('truetype'); 55 | } -------------------------------------------------------------------------------- /src/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import './colors'; 2 | @import './fonts'; -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * settings.js is used to control url-modifiable settings of the demo. 20 | * If you need to add additional modes or settings, this is the place 21 | * to start: 22 | * - modify the 'modes' object to add new sets of settings values, or to 23 | * adjust the default settings. 24 | * - modify the 'validSettings' object to add entirely new settings, along 25 | * with defining their valid types and arrays of supported values. 26 | * 27 | * Usage: 28 | * 29 | * import getSettings from './settings'; 30 | * const settings = getSettings(); 31 | */ 32 | 33 | /** 34 | * The modes object consists of key-value pairs, where each key is the name of 35 | * a mode the demo can be placed in and the value is another object which 36 | * defines the settings for that mode. You can change modes by providing 37 | * mode as a url parameter; if you do not, you will get the default settings. 38 | * 39 | * Note that modes aside from default are layered on top of the default 40 | * settings, so you don't need to redefine any settings you want to stay the 41 | * same. 42 | */ 43 | const modes = { 44 | 'default': { 45 | 'mode': 'default', 46 | 'sharing': true, 47 | 'share-qr': true, 48 | 'share-link': true, 49 | 'asteroid-group': 0, 50 | 'model-stride': 16, 51 | 'multiplier': .5, 52 | 'min-part-conf': .5, 53 | 'min-pose-conf': .5, 54 | 'input-res': 257, 55 | 'timeout': 15, 56 | 'speed': 'default', 57 | 'hide-cursor': false, 58 | }, 59 | 'kiosk': { 60 | 'mode': 'kiosk', 61 | 'timeout': 30, 62 | 'hide-cursor': true, 63 | 'multiplier': .75, 64 | 'input-res': 417, 65 | }, 66 | 'kiosk-noqr': { 67 | 'mode': 'kiosk-noqr', 68 | 'sharing': false, 69 | 'hide-cursor': true, 70 | 'multiplier': .75, 71 | 'input-res': 417, 72 | }, 73 | }; 74 | 75 | /** 76 | * The validSettings object consists of key-value paris. Each key defines the 77 | * name of a setting you can provide either by defining it in a mode or by 78 | * passing it as a url parameter. The value conists of an object with the 79 | * following properties: 80 | * - type: one of 'string', 'bool', 'int', 'float', or 81 | * 'confidence' (a float value from 0 to 1) 82 | * - valid: optional array of all valid values allowed by the parameter. 83 | */ 84 | const validSettings = { 85 | 'sharing': {type: 'bool'}, 86 | 'share-qr': {type: 'bool'}, 87 | 'share-link': {type: 'bool'}, 88 | 'hide-cursor': {type: 'bool'}, 89 | 'timeout': {type: 'int'}, 90 | 'speed': {type: 'string', validValues: ['relaxed', 'frantic']}, 91 | 'asteroid-group': {type: 'int', validValues: [0, 1, 2, 3, 4, 5]}, 92 | 'model-stride': {type: 'int', validValues: [8, 16]}, 93 | 'multiplier': {type: 'float', validValues: [.5, .75, 1]}, 94 | 'min-part-conf': {type: 'confidence'}, 95 | 'min-pose-conf': {type: 'confidence'}, 96 | 'input-res': { 97 | type: 'int', 98 | validValues: [161, 193, 257, 289, 321, 353, 385, 417, 99 | 449, 481, 513, 801, 1217], 100 | }, 101 | }; 102 | 103 | /** 104 | * getSettings function checks the url parameters for the mode and for each 105 | * of the valid settings found in the validSettings object, above. It returns 106 | * an object of settings and their values. The settings returned start with 107 | * the defaults, then apply any settings from the modes on top of those, 108 | * then apply any individual url paramter overrides. 109 | * 110 | * @return {Object} settings 111 | */ 112 | export default function getSettings() { 113 | const urlParams = new URLSearchParams(window.location.search); 114 | const overrides = {}; 115 | 116 | const entries = Object.entries(validSettings); 117 | for (const [setting, properties] of entries) { 118 | if (!urlParams.has(setting)) { 119 | continue; 120 | } 121 | 122 | const {type, validValues} = properties; 123 | const rawValue = urlParams.get(setting); 124 | let value; 125 | 126 | if (type === 'string') { 127 | value = rawValue; 128 | } else if (type === 'bool') { 129 | value = (rawValue == 'true' || rawValue == '1'); 130 | } else if (type === 'int') { 131 | value = parseInt(rawValue); 132 | } else if (type === 'float') { 133 | value = parseFloat(rawValue); 134 | } else if (type === 'confidence') { 135 | value = parseFloat(rawValue); 136 | } 137 | 138 | if (validValues && !validValues.includes(value)) { 139 | continue; 140 | } 141 | 142 | if (type === 'confidence' && (value < 0 || value > 1)) { 143 | continue; 144 | } 145 | 146 | overrides[setting] = value; 147 | } 148 | 149 | const rawMode = urlParams.get('mode'); 150 | const mode = (Object.keys(modes).includes(rawMode)) ? rawMode : 'default'; 151 | const settings = (mode === 'default') 152 | ? {...modes['default'], ...overrides} 153 | : {...modes['default'], ...modes[mode], ...overrides}; 154 | 155 | return settings; 156 | } 157 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Vue from 'vue'; 19 | import Vuex from 'vuex'; 20 | 21 | Vue.use(Vuex); 22 | 23 | let attractTimerId; 24 | const gameTimerSeconds = 60; 25 | 26 | export default new Vuex.Store({ 27 | state: { 28 | activeState: 'attract', 29 | gameState: 'inactive', 30 | hasPerson: false, 31 | poseActivated: false, 32 | calibrated: false, 33 | gameCompleted: false, 34 | videoFeed: false, 35 | poseData: {}, 36 | zoneDetectedLoaded: false, 37 | zoneDetectedInstance: false, 38 | zoneDetectedOne: false, 39 | zoneDetectedTwo: false, 40 | zoneDetectedThree: false, 41 | zoneDetectedFour: false, 42 | zoneDetectedFive: false, 43 | zoneDetectedSix: false, 44 | zoneDetectedSeven: false, 45 | debugBar: false, 46 | demoMode: true, 47 | videoToggle: false, 48 | displayZones: false, 49 | gameRank: 'first', 50 | gameScore: 1.0, 51 | gameTime: gameTimerSeconds, 52 | gameTimeCountDown: gameTimerSeconds, 53 | gameCountDownToPlay: 3, 54 | asteroidCounter: 30, 55 | asteroidTotalCount: 30, 56 | asteroidHits: 0, 57 | asteroidBlocks: 30, 58 | asteroidBlocksText: '30', 59 | share_qr: true, 60 | share_link: true, 61 | asteroidGroup: 0, 62 | modelStride: 16, 63 | minPartConfidence: .5, 64 | minPoseConfidence: .5, 65 | inputResolution: 257, 66 | multiplier: .5, 67 | noVideo: false, 68 | errorState: false, 69 | globalTimeout: 15000, 70 | mode: 'default', 71 | speed: 'default', 72 | hideCursor: false, 73 | }, 74 | getters: { 75 | activeState: (state) => { 76 | state.activeState; 77 | }, 78 | hasPerson: (state) => { 79 | state.hasPerson; 80 | }, 81 | gameState: (state) => { 82 | state.gameState; 83 | }, 84 | poseActivated: (state) => { 85 | state.poseActivated; 86 | }, 87 | calibrated: (state) => { 88 | state.calibrated; 89 | }, 90 | gameCompleted: (state) => { 91 | state.gameCompleted; 92 | }, 93 | videoFeed: (state) => { 94 | state.videoFeed; 95 | }, 96 | zoneDetectedInstance: (state) => { 97 | state.zoneDetectedInstance; 98 | }, 99 | zoneDetectedOne: (state) => { 100 | state.zoneDetectedOne; 101 | }, 102 | zoneDetectedTwo: (state) => { 103 | state.zoneDetectedTwo; 104 | }, 105 | zoneDetectedThree: (state) => { 106 | state.zoneDetectedThree; 107 | }, 108 | zoneDetectedFour: (state) => { 109 | state.zoneDetectedFour; 110 | }, 111 | zoneDetectedFive: (state) => { 112 | state.zoneDetectedFive; 113 | }, 114 | zoneDetectedSix: (state) => { 115 | state.zoneDetectedSix; 116 | }, 117 | zoneDetectedSeven: (state) => { 118 | state.zoneDetectedSeven; 119 | }, 120 | gameScore: (state) => { 121 | state.gameScore; 122 | }, 123 | asteroidTotalCount: (state) => { 124 | state.asteroidTotalCount; 125 | }, 126 | asteroidCounter: (state) => { 127 | state.asteroidCounter; 128 | }, 129 | asteroidHits: (state) => { 130 | state.asteroidHits; 131 | }, 132 | }, 133 | mutations: { 134 | // Application state manager 135 | setStates(state) { 136 | if (state.noVideo === true) { 137 | // User has blocked camera permissions 138 | // change state to no-video 139 | state.activeState = 'no-video'; 140 | } else if (state.errorState == true) { 141 | // General error state 142 | state.activeState = 'generic-error'; 143 | } else { 144 | if (state.activeState === 'attract' && 145 | state.hasPerson && state.poseActivated && !state.gameCompleted) { 146 | // Person Detected, Pose Detected, Not Calibrated 147 | // and Not Game Complete - 148 | // changing state to calibration 149 | state.activeState = 'calibration'; 150 | } else if (state.activeState === 'calibration' && state.hasPerson && 151 | state.poseActivated && state.calibrated && !state.gameCompleted) { 152 | // Person Detected, Pose Detected, Calibration Complete 153 | // and Not Game Complete - 154 | // changing state to game 155 | state.activeState = 'game'; 156 | } else if (state.activeState === 'game' && state.poseActivated && 157 | state.calibrated && !state.gameCompleted) { 158 | // In game state - toggling between three states; 159 | // Error, Countdown, Playing 160 | if (state.gameCountDownToPlay !== 0) { 161 | // Game state - Need to Countdown 162 | // Setting Countdown state 163 | state.gameState = 'countdown'; 164 | } else if (!state.hasPerson) { 165 | // Game state - No person detected - Error state 166 | state.gameState = 'error'; 167 | } else { 168 | // Game state - Person Detected - Countdown is zero - Playing etate 169 | state.gameState = 'playing'; 170 | } 171 | } else if (state.activeState === 'game' && state.hasPerson && 172 | state.poseActivated && state.calibrated && state.gameCompleted) { 173 | // Person Detected, Pose Detected, Calibration Completed, 174 | // Game Completed - 175 | // changing state to share 176 | state.activeState = 'share'; 177 | state.gameState = 'inactive'; 178 | } else if (state.activeState === 'attract') { 179 | // Application is in attract 180 | // resetting application states 181 | setTimeout(function() { 182 | state.asteroidBlocksText = '30'; 183 | state.asteroidBlocks = 30; 184 | state.asteroidCounter = 30; 185 | 186 | if (state.speed == 'frantic') { 187 | state.asteroidCounter = 60; 188 | state.asteroidBlocks = 60; 189 | state.asteroidBlocksText = '60'; 190 | } else if (state.speed == 'relaxed') { 191 | state.asteroidCounter = 15; 192 | state.asteroidBlocks = 15; 193 | state.asteroidBlocksText = '15'; 194 | } 195 | 196 | state.asteroidHits = 0; 197 | state.gameScore = 1; 198 | state.gameRank = 'first'; 199 | state.gameTimeCountDown = state.gameTime; 200 | state.gameCountDownToPlay = 3; 201 | }, 500); 202 | } 203 | } 204 | }, 205 | 206 | // Toggling if a person/pose is detected 207 | setTogglePersonDetected(state, data) { 208 | state.hasPerson = data; 209 | }, 210 | 211 | // Lost person/pose timeout to reset application if no person/pose 212 | // within a timeframe set in the environments variable 213 | setHasPerson(state) { 214 | if (!state.hasPerson && !state.gameCompleted) { 215 | if (attractTimerId) clearTimeout(attractTimerId); 216 | // No person detected - setting attract timeout 217 | attractTimerId = setTimeout(function() { 218 | // Resetting - application state to Attract 219 | state.activeState = 'attract'; 220 | state.gameState = 'inactive'; 221 | state.calibrated = false; 222 | state.gameCompleted = false; 223 | state.poseActivated = false; 224 | }, state.globalTimeout); 225 | } else if (state.hasPerson && !state.gameCompleted) { 226 | // Person Detected - clearing attract timeout 227 | if (attractTimerId) clearTimeout(attractTimerId); 228 | } 229 | }, 230 | 231 | // Game complete timeout to reset application after game completion 232 | setGameCompletedTimeout(state) { 233 | if (state.gameCompleted) { 234 | if (attractTimerId) clearTimeout(attractTimerId); 235 | // Game Completed - setting application reset timer 236 | attractTimerId = setTimeout(function() { 237 | // Reset all states application to attract/intro state 238 | state.activeState = 'attract'; 239 | state.gameState = 'inactive'; 240 | state.calibrated = false; 241 | state.gameCompleted = false; 242 | state.poseActivated = false; 243 | }, state.globalTimeout); 244 | } else if (!state.gameCompleted) { 245 | if (attractTimerId) clearTimeout(attractTimerId); 246 | } 247 | }, 248 | 249 | // Set game completed to true 250 | setGameCompleted(state) { 251 | state.gameCompleted = true; 252 | }, 253 | 254 | // Set the video feed to a global state 255 | setVideoFeed(state, data) { 256 | state.videoFeed = data; 257 | }, 258 | 259 | // Update pose activation 260 | setPoseActivated(state, data) { 261 | Vue.set(state, 'poseActivated', data); 262 | }, 263 | 264 | // Update calibration status so game play can begin 265 | setCalibrated(state, data) { 266 | state.calibrated = data; 267 | }, 268 | 269 | // Update pose data 270 | setPoseData(state, data) { 271 | Vue.set(state, 'poseData', data); 272 | }, 273 | 274 | // Set zoneDetectedLoaded 275 | setZoneDetectedLoaded(state, data) { 276 | state.zoneDetectedLoaded = data; 277 | }, 278 | 279 | // Set the ZoneDetectedInstance 280 | setZoneDetectedInstance(state, data) { 281 | state.zoneDetectedInstance = data; 282 | }, 283 | 284 | // Toggling zone state 285 | toggleZoneDetectedOne(state, data) { 286 | state.zoneDetectedOne = data; 287 | }, 288 | 289 | toggleZoneDetectedTwo(state, data) { 290 | state.zoneDetectedTwo = data; 291 | }, 292 | 293 | toggleZoneDetectedThree(state, data) { 294 | state.zoneDetectedThree = data; 295 | }, 296 | 297 | toggleZoneDetectedFour(state, data) { 298 | state.zoneDetectedFour = data; 299 | }, 300 | 301 | toggleZoneDetectedFive(state, data) { 302 | state.zoneDetectedFive = data; 303 | }, 304 | 305 | toggleZoneDetectedSix(state, data) { 306 | state.zoneDetectedSix = data; 307 | }, 308 | 309 | toggleZoneDetectedSeven(state, data) { 310 | state.zoneDetectedSeven = data; 311 | }, 312 | 313 | asteroidCollision(state, data) { 314 | if (state.asteroidCounter > 0) { 315 | if (data === 'hit') { 316 | state.asteroidHits++; 317 | state.asteroidBlocks--; 318 | 319 | state.asteroidBlocksText = 320 | (state.asteroidBlocks < 10) ? 321 | '0' + state.asteroidBlocks.toString() : 322 | state.asteroidBlocks.toString(); 323 | 324 | state.gameScore = 325 | Number((state.asteroidTotalCount - state.asteroidHits) / 326 | state.asteroidTotalCount); 327 | 328 | if (state.gameScore == 0) state.gameScore = 0.01; 329 | 330 | if (state.gameScore >= 0.75) { 331 | state.gameRank = 'first'; 332 | } else if (state.gameScore >= 0.5) { 333 | state.gameRank = 'second'; 334 | } else if (state.gameScore >= 0.25) { 335 | state.gameRank = 'third'; 336 | } else { 337 | state.gameRank = 'fourth'; 338 | } 339 | } 340 | 341 | state.asteroidCounter--; 342 | } 343 | }, 344 | }, 345 | actions: {}, 346 | }); 347 | -------------------------------------------------------------------------------- /src/views/mainLayout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | module.exports = { 19 | plugins: [ 20 | 'cypress' 21 | ], 22 | env: { 23 | mocha: true, 24 | 'cypress/globals': true 25 | }, 26 | rules: { 27 | strict: 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | // https://docs.cypress.io/guides/guides/plugins-guide.html 20 | 21 | // if you need a custom webpack configuration you can uncomment the following import 22 | // and then use the `file:preprocessor` event 23 | // as explained in the cypress docs 24 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 25 | 26 | /* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */ 27 | // const webpack = require('@cypress/webpack-preprocessor') 28 | 29 | module.exports = (on, config) => { 30 | // on('file:preprocessor', webpack({ 31 | // webpackOptions: require('@vue/cli-service/webpack.config'), 32 | // watchOptions: {} 33 | // })) 34 | 35 | return Object.assign({}, config, { 36 | fixturesFolder: 'tests/e2e/fixtures', 37 | integrationFolder: 'tests/e2e/specs', 38 | screenshotsFolder: 'tests/e2e/screenshots', 39 | videosFolder: 'tests/e2e/videos', 40 | supportFile: 'tests/e2e/support/index.js' 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | // https://docs.cypress.io/api/introduction/api.html 20 | 21 | describe('My First Test', () => { 22 | it('Visits the app root url', () => { 23 | cy.visit('/') 24 | cy.contains('h1', 'Welcome to Your Vue.js App') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | // *********************************************** 20 | // This example commands.js shows you how to 21 | // create various custom commands and overwrite 22 | // existing commands. 23 | // 24 | // For more comprehensive examples of custom 25 | // commands please read more here: 26 | // https://on.cypress.io/custom-commands 27 | // *********************************************** 28 | // 29 | // 30 | // -- This is a parent command -- 31 | // Cypress.Commands.add("login", (email, password) => { ... }) 32 | // 33 | // 34 | // -- This is a child command -- 35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 36 | // 37 | // 38 | // -- This is a dual command -- 39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 40 | // 41 | // 42 | // -- This is will overwrite an existing command -- 43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 44 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // *********************************************************** 19 | // This example support/index.js is processed and 20 | // loaded automatically before your test files. 21 | // 22 | // This is a great place to put global configuration and 23 | // behavior that modifies Cypress. 24 | // 25 | // You can change the location of this file or turn off 26 | // automatically serving support files with the 27 | // 'supportFile' configuration option. 28 | // 29 | // You can read more here: 30 | // https://on.cypress.io/configuration 31 | // *********************************************************** 32 | 33 | // Import commands.js using ES2015 syntax: 34 | import './commands' 35 | 36 | // Alternatively you can use CommonJS syntax: 37 | // require('./commands') 38 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | module.exports = { 20 | env: { 21 | jest: true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | import { shallowMount } from '@vue/test-utils' 20 | import HelloWorld from '@/components/HelloWorld.vue' 21 | 22 | describe('HelloWorld.vue', () => { 23 | it('renders props.msg when passed', () => { 24 | const msg = 'new message' 25 | const wrapper = shallowMount(HelloWorld, { 26 | propsData: { msg } 27 | }) 28 | expect(wrapper.text()).toMatch(msg) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | module.exports = { 19 | publicPath: '/play/', 20 | css: { 21 | loaderOptions: { 22 | sass: { 23 | data: `@import "@/scss/global.scss";`, 24 | }, 25 | }, 26 | }, 27 | pluginOptions: { 28 | i18n: { 29 | locale: 'en', 30 | fallbackLocale: 'en', 31 | localeDir: 'locales', 32 | enableInSFC: false, 33 | }, 34 | }, 35 | 36 | chainWebpack: (config) => { 37 | const svgRule = config.module.rule('svg'); 38 | 39 | svgRule.uses.clear(); 40 | 41 | svgRule.use('vue-svg-loader') 42 | .loader('vue-svg-loader'); 43 | // Note that it's possible to modify the svg compression settings as 44 | // shown below; available options can be found here: 45 | // https://github.com/svg/svgo 46 | // .options({ 47 | // svgo: { 48 | // plugins: [ 49 | // { removeStyleElement: true }, 50 | // { removeUselessDefs: false }, 51 | // { prefixIds: true }, 52 | // { inlineStyles: false }, 53 | // { convertStyleToAttrs: true }, 54 | // ], 55 | // }, 56 | // }); 57 | }, 58 | }; 59 | --------------------------------------------------------------------------------