├── .cfignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── doc ├── cloud-deploy.md └── images │ ├── app-classify.png │ ├── app-predictions.png │ └── arch-diagram.png ├── download_model.py ├── manifest.yml ├── model_info.txt ├── package.json ├── public ├── images │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── icon-192.png │ └── icon-512.png ├── index.html ├── manifest.json └── model │ └── README.md ├── server.js ├── src ├── App.css ├── App.js ├── App.test.js ├── Routes.js ├── components │ ├── AlertDismissable.js │ └── LoadButton.js ├── config.js ├── index.css ├── index.js ├── model │ └── classes.js ├── pages │ ├── About.css │ ├── About.js │ ├── Classify.css │ ├── Classify.js │ ├── NotFound.css │ └── NotFound.js └── serviceWorker.js └── yarn.lock /.cfignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.DS_Store 3 | README.md 4 | .github/ 5 | .git/ 6 | .gitignore 7 | logs 8 | *.log 9 | doc/ 10 | *.h5 11 | my-model/ 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react" 4 | ], 5 | "parserOptions": { 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "react-app" 13 | ], 14 | "rules": { 15 | "semi": 2, 16 | "no-trailing-spaces": 2, 17 | "max-len": ["error", { "code": 100 }], 18 | "no-console": 1, 19 | "quotes": ["error", "single"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # model files 64 | public/model/model.json 65 | public/model/*shard* 66 | 67 | .DS_Store 68 | dist/ 69 | build/ 70 | *.h5 71 | my-model/ 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | script: 5 | - yarn lint 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This is an open source project, and we appreciate your help! 4 | 5 | We use the GitHub issue tracker to discuss new features and non-trivial bugs. 6 | 7 | In addition to the issue tracker, [#journeys on 8 | Slack](https://dwopen.slack.com) is the best way to get into contact with the 9 | project's maintainers. 10 | 11 | To contribute code, documentation, or tests, please submit a pull request to 12 | the GitHub repository. Generally, we expect two maintainers to review your pull 13 | request before it is approved for merging. For more details, see the 14 | [MAINTAINERS](MAINTAINERS.md) page. 15 | 16 | Contributions are subject to the [Developer Certificate of Origin, Version 1.1](https://developercertificate.org/) and the [Apache License, Version 2](https://www.apache.org/licenses/LICENSE-2.0.txt). 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for maintainers - anybody with commit access to one or 4 | more Code Pattern repositories. 5 | 6 | ## Methodology 7 | 8 | This repository does not have a traditional release management cycle, but 9 | should instead be maintained as a useful, working, and polished reference at 10 | all times. While all work can therefore be focused on the master branch, the 11 | quality of this branch should never be compromised. 12 | 13 | The remainder of this document details how to merge pull requests to the 14 | repositories. 15 | 16 | ## Merge approval 17 | 18 | The project maintainers use LGTM (Looks Good To Me) in comments on the pull 19 | request to indicate acceptance prior to merging. A change requires LGTMs from 20 | two project maintainers. If the code is written by a maintainer, the change 21 | only requires one additional LGTM. 22 | 23 | ## Reviewing Pull Requests 24 | 25 | We recommend reviewing pull requests directly within GitHub. This allows a 26 | public commentary on changes, providing transparency for all users. When 27 | providing feedback be civil, courteous, and kind. Disagreement is fine, so long 28 | as the discourse is carried out politely. If we see a record of uncivil or 29 | abusive comments, we will revoke your commit privileges and invite you to leave 30 | the project. 31 | 32 | During your review, consider the following points: 33 | 34 | ### Does the change have positive impact? 35 | 36 | Some proposed changes may not represent a positive impact to the project. Ask 37 | whether or not the change will make understanding the code easier, or if it 38 | could simply be a personal preference on the part of the author (see 39 | [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding)). 40 | 41 | Pull requests that do not have a clear positive impact should be closed without 42 | merging. 43 | 44 | ### Do the changes make sense? 45 | 46 | If you do not understand what the changes are or what they accomplish, ask the 47 | author for clarification. Ask the author to add comments and/or clarify test 48 | case names to make the intentions clear. 49 | 50 | At times, such clarification will reveal that the author may not be using the 51 | code correctly, or is unaware of features that accommodate their needs. If you 52 | feel this is the case, work up a code sample that would address the pull 53 | request for them, and feel free to close the pull request once they confirm. 54 | 55 | ### Does the change introduce a new feature? 56 | 57 | For any given pull request, ask yourself "is this a new feature?" If so, does 58 | the pull request (or associated issue) contain narrative indicating the need 59 | for the feature? If not, ask them to provide that information. 60 | 61 | Are new unit tests in place that test all new behaviors introduced? If not, do 62 | not merge the feature until they are! Is documentation in place for the new 63 | feature? (See the documentation guidelines). If not do not merge the feature 64 | until it is! Is the feature necessary for general use cases? Try and keep the 65 | scope of any given component narrow. If a proposed feature does not fit that 66 | scope, recommend to the user that they maintain the feature on their own, and 67 | close the request. You may also recommend that they see if the feature gains 68 | traction among other users, and suggest they re-submit when they can show such 69 | support. 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.travis-ci.com/IBM/tfjs-web-app.svg?branch=master)](https://travis-ci.com/IBM/tfjs-web-app) 2 | 3 | # Create a progressive web application for offline image classification 4 | 5 | After creating deep learning models, users typically want to deploy their trained models to be used 6 | in their applications. There are several ways to do this, and how users do it depends largely on 7 | their use cases and requirements. One such requirement is the ability to run a model offline in 8 | areas where Internet connectivity may be sparse or nonexistent. To do this, one solution is to 9 | create native apps for mobile platforms which will package and load a compressed version of their 10 | models. However, this has the overhead of needing developers with expertise in Android and iOS 11 | development. 12 | 13 | Here, we go over an alternative, easier way to satisfy this offline mobile 14 | requirement by 15 | creating a [progressive web application](https://developers.google.com/web/progressive-web-apps/) 16 | with our model using React and TensorFlow.js. Progressive web applications (PWAs) give a native 17 | app-like feel and can run on most modern web browsers. This makes cross-platform development much 18 | easier as the application only has to be developed once in HTML/JavaScript. Furthermore, through 19 | the use of [service workers](https://developers.google.com/web/fundamentals/primers/service-workers/), 20 | PWAs can provide fully offline functionality. 21 | 22 | With [TensorFlow.js](https://www.tensorflow.org/js), we can convert our pre-trained TensorFlow or 23 | Keras models into JavaScript to be run in the browser through the app! 24 | 25 | In then end, we will have a cross-platform application where users can classify 26 | images selected locally or taken with their device's camera. The app uses TensorFlow.js and a 27 | pre-trained model converted to the TensorFlow.js format to provide the inference capabilities. 28 | This model is saved locally in the browser using IndexedDB, and a service worker is used to 29 | provide offline capabilities. 30 | 31 | ![architecture](doc/images/arch-diagram.png) 32 | 33 | ## Flow 34 | 35 | 1. A pre-trained Keras/TensorFlow model is converted to the TensorFlow.js web friendly format and 36 | integrated with app. 37 | 2. User launches progressive web application. 38 | 3. App assets and TensorFlow.js model files are downloaded from the web. 39 | 4. Assets and model are stored locally using browser cache and IndexedDB storage. 40 | 5. User takes photo with device camera or selects local image. 41 | 6. Image is sent through the model for inference and top predictions are given. 42 | 43 | 44 | ## Included Components 45 | 46 | * [React](https://reactjs.org/): A JavaScript library for building user interfaces. 47 | * [TensorFlow.js](https://js.tensorflow.org/): A JavaScript library for training and deploying ML 48 | models in the browser and on Node.js. 49 | 50 | ## Featured Technologies 51 | 52 | * [Deep Learning](https://developer.ibm.com/technologies/deep-learning/): Subset of AI that uses 53 | multi-layers neural networks that learn from lots of data. 54 | * [Mobile](https://developer.ibm.com/technologies/mobile/): An environment to 55 | develop apps and enable engagements that are designed specifically for mobile 56 | users. 57 | * [Web Development](https://developer.ibm.com/technologies/web-development/): The construction of 58 | modern web apps using open-standards technologies. 59 | * [Visual Recognition](https://developer.ibm.com/technologies/vision/): Tag, classify, and train 60 | visual content using machine learning. 61 | 62 | ## Key Concepts 63 | 64 | **Data remains on-device and classification is performed locally**
65 | No image is ever uploaded to the server because with TensorFlow.js, inference is done locally, and 66 | user data is kept private. There is no need for a persistent network connection to continue performing inferences. 67 | 68 | **Assets are stored in browser cache and storage**
69 | On the user's first visit, a service worker is used to cache page resources (i.e. HTML, CSS, and JS files). 70 | Each device must have network connectivity for this first visit, but on subsequent visits, the app 71 | will still load and work as assets will be served from the cache. Similarly on the first visit, 72 | the pre-trained model is downloaded and saved in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), 73 | a browser API for client-side storage. Subsequent loads to the page will retrieve the model from IndexedDB if 74 | it is available. This saves from having to continually re-download the model. 75 | 76 | **App can run on desktop and be 'installed' on mobile**
77 | Regardless of what platform the user is on, as long as the app is run on a modern browser, everything 78 | should work. With the use of our [manifest file](https://developers.google.com/web/fundamentals/web-app-manifest/), 79 | the app can be 'installed' on mobile devices, making it look like a native app with its own app icon 80 | on the home screen. 81 | 82 | **Content can still be updated by prompting the user**
83 | Since content is served cache/storage first, we need a way to serve new content to the end-user. 84 | For this, when new content is available a new service worker is ready to be installed, the user is 85 | notified with a prompt to reload the page and get the latest changes. For updating the 86 | pre-trained model, we use a server API endpoint to query the date the model on the server was last 87 | updated. If the app can hit the endpoint and detects the locally saved model is older than the model on 88 | the server, the user is given a prompt with the option to update. 89 | 90 | ## Watch the Video 91 | 92 | [![](https://img.youtube.com/vi/DmlI0Dlr6iQ/0.jpg)](https://youtu.be/DmlI0Dlr6iQ) 93 | 94 | ## Steps 95 | 96 | 1. [Clone the repo](#1-clone-the-repo) 97 | 2. [Install app dependencies](#2-install-app-dependencies) 98 | 3. [Download and convert pre-trained model](#3-download-and-convert-pre-trained-model) 99 | 4. [Setup configuration files](#4-setup-configuration-files) 100 | 5. [Deploy app and classify](#5-deploy-app-and-classify) 101 | 102 | 103 | ### 1. Clone the repo 104 | 105 | Clone the `tfjs-web-app` locally. In a terminal, run: 106 | 107 | ``` 108 | git clone https://github.com/pvaneck/tfjs-web-app 109 | ``` 110 | 111 | Now go to the cloned repo directory: 112 | 113 | ``` 114 | cd tfjs-web-app 115 | ``` 116 | 117 | 118 | ### 2. Install app dependencies 119 | 120 | In the project directory, run: 121 | 122 | ``` 123 | yarn install 124 | ``` 125 | 126 | > **Note**: If you don't have yarn installed, instructions can be found 127 | [here](https://yarnpkg.com/lang/en/docs/install/). You can alternatively use `npm`. 128 | 129 | ### 3. Download and convert pre-trained model 130 | 131 | For this pattern, we are going to download a MobileNet model. However, any image 132 | classification model can be used including any custom made ones. You just have to be able to 133 | convert it with `tfjs-converter`. 134 | 135 | The `tfjs-converter` library can convert models that are in formats such as TensorFlow SavedModel and Keras 136 | HDF5. More information about converting Python models to a web-friendly format can be found 137 | in the `tfjs-converter` [repository](https://github.com/tensorflow/tfjs/tree/master/tfjs-converter). 138 | 139 | Now, let's get our environment set up to use the `tensorflowjs` Python package. 140 | 141 | The general recommendation for Python development is to use a virtual environment 142 | [(venv)](https://docs.python.org/3/tutorial/venv.html). To install and initialize a virtual environment, 143 | use the `venv` module on Python 3 (you install the virtualenv library for Python 2.7): 144 | 145 | ```bash 146 | # Create the virtual environment using Python. Use one of the two commands depending on your Python version. 147 | # Note, it may be named python3 on your system. 148 | 149 | $ python -m venv myenv # Python 3.X 150 | $ virtualenv myenv # Python 2.X 151 | 152 | # Now source the virtual environment. Use one of the two commands depending on your OS. 153 | 154 | $ source myenv/bin/activate # Mac or Linux 155 | $ ./myenv/Scripts/activate # Windows PowerShell 156 | ``` 157 | 158 | Install the `tensorflowjs` package. 159 | ```bash 160 | pip install tensorflowjs 161 | ``` 162 | 163 | Now let's download the Keras MobileNet model. A simple script has been provided to make sure 164 | that MobileNet is downloaded in the proper HDF5 format. Just run: 165 | ```bash 166 | python download_model.py 167 | ``` 168 | 169 | After this is complete, the current directory should now contain `mobilenet-model.h5`. Let's convert 170 | it so it can be used in our app: 171 | ```bash 172 | tensorflowjs_converter --input_format=keras ./mobilenet-model.h5 ./my-model 173 | ``` 174 | 175 | We now have a `model.json` file and multiple sharded binary files located in `./my-model` that we 176 | will use in our web app. 177 | 178 | ### 4. Setup configuration files 179 | 180 | In the `public` folder, you will see a `model` folder. The TensorFlow.js model files need to go there 181 | (i.e. the `model.json` and `*shard*` files). If not already there, let's move them: 182 | ```bash 183 | mv ./my-model/* ./public/model/ 184 | ``` 185 | 186 | In `src/model`, there is a `classes.js` file which lists the possible classes that the model can classify 187 | for. Since we used a MobileNet model which was trained using ImageNet, we will use the ImageNet 188 | classes. You can alter this to fit your model if it is different. 189 | 190 | If deploying the application, change the `API_ENDPOINT` in `src/config.js` to the proper endpoint. 191 | For development and local testing, leave it as is. 192 | 193 | ### 5. Deploy app and classify 194 | 195 | You can either deploy in development mode or production mode. Service workers and offline usage 196 | will only work if you deploy the app in production mode. 197 | 198 | #### Development Mode 199 | 200 | In the project directory, run: 201 | 202 | ``` 203 | yarn start-dev 204 | ``` 205 | 206 | Runs the app in the development mode.
207 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 208 | 209 | The page will reload if you make edits to the UI. You will also see any lint errors in the console. 210 | 211 | The API server is hosted on `http://localhost:5000` by default. 212 | 213 | #### Production Mode 214 | 215 | In the project directory, run: 216 | 217 | ``` 218 | yarn build 219 | ``` 220 | 221 | Builds the app for production to the `build` folder. It correctly bundles React in production mode 222 | and optimizes the build for the best performance. 223 | 224 | The build is minified and the filenames include the hashes. 225 | 226 | Since we use Node.js to deploy the app, simply run: 227 | 228 | ``` 229 | node server.js 230 | ``` 231 | 232 | This will bring up the server which will serve both the API and built UI code. 233 | Visit it at `http://localhost:5000`. 234 | 235 | **Note**: Since the production app uses a service worker, assets are served from the cache first. A 236 | notification should appear on the web page when changes (new builds) are detected, prompting you to reload. 237 | However, if you still don't see your changes after reloading, try ensuring all tabs of the app 238 | in the browser are closed completely to prompt an update of the code when you revisit the page. 239 | 240 | #### Other Information 241 | 242 | To prompt the browser to download a new model if one is available, the app queries a simple endpoint 243 | `/api/model_info` from the server. This endpoint provides the date the model was last updated as provided by 244 | `model_info.txt`. This can be changed to other means of assessing model versions, but is deliberately 245 | kept simple here. This can be updated with `date > model_info.txt`. If the date of the app's 246 | locally stored model is before this date, a prompt is given to user with the option to update the model. 247 | The user can choose to dismiss the update, or if the API call fails, then the locally saved model will 248 | continue to be used. 249 | 250 | #### Deploy on IBM Cloud 251 | 252 | Deployment of the production app on the IBM Cloud is easy. Instructions can be found [here](doc/cloud-deploy.md). 253 | 254 | #### Using the App 255 | 256 | The app allows you to either use your device's camera to snap an image or select a local image from 257 | the device's filesystem. Select an image of an object or put the object in frame using your camera, 258 | then click classify. Local inference will then be performed, and the top five results will be given. 259 | 260 | ![Classify with App](doc/images/app-classify.png "Classify with App") 261 | ![App Predictions](doc/images/app-predictions.png "App Predictions") 262 | 263 | ## Links 264 | 265 | * [TensorFlow.js](https://www.tensorflow.org/js) 266 | * [React](https://reactjs.org/) 267 | * [Progressive Web Apps](https://developers.google.com/web/progressive-web-apps/) 268 | * [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) 269 | * [Web App Manifest](https://developers.google.com/web/fundamentals/web-app-manifest/) 270 | * [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 271 | * [React Bootstrap](https://react-bootstrap.github.io/) 272 | 273 | ## License 274 | 275 | This code pattern is licensed under the Apache Software License, Version 2. Separate third party code objects invoked within this code pattern are licensed by their respective providers pursuant to their own separate licenses. Contributions are subject to the [Developer Certificate of Origin, Version 1.1 (DCO)](https://developercertificate.org/) and the [Apache Software License, Version 2](https://www.apache.org/licenses/LICENSE-2.0.txt). 276 | 277 | [Apache Software License (ASL) FAQ](https://www.apache.org/foundation/license-faq.html#WhatDoesItMEAN) 278 | -------------------------------------------------------------------------------- /doc/cloud-deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy web app on IBM Cloud using Cloud Foundry 2 | 3 | These are instructions for a quick deployment of the web app on IBM Cloud. 4 | 5 | ## Prerequisites 6 | 7 | * An IBM Cloud account. If you do not have one, you can register for 8 | free [here](https://cloud.ibm.com/registration): 9 | * Ensure that the [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/index.html) 10 | tool is installed locally. Follow the instructions in the linked documentation to 11 | configure your environment. 12 | 13 | ## Update manifest.yml 14 | 15 | In the `manifest.yml` file found in the root of the project, change the `route` value to a 16 | unique route of your choosing by replacing the `your-host` placeholder. For example, route could be 17 | `- route: my-web-app.mybluemix.net`. Just make sure that the subdomain is not already taken. 18 | This route corresponds to the URL for which the app will be accessible from. Also, feel free 19 | to change the value for `name` which will correspond to the name of your Cloud Foundry app. 20 | 21 | ## Update app config.js 22 | 23 | In `src/config.js`, update the parameter `API_ENDPOINT` to use the route you 24 | previously specified in the `manifest.yml` file. Using the above example route, this would be: 25 | 26 | ``` 27 | API_ENDPOINT: 'https://my-web-app.mybluemix.net/api', 28 | ``` 29 | 30 | > **NOTE**: `https` is used because Cloud Foundry apps on the IBM Cloud are hosted using HTTPS. 31 | 32 | ## Make sure build files are up to date 33 | 34 | From the root of the project, run: 35 | ```bash 36 | yarn build 37 | ``` 38 | 39 | If changes are made to the code, and the app needs to be updated, you will need to `build` again. 40 | 41 | ## Deploy the app 42 | 43 | From the root of the project run: 44 | 45 | ```bash 46 | ibmcloud cf push 47 | ``` 48 | 49 | This will push a new app or update the app on the IBM Cloud. You can view your apps online from the 50 | IBM Cloud [resource list](https://cloud.ibm.com/resources). 51 | 52 | ## Visit deployed app 53 | 54 | In a browser, navigate to `https://`. 55 | -------------------------------------------------------------------------------- /doc/images/app-classify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/doc/images/app-classify.png -------------------------------------------------------------------------------- /doc/images/app-predictions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/doc/images/app-predictions.png -------------------------------------------------------------------------------- /doc/images/arch-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/doc/images/arch-diagram.png -------------------------------------------------------------------------------- /download_model.py: -------------------------------------------------------------------------------- 1 | from tensorflow.keras import applications 2 | 3 | 4 | model = applications.mobilenet.MobileNet() 5 | model.save('./mobilenet-model.h5') 6 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: web-classify 4 | memory: 512M 5 | disk_quota: 2048M 6 | routes: 7 | - route: [UPDATE-ME].mybluemix.net 8 | command: npm run start-prod 9 | -------------------------------------------------------------------------------- /model_info.txt: -------------------------------------------------------------------------------- 1 | Mon Sep 30 17:30:58 PDT 2019 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tfjs-web-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "dependencies": { 7 | "@tensorflow/tfjs": "^1.2.7", 8 | "bootstrap": "^4.3.1", 9 | "cors": "^2.8.5", 10 | "express": "^4.17.1", 11 | "idb": "^4.0.3", 12 | "react": "^16.8.6", 13 | "react-bootstrap": "^1.0.0-beta.8", 14 | "react-cropper": "^1.2.0", 15 | "react-dom": "^16.8.6", 16 | "react-icons": "^3.7.0", 17 | "react-router-bootstrap": "^0.25.0", 18 | "react-router-dom": "^5.0.0", 19 | "react-scripts": "3.0.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "start-dev": "concurrently --kill-others-on-fail \"node server.js\" \"yarn start\"", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "start-prod": "node server.js", 28 | "lint": "eslint ./src" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "concurrently": "^4.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/public/images/icon-192.png -------------------------------------------------------------------------------- /public/images/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/public/images/icon-512.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Web Classify App 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WebClassify", 3 | "name": "Progressive Web Classification App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "/images/icon-192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "/images/icon-512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/model/README.md: -------------------------------------------------------------------------------- 1 | TensorFlow.js model files should go in this directory. 2 | 3 | * `model.json` file 4 | * Sharded binary weight files 5 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cors = require('cors'); 4 | const express = require('express'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const app = express(); 9 | const port = process.env.PORT || 5000; 10 | 11 | app.use(cors()); 12 | 13 | app.get('/api/model_info', (req, res) => { 14 | const date = fs.readFileSync('model_info.txt', 'utf8'); 15 | return res.status(200).json({ 16 | last_updated: date.trim() 17 | }); 18 | }); 19 | 20 | app.use(express.static(path.join(__dirname, 'build'))); 21 | 22 | app.get('/*', function (req, res) { 23 | res.sendFile(path.join(__dirname, 'build', 'index.html')); 24 | }); 25 | 26 | app.listen(process.env.PORT || port); 27 | console.log(`Running on http://localhost:${port}`); 28 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | margin-top: 15px; 3 | text-align: center; 4 | } 5 | 6 | .App .navbar-brand { 7 | font-weight: bold; 8 | } 9 | 10 | .app-nav-bar { 11 | background-color: #49494b; 12 | } 13 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import { Container, Nav, Navbar } from 'react-bootstrap'; 5 | import AlertDismissable from './components/AlertDismissable'; 6 | import Routes from './Routes'; 7 | import './App.css'; 8 | 9 | 10 | class App extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | const reloadMsg = ` 15 | New content is available.
16 | Please reload.
17 | If reloading doesn't work, close all tabs/windows of this web application, 18 | and then reopen the application. 19 | `; 20 | this.state = { 21 | showUpdateAlert: true, 22 | reloadMsg: reloadMsg 23 | }; 24 | } 25 | 26 | dismissUpdateAlert = event => { 27 | this.setState({ showUpdateAlert: false }); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 34 | 35 | WebClassify 36 | 37 | 38 | 42 | 43 | 44 | { this.props.updateAvailable && this.state.showUpdateAlert && 45 |
46 | 52 |
53 | } 54 |
55 | 56 | 57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | App.propTypes = { 64 | updateAvailable: PropTypes.bool.isRequired, 65 | }; 66 | 67 | export default withRouter(App); 68 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import About from './pages/About'; 4 | import Classify from './pages/Classify'; 5 | import NotFound from './pages/NotFound'; 6 | 7 | export default ({ childProps }) => 8 | 9 | 10 | 11 | 12 | ; 13 | -------------------------------------------------------------------------------- /src/components/AlertDismissable.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { Alert } from 'react-bootstrap'; 4 | 5 | /** 6 | * Class to handle the rendering of a dismissiable alert to use for things like errors. 7 | * @extends React.Component 8 | */ 9 | class AlertDismissable extends Component { 10 | 11 | render() { 12 | if (this.props.show) { 13 | return ( 14 | 15 | { this.props.title &&
{this.props.title}
} 16 |
17 | 18 | ); 19 | } 20 | return null; 21 | } 22 | } 23 | 24 | export default AlertDismissable; 25 | -------------------------------------------------------------------------------- /src/components/LoadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Spinner } from 'react-bootstrap'; 3 | 4 | /** 5 | * This produces a button that will have a loading animation while the isLoading property is true. 6 | */ 7 | export default ({ 8 | isLoading, 9 | text, 10 | loadingText, 11 | className = '', 12 | disabled = false, 13 | ...props 14 | }) => 15 | ; 29 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | API_ENDPOINT: 'http://localhost:5000/api', 3 | // API_ENDPOINT: 'https://[YOUR-ROUTE].mybluemix.net/api' 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #f6f5f3; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import 'bootstrap/dist/css/bootstrap.css'; 5 | import './index.css'; 6 | import App from './App'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | 10 | class Index extends Component { 11 | state = { 12 | contentCached: false, 13 | updateAvailable: false, 14 | }; 15 | 16 | componentDidMount() { 17 | const config = { 18 | onUpdate: this.handleUpdate, 19 | }; 20 | 21 | // If you want your app to work offline and load faster, you can change 22 | // unregister() to register() below. Note this comes with some pitfalls. 23 | // Learn more about service workers: https://bit.ly/CRA-PWA 24 | serviceWorker.register(config); 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | handleUpdate = (registration) => { 36 | const waitingServiceWorker = registration.waiting; 37 | 38 | if (waitingServiceWorker) { 39 | waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' }); 40 | } 41 | this.setState({ updateAvailable: true}); 42 | } 43 | } 44 | 45 | ReactDOM.render(, document.getElementById('root')); 46 | -------------------------------------------------------------------------------- /src/model/classes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Theses classes should correspond to the softmax output of your model. 3 | 4 | export const MODEL_CLASSES = { 5 | 0: 'tench, Tinca tinca', 6 | 1: 'goldfish, Carassius auratus', 7 | 2: 'great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias', 8 | 3: 'tiger shark, Galeocerdo cuvieri', 9 | 4: 'hammerhead, hammerhead shark', 10 | 5: 'electric ray, crampfish, numbfish, torpedo', 11 | 6: 'stingray', 12 | 7: 'cock', 13 | 8: 'hen', 14 | 9: 'ostrich, Struthio camelus', 15 | 10: 'brambling, Fringilla montifringilla', 16 | 11: 'goldfinch, Carduelis carduelis', 17 | 12: 'house finch, linnet, Carpodacus mexicanus', 18 | 13: 'junco, snowbird', 19 | 14: 'indigo bunting, indigo finch, indigo bird, Passerina cyanea', 20 | 15: 'robin, American robin, Turdus migratorius', 21 | 16: 'bulbul', 22 | 17: 'jay', 23 | 18: 'magpie', 24 | 19: 'chickadee', 25 | 20: 'water ouzel, dipper', 26 | 21: 'kite', 27 | 22: 'bald eagle, American eagle, Haliaeetus leucocephalus', 28 | 23: 'vulture', 29 | 24: 'great grey owl, great gray owl, Strix nebulosa', 30 | 25: 'European fire salamander, Salamandra salamandra', 31 | 26: 'common newt, Triturus vulgaris', 32 | 27: 'eft', 33 | 28: 'spotted salamander, Ambystoma maculatum', 34 | 29: 'axolotl, mud puppy, Ambystoma mexicanum', 35 | 30: 'bullfrog, Rana catesbeiana', 36 | 31: 'tree frog, tree-frog', 37 | 32: 'tailed frog, bell toad, ribbed toad, tailed toad, Ascaphus trui', 38 | 33: 'loggerhead, loggerhead turtle, Caretta caretta', 39 | 34: 'leatherback turtle, leatherback, leathery turtle, Dermochelys coriacea', 40 | 35: 'mud turtle', 41 | 36: 'terrapin', 42 | 37: 'box turtle, box tortoise', 43 | 38: 'banded gecko', 44 | 39: 'common iguana, iguana, Iguana iguana', 45 | 40: 'American chameleon, anole, Anolis carolinensis', 46 | 41: 'whiptail, whiptail lizard', 47 | 42: 'agama', 48 | 43: 'frilled lizard, Chlamydosaurus kingi', 49 | 44: 'alligator lizard', 50 | 45: 'Gila monster, Heloderma suspectum', 51 | 46: 'green lizard, Lacerta viridis', 52 | 47: 'African chameleon, Chamaeleo chamaeleon', 53 | 48: 'Komodo dragon, Komodo lizard, dragon lizard, giant lizard, Varanus komodoensis', 54 | 49: 'African crocodile, Nile crocodile, Crocodylus niloticus', 55 | 50: 'American alligator, Alligator mississipiensis', 56 | 51: 'triceratops', 57 | 52: 'thunder snake, worm snake, Carphophis amoenus', 58 | 53: 'ringneck snake, ring-necked snake, ring snake', 59 | 54: 'hognose snake, puff adder, sand viper', 60 | 55: 'green snake, grass snake', 61 | 56: 'king snake, kingsnake', 62 | 57: 'garter snake, grass snake', 63 | 58: 'water snake', 64 | 59: 'vine snake', 65 | 60: 'night snake, Hypsiglena torquata', 66 | 61: 'boa constrictor, Constrictor constrictor', 67 | 62: 'rock python, rock snake, Python sebae', 68 | 63: 'Indian cobra, Naja naja', 69 | 64: 'green mamba', 70 | 65: 'sea snake', 71 | 66: 'horned viper, cerastes, sand viper, horned asp, Cerastes cornutus', 72 | 67: 'diamondback, diamondback rattlesnake, Crotalus adamanteus', 73 | 68: 'sidewinder, horned rattlesnake, Crotalus cerastes', 74 | 69: 'trilobite', 75 | 70: 'harvestman, daddy longlegs, Phalangium opilio', 76 | 71: 'scorpion', 77 | 72: 'black and gold garden spider, Argiope aurantia', 78 | 73: 'barn spider, Araneus cavaticus', 79 | 74: 'garden spider, Aranea diademata', 80 | 75: 'black widow, Latrodectus mactans', 81 | 76: 'tarantula', 82 | 77: 'wolf spider, hunting spider', 83 | 78: 'tick', 84 | 79: 'centipede', 85 | 80: 'black grouse', 86 | 81: 'ptarmigan', 87 | 82: 'ruffed grouse, partridge, Bonasa umbellus', 88 | 83: 'prairie chicken, prairie grouse, prairie fowl', 89 | 84: 'peacock', 90 | 85: 'quail', 91 | 86: 'partridge', 92 | 87: 'African grey, African gray, Psittacus erithacus', 93 | 88: 'macaw', 94 | 89: 'sulphur-crested cockatoo, Kakatoe galerita, Cacatua galerita', 95 | 90: 'lorikeet', 96 | 91: 'coucal', 97 | 92: 'bee eater', 98 | 93: 'hornbill', 99 | 94: 'hummingbird', 100 | 95: 'jacamar', 101 | 96: 'toucan', 102 | 97: 'drake', 103 | 98: 'red-breasted merganser, Mergus serrator', 104 | 99: 'goose', 105 | 100: 'black swan, Cygnus atratus', 106 | 101: 'tusker', 107 | 102: 'echidna, spiny anteater, anteater', 108 | 103: 'platypus, duckbill, duckbilled platypus, duck-billed platypus, Ornithorhynchus anatinus', 109 | 104: 'wallaby, brush kangaroo', 110 | 105: 'koala, koala bear, kangaroo bear, native bear, Phascolarctos cinereus', 111 | 106: 'wombat', 112 | 107: 'jellyfish', 113 | 108: 'sea anemone, anemone', 114 | 109: 'brain coral', 115 | 110: 'flatworm, platyhelminth', 116 | 111: 'nematode, nematode worm, roundworm', 117 | 112: 'conch', 118 | 113: 'snail', 119 | 114: 'slug', 120 | 115: 'sea slug, nudibranch', 121 | 116: 'chiton, coat-of-mail shell, sea cradle, polyplacophore', 122 | 117: 'chambered nautilus, pearly nautilus, nautilus', 123 | 118: 'Dungeness crab, Cancer magister', 124 | 119: 'rock crab, Cancer irroratus', 125 | 120: 'fiddler crab', 126 | 121: 'king crab, Alaska crab, Alaskan king crab, Alaska king crab, Paralithodes camtschatica', 127 | 122: 'American lobster, Northern lobster, Maine lobster, Homarus americanus', 128 | 123: 'spiny lobster, langouste, rock lobster, crawfish, crayfish, sea crawfish', 129 | 124: 'crayfish, crawfish, crawdad, crawdaddy', 130 | 125: 'hermit crab', 131 | 126: 'isopod', 132 | 127: 'white stork, Ciconia ciconia', 133 | 128: 'black stork, Ciconia nigra', 134 | 129: 'spoonbill', 135 | 130: 'flamingo', 136 | 131: 'little blue heron, Egretta caerulea', 137 | 132: 'American egret, great white heron, Egretta albus', 138 | 133: 'bittern', 139 | 134: 'crane', 140 | 135: 'limpkin, Aramus pictus', 141 | 136: 'European gallinule, Porphyrio porphyrio', 142 | 137: 'American coot, marsh hen, mud hen, water hen, Fulica americana', 143 | 138: 'bustard', 144 | 139: 'ruddy turnstone, Arenaria interpres', 145 | 140: 'red-backed sandpiper, dunlin, Erolia alpina', 146 | 141: 'redshank, Tringa totanus', 147 | 142: 'dowitcher', 148 | 143: 'oystercatcher, oyster catcher', 149 | 144: 'pelican', 150 | 145: 'king penguin, Aptenodytes patagonica', 151 | 146: 'albatross, mollymawk', 152 | 147: 'grey whale, gray whale, devilfish, Eschrichtius gibbosus, Eschrichtius robustus', 153 | 148: 'killer whale, killer, orca, grampus, sea wolf, Orcinus orca', 154 | 149: 'dugong, Dugong dugon', 155 | 150: 'sea lion', 156 | 151: 'Chihuahua', 157 | 152: 'Japanese spaniel', 158 | 153: 'Maltese dog, Maltese terrier, Maltese', 159 | 154: 'Pekinese, Pekingese, Peke', 160 | 155: 'Shih-Tzu', 161 | 156: 'Blenheim spaniel', 162 | 157: 'papillon', 163 | 158: 'toy terrier', 164 | 159: 'Rhodesian ridgeback', 165 | 160: 'Afghan hound, Afghan', 166 | 161: 'basset, basset hound', 167 | 162: 'beagle', 168 | 163: 'bloodhound, sleuthhound', 169 | 164: 'bluetick', 170 | 165: 'black-and-tan coonhound', 171 | 166: 'Walker hound, Walker foxhound', 172 | 167: 'English foxhound', 173 | 168: 'redbone', 174 | 169: 'borzoi, Russian wolfhound', 175 | 170: 'Irish wolfhound', 176 | 171: 'Italian greyhound', 177 | 172: 'whippet', 178 | 173: 'Ibizan hound, Ibizan Podenco', 179 | 174: 'Norwegian elkhound, elkhound', 180 | 175: 'otterhound, otter hound', 181 | 176: 'Saluki, gazelle hound', 182 | 177: 'Scottish deerhound, deerhound', 183 | 178: 'Weimaraner', 184 | 179: 'Staffordshire bullterrier, Staffordshire bull terrier', 185 | 180: 'American Staffordshire terrier, Staffordshire terrier, American pit bull terrier, pit bull terrier', 186 | 181: 'Bedlington terrier', 187 | 182: 'Border terrier', 188 | 183: 'Kerry blue terrier', 189 | 184: 'Irish terrier', 190 | 185: 'Norfolk terrier', 191 | 186: 'Norwich terrier', 192 | 187: 'Yorkshire terrier', 193 | 188: 'wire-haired fox terrier', 194 | 189: 'Lakeland terrier', 195 | 190: 'Sealyham terrier, Sealyham', 196 | 191: 'Airedale, Airedale terrier', 197 | 192: 'cairn, cairn terrier', 198 | 193: 'Australian terrier', 199 | 194: 'Dandie Dinmont, Dandie Dinmont terrier', 200 | 195: 'Boston bull, Boston terrier', 201 | 196: 'miniature schnauzer', 202 | 197: 'giant schnauzer', 203 | 198: 'standard schnauzer', 204 | 199: 'Scotch terrier, Scottish terrier, Scottie', 205 | 200: 'Tibetan terrier, chrysanthemum dog', 206 | 201: 'silky terrier, Sydney silky', 207 | 202: 'soft-coated wheaten terrier', 208 | 203: 'West Highland white terrier', 209 | 204: 'Lhasa, Lhasa apso', 210 | 205: 'flat-coated retriever', 211 | 206: 'curly-coated retriever', 212 | 207: 'golden retriever', 213 | 208: 'Labrador retriever', 214 | 209: 'Chesapeake Bay retriever', 215 | 210: 'German short-haired pointer', 216 | 211: 'vizsla, Hungarian pointer', 217 | 212: 'English setter', 218 | 213: 'Irish setter, red setter', 219 | 214: 'Gordon setter', 220 | 215: 'Brittany spaniel', 221 | 216: 'clumber, clumber spaniel', 222 | 217: 'English springer, English springer spaniel', 223 | 218: 'Welsh springer spaniel', 224 | 219: 'cocker spaniel, English cocker spaniel, cocker', 225 | 220: 'Sussex spaniel', 226 | 221: 'Irish water spaniel', 227 | 222: 'kuvasz', 228 | 223: 'schipperke', 229 | 224: 'groenendael', 230 | 225: 'malinois', 231 | 226: 'briard', 232 | 227: 'kelpie', 233 | 228: 'komondor', 234 | 229: 'Old English sheepdog, bobtail', 235 | 230: 'Shetland sheepdog, Shetland sheep dog, Shetland', 236 | 231: 'collie', 237 | 232: 'Border collie', 238 | 233: 'Bouvier des Flandres, Bouviers des Flandres', 239 | 234: 'Rottweiler', 240 | 235: 'German shepherd, German shepherd dog, German police dog, alsatian', 241 | 236: 'Doberman, Doberman pinscher', 242 | 237: 'miniature pinscher', 243 | 238: 'Greater Swiss Mountain dog', 244 | 239: 'Bernese mountain dog', 245 | 240: 'Appenzeller', 246 | 241: 'EntleBucher', 247 | 242: 'boxer', 248 | 243: 'bull mastiff', 249 | 244: 'Tibetan mastiff', 250 | 245: 'French bulldog', 251 | 246: 'Great Dane', 252 | 247: 'Saint Bernard, St Bernard', 253 | 248: 'Eskimo dog, husky', 254 | 249: 'malamute, malemute, Alaskan malamute', 255 | 250: 'Siberian husky', 256 | 251: 'dalmatian, coach dog, carriage dog', 257 | 252: 'affenpinscher, monkey pinscher, monkey dog', 258 | 253: 'basenji', 259 | 254: 'pug, pug-dog', 260 | 255: 'Leonberg', 261 | 256: 'Newfoundland, Newfoundland dog', 262 | 257: 'Great Pyrenees', 263 | 258: 'Samoyed, Samoyede', 264 | 259: 'Pomeranian', 265 | 260: 'chow, chow chow', 266 | 261: 'keeshond', 267 | 262: 'Brabancon griffon', 268 | 263: 'Pembroke, Pembroke Welsh corgi', 269 | 264: 'Cardigan, Cardigan Welsh corgi', 270 | 265: 'toy poodle', 271 | 266: 'miniature poodle', 272 | 267: 'standard poodle', 273 | 268: 'Mexican hairless', 274 | 269: 'timber wolf, grey wolf, gray wolf, Canis lupus', 275 | 270: 'white wolf, Arctic wolf, Canis lupus tundrarum', 276 | 271: 'red wolf, maned wolf, Canis rufus, Canis niger', 277 | 272: 'coyote, prairie wolf, brush wolf, Canis latrans', 278 | 273: 'dingo, warrigal, warragal, Canis dingo', 279 | 274: 'dhole, Cuon alpinus', 280 | 275: 'African hunting dog, hyena dog, Cape hunting dog, Lycaon pictus', 281 | 276: 'hyena, hyaena', 282 | 277: 'red fox, Vulpes vulpes', 283 | 278: 'kit fox, Vulpes macrotis', 284 | 279: 'Arctic fox, white fox, Alopex lagopus', 285 | 280: 'grey fox, gray fox, Urocyon cinereoargenteus', 286 | 281: 'tabby, tabby cat', 287 | 282: 'tiger cat', 288 | 283: 'Persian cat', 289 | 284: 'Siamese cat, Siamese', 290 | 285: 'Egyptian cat', 291 | 286: 'cougar, puma, catamount, mountain lion, painter, panther, Felis concolor', 292 | 287: 'lynx, catamount', 293 | 288: 'leopard, Panthera pardus', 294 | 289: 'snow leopard, ounce, Panthera uncia', 295 | 290: 'jaguar, panther, Panthera onca, Felis onca', 296 | 291: 'lion, king of beasts, Panthera leo', 297 | 292: 'tiger, Panthera tigris', 298 | 293: 'cheetah, chetah, Acinonyx jubatus', 299 | 294: 'brown bear, bruin, Ursus arctos', 300 | 295: 'American black bear, black bear, Ursus americanus, Euarctos americanus', 301 | 296: 'ice bear, polar bear, Ursus Maritimus, Thalarctos maritimus', 302 | 297: 'sloth bear, Melursus ursinus, Ursus ursinus', 303 | 298: 'mongoose', 304 | 299: 'meerkat, mierkat', 305 | 300: 'tiger beetle', 306 | 301: 'ladybug, ladybeetle, lady beetle, ladybird, ladybird beetle', 307 | 302: 'ground beetle, carabid beetle', 308 | 303: 'long-horned beetle, longicorn, longicorn beetle', 309 | 304: 'leaf beetle, chrysomelid', 310 | 305: 'dung beetle', 311 | 306: 'rhinoceros beetle', 312 | 307: 'weevil', 313 | 308: 'fly', 314 | 309: 'bee', 315 | 310: 'ant, emmet, pismire', 316 | 311: 'grasshopper, hopper', 317 | 312: 'cricket', 318 | 313: 'walking stick, walkingstick, stick insect', 319 | 314: 'cockroach, roach', 320 | 315: 'mantis, mantid', 321 | 316: 'cicada, cicala', 322 | 317: 'leafhopper', 323 | 318: 'lacewing, lacewing fly', 324 | 319: "dragonfly, darning needle, devil's darning needle, sewing needle, snake feeder, snake doctor, mosquito hawk, skeeter hawk", 325 | 320: 'damselfly', 326 | 321: 'admiral', 327 | 322: 'ringlet, ringlet butterfly', 328 | 323: 'monarch, monarch butterfly, milkweed butterfly, Danaus plexippus', 329 | 324: 'cabbage butterfly', 330 | 325: 'sulphur butterfly, sulfur butterfly', 331 | 326: 'lycaenid, lycaenid butterfly', 332 | 327: 'starfish, sea star', 333 | 328: 'sea urchin', 334 | 329: 'sea cucumber, holothurian', 335 | 330: 'wood rabbit, cottontail, cottontail rabbit', 336 | 331: 'hare', 337 | 332: 'Angora, Angora rabbit', 338 | 333: 'hamster', 339 | 334: 'porcupine, hedgehog', 340 | 335: 'fox squirrel, eastern fox squirrel, Sciurus niger', 341 | 336: 'marmot', 342 | 337: 'beaver', 343 | 338: 'guinea pig, Cavia cobaya', 344 | 339: 'sorrel', 345 | 340: 'zebra', 346 | 341: 'hog, pig, grunter, squealer, Sus scrofa', 347 | 342: 'wild boar, boar, Sus scrofa', 348 | 343: 'warthog', 349 | 344: 'hippopotamus, hippo, river horse, Hippopotamus amphibius', 350 | 345: 'ox', 351 | 346: 'water buffalo, water ox, Asiatic buffalo, Bubalus bubalis', 352 | 347: 'bison', 353 | 348: 'ram, tup', 354 | 349: 'bighorn, bighorn sheep, cimarron, Rocky Mountain bighorn, Rocky Mountain sheep, Ovis canadensis', 355 | 350: 'ibex, Capra ibex', 356 | 351: 'hartebeest', 357 | 352: 'impala, Aepyceros melampus', 358 | 353: 'gazelle', 359 | 354: 'Arabian camel, dromedary, Camelus dromedarius', 360 | 355: 'llama', 361 | 356: 'weasel', 362 | 357: 'mink', 363 | 358: 'polecat, fitch, foulmart, foumart, Mustela putorius', 364 | 359: 'black-footed ferret, ferret, Mustela nigripes', 365 | 360: 'otter', 366 | 361: 'skunk, polecat, wood pussy', 367 | 362: 'badger', 368 | 363: 'armadillo', 369 | 364: 'three-toed sloth, ai, Bradypus tridactylus', 370 | 365: 'orangutan, orang, orangutang, Pongo pygmaeus', 371 | 366: 'gorilla, Gorilla gorilla', 372 | 367: 'chimpanzee, chimp, Pan troglodytes', 373 | 368: 'gibbon, Hylobates lar', 374 | 369: 'siamang, Hylobates syndactylus, Symphalangus syndactylus', 375 | 370: 'guenon, guenon monkey', 376 | 371: 'patas, hussar monkey, Erythrocebus patas', 377 | 372: 'baboon', 378 | 373: 'macaque', 379 | 374: 'langur', 380 | 375: 'colobus, colobus monkey', 381 | 376: 'proboscis monkey, Nasalis larvatus', 382 | 377: 'marmoset', 383 | 378: 'capuchin, ringtail, Cebus capucinus', 384 | 379: 'howler monkey, howler', 385 | 380: 'titi, titi monkey', 386 | 381: 'spider monkey, Ateles geoffroyi', 387 | 382: 'squirrel monkey, Saimiri sciureus', 388 | 383: 'Madagascar cat, ring-tailed lemur, Lemur catta', 389 | 384: 'indri, indris, Indri indri, Indri brevicaudatus', 390 | 385: 'Indian elephant, Elephas maximus', 391 | 386: 'African elephant, Loxodonta africana', 392 | 387: 'lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens', 393 | 388: 'giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca', 394 | 389: 'barracouta, snoek', 395 | 390: 'eel', 396 | 391: 'coho, cohoe, coho salmon, blue jack, silver salmon, Oncorhynchus kisutch', 397 | 392: 'rock beauty, Holocanthus tricolor', 398 | 393: 'anemone fish', 399 | 394: 'sturgeon', 400 | 395: 'gar, garfish, garpike, billfish, Lepisosteus osseus', 401 | 396: 'lionfish', 402 | 397: 'puffer, pufferfish, blowfish, globefish', 403 | 398: 'abacus', 404 | 399: 'abaya', 405 | 400: "academic gown, academic robe, judge's robe", 406 | 401: 'accordion, piano accordion, squeeze box', 407 | 402: 'acoustic guitar', 408 | 403: 'aircraft carrier, carrier, flattop, attack aircraft carrier', 409 | 404: 'airliner', 410 | 405: 'airship, dirigible', 411 | 406: 'altar', 412 | 407: 'ambulance', 413 | 408: 'amphibian, amphibious vehicle', 414 | 409: 'analog clock', 415 | 410: 'apiary, bee house', 416 | 411: 'apron', 417 | 412: 'ashcan, trash can, garbage can, wastebin, ash bin, ash-bin, ashbin, dustbin, trash barrel, trash bin', 418 | 413: 'assault rifle, assault gun', 419 | 414: 'backpack, back pack, knapsack, packsack, rucksack, haversack', 420 | 415: 'bakery, bakeshop, bakehouse', 421 | 416: 'balance beam, beam', 422 | 417: 'balloon', 423 | 418: 'ballpoint, ballpoint pen, ballpen, Biro', 424 | 419: 'Band Aid', 425 | 420: 'banjo', 426 | 421: 'bannister, banister, balustrade, balusters, handrail', 427 | 422: 'barbell', 428 | 423: 'barber chair', 429 | 424: 'barbershop', 430 | 425: 'barn', 431 | 426: 'barometer', 432 | 427: 'barrel, cask', 433 | 428: 'barrow, garden cart, lawn cart, wheelbarrow', 434 | 429: 'baseball', 435 | 430: 'basketball', 436 | 431: 'bassinet', 437 | 432: 'bassoon', 438 | 433: 'bathing cap, swimming cap', 439 | 434: 'bath towel', 440 | 435: 'bathtub, bathing tub, bath, tub', 441 | 436: 'beach wagon, station wagon, wagon, estate car, beach waggon, station waggon, waggon', 442 | 437: 'beacon, lighthouse, beacon light, pharos', 443 | 438: 'beaker', 444 | 439: 'bearskin, busby, shako', 445 | 440: 'beer bottle', 446 | 441: 'beer glass', 447 | 442: 'bell cote, bell cot', 448 | 443: 'bib', 449 | 444: 'bicycle-built-for-two, tandem bicycle, tandem', 450 | 445: 'bikini, two-piece', 451 | 446: 'binder, ring-binder', 452 | 447: 'binoculars, field glasses, opera glasses', 453 | 448: 'birdhouse', 454 | 449: 'boathouse', 455 | 450: 'bobsled, bobsleigh, bob', 456 | 451: 'bolo tie, bolo, bola tie, bola', 457 | 452: 'bonnet, poke bonnet', 458 | 453: 'bookcase', 459 | 454: 'bookshop, bookstore, bookstall', 460 | 455: 'bottlecap', 461 | 456: 'bow', 462 | 457: 'bow tie, bow-tie, bowtie', 463 | 458: 'brass, memorial tablet, plaque', 464 | 459: 'brassiere, bra, bandeau', 465 | 460: 'breakwater, groin, groyne, mole, bulwark, seawall, jetty', 466 | 461: 'breastplate, aegis, egis', 467 | 462: 'broom', 468 | 463: 'bucket, pail', 469 | 464: 'buckle', 470 | 465: 'bulletproof vest', 471 | 466: 'bullet train, bullet', 472 | 467: 'butcher shop, meat market', 473 | 468: 'cab, hack, taxi, taxicab', 474 | 469: 'caldron, cauldron', 475 | 470: 'candle, taper, wax light', 476 | 471: 'cannon', 477 | 472: 'canoe', 478 | 473: 'can opener, tin opener', 479 | 474: 'cardigan', 480 | 475: 'car mirror', 481 | 476: 'carousel, carrousel, merry-go-round, roundabout, whirligig', 482 | 477: "carpenter's kit, tool kit", 483 | 478: 'carton', 484 | 479: 'car wheel', 485 | 480: 'cash machine, cash dispenser, automated teller machine, automatic teller machine, automated teller, automatic teller, ATM', 486 | 481: 'cassette', 487 | 482: 'cassette player', 488 | 483: 'castle', 489 | 484: 'catamaran', 490 | 485: 'CD player', 491 | 486: 'cello, violoncello', 492 | 487: 'cellular telephone, cellular phone, cellphone, cell, mobile phone', 493 | 488: 'chain', 494 | 489: 'chainlink fence', 495 | 490: 'chain mail, ring mail, mail, chain armor, chain armour, ring armor, ring armour', 496 | 491: 'chain saw, chainsaw', 497 | 492: 'chest', 498 | 493: 'chiffonier, commode', 499 | 494: 'chime, bell, gong', 500 | 495: 'china cabinet, china closet', 501 | 496: 'Christmas stocking', 502 | 497: 'church, church building', 503 | 498: 'cinema, movie theater, movie theatre, movie house, picture palace', 504 | 499: 'cleaver, meat cleaver, chopper', 505 | 500: 'cliff dwelling', 506 | 501: 'cloak', 507 | 502: 'clog, geta, patten, sabot', 508 | 503: 'cocktail shaker', 509 | 504: 'coffee mug', 510 | 505: 'coffeepot', 511 | 506: 'coil, spiral, volute, whorl, helix', 512 | 507: 'combination lock', 513 | 508: 'computer keyboard, keypad', 514 | 509: 'confectionery, confectionary, candy store', 515 | 510: 'container ship, containership, container vessel', 516 | 511: 'convertible', 517 | 512: 'corkscrew, bottle screw', 518 | 513: 'cornet, horn, trumpet, trump', 519 | 514: 'cowboy boot', 520 | 515: 'cowboy hat, ten-gallon hat', 521 | 516: 'cradle', 522 | 517: 'crane', 523 | 518: 'crash helmet', 524 | 519: 'crate', 525 | 520: 'crib, cot', 526 | 521: 'Crock Pot', 527 | 522: 'croquet ball', 528 | 523: 'crutch', 529 | 524: 'cuirass', 530 | 525: 'dam, dike, dyke', 531 | 526: 'desk', 532 | 527: 'desktop computer', 533 | 528: 'dial telephone, dial phone', 534 | 529: 'diaper, nappy, napkin', 535 | 530: 'digital clock', 536 | 531: 'digital watch', 537 | 532: 'dining table, board', 538 | 533: 'dishrag, dishcloth', 539 | 534: 'dishwasher, dish washer, dishwashing machine', 540 | 535: 'disk brake, disc brake', 541 | 536: 'dock, dockage, docking facility', 542 | 537: 'dogsled, dog sled, dog sleigh', 543 | 538: 'dome', 544 | 539: 'doormat, welcome mat', 545 | 540: 'drilling platform, offshore rig', 546 | 541: 'drum, membranophone, tympan', 547 | 542: 'drumstick', 548 | 543: 'dumbbell', 549 | 544: 'Dutch oven', 550 | 545: 'electric fan, blower', 551 | 546: 'electric guitar', 552 | 547: 'electric locomotive', 553 | 548: 'entertainment center', 554 | 549: 'envelope', 555 | 550: 'espresso maker', 556 | 551: 'face powder', 557 | 552: 'feather boa, boa', 558 | 553: 'file, file cabinet, filing cabinet', 559 | 554: 'fireboat', 560 | 555: 'fire engine, fire truck', 561 | 556: 'fire screen, fireguard', 562 | 557: 'flagpole, flagstaff', 563 | 558: 'flute, transverse flute', 564 | 559: 'folding chair', 565 | 560: 'football helmet', 566 | 561: 'forklift', 567 | 562: 'fountain', 568 | 563: 'fountain pen', 569 | 564: 'four-poster', 570 | 565: 'freight car', 571 | 566: 'French horn, horn', 572 | 567: 'frying pan, frypan, skillet', 573 | 568: 'fur coat', 574 | 569: 'garbage truck, dustcart', 575 | 570: 'gasmask, respirator, gas helmet', 576 | 571: 'gas pump, gasoline pump, petrol pump, island dispenser', 577 | 572: 'goblet', 578 | 573: 'go-kart', 579 | 574: 'golf ball', 580 | 575: 'golfcart, golf cart', 581 | 576: 'gondola', 582 | 577: 'gong, tam-tam', 583 | 578: 'gown', 584 | 579: 'grand piano, grand', 585 | 580: 'greenhouse, nursery, glasshouse', 586 | 581: 'grille, radiator grille', 587 | 582: 'grocery store, grocery, food market, market', 588 | 583: 'guillotine', 589 | 584: 'hair slide', 590 | 585: 'hair spray', 591 | 586: 'half track', 592 | 587: 'hammer', 593 | 588: 'hamper', 594 | 589: 'hand blower, blow dryer, blow drier, hair dryer, hair drier', 595 | 590: 'hand-held computer, hand-held microcomputer', 596 | 591: 'handkerchief, hankie, hanky, hankey', 597 | 592: 'hard disc, hard disk, fixed disk', 598 | 593: 'harmonica, mouth organ, harp, mouth harp', 599 | 594: 'harp', 600 | 595: 'harvester, reaper', 601 | 596: 'hatchet', 602 | 597: 'holster', 603 | 598: 'home theater, home theatre', 604 | 599: 'honeycomb', 605 | 600: 'hook, claw', 606 | 601: 'hoopskirt, crinoline', 607 | 602: 'horizontal bar, high bar', 608 | 603: 'horse cart, horse-cart', 609 | 604: 'hourglass', 610 | 605: 'iPod', 611 | 606: 'iron, smoothing iron', 612 | 607: "jack-o'-lantern", 613 | 608: 'jean, blue jean, denim', 614 | 609: 'jeep, landrover', 615 | 610: 'jersey, T-shirt, tee shirt', 616 | 611: 'jigsaw puzzle', 617 | 612: 'jinrikisha, ricksha, rickshaw', 618 | 613: 'joystick', 619 | 614: 'kimono', 620 | 615: 'knee pad', 621 | 616: 'knot', 622 | 617: 'lab coat, laboratory coat', 623 | 618: 'ladle', 624 | 619: 'lampshade, lamp shade', 625 | 620: 'laptop, laptop computer', 626 | 621: 'lawn mower, mower', 627 | 622: 'lens cap, lens cover', 628 | 623: 'letter opener, paper knife, paperknife', 629 | 624: 'library', 630 | 625: 'lifeboat', 631 | 626: 'lighter, light, igniter, ignitor', 632 | 627: 'limousine, limo', 633 | 628: 'liner, ocean liner', 634 | 629: 'lipstick, lip rouge', 635 | 630: 'Loafer', 636 | 631: 'lotion', 637 | 632: 'loudspeaker, speaker, speaker unit, loudspeaker system, speaker system', 638 | 633: "loupe, jeweler's loupe", 639 | 634: 'lumbermill, sawmill', 640 | 635: 'magnetic compass', 641 | 636: 'mailbag, postbag', 642 | 637: 'mailbox, letter box', 643 | 638: 'maillot', 644 | 639: 'maillot, tank suit', 645 | 640: 'manhole cover', 646 | 641: 'maraca', 647 | 642: 'marimba, xylophone', 648 | 643: 'mask', 649 | 644: 'matchstick', 650 | 645: 'maypole', 651 | 646: 'maze, labyrinth', 652 | 647: 'measuring cup', 653 | 648: 'medicine chest, medicine cabinet', 654 | 649: 'megalith, megalithic structure', 655 | 650: 'microphone, mike', 656 | 651: 'microwave, microwave oven', 657 | 652: 'military uniform', 658 | 653: 'milk can', 659 | 654: 'minibus', 660 | 655: 'miniskirt, mini', 661 | 656: 'minivan', 662 | 657: 'missile', 663 | 658: 'mitten', 664 | 659: 'mixing bowl', 665 | 660: 'mobile home, manufactured home', 666 | 661: 'Model T', 667 | 662: 'modem', 668 | 663: 'monastery', 669 | 664: 'monitor', 670 | 665: 'moped', 671 | 666: 'mortar', 672 | 667: 'mortarboard', 673 | 668: 'mosque', 674 | 669: 'mosquito net', 675 | 670: 'motor scooter, scooter', 676 | 671: 'mountain bike, all-terrain bike, off-roader', 677 | 672: 'mountain tent', 678 | 673: 'mouse, computer mouse', 679 | 674: 'mousetrap', 680 | 675: 'moving van', 681 | 676: 'muzzle', 682 | 677: 'nail', 683 | 678: 'neck brace', 684 | 679: 'necklace', 685 | 680: 'nipple', 686 | 681: 'notebook, notebook computer', 687 | 682: 'obelisk', 688 | 683: 'oboe, hautboy, hautbois', 689 | 684: 'ocarina, sweet potato', 690 | 685: 'odometer, hodometer, mileometer, milometer', 691 | 686: 'oil filter', 692 | 687: 'organ, pipe organ', 693 | 688: 'oscilloscope, scope, cathode-ray oscilloscope, CRO', 694 | 689: 'overskirt', 695 | 690: 'oxcart', 696 | 691: 'oxygen mask', 697 | 692: 'packet', 698 | 693: 'paddle, boat paddle', 699 | 694: 'paddlewheel, paddle wheel', 700 | 695: 'padlock', 701 | 696: 'paintbrush', 702 | 697: "pajama, pyjama, pj's, jammies", 703 | 698: 'palace', 704 | 699: 'panpipe, pandean pipe, syrinx', 705 | 700: 'paper towel', 706 | 701: 'parachute, chute', 707 | 702: 'parallel bars, bars', 708 | 703: 'park bench', 709 | 704: 'parking meter', 710 | 705: 'passenger car, coach, carriage', 711 | 706: 'patio, terrace', 712 | 707: 'pay-phone, pay-station', 713 | 708: 'pedestal, plinth, footstall', 714 | 709: 'pencil box, pencil case', 715 | 710: 'pencil sharpener', 716 | 711: 'perfume, essence', 717 | 712: 'Petri dish', 718 | 713: 'photocopier', 719 | 714: 'pick, plectrum, plectron', 720 | 715: 'pickelhaube', 721 | 716: 'picket fence, paling', 722 | 717: 'pickup, pickup truck', 723 | 718: 'pier', 724 | 719: 'piggy bank, penny bank', 725 | 720: 'pill bottle', 726 | 721: 'pillow', 727 | 722: 'ping-pong ball', 728 | 723: 'pinwheel', 729 | 724: 'pirate, pirate ship', 730 | 725: 'pitcher, ewer', 731 | 726: "plane, carpenter's plane, woodworking plane", 732 | 727: 'planetarium', 733 | 728: 'plastic bag', 734 | 729: 'plate rack', 735 | 730: 'plow, plough', 736 | 731: "plunger, plumber's helper", 737 | 732: 'Polaroid camera, Polaroid Land camera', 738 | 733: 'pole', 739 | 734: 'police van, police wagon, paddy wagon, patrol wagon, wagon, black Maria', 740 | 735: 'poncho', 741 | 736: 'pool table, billiard table, snooker table', 742 | 737: 'pop bottle, soda bottle', 743 | 738: 'pot, flowerpot', 744 | 739: "potter's wheel", 745 | 740: 'power drill', 746 | 741: 'prayer rug, prayer mat', 747 | 742: 'printer', 748 | 743: 'prison, prison house', 749 | 744: 'projectile, missile', 750 | 745: 'projector', 751 | 746: 'puck, hockey puck', 752 | 747: 'punching bag, punch bag, punching ball, punchball', 753 | 748: 'purse', 754 | 749: 'quill, quill pen', 755 | 750: 'quilt, comforter, comfort, puff', 756 | 751: 'racer, race car, racing car', 757 | 752: 'racket, racquet', 758 | 753: 'radiator', 759 | 754: 'radio, wireless', 760 | 755: 'radio telescope, radio reflector', 761 | 756: 'rain barrel', 762 | 757: 'recreational vehicle, RV, R.V.', 763 | 758: 'reel', 764 | 759: 'reflex camera', 765 | 760: 'refrigerator, icebox', 766 | 761: 'remote control, remote', 767 | 762: 'restaurant, eating house, eating place, eatery', 768 | 763: 'revolver, six-gun, six-shooter', 769 | 764: 'rifle', 770 | 765: 'rocking chair, rocker', 771 | 766: 'rotisserie', 772 | 767: 'rubber eraser, rubber, pencil eraser', 773 | 768: 'rugby ball', 774 | 769: 'rule, ruler', 775 | 770: 'running shoe', 776 | 771: 'safe', 777 | 772: 'safety pin', 778 | 773: 'saltshaker, salt shaker', 779 | 774: 'sandal', 780 | 775: 'sarong', 781 | 776: 'sax, saxophone', 782 | 777: 'scabbard', 783 | 778: 'scale, weighing machine', 784 | 779: 'school bus', 785 | 780: 'schooner', 786 | 781: 'scoreboard', 787 | 782: 'screen, CRT screen', 788 | 783: 'screw', 789 | 784: 'screwdriver', 790 | 785: 'seat belt, seatbelt', 791 | 786: 'sewing machine', 792 | 787: 'shield, buckler', 793 | 788: 'shoe shop, shoe-shop, shoe store', 794 | 789: 'shoji', 795 | 790: 'shopping basket', 796 | 791: 'shopping cart', 797 | 792: 'shovel', 798 | 793: 'shower cap', 799 | 794: 'shower curtain', 800 | 795: 'ski', 801 | 796: 'ski mask', 802 | 797: 'sleeping bag', 803 | 798: 'slide rule, slipstick', 804 | 799: 'sliding door', 805 | 800: 'slot, one-armed bandit', 806 | 801: 'snorkel', 807 | 802: 'snowmobile', 808 | 803: 'snowplow, snowplough', 809 | 804: 'soap dispenser', 810 | 805: 'soccer ball', 811 | 806: 'sock', 812 | 807: 'solar dish, solar collector, solar furnace', 813 | 808: 'sombrero', 814 | 809: 'soup bowl', 815 | 810: 'space bar', 816 | 811: 'space heater', 817 | 812: 'space shuttle', 818 | 813: 'spatula', 819 | 814: 'speedboat', 820 | 815: "spider web, spider's web", 821 | 816: 'spindle', 822 | 817: 'sports car, sport car', 823 | 818: 'spotlight, spot', 824 | 819: 'stage', 825 | 820: 'steam locomotive', 826 | 821: 'steel arch bridge', 827 | 822: 'steel drum', 828 | 823: 'stethoscope', 829 | 824: 'stole', 830 | 825: 'stone wall', 831 | 826: 'stopwatch, stop watch', 832 | 827: 'stove', 833 | 828: 'strainer', 834 | 829: 'streetcar, tram, tramcar, trolley, trolley car', 835 | 830: 'stretcher', 836 | 831: 'studio couch, day bed', 837 | 832: 'stupa, tope', 838 | 833: 'submarine, pigboat, sub, U-boat', 839 | 834: 'suit, suit of clothes', 840 | 835: 'sundial', 841 | 836: 'sunglass', 842 | 837: 'sunglasses, dark glasses, shades', 843 | 838: 'sunscreen, sunblock, sun blocker', 844 | 839: 'suspension bridge', 845 | 840: 'swab, swob, mop', 846 | 841: 'sweatshirt', 847 | 842: 'swimming trunks, bathing trunks', 848 | 843: 'swing', 849 | 844: 'switch, electric switch, electrical switch', 850 | 845: 'syringe', 851 | 846: 'table lamp', 852 | 847: 'tank, army tank, armored combat vehicle, armoured combat vehicle', 853 | 848: 'tape player', 854 | 849: 'teapot', 855 | 850: 'teddy, teddy bear', 856 | 851: 'television, television system', 857 | 852: 'tennis ball', 858 | 853: 'thatch, thatched roof', 859 | 854: 'theater curtain, theatre curtain', 860 | 855: 'thimble', 861 | 856: 'thresher, thrasher, threshing machine', 862 | 857: 'throne', 863 | 858: 'tile roof', 864 | 859: 'toaster', 865 | 860: 'tobacco shop, tobacconist shop, tobacconist', 866 | 861: 'toilet seat', 867 | 862: 'torch', 868 | 863: 'totem pole', 869 | 864: 'tow truck, tow car, wrecker', 870 | 865: 'toyshop', 871 | 866: 'tractor', 872 | 867: 'trailer truck, tractor trailer, trucking rig, rig, articulated lorry, semi', 873 | 868: 'tray', 874 | 869: 'trench coat', 875 | 870: 'tricycle, trike, velocipede', 876 | 871: 'trimaran', 877 | 872: 'tripod', 878 | 873: 'triumphal arch', 879 | 874: 'trolleybus, trolley coach, trackless trolley', 880 | 875: 'trombone', 881 | 876: 'tub, vat', 882 | 877: 'turnstile', 883 | 878: 'typewriter keyboard', 884 | 879: 'umbrella', 885 | 880: 'unicycle, monocycle', 886 | 881: 'upright, upright piano', 887 | 882: 'vacuum, vacuum cleaner', 888 | 883: 'vase', 889 | 884: 'vault', 890 | 885: 'velvet', 891 | 886: 'vending machine', 892 | 887: 'vestment', 893 | 888: 'viaduct', 894 | 889: 'violin, fiddle', 895 | 890: 'volleyball', 896 | 891: 'waffle iron', 897 | 892: 'wall clock', 898 | 893: 'wallet, billfold, notecase, pocketbook', 899 | 894: 'wardrobe, closet, press', 900 | 895: 'warplane, military plane', 901 | 896: 'washbasin, handbasin, washbowl, lavabo, wash-hand basin', 902 | 897: 'washer, automatic washer, washing machine', 903 | 898: 'water bottle', 904 | 899: 'water jug', 905 | 900: 'water tower', 906 | 901: 'whiskey jug', 907 | 902: 'whistle', 908 | 903: 'wig', 909 | 904: 'window screen', 910 | 905: 'window shade', 911 | 906: 'Windsor tie', 912 | 907: 'wine bottle', 913 | 908: 'wing', 914 | 909: 'wok', 915 | 910: 'wooden spoon', 916 | 911: 'wool, woolen, woollen', 917 | 912: 'worm fence, snake fence, snake-rail fence, Virginia fence', 918 | 913: 'wreck', 919 | 914: 'yawl', 920 | 915: 'yurt', 921 | 916: 'web site, website, internet site, site', 922 | 917: 'comic book', 923 | 918: 'crossword puzzle, crossword', 924 | 919: 'street sign', 925 | 920: 'traffic light, traffic signal, stoplight', 926 | 921: 'book jacket, dust cover, dust jacket, dust wrapper', 927 | 922: 'menu', 928 | 923: 'plate', 929 | 924: 'guacamole', 930 | 925: 'consomme', 931 | 926: 'hot pot, hotpot', 932 | 927: 'trifle', 933 | 928: 'ice cream, icecream', 934 | 929: 'ice lolly, lolly, lollipop, popsicle', 935 | 930: 'French loaf', 936 | 931: 'bagel, beigel', 937 | 932: 'pretzel', 938 | 933: 'cheeseburger', 939 | 934: 'hotdog, hot dog, red hot', 940 | 935: 'mashed potato', 941 | 936: 'head cabbage', 942 | 937: 'broccoli', 943 | 938: 'cauliflower', 944 | 939: 'zucchini, courgette', 945 | 940: 'spaghetti squash', 946 | 941: 'acorn squash', 947 | 942: 'butternut squash', 948 | 943: 'cucumber, cuke', 949 | 944: 'artichoke, globe artichoke', 950 | 945: 'bell pepper', 951 | 946: 'cardoon', 952 | 947: 'mushroom', 953 | 948: 'Granny Smith', 954 | 949: 'strawberry', 955 | 950: 'orange', 956 | 951: 'lemon', 957 | 952: 'fig', 958 | 953: 'pineapple, ananas', 959 | 954: 'banana', 960 | 955: 'jackfruit, jak, jack', 961 | 956: 'custard apple', 962 | 957: 'pomegranate', 963 | 958: 'hay', 964 | 959: 'carbonara', 965 | 960: 'chocolate sauce, chocolate syrup', 966 | 961: 'dough', 967 | 962: 'meat loaf, meatloaf', 968 | 963: 'pizza, pizza pie', 969 | 964: 'potpie', 970 | 965: 'burrito', 971 | 966: 'red wine', 972 | 967: 'espresso', 973 | 968: 'cup', 974 | 969: 'eggnog', 975 | 970: 'alp', 976 | 971: 'bubble', 977 | 972: 'cliff, drop, drop-off', 978 | 973: 'coral reef', 979 | 974: 'geyser', 980 | 975: 'lakeside, lakeshore', 981 | 976: 'promontory, headland, head, foreland', 982 | 977: 'sandbar, sand bar', 983 | 978: 'seashore, coast, seacoast, sea-coast', 984 | 979: 'valley, vale', 985 | 980: 'volcano', 986 | 981: 'ballplayer, baseball player', 987 | 982: 'groom, bridegroom', 988 | 983: 'scuba diver', 989 | 984: 'rapeseed', 990 | 985: 'daisy', 991 | 986: "yellow lady's slipper, yellow lady-slipper, Cypripedium calceolus, Cypripedium parviflorum", 992 | 987: 'corn', 993 | 988: 'acorn', 994 | 989: 'hip, rose hip, rosehip', 995 | 990: 'buckeye, horse chestnut, conker', 996 | 991: 'coral fungus', 997 | 992: 'agaric', 998 | 993: 'gyromitra', 999 | 994: 'stinkhorn, carrion fungus', 1000 | 995: 'earthstar', 1001 | 996: 'hen-of-the-woods, hen of the woods, Polyporus frondosus, Grifola frondosa', 1002 | 997: 'bolete', 1003 | 998: 'ear, spike, capitulum', 1004 | 999: 'toilet tissue, toilet paper, bathroom tissue' 1005 | }; 1006 | -------------------------------------------------------------------------------- /src/pages/About.css: -------------------------------------------------------------------------------- 1 | .About { 2 | margin-top: 20px; 3 | text-align: left; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/About.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './About.css'; 3 | 4 | /** 5 | * Class to handle the rendering of the Home page. 6 | * @extends React.Component 7 | */ 8 | export default class Home extends Component { 9 | render() { 10 | return ( 11 |
12 |

About

13 |

14 | This is a TensorFlow.js web application where users can classify images selected locally 15 | or taken with their device's camera. The app uses TensorFlow.js and a pre-trained model 16 | converted to the TensorFlow.js format to provide the inference capabilities. 17 | This model is saved locally in the browser using IndexedDB. A service worker is also used 18 | to provide offline capabilities. 19 |

20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/Classify.css: -------------------------------------------------------------------------------- 1 | .Classify { 2 | margin-top: 20px; 3 | } 4 | 5 | .Classify .imagefile { 6 | width: 0.1px; 7 | height: 0.1px; 8 | opacity: 0; 9 | overflow: hidden; 10 | position: absolute; 11 | z-index: -1; 12 | } 13 | 14 | .Classify .imagelabel { 15 | font-size: 16px; 16 | color: #555; 17 | font-weight: 500; 18 | height: 34px; 19 | padding: 6px 12px; 20 | background-color: #fff; 21 | display: inline-block; 22 | cursor: pointer; 23 | border: 1px solid #ccc; 24 | border-radius: 4px; 25 | margin-bottom: 0px; 26 | } 27 | 28 | .Classify .tab-pane { 29 | padding-top: 40px; 30 | } 31 | 32 | .Classify #no-webcam { 33 | display: none; 34 | text-align: center; 35 | font-size: 22px; 36 | padding: 10px; 37 | line-height: 30px; 38 | } 39 | .Classify #no-webcam .camera-icon { 40 | font-size: 40px; 41 | display: block; 42 | padding-bottom: 24px; 43 | } 44 | 45 | .Classify .webcam-box-outer { 46 | box-sizing: border-box; 47 | display: inline-block; 48 | } 49 | 50 | .Classify .webcam-box-inner { 51 | border: 1px solid #585858; 52 | box-sizing: border-box; 53 | display: flex; 54 | justify-content: center; 55 | overflow: hidden; 56 | width: 280px; 57 | } 58 | 59 | .Classify #webcam { 60 | height: 280px; 61 | } 62 | 63 | .Classify .button-container { 64 | margin-top: 18px; 65 | } 66 | 67 | .Classify .classify-panel-header { 68 | margin-top: 10px; 69 | width: 100%; 70 | background-color: rgba(0,0,0,.03);; 71 | color: #585858; 72 | border-color: rgba(0,0,0,.125);; 73 | } 74 | 75 | .Classify .classify-panel-header:hover { 76 | background-color: rgba(0,0,0,.1);; 77 | color: #585858; 78 | } 79 | 80 | .Classify .classify-panel-header:active { 81 | background-color: #585858 !important; 82 | color: #fff !important; 83 | } 84 | 85 | .Classify #photo-selection-pane { 86 | padding: 12px; 87 | background: #fcfcfc; 88 | padding-top: 12px; 89 | padding-bottom: 12px; 90 | } 91 | 92 | .Classify .panel-arrow { 93 | float: right; 94 | } 95 | 96 | .Classify .classification-results { 97 | margin-top: 18px; 98 | } 99 | 100 | .Classify .loading-model-text { 101 | padding-left: 12px; 102 | font-size: 36px; 103 | font-weight: 600; 104 | } 105 | -------------------------------------------------------------------------------- /src/pages/Classify.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { 3 | Alert, Button, Collapse, Container, Form, Spinner, ListGroup, Tabs, Tab 4 | } from 'react-bootstrap'; 5 | import { FaCamera, FaChevronDown, FaChevronRight } from 'react-icons/fa'; 6 | import { openDB } from 'idb'; 7 | import Cropper from 'react-cropper'; 8 | import * as tf from '@tensorflow/tfjs'; 9 | import LoadButton from '../components/LoadButton'; 10 | import { MODEL_CLASSES } from '../model/classes'; 11 | import config from '../config'; 12 | import './Classify.css'; 13 | import 'cropperjs/dist/cropper.css'; 14 | 15 | 16 | const MODEL_PATH = '/model/model.json'; 17 | const IMAGE_SIZE = 224; 18 | const CANVAS_SIZE = 224; 19 | const TOPK_PREDICTIONS = 5; 20 | 21 | const INDEXEDDB_DB = 'tensorflowjs'; 22 | const INDEXEDDB_STORE = 'model_info_store'; 23 | const INDEXEDDB_KEY = 'web-model'; 24 | 25 | /** 26 | * Class to handle the rendering of the Classify page. 27 | * @extends React.Component 28 | */ 29 | export default class Classify extends Component { 30 | 31 | constructor(props) { 32 | super(props); 33 | 34 | this.webcam = null; 35 | this.model = null; 36 | this.modelLastUpdated = null; 37 | 38 | this.state = { 39 | modelLoaded: false, 40 | filename: '', 41 | isModelLoading: false, 42 | isClassifying: false, 43 | predictions: [], 44 | photoSettingsOpen: true, 45 | modelUpdateAvailable: false, 46 | showModelUpdateAlert: false, 47 | showModelUpdateSuccess: false, 48 | isDownloadingModel: false 49 | }; 50 | } 51 | 52 | async componentDidMount() { 53 | if (('indexedDB' in window)) { 54 | try { 55 | this.model = await tf.loadLayersModel('indexeddb://' + INDEXEDDB_KEY); 56 | 57 | // Safe to assume tensorflowjs database and related object store exists. 58 | // Get the date when the model was saved. 59 | try { 60 | const db = await openDB(INDEXEDDB_DB, 1, ); 61 | const item = await db.transaction(INDEXEDDB_STORE) 62 | .objectStore(INDEXEDDB_STORE) 63 | .get(INDEXEDDB_KEY); 64 | const dateSaved = new Date(item.modelArtifactsInfo.dateSaved); 65 | await this.getModelInfo(); 66 | console.log(this.modelLastUpdated); 67 | if (!this.modelLastUpdated || dateSaved >= new Date(this.modelLastUpdated).getTime()) { 68 | console.log('Using saved model'); 69 | } 70 | else { 71 | this.setState({ 72 | modelUpdateAvailable: true, 73 | showModelUpdateAlert: true, 74 | }); 75 | } 76 | 77 | } 78 | catch (error) { 79 | console.warn(error); 80 | console.warn('Could not retrieve when model was saved.'); 81 | } 82 | 83 | } 84 | // If error here, assume that the object store doesn't exist and the model currently isn't 85 | // saved in IndexedDB. 86 | catch (error) { 87 | console.log('Not found in IndexedDB. Loading and saving...'); 88 | console.log(error); 89 | this.model = await tf.loadLayersModel(MODEL_PATH); 90 | await this.model.save('indexeddb://' + INDEXEDDB_KEY); 91 | } 92 | } 93 | // If no IndexedDB, then just download like normal. 94 | else { 95 | console.warn('IndexedDB not supported.'); 96 | this.model = await tf.loadLayersModel(MODEL_PATH); 97 | } 98 | 99 | this.setState({ modelLoaded: true }); 100 | this.initWebcam(); 101 | 102 | // Warm up model. 103 | let prediction = tf.tidy(() => this.model.predict(tf.zeros([1, IMAGE_SIZE, IMAGE_SIZE, 3]))); 104 | prediction.dispose(); 105 | } 106 | 107 | async componentWillUnmount() { 108 | if (this.webcam) { 109 | this.webcam.stop(); 110 | } 111 | 112 | // Attempt to dispose of the model. 113 | try { 114 | this.model.dispose(); 115 | } 116 | catch (e) { 117 | // Assume model is not loaded or already disposed. 118 | } 119 | } 120 | 121 | initWebcam = async () => { 122 | try { 123 | this.webcam = await tf.data.webcam( 124 | this.refs.webcam, 125 | {resizeWidth: CANVAS_SIZE, resizeHeight: CANVAS_SIZE, facingMode: 'environment'} 126 | ); 127 | } 128 | catch (e) { 129 | this.refs.noWebcam.style.display = 'block'; 130 | } 131 | } 132 | 133 | startWebcam = async () => { 134 | if (this.webcam) { 135 | this.webcam.start(); 136 | } 137 | } 138 | 139 | stopWebcam = async () => { 140 | if (this.webcam) { 141 | this.webcam.stop(); 142 | } 143 | } 144 | 145 | getModelInfo = async () => { 146 | await fetch(`${config.API_ENDPOINT}/model_info`, { 147 | method: 'GET', 148 | }) 149 | .then(async (response) => { 150 | await response.json().then((data) => { 151 | this.modelLastUpdated = data.last_updated; 152 | }) 153 | .catch((err) => { 154 | console.log('Unable to get parse model info.'); 155 | }); 156 | }) 157 | .catch((err) => { 158 | console.log('Unable to get model info'); 159 | }); 160 | } 161 | 162 | updateModel = async () => { 163 | // Get the latest model from the server and refresh the one saved in IndexedDB. 164 | console.log('Updating the model: ' + INDEXEDDB_KEY); 165 | this.setState({ isDownloadingModel: true }); 166 | this.model = await tf.loadLayersModel(MODEL_PATH); 167 | await this.model.save('indexeddb://' + INDEXEDDB_KEY); 168 | this.setState({ 169 | isDownloadingModel: false, 170 | modelUpdateAvailable: false, 171 | showModelUpdateAlert: false, 172 | showModelUpdateSuccess: true 173 | }); 174 | } 175 | 176 | classifyLocalImage = async () => { 177 | this.setState({ isClassifying: true }); 178 | 179 | const croppedCanvas = this.refs.cropper.getCroppedCanvas(); 180 | const image = tf.tidy( () => tf.browser.fromPixels(croppedCanvas).toFloat()); 181 | 182 | // Process and resize image before passing in to model. 183 | const imageData = await this.processImage(image); 184 | const resizedImage = tf.image.resizeBilinear(imageData, [IMAGE_SIZE, IMAGE_SIZE]); 185 | 186 | const logits = this.model.predict(resizedImage); 187 | const probabilities = await logits.data(); 188 | const preds = await this.getTopKClasses(probabilities, TOPK_PREDICTIONS); 189 | 190 | this.setState({ 191 | predictions: preds, 192 | isClassifying: false, 193 | photoSettingsOpen: !this.state.photoSettingsOpen 194 | }); 195 | 196 | // Draw thumbnail to UI. 197 | const context = this.refs.canvas.getContext('2d'); 198 | const ratioX = CANVAS_SIZE / croppedCanvas.width; 199 | const ratioY = CANVAS_SIZE / croppedCanvas.height; 200 | const ratio = Math.min(ratioX, ratioY); 201 | context.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); 202 | context.drawImage(croppedCanvas, 0, 0, 203 | croppedCanvas.width * ratio, croppedCanvas.height * ratio); 204 | 205 | // Dispose of tensors we are finished with. 206 | image.dispose(); 207 | imageData.dispose(); 208 | resizedImage.dispose(); 209 | logits.dispose(); 210 | } 211 | 212 | classifyWebcamImage = async () => { 213 | this.setState({ isClassifying: true }); 214 | 215 | const imageCapture = await this.webcam.capture(); 216 | 217 | const resized = tf.image.resizeBilinear(imageCapture, [IMAGE_SIZE, IMAGE_SIZE]); 218 | const imageData = await this.processImage(resized); 219 | const logits = this.model.predict(imageData); 220 | const probabilities = await logits.data(); 221 | const preds = await this.getTopKClasses(probabilities, TOPK_PREDICTIONS); 222 | 223 | this.setState({ 224 | predictions: preds, 225 | isClassifying: false, 226 | photoSettingsOpen: !this.state.photoSettingsOpen 227 | }); 228 | 229 | // Draw thumbnail to UI. 230 | const tensorData = tf.tidy(() => imageCapture.toFloat().div(255)); 231 | await tf.browser.toPixels(tensorData, this.refs.canvas); 232 | 233 | // Dispose of tensors we are finished with. 234 | resized.dispose(); 235 | imageCapture.dispose(); 236 | imageData.dispose(); 237 | logits.dispose(); 238 | tensorData.dispose(); 239 | } 240 | 241 | processImage = async (image) => { 242 | return tf.tidy(() => image.expandDims(0).toFloat().div(127).sub(1)); 243 | } 244 | 245 | /** 246 | * Computes the probabilities of the topK classes given logits by computing 247 | * softmax to get probabilities and then sorting the probabilities. 248 | * @param logits Tensor representing the logits from MobileNet. 249 | * @param topK The number of top predictions to show. 250 | */ 251 | getTopKClasses = async (values, topK) => { 252 | const valuesAndIndices = []; 253 | for (let i = 0; i < values.length; i++) { 254 | valuesAndIndices.push({value: values[i], index: i}); 255 | } 256 | valuesAndIndices.sort((a, b) => { 257 | return b.value - a.value; 258 | }); 259 | const topkValues = new Float32Array(topK); 260 | const topkIndices = new Int32Array(topK); 261 | for (let i = 0; i < topK; i++) { 262 | topkValues[i] = valuesAndIndices[i].value; 263 | topkIndices[i] = valuesAndIndices[i].index; 264 | } 265 | 266 | const topClassesAndProbs = []; 267 | for (let i = 0; i < topkIndices.length; i++) { 268 | topClassesAndProbs.push({ 269 | className: MODEL_CLASSES[topkIndices[i]], 270 | probability: (topkValues[i] * 100).toFixed(2) 271 | }); 272 | } 273 | return topClassesAndProbs; 274 | } 275 | 276 | handlePanelClick = event => { 277 | this.setState({ photoSettingsOpen: !this.state.photoSettingsOpen }); 278 | } 279 | 280 | handleFileChange = event => { 281 | if (event.target.files && event.target.files.length > 0) { 282 | this.setState({ 283 | file: URL.createObjectURL(event.target.files[0]), 284 | filename: event.target.files[0].name 285 | }); 286 | } 287 | } 288 | 289 | handleTabSelect = activeKey => { 290 | switch(activeKey) { 291 | case 'camera': 292 | this.startWebcam(); 293 | break; 294 | case 'localfile': 295 | this.setState({filename: null, file: null}); 296 | this.stopWebcam(); 297 | break; 298 | default: 299 | } 300 | } 301 | 302 | render() { 303 | return ( 304 |
305 | 306 | { !this.state.modelLoaded && 307 | 308 | 309 | Loading... 310 | 311 | {' '}Loading Model 312 | 313 | } 314 | 315 | { this.state.modelLoaded && 316 | 317 | 331 | 332 |
333 | { this.state.modelUpdateAvailable && this.state.showModelUpdateAlert && 334 | 335 | this.setState({ showModelUpdateAlert: false})} 339 | dismissible> 340 | An update for the {this.state.modelType} model is available. 341 |
342 | {!this.state.isDownloadingModel && 343 | 347 | } 348 | {this.state.isDownloadingModel && 349 |
350 | 351 | Downloading... 352 | 353 | {' '}Downloading... 354 |
355 | } 356 |
357 |
358 |
359 | } 360 | {this.state.showModelUpdateSuccess && 361 | 362 | this.setState({ showModelUpdateSuccess: false})} 364 | dismissible> 365 | The {this.state.modelType} model has been updated! 366 | 367 | 368 | } 369 | 371 | 372 |
373 | 374 | No camera found.
375 | Please use a device with a camera, or upload an image instead. 376 |
377 |
378 |
379 | 382 |
383 |
384 |
385 | 393 |
394 |
395 | 396 | 397 | Select Image File
398 | 399 | {this.state.filename ? this.state.filename : 'Browse...'} 400 | 401 | 406 |
407 | { this.state.file && 408 | 409 |
410 | 418 |
419 |
420 | 429 |
430 |
431 | } 432 |
433 |
434 |
435 |
436 | { this.state.predictions.length > 0 && 437 |
438 |

Predictions

439 | 440 |
441 | 442 | {this.state.predictions.map((category) => { 443 | return ( 444 | 445 | {category.className} {category.probability}% 446 | ); 447 | })} 448 | 449 |
450 | } 451 |
452 | } 453 |
454 | ); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/pages/NotFound.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/tfjs-web-app/efc27b8505c54ef75458439dcdda412c09815671/src/pages/NotFound.css -------------------------------------------------------------------------------- /src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './NotFound.css'; 3 | 4 | /** 5 | * This is rendered when a route is not found (404). 6 | */ 7 | export default () => 8 |
9 |

404

10 |

The page you were looking for is not here.

11 | Go Home 12 |
; 13 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | // A new service worker has previously finished installing, and is now waiting. 62 | if (registration.waiting && registration.active) { 63 | newerSwAvailable(registration.waiting); 64 | } 65 | registration.onupdatefound = () => { 66 | const installingWorker = registration.installing; 67 | if (installingWorker == null) { 68 | return; 69 | } 70 | installingWorker.onstatechange = () => { 71 | if (installingWorker.state === 'installed') { 72 | if (navigator.serviceWorker.controller) { 73 | newerSwAvailable(installingWorker); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // "Content is cached for offline use." message. 78 | console.log('Content is cached for offline use.'); 79 | 80 | // Execute callback 81 | if (config && config.onSuccess) { 82 | config.onSuccess(registration); 83 | } 84 | } 85 | } 86 | }; 87 | }; 88 | function newerSwAvailable(sw){ 89 | // At this point, the updated precached content has been fetched, 90 | // but the previous service worker will still serve the older 91 | // content until all client tabs are closed. 92 | console.log( 93 | 'New content is available and will be used when all ' + 94 | 'tabs for this page are closed. See http://bit.ly/CRA2-PWA.' 95 | ); 96 | if (config && config.onUpdate) { 97 | config.onUpdate(registration, sw); 98 | } 99 | } 100 | }) 101 | .catch(error => { 102 | console.error('Error during service worker registration:', error); 103 | }); 104 | } 105 | 106 | function checkValidServiceWorker(swUrl, config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl) 109 | .then(response => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get('content-type'); 112 | if ( 113 | response.status === 404 || 114 | (contentType != null && contentType.indexOf('javascript') === -1) 115 | ) { 116 | // No service worker found. Probably a different app. Reload the page. 117 | navigator.serviceWorker.ready.then(registration => { 118 | registration.unregister().then(() => { 119 | window.location.reload(); 120 | }); 121 | }); 122 | } else { 123 | // Service worker found. Proceed as normal. 124 | registerValidSW(swUrl, config); 125 | } 126 | }) 127 | .catch(() => { 128 | console.log( 129 | 'No internet connection found. App is running in offline mode.' 130 | ); 131 | }); 132 | } 133 | 134 | export function unregister() { 135 | if ('serviceWorker' in navigator) { 136 | navigator.serviceWorker.ready.then(registration => { 137 | registration.unregister(); 138 | }); 139 | } 140 | } 141 | --------------------------------------------------------------------------------