import numpy
from OpenGL.GL import *
import threading
import wx
import wx.glcanvas
import numpy
import OpenGL.GL as GL
import threading
## Maps numpy datatypes to OpenGL datatypes
dtypeToGlTypeMap = {
numpy.uint8: GL.GL_UNSIGNED_BYTE,
numpy.uint16: GL.GL_UNSIGNED_SHORT,
numpy.int16: GL.GL_SHORT,
numpy.float32: GL.GL_FLOAT,
numpy.float64: GL.GL_FLOAT,
numpy.int32: GL.GL_FLOAT,
numpy.uint32: GL.GL_FLOAT,
numpy.complex64: GL.GL_FLOAT,
numpy.complex128: GL.GL_FLOAT,
}
## Maps numpy datatypes to the maximum value the datatype can represent
dtypeToMaxValMap = {
numpy.uint16: (1 << 16) - 1,
numpy.int16: (1 << 15) - 1,
numpy.uint8: (1 << 8) - 1,
numpy.bool_: (1 << 8) - 1,
numpy.float32: 1
}
## This class handles display of a single 2D array of pixel data.
class Image:
def __init__(self):
self.imageData = None
self.imageMin = None
self.imageMax = None
self.textureID = None
self.color = (1, 1, 1)
self.lock = threading.Lock()
self.bindTexture()
self.refresh()
def bindTexture(self):
if self.imageData is None:
return
self.lock.acquire()
pic_ny, pic_nx = self.imageData.shape
if self.imageMin == 0 and self.imageMax == 0:
self.imageMin = self.imageData.min()
self.imageMax = self.imageData.max()
# Generate texture sizes that are powers of 2
tex_nx = 2
while tex_nx < pic_nx:
tex_nx *= 2
tex_ny = 2
while tex_ny < pic_ny:
tex_ny *= 2
self.picTexRatio_x = float(pic_nx) / tex_nx
self.picTexRatio_y = float(pic_ny) / tex_ny
self.textureID = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.textureID)
# Define this new texture object based on self.imageData's geometry
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER,
GL.GL_NEAREST)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER,
GL.GL_NEAREST)
imgType = self.imageData.dtype.type
if imgType not in dtypeToGlTypeMap:
raise ValueError, "Unsupported data mode %s" % str(imgType)
GL.glTexImage2D(GL.GL_TEXTURE_2D,0, GL.GL_RGB, tex_nx,tex_ny, 0,
GL.GL_LUMINANCE, dtypeToGlTypeMap[imgType], None)
self.lock.release()
def refresh(self):
if self.imageData is None:
return
self.bindTexture()
minMaxRange = float(self.imageMax - self.imageMin)
if abs(self.imageMax - self.imageMin) < 1:
minMaxRange = 1
imgType = self.imageData.dtype.type
fBias = -self.imageMin / minMaxRange
f = dtypeToMaxValMap[imgType] / minMaxRange
GL.glBindTexture(GL.GL_TEXTURE_2D, self.textureID)
GL.glPixelTransferf(GL.GL_RED_SCALE, f)
GL.glPixelTransferf(GL.GL_GREEN_SCALE, f)
GL.glPixelTransferf(GL.GL_BLUE_SCALE, f)
GL.glPixelTransferf(GL.GL_RED_BIAS, fBias)
GL.glPixelTransferf(GL.GL_GREEN_BIAS, fBias)
GL.glPixelTransferf(GL.GL_BLUE_BIAS, fBias)
GL.glPixelTransferf(GL.GL_MAP_COLOR, False)
GL.glPixelStorei(GL.GL_UNPACK_SWAP_BYTES,
not self.imageData.dtype.isnative)
GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, self.imageData.itemsize)
imgString = self.imageData.tostring()
pic_ny, pic_nx = self.imageData.shape
if imgType not in dtypeToGlTypeMap:
raise ValueError, "Unsupported data mode %s" % str(imgType)
GL.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, 0, pic_nx, pic_ny,
GL.GL_LUMINANCE, dtypeToGlTypeMap[imgType], imgString)
def render(self):
if self.imageData is None or self.textureID is None:
return
self.refresh()
GL.glPushMatrix()
GL.glColor3fv(self.color)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.textureID)
GL.glBegin(GL.GL_QUADS)
pic_ny, pic_nx = self.imageData.shape
###//(0,0) at left bottom
GL.glTexCoord2f(0, 0)
GL.glVertex2i(0, 0)
GL.glTexCoord2f(self.picTexRatio_x, 0)
GL.glVertex2i(pic_nx, 0)
GL.glTexCoord2f(self.picTexRatio_x, self.picTexRatio_y)
GL.glVertex2i(pic_nx, pic_ny)
GL.glTexCoord2f(0, self.picTexRatio_y)
GL.glVertex2i(0, pic_ny)
GL.glEnd()
GL.glPopMatrix()
## Free the allocated GL texture
def wipe(self):
self.lock.acquire()
if self.textureID is not None:
GL.glDeleteTextures(self.textureID)
self.textureID = None
self.lock.release()
## Accept a new array of image data.
def updateImage(self, imageData, imageMin, imageMax):
self.imageData = imageData
self.imageMin = imageMin
self.imageMax = imageMax
self.wipe()
self.refresh()
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
## Simple window that contains a GLViewer instance.
class ViewerWindow(wx.Frame):
def __init__(self, parent, **kwargs):
title = "Multi-wavelength view"
wx.Frame.__init__(self, parent, title = title,
style = wx.DEFAULT_FRAME_STYLE)
sizer = wx.BoxSizer(wx.VERTICAL)
self.viewer = GLViewer(self, size = (512, 512), **kwargs)
sizer.Add(self.viewer)
self.SetSizerAndFit(sizer)
## This lock prevents us from trying to update more than one
# viewer at a time, evading some OpenGL errors.
self.lock = threading.Lock()
self.Show()
## Display a new image in the viewer, or hide the viewer if it's
# been disconnected.
def processNewImage(self, camId):
if not self.viewer.haveInitedGL:
return
self.lock.acquire()
data = numpy.random.random_integers(0, 10, (512, 512)).astype(numpy.uint16)
self.viewer.updateImage(camId, data, 0, 10)
color = colors[camId]
# Remap from [0, 255] to [0, 1]
color = [c / 255.0 for c in color]
self.viewer.setColor(camId, color, False)
self.viewer.Refresh()
self.lock.release()
## OpenGL canvas for displaying multiple camera views layered on top of each
# other.
class GLViewer(wx.glcanvas.GLCanvas):
## Instantiate.
def __init__(self, parent,
style = 0, size = wx.DefaultSize):
wx.glcanvas.GLCanvas.__init__(self, parent, style = style, size = size)
## List of Image instances
self.imgList = []
for i in xrange(3):
self.imgList.append(Image())
## Whether or not we've done some one-time initialization work.
self.haveInitedGL = False
wx.EVT_PAINT(self, self.OnPaint)
# Do nothing on background erasure, to avoid flickering.
wx.EVT_ERASE_BACKGROUND(self, lambda event: event)
def InitGL(self):
self.w, self.h = self.GetClientSizeTuple()
self.SetCurrent()
glClearColor(0.3, 0.3, 0.3, 0.0) ## background color
self.haveInitedGL = True
def updateImage(self, index, data, imageMin, imageMax):
self.pic_ny, self.pic_nx = data.shape
self.imgList[index].updateImage(data, imageMin, imageMax)
def setColor(self, imgidx, color, RefreshNow=1):
self.imgList[imgidx].color = color
if RefreshNow:
self.Refresh(0)
def OnPaint(self, event):
try:
dc = wx.PaintDC(self)
except:
return
if not self.haveInitedGL:
self.InitGL()
self.SetCurrent()
glViewport(0, 0, self.w, self.h)
glMatrixMode (GL_PROJECTION)
glLoadIdentity ()
glOrtho (0, self.w, 0, self.h, 1., -1.)
glMatrixMode (GL_MODELVIEW)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glPushMatrix()
glLoadIdentity()
glEnable(GL_TEXTURE_2D)
glEnable(GL_BLEND)
glBlendFunc(GL_ONE, GL_ONE)
for image in self.imgList:
if image.imageData is not None:
image.render()
glDisable(GL_TEXTURE_2D)
glDisable(GL_BLEND)
glFlush()
glPopMatrix()
self.SwapBuffers()
def OnReload(self, event=None):
self.Refresh(False)
app = wx.App(False)
window = ViewerWindow(None)
import time
def eventSpammer():
while True:
for i in xrange(3):
event = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, i)
wx.PostEvent(window, event)
time.sleep(.05)
def onNewImageReady(event):
window.processNewImage(event.GetId())
# Listen to events
for i in xrange(3):
wx.EVT_BUTTON(window, i, onNewImageReady)
print "Event listeners started"
threading.Thread(target = eventSpammer).start()
print "Spammer started"
app.MainLoop()