Ice Cream Or Dalmatian? Who Can Tell?! Building a Machine-Learning Powered PWA
Tags: javascript, vue, staticwebapps, pwa
Published: 2020-04-30 13:42:38

---
title: Ice Cream Or Dalmatian? Who Can Tell?! Building a Machine-Learning Powered PWA
published: true
description: Build an offline-first Progressive Web App that uses a machine learning model built on CustomVison.ai's service to differentiate between tricky images and deploy it using a GitHub Action
tags: JavaScript, Vue, staticwebapps, PWA
coverImage: https://dev-to-uploads.s3.amazonaws.com/i/2vbbregxhkcgw290q1ne.jpg
---
*Tricky images courtesy of [Karen Zack](https://twitter.com/teenybiscuit)*

## Create a PWA for Image Inference Using Vue.js and Custom Vision AI

> Tip! Do you want to try this tutorial in a cool interactive environment and earn a badge for completing it? Check out the [companion module on Microsoft Learn](https://docs.microsoft.com/learn/modules/create-web-app-with-refreshable-models/?WT.mc_id=aiapril-devto-jelooper)!

I've been fascinated for a long time - [inexplicably so](https://www.youtube.com/watch?v=ZG7GtgfUS6g&t=33s) - by a series of Instagram images created by Karen Zack (@teenybiscuit on Insta) who has generated a wonderful service to a world gripped by heavy news recently: the ability to determine images of parrots from guacamole, dachshunds from bagels, Labradoodles from fried chicken, and much more. Here is a sample of this seminal work:

### Kitten Vs. Caramel

![kitten vs. caramel](https://dev-to-uploads.s3.amazonaws.com/i/c8l4pvim63d8cp1zeqk5.jpg)

### Puppy Vs. Bagel

![puppy vs. bagel](https://dev-to-uploads.s3.amazonaws.com/i/0mibco2lwzhysq0fk6ui.jpg)

### Parrot Vs. Guacamole

![parrot vs. guacamole](https://dev-to-uploads.s3.amazonaws.com/i/1kljp5bo5vlktqrv301w.jpg)

> Note: you can build your app by following along in Microsoft Learn where this content is explained in more depth. Take a look! The entire codebase is also found here.

Working with these kinds of tricky images helps us to both lighten our mood, and to discover new ways of testing various methods of machine learning for image recognition and classification. In this tutorial, you'll learn how to use a snappy tool for image inference called [Custom Vision AI](https://docs.microsoft.com/azure/cognitive-services/Custom-Vision-Service/home?WT.mc_id=aiapril-devto-jelooper) so you don't have to train a model from scratch. You'll build a web app that can cycle through these images to determine whether the model can make a correct prediction. 

Using a cognitive service that builds on pre-trained models is a great way to dip your toe into machine learning models using some of the excellent [Azure Machine Learning cognitive services](https://docs.microsoft.com/azure/cognitive-services/?WT.mc_id=aiapril-devto-jelooper) (my favorite cognitive services ML platform), build a completely useless web app (my favorite kind) and have some fun doing it (my favorite activity). Let's get started!

> Bonus! we're going to turn this app into a PWA (Progressive Web App). This kind of app works offline and on your mobile phone, even while leveraging a ML model - no API calls outside the app will be made! We might as well learn how to do this since it's a neat way to build a ML-infused app and a good thing to know how to do.

## Scaffold your PWA

First, spin up a plain vanilla Vue.js app. Use the snazzy [`vue ui`](https://github.com/vuejs/ui) or start from the Vue CLI, assuming you have all the necessary requirements installed on your local machine. If you're using the Vue CLI via `vue create my-tricky-app`, manually select the features you want to install into your app, making sure to select 'PWA':

![cli](https://dev-to-uploads.s3.amazonaws.com/i/b5imir45bfd29c2hijqa.png)

A basic Vue web site will be created with some extra files that control the behavior of your PWA. Specifically, these files include:

- registerServiceWorker.js
- service-worker.js
- several icons for use on various platforms
- manifest.json in the `/public` folder that manages these icons and other important elements

You can use your browser's Developer Tools > Audits pane to run a Lighthouse audit on your web app. This will reveal interesting data such as how it measures up in an offline scenario. Note, you need to be running your app on a webserver in production to view a proper Lighthouse audit, so you can come back to this step later to get this more detailed information.

![web app](https://dev-to-uploads.s3.amazonaws.com/i/moy35p85armv2j9q5o40.png)

Now you're ready to build out the app's inference that will test various images against the machine learning model you're going to build in Custom Vision AI. You're going to use the images Karen Zack used to create her Dalmatians vs. Ice Cream collage, which I've separated into 16 images. You can download them [here](https://github.com/jlooper/thisorthat-mslearn/tree/master/src/assets/images) and include them in your local `/src/assets/images` folder.

> For a more detailed explanation of the reasoning behind the following code snippets, please visit the [Learn module](https://docs.microsoft.com/learn/modules/create-web-app-with-refreshable-models/?WT.mc_id=aiapril-devto-jelooper).

## Build Your Interface

Rename the default `<HelloWorld/>` page to `<DetectImage/>`, referencing it as such in `App.vue`. Then, create a simple template with a header, image and button, changing the value of the `msg` prop to `What do you see?`:

```
  <template>
    <div class="hello">
      <h1>{{ msg }}</h1>
      <div>
        <img class="image" ref="img" :src="require('../assets/images/' + getImgIndex + '.jpg')" />
      </div>
      <div>
        <button class="button" @click="next()" :disabled="disable">Next</button>
      </div>
      <div
        v-for="pred in predictions"
        :key="pred.index"
      >{{ pred.label }}: {{ pred.probability.toFixed(0) + '%' }}</div>
      <div v-if="!predictions.length">hmm.....</div>
    </div>
  </template>
```

Now you need to get your app ready to host a model trained in Custom Vision AI.

## Train Your Model

Here's where the fun starts. Go to CustomVision.ai and sign in. Create a project on a resource group available to you (create one if you don't have one handy). This project should be created as a `classification` project as you're doing a binary classification. Select MultiClass as there's only one tag per image, and choose the General (compact) domain so that you can use your model on the web. Export it for a Basic platform as you'll use it within a TensorFlow.js-powered context.

![create a project](https://dev-to-uploads.s3.amazonaws.com/i/1kut85t6p86yzgw5nu99.png)

Now you're going to teach the pretrained models a little about ice cream and dalmatians! To do this, you're going to need several images of these things - start with about ten of each class. I searched for 'chocolate chip ice cream` and used a [cool extension](https://chrome.google.com/webstore/detail/download-all-images/nnffbdeachhbpfapjklmpnmjcgamcdmm) to scrape the images off of the web page to create a training imageset.

> Note, I only trained on six images per class. This is too small a set, of course, for accurate transfer learning but Custom Vision AI still handles this small set pretty well. For a production caliber model, you would of course want a bigger imageset.

Save your ice cream and dalmatian images in two separate folders per class (`ice cream` and `dalmatian`) on your local machine. In the Custom Vision AI interface, drag and drop your folders, one at a time, into the web page. Tag the dog images `dalmatian` and the ice cream images `ice cream`:

![tag your images](https://dev-to-uploads.s3.amazonaws.com/i/0e7uulssrfzhg37nxoyc.png)

When your images are uploaded and tagged, you can start the training routine. Select the `train` button and watch your model build! When it's done, you will see its accuracy. Test it against a new image of a cute doggo. How accurate is your model?

![test](https://dev-to-uploads.s3.amazonaws.com/i/v8xkgooyiplldjbgvun6.png)

Now you can download the model files that were generated and place them in your web app in `public/models`:

-  cvexport.manifest
-  labels.txt
-  model.json
-  weights.bin

> Why place these files in `/public`, rather than in `/assets` or elsewhere? The `public` folder in a Vue.js app is a place to store static assets that should not be built by webpack. The four files produced by Custom Vision AI's build process need to stay untouched and be served ad hoc by your app without being bundled by webpack.

Now you can use these in your web app.

## Complete The Web App

You need to install a few libraries via npm to support the use of the machine learning files. 

1. In your `package.json` file in the root of your web app, add `"customvision-tfjs": "^1.0.1",` to the `dependencies` list.
2. In the same file, also add `"raw-loader": "^4.0.0",` to the `devDependencies` list. You need this package to manage reading .txt files in your Vue app.
3. In the same file, finally add `"webpack-cli": "^3.3.10"` to the `devDependencies` list so that the webpack CLI will be usable within the app, also necessary for text file parsing.

> Fun fact! CustomVision-tfjs uses TensorFlow.js under the hood.

In your terminal in VS Code, stop your app if it's currently running (ctrl-c) and add these packages: `npm install`. Now you can start building the `<script>` area of your app.

Under the `<template>`'s closing tag, create a new `<script>` tag with the following code:

    <script>
    import * as cvstfjs from "customvision-tfjs";
    import labels from "raw-loader!../../public/models/labels.txt";
    export default {
      name: "DetectImage",
      props: {
        msg: String
      },
      data() {
        return {
          labels: labels,
          model: null,
          predictions: [],
          image: 0,
          numImages: 16
        };
      },
      computed: {
        getImgIndex() {
          return this.image.toString();
        },
        disable() {
          if (this.image == this.numImages) {
            return true;
          } else return false;
        }
      },

      async mounted() {
        this.image++;
        //load up a new model
        this.model = new cvstfjs.ClassificationModel();
        await this.model.loadModelAsync("models/model.json");
        //parse labels
        this.labels = labels.split("\n").map(e => {
          return e.trim();
        });
        //run prediction
        this.predict();
      },

      methods: {
        async predict() {
          //execute inference
          let prediction = await this.model.executeAsync(this.$refs.img);
          let label = prediction[0];
          //build up a predictions object
          this.predictions = label.map((p, i) => {
            return { index: i, label: this.labels[i], probability: p * 100 };
          });
        },

        next() {
          this.image++;
          this.predictions = [];
          setTimeout(this.predict, 500);
        }
      }
    };
    </script>

Let's walk through this code. First, we import `cvstfjs` from the npm library we installed earlier, to help manage the Custom Vision models we built.

Then, we load the labels `.txt` file. This makes use of the `raw-loader` package. You need to tell webpack how to handle this type of text file, so add a new file to your root, if it's not there, called `webpack.config.js` with the following code:


    module.exports = {
      module: {
        rules: [
          {
            test: /\.txt$/i,
            use: 'raw-loader',
          },
        ],
      },
    };

Your Data object stores references to the variables you will use while building the inference methods.

There are also some computed properties. These are used to compute various UI elements, such as the index of the image being shown and the moment the 'next' button needs to be disabled when there are no more images to be shown.

In the asynchronous `mounted` lifecycle hook, you load your model. Models can be large, so it's best to wait till they load along with the labels files, which must also be parsed, before starting inference. Finally, when everything is ready, you call `predict`.

`Predict()` is also asynchronous, and uses Custom Vision's npm library to match predictions to labels. After a prediction is made, the `next` button can be clicked and prediction can start on the following image. Note, you use a `setTimeout` method to slow the prediction from starting until the image is loaded.

Once you are satisfied with the performance of your model and its predictions, you can publish your web app to a hosting provider such as Azure websites.

> Remember how your app is a PWA? Once your app is built and published, you can switch to 'offline' mode using DevTools and watch how you can continue to use the inference methods. Custom Vision AI also allows you to create endpoints for your models, but using them requires online latency.

## Publishing your app to Azure Websites

The absolute easiest way to do this is via a GitHub Action. Follow [these instructions](https://dev.to/azure/deploy-your-website-on-azure-with-github-actions-like-a-hipster-4da3) to create a workflow and connect the Azure portal to GitHub. Every time a change is made to your app, it will be rebuilt. It's a good way to refresh your models simply with a push to GitHub.

But wait! If you do publish to Azure, you need one more file in your root, a `web.config` file that will enable `.json` files to be parsed, so create this file and add this code to it:


    <?xml version="1.0" encoding="utf-8"?>
      <configuration>
        <system.webServer>
          <staticContent>
            <remove fileExtension=".json"/>
            <mimeMap fileExtension=".json" mimeType="application/json"/>
          </staticContent>
        </system.webServer>
    </configuration>


Oh and one more thing! One last change you need to make is to enable service-workers to be built. You need to create one more file in your app's root folder called `vue.config.js` that can contain this code:


    module.exports = {
        pwa: {
            workboxOptions: {
                exclude: [/\.map$/, /web\.config$/],
            },
        },
    };

This file tells the service worker to ignore the web.config file you added earlier, whose existence causes problems for the service-worker build process.

Now you can watch your app working both on and offline when it's published to a web server!

![a dalmatian in a web site](https://dev-to-uploads.s3.amazonaws.com/i/99xc7x6fxozh3gv98noy.png)

## Conclusion

In this article you learned how to build a Vue.js web app powered by machine learning models that can also work offline as it's a PWA with embedded files. Moreover, you learned how to deploy such an app to Azure itself, a true end to end solution for your image inference needs. I hope you try [CustomVision.ai](https://customvision.ai) when looking for a nice solution for image processing, as it's a superb way to handle image inference which is not easy to build from scratch. Please let me know what you build in the comments below! And if you'd like to watch a video of me explaining some of the elements that went into building [this app](https://icecreamordog.azurewebsites.net/), check out the video below.










{% youtube kdXeNKSi76U %}