Toggle dark mode of a page in a blink
Tags: javascript, tensorflow, machinelearning, opensource
Published: 2021-11-09 07:24:59

Article originally posted on [my blog](https://www.ankurkedia.com/blog/toggle-dark-mode-blink).

--- 

This article is about how you can add blink detection in your project using [TensorFlow.js](https://www.tensorflow.org/). As an example, we will toggle the dark mode on a page. It's just a fun project to get started with Machine Learning and TensorFlow.js. Here is the result.

<video controls='true'
        preload='none'
        autoplay='false'
        width='100%'
        height='100%'
      >
        <source src='https://www.ankurkedia.com/blink-detection-demo.mp4' type='video/mp4' />
        Sorry, your browser does not support embedded videos.
      </video>

## What should you know before starting?

Well, nothing as such. In simple terms, you can think of [TensorFlow](https://github.com/tensorflow/tensorflow) as a library that infers patterns from data and identify those patterns when they occur. You can either use [pre-trained models](https://github.com/tensorflow/tfjs-models#models) or train your models with [Teachable Machine](https://teachablemachine.withgoogle.com).

## Let's get started and set up a TensorFlow.js project

* **Load model**: You need to load the model to be able to use it. In this case, we are using [face landmarks detection](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection) model.

```js
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection';
import * as tf from '@tensorflow/tfjs-core';
import '@tensorflow/tfjs-backend-webgl';

const loadModel = async () => {
  await tf.setBackend('webgl');

  model = await faceLandmarksDetection.load(
    faceLandmarksDetection.SupportedPackages.mediapipeFacemesh,
    { maxFaces: 1 }
  );
};
```

* **Setup camera**: To detect the face, the model needs a video stream. We will create a video element and pass its stream to estimate features on the face.

```js
const setUpCamera = async (videoElement) => {
  video = videoElement;
  const mediaDevices = await navigator.mediaDevices.enumerateDevices();

  const defaultWebcam = mediaDevices.find(
    (device) =>
      device.kind === 'videoinput' && device.label.includes('Built-in')
  );

  const cameraId = defaultWebcam ? defaultWebcam.deviceId : null;

  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      facingMode: 'user',
      deviceId: cameraId,
      width: 500,
      height: 500,
    },
  });

  video.srcObject = stream;
  video.play();
  video.width = 500;
  video.height = 500;
}
```

* **Start estimation**: For estimation, we will have to continuously monitor the face and keep checking if the user is blinking or not.

```js
const predictions = await model.estimateFaces({
  input: video,
  returnTensors: false,
  flipHorizontal: false,
  predictIrises: true,
});
```

This returned prediction is an array of objects with values `likefaceInViewConfidence`, `boundingBox`, `mesh`, `scaledMesh`, `annotations`. The [facemesh keypoints](https://github.com/tensorflow/tfjs-models/blob/master/face-landmarks-detection/README.md#keypoints) can be obtained from the `annotations` key.

These were the common steps you will need to do to set up a [TensorFlow.js](https://github.com/tensorflow) project with the face landmarks detection model. If all goes well, the browser should ask for camera permission when you run the project to be able to detect live video, and once the model is loaded, it starts predicting!

## Adding blink detection

With the keypoints, we can calculate the Eye Aspect Ratio(EAR) with the following formula.

> EAR = (|| p2 - p6 || + || p3 - p5 ||) / (2 * (|| p1 - p4||))

Here, `|| d ||` represents the [Euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance) which is essentially the length of a line segment between the two points. And the points p1, p2 and so on, used here are mapped on the facemesh in the image.

![EAR points](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a1wab4fcnms9bi0dzlbs.png)

So, when the person blinks, the EAR will drop close to zero. To detect the blink, the EAR will have to be lesser than a threshold which is close to zero. So, I used an EAR threshold that works for varying distances between the user and the camera. Once the EAR drops below the threshold, we know that eye is blinked. We have separate keypoints for both the eyes, so we can detect both eyes' blink separately.

## To the dark side
Some time back, I read about an easy way to force the dark mode, which uses the [`CSS filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) property. Coupling it with the [`color-scheme`](https://www.w3schools.com/cssref/css3_pr_filter.asp) property, we can add dark mode to a majority of the pages. So, I would add these CSS properties to the root HTML element.

```css
filter: invert(1) hue-rotate(180deg);
color-scheme: dark;
```

> Note: There are some caveats to this method. But this works well enough for us to learn the basics. And hey, you get the dark mode with two lines of CSS. Which I think is pretty cool.

## Blink Detection

To be able to reuse the logic, I created an NPM package named [Blink Detection](https://www.npmjs.com/package/blink-detection) which does the calculations and returns the result.

```js
import blink from 'blink-detection';

const blinkPrediction = await blink.getBlinkPrediction();
```

Here, the blinkPrediction will be an object with the keys `blink`, `wink`, `longBlink`, `left`, `right`, `rate`. Each key except `rate` will be a boolean value.

## Challenges

* With the [face landmarks detection](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection#keypoints) model, you will need to manually map [facemesh keypoints](https://github.com/tensorflow/tfjs-models/blob/master/face-landmarks-detection/README.md#keypoints) on a graph from the data readings. There are a lot of annotations that can be used, but I could not find any source explaining them.

* To decide the EAR(Eye-Aspect-Ratio) threshold, I had to try different values. I use the one that works the best for different distances between the user and the camera. So, it is not bullet-proof.

## More ideas with blink detection

* **Chrome extension**: You can create a chrome extension for the same demo. Another extension idea is to gauge the eye blink rate and use it for different purposes.

* **Blink rate**: There have been [numerous studies](https://scholar.google.co.in/scholar?hl=en&as_sdt=0%2C5&as_vis=1&q=blink+rate) on how blinking patterns can measure or apprise certain aspects of an individual or a situation.

* **Wink detection**: Similar to blinking, wink detection can also be used to perform some actions.

## Wrapping up

That's it! Overall, this project wasn't really about toggling dark mode. I wanted to play around with TensorFlow and create something while doing it! Also, I could not find any project related to blink detection in Javascript, so I developed the [package](https://www.npmjs.com/package/blink-detection). Check out the [demo](https://blink-detection.vercel.app/) and the code.

{% github theankurkedia/blink-detection no-readme %}


## References

I found the following resources helpful. If you want to experiment and create something similar, you can start here.

* [Pre-trained TensorFlow.js models](https://github.com/tensorflow/tfjs-models#models) for different use cases.

* If you are using the face landmarks detection model, the [facemesh keypoints](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection#keypoints) can help you map the points needed for your use case.

* This [amazing project](https://charliegerard.dev/blog/toggle-dark-mode-clapping-hands-chrome-extension/) was a great starting point. Honestly, I just cloned the repo and started experimenting.

* [TensorFlow.js object detection - Codelabs](https://codelabs.developers.google.com/codelabs/tensorflowjs-object-detection).

* [Teachable Machine](https://teachablemachine.withgoogle.com) to train your models.

* [Eye blink detection with OpenCV, Python, and dlib](https://www.pyimagesearch.com/2017/04/24/eye-blink-detection-opencv-python-dlib/).

Thanks for reading. Hope you found it helpful! Happy coding!