vision
Shivan

Shivan

How I used Python to induce Optokinetic Nystagmus

Print Friendly, PDF & Email

Time to go back. Back in time. It’s prime time. Fifth and final year of university. A year filled with stress but also a fun project. And that’s what I want to talk about today.

In our final year of university, we had to complete a project related to an emerging field in Optometry.

Some did novel treatments on dry eye, others delved into the unknown of myopia control. I chose to use pursue a novel method of testing vision with optokinetic nystagmus.

With syllables longer than my degree, you would think I would be over it now? Nope. Since I’ve been learning Python, let’s recreate my good ol’ university days with this lovely language.

Here is the github to the code. And here is the result:

Continue and see how we can come to this result. Let’s begin.

Special Thanks

Let’s begin with a thanks. I would like to thank my old supervisor for inspiring me on this project. On the off change, they are reading this – I really wished I was better and more on to it at the time.

What is Python?

Python is a programming language. Both popular and easy to learn, I talk more about this in a post on my intended learning programming pathway.

Python is blessed with a vast array of libraries/modules. Once of these libraries is called pygame. You can do a log with pygame like build two dimensional games. However for the purpose of this project, we are going to use pygame to display stimulus on a screen.

What is Optokinetic Nystagmus?

Optokinetic nystagmus or OKN is a slow pursuit (slow eye movement) followed by a fast saccade (quick eye movement) in the opposite direction. This is generally involuntary in response to a particular type of stimulus that is repeating and in one direction.

A good example is when you are in a moving car (a passenger of course). When you look at the passing telephone/electrical/fence poles, your eyes will exhibit this response.

What’s the point of this?

What we want to measure is visual acuity or VA.

VA is one of the ‘life signs’ of the eye.

It is a how well detail can be defined. For example a person who is ‘6/6’ vision is considered to be able to see well (i.e. street signs from a distance) compared to someone with ‘6/60’ type vision.

I have written a post about this more extensively and how it is measured here.

Visual acuity for most of the population can be easy to measure. Just use a letter chart.

Where is becomes interesting, is where the person is illiterate. They could be too young to communicate, or they cannot understand how the test works. I know this is rare, but it happens. OKN can be used to measure vision – with a bit more steps involved!

Pygame

Pygame is a way of putting moving objects on a screen. This is what we are going to use to stimulate OKN.

This library has some excellent documentation, which explains how movement is handled.

Before we Begin

For this project, we are going to use Python 3.8. Before we truly begin, let’s set up a virtual environment and get those libraries that are not included in the standard installation. I’m using VScode for this project, as well.

We create a new folder and link the correction Python as the interpreter. Let’s create that virtual environment:

python3 -m venv VAtest_withOKN

We can activate this environment using:

source ./VAtest_withOKN/bin/activate

As per convention, let’s create a requirements.txt.

pygame >= 2.0.0
numpy >= 1.19.4

Let’s them install our libraries from the requirements.txt.

pip install -r requirememts.txt

This should install the appropriate version of pygame, version 2.0.0, which is the latest at the time of posting this.

Importing the libraries

Let’s start by importing our libraries that we are going to use. You will see the libraries in action as we go on.

import sys
import random
import numpy as np
import pygame
import time

from pygame.locals import *

Building a screen

Creating our screen is quite simple. We will build a screen that is 2000 by 1500 pixels.

pygame.init()
(W, H) = (2000, 1500) # this creates a window 2000 by 1500 pixels in size
		      # you may have to vary this based on resolution of your monitor

tup = (W, H)

screen = pygame.display.set_mode(tup)
pygame
Blank Pygame Window

This is pretty uninteresting window. Let’s give it a title:

pygame.display.set_caption('OKN_VA')

Now let’s alter the background. We do this in multiple steps: we set the background colour and then ‘blit’ it onto screen.

# creating the background
background = pygame.Surface(screen.get_size())
background = background.convert()

grey_50 = (128, 128, 128)
background.fill(grey_50) # 50% grey

# blit on to screen
screen.blit(background, (0, 0))
pygame.display.flip() # updates background

And this is what we are left with:

pygame
Title and background is now changed

If we were to run this of a program and not the command line, we need to create a while loop that will run indefinitely, so the screen does not exit at the end of the program’s runtime.

while True:
	for event in pygame.event.get():
            if event.type in (QUIT, KEYDOWN):
                sys.exit()

        screen.blit(background, (0, 0))
        pygame.display.flip()

This will mean the program is running until a key is pressed or the program is forced to end.

Creating the Optotypes

We have a blank screen now but with no stimulus. We are going to use an optotype. An optotype is what we use to measure vision. Letters on a vision chart are optotypes.

In choosing a optotype, I think it will be a good idea to introduce my old supervisor’s research. He has developed something much better than what I could have done. This can be found here.

There are a few option in what shape for an optotype can be used. I’m thinking the circles would be easy to replicate.

Inspiration from an old supervisor – circle stimuli

I’m going to change these circles and make them into high contrast targets. This is inspired again1.

okn
More inspiration – high contrast

The idea is that if blur were introduced, the blacks and whites would be averaged to the gray background and would no longer be visible. What would alter is the ‘thickness’ of the black and the white to reflect the visual acuity.

Thankfully, pygame comes with an easy way to draw circles:

cir_col = (0, 0, 0) # Circle colour
cir_pos = (W/2, H/2) # middle of screen
cir_r = W/10 # radius of circle (size)
cir_border = 0 # border of circle which we will exclude

circle = pygame.draw.circle(background, cir_col, cir_pos, cir_r, cir_border) 

Here is the result, a circle.

A lone circle

Here’s a circle, but let’s apply our principle of high contrast. To do this, we need to draw circles on top of each other. The subsequent circles would be smaller in diameter by our desired thickness.

d = 10 # the smaller circles, this number is arbitary

circle_1 = pygame.draw.circle(background, cir_col, cir_pos, cir_r, cir_border) 
circle_2 = pygame.draw.circle(background, cir_col, cir_pos, cir_r-d, cir_border) 
circle_3 = pygame.draw.circle(background, cir_col, cir_pos, cir_r-3*d, cir_border) 
circle_4 = pygame.draw.circle(background, cir_col, cir_pos, cir_r-4*d, cir_border) 
okn
A lone high contrast circle

Notice how there are four circles. The first circle is white and largest in diameter. The second circle is black but smaller in diameter; we take away d to achieve this. The next circle is white but smaller by not twice d but three times. We do this because we need the black edge to be twice thick the white edges. This means it will average out to zero.

class DrawCircle:

    def __init__(self, screen_, x_pos, y_pos, radius, diff):
        self.screen_ = screen_
        self.x_pos = x_pos
        self.y_pos = y_pos
        self.radius = radius
        self.diff = diff

        return

    def drawCircle(self):

        white = (255, 255, 255)
        black = (0, 0, 0)
        grey = (128, 128, 128)

        cir_pos = (self.x_pos+self.radius, self.y_pos+self.radius)
        r = self.radius
        d = self.diff

        pygame.draw.circle(self.screen_, white, cir_pos, r, 0)
        pygame.draw.circle(self.screen_, black, cir_pos, r-d, 0)
        pygame.draw.circle(self.screen_, white, cir_pos, r-3*d, 0)
        pygame.draw.circle(self.screen_, grey, cir_pos, r-4*d, 0)

        return
    
    def move(self, move_speed):

        self.x_pos = self.x_pos + move_speed

        if self.x_pos < 0:
            self.x_pos = self.screen_.get_size()[0]
        elif self.x_pos > self.screen_.get_size()[0]:
            self.x_pos = 0

        return

    def changeVA(self, acuity):
        # changes thickness of circle lines

        self.diff = acuity

        return

Don’t believe me? One white edge is half-d thick, the other white edge is half-d thick, so we need black to be d thick to negate the white.

I do not think one circle will cut it. We will need to make this into a class where we can make many objects from this.

Now we have the ability to create multiple circles from this class. We can start with having these circles in a line.

There are a few methods here:

  • drawCiricle() – creates multiple circles that make up the optotype
  • move() – this moves to circles and resets their position once they reach the end of the screen
  • changeVA()– this changes how visible the stimulus. In the future, the monitor size and dimensions can be taken into consideration as well as how far the user is to determine the true visual acuity.

Here is our result:

nx = 10 # number of circles in a row

circles = [DrawCircle(background, x, 100, 50, 5, 15)
                for x in range(0, W, int(W/n_circles))]

for circle in circles:
	circle.drawCircle()
A row of circles

One line won’t suffice. Let’s fill our screen up. We are going to rewrite the top section of code. We are also going to use np.arange which creates an evenly spaced array of numbers. This array is a bit faster to access than using a python list or range.

nx, ny = 15, 10 # columns, rows of circles

a = np.arange(0, W, W/nx)
b = np.arange(0, H, H/ny)

circles = [DrawCircle(background, x, y, 50, 5, 15) for x in a for y in b]

for circle in circles:
	circle.drawCircle()

And here we have all our circles. We’ve also made the circles a bit smaller so they can fit on screen.

A screen-ful of circles

Just as a side: the list comprehension really helps when you are in a situation where you have a for-loop-in-a-for-loop. What do I mean by this? When we drew out our circles, we would have to replicate them over rows and columns. This requires two for loops:

a = np.arange(0, W, W/nx)
b = np.arange(0, H, H/ny)

circles = []
for x in a:
	for y in b:
	circles.append(DrawCircle(background, x, y, 50, 5, 15))

Thanks to Python’s list comprehension, we can write this in one single line.

circles = [DrawCircle(background, x, y, 50, 5, 15) for x in a for y in b]

Beautiful!

Getting things moving

Obviously, a bunch of static circles is not going to simulate OKN. We need to get them moving. Thankfully, we can do this with the move() method we created earlier.

Let’s go back to our while loop that we made before.

while True:

        for event in pygame.event.get():
            if event.type in (QUIT, KEYDOWN):
                sys.exit()

        for circle in circles:
            circle.move(11) # speed of OKN stimilus
            circle.drawCircle()

        screen.blit(background, (0, 0))
        grey_50 = (128, 128, 128)
        background.fill(grey_50)
        pygame.display.update()

return

This while loop, which we will call the ‘event’ loop runs indefinitely. It moves the circles, draws the circles. The display updates to blank and the process is repeated. This gives us the illusion that the circles are moving across the screen.

Here we have it. Click the play button below.

We need to take this one step further. Let’s change the ‘acuity’ of the circles for an elapsed period of time. Let’s expand on our event loop from above.

rand_dir = 1
timer = time.time()
    
    while True:

        for event in pygame.event.get():
            if event.type in (QUIT, KEYDOWN):
                sys.exit()

        for circle in circles:
            circle.move(rand_dir*11) # speed of OKN stimilus
            circle.drawCircle()

        screen.blit(background, (0, 0))
        grey_50 = (128, 128, 128)
        background.fill(grey_50)
        pygame.display.update()

        if time.time() - timer >= 5: # length of 'VA' shown for
 
            pygame.time.delay(500)

            timer = time.time()

            rand_VA = random.randint(0, 10)
            rand_dir = random.choice([-1, 1])

            for circle in circles:
                circle.changeVA(rand_VA)

    return

You can see we’ve added rand_dir, which is a modifier for direction the stimulus travels. There is also a timer, which we will use to define how long the stimulus has been displayed for. When we reach that time a condition is activated, this results in the stimulus size changing randomly in both ‘acuity’ and direction.

Let’s check it out! Click on the play button. The stimulus changes every 5 seconds. Funnily enough, the first change shows no circles because black and white circles have no thickness.

Conclusion

We have done quite a lot in this project and I’m hoping you are feeling quite satisfied. We learned about optokinetic nystagmus: an involuntary eye movement which is characterised as a smooth motion followed by a rapid eye movement in the opposing direction to a moving and repeating stimulus.

Next, we used the Python library, pygame, to create stimulus and get it moving across the screen.

We used the class method – a foray into object orientated programming – to create multiple stimuli in the form of stacked circles.

There is more that could be improved on, but if we set out the fix everything, then time would pass us by. We can always improve on these things later.

Thank you for reading this. I really hoped you enjoyed this. If you have any feedback, please get in touch with me or comment below. Share this with anyone who you would think find useful.

What can be done better

Whenever you have a project, you can fall down the rabbit hole of continually wanting to add more features and make it better and better.

We can do this, but you probably have a life. Project perfection is time consuming and you often notice time going on without you. Let’s just be satisfied with how far we have come and list down things we want to work on for the future, which may or may not happen.

They are:

  • More accuracy in the visual acuity
  • Contrast
  • Detecting OKN using a camera

Let me elaborate on these points a bit more.

Let’s start with the simplest: more accurate visual acuity. Instead of randomly changing the thicknesses of the circles arbitrarily, the monitor size, pixel density and distance away from the monitor should be taken into account to workout the visual acuity.

On top of this, high contrast targets were used. Maybe this could vary with low contrast targets or being able to adjust this at least.

Finally, what we can consider a huge commitment, would be to use some extra python libraries like OpenCV to detect the pupil and observe OKN as it were happening. By measuring the amount of OKN, the visual acuity could be adjusted to determine how good their vision is.

References
  1. Shah N, Dakin SC, Redmond T & Anderson RS. Vanishing Optotype acuity: repeatability and effect of the number of alter-natives.Ophthalmic Physiol Opt2011,31, 17–22. doi: 10.1111/j.1475-1313.2010.00806.x[]

Related posts

Optometry

Beating Boredom in Optometry

Reading Time: 6 minutes The opposite of love is indifference, and the opposite of happiness is boredom. Tim Ferris, 4-Hour Work Week At face value, you turn up, do

Read More »
wordcloud

Thesis Wordcloud Project

Reading Time: 7 minutes We will be generating a wordcloud of their thesis… well not their thesis because we have to protect their privacy, but we are going to use something similar.

Read More »
Lynda.com

How You Can Learn Everyday

Reading Time: 2 minutes Everyday, the journey of life is learning, and Lynda.com is your ticket! In our careers, we are measured by our flexibility. What was: work one

Read More »

Share this post

Share on facebook
Share on twitter
Share on linkedin
Share on pinterest
Share on email
Print Friendly, PDF & Email

Leave a Comment

Your email address will not be published. Required fields are marked *