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. Next, the image_group display group is activated, a sample of the iron spectrum colors is displayed for 0.75 seconds, and a "ready" tone is sounded.

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
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 list when the DISPLAY_HOLD 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 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.

# ###--- PRIMARY PROCESS LOOP ---###
while True:
    mkr_t2 = time.monotonic()  # Time marker: Acquire Sensor Data
        flash_status("-HOLD-", 0.25)
        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 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.

# 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 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 array are copied into the GRID_DATA 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 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.

The sensor_data array elements are copied into the grid_data array to prepare for interpolation.
# Normalize temperature to index values and interpolate
    mkr_t5 = time.monotonic()  # Time marker: Normalize and Interpolate
    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 flag to see whether to display a sensor image or histogram. If DISPLAY_IMAGE is True, the update_image_frame() helper is used to display the data contained in the GRID_DATA array as a thermal image. When False, update_histo_frame() displays the data as a histogram distribution.

# Display image or histogram
    mkr_t6 = time.monotonic()  # Time marker: Display Image

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.

# If alarm threshold is reached, flash NeoPixels and play alarm tone
    if v_max >= ALARM_C:
        play_tone(880, 0.015)  # Musical note A5

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 acknowledgment tone is sounded and the boolean DISPLAY_HOLD parameter is toggled to the opposite state.

# 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

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

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

        min_histo.color = None
        max_histo.color = None
        range_histo.color = None
        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 value is toggled to the opposite state.

If DISPLAY_FOCUS is True, the default display range values MIN_RANGE_F and MAX_RANGE_F 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 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.

if buttons.key_number == BUTTON_FOCUS:  # Toggle display focus mode
    play_tone(698, 0.030)  # Musical note F5
        # 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)
        # 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. 

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.

mkr_t7 = time.monotonic()  # Time marker: End of Primary Process
    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("                          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")

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:

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.

This guide was first published on Jun 09, 2021. It was last updated on Apr 22, 2024.

This page (Primary Process) was last updated on Mar 23, 2024.

Text editor powered by tinymce.