Forum Rule: Always post complete source code & details to reproduce any issue!

# Thread: [posted] Thermal imaging camera

1. ## [posted] Thermal imaging camera

I have a leak in my A/C duct system and to help isolate the problem, I built a thermal imaging camera. It’s based on an AMG8833 sensor that has an 8x8 sensor grid. The unit was packaged in a breakout board from Adafruit, who also published a library for fast startup use. Connection is via I2C, and the examples made getting the unit working a snap.

Out of the box the results were not very good, namely because of a small 8x8 sensor and the few colors that were assigned to for each pixel to represent temperature. The library is just to get started, so no complaints—I’ll have to thank any library writer for doing the heavy lifting. My code improves the results by taking advantage of all colors on my 2.8” TFT screen (65K), and turned up the resolution to 70x70 with some basic interpolation.

My algorithm for color assignment based on temperature is linear (namely due to the need to keep code running as fast as possible), and 65K colors will not be perfectly smooth anyway. I found a neat website that had RGB color definition per temp, so i just had to plot the RGB values and develop some equations, using y=mx + b.

To generate data at the interior points I had to interpolate each row, expanding 8 points to 70 for each row, and when complete I ran interpolation against each of the 70 columns. My interpolation routine to generate 10 points between each measured point is also linear. Again, in an attempt to display sensor readings at the maximum of 10 fps (as governed by the sensor), I wanted to keep the math as fast as possible. Also the 65K color limit prevents the need for supper accurate log-based color interpolation.

I also added some touch screen capability so the user can set the max/min temperature for converting the temp to color before drawing the results to the screen, and a grid that uses an interesting concept to avoid flickering due to repeatedly drawing grid lines.

I’m using a Teensy 3.2 running at 120 mhz and the screen refreshes around 3 fps—good enough for an \$80 project. I still can't find my leak though...

To help others get started, I created a YouTube video of this project and have a link to the source code. Have a look.

Happy measuring

Kris

2. @KrisKasprzak. Nice job on the imager and especially the interpolation. Had a project to update based on the mlx90620 chip. Have to put it on my list of things to do this winter Thanks for sharing.

3. That's awesome! Nicely done. That looks really nice from an 8x8 grid.

I wonder if there is a clever way to create a heat map of an area by repeatedly panning over a certain area. Basically, use the motion to gather info on the gaps.
Also would be kinda cool to do some stitching so you could create a detailed heat map of your entire wall. Just dumping all the values to an sdcard might be useful, and then doing all the processing on the PC.

4. @linuzgeek and @@KrisKasprzak. A lot of years ago some one had posted on how to do that with a mlx90614. The site is now defunct but he had the Arduino code and processing sketch to get some detailed images. I tried doing that with the 90620 but ran into problem with the interpolation to get a good image. If there is any interest by anyone for this I can post a copy of the page and code someplace.

UPDATE: Heres a link for a couple of the original 90614 project. https://drive.google.com/open?id=1Y4...O85AvB0EnxSXI3

5. Have one of these in the mail, so interested in your results. With tiling images together do you have any idea what the actual FOV for each pixel is? If you have a point IR source some distance out does it always light up one pixel (pixels touch) sometimes vanish (pixels have dead space) or sometimes show up twice (pixels actually overlap).

If there are gaps wondering if you could use a mirror or prisim assembly to scan the FOV across the sensor to trade time for more pixels?

Probably winds up cheaper to just buy a real camera but many moons ago worked with an 8 sensor IR system that used a very complex scanning mirror system to make up a TV image from those 8 pixels.

6. Originally Posted by GremlinWrangler
If there are gaps wondering if you could use a mirror or prisim assembly to scan the FOV across the sensor to trade time for more pixels?
That's a great idea!

7. Thanks for the compliments. The FOV is about 60 degrees so maybe if you get close enough to the heat source you could scan and build a larger array to avoid interpolation. At around 5 inches from the sensor i can "see" my fingers holding up a peace sign, at about 10 inches, the peace sign becomes a single blob. I really think there's not much you can do with an 8x8 array. Lately i've gotten to printing my own PCB boards, paper transfer from a laser printer, and 20 min in Muriatic+H2O2 acid. Works perfectly for a garage DYI. I forgot to reverse the pins on my 3v3 regulator as it's mounted on the copper side so some jumpers need to be added and I'm designing an enclosure to be 3D printed. I've become quite proficient at this process and warrants another YouTube video.

8. @@KrisKasprzak. Nice job on the circuit board. Never tried that will have to give it a shot at some point. Anyway, you guys got me going on this again, haven't touched for a few year, thanks for the inspiration. This was one of my first Arduino projects (it was before I discovered Teensies ). Anyway here is some more info that I thought might be of interest:

If there are gaps wondering if you could use a mirror or prisim assembly to scan the FOV across the sensor to trade time for more pixels?
Had in my notes that somebody did that with a lepton sensor, you can check this out Poor Mans Thermograph

I found this last night on someone that did some testing using several sensors that I thought was interesting, Low-cost IR Array Sensors performance characterization (Thermal Imaging)

Cheers

9. @KrisKasprzak Well done, i build your Thermo camera a few days ago together and install your source code.
But i cannot use the touchscreen. Can you send me your missing code to me please?

10. @KrisKasprzak, nice project. i enjoyed the video, good job on that and thanks for the code.
@mjs513, thank you for the code.

11. For those of you who downloaded my code, my first post did non include the touchscreen stuff. This was brought to my attention from a YouTube comment. A few weeks back i updated the code to the latest--perhaps josh911 downloaded before my update? At any rate feel free to download again, but attached here (hopefully i'm allowed to post code in this part of the forum).

Code:
```/*

This program is for upsizing an 8 x 8 array of thermal camera readings
it will size up by 10x and display to a 240 x 320
interpolation is linear and "good enough" given the display is a 5-6-5 color palet
Total final array is an array of 70 x 70 of internal points only

Revisions
1.0     Kasprzak      Initial code

MCU                       https://www.amazon.com/Teensy-3-2-with-pins/dp/B015QUPO5Y/ref=sr_1_2?s=industrial&ie=UTF8&qid=1510373806&sr=1-2&keywords=teensy+3.2
Display                   https://www.amazon.com/Wrisky-240x320-Serial-Module-ILI9341/dp/B01KX26JJU/ref=sr_1_10?ie=UTF8&qid=1510373771&sr=8-10&keywords=240+x+320+tft
display library           https://github.com/PaulStoffregen/ILI9341_t3
font library              https://github.com/PaulStoffregen/ILI9341_fonts
touchscreen lib           https://github.com/dgolda/UTouch

Pinouts
MCU         Device
A4          AMG SDA
A5          AMG SCL
Gnd         Dispaly GND, AMG Gnd
3v3         Dispaly Vcc,Display LED,Display RST, AMG Vcc
2           Dispaly T_CLK
3           Dispaly T_CS
4           Dispaly T_DIN
5           Dispaly T_DO
6           Dispaly T_IRQ
9           Display D/C
10          Display CS
11          Display MOSI
12          Dispaly MISO
13          Display SCK

*/

#include "ILI9341_t3.h"             // very fast library
#include "font_ArialBoldItalic.h"   // fonts for the startup screen
#include "font_Arial.h"             // fonts for the legend
#include "font_DroidSans.h"         // fonts for the startup screen
#include <Adafruit_AMG88xx.h>       // thermal camera lib
#include <UTouch.h>                 // touchscreen lib

#define PIN_CS 10                   // chip select for the display
#define PIN_DC 9                    // d/c pin for the display

// constants for the cute little keypad
#define BUTTON_W 60
#define BUTTON_H 30
#define BUTTON_SPACING_X 10
#define BUTTON_SPACING_Y 10
#define BUTTON_TEXTSIZE 2

// fire up the display using a very fast driver
// this next line is for my modified library where I pass the screen dimensions in--that way i can use the same lib for my 3.5", 2.8" and other sizes
// ILI9341_t3 Display = ILI9341_t3(PIN_CS, PIN_DC, 240, 320);

// you will need to use this line
ILI9341_t3 Display = ILI9341_t3(PIN_CS, PIN_DC);

// create some colors for the keypad buttons
#define C_BLUE Display.color565(0,0,255)
#define C_RED Display.color565(255,0,0)
#define C_GREEN Display.color565(0,255,0)
#define C_WHITE Display.color565(255,255,255)
#define C_BLACK Display.color565(0,0,0)
#define C_LTGREY Display.color565(200,200,200)
#define C_DKGREY Display.color565(80,80,80)
#define C_GREY Display.color565(127,127,127)

// create some text for the keypad butons
char KeyPadBtnText[12][5] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "Done", "0", "Clr" };

// define some colors for the keypad buttons
uint16_t KeyPadBtnColor[12] = {C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_BLUE, C_GREEN, C_BLUE, C_RED };

uint16_t MinTemp = 25;
uint16_t MaxTemp = 35;

// variables for interpolated colors
byte red, green, blue;

// variables for row/column interpolation
byte i, j, k, row, col, incr;
float intPoint, val, a, b, c, d, ii;
byte aLow, aHigh;

// size of a display "pixel"
byte BoxWidth = 3;
byte BoxHeight = 3;

int x, y;
char buf[20];

// variable to toggle the display grid
int ShowGrid = -1;
int DefaultTemp = -1;

// array for the 8 x 8 measured pixels
float pixels[64];

// array for the interpolated array
float HDTemp[80][80];

// note the ILI9438_3t library makes use of the Adafruit_GFX library (which makes use of the Adafruit_button library)

// create the camara object

// create the touch screen object
UTouch  Touch( 2, 3, 4, 5, 6);

void setup() {

// Serial.begin(115200);

// start the display and set the background to black
Display.begin();
Display.fillScreen(C_BLACK);

// initialize the touch screen and set location precision
Touch.InitTouch();
Touch.setPrecision(PREC_EXTREME);

for (row = 0; row < 4; row++) {
for (col = 0; col < 3; col++) {
KeyPadBtn[col + row * 3].initButton(&Display, BUTTON_H + BUTTON_SPACING_X + KEYPAD_LEFT + col * (BUTTON_W + BUTTON_SPACING_X ),
KEYPAD_TOP + 2 * BUTTON_H + row * (BUTTON_H + BUTTON_SPACING_Y),
BUTTON_W, BUTTON_H, C_WHITE, KeyPadBtnColor[col + row * 3], C_WHITE,
KeyPadBtnText[col + row * 3], BUTTON_TEXTSIZE);
}
}

// set display rotation (you may need to change to 0 depending on your display
Display.setRotation(3);

// show a cute splash screen (paint text twice to show a little shadow
Display.setFont(DroidSans_40);
Display.setCursor(22, 21);
Display.setTextColor(C_WHITE, C_BLACK);
Display.print("Thermal");

Display.setFont(DroidSans_40);
Display.setCursor(20, 20);
Display.setTextColor(C_BLUE, C_BLACK);
Display.print("Thermal");

Display.setFont(Arial_48_Bold_Italic);
Display.setCursor(52, 71);
Display.setTextColor(C_WHITE, C_BLACK);
Display.print("Camera");

Display.setFont(Arial_48_Bold_Italic);
Display.setCursor(50, 70);
Display.setTextColor(C_RED, C_BLACK);
Display.print("Camera");

// let sensor boot up
bool status = ThermalSensor.begin();
delay(100);

// check status and display results
if (!status) {
while (1) {
Display.setFont(DroidSans_20);
Display.setCursor(20, 150);
Display.setTextColor(C_RED, C_BLACK);
Display.print("Sensor: FAIL");
delay(500);
Display.setFont(DroidSans_20);
Display.setCursor(20, 150);
Display.setTextColor(C_BLACK, C_BLACK);
Display.print("Sensor: FAIL");
delay(500);
}
}
else {
Display.setFont(DroidSans_20);
Display.setCursor(20, 150);
Display.setTextColor(C_GREEN, C_BLACK);
Display.print("Sensor: FOUND");
}

// read the camera for initial testing

// check status and display results
if (pixels[0] < 0) {
while (1) {
Display.setFont(DroidSans_20);
Display.setCursor(20, 180);
Display.setTextColor(C_RED, C_BLACK);
delay(500);
Display.setFont(DroidSans_20);
Display.setCursor(20, 180);
Display.setTextColor(C_BLACK, C_BLACK);
delay(500);
}
}
else {
Display.setFont(DroidSans_20);
Display.setCursor(20, 180);
Display.setTextColor(C_GREEN, C_BLACK);
delay(2000);
}

// set display rotation and clear the fonts..the rotation of this display is a bit weird

Display.fillScreen(C_BLACK);

// get the cutoff points for the color interpolation routines
// note this function called when the temp scale is changed
Getabcd();

// draw a cute legend with the scale that matches the sensors max and min
DrawLegend();

// draw a large white border for the temperature area
Display.fillRect(10, 10, 220, 220, C_WHITE);

}

void loop() {

// if someone touched the screen do something with it
if (Touch.dataAvailable()) {
ProcessTouch();
}

// now that we have an 8 x 8 sensor array
// interpolate to get a bigger screen
InterpolateRows();

// now that we have row data with 70 columns
// interpolate each of the 70 columns
// forget Arduino..no where near fast enough..Teensy at > 72 mhz is the starting point
InterpolateCols();

// display the 70 x 70 array

}

// interplation function to create 70 columns for 8 rows
void InterpolateRows() {

// interpolate the 8 rows (interpolate the 70 column points between the 8 sensor pixels first)
for (row = 0; row < 8; row ++) {
for (col = 0; col < 70; col ++) {
// get the first array point, then the next
// also need to bump by 8 for the subsequent rows
aLow =  col / 10 + (row * 8);
aHigh = (col / 10) + 1 + (row * 8);
// get the amount to interpolate for each of the 10 columns
// here were doing simple linear interpolation mainly to keep performace high and
// display is 5-6-5 color palet so fancy interpolation will get lost in low color depth
intPoint =   (( pixels[aHigh] - pixels[aLow] ) / 10.0 );
// determine how much to bump each column (basically 0-9)
incr = col % 10;
// find the interpolated value
val = (intPoint * incr ) +  pixels[aLow];
// store in the 70 x 70 array
// since display is pointing away, reverse row to transpose row data
HDTemp[ (7 - row) * 10][col] = val;

}
}
}

// interplation function to interpolate 70 columns from the interpolated rows
void InterpolateCols() {

// then interpolate the 70 rows between the 8 sensor points
for (col = 0; col < 70; col ++) {
for (row = 0; row < 70; row ++) {
// get the first array point, then the next
// also need to bump by 8 for the subsequent cols
aLow =  (row / 10 ) * 10;
aHigh = aLow + 10;
// get the amount to interpolate for each of the 10 columns
// here were doing simple linear interpolation mainly to keep performace high and
// display is 5-6-5 color palet so fancy interpolation will get lost in low color depth
intPoint =   (( HDTemp[aHigh][col] - HDTemp[aLow][col] ) / 10.0 );
// determine how much to bump each column (basically 0-9)
incr = row % 10;
// find the interpolated value
val = (intPoint * incr ) +  HDTemp[aLow][col];
// store in the 70 x 70 array
HDTemp[ row ][col] = val;
}
}
}

// function to display the results

Display.setRotation(2);

// rip through 70 rows
for (row = 0; row < 70; row ++) {

// fast way to draw a non-flicker grid--just make every 10 pixels 2x2 as opposed to 3x3
// drawing lines after the grid will just flicker too much
if (ShowGrid < 0) {
BoxWidth = 3;
}
else {
if ((row % 10 == 9) ) {
BoxWidth = 2;
}
else {
BoxWidth = 3;
}
}
// then rip through each 70 cols
for (col = 0; col < 70; col++) {

// fast way to draw a non-flicker grid--just make every 10 pixels 2x2 as opposed to 3x3
if (ShowGrid < 0) {
BoxHeight = 3;
}
else {
if ( (col % 10 == 9)) {
BoxHeight = 2;
}
else {
BoxHeight = 3;
}
}
// finally we can draw each the 70 x 70 points, note the call to get interpolated color
Display.fillRect((row * 3) + 15, (col * 3) + 15, BoxWidth, BoxHeight, GetColor(HDTemp[row][col]));
}
}

Display.setRotation(3);

}

// my fast yet effective color interpolation routine
uint16_t GetColor(float val) {

/*
pass in value and figure out R G B
several published ways to do this I basically graphed R G B and developed simple linear equations
again a 5-6-5 color display will not need accurate temp to R G B color calculation

equations based on

*/

red = constrain(255.0 / (c - b) * val - ((b * 255.0) / (c - b)), 0, 255);

if ((val > MinTemp) & (val < a)) {
green = constrain(255.0 / (a - MinTemp) * val - (255.0 * MinTemp) / (a - MinTemp), 0, 255);
}
else if ((val >= a) & (val <= c)) {
green = 255;
}
else if (val > c) {
green = constrain(255.0 / (c - d) * val - (d * 255.0) / (c - d), 0, 255);
}
else if ((val > d) | (val < a)) {
green = 0;
}

if (val <= b) {
blue = constrain(255.0 / (a - b) * val - (255.0 * b) / (a - b), 0, 255);
}
else if ((val > b) & (val <= d)) {
blue = 0;
}
else if (val > d) {
blue = constrain(240.0 / (MaxTemp - d) * val - (d * 240.0) / (MaxTemp - d), 0, 240);
}

// use the displays color mapping function to get 5-6-5 color palet (R=5 bits, G=6 bits, B-5 bits)
return Display.color565(red, green, blue);

}

// function to automatically set the max / min scale based on adding an offset to the average temp from the 8 x 8 array
// you could also try setting max and min based on the actual max min
void SetTempScale() {

if (DefaultTemp < 0) {
MinTemp = 25;
MaxTemp = 35;
Getabcd();
DrawLegend();
}
else {

val = 0.0;
for (i = 0; i < 64; i++) {
val = val + pixels[i];
}
val = val / 64.0;

MaxTemp = val + 2.0;
MinTemp = val - 2.0;
Getabcd();
DrawLegend();
}

}

// function to get the cutoff points in the temp vs RGB graph
void Getabcd() {

a = MinTemp + (MaxTemp - MinTemp) * 0.2121;
b = MinTemp + (MaxTemp - MinTemp) * 0.3182;
c = MinTemp + (MaxTemp - MinTemp) * 0.4242;
d = MinTemp + (MaxTemp - MinTemp) * 0.8182;

}

// function to handle screen touches
void ProcessTouch() {

x = Touch.getX();
y = Touch.getY();

// yea i know better to have buttons
if (x > 200) {
if (y < 80) {
}
else if (y > 160) {
}
else {
DefaultTemp = DefaultTemp * -1;
SetTempScale();
}
}

else if (x <= 200) {
// toggle grid
ShowGrid = ShowGrid * -1;
if (ShowGrid > 0) {
Display.fillRect(15, 15, 210, 210, C_BLACK);
}
}
}

// function to draw a cute little legend
void DrawLegend() {

// my cute little color legend with max and min text
j = 0;

float inc = (MaxTemp - MinTemp ) / 160.0;

for (ii = MinTemp; ii < MaxTemp; ii += inc) {
Display.drawFastHLine(260, 200 - j++, 30, GetColor(ii));
}

Display.setTextSize(2);
Display.setCursor(245, 20);
Display.setTextColor(C_WHITE, C_BLACK);
sprintf(buf, "%2d/%2d", MaxTemp, (int) (MaxTemp * 1.8) + 32);
Display.fillRect(233, 15, 94, 22, C_BLACK);
Display.setFont(Arial_14);
Display.print(buf);

Display.setTextSize(2);
// Display.setFont(Arial_24_Bold);
Display.setCursor(245, 220);
Display.setTextColor(C_WHITE, C_BLACK);
sprintf(buf, "%2d/%2d", MinTemp, (int) (MinTemp * 1.8) + 32);
Display.fillRect(233, 215, 94, 55, C_BLACK);
Display.setFont(Arial_14);
Display.print(buf);

}

// function to draw a numeric keypad

int wide = (3 * BUTTON_W ) + (4 * BUTTON_SPACING_X);
int high = (5 * BUTTON_H) +  (6 * BUTTON_SPACING_Y);
int TempNum = TheNumber;
bool KeepIn = true;

Display.fillRect(left, top, wide , high, C_DKGREY);
Display.drawRect(left, top, wide , high, C_LTGREY);

Display.fillRect(left + 10, top + 10, wide - 20 , 30, C_WHITE);
Display.drawRect(left + 10, top + 10, wide - 20 , 30, C_DKGREY);

Display.setCursor(left  + 20 , top + 20);
Display.setTextColor(C_BLACK, C_WHITE);

Display.print(TheNumber);

for (row = 0; row < 4; row++) {
for (col = 0; col < 3; col++) {
}
}
delay(300); // debounce
while (KeepIn) {
// get the touch point

x = Touch.getX();
y = Touch.getY();

// if nothing or user didn't press hard enough, don't do anything
if (Touch.dataAvailable()) {

// go thru all the KeyPadBtn, checking if they were pressed
for (byte b = 0; b < 12; b++) {
if (PressIt(KeyPadBtn[b], x, y) == true) {

if ((b < 9) | (b == 10)) {

TempNum *= 10;
if (TempNum == 0) {
Display.fillRect(left + 10, top + 10, wide - 20 , 30, C_WHITE);
}

if (b != 10) {
TempNum += (b + 1);
}

if (TempNum > 80) {
Display.fillRect(left + 10, top + 10, wide - 20 , 30, C_WHITE);
//Display.setCursor(left  + 100 , top + 20);
//Display.setTextColor(C_RED, C_WHITE);
//Display.print("Max 100");
TempNum = 0;
TempNum += (b + 1);
}
}
// clr button
if (b == 11) {
Display.fillRect(left + 10, top + 10, wide - 20 , 30, C_WHITE);
Display.drawRect(left + 10, top + 10, wide - 20 , 30, C_DKGREY);
TempNum = 0;
}
if (b == 9) {
KeepIn = false;
}

Display.setCursor(left  + 20 , top + 20);
Display.setTextColor(C_BLACK, C_WHITE);

Display.print(TempNum);

}
}
}
}

// clear screen redraw previous screen
// update the Current Channel
TheNumber = TempNum;

Display.fillRect(0, 0, 319, 239, C_BLACK);
Display.fillRect(10, 10, 220, 220, C_WHITE);
Display.fillRect(15, 15, 210, 210, C_BLACK);
Getabcd();
DrawLegend();

}

// function to handle color inversion of a pressed button
bool PressIt(Adafruit_GFX_Button TheButton, int x, int y) {

if (TheButton.contains(x, y)) {
TheButton.press(true);  // tell the button it is pressed
} else {
TheButton.press(false);  // tell the button it is NOT pressed
}
if (TheButton.justReleased()) {
TheButton.drawButton();  // draw normal
}
if (TheButton.justPressed()) {
TheButton.drawButton(true);  // draw invert!
delay(200); // UI debouncing
TheButton.drawButton(false);  // draw invert!
return true;
}
return false;
}

// end of code```

12. @KrisKasprzak, your code here is the same as in the youtube video, but there is no update for the touchscreen inside.

13. @KrisKasprzak, in your video you did change the value of the upper and lower temperature with the keypad. Can you give me your solution please?

14. josh911,

I'ts in my code that i posted on YouTube and above. There is a function called processTouch that allows the user to change the upper and lower. There is also a KeyPad function and some other supporting functions.

Not sure why you think the keypad stuff is not there.

Code:
```// function to handle screen touches
void ProcessTouch() {

x = Touch.getX();
y = Touch.getY();

// yea i know better to have buttons
if (x > 200) {
if (y < 80) {
}
else if (y > 160) {
}
else {
DefaultTemp = DefaultTemp * -1;
SetTempScale();
}
}

else if (x <= 200) {
// toggle grid
ShowGrid = ShowGrid * -1;
if (ShowGrid > 0) {
Display.fillRect(15, 15, 210, 210, C_BLACK);
}
}
}```

15. Sorry my mistake. I am a beginner from programming of Teensy. In your code is the Keypad function present.
I can switch the grid off and on, but if i want to change the value from the upper an lower temperture it is impossible.