Created
September 26, 2020 21:51
-
-
Save FoamyGuy/7a211743941188432b16f8a81b9ea2dd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Thermal_Cam_v32.py | |
# 2020-01-29 v3.2 | |
# (c) 2020 Jan Goolsbey for Adafruit Industries | |
import time | |
import board | |
import displayio | |
from simpleio import map_range | |
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 adafruit_pybadger import pybadger as panel | |
from thermal_cam_converters import celsius_to_fahrenheit, fahrenheit_to_celsius | |
# Load default alarm and min/max range values list from config file | |
from thermal_cam_config import ALARM_F, MIN_RANGE_F, MAX_RANGE_F | |
# Establish panel instance and check for joystick | |
panel.pixels.brightness = 0.1 # Set NeoPixel brightness | |
panel.pixels.fill(0) # Clear all NeoPixels | |
if hasattr(board, "JOYSTICK_X"): | |
panel.has_joystick = True # PyGamer | |
else: panel.has_joystick = False # Must be PyBadge | |
# Establish I2C interface for the AMG8833 Thermal Camera | |
i2c = board.I2C() | |
amg8833 = adafruit_amg88xx.AMG88XX(i2c) | |
# Load the text font from the fonts folder | |
font = bitmap_font.load_font("/fonts/OpenSans-9.bdf") | |
# Display spash graphics and play startup tones | |
with open("/thermal_cam_splash.bmp", "rb") as bitmap_file: | |
bitmap = displayio.OnDiskBitmap(bitmap_file) | |
splash = displayio.Group() | |
splash.append(displayio.TileGrid(bitmap, | |
pixel_shader=displayio.ColorConverter())) | |
board.DISPLAY.show(splash) | |
time.sleep(0.1) # Allow the splash to display | |
panel.play_tone(440, 0.1) # A4 | |
panel.play_tone(880, 0.1) # A5 | |
# The image sensor's design-limited temperature range | |
MIN_SENSOR_C = 0 | |
MAX_SENSOR_C = 80 | |
MIN_SENSOR_F = celsius_to_fahrenheit(MIN_SENSOR_C) | |
MAX_SENSOR_F = celsius_to_fahrenheit(MAX_SENSOR_C) | |
# 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) | |
# The board's integral display size | |
WIDTH = board.DISPLAY.width # 160 for PyGamer and PyBadge | |
HEIGHT = board.DISPLAY.height # 128 for PyGamer and PyBadge | |
ELEMENT_SIZE = WIDTH // 10 # Size of element_grid blocks in pixels | |
# Default colors | |
BLACK = 0x000000 | |
RED = 0xFF0000 | |
ORANGE = 0xFF8811 | |
YELLOW = 0xFFFF00 | |
GREEN = 0x00FF00 | |
CYAN = 0x00FFFF | |
BLUE = 0x0000FF | |
VIOLET = 0x9900FF | |
WHITE = 0xFFFFFF | |
GRAY = 0x444455 | |
# Block colors for the thermal image grid | |
element_color = [GRAY, BLUE, GREEN, YELLOW, ORANGE, RED, VIOLET, WHITE] | |
# Text colors for on-screen parameters | |
param_list = [("ALARM", WHITE), ("RANGE", RED), ("RANGE", CYAN)] | |
### Helpers ### | |
def element_grid(col0, row0): # Determine display coordinates for column, row | |
x = int(ELEMENT_SIZE * col0 + 30) # x coord + margin | |
y = int(ELEMENT_SIZE * row0 + 1) # y coord + margin | |
return x, y # Return display coordinates | |
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) | |
return | |
def update_image_frame(): # Get camera data and display | |
minimum = MAX_SENSOR_C # Set minimum to sensor's maximum C value | |
maximum = MIN_SENSOR_C # Set maximum to sensor's minimum C value | |
min_histo.text = "" # Clear histogram legend | |
max_histo.text = "" | |
range_histo.text = "" | |
sum_bucket = 0 # Clear bucket for building average value | |
for row1 in range(0, 8): # Parse camera data list and update display | |
for col1 in range(0, 8): | |
value = map_range(image[7 - row1][7 - col1], | |
MIN_SENSOR_C, MAX_SENSOR_C, | |
MIN_SENSOR_C, MAX_SENSOR_C) | |
color_index = int(map_range(value, MIN_RANGE_C, MAX_RANGE_C, 0, 7)) | |
image_group[((row1 * 8) + col1) + 1].fill = element_color[color_index] | |
sum_bucket = sum_bucket + value # Calculate sum for average | |
minimum = min(value, minimum) | |
maximum = max(value, maximum) | |
return minimum, maximum, sum_bucket | |
def update_histo_frame(): | |
minimum = MAX_SENSOR_C # Set minimum to sensor's maximum C value | |
maximum = MIN_SENSOR_C # Set maximum to sensor's minimum C value | |
min_histo.text = str(MIN_RANGE_F) # Display histogram legend | |
max_histo.text = str(MAX_RANGE_F) | |
range_histo.text = "-RANGE-" | |
sum_bucket = 0 # Clear bucket for building average value | |
histo_bucket = [0, 0, 0, 0, 0, 0, 0, 0] # Clear histogram bucket | |
for row2 in range(7, -1, -1): # Collect camera data and calculate spectrum | |
for col2 in range(0, 8): | |
value = map_range(image[col2][row2], | |
MIN_SENSOR_C, MAX_SENSOR_C, | |
MIN_SENSOR_C, MAX_SENSOR_C) | |
histo_index = int(map_range(value, MIN_RANGE_C, MAX_RANGE_C, 0, 7)) | |
histo_bucket[histo_index] = histo_bucket[histo_index] + 1 | |
sum_bucket = sum_bucket + value # Calculate sum for average | |
minimum = min(value, minimum) | |
maximum = max(value, maximum) | |
for col2 in range(0, 8): # Display histogram | |
for row2 in range(0, 8): | |
if histo_bucket[col2] / 8 > 7 - row2: | |
image_group[((row2 * 8) + col2) + 1].fill = element_color[col2] | |
else: | |
image_group[((row2 * 8) + col2) + 1].fill = BLACK | |
return minimum, maximum, sum_bucket | |
#pylint: disable=too-many-branches,too-many-statements | |
def setup_mode(): # Set 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 | |
# Select parameter to set | |
while not panel.button.start: | |
while (not panel.button.a) and (not panel.button.start): | |
up, down = move_buttons(joystick=panel.has_joystick) | |
if up: | |
param_index = param_index - 1 | |
if down: | |
param_index = param_index + 1 | |
param_index = max(0, min(2, param_index)) | |
status_label.text = param_list[param_index][0] | |
image_group[param_index + 66].color = BLACK | |
status_label.color = BLACK | |
time.sleep(0.2) | |
image_group[param_index + 66].color = param_list[param_index][1] | |
status_label.color = WHITE | |
time.sleep(0.2) | |
if panel.button.a: # Button A pressed | |
panel.play_tone(1319, 0.030) # E6 | |
while panel.button.a: # wait for button release | |
pass | |
# Adjust parameter value | |
param_value = int(image_group[param_index + 70].text) | |
while (not panel.button.a) and (not panel.button.start): | |
up, down = move_buttons(joystick=panel.has_joystick) | |
if up: | |
param_value = param_value + 1 | |
if down: | |
param_value = param_value - 1 | |
param_value = max(MIN_SENSOR_F, min(MAX_SENSOR_F, param_value)) | |
image_group[param_index + 70].text = str(param_value) | |
image_group[param_index + 70].color = BLACK | |
status_label.color = BLACK | |
time.sleep(0.05) | |
image_group[param_index + 70].color = param_list[param_index][1] | |
status_label.color = WHITE | |
time.sleep(0.2) | |
if panel.button.a: # Button A pressed | |
panel.play_tone(1319, 0.030) # E6 | |
while panel.button.a: # wait for button release | |
pass | |
# Exit setup process | |
if panel.button.start: # Start button pressed | |
panel.play_tone(784, 0.030) # G5 | |
while panel.button.start: # wait for button release | |
pass | |
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) | |
#pylint: enable=too-many-branches,too-many-statements | |
def move_buttons(joystick=False): # Read position buttons and joystick | |
move_u = move_d = False | |
if joystick: # For PyGamer: interpret joystick as buttons | |
if panel.joystick[1] < 20000: | |
move_u = True | |
elif panel.joystick[1] > 44000: | |
move_d = True | |
else: # For PyBadge read the buttons | |
if panel.button.up: | |
move_u = True | |
if panel.button.down: | |
move_d = True | |
return move_u, move_d | |
### Define the display group ### | |
image_group = displayio.Group(max_size=77) | |
# Create a background color fill layer; image_group[0] | |
color_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 1) | |
color_palette = displayio.Palette(1) | |
color_palette[0] = BLACK | |
background = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, | |
x=0, y=0) | |
image_group.append(background) | |
# Define the foundational thermal image element layers; image_group[1:64] | |
# image_group[#]=(row * 8) + column | |
for row in range(0, 8): | |
for col in range(0, 8): | |
pos_x, pos_y = element_grid(col, row) | |
element = Rect(x=pos_x, y=pos_y, | |
width=ELEMENT_SIZE, height=ELEMENT_SIZE, | |
fill=None, outline=None, stroke=0) | |
image_group.append(element) | |
# Define labels and values using element grid coordinates | |
status_label = Label(font, text="", color=BLACK, max_glyphs=6) | |
pos_x, pos_y = element_grid(2.5, 4) | |
status_label.x = pos_x | |
status_label.y = pos_y | |
image_group.append(status_label) # image_group[65] | |
alarm_label = Label(font, text="alm", color=WHITE, max_glyphs=3) | |
pos_x, pos_y = element_grid(-1.8, 1.5) | |
alarm_label.x = pos_x | |
alarm_label.y = pos_y | |
image_group.append(alarm_label) # image_group[66] | |
max_label = Label(font, text="max", color=RED, max_glyphs=3) | |
pos_x, pos_y = element_grid(-1.8, 3.5) | |
max_label.x = pos_x | |
max_label.y = pos_y | |
image_group.append(max_label) # image_group[67] | |
min_label = Label(font, text="min", color=CYAN, max_glyphs=3) | |
pos_x, pos_y = element_grid(-1.8, 7.5) | |
min_label.x = pos_x | |
min_label.y = pos_y | |
image_group.append(min_label) # image_group[68] | |
ave_label = Label(font, text="ave", color=YELLOW, max_glyphs=3) | |
pos_x, pos_y = element_grid(-1.8, 5.5) | |
ave_label.x = pos_x | |
ave_label.y = pos_y | |
image_group.append(ave_label) # image_group[69] | |
alarm_value = Label(font, text=str(ALARM_F), color=WHITE, max_glyphs=5) | |
pos_x, pos_y = element_grid(-1.8, 0.5) | |
alarm_value.x = pos_x | |
alarm_value.y = pos_y | |
image_group.append(alarm_value) # image_group[70] | |
max_value = Label(font, text=str(MAX_RANGE_F), color=RED, max_glyphs=5) | |
pos_x, pos_y = element_grid(-1.8, 2.5) | |
max_value.x = pos_x | |
max_value.y = pos_y | |
image_group.append(max_value) # image_group[71] | |
min_value = Label(font, text=str(MIN_RANGE_F), color=CYAN, max_glyphs=5) | |
pos_x, pos_y = element_grid(-1.8, 6.5) | |
min_value.x = pos_x | |
min_value.y = pos_y | |
image_group.append(min_value) # image_group[72] | |
ave_value = Label(font, text="---", color=YELLOW, max_glyphs=5) | |
pos_x, pos_y = element_grid(-1.8, 4.5) | |
ave_value.x = pos_x | |
ave_value.y = pos_y | |
image_group.append(ave_value) # image_group[73] | |
min_histo = Label(font, text="", color=CYAN, max_glyphs=3) | |
pos_x, pos_y = element_grid(0.5, 7.5) | |
min_histo.x = pos_x | |
min_histo.y = pos_y | |
image_group.append(min_histo) # image_group[74] | |
max_histo = Label(font, text="", color=RED, max_glyphs=3) | |
pos_x, pos_y = element_grid(6.5, 7.5) | |
max_histo.x = pos_x | |
max_histo.y = pos_y | |
image_group.append(max_histo) # image_group[75] | |
range_histo = Label(font, text="", color=BLUE, max_glyphs=7) | |
pos_x, pos_y = element_grid(2.5, 7.5) | |
range_histo.x = pos_x | |
range_histo.y = pos_y | |
image_group.append(range_histo) # image_group[76] | |
###--- PRIMARY PROCESS SETUP ---### | |
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 | |
orig_max_range_f = 0 # There are no initial range values | |
orig_min_range_f = 0 | |
# Activate display and play welcome tone | |
board.DISPLAY.show(image_group) | |
panel.play_tone(880, 0.1) # A5; ready to start looking | |
###--- PRIMARY PROCESS LOOP ---### | |
while True: | |
if display_hold: # Flash hold status text label | |
flash_status("-HOLD-") | |
else: | |
image = amg8833.pixels # Get camera data list if not in hold mode | |
status_label.text = "" # Clear hold mode status text label | |
if display_image: # Image display mode and gather min, max, and sum stats | |
v_min, v_max, v_sum = update_image_frame() | |
else: # Histogram display mode and gather min, max, and sum stats | |
v_min, v_max, v_sum = update_histo_frame() | |
# Display alarm setting and maxumum, minimum, and average stats | |
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_sum // 64)) | |
# Flash first NeoPixel and play alarm notes if alarm threshold is exceeded | |
# Second alarm note frequency is proportional to value above threshold | |
if v_max >= ALARM_C: | |
panel.pixels.fill(RED) | |
panel.play_tone(880, 0.015) # A5 | |
panel.play_tone(880 + (10 * (v_max - ALARM_C)), 0.015) # A5 | |
panel.pixels.fill(BLACK) | |
# See if a panel button is pressed | |
if panel.button.a: # Toggle display hold (shutter = button A) | |
panel.play_tone(1319, 0.030) # E6 | |
while panel.button.a: | |
pass # wait for button release | |
if not display_hold: | |
display_hold = True | |
else: | |
display_hold = False | |
if panel.button.b: # Toggle image/histogram mode (display mode = button B) | |
panel.play_tone(659, 0.030) # E5 | |
while panel.button.b: | |
pass # wait for button release | |
if display_image: | |
display_image = False | |
else: | |
display_image = True | |
if panel.button.select: # toggle focus mode (focus mode = select button) | |
panel.play_tone(698, 0.030) # F5 | |
if display_focus: | |
display_focus = False # restore previous (original) range values | |
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) | |
else: | |
display_focus = True # set range values to image min/max | |
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) | |
MIN_RANGE_C = v_min # update range temp in Celsius | |
MAX_RANGE_C = v_max # update range temp in Celsius | |
flash_status("FOCUS", 0.2) | |
while panel.button.select: | |
pass # wait for button release | |
if panel.button.start: # activate setup mode (setup mode = start button) | |
panel.play_tone(784, 0.030) # G5 | |
while panel.button.start: | |
pass # wait for button release | |
# 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) | |
# bottom of primary loop |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment