diff --git a/.gitignore b/.gitignore index db4561e..630b7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ develop-eggs/ dist/ downloads/ eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/README.md b/README.md index 7546833..424a165 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,5 @@ pyseek ====== Python interface for Seek Thermal device + +See pyseek/examples for example usage diff --git a/pyseek/__init__.py b/pyseek/__init__.py new file mode 100644 index 0000000..bf38c41 --- /dev/null +++ b/pyseek/__init__.py @@ -0,0 +1,181 @@ +'''pyseek thermal camera library''' + +''' + This is based on pyseek by Fry-kun, which has these credits: + + "Many thanks to the folks at eevblog, especially (in no particular order) + miguelvp, marshallh, mikeselectricstuff, sgstair and many others + for the inspiration to figure this out" + +''' + +import usb.core +import usb.util +from PIL import Image +from scipy.misc import toimage +import numpy, sys + +class PySeekError(IOError): + '''pyseek thermal camera error''' + pass + +class PySeek: + '''pyseek thermal camera control''' + + def __init__(self): + self.calibration = None + self.dev = None + self.debug = False + + def send_msg(self, bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None): + '''send a message to the camera''' + ret = self.dev.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, data_or_wLength, timeout) + if ret != len(data_or_wLength): + raise PySeekError() + + def receive_msg(self, bRequest, wValue, wIndex, data_or_wLength): + '''receive a message from camera''' + return self.dev.ctrl_transfer(0xC1, bRequest, wValue, wIndex, data_or_wLength) + + def deinit(self): + '''Deinit the device''' + msg = '\x00\x00' + for i in range(3): + self.send_msg(0x41, 0x3C, 0, 0, msg) + + def open(self): + '''find and open the camera. Raise PySeekError on error''' + # find our Seek Thermal device 289d:0010 + self.dev = usb.core.find(idVendor=0x289d, idProduct=0x0010) + if not self.dev: + raise PySeekError() + + # set the active configuration. With no arguments, the first configuration will be the active one + self.dev.set_configuration() + + # get an endpoint instance + cfg = self.dev.get_active_configuration() + intf = cfg[(0,0)] + + custom_match = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT + ep = usb.util.find_descriptor(intf, custom_match=custom_match) # match the first OUT endpoint + assert ep is not None + + # Setup device + try: + msg = '\x01' + self.send_msg(0x41, 0x54, 0, 0, msg) + except Exception as e: + self.deinit() + msg = '\x01' + self.send_msg(0x41, 0x54, 0, 0, msg) + + # Some day we will figure out what all this init stuff is and + # what the returned values mean. + self.send_msg(0x41, 0x3C, 0, 0, '\x00\x00') + ret1 = self.receive_msg(0x4E, 0, 0, 4) + ret2 = self.receive_msg(0x36, 0, 0, 12) + + self.send_msg(0x41, 0x56, 0, 0, '\x20\x00\x30\x00\x00\x00') + ret3 = self.receive_msg(0x58, 0, 0, 0x40) + + self.send_msg(0x41, 0x56, 0, 0, '\x20\x00\x50\x00\x00\x00') + ret4 = self.receive_msg(0x58, 0, 0, 0x40) + + self.send_msg(0x41, 0x56, 0, 0, '\x0C\x00\x70\x00\x00\x00') + ret5 = self.receive_msg(0x58, 0, 0, 0x18) + + self.send_msg(0x41, 0x56, 0, 0, '\x06\x00\x08\x00\x00\x00') + ret6 = self.receive_msg(0x58, 0, 0, 0x0C) + + self.send_msg(0x41, 0x3E, 0, 0, '\x08\x00') + ret7 = self.receive_msg(0x3D, 0, 0, 2) + + self.send_msg(0x41, 0x3E, 0, 0, '\x08\x00') + self.send_msg(0x41, 0x3C, 0, 0, '\x01\x00') + ret8 = self.receive_msg(0x3D, 0, 0, 2) + + def cal_ok(self, x, y): + value = self.calibration[x][y] + return value != 0 and value < 15000 + + def get_array(self): + '''return next image from the camera as a numpy array. Raise PySeekError on error''' + tries = 100 + while tries: + tries -= 1 + # Send read frame request + self.send_msg(0x41, 0x53, 0, 0, '\xC0\x7E\x00\x00') + try: + ret9 = self.dev.read(0x81, 0x3F60, 1000) + ret9 += self.dev.read(0x81, 0x3F60, 1000) + ret9 += self.dev.read(0x81, 0x3F60, 1000) + ret9 += self.dev.read(0x81, 0x3F60, 1000) + except usb.USBError as e: + raise PySeekError() + + # Let's see what type of frame it is + # 1 is a Normal frame, 3 is a Calibration frame + # 6 may be a pre-calibration frame + # 5, 10 other... who knows. + status = ret9[20] + if self.debug: + print ('%5d'*21 ) % tuple([ret9[x] for x in range(21)]) + print(status, len(ret9)) + + if status == 1: + # Convert the raw calibration data to a string array + calimg = Image.fromstring("I", (208,156), ret9, "raw", "I;16") + + # Convert the string array to an unsigned numpy int16 array + im2arr = numpy.asarray(calimg) + self.calibration = im2arr.astype('uint16') + + if status == 3 and self.calibration is not None: + # Convert the raw image data to a string array + img = Image.fromstring("I", (208,156), ret9, "raw", "I;16") + + # Convert the string array to an unsigned numpy int16 array + im1arr = numpy.asarray(img) + im1arrF = im1arr.astype('uint16') + + if self.debug: + # Subtract the calibration array from the image array and add an offset + print("Calibration:") + for x in range(30): + for y in range(10): + sys.stdout.write("%4u " % self.calibration[x][y]) + print("") + print("Data:") + for x in range(30): + for y in range(10): + sys.stdout.write("%4u " % im1arrF[x][y]) + print("") + + + ret = (im1arrF-self.calibration) + 800 + + # for some strange reason there are blank lines and + # gaps. This is a rough attempt to fill those in. + # it still leaves some speckling + for x in range(156): + for y in range(208): + if not self.cal_ok(x,y): + if x > 0 and self.cal_ok(x-1,y): + ret[x][y] = ret[x-1][y] + elif x < 155 and self.cal_ok(x+1,y): + ret[x][y] = ret[x+1][y] + elif y > 0 and self.cal_ok(x,y-1): + ret[x][y] = ret[x][y-1] + elif y < 207 and self.cal_ok(x,y+1): + ret[x][y] = ret[x][y+1] + + return ret + + raise PySeekError() + + def get_image(self): + '''return next image from the camera as a scipy image. Raise PySeekError on error''' + a = self.get_array() + return toimage(a) + diff --git a/pyseek/examples/pyseek_capture.py b/pyseek/examples/pyseek_capture.py new file mode 100755 index 0000000..34ab19e --- /dev/null +++ b/pyseek/examples/pyseek_capture.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +'''pyseek thermal camera library - capture example''' + +import pyseek +from pyseek.lib.PGM import PGM_write + +seek = pyseek.PySeek() +seek.open() +for i in range(100): + img = seek.get_array() + PGM_write('seek-%u.pgm' % i, img) diff --git a/pyseek/examples/pyseek_view.py b/pyseek/examples/pyseek_view.py new file mode 100755 index 0000000..ad5953d --- /dev/null +++ b/pyseek/examples/pyseek_view.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +'''pyseek thermal camera library - viewer example''' + +import Tkinter +import pyseek +from pyseek.lib.PGM import PGM_write +from PIL import ImageTk +import sys, os, time + +def show_frame(seek, first=False): + global fps_t, fps_f + + from scipy.misc import toimage + + arr = seek.get_array() + disp_img = toimage(arr) + + if first: + root.geometry('%dx%d' % (disp_img.size[0], disp_img.size[1])) + tkpi = ImageTk.PhotoImage(disp_img) + label_image.imgtk = tkpi + label_image.configure(image=tkpi) + label_image.place(x=0, y=0, width=disp_img.size[0], height=disp_img.size[1]) + + now = int(time.time()) + fps_f += 1 + if fps_t == 0: + fps_t = now + elif fps_t < now: + print '\rFPS: %.2f' % (1.0 * fps_f / (now-fps_t)), + sys.stdout.flush() + fps_t = now + fps_f = 0 + + label_image.after(1, show_frame, seek) # after 1ms, run show_frame again + + +seek = pyseek.PySeek() +seek.open() + +root = Tkinter.Tk() +root.title('Seek Thermal camera') +root.bind("", lambda e: root.quit()) + +label_image = Tkinter.Label(root) +label_image.pack() + +fps_t = 0 +fps_f = 0 + +show_frame(seek, first=True) +root.mainloop() # UI has control until user presses <> diff --git a/pyseek/lib/PGM.py b/pyseek/lib/PGM.py new file mode 100644 index 0000000..b6c0167 --- /dev/null +++ b/pyseek/lib/PGM.py @@ -0,0 +1,74 @@ +'''16 bit PGM read/write code''' + +import numpy + +class PGMError(Exception): + '''PGMLink error class''' + def __init__(self, msg): + Exception.__init__(self, msg) + +def PGM_read(filename): + '''read a 8/16 bit PGM image, returning a numpy array''' + f = open(filename, mode='rb') + fmt = f.readline() + if fmt.strip() != 'P5': + raise PGMError('Expected P5 image in %s' % filename) + dims = f.readline() + dims = dims.split(' ') + width = int(dims[0]) + height = int(dims[1]) + line = f.readline() + if line[0] == '#': + # discard comment + line = f.readline() + line = line.strip() + if line == "65535": + eightbit = False + elif line == "255": + eightbit = True + else: + raise PGMError('Expected 8/16 bit image image in %s - got %s' % (filename, line)) + if eightbit: + rawdata = numpy.fromfile(f, dtype='uint8') + rawdata = numpy.reshape(rawdata, (height,width)) + else: + rawdata = numpy.fromfile(f, dtype='uint16') + rawdata = rawdata.byteswap(True) + rawdata = numpy.reshape(rawdata, (height, width)) + f.close() + return rawdata.byteswap(True) + + +def PGM_write(filename, rawdata): + '''write a 8/16 bit PGM image given a numpy array''' + if rawdata.dtype == numpy.dtype('uint8'): + numvalues = 255 + elif rawdata.dtype == numpy.dtype('uint16'): + numvalues = 65535 + else: + raise PGMError("Invalid array data type '%s'" % rawdata.dtype) + shape = rawdata.shape + if len(shape) != 2: + raise PGMError("Invalid array shape '%s'" % shape) + height = shape[0] + width = shape[1] + + f = open(filename, mode='wb') + f.write('''P5 +%u %u +%u +''' % (width, height, numvalues)) + + rawdata = rawdata.byteswap(True) + rawdata = rawdata.tofile(f) + f.close() + +if __name__ == "__main__": + import sys + filename = sys.argv[1] + print("Reading %s" % filename) + a = PGM_read(filename) + + filename = filename + ".test" + print("Writing %s" % filename) + PGM_write(filename, a) diff --git a/pyseek/lib/__init__.py b/pyseek/lib/__init__.py new file mode 100644 index 0000000..0ecc715 --- /dev/null +++ b/pyseek/lib/__init__.py @@ -0,0 +1 @@ +'''pyseek thermal camera library - library code''' diff --git a/seek.py b/seek.py deleted file mode 100644 index 649a91d..0000000 --- a/seek.py +++ /dev/null @@ -1,180 +0,0 @@ -# You will need to have python 2.7 (3+ may work) -# and PyUSB 1.0 -# and PIL 1.1.6 or better -# and numpy -# and scipy -# and ImageMagick - -# Many thanks to the folks at eevblog, especially (in no particular order) -# miguelvp, marshallh, mikeselectricstuff, sgstair and many others -# for the inspiration to figure this out -# This is not a finished product and you can use it if you like. Don't be -# surprised if there are bugs as I am NOT a programmer..... ;>)) - - -## https://github.com/sgstair/winusbdotnet/blob/master/UsbDevices/SeekThermal.cs - -import usb.core -import usb.util -import Tkinter -from PIL import Image, ImageTk -import numpy -from scipy.misc import toimage -import sys, os, time - - -# find our Seek Thermal device 289d:0010 -dev = usb.core.find(idVendor=0x289d, idProduct=0x0010) -if not dev: raise ValueError('Device not found') - -def send_msg(bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None): - assert (dev.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, data_or_wLength, timeout) == len(data_or_wLength)) - -# alias method to make code easier to read -receive_msg = dev.ctrl_transfer - -def deinit(): - '''Deinit the device''' - msg = '\x00\x00' - for i in range(3): - send_msg(0x41, 0x3C, 0, 0, msg) - - -# set the active configuration. With no arguments, the first configuration will be the active one -dev.set_configuration() - -# get an endpoint instance -cfg = dev.get_active_configuration() -intf = cfg[(0,0)] - -custom_match = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT -ep = usb.util.find_descriptor(intf, custom_match=custom_match) # match the first OUT endpoint -assert ep is not None - - -# Setup device -try: - msg = '\x01' - send_msg(0x41, 0x54, 0, 0, msg) -except Exception as e: - deinit() - msg = '\x01' - send_msg(0x41, 0x54, 0, 0, msg) - -# Some day we will figure out what all this init stuff is and -# what the returned values mean. - -send_msg(0x41, 0x3C, 0, 0, '\x00\x00') -ret1 = receive_msg(0xC1, 0x4E, 0, 0, 4) -#print ret1 -ret2 = receive_msg(0xC1, 0x36, 0, 0, 12) -#print ret2 - -send_msg(0x41, 0x56, 0, 0, '\x20\x00\x30\x00\x00\x00') -ret3 = receive_msg(0xC1, 0x58, 0, 0, 0x40) -#print ret3 - -send_msg(0x41, 0x56, 0, 0, '\x20\x00\x50\x00\x00\x00') -ret4 = receive_msg(0xC1, 0x58, 0, 0, 0x40) -#print ret4 - -send_msg(0x41, 0x56, 0, 0, '\x0C\x00\x70\x00\x00\x00') -ret5 = receive_msg(0xC1, 0x58, 0, 0, 0x18) -#print ret5 - -send_msg(0x41, 0x56, 0, 0, '\x06\x00\x08\x00\x00\x00') -ret6 = receive_msg(0xC1, 0x58, 0, 0, 0x0C) -#print ret6 - -send_msg(0x41, 0x3E, 0, 0, '\x08\x00') -ret7 = receive_msg(0xC1, 0x3D, 0, 0, 2) -#print ret7 - -send_msg(0x41, 0x3E, 0, 0, '\x08\x00') -send_msg(0x41, 0x3C, 0, 0, '\x01\x00') -ret8 = receive_msg(0xC1, 0x3D, 0, 0, 2) -#print ret8 - -im2arrF = None -def get_image(): - global im2arrF - while True: - # Send read frame request - send_msg(0x41, 0x53, 0, 0, '\xC0\x7E\x00\x00') - - try: - ret9 = dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - except usb.USBError as e: - sys.exit() - - # Let's see what type of frame it is - # 1 is a Normal frame, 3 is a Calibration frame - # 6 may be a pre-calibration frame - # 5, 10 other... who knows. - status = ret9[20] - #print ('%5d'*21 ) % tuple([ret9[x] for x in range(21)]) - - if status == 1: - # Convert the raw calibration data to a string array - calimg = Image.fromstring("I", (208,156), ret9, "raw", "I;16") - - # Convert the string array to an unsigned numpy int16 array - im2arr = numpy.asarray(calimg) - im2arrF = im2arr.astype('uint16') - - if status == 3: - # Convert the raw calibration data to a string array - img = Image.fromstring("I", (208,156), ret9, "raw", "I;16") - - # Convert the string array to an unsigned numpy int16 array - im1arr = numpy.asarray(img) - im1arrF = im1arr.astype('uint16') - - # Subtract the calibration array from the image array and add an offset - additionF = (im1arrF-im2arrF)+ 800 - - # convert to an image and display with imagemagick - disp_img = toimage(additionF) - #disp_img.show() - - return disp_img - - -root = Tkinter.Tk() -root.title('Seek Thermal camera') -root.bind("", lambda e: root.quit()) -#root.geometry('+%d+%d' % (208,156)) - -label_image = Tkinter.Label(root) -label_image.pack() - -fps_t = 0 -fps_f = 0 - -def show_frame(first=False): - global fps_t, fps_f - - disp_img = get_image() - if first: root.geometry('%dx%d' % (disp_img.size[0], disp_img.size[1])) - tkpi = ImageTk.PhotoImage(disp_img) - label_image.imgtk = tkpi - label_image.configure(image=tkpi) - label_image.place(x=0, y=0, width=disp_img.size[0], height=disp_img.size[1]) - - now = int(time.time()) - fps_f += 1 - if fps_t == 0: - fps_t = now - elif fps_t < now: - print '\rFPS: %.2f' % (1.0 * fps_f / (now-fps_t)), - sys.stdout.flush() - fps_t = now - fps_f = 0 - - label_image.after(1, show_frame) # after 1ms, run show_frame again - -show_frame(first=True) -root.mainloop() # UI has control until user presses <> diff --git a/seek_orig.py b/seek_orig.py deleted file mode 100644 index 44edfcc..0000000 --- a/seek_orig.py +++ /dev/null @@ -1,175 +0,0 @@ -# You will need to have python 2.7 (3+ may work) -# and PyUSB 1.0 -# and PIL 1.1.6 or better -# and numpy -# and scipy -# and ImageMagick - -# Many thanks to the folks at eevblog, especially (in no particular order) -# miguelvp, marshallh, mikeselectricstuff, sgstair and many others -# for the inspiration to figure this out -# This is not a finished product and you can use it if you like. Don't be -# surprised if there are bugs as I am NOT a programmer..... ;>)) - - -import usb.core -import usb.util -import sys -import Image -import numpy -from scipy.misc import toimage - -# find our Seek Thermal device 289d:0010 -dev = usb.core.find(idVendor=0x289d, idProduct=0x0010) - -# was it found? -if dev is None: - raise ValueError('Device not found') - -# set the active configuration. With no arguments, the first -# configuration will be the active one -dev.set_configuration() - -# get an endpoint instance -cfg = dev.get_active_configuration() -intf = cfg[(0,0)] - -ep = usb.util.find_descriptor( - intf, - # match the first OUT endpoint - custom_match = \ - lambda e: \ - usb.util.endpoint_direction(e.bEndpointAddress) == \ - usb.util.ENDPOINT_OUT) - -assert ep is not None - - -# Deinit the device - -msg= '\x00\x00' -assert dev.ctrl_transfer(0x41, 0x3C, 0, 0, msg) == len(msg) -assert dev.ctrl_transfer(0x41, 0x3C, 0, 0, msg) == len(msg) -assert dev.ctrl_transfer(0x41, 0x3C, 0, 0, msg) == len(msg) - - -# Setup device - -#msg = x01 -assert dev.ctrl_transfer(0x41, 0x54, 0, 0, 0x01) - -# Some day we will figure out what all this init stuff is and -# what the returned values mean. - -msg = '\x00\x00' -assert dev.ctrl_transfer(0x41, 0x3C, 0, 0, msg) == len(msg) - -ret1 = dev.ctrl_transfer(0xC1, 0x4E, 0, 0, 4) -ret2 = dev.ctrl_transfer(0xC1, 0x36, 0, 0, 12) - -#print ret1 -#print ret2 - -# - -msg = '\x20\x00\x30\x00\x00\x00' -assert dev.ctrl_transfer(0x41, 0x56, 0, 0, msg) == len(msg) - -ret3 = dev.ctrl_transfer(0xC1, 0x58, 0, 0, 0x40) -#print ret3 - -# - -msg = '\x20\x00\x50\x00\x00\x00' -assert dev.ctrl_transfer(0x41, 0x56, 0, 0, msg) == len(msg) - -ret4 = dev.ctrl_transfer(0xC1, 0x58, 0, 0, 0x40) -#print ret4 - -# - -msg = '\x0C\x00\x70\x00\x00\x00' -assert dev.ctrl_transfer(0x41, 0x56, 0, 0, msg) == len(msg) - -ret5 = dev.ctrl_transfer(0xC1, 0x58, 0, 0, 0x18) -#print ret5 - -# - -msg = '\x06\x00\x08\x00\x00\x00' -assert dev.ctrl_transfer(0x41, 0x56, 0, 0, msg) == len(msg) - -ret6 = dev.ctrl_transfer(0xC1, 0x58, 0, 0, 0x0C) -#print ret6 - -# - -msg = '\x08\x00' -assert dev.ctrl_transfer(0x41, 0x3E, 0, 0, msg) == len(msg) - -ret7 = dev.ctrl_transfer(0xC1, 0x3D, 0, 0, 2) -#print ret7 - -# - -msg = '\x08\x00' -assert dev.ctrl_transfer(0x41, 0x3E, 0, 0, msg) == len(msg) -msg = '\x01\x00' -assert dev.ctrl_transfer(0x41, 0x3C, 0, 0, msg) == len(msg) - -ret8 = dev.ctrl_transfer(0xC1, 0x3D, 0, 0, 2) -#print ret8 - -# - -x=0 - -while x < 5: - -# Send read frame request - - msg = '\xC0\x7E\x00\x00' - assert dev.ctrl_transfer(0x41, 0x53, 0, 0, msg) == len(msg) - - ret9 = dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - ret9 += dev.read(0x81, 0x3F60, 1000) - -# Let's see what type of frame it is -# 1 is a Normal frame, 3 is a Calibration frame -# 6 may be a pre-calibration frame -# 5, 10 other... who knows. - - status = ret9[20] - - if status == 1: - -# Convert the raw calibration data to a string array - - calimg = Image.fromstring("I", (208,156), ret9, "raw", "I;16") - -# Convert the string array to an unsigned numpy int16 array - - im2arr = numpy.asarray(calimg) - im2arrF = im2arr.astype('uint16') - - if status == 3: - -# Convert the raw calibration data to a string array - - img = Image.fromstring("I", (208,156), ret9, "raw", "I;16") - -# Convert the string array to an unsigned numpy int16 array - - im1arr = numpy.asarray(img) - im1arrF = im1arr.astype('uint16') - -# Subtract the calibration array from the image array and add an offset - - additionF = (im1arrF-im2arrF)+ 800 - -# convert to an image and display with imagemagick - - toimage(additionF).show() - x = x + 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2053039 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +from setuptools import setup + +version = "0.0.1" + +setup(name='pyseek', + version=version, + zip_safe=True, + description='library for control of seek thermal camera', + url='https://github.com/Fry-kun/pyseek', + author='Fry Kun', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Science/Research', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Topic :: Scientific/Engineering'], + license='MIT', + packages=['pyseek', + 'pyseek.lib'], + install_requires=['pyusb', + 'numpy', + 'scipy'] + )