How to control a liquid crystal SLM with Python

Most liquid crystal Spatial Light Modulators (SLMs) and some digital micromirror devices (DMDs) are controlled via an analog (VGA) or digital (HDMI/DVI) monitor standard communication protocol. In other words, you plug it to your computer and it is recognized as a monitor display. There is usually no useful tool or API provided with the device to dynamically control the SLM. I previously introduced a way to control an SLM using Matlab/Octave, now that I switched to Python, I present here a way to do this using Python.

DOI

In Matlab I used a toolbox designed for creating visual stimuli. While there is plenty of them in Python, I did not find one that did exactly what I wanted and not much more (to keep it simple). Thus I decided to write my own (simple) Python module that I share here. 

 

The code and this tutorial are also available on this repository: https://github.com/wavefrontshaping/slmPy

 

A short explanation

You can skip this part if you just want to get a working example code, I want to briefly comment on how I wrote this code and why.

Displaying an image on an SLM is not harder than displaying an image on a screen. We can for example simply drag a window on the SLM "screen". However, we want to be sure to control what happens on each pixel of the SLM and avoid any interpolation that occurs when an image is resized. We then need a full-screen window in which we display an image of the same resolution as the screen. My first idea was to search in the video games oriented modules, like Pygame and Pyglet. It turns out that Pygame does not support multiple screens and while Pyglet is supposed to handle them, I was only able to make it work when all the screens had the same resolution. I then searched into modules for building graphical user interfaces. Tkinter and wxPython seem to be the most popular ones. I chose wxPython, but I know it would be possible with Tkinter too.

The SLMdisplay class of the slmpy module creates a frame (window) that is constantly refreshed in a loop. As we want to be able to send an image to display from an external code, this loop is run in a separate thread (using the Python module thread included in the standard distributions). I was largely inspired by this tutorial http://wiki.wxpython.org/MainLoopAsThread.
 

Requirements

  • A Python distribution under Linux or Windows (I did not test Mac OS, there may be issues),
  • The wxPython module available from here,
  • An SLM with a VGA/HDMI/DVI cable,
  • The slmpy.py code attached to this article (bottom of the page).
     

A simple example

First, we import the slmpy module

import slmpy

We also need the numpy module, since we send the images to display as numpy arrays, and the time module.

import numpy as np
import time

We then create the object that handles the SLM array.

slm = slmpy.SLMdisplay()

By default, slmpy uses the second monitor for displaying images. If you have more that one monitor in addition to the SLM, you may want to specify which monitor is the SLM.

slm = slmpy.SLMdisplay(monitor = x)

with x the id of the SLM display as set up in the operating system. 0 corresponds to the primary screen. By default, monitor is set to 1.

We then retrieve the size of the SLM display. These values correspond to the ones set up in your operating system, make sure they are set to the native resolution of your device. 

resX, resY = slm.getSize()

We then generate a test image to display. Note that the image has to be converted to 8-bit integers, be careful to have integers between 0 and 255.

X,Y = np.meshgrid(np.linspace(0,resX,resX),np.linspace(0,resY,resY))
testIMG = np.round((2**8-1)*(0.5+0.5*np.sin(2*np.pi*X/50))).astype('uint8'))

We can then display the image on the SLM

slm.updateArray(testIMG)

and finally, close the window once finished.

slm.close()

 

Image resolution and SLM resolution

In the previous example, we were careful to create an image with the same resolution as the SLM. Thus, one pixel of the image corresponds to one pixel of the SLM. However, if one sends am image of any given resolution, it will be deformed to fit the SLM array. This can be useful when one does not need a high-resolution image, for example, one can send a 400x300 image on a 800x600 screen, one pixel of the image will correspond to exactly 4 pixels of the SLM. This is faster than generating a 800x600 image with 2x2 squares of the same color as the software handles arrays 4 times smaller. However, if the image resolution is not a sub-multiple of the SLM resolution, the interpolation could lead to dramatic effects, for example when one wants to display a grating or if the SLM is DMD (binary) modulator.
 

Image lock

In SLM experiments, it is common to display images in a loop. What will happen if the mage does not have the time to be displayed between two iterations of the loop? In some cases, you want to be sure that the image is sent using updateArray() before going further, in order to measure its effect for example, but in some cases, you do not want to lose sync, even if it means skipping images. You can control that with the imageLock parameter.

slm = slmpy.SLMdisplay(isImageLock = True)

If isImageLock is set to True, the program will wait for the image to be displayed before returning from the updateArray() function. If it is set to False, it will not. By default, isImageLock is True.

Check the following example with isImageLock = True or IsImageLock = False. The code sent 100 times the same blank image on the screen, the image does not change in order to not spend too much computational time treating the data array. You will notice that the time spent in the loop without the image can be faster than the actual refresh rate of the monitor if isImageLock is set to False.

import slmpy
import time
slm = slmpy.SLMdisplay(isImageLock = False)
resX, resY = slm.getSize()
testIMG = np.zeros([resY,resX]).astype('uint8')
t0 = time.time()
for i in range(100):
   slm.updateArray(testIMG)
   print time.time() - t0
slm.close()

 

Color layers

 

The module can display monochromatic or color images. A standard monitor display is controlled using three 8-bit color layers (red, green, and blue). For most SLMs, there are 256 or fewer values possible for the phase or amplitude of the pixels. The array is controlled by only one 8-bit color channel. We can then only display black and white images.

However, there exist 16-bit SLMs which uses two 8-bit color channels to encode the information. For those devices, it is needed to display "color images", i.e. to control the three color layers independently (while it would still be used with a monochromatic illumination). The updateArray() function automatically detects if the array sent is a 2 or 3 dimensional one.

Here is an example of how to show a color image. On the green layer, we display sine oscillation and nothing on the other layer. If you use a secondary monitor to test, you will see green fringes.

slm = slmpy.SLMdisplay(isImageLock = False)
resX, resY = slm.getSize()
X,Y = np.meshgrid(np.linspace(0,resX,resX),np.linspace(0,resY,resY))
# The image we want on the green layer
greenIMG = np.round((2**8-1)*(0.5+0.5*np.sin(2*np.pi*X/50)))
# We need a third dimension corresponding to the color layer
greenIMG.shape = greenIMG.shape[0], greenIMG.shape[1], 1
# The two other layers are blank arrays
blankImage = np.zeros([greenIMG.shape[0], greenIMG.shape[1], 1])
# We merge the three layers in a (resY,resX,3) color array
color_array = np.concatenate((blankImage,greenIMG,blankImage), axis=2).astype('uint8')
# The image is sent to the slm
slm.updateArray(color_array)
# Wait 10 seconds
time.sleep(10)
# Close the window
slm.close

 

A dynamic example

 

As a final example, the following code generated moving fringes. The resolution of the images is set to half the one of the SLM.

import slmpy
import time
import numpy as np
slm = slmpy.SLMdisplay(isImageLock = True)
resX, resY = slm.getSize()
# We use images twice smaller than the resolution of the slm
ImgResX = resX//2
ImgResY = resY//2
X,Y = np.meshgrid(np.linspace(0,ImgResX,ImgResX),np.linspace(0,ImgResY,ImgResY))
for i in range(100):
   testIMG = np.round((2**8-1)*(0.5+0.5*np.sin(2*np.pi*X/50+1.0*i/10*np.pi))).astype('uint8')
   slm.updateArray(testIMG)
   time.sleep(0.05)
slm.close()

 

Citing this software

If this code was useful to your work, please consider citing it using its DOI:

 DOI



Created by sebastien.popoff on 09/12/2015