├── .gitignore ├── License.txt ├── README.md ├── Training_and_converting_to_js.ipynb ├── index.html └── static ├── classifier.css └── classifier.js /.gitignore: -------------------------------------------------------------------------------- 1 | weights/ -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the \"License\"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | https://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an \"AS IS\" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A cat dog image classifier deployed client side using javascript 2 | 3 | ![image](https://novasush.com/blog/images/cat_vs_dog_in_javascript.jpg) 4 | 5 | # Instructions 6 | 1. Train a keras classifier and save its weights 7 | 2. install `tensorflowjs` python library 8 | ```bash 9 | pip install tensorflowjs 10 | ``` 11 | 3. Use tensorflowjs converter tool to convert h5 weights to js format. 12 | ```bash 13 | tensorflowjs_converter --input_format=keras ./model.h5 ./jsweights 14 | ``` 15 | * Make sure the .h5 weights location is correct and also destination path exists. 16 | 17 | ### After running above command you will see `model.json` and `.bin` files, tensorflowjs converter dumps model architecture in `model.json` and it's weights in `.bin` files. 18 | 19 | # setup 20 | * Place your converted weights inside `weights` folder 21 | 22 | * Open `classifier.js` and update `model.json` path according to your weights folder 23 | 24 | * After everything is setup up perfectly run below command inside the directory. 25 | 26 | ```bash 27 | python3 -m http.server 28 | ``` 29 | 30 | * navigate to [localhost](http:127.0.0.1:8000) 31 | 32 | # Output 33 | 34 | ![courage_garfield](https://novasush.com/blog/images/courage_garfield.jpg) 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cat Dog Classifier in Javascript 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Cat Dog Classifier in Javascript

15 |
16 |
17 | 18 |
19 | 20 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | 36 |
37 | 38 | 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /static/classifier.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 3 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | background-color: #f8f8f8; 6 | } 7 | 8 | /* Global button style */ 9 | .button { 10 | font-family: inherit; 11 | text-align: center; 12 | cursor: pointer; 13 | border: none; 14 | text-decoration: none; 15 | outline: none; 16 | color: #ffffff; 17 | background-color: rgb(0, 120, 212); 18 | padding: 0.5rem 1.2rem; 19 | border-radius: 2px; 20 | font-size: 1rem; 21 | min-width: 6rem; 22 | margin-left: 0.5rem; 23 | margin-right: 0.5rem; 24 | } 25 | 26 | .button:hover { 27 | background-color: rgb(16, 110, 190); 28 | } 29 | 30 | .button.disabled { 31 | pointer-events: none; 32 | background-color: #cccccc; 33 | color: #666666; 34 | } 35 | 36 | /* Main section */ 37 | 38 | .main { 39 | box-sizing: border-box; 40 | display: flex; 41 | flex-direction: column; 42 | align-items: center; 43 | } 44 | 45 | .main .title h3 { 46 | font-size: 2.3rem; 47 | font-weight: 100; 48 | margin: 0.8rem 0; 49 | } 50 | 51 | .panel{ 52 | display: flex; 53 | flex-direction: row; 54 | justify-content: center; 55 | } 56 | 57 | .button_group { 58 | margin-bottom: 2rem; 59 | display: flex; 60 | flex-direction: row; 61 | justify-content: center; 62 | } 63 | 64 | .hidden { 65 | display: none; 66 | } 67 | 68 | .reveal { 69 | opacity: 0; 70 | } 71 | 72 | .reveal:hover { 73 | opacity: 0.2; 74 | } 75 | 76 | /* Upload box */ 77 | .upload-box { 78 | font-size: 0.8rem; 79 | color: #666666; 80 | cursor: pointer; 81 | width: 16rem; 82 | height: 10rem; 83 | background: #fff; 84 | border: 0.1rem dashed #838388; 85 | border-radius: 0.4rem; 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | flex-direction: column; 90 | margin: 1rem 0 2rem 0; 91 | } 92 | 93 | .upload-box.dragover { 94 | /* background-color: grey; */ 95 | color: #eeeeee; 96 | border: 0.1rem solid rgb(0, 120, 212); 97 | box-shadow: inset 0 0 0 0.1rem rgb(0, 120, 212); 98 | } 99 | 100 | .upload-box:hover { 101 | border-color: rgb(0, 120, 212); 102 | } 103 | 104 | .upload-box #image-preview { 105 | max-width: 14rem; 106 | max-height: 8rem; 107 | box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.19); 108 | } 109 | 110 | #image-result { 111 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 112 | max-height: 20rem; 113 | } 114 | 115 | #image-box { 116 | position: relative; 117 | width: auto; 118 | float: center; 119 | margin-bottom: 2rem; 120 | display: flex; 121 | flex-direction: row; 122 | justify-content: center; 123 | } 124 | 125 | #image-display { 126 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 127 | max-height: 20rem; 128 | } 129 | 130 | #image-display.loading { 131 | filter: brightness(30%); 132 | } 133 | 134 | #pred-result { 135 | color: black; 136 | font-size: 2.5rem; 137 | position: absolute; 138 | top: 50%; 139 | left: 50%; 140 | transform: translate(-50%, -50%); 141 | } 142 | 143 | #loader { 144 | position: absolute; 145 | top: 50%; 146 | left: 50%; 147 | transform: translate(-50%, -50%); 148 | z-index: 10; 149 | margin: 0 auto; 150 | } 151 | 152 | /* Animation */ 153 | #spinner { 154 | box-sizing: border-box; 155 | stroke: #cccccc; 156 | stroke-width: 3px; 157 | transform-origin: 50%; 158 | animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, 159 | rotate 1.6s linear infinite; 160 | } 161 | @keyframes rotate { 162 | from { 163 | transform: rotate(0); 164 | } 165 | to { 166 | transform: rotate(450deg); 167 | } 168 | } 169 | @keyframes line { 170 | 0% { 171 | stroke-dasharray: 2, 85.964; 172 | transform: rotate(0); 173 | } 174 | 50% { 175 | stroke-dasharray: 65.973, 21.9911; 176 | stroke-dashoffset: 0; 177 | } 178 | 100% { 179 | stroke-dasharray: 2, 85.964; 180 | stroke-dashoffset: -65.973; 181 | transform: rotate(90deg); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /static/classifier.js: -------------------------------------------------------------------------------- 1 | //======================================================================== 2 | // Drag and drop image handling 3 | //======================================================================== 4 | 5 | var fileDrag = document.getElementById("file-drag"); 6 | var fileSelect = document.getElementById("file-upload"); 7 | 8 | // Add event listeners 9 | fileDrag.addEventListener("dragover", fileDragHover, false); 10 | fileDrag.addEventListener("dragleave", fileDragHover, false); 11 | fileDrag.addEventListener("drop", fileSelectHandler, false); 12 | fileSelect.addEventListener("change", fileSelectHandler, false); 13 | 14 | function fileDragHover(e) { 15 | // prevent default behaviour 16 | e.preventDefault(); 17 | e.stopPropagation(); 18 | 19 | fileDrag.className = e.type === "dragover" ? "upload-box dragover" : "upload-box"; 20 | } 21 | 22 | function fileSelectHandler(e) { 23 | // handle file selecting 24 | var files = e.target.files || e.dataTransfer.files; 25 | fileDragHover(e); 26 | for (var i = 0, f; (f = files[i]); i++) { 27 | previewFile(f); 28 | } 29 | } 30 | 31 | //======================================================================== 32 | // Web page elements for functions to use 33 | //======================================================================== 34 | 35 | var imagePreview = document.getElementById("image-preview"); 36 | var imageDisplay = document.getElementById("image-display"); 37 | var uploadCaption = document.getElementById("upload-caption"); 38 | var predResult = document.getElementById("pred-result2"); 39 | var loader = document.getElementById("loader"); 40 | var model = undefined; 41 | 42 | //======================================================================== 43 | // Main button events 44 | //======================================================================== 45 | 46 | 47 | async function initialize() { 48 | model = await tf.loadLayersModel('/weights/catsvsdogs/model.json'); 49 | } 50 | 51 | async function predict() { 52 | // action for the submit button 53 | if (!imageDisplay.src || !imageDisplay.src.startsWith("data")) { 54 | window.alert("Please select an image before submit."); 55 | return; 56 | } 57 | 58 | let tensorImg = tf.browser.fromPixels(imagePreview).resizeNearestNeighbor([150, 150]).toFloat().expandDims(); 59 | prediction = await model.predict(tensorImg).data(); 60 | 61 | if (prediction[0] === 0) { 62 | predResult.innerHTML = "I think it's a cat"; 63 | 64 | } else if (prediction[0] === 1) { 65 | predResult.innerHTML = "I think it's a dog"; 66 | 67 | } else { 68 | predResult.innerHTML = "This is Something else"; 69 | } 70 | show(predResult) 71 | 72 | } 73 | 74 | function clearImage() { 75 | // reset selected files 76 | fileSelect.value = ""; 77 | 78 | // remove image sources and hide them 79 | imagePreview.src = ""; 80 | imageDisplay.src = ""; 81 | predResult.innerHTML = ""; 82 | 83 | hide(imagePreview); 84 | hide(imageDisplay); 85 | hide(loader); 86 | hide(predResult); 87 | show(uploadCaption); 88 | 89 | imageDisplay.classList.remove("loading"); 90 | } 91 | 92 | function previewFile(file) { 93 | // show the preview of the image 94 | var fileName = encodeURI(file.name); 95 | 96 | var reader = new FileReader(); 97 | reader.readAsDataURL(file); 98 | reader.onloadend = () => { 99 | imagePreview.src = URL.createObjectURL(file); 100 | 101 | show(imagePreview); 102 | hide(uploadCaption); 103 | 104 | // reset 105 | predResult.innerHTML = ""; 106 | imageDisplay.classList.remove("loading"); 107 | 108 | displayImage(reader.result, "image-display"); 109 | }; 110 | } 111 | 112 | //======================================================================== 113 | // Helper functions 114 | //======================================================================== 115 | 116 | function displayImage(image, id) { 117 | // display image on given id element 118 | let display = document.getElementById(id); 119 | display.src = image; 120 | show(display); 121 | } 122 | 123 | function hide(el) { 124 | // hide an element 125 | el.classList.add("hidden"); 126 | } 127 | 128 | function show(el) { 129 | // show an element 130 | el.classList.remove("hidden"); 131 | } 132 | 133 | initialize(); --------------------------------------------------------------------------------