Home Reference Source

js/main.js

import Vue from 'vue';
import Camera from './camera';
import FaceAPI from './faceapi';

/*
 * This needs to be done so that the styles get compiled.
 * Normally I'd use more components, more layouts, etc. and with this you could
 * very well separate concerns. It's somewhat useless here.
 */
import styles from '../scss/style.scss';

// Suppress linter errors
let faceApi;
let camera;

/*
 * I'm using the Vue.js framework to dynamically update the front-end values and
 * to utilize a simple templating engine.
 *
 * See index.html for the template. All components could be stored in separate
 * files, but I didn't like the way that would have to be structured, so I
 * didn't do it.
 *
 * 'el' is the element used by it.
 *
 * 'data' is like an application state. Changing it makes Vue update the
 * corresponding fields in the HTML template.
 *
 * 'methods' is an object of functions which can be executed, again, from the
 * template.
 */
const app = new Vue({
  el: '#app',
  data: {
    config: {
      key: localStorage.getItem('api-key') || '',
      intervalSpeed: 6000, // the interval's cooldown in milliseconds
    },
    // Possible errors to display status in the front-end
    errors: {
      needTrain: false,
      noneSelected: false,
      noKey: false,
    },
    groupId: localStorage.getItem('person-group-id') || Math.round(Math.random() * 10000).toString(2).substring(0, 32), // TODO: Implement better generation method
    personList: [], // persons in the personGroup
    detectedFaces: [], // currently detected faces
    selectedPerson: null, // currently selected person (for updating)
  },
  methods: {
    // Changes the selected person by index
    setSelectedPerson: (index) => {
      app.errors.noneSelected = false;
      app.selectedPerson = index;
    },
    // Creates a new person based on the front-end form and resets the form
    createPerson: () => {
      faceApi.createPerson(app.groupId, app.$refs.newPersonName.value, null)
        .then(person => app.personList.push(person));

      app.$refs.newPersonName.value = null;
    },
    // Deletes a person
    deletePerson: (index) => {
      faceApi.deletePerson(app.groupId, app.personList[index].personId);
      app.personList.splice(index, 1);
    },
    // Trains the person group
    trainPersonGroup: () => {
      app.errors.needTrain = false;
      faceApi.trainPersonGroup(app.groupId);
    },
    // Add a face to the selected person
    addFaceToPerson: (faceRectangle) => {
      if (app.selectedPerson === null) return app.errors.noneSelected = true;
      app.errors.noneSelected = false;
      app.errors.needTrain = true;

      return camera.takeImage().toBlob(blob =>
        faceApi.addPersonFace(app.groupId, app.personList[app.selectedPerson].personId, blob, faceRectangle),
      );
    },
    // Set the API key to the input's value
    setApiKey: (e) => {
      const key = e.target.value;

      localStorage.setItem('api-key', key);
      faceApi.setKey(key);

      setupPersonGroup();
    },
  },
});

/**
 * Initialize the FaceAPI class used for accessing Microsoft's API.
 * Camera needs to be initialized after Vue.js because it otherwise hijacks the
 * video element.
 */
faceApi = new FaceAPI(app.config.key);
camera = new Camera();

/**
 * Checks whether the key is set.
 * This is a very simple error handler, I would've liked to expand it to also
 * check for other errors.
 */
function checkKey() {
  if (app.config.key.length < 6) {
    app.errors.noKey = true;
    return false;
  }

  app.errors.noKey = false;
  return true;
}

/**
 * Here we're setting up the personGroup.
 * I opted to use one personGroup for each user, as we don't need mulitple for
 * the use case specified. We save the id in the localStorage for persistence.
 * The FaceAPI does support multiple personGroups.
 */
function setupPersonGroup() {
  if (!checkKey()) return;

  faceApi.getPersonGroupOrCreate(app.groupId)
    .then((group) => {
      // If there was an error (most often incorrect api key, reset persons)
      if (group.response && group.response.error) {
        app.personList = [];
        return;
      }

      // save to localStorage
      localStorage.setItem('person-group-id', app.groupId);

      // If the group is not newly created, fetch all existing persons.
      if (!group.newlyCreated) {
        faceApi.listPersons(app.groupId)
          .then((persons) => {
            app.personList = persons;
          });
      }
    });
}

setupPersonGroup();

/**
 * The function detectInImage runs every six seconds, as is restricted by the
 * free rate limit (20 requests per minute, this function does two - detect and
 * identify).
 */
setTimeout(function detectInImage() {
  if (!checkKey()) {
    setTimeout(detectInImage, app.config.intervalSpeed);
    return;
  }

  // Take an image and convert it to a POSTable format (blob)
  camera.takeImage().toBlob((blob) => {
    // Send a request to the API to detect the faces
    faceApi.detect(blob).then((resp) => {
      if (resp.error) return [];

      return resp;
    }).then((faces) => {
      // Identify faces in the detected faces.
      faceApi.identify(app.groupId, faces.map(face => face.faceId))
        .then((response) => {
          // Reset every smiling variable. Smiling doubles as an 'isIdentified' boolean.
          app.personList.forEach((p, i) => delete app.personList[i].smiling);

          // If there was an error, skip the assigning and return originally detected faces.
          if (response.error) return faces;

          // Assign the most likely person's name to the face for displaying
          response.forEach((identifiedFace) => {
            // If the API couldn't find anyone, return.
            if (identifiedFace.candidates.length < 1) return;

            // Find the identified person and then the detected face
            const personIndex = app.personList.findIndex(p =>
              p.personId === identifiedFace.candidates[0].personId);

            const faceIndex = faces.findIndex(f =>
              f.faceId === identifiedFace.faceId);

            const person = app.personList[personIndex];
            const face = faces[faceIndex];

            // If the person couldn't be found, it's likely because it has been
            // deleted before and the API hasn't been retrained.
            if (person) {
              face.identifiedAs = person.name;
              person.smiling = face.faceAttributes.smile;
            } else {
              face.identifiedAs = 'Error. Please train.';
              app.errors.needTrain = true;
            }
          });

          return faces;
        }).then((detectedFaces) => {
          // Finally, display these faces
          app.detectedFaces = detectedFaces;
          // Repeat after 6 seconds, see comment above the initial declaration
          setTimeout(detectInImage, app.config.intervalSpeed);
        });
    });
  });
}, app.config.intervalSpeed);