[tutorial] How to control a lquid 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.


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 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 happen 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 graphic 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.


  • 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 correspond to the primary screen. By default, monitor is set to 1.

We then retreive the size of the SLM display. These values correspond to the ones set up in you 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 


and close the window once finished.



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 correspond to one pixel of the SLM. However, if one send am image of any given resolution, it will be deformed to fit the SLM array. This can be usefull 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 that 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-mulitple of the SLM resolution, the interpolation could lead to dramatic effects, for example when one want 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 usin updateArray() before going further, in order to measure its effect for example, but in some cases, you do not want to loose 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 not to 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):
   print time.time() - t0

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 is 256 or less 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 exists 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
# Wait 10 seconds
# Close the window

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')

Citing this software

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



Submit to FacebookSubmit to Google BookmarksSubmit to TwitterSubmit to LinkedIn
Download this file (slmpy.p)slmpy.py[slmpy v0.1]5 kB


#9 Petro 2017-02-20 23:37
Quoting Sébastien Popoff:

If I remember correctly, the number 0 does not systematically mean that it is the primary display. One simply has to try...

It probably some crazy hardware in my setup ....
I have two outputs. HDMI and DVI.
The ¨main¨ monitor is connected to HDMI.
The 'Secondary' monitor and SLM are connected to DVI output via a DVI to 2HDMI splitter. In this way I can see what is going on SLM.

Now, if SLM is disconnected, everything works fine. Main monitor is 0 secondary is 1.

Once I connect the SLM to the splitter, in parallel with the secondary monitor, indexing switch. I know it by reading monitor resolution using wx python methods like in slmpy.

And then script refuses to show pattern on the SLM no matter what monitor parameter I put.

I have not tried SLM without secondary monitor, may be worth trying.

Another strange thing, not related to script, is that computer refuses to boot when SLM and monitor are both connected to DVI port. Probably has troubles deciding which resolution to use.
So I usually connect SLM after boot.

Anyway, if I hardcode monitor to 0 by editing slmpy everything works.

Thanks for script.
#8 Sébastien Popoff 2017-02-20 13:13
Quoting Petro:

One thing, on my setup slm is monitor 0, and setting
slm = slmpy.SLMdisplay(monitor = 0, isImageLock = False) had no effect.

Hi Petro,

The numbering depends on the operating system and on how the displays (monitor and SLM) are configured. Usually, monitor 0 would the monitor, not the SLM, but I did that in the tutorial so that we see on our screen what happens.

If I remember correctly, the number 0 does not systematically mean that it is the primary display. One simply has to try...


#7 Petro 2017-02-20 13:06
Great thing, I was using matplotlib for the same thing, but it was ugly and slow.

One thing, on my setup slm is monitor 0, and setting
slm = slmpy.SLMdispla y(monitor = 0, isImageLock = False)

Had no effect.
I had no time to figure out where is the bug so I simply replaced monitor = 1 to monitor = 0 everywhere in slmpy script.

#6 Sébastien Popoff 2016-07-12 16:40
Hi Aurélien,

Sorry I missed your initial comment and I am glad you found the solution.

It is indeed a common thing. Some SLMs have a non typical resolution while, as they are detected as display monitors, they have to use one of the video standards. They usually are detected as a bigger resolution screen with the real image (the one actually displayed) on the top left corner.
#5 Aurélien 2016-07-12 10:13
So, I finally found the answer to my issue: I looked at the phase patterns displayed by the software provided by the manufacturer(by doing a print screen) and it turns out that they add 8 black pixels on the right of each image. I hope it can help someone else!
#4 Aurélien 2016-06-28 09:57
Hi Sebastien,

Thanks for this excellent tutorial! We got slmpy working and we displayed some holograms with our SLM, however I am a bit concerned about the actual size of the image displayed: my SLM has 792*600 piels and I can only set the resolution of the 2nd monitor to 800*600 in the windows displays manager. The method getSize() returns 800*600 and so I am afraid that displaying my 792*600 phase masks leads to unwanted interpolations. Has someone already encountered this problem? Would you know how to actually check if the images are displayed correctly?
#3 Sun 2016-03-24 10:23
Hi Sébastien,
Thanks for your patient explanation. I have read the manual in detail, combined with your explanation and reply of the support, now I know much more about it. I will try to do it. Maybe I will encounter with some problems later, which need to consult you, thanks in advanced! :)
Best wishes
#2 Sébastien Popoff 2016-03-22 07:46
Hi Sun,

If nothing shows on the red layers, try the other one.

I am sorry I will not be able to help you much on this matter because it seems that all your answers should be in the user manual of your device.

With BNS, if I do remember correctly, it uses 16 bits, the 8 low order bits on one channel and the 8 high order ones on another one. So one image takes two layers, read the manual.

Why do you want to put the correction image on a different layer than the image you want to display?
The correction image just corresponds to a flat phase, so if you send it the same way you display any other image, it compensates for the unwanted variations of the liquid cristals and reflected light approximately as a flat mirror.
When you want to send any image, you just add (or substract) the correction image to obtain what you want.
Again, please look in the manual.


#1 Sun 2016-03-17 15:51
Hi Sébastien,
Thanks a lot for your sharing of python code.
But now I encounter a problem of adding compensation image of wavefront abbration on phase mask image. Since we use BNS 16 bit SLM. I tried to show the correction image on red layer, but there is nothing showed on the SLM. I think maybe you have solutions.
besides, is it correct to show the correction image and phase on different colour layers?
The matrixs of redImage and greenImage are 8 bit number or 16 bit number?
Are some parameters of following sentence should be revised?
color_array = np.concatenate( (redImage,green IMG,blueImage), axis=2).astype('uint8')
Expect your reply!:)
Best wishes

Additional information