Adafruit_Learning_System_Gu.../PyGamer_Improved_Thermal_Camera/code.py
Cedar Grove Maker Studios a8c96933e3 update docs
2022-11-04 10:00:27 -07:00

502 lines
18 KiB
Python
Executable file

# SPDX-FileCopyrightText: 2022 Jan Goolsbey for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
`thermalcamera`
================================================================================
PyGamer/PyBadge Thermal Camera Project
"""
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
# 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)
# 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
# Initiate the AMG8833 Thermal Camera
i2c = busio.I2C(board.SCL, board.SDA, frequency=400000)
amg8833 = adafruit_amg88xx.AMG88XX(i2c)
# 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.show(splash)
# 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 > 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)
# 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)
# 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)]
# ### Helpers ###
def play_tone(freq=440, duration=0.01):
"""Play a tone over the speaker"""
tone(board.A0, freq, duration)
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 = ""
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
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 <= 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 > 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
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
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
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
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
# 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
# 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)
def get_joystick():
"""Read the joystick and interpret as up/down buttons (PyGamer)"""
if HAS_JOYSTICK:
if joystick_y.value < 20000:
# Up
return 1
if joystick_y.value > 44000:
# Down
return -1
return 0
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 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 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]
# ###--- 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.show(image_group)
update_image_frame()
flash_status("IRON", 0.75)
play_tone(880, 0.010) # Musical note A5
# ###--- 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)
# 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))
# 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
# Display image or histogram
mkr_t6 = time.monotonic() # Time marker: Display Image
if DISPLAY_IMAGE:
update_image_frame(selfie=SELFIE)
else:
update_histo_frame()
# If alarm threshold is reached, flash NeoPixels and play alarm tone
if v_max >= ALARM_C:
pixels.fill(RED)
play_tone(880, 0.015) # Musical note A5
pixels.fill(BLACK)
# 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 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
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)
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)
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("")