# Improved AMG8833 PyGamer Thermal Camera

## Overview

![](https://cdn-learn.adafruit.com/assets/assets/000/102/575/medium800/temperature___humidity_IMG_0181.jpg?1622739792 Thermal Camera displaying a thermographic image.)

As with the original [PyGamer Thermal Camera](https://learn.adafruit.com/pygamer-thermal-camera-amg8833), this portable thermal camera project combines an AMG8833 IR Thermal Camera FeatherWing with a PyGamer. The upgraded&nbsp;CircuitPython code used in this version increases the camera resolution from 64 pixels (8 x 8) to 225 pixels (15 x 15) and deepens the color depth from 8 colors to 100 colors, all without hardware modifications.

The new code improves the camera's ability to visualize thermal images to help discern heating and air conditioning ventilation issues, to evaluate the quality of your home's insulation, and to avoid the sleeping cat when heading to the kitchen in the middle of the night.&nbsp;

Increasing the display's resolution required changes to the original camera's code to maintain a useful image frame display rate. As a result, performance monitoring was built-in to the new CircuitPython code as a series of time markers with a summary performance report printed to the serial port at the end of each frame update. See the section on **Performance Monitoring** &nbsp;for more information.

## Thermal Camera Features

The camera's thermal image can be frozen or focused at the touch of a button. The focus feature fine-tunes the display's temperature range to match the current image's maximum and maximum measurements, improving the detail of the image. To get a statistical view of an object's heat, switch to histogram mode. A settable alarm flashes lights and beeps when the camera sees a temperature at or above the threshold. The setup function is used to set the temperature display range and the alarm threshold. An editable configuration file contains the camera's power-up settings for the default temperature range and camera sensor direction.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/529/medium800/camera_OyIV3WZD3-pJf6NjA-Y3NhTq2epce85MyW3q1LcUq9HuKXFujnTjXaxINUWl-zMS4gB6NtfaU_lnsx6WetmPrdPKHWbu5Ngh8zxx.png?1622587552 Internal view of the AMG8833's 8x8 sensor array and control circuitry. Microphotograph courtesy of Adam McCombs.)

The camera's thermal imaging sensor is an 8 by 8 thermopile array that reads temperatures from 32°F to 176°F (0°C to 80°C)&nbsp;with an absolute accuracy of +- 4.5°F (2.5°C) and resolution of 0.9°F (0.5°C). To improve object recognition, the camera software algorithmically enlarges the number of imaged elements from 64 to 225 by calculating the in-between values using a technique called bilinear interpolation. See the guide section **1-2-3s of Bilinear Interpolation** &nbsp;for more detail about the technique.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/508/medium800/camera_iron_spectrum_ref_short.jpeg?1622570364 Visual representation of the iron temperature pseudocolor spectrum.)

Temperatures are represented in the&nbsp;displayed image as colors in a spectrum, ranging from a&nbsp;cold blue to white-hot. The color spectrum is based on a frequently-used palette similar to the range of colors seen when an iron bar is heated -- a technique a blacksmith might use to gauge the&nbsp;malleability&nbsp;of metal.

The camera's numeric temperature values are displayed as degrees Fahrenheit. Converting the values to Celsius is possible but is left as an exercise.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/576/medium800/temperature___humidity_IMG_0180.jpg?1622739907 Thermal Camera displaying a temperature histogram.)

The PyGamer Thermal Camera's custom cover skin was produced by a commercial on-line sticker printing service using the image file below.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/814/medium800/temperature___humidity_Thermal_Cam_panel_label_sticker.jpg?1623271646)

Warning: 

## Parts
### Adafruit AMG8833 IR Thermal Camera FeatherWing

[Adafruit AMG8833 IR Thermal Camera FeatherWing](https://www.adafruit.com/product/3622)
A Feather board without ambition is a Feather board without FeatherWings! This is the **Thermal Camera FeatherWing** : thanks to the Panasonic AMG8833 8x8 GridEYE sensor, it adds heat-vision to _any_ Feather main board. Using our <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/3622)
[Related Guides to the Product](https://learn.adafruit.com/products/3622/guides)
![TFT with thermal camera display, a hand with two fingers slowly passes in front, so the heat map is visible.](https://cdn-shop.adafruit.com/product-videos/640x480/3622-03.jpg)

### Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino

[Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino](https://www.adafruit.com/product/4242)
What&nbsp;fits in your pocket, is fully Open Source, and can run CircuitPython, MakeCode Arcade or Arduino games you write yourself? That's right, it's the **Adafruit PyGamer!** We wanted to make an entry-level gaming handheld for DIY gaming, and maybe a little...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4242)
[Related Guides to the Product](https://learn.adafruit.com/products/4242/guides)
![Angled shot of Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino.](https://cdn-shop.adafruit.com/640x480/4242-00.jpg)

### Lithium Ion Polymer Battery with Short Cable - 3.7V 350mAh

[Lithium Ion Polymer Battery with Short Cable - 3.7V 350mAh](https://www.adafruit.com/product/4237)
Lithium-ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light, and powerful. The output ranges from 4.2V when completely charged to 3.7V. This battery has a capacity of 350mAh for a total of about 1.3 Wh. If you need a larger (or smaller!) battery, <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/4237)
[Related Guides to the Product](https://learn.adafruit.com/products/4237/guides)
![Lithium Ion Polymer Battery 3.7v 350mAh with JST 2-PH connector and short cable](https://cdn-shop.adafruit.com/640x480/4237-04.jpg)

### Adafruit PyGamer Acrylic Enclosure Kit

[Adafruit PyGamer Acrylic Enclosure Kit](https://www.adafruit.com/product/4238)
You've got your PyGamer, and you're ready to start jammin' on your favorite arcade games. You gaze adoringly at the charming silkscreen designed by Ada-friend PaintYourDragon.&nbsp;The nostalgia is palpable!

Cradling the PCB in your hands, you realize there's something...

In Stock
[Buy Now](https://www.adafruit.com/product/4238)
[Related Guides to the Product](https://learn.adafruit.com/products/4238/guides)
![Enclosure pieces and black, plastic hardware for a DIY handheld game console.](https://cdn-shop.adafruit.com/640x480/4238-00.jpg)

### Mini Oval Speaker with Short Wires - 8 Ohm 1 Watt

[Mini Oval Speaker with Short Wires - 8 Ohm 1 Watt](https://www.adafruit.com/product/4227)
Hear the good news! This wee speaker&nbsp;is&nbsp;a&nbsp;great addition to any audio project where you need 8 ohm impedance and 1W or less of power. We particularly like this&nbsp;speaker&nbsp;as it is&nbsp;small and comes with nice skinny wires with a connector on the end. It has a handy...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4227)
[Related Guides to the Product](https://learn.adafruit.com/products/4227/guides)
![Mini Oval Speaker with Short Wires ](https://cdn-shop.adafruit.com/640x480/4227-06.jpg)

### Plastic Button Caps For Square Top (10-pack) - 8mm Diameter

[Plastic Button Caps For Square Top (10-pack) - 8mm Diameter](https://www.adafruit.com/product/4228)
These Reese's Piece's lookin' bits fit perfectly on top of tactile buttons with 2.4mm square tops and give a satisfying 8mm diameter surface area for your fingers to press.  
  
You get 10 candy-colored round caps. You get two of each color: **red, yellow, white,...**

In Stock
[Buy Now](https://www.adafruit.com/product/4228)
[Related Guides to the Product](https://learn.adafruit.com/products/4228/guides)
![Angled shot of 10 plastic button caps colored reddish-orange, yellow, white, and black.](https://cdn-shop.adafruit.com/640x480/4228-04.jpg)

Other than the AMG8833 Thermal Camera FeatherWing, the following kit contains the PyGamer parts for this project including a nifty carrying case.

### Adafruit PyGamer Starter Kit

[Adafruit PyGamer Starter Kit](https://www.adafruit.com/product/4277)
**Please note: you may get a royal blue _or_ purple case with your starter kit (they're both lovely colors)**

What&nbsp;fits in your pocket, is fully Open Source, and can run CircuitPython, MakeCode Arcade or Arduino games you write yourself? That's right,...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4277)
[Related Guides to the Product](https://learn.adafruit.com/products/4277/guides)
![Adafruit PyGamer Starter Kit with PCB, enclosure, buttons, and storage bag](https://cdn-shop.adafruit.com/640x480/4277-08.jpg)

## Acknowledgements

Thank you to Adam McCombs for the highly detailed optical and electron microscope photographs of the de-capped AMG8833 sensor. It's fascinating to see how it operates under the covers.

Special thanks to David Glaude and Zoltán Vörös for the&nbsp;_ulab_-based bilinear interpolation helper. Array calculations using CircuitPython's integral&nbsp;_ulab_ (micro lab)&nbsp;library are amazingly fast and efficient!

For more information about _ulab_, check out Jeff Epler's [ulab: Crunch Numbers Fast in CircuitPython](https://learn.adafruit.com/ulab-crunch-numbers-fast-with-circuitpython "ulab: Crunch Numbers fast in CircuitPython")&nbsp;learning guide.

# Improved AMG8833 PyGamer Thermal Camera

## Features and Operation

![](https://cdn-learn.adafruit.com/assets/assets/000/102/667/medium800/temperature___humidity_Base39.tifSat-05-Jan-2019_11-11-53.jpg?1623054839 Close-up view of a few thermopile elements within the AMG8833 thermal camera's sensor array. Electron microscope photograph courtesy of Adam McCombs.)

The Thermal Camera's controls are used to switch display modes, take snapshots, automatically increase or decrease image temperature gradient detail, and facilitate setting alarm and maximum/minimum display range parameters. The display shows the image or histogram and the currently measured maximum, minimum, and average temperature values in Fahrenheit.

## Display Layout
![](https://cdn-learn.adafruit.com/assets/assets/000/102/666/medium800/temperature___humidity_interpolation_grid_example.003.jpeg?1623053892 Thermal Camera Display Layout)

The camera's display is divided into four zones. The temperature value sidebar is used to display the alarm ( **alm** ) threshold setting, the measured maximum temperature ( **max** ), the average temperature calculation ( **ave** ), and the measured minimum temperature ( **min** ). The sidebar continuously displays measured values during normal operation. When in the **Setup** mode, the sidebar indicates the current alarm threshold, the maximum display range, and the minimum display range.

The image grid area consists of 225 blocks in a 15 column by 15 row array. The image grid is used to display a thermal sensor image or histogram.

Superimposed over the display grid are the status message area (centered in the image array area) and the histogram legend area (near the bottom of the image array area). The status message area indicates various operational states including **Hold** , **Focus** , and the **Setup** mode. The histogram legend area shows the current minimum and maximum display range settings when viewing a histogram

![](https://cdn-learn.adafruit.com/assets/assets/000/102/664/medium800/temperature___humidity_interpolation_grid_example.004.jpeg?1623053424 Thermal Camera Operation)

## Hold Mode

The **HOLD** button (PyGamer **BUTTON\_A** ) freezes and releases the image or histogram display contents. Press the button once to hold the display; press it again to resume normal operation. The **IMAGE** &nbsp;and **FOCUS** buttons continue to operate normally regardless of whether or not the display is held.

## Image / Histogram Mode

The **IMAGE** (PyGamer **BUTTON\_B** ) is used to toggle between a temperature gradient image and a temperature distribution representation of the thermopile sensor's measurements. The **IMAGE** &nbsp;button is operational when in **Hold** &nbsp;mode to allow analysis of held measurements.

## Focus Range / Default Range

The **FOCUS** button (PyGamer **BUTTON\_SELECT** ) automatically changes the minimum and maximum display range values to provide increased or decreased detail based on the currently measured maximum and minimum temperatures. Press **FOCUS** once to change the current display range from the current setting to a range that matches the measured minimum and maximum values. Press it again to return to the original display range settings. **Focus** mode is useful when looking for increased temperature gradient detail or when the temperature of the object is outside of the default display range.

## Setup Function

Pressing the **SET** button (PyGamer **BUTTON\_START** ) will stop normal operation and enter the **Setup** mode to adjust the alarm threshold and maximum/minimum display range. Use the joystick or the PyBadge D-Pad buttons to highlight the parameter to change, then press the **HOLD** button to select. Use the joystick to increase or decrease the parameter value. Press the&nbsp; **HOLD** button to select the new value. To exit the **Setup** mode, press the **SET** button.

The newly selected values will go into effect when exiting **Setup** mode, but will not be preserved if the camera's power is turned off. To change power-on parameter values, edit the **thermalcamera\_config.py** file with _mu_ or your favorite text editor.

# Improved AMG8833 PyGamer Thermal Camera

## Build the Camera

It's time to get the PyGamer ready by installing CircuitPython and its libraries, plug in the speaker and battery, and put it into an elegant enclosure. Once the enclosure is in place, we'll attach the AMG8833 FeatherWing and load the Thermal Camera code.

You can build the Thermal Camera from individual components or from the [PyGamer Starter Kit](https://www.adafruit.com/product/4277 "PyGamer Starter Kit"). Add the [AMG8833 FeatherWing](https://www.adafruit.com/product/3622 "AMG8833 Thermal Camera FeatherWing") and you'll be ready to go.

## Assembling the PyGamer

The [PyGamer Introduction](https://learn.adafruit.com/adafruit-pygamer "Introduction to PyGamer") will guide you through the process of setting up the PyGamer to include the case, battery, and speaker.

You may follow the [Starter Kit enclosure instructions](https://learn.adafruit.com/adafruit-pygamer/build-the-pygamer-case "Starter Kit Enclosure Instructions") for installing the speaker and battery, even if you don't plan to use the enclosure.

![](https://cdn-learn.adafruit.com/assets/assets/000/087/150/medium800/temperature___humidity_IMG_0448a.jpg?1579317963)

## Prepare the FeatherWing
Solder the included male headers onto the AMG8833 FeatherWing and attach it through the acrylic back panel into the PyGamer's Feather connector. Refer to the [soldering guide](https://learn.adafruit.com/adafruit-guide-excellent-soldering/tools "Guide to Excellent Soldering") if this is your first time with a soldering iron.

![temperature___humidity_IMG_0497a.jpg](https://cdn-learn.adafruit.com/assets/assets/000/087/149/medium640/temperature___humidity_IMG_0497a.jpg?1579318082)

## Preparing the PyGamer with CircuitPython, Libraries, and Accessories

The [PyGamer Introduction](https://learn.adafruit.com/adafruit-pygamer "Introduction to PyGamer") guide also has the information needed to [install CircuitPython](https://learn.adafruit.com/adafruit-pygamer/circuitpython "Install CircuitPython") and its [libraries](https://learn.adafruit.com/adafruit-pygamer/circuitpython-libraries "CircuitPython Libraries").

# Improved AMG8833 PyGamer Thermal Camera

## Software Setup

![](https://cdn-learn.adafruit.com/assets/assets/000/102/486/medium800/camera_temperature___humidity_robotics___cnc_circuitpython_blinka-small.png?1622494900)

This project uses CircuitPython, a user friendly version of Python for microcontrollers. The files are just text files and are copied over to the PyGamer to the flash drive **CIRCUITPY** which appears when the PyGamer is attached to a computer via a USB cable.

## Preparing the PyGamer with CircuitPython and Software Libraries

The [PyGamer Introduction](https://learn.adafruit.com/adafruit-pygamer "Introduction to PyGamer") guide also has the information needed to [install CircuitPython](https://learn.adafruit.com/adafruit-pygamer/circuitpython "Install CircuitPython") and its [libraries](https://learn.adafruit.com/adafruit-pygamer/circuitpython-libraries "CircuitPython Libraries").

See the following pages on how to perform these operations.

# Improved AMG8833 PyGamer Thermal Camera

## CircuitPython

[CircuitPython](https://github.com/adafruit/circuitpython) is a derivative of [MicroPython](https://micropython.org) designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the **CIRCUITPY** &nbsp;flash drive to iterate.

The following instructions will show you how to install CircuitPython. If you've already installed CircuitPython but are looking to update it or reinstall it, the same steps work for that as well!

## Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

[Download the latest version of CircuitPython for PyGamer via circuitpython.org](https://circuitpython.org/board/pygamer/)
## Further Information

For more detailed info on installing CircuitPython, check out [Installing CircuitPython](https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython).

 **Click the link above and download the latest UF2 file.**

Download and save it to your desktop (or wherever is handy).

![adafruit_products_PyGamer_Download_UF2.png](https://cdn-learn.adafruit.com/assets/assets/000/076/054/medium640/adafruit_products_PyGamer_Download_UF2.png?1558746705)

Plug your PyGamer into your computer using a known-good USB cable.

**A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.**

Double-click the **Reset button on the top** of your board (indicated by the red arrow in the first image). You will see an image on the display instructing you to drag a UF2 file to your board, and **the row of NeoPixel RGB LEDs on the front will turn green** (indicated by the green arrow and square in the image). If they turn red, check the USB cable, try another USB port, etc.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

![adafruit_products_PyGamer_Bootloader.jpg](https://cdn-learn.adafruit.com/assets/assets/000/076/055/medium640/adafruit_products_PyGamer_Bootloader.jpg?1558747721)

You will see a new disk drive appear called **PYGAMERBOOT**.

&nbsp;

Drag the **adafruit\_circuitpython\_etc.uf2** file to **PYGAMERBOOT.**

![adafruit_products_PyGamer_PYGAMERBOOT.png](https://cdn-learn.adafruit.com/assets/assets/000/076/056/medium640/adafruit_products_PyGamer_PYGAMERBOOT.png?1558747844)

![adafruit_products_PyGamer_Drag_UF2.png](https://cdn-learn.adafruit.com/assets/assets/000/076/057/medium640/adafruit_products_PyGamer_Drag_UF2.png?1558747852)

The LEDs will flash. Then, the **PYGAMERBOOT** drive will disappear and a new disk drive called **CIRCUITPY** will appear.

That's it, you're done! :)

![adafruit_products_PyGamer_CIRCUITPY.png](https://cdn-learn.adafruit.com/assets/assets/000/076/058/medium640/adafruit_products_PyGamer_CIRCUITPY.png?1558747892)

# Improved AMG8833 PyGamer Thermal Camera

## CircuitPython Libraries

Danger: 

Each CircuitPython program you run needs to have a lot of information to work. The reason CircuitPython is so simple to use is that most of that information is stored in other files and works in the background. These files are called _libraries_. Some of them are built into CircuitPython. Others are stored on your **CIRCUITPY** drive in a folder called **lib**. Part of what makes CircuitPython so great is its ability to store code separately from the firmware itself. Storing code separately from the firmware makes it easier to update both the code you write and the libraries you depend.

Your board may ship with a **lib** folder already, it's in the base directory of the drive. If not, simply create the folder yourself. When you first install CircuitPython, an empty **lib** directory will be created for you.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/961/medium800/circuitpython_WtCP_CIRCUITPY_contents.png?1635281033)

CircuitPython libraries work in the same way as regular Python modules so the [Python docs](https://docs.python.org/3/tutorial/modules.html) are an excellent reference for how it all should work. In Python terms, you can place our library files in the **lib** directory because it's part of the Python path by default.

One downside of this approach of separate libraries is that they are not built in. To use them, one needs to copy them to the **CIRCUITPY** drive before they can be used. Fortunately, there is a library bundle.

The bundle and the library releases on GitHub also feature optimized versions of the libraries with the **.mpy** file extension. These files take less space on the drive and have a smaller memory footprint as they are loaded.

Due to the regular updates and space constraints, Adafruit does not ship boards with the entire bundle. Therefore, you will need to load the libraries you need when you begin working with your board. You can find example code in the guides for your board that depends on external libraries.

Either way, as you start to explore CircuitPython, you'll want to know how to get libraries on board.

# The Adafruit Learn Guide Project Bundle

The quickest and easiest way to get going with a project from the Adafruit Learn System is by utilising the Project Bundle. Most guides now have a **Download Project Bundle** button available at the top of the full code example embed. This button downloads all the necessary files, including images, etc., to get the guide project up and running. Simply click, open the resulting zip, copy over the right files, and you're good to go!

The first step is to find the Download Project Bundle button in the guide you're working on.

Info: 

![](https://cdn-learn.adafruit.com/assets/assets/000/111/837/medium800/circuitpython_PB_download_project_bundle_button.png?1652915277)

Warning: 

The Download Project Bundle button downloads a zip file. This zip contains a series of directories, nested within which is the **code.py** , any applicable assets like images or audio, and the **lib/** folder containing all the necessary libraries. The following zip was downloaded from the Piano in the Key of Lime guide.

![](https://cdn-learn.adafruit.com/assets/assets/000/111/838/medium800/circuitpython_PB_downloaded_and_expanded_zip.png?1652915317)

Info: 

When you open the zip, you'll find some nested directories. Navigate through them until you find what you need. You'll eventually find a directory for your CircuitPython version (in this case, 7.x). In the version directory, you'll find the file and directory you need: **code.py** and **lib/**. Once you find the content you need, you can copy it all over to your **CIRCUITPY** drive, replacing any files already on the drive with the files from the freshly downloaded zip.

Info: 

Once you copy over all the relevant files, the project should begin running! If you find that the project is not running as expected, make sure you've copied ALL of the project files onto your microcontroller board.

That's all there is to using the Project Bundle!

# The Adafruit CircuitPython Library Bundle

Adafruit provides CircuitPython libraries for much of the hardware they provide, including sensors, breakouts and more. To eliminate the need for searching for each library individually, the libraries are available together in the Adafruit CircuitPython Library Bundle. The bundle contains all the files needed to use each library.

## Downloading the Adafruit CircuitPython Library Bundle

You can download the latest Adafruit CircuitPython Library Bundle release by clicking the button below. The libraries are being constantly updated and improved, so you'll always want to download the latest bundle.&nbsp;

**Match up the bundle version with the version of CircuitPython you are running.** For example, you would download the 6.x library bundle if you're running any version of CircuitPython 6, or the 7.x library bundle if you're running any version of CircuitPython 7, etc. If you mix libraries with major CircuitPython versions, you will get incompatible mpy errors due to changes in library interfaces possible during major version changes.

[Click to visit circuitpython.org for the latest Adafruit CircuitPython Library Bundle](https://circuitpython.org/libraries)
 **Download the bundle version that matches your CircuitPython firmware version.** If you don't know the version, check the version info in **boot\_out.txt** file on the **CIRCUITPY** drive, or the initial prompt in the CircuitPython REPL. For example, if you're running v7.0.0, download the 7.x library bundle.

There's also a **py** bundle which contains the uncompressed python files, you probably _don't_ want that unless you are doing advanced work on libraries.

# The CircuitPython Community Library Bundle

The CircuitPython Community Library Bundle is made up of libraries written and provided by members of the CircuitPython community. These libraries are often written when community members encountered hardware not supported in the Adafruit Bundle, or to support a personal project. The authors all chose to submit these libraries to the Community Bundle make them available to the community.

**These libraries are maintained by their authors and are not supported by Adafruit.** As you would with any library, if you run into problems, feel free to file an issue on the GitHub repo for the library. Bear in mind, though, that most of these libraries are supported by a single person and you should be patient about receiving a response. Remember, these folks are not paid by Adafruit, and are volunteering their personal time when possible to provide support.

## Downloading the CircuitPython Community Library Bundle

You can download the latest CircuitPython Community Library Bundle release by clicking the button below. The libraries are being constantly updated and improved, so you'll always want to download the latest bundle.

[Click for the latest CircuitPython Community Library Bundle release](https://github.com/adafruit/CircuitPython_Community_Bundle/releases)
The link takes you to the latest release of the CircuitPython Community Library Bundle on GitHub. There are multiple versions of the bundle available. **Download the bundle version that matches your CircuitPython firmware version.** If you don't know the version, check the version info in **boot\_out.txt** file on the **CIRCUITPY** drive, or the initial prompt in the CircuitPython REPL. For example, if you're running v7.0.0, download the 7.x library bundle.

# Understanding the Bundle

After downloading the zip, extract its contents. This is usually done by double clicking on the zip. On Mac OSX, it places the file in the same directory as the zip.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/908/medium800/circuitpython_WtCP_lib_bundle_extracted.png?1635183852)

Open the bundle folder. Inside you'll find two information files, and two folders. One folder is the lib bundle, and the other folder is the examples bundle.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/909/medium800/circuitpython_WtCP_lib_zip_contents.png?1635183864)

Now open the lib folder. When you open the folder, you'll see a large number of **.**** mpy** files, and folders.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/910/medium800/circuitpython_WtCP_lib_bundle_folder_contents.png?1635183871)

## Example Files

All example files from each library are now included in the bundles in an **examples** directory (as seen above), as well as an examples-only bundle. These are included for two main reasons:

- Allow for quick testing of devices.
- Provide an example base of code, that is easily built upon for individualized purposes.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/911/medium800/circuitpython_WtCP_examples_bundle_directory_contents.png?1635184002)

## Copying Libraries to Your Board

First open the **lib** folder on your **CIRCUITPY** drive. Then, open the **lib** folder you extracted from the downloaded zip. Inside you'll find a number of folders and **.mpy** files. Find the library you'd like to use, and copy it to the **lib** folder on **CIRCUITPY**.

If the library is a directory with multiple **.mpy** files in it, be sure to **copy the entire folder to CIRCUITPY/lib**.

This also applies to example files. Open the **examples** folder you extracted from the downloaded zip, and copy the applicable file to your **CIRCUITPY** drive. Then, rename it to **code.py** to run it.

Info: 

# Understanding Which Libraries to Install

You now know how to load libraries on to your CircuitPython-compatible microcontroller board. You may now be wondering, how do you know _which_ libraries you need to install? Unfortunately, it's not always straightforward. Fortunately, there is an obvious place to start, and a relatively simple way to figure out the rest. First up: the best place to start.

When you look at most CircuitPython examples, you'll see they begin with one or more `import` statements. These typically look like the following:

- `import library_or_module`

However, `import` statements can also sometimes look like the following:

- `from library_or_module import name`
- `from library_or_module.subpackage import name`
- `from library_or_module import name as local_name`

They can also have more complicated formats, such as including a `try` / `except` block, etc.

The important thing to know is that **an** `import` **statement will always include the name of the module or library that you're importing**.

Therefore, the best place to start is by reading through the `import` statements.

Here is an example import list for you to work with in this section. There is no setup or other code shown here, as the purpose of this section involves only the import list.

```python
import time
import board
import neopixel
import adafruit_lis3dh
import usb_hid
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode
```

Keep in mind, not all imported items are libraries. Some of them are almost always built-in CircuitPython modules. How do you know the difference? Time to visit the REPL.

In the [Interacting with the REPL section](https://learn.adafruit.com/welcome-to-circuitpython/the-repl#interacting-with-the-repl-2977486-14) on [The REPL page](https://learn.adafruit.com/welcome-to-circuitpython/the-repl) in this guide, the `help("modules")` command is discussed. This command provides a list of all of the built-in modules available in CircuitPython for your board. So, if you connect to the serial console on your board, and enter the REPL, you can run `help("modules")` to see what modules are available for your board. Then, as you read through the `import` statements, you can, for the purposes of figuring out which libraries to load, ignore the statement that import modules.

The following is the list of modules built into CircuitPython for the Feather RP2040. Your list may look similar or be anything down to a significant subset of this list for smaller boards.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/967/medium800/circuitpython_WtCP_CP_libs_help_modules_feather_rp2040.png?1635369618)

Now that you know what you're looking for, it's time to read through the import statements. The first two, `time` and `board`, are on the modules list above, so they're built-in.

The next one, `neopixel`, is not on the module list. That means it's your first library! So, you would head over to the bundle zip you downloaded, and search for **neopixel**. There is a **neopixel.mpy** file in the bundle zip. Copy it over to the **lib** folder on your **CIRCUITPY** drive. The following one, `adafruit_lis3dh`, is also not on the module list. Follow the same process for **adafruit\_lis3dh** , where you'll find **adafruit\_lis3dh.mpy** , and copy that over.

The fifth one is `usb_hid`, and it is in the modules list, so it is built in. Often all of the built-in modules come first in the import list, but sometimes they don't! Don't assume that everything after the first library is also a library, and verify each import with the modules list to be sure. Otherwise, you'll search the bundle and come up empty!

The final two imports are not as clear. Remember, when `import` statements are formatted like this, the first thing after the `from` is the library name. In this case, the library name is `adafruit_hid`. A search of the bundle will find an **adafruit\_hid _folder_**. When a library is a folder, you must copy the **entire folder and its contents&nbsp;_as it is in the bundle_** to the **lib** folder on your **CIRCUITPY** drive. In this case, you would copy the entire **adafruit\_hid** folder to your **CIRCUITPY/lib** folder.

Notice that there are _two_ imports that begin with `adafruit_hid`. Sometimes you will need to import more than one thing from the same library. Regardless of how many times you import the same library, you only need to load the library by copying over the **adafruit\_hid** folder _once_.

That is how you can use your example code to figure out what libraries to load on your CircuitPython-compatible board!

There are cases, however, where libraries require other libraries internally. The internally required library is called a _dependency_. In the event of library dependencies, the easiest way to figure out what other libraries are required is to connect to the serial console and follow along with the `ImportError` printed there. The following is a very simple example of an `ImportError`, but the concept is the same for any missing library.

# Example: `ImportError` Due to Missing Library

If you choose to load libraries as you need them, or you're starting fresh with an existing example, you may end up with code that tries to use a library you haven't yet loaded.&nbsp; This section will demonstrate what happens when you try to utilise a library that you don't have loaded on your board, and cover the steps required to resolve the issue.

This demonstration will only return an error if you do not have the required library loaded into the **lib** folder on your **CIRCUITPY** drive **.**

Let's use a modified version of the Blink example.

```auto
import board
import time
import simpleio

led = simpleio.DigitalOut(board.LED)

while True:
    led.value = True
    time.sleep(0.5)
    led.value = False
    time.sleep(0.5)
```

Save this file. Nothing happens to your board. Let's check the serial console to see what's going on.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/964/medium800/circuitpython_WtCP_serial_console_ImportError.png?1635355664)

You have an `ImportError`. It says there is `no module named 'simpleio'`. That's the one you just included in your code!

Click the link above to download the correct bundle. Extract the lib folder from the downloaded bundle file. Scroll down to find **simpleio.mpy**. This is the library file you're looking for! Follow the steps above to load an individual library file.

The LED starts blinking again! Let's check the serial console.

![](https://cdn-learn.adafruit.com/assets/assets/000/105/965/medium800/circuitpython_WtCP_CP_libraries_ImportError_resolved.png?1635355829)

No errors! Excellent. You've successfully resolved an `ImportError`!

If you run into this error in the future, follow along with the steps above and choose the library that matches the one you're missing.

# Library Install on Non-Express Boards

If you have an M0 non-Express board such as Trinket M0, Gemma M0, QT Py M0, or one of the M0 Trinkeys, you'll want to follow the same steps in the example above to install libraries as you need them. Remember, you don't need to wait for an `ImportError` if you know what library you added to your code. Open the library bundle you downloaded, find the library you need, and drag it to the **lib** folder on your **CIRCUITPY** drive.

You can still end up running out of space on your M0 non-Express board even if you only load libraries as you need them. There are a number of steps you can use to try to resolve this issue. You'll find suggestions on the [Troubleshooting page](https://learn.adafruit.com/welcome-to-circuitpython/troubleshooting).

# Updating CircuitPython Libraries and Examples

Libraries and examples are updated from time to time, and it's important to update the files you have on your **CIRCUITPY** drive.

To update a single library or example, follow the same steps above. When you drag the library file to your lib folder, it will ask if you want to replace it. Say yes. That's it!

A new library bundle is released every time there's an update to a library. Updates include things like bug fixes and new features. It's important to check in every so often to see if the libraries you're using have been updated.

## CircUp CLI Tool

There is a command line interface (CLI) utility called [CircUp](https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup) that can be used to easily install and update libraries on your device. Follow the directions on the [install page within the CircUp learn guide](https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup/install-circup). Once you've got it installed you run the command `circup update` in a terminal to interactively update all libraries on the connected CircuitPython device. See the [usage page in the CircUp guide](https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup/usage) for a full list of functionality

# Improved AMG8833 PyGamer Thermal Camera

## CircuitPython Code

## PyGamer Thermal Camera Source Code

Download the project's source files and copy them to the PyGamer's **CIRCUITPY** root directory, including the **fonts** and **index\_to\_rgb** folders.

In the code window below, click the link **Download Project Bundle**. This will download a zip file containing the code (4 .py files), the index\_to\_rgb folder, needed library files and the font folder.

![](https://cdn-learn.adafruit.com/assets/assets/000/116/443/medium800/temperature___humidity_thermalcamera_circuitpy_directory.png?1667526952)

The zip folder contains the following folders and files:

- **code.py&nbsp;** main thermal camera code
- **fonts** folder
  - _OpenSans-9.bdf_&nbsp; font file

- **index\_to\_rgb** folder
  - _iron\_spectrum.py_ color converter method file

- A **lib** folder containing these required libraries:

- **thermalcamera\_config.py** &nbsp; start-up default settings
- **thermalcamera\_converters.py** &nbsp; temperature converter helpers
- **thermalcamera\_splash.bmp** &nbsp; startup screen graphic

Here's the main CircuitPython code for the Thermal Camera. It's contained in the project zip folder as _ **code.py** _. Copy this to the main (root) folder of the **CIRCUITPY** drive that appears when your PyGamer is connected to your computer via a known good USB cable.

https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/main/PyGamer_Improved_Thermal_Camera/code.py

The Thermal Camera needs some helpers to convert back and forth between Celsius and Fahrenheit units. This file is contained in the project zip folder as **thermalcamera\_converters.py**.

https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/main/PyGamer_Improved_Thermal_Camera/thermalcamera_converters.py

The color spectrum is calculated by the **iron.py** &nbsp;helper file within the **index\_to\_rgb** folder. The helper calculates a 24-bit red, green, and blue (RGB) color value from an input value of 0 to 1.0.

https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/main/PyGamer_Improved_Thermal_Camera/index_to_rgb/iron.py

Finally, the power-up alarm threshold, temperature display range settings, and camera orientation are contained in the **thermalcamera\_config.py** file. All values are in degrees Fahrenheit. A `SELFIE` value of `True` adjusts the image for a front-facing camera&nbsp;orientation; `False` is used for cameras facing away from the viewer.

https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/main/PyGamer_Improved_Thermal_Camera/thermalcamera_config.py

After copying all the project files to the PyGamer, you'll see the camera's splash graphics and a sample of the iron color spectrum. After a couple of beeps, the thermal image will appear.

The next section shows the features of the camera and how it operates.

Info: 

# Improved AMG8833 PyGamer Thermal Camera

## CircuitPython Code Details

The CircuitPython code for the Thermal Camera project is contained in four files:

- **code.py** , the main code module,
- **thermalcamera\_converters.py** the temperature unit conversion helper,
- **iron.py** &nbsp;(in the **index\_to\_rgb** folder), the iron pseudocolor spectrum conversion helper, and
- **thermalcamera\_config.py** , the start-up default parameter file.

## Code Details

Let's take a walk through the code and look in more detail how each section works starting with **code.py**.

The main module, **code.py** , prepares and operates the Thermal Camera. It consists of the following major sections:

- **Import and Initialize:** Libraries, Devices, and Welcome Screen
- **Constants:** Display, Min/Max, and Alarm Threshold Values
- **Helpers:** Display, Interpolate, Joystick, and Setup Functions
- **Display:** Define Group Layers
- **Primary Process:** Setup and Loop

Things are started with importing libraries, establishing devices, and saying hello, all of which are described on the following pages.

Info: 

# Improved AMG8833 PyGamer Thermal Camera

## Import and Initialize

When the PyGamer's power is turned on, the **code.py** module first imports all the required libraries. That includes the **thermalcamera\_converters** &nbsp;and **index\_to\_rgb** &nbsp;helper files that we'll review later.

```python
import time
import gc
import board
import keypad
import busio
from ulab import numpy as np
import displayio
import neopixel
from analogio import AnalogIn
from digitalio import DigitalInOut
from simpleio import map_range, tone
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_display_shapes.rect import Rect
import adafruit_amg88xx
from index_to_rgb.iron import index_to_rgb
from thermalcamera_converters import celsius_to_fahrenheit, fahrenheit_to_celsius
from thermalcamera_config import ALARM_F, MIN_RANGE_F, MAX_RANGE_F, SELFIE
```

After importing libraries, the display and default font are instantiated, the speaker is enabled, and the on-board NeoPixels are defined.

If the PyGamer's joystick is present, the `HAS_JOYSTICK` flag is set to `True`. If not, then the host device is probably a PyBadge or EdgeBadge. This allows the code to work for those devices in addition to the PyGamer, interpreting the Badge D-Pad buttons like the Gamer's joystick.

```python
# Instantiate the integral display and define its size
display = board.DISPLAY
display.brightness = 1.0
WIDTH = display.width
HEIGHT = display.height

# Load the text font from the fonts folder
font_0 = bitmap_font.load_font("/fonts/OpenSans-9.bdf")

# Instantiate the joystick if available
if hasattr(board, "JOYSTICK_X"):
    # PyGamer with joystick
    HAS_JOYSTICK = True
    joystick_x = AnalogIn(board.JOYSTICK_X)
    joystick_y = AnalogIn(board.JOYSTICK_Y)
else:
    # PyBadge with buttons
    HAS_JOYSTICK = False  # PyBadge with buttons

# Enable the speaker
DigitalInOut(board.SPEAKER_ENABLE).switch_to_output(value=True)

# Instantiate and clear the NeoPixels
pixels = neopixel.NeoPixel(board.NEOPIXEL, 5, pixel_order=neopixel.GRB)
pixels.brightness = 0.25
pixels.fill(0x000000)
```

The PyGamer and PyBadge control buttons are connected to a hardware shift register chip that is controlled by the `keypad.ShiftRegisterKeys` class. This section of the code defines each button's bit position within the shift register. The local `keypad.ShiftRegisterKeys` class, `panel`, will be used to read the buttons in the primary process loop and setup helper.

```python
# Initialize ShiftRegisterKeys to read PyGamer/PyBadge buttons
panel = keypad.ShiftRegisterKeys(
    clock=board.BUTTON_CLOCK,
    data=board.BUTTON_OUT,
    latch=board.BUTTON_LATCH,
    key_count=8,
    value_when_pressed=True,
)

# Define front panel button event values
BUTTON_LEFT = 7  # LEFT button
BUTTON_UP = 6  # UP button
BUTTON_DOWN = 5  # DOWN button
BUTTON_RIGHT = 4  # RIGHT button
BUTTON_FOCUS = 3  # SELECT button
BUTTON_SET = 2  # START button
BUTTON_HOLD = 1  # button A
BUTTON_IMAGE = 0  # button B
```

Now it's time to connect to and instantiate the AMG8833 thermal camera FeatherWing using the I2C bus connection. The I2C serial bus speed is increased from the default 100K to 400K bits per second to improve data acquisition speed and ultimately the display frame rate.

This section of the code will also work if the AMG8833 thermal camera STEMMA breakout is used in place of the FeatherWing version. STEMMA cable length may impact sensor performance, so if you encounter issues with the breakout version, try reducing the I2C bus speed to the default 100K bits per second rate.

```python
# Initiate the AMG8833 Thermal Camera
i2c = busio.I2C(board.SCL, board.SDA, frequency=400000)
amg8833 = adafruit_amg88xx.AMG88XX(i2c)
```

Next, the welcome graphics screen,&nbsp; **thermalcamera\_splash.bmp** is displayed. The size of the image is scaled to fit the size of the PyGamer's display.

```python
# Display splash graphics
splash = displayio.Group(scale=display.width // 160)
bitmap = displayio.OnDiskBitmap("/thermalcamera_splash.bmp")
splash.append(displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader))
board.DISPLAY.root_group = splash
```

Finally, the _ulab_&nbsp;(micro lab) arrays needed to hold the normalized 8x8 sensor index and the transformed 15x15 display grid index are defined. A sample color spectrum is placed in the display grid index array. In addition, an array to hold histogram statistical data is established.

An array defined for _ulab_&nbsp;use is an&nbsp;`narray`&nbsp;type; a format different than other CircuitPython arrays. The special `narray` array type (named after a close cousin, the _numpy array_) is designed to support rapid array calculation and transformation.

`PALETTE_SIZE` is used to select the maximum number of display colors across the iron spectrum to map to temperature values. The palette size of `100` colors was selected&nbsp;empirically as a value that balanced the sensor resolution of 0.5°C with the ability to visually discern objects. Increasing the number of colors beyond 160 does not improve readability and can slow the display frame rate. Fewer than `80` palette colors significantly decreases visual object detection.

```python
# Thermal sensor grid axis size; AMG8833 sensor is 8x8
SENSOR_AXIS = 8

# Display grid parameters
GRID_AXIS = (2 * SENSOR_AXIS) - 1  # Number of cells per axis
GRID_SIZE = HEIGHT  # Axis size (pixels) for a square grid
GRID_X_OFFSET = WIDTH - GRID_SIZE  # Right-align grid with display boundary
CELL_SIZE = GRID_SIZE // GRID_AXIS  # Size of a grid cell in pixels
PALETTE_SIZE = 100  # Number of display colors in spectral palette (must be &gt; 0)

# Set up the 2-D sensor data narray
SENSOR_DATA = np.array(range(SENSOR_AXIS**2)).reshape((SENSOR_AXIS, SENSOR_AXIS))
# Set up and load the 2-D display color index narray with a spectrum
GRID_DATA = np.array(range(GRID_AXIS**2)).reshape((GRID_AXIS, GRID_AXIS)) / (
    GRID_AXIS**2
)
# Set up the histogram accumulation narray
# HISTOGRAM = np.zeros(GRID_AXIS)
```

A series of numerical constants are needed to set boundaries and limits for thermal camera calculations. We'll talk about those next.

# Improved AMG8833 PyGamer Thermal Camera

## Constants

After setting up the hardware devices and saying hello, a few commonly used constants and variables are defined. These include the alarm threshold, display minimum/maximum, and camera orientation settings that were previously loaded from the **thermalcamera\_config.py** file. Default values in Celsius are converted to Fahrenheit where needed.

```python
# Convert default alarm and min/max range values from config file
ALARM_C = fahrenheit_to_celsius(ALARM_F)
MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
```

Color values are defined next.&nbsp;The color definitions are used for the various text labels and measured values in the display's sidebar. The `param_colors` list is used by the setup helper that we'll discuss in the next section.

```python
# Default colors for temperature value sidebar
BLACK = 0x000000
RED = 0xFF0000
YELLOW = 0xFFFF00
CYAN = 0x00FFFF
BLUE = 0x0000FF
WHITE = 0xFFFFFF

# Text colors for setup helper's on-screen parameters
SETUP_COLORS = [("ALARM", WHITE), ("RANGE", RED), ("RANGE", CYAN)]
```

# Improved AMG8833 PyGamer Thermal Camera

## Helpers

## Helpers for Display, Buttons, and Setup Functions

Helpers are used to simplify the primary loop code. The helpers:

- Play a tone to signify a button press or alert;
- Display a status message in the center of the image area;
- Display and refresh the sensor image;
- Display and refresh the histogram image;
- Enlarge the 8x8 sensor data into a 15x15 display array;
- Change default parameters for temperature range and alarm threshold;
- Convert joystick movement to simulate up, down, left, and right button presses to support use with either PyGamer or PyBadge boards.

### `play_tone()` Helper

Using the `tone()` helper that's contained in the **simpleio** library, the thermal camera's&nbsp;`play_tone()` helper plays a musical note through the PyGamer's speaker. The frequency in Hertz and duration in seconds are passed to the helper as the parameters&nbsp;`freq` and `duration`.

```python
# ### Helpers ###
def play_tone(freq=440, duration=0.01):
    """Play a tone over the speaker"""
    tone(board.A0, freq, duration)
```

### `flash_status()` Helper

The `flash_status()` helper accepts a text string and displays it in the status area of the display. The text appears as white letters for a time specified by `duration` then as black letters for `duration` length in seconds. This is very useful for flashing a message that can be seen regardless of the background colors, especially handy while displaying a sensor image.

```python
def flash_status(text="", duration=0.05):
    """Flash status message once"""
    status_label.color = WHITE
    status_label.text = text
    time.sleep(duration)
    status_label.color = BLACK
    time.sleep(duration)
    status_label.text = ""
```

### `update_image_frame()` Helper

The `update_image_frame()` helper looks through a list of 225 indexed color values stored by row and column in the `GRID_DATA`&nbsp;array. The helper converts the color index into a displayable RGB color value and updates the fill color of the corresponding display cell.

To save processing time and improve image frame rate, a cell is only updated if the calculated RGB value has changed from one frame to the next.

```python
def update_image_frame(selfie=False):
    """Get camera data and update display"""
    for _row in range(0, GRID_AXIS):
        for _col in range(0, GRID_AXIS):
            if selfie:
                color_index = GRID_DATA[GRID_AXIS - 1 - _row][_col]
            else:
                color_index = GRID_DATA[GRID_AXIS - 1 - _row][GRID_AXIS - 1 - _col]
            color = index_to_rgb(round(color_index * PALETTE_SIZE, 0) / PALETTE_SIZE)
            if color != image_group[((_row * GRID_AXIS) + _col)].fill:
                image_group[((_row * GRID_AXIS) + _col)].fill = color
```

### `update_histo_frame()` Helper

The `update_histo_frame()` helper collects a distribution of 15 temperature sub-ranges within the current temperature display range (one for each color) and displays a histogram of relative temperature values. The helper scans all 225 sensor color index values in the `GRID_DATA`&nbsp;array and counts the number of times a value falls within one of 15 sub-ranges.

When invoked, the helper displays the histogram range legend values and clears the `histogram` array used to accumulate the 15 sub-range values. After collecting the histogram data from the array, the largest sub-range value is stored in the `histo_scale` variable is used to scale the results when the histogram is displayed.

The second part of the helper updates the image area to display the histogram as a series of vertical bars with height proportional to the accumulated sub-range value. The display update starts&nbsp;at the upper left of the display's image area and works down to the lower right. Each cell is filled with a color that corresponds to the color index value. The remainder of boxes in the histogram display area are colored black if not used to build a histogram bar.

```python
def update_histo_frame():
    """Calculate and display histogram"""
    min_histo.text = str(MIN_RANGE_F)  # Display the legend
    max_histo.text = str(MAX_RANGE_F)

    histogram = np.zeros(GRID_AXIS)  # Clear histogram accumulation array
    # Collect camera data and calculate the histogram
    for _row in range(0, GRID_AXIS):
        for _col in range(0, GRID_AXIS):
            histo_index = int(map_range(GRID_DATA[_col, _row], 0, 1, 0, GRID_AXIS - 1))
            histogram[histo_index] = histogram[histo_index] + 1

    histo_scale = np.max(histogram) / (GRID_AXIS - 1)
    if histo_scale &lt;= 0:
        histo_scale = 1

    # Display the histogram
    for _col in range(0, GRID_AXIS):
        for _row in range(0, GRID_AXIS):
            if histogram[_col] / histo_scale &gt; GRID_AXIS - 1 - _row:
                image_group[((_row * GRID_AXIS) + _col)].fill = index_to_rgb(
                    round((_col / GRID_AXIS), 3)
                )
            else:
                image_group[((_row * GRID_AXIS) + _col)].fill = BLACK
```

### `ulab_bilinear_interpolation()` Helper

The `ulab_bilinear_interpolation()`&nbsp;helper utilizes&nbsp;_ulab_&nbsp;array calculations to find values for cells in the 225-cell `GRID_DATA` array that fall between the 64 known sensor element values. First, the even rows are scanned, assigning the average of the adjacent known cells to each unknown cell. Next, odd rows are scanned, assigning the average of the values above and below to every cell in the row. See the section, **1-2-3s of Bilinear Interpolation** for the details of the interpolation method.

```python
def ulab_bilinear_interpolation():
    """2x bilinear interpolation to upscale the sensor data array; by @v923z
    and @David.Glaude."""
    GRID_DATA[1::2, ::2] = SENSOR_DATA[:-1, :]
    GRID_DATA[1::2, ::2] += SENSOR_DATA[1:, :]
    GRID_DATA[1::2, ::2] /= 2
    GRID_DATA[::, 1::2] = GRID_DATA[::, :-1:2]
    GRID_DATA[::, 1::2] += GRID_DATA[::, 2::2]
    GRID_DATA[::, 1::2] /= 2
```

### `setup_mode()` Helper

The `setup_mode()` helper pauses normal operation and collects user input to set alarm threshold and display range min/max values. During the **Setup&nbsp;** mode, the display's average value and label are blanked.

The joystick or PyBadge D-Pad is used to select the parameter to change and to increase or decrease the parameter value. The **HOLD** button acts as the parameter select button. Pressing the **SET** button at any time during the **Setup** mode will exit back to the primary process loop.

The first task is to temporarily display a status message that indicates the camera is in the **Setup** mode. The display's average value and label are blanked and the measured maximum and minimum values are replaced with the current maximum and minimum display range values (`MAX_RANGE_F` and `MIN_RANGE_F`).

After waiting a bit for the status message to be read and prior to watching for button and joystick changes, the index pointer (`param_index`) is reset to point to the alarm threshold parameter.

```python
def setup_mode():
    """Change alarm threshold and minimum/maximum range values"""
    status_label.color = WHITE
    status_label.text = "-SET-"

    ave_label.color = BLACK  # Turn off average label and value display
    ave_value.color = BLACK

    max_value.text = str(MAX_RANGE_F)  # Display maximum range value
    min_value.text = str(MIN_RANGE_F)  # Display minimum range value

    time.sleep(0.8)  # Show SET status text before setting parameters
    status_label.text = ""  # Clear status text

    param_index = 0  # Reset index of parameter to set
```

The following is the meat of the setup process. As long as the&nbsp; **HOLD** (select) or the **SET** (setup mode exit) buttons have not been pressed, the code loops. During the loop, the joystick is watched or alternately, the **UP** and **DOWN** buttons. If the joystick is moved down, the parameter index is incremented, pointing to the next parameter. If moved up, the index will point to the previous parameter. The parameter label text flashes black and white, indicating which parameter is ready to be changed.

In the `image_group` list (that is defined later in the display portion of the code just before the primary process loop), the three parameter text labels for alarm, maximum, and minimum are sequentially positioned in the list:

- Alarm text label &nbsp; &nbsp; &nbsp; --\> `image_group[226]`
- Maximum text label --\> `image_group[227]`
- Minimum text label&nbsp; --\> `image_group[228]` &nbsp; &nbsp;

Using an indexed position in `image_group` for the parameters makes it simpler to sequentially step from one parameter to the next.

```python
setup_state = "SETUP"  # Set initial state
    while setup_state == "SETUP":
        # Select parameter to set
        setup_state = "SELECT_PARAM"  # Parameter selection state
        while setup_state == "SELECT_PARAM":
            param_index = max(0, min(2, param_index))
            status_label.text = SETUP_COLORS[param_index][0]
            image_group[param_index + 226].color = BLACK
            status_label.color = BLACK
            time.sleep(0.25)
            image_group[param_index + 226].color = SETUP_COLORS[param_index][1]
            status_label.color = WHITE
            time.sleep(0.25)

            param_index -= get_joystick()

            _buttons = panel.events.get()
            if _buttons and _buttons.pressed:
                if _buttons.key_number == BUTTON_UP:  # HOLD button pressed
                    param_index = param_index - 1
                if _buttons.key_number == BUTTON_DOWN:  # SET button pressed
                    param_index = param_index + 1
                if _buttons.key_number == BUTTON_HOLD:  # HOLD button pressed
                    play_tone(1319, 0.030)  # Musical note E6
                    setup_state = "ADJUST_VALUE"  # Next state
                if _buttons.key_number == BUTTON_SET:  # SET button pressed
                    play_tone(1319, 0.030)  # Musical note E6
                    setup_state = "EXIT"  # Next state
```

After the **HOLD** button is pressed, the selected parameter represented by the value of `param_index` can be changed.

The selected parameter value is incrementally changed by the joystick's up and down movements or pressed **UP** or **DOWN** buttons. The new value is checked against and limited to the sensor's factory min/max limits (`MIN_SENSOR_F`, `MAX_SENSOR_F`).

In the `image_group` list the three parameter value labels for alarm, maximum, and minimum are sequentially positioned in the list:

- Alarm value label &nbsp; &nbsp; &nbsp; --\> `image_group[230]`
- Maximum value label --\> `image_group[231]`
- Minimum value label&nbsp; --\> `image_group[232]` &nbsp;

The value label for the selected parameter is changed and displayed.

Meanwhile, a flashing status message indicates which type of parameter is being changed, either the alarm or one of the range values.

When the desired value is reached and the **HOLD** (select) button is pressed, the Setup process continues back to the parameter select mode. If **SET** is pressed instead of **HOLD** , the **Setup&nbsp;** process prepares to exit back to the primary process loop.

```python
# Adjust parameter value
        param_value = int(image_group[param_index + 230].text)

        while setup_state == "ADJUST_VALUE":
            param_value = max(32, min(157, param_value))
            image_group[param_index + 230].text = str(param_value)
            image_group[param_index + 230].color = BLACK
            status_label.color = BLACK
            time.sleep(0.05)
            image_group[param_index + 230].color = SETUP_COLORS[param_index][1]
            status_label.color = WHITE
            time.sleep(0.2)

            param_value += get_joystick()

            _buttons = panel.events.get()
            if _buttons and _buttons.pressed:
                if _buttons.key_number == BUTTON_UP:  # HOLD button pressed
                    param_value = param_value + 1
                if _buttons.key_number == BUTTON_DOWN:  # SET button pressed
                    param_value = param_value - 1
                if _buttons.key_number == BUTTON_HOLD:  # HOLD button pressed
                    play_tone(1319, 0.030)  # Musical note E6
                    setup_state = "SETUP"  # Next state
                if _buttons.key_number == BUTTON_SET:  # SET button pressed
                    play_tone(1319, 0.030)  # Musical note E6
                    setup_state = "EXIT"  # Next state
```

Before exiting, a resumption status message is displayed and the display of the average label and value are restored.

Finally, the text strings that may have changed during the **Setup** process are converted to integer numeric values and returned to the primary process loop.

```python
# Exit setup process
    status_label.text = "RESUME"
    time.sleep(0.5)
    status_label.text = ""

    # Display average label and value
    ave_label.color = YELLOW
    ave_value.color = YELLOW
    return int(alarm_value.text), int(max_value.text), int(min_value.text)
```

### `get_joystick()` Helper

The `get_joystick()`&nbsp;reads the joystick if `HAS_JOYSTICK`&nbsp;is `True`. Joystick movements beyond set thresholds are represented as button depressions. For example, a value for `panel.joystick[1]` of less than 20000 means that the joystick was moved upwards; the helper returns the value of +1. A value greater than 44000 indicates downward movement and returns the value of -1.

If the `joystick` argument is `False`, the helper returns the increment value of 0 is returned&nbsp;to the calling module.

```python
def get_joystick():
    """Read the joystick and interpret as up/down buttons (PyGamer)"""
    if HAS_JOYSTICK:
        if joystick_y.value &lt; 20000:
            # Up
            return 1
        if joystick_y.value &gt; 44000:
            # Down
            return -1
    return 0
```

After the helpers are defined, we move on to specifying the text and graphic features of the display.

# Improved AMG8833 PyGamer Thermal Camera

## Display

![](https://cdn-learn.adafruit.com/assets/assets/000/102/813/medium800/temperature___humidity_interpolation_grid_example.003.jpeg?1623270551 Thermal camera display layout.)

## Define Display Group Layers

Within CircuitPython's `displayio` library, a display group is a list of label or graphic attributes that are defined for each object of the display. This section of the Thermal Camera's primary process module defines the `image_group`display group that the camera will use to show measured values, the sensor image or histogram, status message, and the histogram legend.

The camera's display group, `image_group`, consists of layered objects. The first 225 objects of the display make up the colored cells used for the image grid area.

The status message label comes next, followed by the values and labels in the display sidebar area. Finally, objects that make up the histogram legend top off the stack of display objects in `image_group`.

The objects and their attributes are appended to the `image_group` list one-at-a-time when first defined. When appended to `image_group`, the attributes of each object are defined. For example, the alarm label **alm** is defined as a **Label** object with attributes that include the label's font, text contents, and text color:

```python
status_label = Label(font_0, text="", color=None)
status_label.anchor_point = (0.5, 0.5)
status_label.anchored_position = ((WIDTH // 2) + (GRID_X_OFFSET // 2), HEIGHT // 2)
image_group.append(status_label)  # image_group[225]
```

The **anchored\_position** &nbsp;(x/y&nbsp;coordinates) and **anchor\_point** &nbsp;(left/right/center justification) of the alarm label on the PyGamer's display screen are calculated to&nbsp;appear in the center of the image grid area. Other display label and value positions were determined and fine-tuned empirically.&nbsp;After defining the display object's attributes, it is appended to the `image_group` display group.

This process is repeated, starting from the back of the display and progressing towards the front, as each new object is appended to the display group.

### Define the Image Group

After playing a couple of musical tones and storing the elapsed time to mark the beginning of the display definition process, the `image_group` definition list for display group objects comes next. The `scale`&nbsp;argument adjusts image group element position and size parameters. For the PyGamer's display, no adjustment is required so `scale=1`.

The time marker&nbsp;`mkr_t0` along with seven other process time markers will be reported at the end of each displayed frame to calculate thermal camera code performance. This marker establishes the time that the display group definition phase began.

```python
play_tone(440, 0.1)  # Musical note A4
play_tone(880, 0.1)  # Musical note A5

# ### Define the display group ###
mkr_t0 = time.monotonic()  # Time marker: Define Display Elements
image_group = displayio.Group(scale=1)
```

### Define the Thermal Image Display Group Layers

Next, the 225 square cells used to represent sensor array temperatures are defined and appended to `image_group`. Two `for` loops are used to step through each column and row of cells. Each square is defined as a rectangle with width and height equal to `CELL_SIZE`. No color attribute is defined for the cell, making it transparent -- for now.

```python
# Define the foundational thermal image grid cells; image_group[0:224]
#   image_group[#] = image_group[ (row * GRID_AXIS) + column ]
for row in range(0, GRID_AXIS):
    for col in range(0, GRID_AXIS):
        cell_x = (col * CELL_SIZE) + GRID_X_OFFSET
        cell_y = row * CELL_SIZE
        cell = Rect(
            x=cell_x,
            y=cell_y,
            width=CELL_SIZE,
            height=CELL_SIZE,
            fill=None,
            outline=None,
            stroke=0,
        )
        image_group.append(cell)
```

### Define the Text Label Display Group Layers

Finally, the remaining text objects that display legends and values are defined and appended to the `image_group` display group.

For each object, the label name is defined along with the font, text contents, and font color. Next, the object's `anchor_point`&nbsp;(justification) and `anchored_position` (x/y coordinates) attributes are defined.&nbsp;After the attributes are defined, each object is appended to `image_group`.

```python
# Define labels and values
status_label = Label(font_0, text="", color=None)
status_label.anchor_point = (0.5, 0.5)
status_label.anchored_position = ((WIDTH // 2) + (GRID_X_OFFSET // 2), HEIGHT // 2)
image_group.append(status_label)  # image_group[225]

alarm_label = Label(font_0, text="alm", color=WHITE)
alarm_label.anchor_point = (0, 0)
alarm_label.anchored_position = (1, 16)
image_group.append(alarm_label)  # image_group[226]

max_label = Label(font_0, text="max", color=RED)
max_label.anchor_point = (0, 0)
max_label.anchored_position = (1, 46)
image_group.append(max_label)  # image_group[227]

min_label = Label(font_0, text="min", color=CYAN)
min_label.anchor_point = (0, 0)
min_label.anchored_position = (1, 106)
image_group.append(min_label)  # image_group[228]

ave_label = Label(font_0, text="ave", color=YELLOW)
ave_label.anchor_point = (0, 0)
ave_label.anchored_position = (1, 76)
image_group.append(ave_label)  # image_group[229]

alarm_value = Label(font_0, text=str(ALARM_F), color=WHITE)
alarm_value.anchor_point = (0, 0)
alarm_value.anchored_position = (1, 5)
image_group.append(alarm_value)  # image_group[230]

max_value = Label(font_0, text=str(MAX_RANGE_F), color=RED)
max_value.anchor_point = (0, 0)
max_value.anchored_position = (1, 35)
image_group.append(max_value)  # image_group[231]

min_value = Label(font_0, text=str(MIN_RANGE_F), color=CYAN)
min_value.anchor_point = (0, 0)
min_value.anchored_position = (1, 95)
image_group.append(min_value)  # image_group[232]

ave_value = Label(font_0, text="---", color=YELLOW)
ave_value.anchor_point = (0, 0)
ave_value.anchored_position = (1, 65)
image_group.append(ave_value)  # image_group[233]

min_histo = Label(font_0, text="", color=None)
min_histo.anchor_point = (0, 0.5)
min_histo.anchored_position = (GRID_X_OFFSET, 121)
image_group.append(min_histo)  # image_group[234]

max_histo = Label(font_0, text="", color=None)
max_histo.anchor_point = (1, 0.5)
max_histo.anchored_position = (WIDTH - 2, 121)
image_group.append(max_histo)  # image_group[235]

range_histo = Label(font_0, text="-RANGE-", color=None)
range_histo.anchor_point = (0.5, 0.5)
range_histo.anchored_position = ((WIDTH // 2) + (GRID_X_OFFSET // 2), 121)
image_group.append(range_histo)  # image_group[236]
```

Whew. We've imported libraries, listed the essential constants, established some helpers, and defined the elements of the display. After a quick aside to talk about how a display group can be accessed, it'll be time to bring it all together in the thermal camera's primary process.

### Fun Facts about Display Group Objects

Objects and their attributes in the display group can be accessed in two ways. The most commonly-used method is to assign a name attribute to the object. For example, the text of the status message label can be set to display the text _WELCOME_ in this manner:

`status_label.text = "WELCOME"`

Objects in `image_group` can also be accessed by their indexed position in the display group. An index of 0 is the back-most object in the display group; the highest index value is front-most. The status message text can also be changed using the index:

`image_group[225].text = "WELCOME"`

The Thermal Camera uses both techniques. Named display objects are used whenever possible to clearly identify which object is being changed. For efficiency, however, the index position method is used when stepping through a sequence of `image_group` objects, as when displaying the 225 colored cells for the sensor image. The index position method is also used by the `setup_mode()` helper when moving on-screen to select the alarm, maximum, or minimum parameter.

# Improved AMG8833 PyGamer Thermal Camera

## Primary Process

We've finally arrived at the portion of the code that controls the Thermal Camera's primary process. Before getting started in the main process, we need to define a couple of things to get the camera ready for looping.

After taking a performance time stamp at the beginning with the time marker variable `mkr_t1`, the default display mode flags and the initial ranges values are established.&nbsp;Next, the `image_group` display group is activated, a sample of the iron spectrum colors is&nbsp;displayed for 0.75 seconds,&nbsp;and a "ready" tone is sounded.

```python
# ###--- PRIMARY PROCESS SETUP ---###
mkr_t1 = time.monotonic()  # Time marker: Primary Process Setup
# pylint: disable=no-member
mem_fm1 = gc.mem_free()  # Monitor free memory
DISPLAY_IMAGE = True  # Image display mode; False for histogram
DISPLAY_HOLD = False  # Active display mode; True to hold display
DISPLAY_FOCUS = False  # Standard display range; True to focus display range

# pylint: disable=invalid-name
orig_max_range_f = 0  # Establish temporary range variables
orig_min_range_f = 0

# Activate display, show preloaded sample spectrum, and play welcome tone
display.root_group = image_group
update_image_frame()
flash_status("IRON", 0.75)
play_tone(880, 0.010)  # Musical note A5
```

## Primary Process Loop, Part I

Because of its complexity, the primary process loop is divided into two sections to make it easier to understand. The first section fetches the image sensor's data, analyzes and displays the sensor data as an image or histogram, and checks to see if any of the sensor elements have exceeded the alarm threshold. The second section looks at the buttons and joystick to select display modes and to run the Setup helper.

### Retrieve Sensor Data, Display Image or Histogram, Check Alarm Threshold

At time `mkr_t2`, the image sensor's 64 data elements are moved into the `sensor`&nbsp;list when the `DISPLAY_HOLD`&nbsp;flag is false; otherwise a "-HOLD-" status message is displayed. To allow the sensor's temperature data to be used by the ultra fast _ulab_ interpolation helper, the sensor list is copied into a&nbsp;_ulab_-compatible array, `SENSOR_DATA`. The data is also constrained ("clipped" in ulab nomenclature) to the valid temperature range of the AMG8833 sensor, 0°C to 80°C.

```python
# ###--- PRIMARY PROCESS LOOP ---###
while True:
    mkr_t2 = time.monotonic()  # Time marker: Acquire Sensor Data
    if DISPLAY_HOLD:
        flash_status("-HOLD-", 0.25)
    else:
        sensor = amg8833.pixels  # Get sensor_data data
    # Put sensor data in array; limit to the range of 0, 80
    SENSOR_DATA = np.clip(np.array(sensor), 0, 80)
```

Before the temperature data in the `SENSOR_DATA`&nbsp;array is altered for the interpolation process, the minimum, maximum, and average values of the array are captured in the variables `v_min`, `v_max`, and `v_ave`. The display is then updated with the Fahrenheit values of the current alarm setting as well as the converted minimum, maximum, and average Fahrenheit values. This section starts at time marker `mkr_t4`.

```python
# Update and display alarm setting and max, min, and ave stats
    mkr_t4 = time.monotonic()  # Time marker: Display Statistics
    v_max = np.max(SENSOR_DATA)
    v_min = np.min(SENSOR_DATA)
    v_ave = np.mean(SENSOR_DATA)

    alarm_value.text = str(ALARM_F)
    max_value.text = str(celsius_to_fahrenheit(v_max))
    min_value.text = str(celsius_to_fahrenheit(v_min))
    ave_value.text = str(celsius_to_fahrenheit(v_ave))
```

It's time to use bilinear interpolation to enlarge the sensor's 64 elements (8x8) into a grid of 225 elements (15x 15). The interpolation process commences at time marker `mkr_t5` and begins by converting each of the 64 temperature values in the `SENSOR_DATA`&nbsp;array to a normalized value that ranges from 0.0 to 1.0 depending on the recorded temperature value as compared to the currently displayed temperature range. For example, a normalized value of 0.0 represents a temperature at the minimum of the currently displayed range, `MIN_RANGE_C;` a normalized value of 1.0 represents the maximum of the range, `MAX_RANGE_C`. Normalizing the temperature values makes it easier to display a full range of pseudocolors for any temperature range that the FOCUS mode may invoke.

After normalization, the known values from the `SENSOR_DATA`&nbsp;array are copied into the `GRID_DATA`&nbsp;array, starting at [0, 0], the upper left corner, and placed into the cells of the even columns. The odd rows in the `GRID_DATA`&nbsp;array are initially left blank. Once the `GRID_DATA` array is filled, the missing values are replaced with the results of the interpolation helper, `ulab_bilinear_interpolation()`. Refer to the **1-2-3s of Bilinear Interpolation** section for details of the image enlargement process.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/687/medium800/temperature___humidity_interpolation_grid_example.001.jpeg?1623087502 The sensor_data array elements are copied into the grid_data array to prepare for interpolation.)

```python
# Normalize temperature to index values and interpolate
    mkr_t5 = time.monotonic()  # Time marker: Normalize and Interpolate
    SENSOR_DATA = (SENSOR_DATA - MIN_RANGE_C) / (MAX_RANGE_C - MIN_RANGE_C)
    GRID_DATA[::2, ::2] = SENSOR_DATA  # Copy sensor data to the grid array
    ulab_bilinear_interpolation()  # Interpolate to produce 15x15 result
```

This section checks the `DISPLAY_IMAGE`&nbsp;flag to see whether to display a sensor image or histogram. If `DISPLAY_IMAGE`&nbsp;is `True`, the `update_image_frame()` helper is used to display the data contained in the `GRID_DATA`&nbsp;array as&nbsp;a&nbsp;thermal image. When `False`, `update_histo_frame()` displays the data as a histogram distribution.

```python
# Display image or histogram
    mkr_t6 = time.monotonic()  # Time marker: Display Image
    if DISPLAY_IMAGE:
        update_image_frame(selfie=SELFIE)
    else:
        update_histo_frame()
```

The next step in the primary process loop checks the returned maximum value against the current alarm threshold (`ALARM_C`). If the threshold is met or exceeded, the NeoPixels flash red and a warning tone is played through the speaker.

```python
# If alarm threshold is reached, flash NeoPixels and play alarm tone
    if v_max &gt;= ALARM_C:
        pixels.fill(RED)
        play_tone(880, 0.015)  # Musical note A5
        pixels.fill(BLACK)
```

## Primary Process Loop, Part II

The second portion of the primary process loop checks to see if any buttons have been pressed and sets the appropriate flags to select camera functions. This section also watches the **SET** button to activate the `setup_mode()` helper to permit changing camera parameters. Finally, all the time markers are analyzed and a code performance report is printed.

### Watch the Buttons and Change Parameters
First the **HOLD** button (the PyGamer's **BUTTON\_A** ) is checked. If pressed, an&nbsp;acknowledgment tone is sounded and the boolean&nbsp;`DISPLAY_HOLD`&nbsp;parameter is toggled to the opposite state.

```python
# See if a panel button is pressed
    buttons = panel.events.get()
    if buttons and buttons.pressed:
        if buttons.key_number == BUTTON_HOLD:
            # Toggle display hold (shutter)
            play_tone(1319, 0.030)  # Musical note E6
            DISPLAY_HOLD = not DISPLAY_HOLD
```

If the **IMAGE** &nbsp;button ( **BUTTON\_B** ) is pressed, a tone is played and the boolean&nbsp;`DISPLAY_IMAGE`&nbsp;value is toggled to the opposite state. After waiting until the button is released, the variable `DISPLAY_IMAGE`&nbsp;is checked. If `True` (display image), the histogram legend colors are disabled. If `False`&nbsp;(display histogram), the histogram legend colors are enabled.

```python
if buttons.key_number == BUTTON_IMAGE:
    # Toggle image/histogram mode (display image)
    play_tone(659, 0.030)  # Musical note E5
    DISPLAY_IMAGE = not DISPLAY_IMAGE

    if DISPLAY_IMAGE:
        min_histo.color = None
        max_histo.color = None
        range_histo.color = None
    else:
        min_histo.color = CYAN
        max_histo.color = RED
        range_histo.color = BLUE
```

When the **FOCUS** button (PyGamer **BUTTON\_SELECT** ) is pressed, a tone is played and the boolean `DISPLAY_FOCUS`&nbsp;value is toggled to the opposite state.

If `DISPLAY_FOCUS`&nbsp;is `True`, the default display range values `MIN_RANGE_F` and&nbsp;`MAX_RANGE_F`&nbsp;are stored in temporary variables `orig_min_range_f` and `orig_max_range_f`. The display range is then updated with the current minimum and maximum values `v_min` and `v_max`. This change causes the color spectrum of the display to conform to the new range. The status "FOCUS" is then flashed on the display.

If `DISPLAY_FOCUS`&nbsp;is `False`, the previously stored range variables become the current display range. The display range reverts to the original default values and the colors match the original range. The display flashes the "ORIG" status message.

```python
if buttons.key_number == BUTTON_FOCUS:  # Toggle display focus mode
    play_tone(698, 0.030)  # Musical note F5
    DISPLAY_FOCUS = not DISPLAY_FOCUS
    if DISPLAY_FOCUS:
        # Set range values to image min/max for focused image display
        orig_min_range_f = MIN_RANGE_F
        orig_max_range_f = MAX_RANGE_F
        MIN_RANGE_F = celsius_to_fahrenheit(v_min)
        MAX_RANGE_F = celsius_to_fahrenheit(v_max)
        # Update range min and max values in Celsius
        MIN_RANGE_C = v_min
        MAX_RANGE_C = v_max
        flash_status("FOCUS", 0.2)
    else:
        # Restore previous (original) range values for image display
        MIN_RANGE_F = orig_min_range_f
        MAX_RANGE_F = orig_max_range_f
        # Update range min and max values in Celsius
        MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
        MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
        flash_status("ORIG", 0.2)
```

When the **SET** button ( **BUTTON\_START** ) is pressed, a tone is played. The `setup_mode()` helper is then executed returning new values for `ALARM_F`, `MAX_RANGE_F`, and `MIN_RANGE_F` that are promptly converted to Celsius.&nbsp;

```python
if buttons.key_number == BUTTON_SET:
    # Activate setup mode
    play_tone(784, 0.030)  # Musical note G5

    # Invoke startup helper; update alarm and range values
    ALARM_F, MAX_RANGE_F, MIN_RANGE_F = setup_mode()
    ALARM_C = fahrenheit_to_celsius(ALARM_F)
    MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
    MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
```

Before looping to the start of the primary process loop to display the next image, the time marker `mkr_t7` is used to record the time at the end of the code loop. The code performance time markers are analyzed and performance results printed to the REPL's serial output.

```python
mkr_t7 = time.monotonic()  # Time marker: End of Primary Process
    gc.collect()
    mem_fm7 = gc.mem_free()

    # Print frame performance report
    print("*** PyBadge/Gamer Performance Stats ***")
    print(f"  define display: {(mkr_t1 - mkr_t0):6.3f} sec")
    print(f"  free memory:    {mem_fm1 / 1000:6.3f} Kb")
    print("")
    print("                          rate")
    print(f" 1) acquire: {(mkr_t4 - mkr_t2):6.3f} sec  ", end="")
    print(f"{(1 / (mkr_t4 - mkr_t2)):5.1f}  /sec")
    print(f" 2) stats:   {(mkr_t5 - mkr_t4):6.3f} sec")
    print(f" 3) convert: {(mkr_t6 - mkr_t5):6.3f} sec")
    print(f" 4) display: {(mkr_t7 - mkr_t6):6.3f} sec")
    print("             =======")
    print(f"total frame: {(mkr_t7 - mkr_t2):6.3f} sec  ", end="")
    print(f"{(1 / (mkr_t7 - mkr_t2)):5.1f}   /sec")
    print(f"           free memory:   {mem_fm7 / 1000:6.3f} Kb")
    print("")
```

Each phase of code execution is calculated from the time markers:

- _ **define display** :_ the time in seconds to define the _displayio_ elements before the primary loop begins and the amount of available free memory.
- **_1) acquire_**: elapsed time and the calculated ideal rate for acquiring and conditioning sensor data.
- **_2) stats_**: update the on-screen alarm, min, max, and average values.
- **_3) convert_**: normalize the 8 x 8 sensor data and enlarge the image to 15 x 15.
- **_4) display_**: using displayio, refresh the screen image.
- **_total frame_** : the elapsed time to generate a frame (steps 1 through 4); also includes the frame-per-second rate of the just-displayed frame along with available free memory.

The PyGamer displays approximately 5 image frames per second depending on the quantity of changed display grid elements from one frame to the next. Here's a screen shot of a typical performance report:

![](https://cdn-learn.adafruit.com/assets/assets/000/116/444/medium800/temperature___humidity_performance_frame_rate.png?1667535545)

Does printing the performance report to the REPL slow performance? Yes, but not significantly. Capturing the time markers and printing the performance report adds less than 0.005 seconds to each frame. That's a frame rate performance impact of approximately 2.9%. I think we can live with that.

Refer to the **Performance Monitoring** section for a further discussion of the method used and comparison of the Thermal Camera code performance on a variety of Adafruit development boards.

# Improved AMG8833 PyGamer Thermal Camera

## Other Modules

## Startup Configuration

When imported, the **thermalcamera\_config.py** file provides the Thermal Camera's initial power-up alarm threshold as well as minimum and maximum display range values. The power-up configuration parameters can be changed by editing the file with your favorite text editor.

Values are in degrees Fahrenheit.

```python
# ### Alarm and range default values in Farenheit ###
ALARM_F = 120
MIN_RANGE_F = 60
MAX_RANGE_F = 120

# ### Display characteristics
SELFIE = False  # Rear camera view; True for front view
```

## Converter Helpers

The **thermalcamera\_converters.py** module consists of two temperature converters, one for Celsius to Fahrenheit and the other for Fahrenheit to Celsius. The value to be converted is passed as an argument to the appropriate helper.&nbsp; Because the Thermal Camera's sensor has limited accuracy, a rounded integer value is returned.

```python
def celsius_to_fahrenheit(deg_c=None):
    """Convert C to F; round to 1 degree C"""
    return round(((9 / 5) * deg_c) + 32)


def fahrenheit_to_celsius(deg_f=None):
    """Convert F to C; round to 1 degree F"""
    return round((deg_f - 32) * (5 / 9))
```

## Pseudocolor Spectrum Converter
![](https://cdn-learn.adafruit.com/assets/assets/000/102/692/medium800/temperature___humidity_iron_spectrum_ref_short.jpeg?1623090616)

Showing a visual image of temperatures requires the use of a spectrum of gradual color changes that correspond to the range of temperatures to be displayed. Since the colors are representative of the measured temperature, the collection of colors is called a pseudocolor spectrum. The pseudocolor spectrum of heated iron was used for this thermographic imaging project.

The color of a heated iron bar as a temperature scale originated with blacksmiths to determine when the metal can be shaped, joined, or hardened. Cold iron starts as a blueish color that changes to purple, red, orange, yellow, and eventually glows white-hot as the temperature increases.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/723/medium800/temperature___humidity_interpolation_grid_example.019.jpeg?1623183486 The RGB values used to create the iron pseudocolor spectrum.)

For this project, a helper was created that converts a temperature index value of 0.0 to 1.0 to the RGB values needed to create the iron pseudocolor spectrum on the PyGamer's color TFT display.

Within the **iron.py** file are two helpers, the primary`index_to_rgb()` code that converts the index to the corresponding RGB value and a `map_range()` helper used to calculate values within the `index_to_rgb()` helper.

## map\_range()

The `map_range()` helper accepts an input value inside of a specified input range and returns a proportional value constrained by a specified output range. The input value `x` is contained in the range `in_min` to `in_max`. The returned output value is constrained to the range `out_min` and `out_max`.

```python
def map_range(x, in_min, in_max, out_min, out_max):
    """
    Maps and constrains an input value from one range of values to another.
    (from adafruit_simpleio)

    :param float x: The value to be mapped. No default.
    :param float in_min: The beginning of the input range. No default.
    :param float in_max: The end of the input range. No default.
    :param float out_min: The beginning of the output range. No default.
    :param float out_max: The end of the output range. No default.

    :return: Returns value mapped to new range
    :rtype: float
    """
    in_range = in_max - in_min
    in_delta = x - in_min
    if in_range != 0:
        mapped = in_delta / in_range
    elif in_delta != 0:
        mapped = in_delta
    else:
        mapped = 0.5
    mapped *= out_max - out_min
    mapped += out_min
    if out_min &lt;= out_max:
        return max(min(mapped, out_max), out_min)
    return min(max(mapped, out_max), out_min)
```

## index\_to\_rgb()

The `index_to_rgb` helper accepts an `index` input value from 0.0 to 1.0, returning a 24-bit RGB color value. Within this helper, the input value is converted to an internal spectrum, represented by the `band` variable. The spectrum band value ranges from 0 to 600, arbitrarily selected to provide a simple way to understand the gradual color shifting within the spectrum.

Within each sub-band, the red, green, and blue components are established and calculated. In the red to orange sub-band (300 to 399) for example, the red value is held at 1.0, blue at 0.0, where green changes proportionally from 0.0 to 0.5 as the band value increases.

```python
if 300 &lt;= band &lt; 400:  # red to orange
    red = 1.0**gamma
    grn = map_range(band, 300, 400, 0.0, 0.5) ** gamma
    blu = 0.0
```

The `gamma` parameter is applied to improve the visual perception of the color spectrum, improving the continuity or smoothness of the color spectrum, helping to compensate for the differences between human visual color perception and the source display's rendition of color. For example, a gamma value of 0.5 works nicely for the PyGamer's TFT display. A gamma value of 1.0 seems to work the best with the MatrixPortal's 32 x 64 RGB LED display.

Finally, the resulting 0.0 to 1.0 values for red, green, and blue determined within a sub-band are used to calculate the returned 24-bit RGB value.

```python
def index_to_rgb(index=0, gamma=0.5):
    """
    Converts a temperature index to an iron thermographic pseudocolor spectrum
    RGB value. Temperature index in range of 0.0 to 1.0. Gamma in range of
    0.0 to 1.0 (1.0=linear), default 0.5 for color TFT displays.

    :param float index: The normalized index value, range 0 to 1.0. Defaults to 0.
    :param float gamma: The gamma color perception value. Defaults to 0.5.

    :return: Returns a 24-bit RGB value
    :rtype: integer
    """

    band = index * 600  # an arbitrary spectrum band index; 0 to 600

    if band &lt; 70:  # dark gray to blue
        red = 0.1
        grn = 0.1
        blu = (0.2 + (0.8 * map_range(band, 0, 70, 0.0, 1.0))) ** gamma
    if 70 &lt;= band &lt; 200:  # blue to violet
        red = map_range(band, 70, 200, 0.0, 0.6) ** gamma
        grn = 0.0
        blu = 1.0**gamma
    if 200 &lt;= band &lt; 300:  # violet to red
        red = map_range(band, 200, 300, 0.6, 1.0) ** gamma
        grn = 0.0
        blu = map_range(band, 200, 300, 1.0, 0.0) ** gamma
    if 300 &lt;= band &lt; 400:  # red to orange
        red = 1.0**gamma
        grn = map_range(band, 300, 400, 0.0, 0.5) ** gamma
        blu = 0.0
    if 400 &lt;= band &lt; 500:  # orange to yellow
        red = 1.0**gamma
        grn = map_range(band, 400, 500, 0.5, 1.0) ** gamma
        blu = 0.0
    if band &gt;= 500:  # yellow to white
        red = 1.0**gamma
        grn = 1.0**gamma
        blu = map_range(band, 500, 580, 0.0, 1.0) ** gamma

    return (int(red * 255) &lt;&lt; 16) + (int(grn * 255) &lt;&lt; 8) + int(blu * 255)
```

# Improved AMG8833 PyGamer Thermal Camera

## 1-2-3s of Bilinear Interpolation

With just 64 display elements, the thermal camera can only display blocky object shapes. It's surprising how visual recognition improves when a bilinear interpolation technique is applied to the thermal sensor data to increase the resolution of the image.

Interpolation is a technique for enhancing limited data sets by estimating "in-between" values. It's often used for enlarging images to make patterns and objects easier to discern. Two varieties of interpolation are commonly used for images, bilinear and bicubic. The bilinear method, based on a linear equation like _y = mx + b_, is the simplest and the least computationally intensive. By comparison, bicubic interpolation is computationally more complicated, usually involving a polynomial function of the second degree or higher such as the quadratic form&nbsp;_y = ax<sup>2 </sup>+ bx + c_. The bicubic method can produce enlarged images with smoother and clearer object edges than the bilinear method -- but at the price of increased computational power and elapsed processing time.

Given the computational power of the PyGamer's SAMD-51 processor, the simpler bilinear approach was chosen to enlarge the thermal camera's image. The AMG8833 sensor's image is enlarged from 8 x 8 (64 elements) to a display grid of 15 x 15 (225 cells). Let's talk about how that is done. For the sake of simplicity, the following conceptual example of the method is limited to a 4 x 4 sensor array (16 elements) &nbsp;and 7 x 7 display grid (49 cells).

![](https://cdn-learn.adafruit.com/assets/assets/000/102/695/medium800/temperature___humidity_interpolation_grid_example.002.jpeg?1623100135)

The first step in the bilinear interpolation process is to copy the contents of the sensor value array into an image grid array of (2n - 1) rows and (2n - 1) columns. In this example n = 4, so the display grid array will have 7 rows and columns.

The first row of the display grid array contains the contents of the first row of the sensor value array with a blank element between each known sensor value. A row is skipped and the next row of sensor data is copied into the display grid array. After the sensor data is placed in the display grid array, the interpolation will replace the unknown cells with a calculated value from the closest known cells using a two-pass process.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/696/medium800/temperature___humidity_interpolation_grid_example.003.jpeg?1623100153)

The first pass starts with the first unknown cell and calculates its value from the preceding and following known cells in that row. For example, the cell between columns 0 and 1 of row 0 is calculated using an average of the two adjacent cells. The unknown cell is updated with the value of 3. The process continues to calculate the remaining unknown cells in the evenly numbered rows. The missing values in the odd numbered rows will be calculated next.

The second pass starts by processing the unknown cells of row 1, calculating the value from the average of the cells directly above and below. For example, the first cell of row 1 is updated with the value 3. The second pass continues to update the remaining unknown cells in the display grid array.

Info: 

![](https://cdn-learn.adafruit.com/assets/assets/000/102/697/medium800/temperature___humidity_interpolation_grid_example.004.jpeg?1623100176)

Here's the finished product, colored to roughly represent each cell's value. Compared to the original 4 x 4 sensor image, the newly interpolated 7 x 7 image has added detail with color gradients that can help to identify the object in the field of view.

# Improved AMG8833 PyGamer Thermal Camera

## Performance Monitoring

![](https://cdn-learn.adafruit.com/assets/assets/000/102/732/medium800/temperature___humidity_interpolation_grid_example.020.jpeg?1623211769 Thermal Camera code structure.)

Because some significant timing and memory challenges were anticipated from the start, the structure of the improved thermal camera CircuitPython code was instrumented to measure its performance. Five&nbsp;functional areas were identified that would provide obvious hints as to where architectural or speed issues may live. Not only did the performance monitoring help solve some tricky timing and memory allocation issues, the resulting structure of the code allowed it to be easily adapted for testing on other development boards. The five code performance areas were:

#### Define Display Elements

The one-time display definitions for rectangles, labels, and on-screen status. This process uses large blocks of memory to build the **displayio** group of&nbsp;display element attributes.

#### Acquire Sensor Data

The first portion of the repeating primary loop that acquires and conditions the thermal sensor data. The acquisition process uses I2C input/output resources, the AMG88xx sensor library, and creates two large arrays in memory to hold and process the data. Floating point calculations constrain the sensor data to a valid temperature range.

#### Display Statistics

Updates the on-screen alarm, min, max, and average values. This process manipulates display element attributes in memory, requires floating point calculations to support **displayio** , utilizes SPI input/output resources, and uses _ulab_ to quickly determine min, max, and average.&nbsp;

#### Normalize and Interpolate

Normalizes the 8 x 8 sensor data, copies it to the display grid array, and interpolates the values within the 15 x 15 display grid array. _ulab_ is used for all calculations.

#### Display Image

Scans elements in the display grid array, calculates the iron spectrum color, and uses **displayio** to update an on-screen rectangle if the color has changed from the previous frame. After updating the image, this code segment checks the operational controls and modifies the display mode as selected. This segment heavily uses floating point, memory, and SPI input/output for the **displayio** functionality.

An elapsed time marker is stored at the beginning of each code segment. At the end of the primary process loop, the markers are analyzed and a report is printed to the serial output to be viewed via the REPL. Here's a screen shot of the performance report:

![](https://cdn-learn.adafruit.com/assets/assets/000/116/442/medium800/temperature___humidity_performance_frame_rate.png?1667526513)

After improvements, the code was ported to run on 9 other development boards in the workshop inventory ranging from SAMD-51 (M4) boards to ESP32-S2, nRF52840, and the RP2040. All of the PyGamer code was left intact except where specific display or button interface requirements were needed. For example, since the PyPortal has no hardware buttons, its touch screen was used to implement button-like controls.&nbsp;Similarly, the Setup helper code was removed if memory capacity issues were identified for a particular development board.

![](https://cdn-learn.adafruit.com/assets/assets/000/102/703/medium800/temperature___humidity_TC_Compare_2021-05-28.png?1623123984 Thermal camera code performance comparison.)

The PyGamer platform performed the best in this comparison. Generally, development boards that use the M4 (SAMD-51) processor performed well, beating the 2 frames-per-second performance threshold.&nbsp;

Since the thermal camera code uses a unique combination of resources suited for displaying temperature images, the comparison of thermal camera performance on the different platforms should not be construed as revealing intractable flaws of a particular development board or processor architecture. Instead, the comparison helps to point out performance bottlenecks unique to the thermal camera application that could benefit from further code refinement.&nbsp;

Many factors from processor architecture to the board's TFT display bus could impact thermal camera performance. For example, the current version of the thermal camera depends heavily on floating point calculations for almost everything, from normalizing and constraining sensor data to the internal calculations of CircuitPython **displayio** functions when it positions objects and justifies on-screen text. Development boards that use the SAMD-51 (M4) have an integral floating point processor in hardware that makes calculations a breeze -- so much so that little attention is given to tuning the code to calculate with integers when floating point math really isn't needed. Development boards such as those with the RP2040 processor do not have integral hardware floating point. Can you see where this is going?

So this comparison wasn't a completely fair test. The thermal camera's code was written to work best with the M4 architecture, not to take advantage of the RP2040's faster clock speed and huge memory capacity (and low cost!). What would it take to modify the code to work better with the RP2040? Are CircuitPython **displayio** and AMG8833 libraries tuned to take advantage of the RP2040's talents? We're going to have to add that project to the list.


## Featured Products

### Adafruit AMG8833 IR Thermal Camera FeatherWing

[Adafruit AMG8833 IR Thermal Camera FeatherWing](https://www.adafruit.com/product/3622)
A Feather board without ambition is a Feather board without FeatherWings! This is the **Thermal Camera FeatherWing** : thanks to the Panasonic AMG8833 8x8 GridEYE sensor, it adds heat-vision to _any_ Feather main board. Using our <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/3622)
[Related Guides to the Product](https://learn.adafruit.com/products/3622/guides)
### Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino

[Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino](https://www.adafruit.com/product/4242)
What&nbsp;fits in your pocket, is fully Open Source, and can run CircuitPython, MakeCode Arcade or Arduino games you write yourself? That's right, it's the **Adafruit PyGamer!** We wanted to make an entry-level gaming handheld for DIY gaming, and maybe a little...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4242)
[Related Guides to the Product](https://learn.adafruit.com/products/4242/guides)
### Lithium Ion Polymer Battery with Short Cable - 3.7V 350mAh

[Lithium Ion Polymer Battery with Short Cable - 3.7V 350mAh](https://www.adafruit.com/product/4237)
Lithium-ion polymer (also known as 'lipo' or 'lipoly') batteries are thin, light, and powerful. The output ranges from 4.2V when completely charged to 3.7V. This battery has a capacity of 350mAh for a total of about 1.3 Wh. If you need a larger (or smaller!) battery, <a...></a...>

In Stock
[Buy Now](https://www.adafruit.com/product/4237)
[Related Guides to the Product](https://learn.adafruit.com/products/4237/guides)
### Mini Oval Speaker with Short Wires - 8 Ohm 1 Watt

[Mini Oval Speaker with Short Wires - 8 Ohm 1 Watt](https://www.adafruit.com/product/4227)
Hear the good news! This wee speaker&nbsp;is&nbsp;a&nbsp;great addition to any audio project where you need 8 ohm impedance and 1W or less of power. We particularly like this&nbsp;speaker&nbsp;as it is&nbsp;small and comes with nice skinny wires with a connector on the end. It has a handy...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4227)
[Related Guides to the Product](https://learn.adafruit.com/products/4227/guides)
### Plastic Button Caps For Square Top (10-pack) - 8mm Diameter

[Plastic Button Caps For Square Top (10-pack) - 8mm Diameter](https://www.adafruit.com/product/4228)
These Reese's Piece's lookin' bits fit perfectly on top of tactile buttons with 2.4mm square tops and give a satisfying 8mm diameter surface area for your fingers to press.  
  
You get 10 candy-colored round caps. You get two of each color: **red, yellow, white,...**

In Stock
[Buy Now](https://www.adafruit.com/product/4228)
[Related Guides to the Product](https://learn.adafruit.com/products/4228/guides)
### Adafruit PyGamer Acrylic Enclosure Kit

[Adafruit PyGamer Acrylic Enclosure Kit](https://www.adafruit.com/product/4238)
You've got your PyGamer, and you're ready to start jammin' on your favorite arcade games. You gaze adoringly at the charming silkscreen designed by Ada-friend PaintYourDragon.&nbsp;The nostalgia is palpable!

Cradling the PCB in your hands, you realize there's something...

In Stock
[Buy Now](https://www.adafruit.com/product/4238)
[Related Guides to the Product](https://learn.adafruit.com/products/4238/guides)
### Adafruit PyGamer Starter Kit

[Adafruit PyGamer Starter Kit](https://www.adafruit.com/product/4277)
**Please note: you may get a royal blue _or_ purple case with your starter kit (they're both lovely colors)**

What&nbsp;fits in your pocket, is fully Open Source, and can run CircuitPython, MakeCode Arcade or Arduino games you write yourself? That's right,...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4277)
[Related Guides to the Product](https://learn.adafruit.com/products/4277/guides)

## Related Guides

- [Introducing Adafruit PyGamer](https://learn.adafruit.com/adafruit-pygamer.md)
- [PixelDust Digital Sand Demos for Arcada](https://learn.adafruit.com/pixeldust-digital-sand-demos-for-arcada.md)
- [Trash Panda 2: Garbage Day](https://learn.adafruit.com/trash-panda-2-dumpster-dive.md)
- [How to Hack NES ROMs to Add Your Own Sprites](https://learn.adafruit.com/how-to-hack-roms-to-add-your-own-sprites.md)
- [AdaBox 012](https://learn.adafruit.com/adabox012.md)
- [PyGamer Thermal Camera with AMG8833](https://learn.adafruit.com/pygamer-thermal-camera-amg8833.md)
- [Bounce - an accelerometer game in Arduino for PyGamer and PyBadge](https://learn.adafruit.com/bounce-an-accelerometer-game-in-arduino-for-pygamer-and-pybadge.md)
- [Re-MakeCode the Classics: Py Hunter](https://learn.adafruit.com/re-makecode-the-classics-spy-hunter.md)
- [Arcada Animated GIF Display](https://learn.adafruit.com/pyportal-animated-gif-display.md)
- [CircuitPython Turtle Graphics](https://learn.adafruit.com/circuitpython-turtle-graphics.md)
- [MakeCode Arcade: Sparky Invaders](https://learn.adafruit.com/makecode-arcade-sparky-invaders.md)
- [How to train new TensorFlow Lite micro speech models](https://learn.adafruit.com/how-to-train-new-tensorflow-lite-micro-speech-models.md)
- [PyBadger Event Badge](https://learn.adafruit.com/pybadger-event-badge.md)
- [Playing Gamebuino META Games on Arcada](https://learn.adafruit.com/playing-gamebuino-meta-games-on-arcada.md)
- [Next Level MakeCode Arcade Games](https://learn.adafruit.com/next-level-makecode-arcade-games.md)
- [MLX90640 Thermal Camera with Image Recording](https://learn.adafruit.com/mlx90640-thermal-image-recording.md)
