Skip to content

Instantly share code, notes, and snippets.

@alexanderhiam
Last active August 29, 2015 14:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexanderhiam/5afe48f4b1c04c12f1d3 to your computer and use it in GitHub Desktop.
Save alexanderhiam/5afe48f4b1c04c12f1d3 to your computer and use it in GitHub Desktop.
First whack at thermal imaging with the BeagleBone Black and AGM88xx 8x8 thermal arrays sensors
"""
bbb_thermal_imager.py
Oct. 2014
First whack at thermal imaging with the BeagleBone Black and AMG88xx 8x8
thermal arrays sensors.
Copyright (c) 2014 Alexander Hiam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import bbio, cv2, colorsys
import numpy as np
class ThermalImager(object):
AMG88_ADDR = 0x68
PIXEL_DATA_START_REG = 0x80
PIXEL_DATA_LENGTH = 128
PIXEL_DATA_BLOCK_SIZE = 32
# For some reason contiuous i2c reads of 64 bytes and up is causing kernel
# panic for me (buffer overflow perhaps?). Read 32 bytes at a time.
HSV_GRADIENT = 0
BLUE_RED_GRADIENT = 1
GREYSCALE_GRADIENT = 2
GREY_RED_GRADIENT = 3
# Should move kernel sizes to lookup tables based on the color map being
# used, like so:
GUASSIAN_KSIZE = {
HSV_GRADIENT : (7,7),
BLUE_RED_GRADIENT : (7,7),
GREYSCALE_GRADIENT : (7,7),
GREY_RED_GRADIENT : (7,7)
}
# Some BGR color values:
RED = [ 0, 0,255]
BLUE = [255, 0, 0]
WHITE = [255,255,255]
BLACK = [ 0, 0, 0]
GRAY = [ 10, 10, 10]
def __init__(self, width=150, height=150, fps=4, gradient=HSV_GRADIENT):
self.width = width
self.height = height
self.fps = fps
self.period_ms = 1000./fps
self.video = cv2.VideoWriter()
self.min_temp = float('inf')
self.max_temp = float('-inf')
self.last_auto_scale = None
self.auto_scale_interval_ms = 5000 # rescale every 5 seconds
self.gradient = gradient
def run(self, filename):
""" Loops until ctrl-C grabbing frames, processing and saving to given
video file. (should be .avi!) """
bbio.Wire1.begin()
# Disabled for now, but working on overlaying the thermal image on a webcam
# feed. (The Logitech C270 has a 60 degree fov like the AMG88xx)
#self.webcam = cv2.VideoCapture(0)
#width = self.webcam.get(3)
#height = self.webcam.get(4)
#self.webcam_ratio = float(width)/height
self.video.open(filename, cv2.cv.CV_FOURCC('M','J','P','G'), self.fps,
(self.width, self.height))
try:
while True:
start = bbio.millis()
frame = self.getFrame()
#ret, webcam_image = self.webcam.read()
self.autoScale(frame.min(), frame.max())
image = self.generateColorMap(frame)
image = cv2.resize(image, (self.width, self.height),
interpolation=cv2.INTER_LANCZOS4)
# Currently does edge and object detection:
image = self.postProcess(image)
# Comment out to get higher frame rates
# Note: the kernels for the edge and object detection filters are
# currently only configured for the HSV gradient, the other color maps
# won't work well without tweaking kernel sizes
# If using the webcam as well this will overlay the two images:
#image = self.createOverlayedImage(image, webcam_image)
self.video.write(image)
dt = bbio.millis() - start
# Print the time it took to process the frame:
print dt # (invert for maximum frame rate)
leftover = self.period_ms - dt
if leftover > 0:
bbio.delay(leftover)
except KeyboardInterrupt:
return
def getFrame(self):
""" Retrieves and returns a frame of temperature data from the sensor. """
registers = []
for i in range(0, self.PIXEL_DATA_LENGTH, self.PIXEL_DATA_BLOCK_SIZE):
registers += bbio.Wire1.readTransaction(self.AMG88_ADDR,
self.PIXEL_DATA_START_REG+i,
self.PIXEL_DATA_BLOCK_SIZE)
frame = []
for i in range(0, 128, 2):
value = registers[i+1]<<8 | registers[i]
if value & (0x1<<11):
# do 2's compliment conversion
value = value - 2048
value *= 0.25 # convert to C
frame.append(value)
return np.array(frame)
def autoScale(self, min, max):
""" Sets minimum and maximum temperature values for image gradient. """
if not self.last_auto_scale:
self.last_auto_scale = bbio.millis()
elif bbio.millis() - self.last_auto_scale > self.auto_scale_interval_ms:
# reset min and max temps to force scaling
self.min_temp = float('inf')
self.max_temp = float('-inf')
self.last_auto_scale = bbio.millis()
if min < self.min_temp: self.min_temp = min
if max > self.max_temp: self.max_temp = max
def hsvGradient(self, frame):
""" Creates and returns a color map based on the HSV gradient. Takes a
ratio map, 0.0 = 0 degrees hue in HSV space and 1.0 = 360 degree hue. """
image = np.zeros((8,8,3), np.uint8)
x = 0
y = 0
for value in frame:
value = colorsys.hsv_to_rgb(value, 1, 1)
image[y][x] = map(lambda x: 255*x, value)
x += 1
if x > 7:
y += 1
x = 0
return image
def twoColorGradient(self, frame, min_color, max_color):
""" Creates and returns a color map based on the given ratio map. Colors
will range linearly from 0.0=min_color to 1.0=max_color. """
image = np.zeros((8,8,3), np.uint8)
x = 0
y = 0
for value in frame:
color = [0]*3
for ch in range(3):
color[ch] = min_color[ch] + value * (max_color[ch] - min_color[ch])
image[y][x] = color
x += 1
if x > 7:
y += 1
x = 0
return image
def generateColorMap(self, frame):
""" Generate color values based on 1D linear gradient algorithm at
http://en.wikibooks.org/wiki/Color_Theory/Color_gradient """
# Create a copy of the frame so we can do some in-place operations:
frame = np.copy(frame)
# Map temperature values to [0,1]
frame -= self.min_temp
frame /= (self.max_temp - self.min_temp)
# Create pixel color map:
if self.gradient == self.HSV_GRADIENT:
return self.hsvGradient(frame)
if self.gradient == self.BLUE_RED_GRADIENT:
return self.twoColorGradient(frame, self.BLUE, self.RED)
if self.gradient == self.GREYSCALE_GRADIENT:
return self.twoColorGradient(frame, self.BLACK, self.WHITE)
return self.twoColorGradient(frame, self.GREY, self.RED)
def createOverlayedImage(self, thermal_image, webcam_image):
""" Combines the frame from the webcam with the thermal image. """
webcam_height = int(self.width/self.webcam_ratio)
image = cv2.resize(webcam_image, (self.width, webcam_height))
top_border = bottom_border = (self.height - webcam_height) / 2
if webcam_height + 2*top_border < self.height:
top_border += 1
image = cv2.copyMakeBorder(image, top_border, bottom_border, 0, 0,
cv2.BORDER_CONSTANT, value=[0,0,0])
cv2.addWeighted(image, 1.0, thermal_image, 1.0, 0, image)
return image
def getColorContours(self, orig_img, hsv_lower=[0,0,0], hsv_upper=[20,255,255]):
""" Finds and returns an array of contours of objects withihn the given
color range in the thermal color map. """
img = cv2.cvtColor(orig_img, cv2.COLOR_BGR2HSV)
hsv_lower = np.array(hsv_lower,np.uint8)
hsv_upper = np.array(hsv_upper,np.uint8)
in_range = cv2.inRange(img, hsv_lower, hsv_upper)
kernel = np.ones((15, 15), "uint8")
in_range = cv2.dilate(in_range, kernel)
contours, hierarchy = cv2.findContours(in_range, cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
return contours
def detectEdges(self, orig_img):
""" Performs a Laplacian transform on the image and returns the result. """
edges = cv2.cvtColor(orig_img, cv2.COLOR_BGR2GRAY)
edges = cv2.GaussianBlur(edges,(7,7),0)
edges = cv2.Laplacian(edges,cv2.CV_8U, ksize=5)
kernel = np.ones((2, 2), "uint8")
edges = cv2.erode(edges, kernel, iterations=1)
#edges = cv2.Canny(edges, 33, 33)
edges = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
return edges
def detectObjects(self, orig_img, only_largest=True, min_area=500):
""" Detects objects and draws boxes around them. """
contours = self.getColorContours(orig_img)
#contours = self.detectEdges(orig_img)
largest_contour = None
largest_area = 0
def drawBox(contour, color):
# Draws a box around the given object
rect = cv2.minAreaRect(contour)
rect = ((rect[0][0], rect[0][1]), (rect[1][0], rect[1][1]), rect[2])
box = cv2.cv.BoxPoints(rect)
box = np.int0(box)
cv2.drawContours(orig_img,[box], 0, color, 2)
for i, contour in enumerate(contours):
area = cv2.contourArea(contour)
if area < min_area: continue
if only_largest:
if area > largest_area:
largest_area = area
largest_contour = contour
else:
drawBox(contour, (0, 0, 255))
if only_largest and largest_contour != None:
drawBox(largest_contour, (0, 0, 255))
return orig_img
def postProcess(self, orig_img):
edges = self.detectEdges(orig_img)
processed_img = self.detectObjects(orig_img)
cv2.addWeighted(edges, 1, processed_img, 1, 0, processed_img)
return processed_img
if __name__ == '__main__':
gradient = ThermalImager.HSV_GRADIENT
ThermalImager(200, 200, 4, gradient=gradient).run('video.avi')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment