Introduction
With a fever being one of the most common symptoms of COVID-19, being able to check someone for a fever could be a very useful first step. It seems that a lot of companies are interested in ways to ensure that their employees are safe when they arrive to work, and having a simple way to check for a fever could be a simple method used. I have devised a simple rig to automatically check someone's temperature and give a pass/fail result, as well as a third 'borderline' where there is elevated temperature, but the person should proceed for further screening to verify. At the end of this write up, I'll post some interesting info on efficacy of using external temperature sensing as an indicator of core body temperature.
I see that another user ninjatrent posted a very similar project a full month ago, so I will post this for the community to enjoy, but would ask to not be considered in the running for any prizes. I had already built my project when I found his post, but since the work was done, I figured I would post anyways.
The Concept
What I wanted to create was a simple device that could check one's temperature. I wanted it to be non-invasive and non-contact. I also gave myself a restraint to only use what I had on-hand and not order any parts, as to not add any additional stress on other companies unduly. I used a Panasonic AMG 8833 sensor on a breakout board from Adafruit as the main sensor. This is an infrared (thermal) sensor with a resolution of 8x8 pixels. I had this laying around as a spare from the Smart Range Hood project (Still up and running!). Most projects using this generate a heat map image, but I wanted to go a little simpler. I just used three LEDs to indicate the status. Green = good, Yellow = borderline, Red = Fever. To trigger the system, I used a VL53LOX Time of Flight sensor. I am able to control the trigger distance this way and only read the thermal sensor when a person is in range. I had picked up two of these years ago and never used them until two weeks ago when I started putting this together. As a controller, I used an Arduino Compatible EtherTen from Freetronics
I had a plan to hack a PiSense hat to use the 8x8 LED array to correlate the temperature from the thermal camera, but couldn't find it in my bins after about 30 minutes of looking around. It would have been a stretch since I would have had to manually interface it to an Arudino instead of a RaspberryPi and I couldn't find any guides online. I would have likely had to switch to the Arduino ProMini that I have to use the 3.3v logic.
I also had another concept that would use an RFID scan to trigger the temperature reading. The idea here is that an employee could scan their badge, have a temperature taken, and the temperature could be logged in a database. This database could then establish a baseline for each employee and apply a individual curve to look changes over time. This could potentially be more accurate, but with the downside of being more invasive as employees might not be comfortable with their employer having this information about them. I have an NFC kit for RaspberryPi in a bin and grabbed it, but later passed on the concept.
Wiring it up
The layout was pretty simple. Both the main sensors are I2C, so they just needed the clock and data lines connected to the SDA/SCK pins of the Arduino. The Green/Yellow/Red LEDs were on pins 8, 9, and 10; each with a 220 Ohm resistor. This was a very simple circuit layout that only took about 30 minutes, with 27 of those minutes with a multi-meter digging through a pile of resistors trying to find the right ones...My sensors could work at 3.3V or on 5V. I opted for 5V since the Arduino I was using was set up on 5V. If I was using the PiSense hat and hacking it into this project, then I would convert everything over to 3.3v.
This project is 'simple enough' to be easily represented in Fritzing, so here it is.
Programming
The code is pretty straight-forward. I continually monitor the distance sensor and set a flag if someone is present. Once that flag is set, the next portion of the code reads the thermal camera to get its digital data. The library for the camera returns an array of values in degrees Celsius. There are some various strategies which I will discuss in the last portion on how to interpret the data when used for this application, and where to aim the sensor (forehead versus temples, vs into an opened mouth).
For this simple demo, I just average all of the readings. This concept is assuming that the subject is close enough to the thermal camera that it has a full field-of-view and that none of the pixels are looking in the wrong place - for instance, a corner or edge pixel could be seeing towards the side of someone and pick up on a hot cup of coffee held by the next guy in line. Quite a lot of this code is taken from the demo sketches for the thermal camera and distance sensor and just tweaked to work together in my program.
Main Loop
This is the main loop described above. I dump some information out the serial port for debugging. I also check if it has been more than 5 seconds since the last time someone was in front of the sensor. If so, I turn the LEDs off. The LEDs will have remained lit in whatever previous state they in until this timer expires. I also have a 100 ms delay at the bottom of the main loop which probably doesn't need to be there any more, but early on it kept the loop from running too fast.
void loop() { bool present = readDistanceSensor(); if(present){ lastReading = millis(); double avg = readGrideye(); checkLEDneed(avg); Serial.print("Average: "); Serial.print(avg); Serial.println("]"); Serial.println(); present = false; } if(millis() > lastReading + 5000) setLED(0); delay(100); }
Reading the distance sensor
This portion uses the library for the VL53LOX distance sensor to obtain a reading in milmeters. If the sensor returns a good (valid) value, then I just compare it against a setpoint (200 mm in my case) and set a flag if 'in range'.
bool readDistanceSensor(){ VL53L0X_RangingMeasurementData_t measure; bool result = false; lox.rangingTest(&measure, false); if (measure.RangeStatus != 4) { // phase failures have incorrect data if(measure.RangeMilliMeter <= rangeSetPoint) { Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter); result= true; } else result= false; } return result; }
Reading the GridEye thermal camera
If someone is present in front of the sensor, then I poll the camera to get the current temperature values via the amg.readPixels(pixels) line. This is much easier than how I had to hack it in NodeRed for the Range Hood project; which was more akin to bit-banging the I2C bus.
The library returns an array of 64 values; to which I perform a simple average *Note this is not the recommended method in a real scenario - see the last section for that* The final value is returned to a subroutine loop to determine how to react.
double readGrideye(){ //read all the pixels amg.readPixels(pixels); Serial.print("["); double avg=0.0; for(int i=1; i<=AMG88xx_PIXEL_ARRAY_SIZE; i++){ avg+=pixels[i-1]; //Serial.print(pixels[i-1]); //Serial.print(", "); //if( i%8 == 0 ) Serial.println(); } avg = avg/64; return avg; }
Lighting the lights
The first section below will "check the need" to light up the LEDs, and call the second function below it set the lights. This is where I have hard-coded the threshold values of <25°C to turn the lights off, 25-32°C as "Green", 32-37.5° as "Yellow", and >37.5°C as "Red".
int checkLEDneed(double val){ int result =0; if(val <= 25) result =0; else if (val >= 37.5) result =3; else if (val >= 32) result =2; else result = 1; //Serial.print("LED result: "); //Serial.print(result); setLED(result); } void setLED(int val){ switch (val) { case 0: digitalWrite(led_green, LOW); digitalWrite(led_yellow, LOW); digitalWrite(led_red, LOW); break; case 1: digitalWrite(led_green, HIGH); digitalWrite(led_yellow, LOW); digitalWrite(led_red, LOW); break; case 2: digitalWrite(led_green, LOW); digitalWrite(led_yellow, HIGH); digitalWrite(led_red, LOW); break; case 3: digitalWrite(led_green, LOW); digitalWrite(led_yellow, LOW); digitalWrite(led_red, HIGH); break; } }
The complete code is posted here, which also has the setup() section and other declarations above.
#define led_green 8 #define led_yellow 9 #define led_red 10 //Define range at which to activate the device #define rangeSetPoint 200 #include <Wire.h> #include <Adafruit_AMG88xx.h> Adafruit_AMG88xx amg; #include "Adafruit_VL53L0X.h" Adafruit_VL53L0X lox = Adafruit_VL53L0X(); float pixels[AMG88xx_PIXEL_ARRAY_SIZE]; unsigned long lastReading; void setup() { Serial.begin(9600); pinMode(led_green, OUTPUT); pinMode(led_yellow, OUTPUT); pinMode(led_red, OUTPUT); bool status; // default settings status = amg.begin(); if (!status) { Serial.println("Could not find a valid AMG88xx sensor, check wiring!"); while (1); } if (!lox.begin()) { Serial.println(F("Failed to boot VL53L0X")); while(1); } delay(100); // let sensor boot up } void loop() { bool present = readDistanceSensor(); if(present){ lastReading = millis(); double avg = readGrideye(); checkLEDneed(avg); Serial.print("Average: "); Serial.print(avg); Serial.println("]"); Serial.println(); present = false; } if(millis() > lastReading + 5000) setLED(0); delay(100); } bool readDistanceSensor(){ VL53L0X_RangingMeasurementData_t measure; bool result = false; lox.rangingTest(&measure, false); if (measure.RangeStatus != 4) { // phase failures have incorrect data if(measure.RangeMilliMeter <= rangeSetPoint) { Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter); result= true; } else result= false; } return result; } double readGrideye(){ //read all the pixels amg.readPixels(pixels); Serial.print("["); double avg=0.0; for(int i=1; i<=AMG88xx_PIXEL_ARRAY_SIZE; i++){ avg+=pixels[i-1]; //Serial.print(pixels[i-1]); //Serial.print(", "); //if( i%8 == 0 ) Serial.println(); } avg = avg/64; return avg; } int checkLEDneed(double val){ int result =0; if(val <= 25) result =0; else if (val >= 37.5) result =3; else if (val >= 32) result =2; else result = 1; //Serial.print("LED result: "); //Serial.print(result); setLED(result); } void setLED(int val){ switch (val) { case 0: digitalWrite(led_green, LOW); digitalWrite(led_yellow, LOW); digitalWrite(led_red, LOW); break; case 1: digitalWrite(led_green, HIGH); digitalWrite(led_yellow, LOW); digitalWrite(led_red, LOW); break; case 2: digitalWrite(led_green, LOW); digitalWrite(led_yellow, HIGH); digitalWrite(led_red, LOW); break; case 3: digitalWrite(led_green, LOW); digitalWrite(led_yellow, LOW); digitalWrite(led_red, HIGH); break; } }
Video Demo
See this video for a walk-through of the system running.
Note: The reason that my hand can make the sensor go from green to yellow is because I am averaging all the pixels in the camera. As I move my hand closer, more pixels see my hand and thus the average moves up. If I used the "max" pixel temperature the behavior would be different.
Efficacy of using external temperature sensing of core body temperature
A key aspect of this project and other similar ones has to do with the correlation of external body temperature versus core body temperature. The golden standard is either oral (under the tongue) or rectal to get the most accurate measure of the core temperature. Studies have shown that there is a "significant but weak" correlation between forehead and temporal temperature versus core body temp, but this can differ by up to 3.5°C. So the forehead could measure 34°, but the core could actually be closer to 37-38°. That amount of error if not accounted for could be cause for a false negative result. With Coronavirus being as contagious as it is, that false negative could have very real consequences. There is also a possibility of having a false positive where someone has a naturally higher exterior temperature. The measurement device would be hard-pressed to simply just add 3.5° to all readings since it will put some amount of healthy people into the range where they wouldn't be allowed in to work and would potentially have to try to speak with a doctor or visit a hospital. This could be very disruptive for a lot of people trying to work at an essential business with limited means of child care and loss of income due to layoffs of others in the household.
(Image taken from the article linked just below here)
This chart illustrates how the forehead is the farthest-away option (largest temperature delta) when compared to the core body temperature, especially in the case of febrile (sick) individuals. Latmax and areamax (max of the entire face region) are the closest.
There are also some significant differences in the delta and core temperature across age and gender which can make blanket assumptions difficult.
See this link for some very useful information: https://www.hkmj.org/system/files/hkm1204sp3p31.pdf
The exact definition of a fever and the thresholds chosen as the cutoff will play a large factor in the number of false positives and false negatives. This is referred to as "sensitivity and specificity". The chart below from an image search demonstrates the concept of these terms. The study done in the document linked above talks in detail about the SARS outbreak in China in 2005/2006 and goes into a lot of detail about how this could have helped (or not). The best results based on the data set was "77% sensitivity and 74% specificity" based on a maximum face temperature of 36°. That equates to roughly 25% of results being false positives and 25% being false negatives. Not a very stellar result, but that is the highest success rate that could be achieved as a result of this study.
The article mentions that reading on the lateral max temperature of the face (temple area) has the highest correlation to the core body temperature, so should be the preferred measurement type as opposed to the center of the forehead (which my work is currently doing...)
Note that none of this data or methods dispute the accuracy of thermography in any way. The sensors are plenty accurate enough, but the methods in which they are applied can cause lead to undesired results. The next step of finding an individual with a fever must be considered. It would be expected that many would contest the accuracy of the device and the methods and make for some headaches on the side of the employers.
One thing of note is that there are many hand-held devices for checking temperature on the forehead and in the ear. These are often "contact based" instead of non-contact, and they also will have one button for forehead and one for ear. The idea is that a different offset will be given for each of the measurements to try correlating to core body temperature.
So any who may be considering this for a workplace should consider the implications very carefully.
Top Comments