Created
November 27, 2018 02:23
-
-
Save kevinlebrun/e767a46855e5fd501d820e1c5fcc527c to your computer and use it in GitHub Desktop.
rangefinder code using Pi camera module
This file contains 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
#! /usr/bin/env python | |
import sys | |
import argparse | |
import cv2 | |
import numpy | |
from picamera import PiCamera | |
from picamera.array import PiRGBArray | |
import RPi.GPIO as GPIO | |
import time | |
import math | |
import Adafruit_Nokia_LCD as LCD | |
import Adafruit_GPIO.SPI as SPI | |
from PIL import Image | |
from PIL import ImageDraw | |
from PIL import ImageFont | |
# Raspberry Pi hardware SPI config: | |
DC = 23 | |
RST = 24 | |
SPI_PORT = 0 | |
SPI_DEVICE = 0 | |
# Calibration parameters | |
HEIGHT = 3.75 | |
GAIN = -0.00114 | |
OFFSET = 0.435 | |
class LaserTracker(object): | |
def __init__(self, cam_width=640, cam_height=480, hue_min=20, hue_max=160, | |
sat_min=100, sat_max=255, val_min=200, val_max=256, | |
display_thresholds=False): | |
""" | |
* ``cam_width`` x ``cam_height`` -- This should be the size of the | |
image coming from the camera. Default is 640x480. | |
HSV color space Threshold values for a RED laser pointer are determined | |
by: | |
* ``hue_min``, ``hue_max`` -- Min/Max allowed Hue values | |
* ``sat_min``, ``sat_max`` -- Min/Max allowed Saturation values | |
* ``val_min``, ``val_max`` -- Min/Max allowed pixel values | |
If the dot from the laser pointer doesn't fall within these values, it | |
will be ignored. | |
* ``display_thresholds`` -- if True, additional windows will display | |
values for threshold image channels. | |
""" | |
self.cam_width = cam_width | |
self.cam_height = cam_height | |
self.hue_min = hue_min | |
self.hue_max = hue_max | |
self.sat_min = sat_min | |
self.sat_max = sat_max | |
self.val_min = val_min | |
self.val_max = val_max | |
self.display_thresholds = display_thresholds | |
self.capture = None # camera capture device | |
self.channels = { | |
'hue': None, | |
'saturation': None, | |
'value': None, | |
'laser': None, | |
} | |
self.previous_position = None | |
self.trail = numpy.zeros((self.cam_height, self.cam_width, 3), | |
numpy.uint8) | |
self.lcd = LCD.PCD8544(DC, RST, spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE, max_speed_hz=4000000)) | |
def handle_quit(self, delay=10): | |
"""Quit the program if the user presses "Esc" or "q".""" | |
key = cv2.waitKey(delay) | |
c = chr(key & 255) | |
if c in ['c', 'C']: | |
self.trail = numpy.zeros((self.cam_height, self.cam_width, 3), | |
numpy.uint8) | |
if c in ['q', 'Q', chr(27)]: | |
GPIO.cleanup() | |
sys.exit(0) | |
def threshold_image(self, channel): | |
if channel == "hue": | |
minimum = self.hue_min | |
maximum = self.hue_max | |
elif channel == "saturation": | |
minimum = self.sat_min | |
maximum = self.sat_max | |
elif channel == "value": | |
minimum = self.val_min | |
maximum = self.val_max | |
(t, tmp) = cv2.threshold( | |
self.channels[channel], # src | |
maximum, # threshold value | |
0, # we dont care because of the selected type | |
cv2.THRESH_TOZERO_INV # t type | |
) | |
(t, self.channels[channel]) = cv2.threshold( | |
tmp, # src | |
minimum, # threshold value | |
255, # maxvalue | |
cv2.THRESH_BINARY # type | |
) | |
if channel == 'hue': | |
# only works for filtering red color because the range for the hue | |
# is split | |
self.channels['hue'] = cv2.bitwise_not(self.channels['hue']) | |
def track(self, frame, mask): | |
""" | |
Track the position of the laser pointer. | |
Code taken from | |
http://www.pyimagesearch.com/2015/09/14/ball-tracking-with-opencv/ | |
""" | |
center = None | |
countours = cv2.findContours(mask, cv2.RETR_EXTERNAL, | |
cv2.CHAIN_APPROX_SIMPLE)[-2] | |
# only proceed if at least one contour was found | |
if len(countours) > 0: | |
# find the largest contour in the mask, then use | |
# it to compute the minimum enclosing circle and | |
# centroid | |
c = max(countours, key=cv2.contourArea) | |
((x, y), radius) = cv2.minEnclosingCircle(c) | |
moments = cv2.moments(c) | |
if moments["m00"] > 0: | |
center = int(moments["m10"] / moments["m00"]), \ | |
int(moments["m01"] / moments["m00"]) | |
else: | |
center = int(x), int(y) | |
# only proceed if the radius meets a minimum size | |
if radius > 10: | |
# draw the circle and centroid on the frame, | |
cv2.circle(frame, (int(x), int(y)), int(radius), | |
(0, 255, 255), 2) | |
cv2.circle(frame, center, 5, (0, 0, 255), -1) | |
# then update the ponter trail | |
if self.previous_position: | |
cv2.line(self.trail, self.previous_position, center, | |
(255, 255, 255), 2) | |
cv2.add(self.trail, frame, frame) | |
self.previous_position = center | |
def detect(self, frame): | |
hsv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
# split the video frame into color channels | |
h, s, v = cv2.split(hsv_img) | |
self.channels['hue'] = h | |
self.channels['saturation'] = s | |
self.channels['value'] = v | |
# Threshold ranges of HSV components; storing the results in place | |
self.threshold_image("hue") | |
self.threshold_image("saturation") | |
self.threshold_image("value") | |
# Perform an AND on HSV components to identify the laser! | |
self.channels['laser'] = cv2.bitwise_and( | |
self.channels['hue'], | |
self.channels['value'] | |
) | |
self.channels['laser'] = cv2.bitwise_and( | |
self.channels['saturation'], | |
self.channels['laser'] | |
) | |
# Merge the HSV components back together. | |
hsv_image = cv2.merge([ | |
self.channels['hue'], | |
self.channels['saturation'], | |
self.channels['value'], | |
]) | |
self.track(frame, self.channels['laser']) | |
return hsv_image | |
def display(self, img, frame): | |
"""Display the combined image and (optionally) all other image channels | |
NOTE: default color space in OpenCV is BGR. | |
""" | |
cv2.imwrite('frame.png', frame) | |
cv2.imwrite('laser.png', self.channels['laser']) | |
def run(self): | |
sys.stdout.write("Using OpenCV version: {0}\n".format(cv2.__version__)) | |
GPIO.setmode(GPIO.BCM) | |
laser_pin = 22 | |
lcd_led = 14 | |
GPIO.setup(laser_pin, GPIO.OUT) | |
GPIO.setup(lcd_led, GPIO.OUT) | |
GPIO.output(lcd_led, GPIO.HIGH) | |
print(GPIO.VERSION) | |
print(GPIO.RPI_INFO) | |
camera = PiCamera() | |
camera.resolution = (self.cam_width, self.cam_height) | |
time.sleep(2) # Camera warm-up time | |
self.lcd.begin(contrast=60) | |
self.lcd.clear() | |
self.lcd.display() | |
while True: | |
GPIO.output(laser_pin, GPIO.HIGH) | |
# capture the current image | |
rawCapture = PiRGBArray(camera) | |
camera.capture(rawCapture, format="bgr") | |
GPIO.output(laser_pin, GPIO.LOW) | |
frame = rawCapture.array | |
hsv_image = self.detect(frame) | |
self.display(hsv_image, frame) | |
print(self.previous_position) | |
image = Image.new('1', (LCD.LCDWIDTH, LCD.LCDHEIGHT)) | |
# Get drawing object to draw on image. | |
draw = ImageDraw.Draw(image) | |
# Draw a white filled box to clear the image. | |
draw.rectangle((0,0,LCD.LCDWIDTH,LCD.LCDHEIGHT), outline=255, fill=255) | |
font = ImageFont.load_default() | |
if self.previous_position: | |
x, y = self.previous_position | |
text = "{}, {}".format(x, y) | |
draw.text((0,0), text, font=font) | |
distance = HEIGHT / math.tan(y*GAIN+OFFSET) | |
disttext = "{} meters".format(distance) | |
draw.text((0,30), disttext, font=font) | |
else: | |
draw.text((0,0), "Unknown", font=font) | |
self.lcd.image(image) | |
self.lcd.display() | |
time.sleep(5000.0 / 1000.0) | |
self.handle_quit() | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser(description='Run the Laser Tracker') | |
parser.add_argument('-W', '--width', | |
default=1024, | |
type=int, | |
help='Camera Width') | |
parser.add_argument('-H', '--height', | |
default=768, | |
type=int, | |
help='Camera Height') | |
parser.add_argument('-u', '--huemin', | |
default=20, | |
type=int, | |
help='Hue Minimum Threshold') | |
parser.add_argument('-U', '--huemax', | |
default=160, | |
type=int, | |
help='Hue Maximum Threshold') | |
parser.add_argument('-s', '--satmin', | |
default=100, | |
type=int, | |
help='Saturation Minimum Threshold') | |
parser.add_argument('-S', '--satmax', | |
default=255, | |
type=int, | |
help='Saturation Maximum Threshold') | |
parser.add_argument('-v', '--valmin', | |
default=200, | |
type=int, | |
help='Value Minimum Threshold') | |
parser.add_argument('-V', '--valmax', | |
default=255, | |
type=int, | |
help='Value Maximum Threshold') | |
parser.add_argument('-d', '--display', | |
action='store_true', | |
help='Display Threshold Windows') | |
params = parser.parse_args() | |
tracker = LaserTracker( | |
cam_width=params.width, | |
cam_height=params.height, | |
hue_min=params.huemin, | |
hue_max=params.huemax, | |
sat_min=params.satmin, | |
sat_max=params.satmax, | |
val_min=params.valmin, | |
val_max=params.valmax, | |
display_thresholds=params.display | |
) | |
tracker.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment