Skip to content

Commit

Permalink
Merge pull request #154 from DuguidLab/develop
Browse files Browse the repository at this point in the history
Add viariable contrast gratings
  • Loading branch information
celefthe authored Mar 28, 2022
2 parents 8c97ce5 + 64f2efc commit e6f2286
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ dmypy.json
.nova/

/devices/
visiomode_data/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ If running over `ssh`, you will need to prepend `DISPLAY=:0` to the `visiomode`
DISPLAY=:0 visiomode
```

This will launch the behaviour window (what the animal sees). The web interface can be accessed from any machine connected on the same network as the Raspberry Pi running Visiomode at `http://<YOUR-PI-HOSTNAME>.local:5000`, where `<YOUR-PI-HOSTNAME>` is the hostname of your Raspberry Pi. If you're unsure on what this is, run `hostname` in a terminal window.


## Upgrading

Expand Down
4 changes: 4 additions & 0 deletions src/visiomode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
def run():
"""Application entry point."""
visiomode.core.Visiomode()


if __name__ == "__main__":
run()
1 change: 1 addition & 0 deletions src/visiomode/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ def __init__(self, path=DEFAULT_PATH):
path: Path to config YAML, defaults to DEFAULT_PATH. Only used if it exists.
"""
self.load_yaml(path)
os.makedirs(self.data_dir, exist_ok=True)
1 change: 1 addition & 0 deletions src/visiomode/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class Trial(Base):
timestamp: str = datetime.datetime.now().isoformat()
correction: bool = False
response_time: int = -1
stimulus: dict = dataclasses.field(default_factory=dict)

def __repr__(self):
return "<Trial {}>".format(str(self.timestamp))
Expand Down
6 changes: 6 additions & 0 deletions src/visiomode/protocols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ def parse_trial(self, trial_start, outcome, response=None, response_time=-1):
response_time=response_time,
timestamp=trial_start,
correction=self.correction_trial,
stimulus={
"target": self.target.get_details(),
"distractor": self.distractor.get_details()
if self.distractor
else None,
},
)
return trial

Expand Down
1 change: 1 addition & 0 deletions src/visiomode/protocols/gonogo.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, target, distractor, corrections_enabled="false", **kwargs):
def show_stimulus(self):
if not self.correction_trial:
self.current_stimulus = self.get_random_stimulus()
self.current_stimulus.generate_new_trial()
self.current_stimulus.show()

def hide_stimulus(self):
Expand Down
3 changes: 3 additions & 0 deletions src/visiomode/protocols/tafc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def __init__(

def show_stimulus(self):
if not self.correction_trial:
self.target.generate_new_trial()
self.distractor.generate_new_trial()

target_x, distr_x = self.shuffle_centerx()
self.target.set_centerx(target_x)
self.distractor.set_centerx(distr_x)
Expand Down
1 change: 1 addition & 0 deletions src/visiomode/protocols/target_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def update_stimulus(self):
self.target.update()

def show_stimulus(self):
self.target.generate_new_trial()
self.target.show()

def hide_stimulus(self):
Expand Down
20 changes: 14 additions & 6 deletions src/visiomode/stimuli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ def load_image(name):
return image, image.get_rect()


def normalise_array(array, contrast=1.0):
def normalise_array(array, contrast=1.0, background_px=127):
"""Cast array to a UINT8 image matrix."""
image = (
((array - np.min(array)) / (np.max(array) - np.min(array)))
* 255
* float(contrast)
)
contrast_min = (1 - contrast) * background_px
contrast_max = (1 + contrast) * background_px

image = np.interp(array, (array.min(), array.max()), (contrast_min, contrast_max))

return image.astype(np.uint8)


Expand All @@ -62,6 +62,7 @@ def __init__(self, background, **kwargs):

def show(self):
self.hidden = False

self.screen.blit(self.image, self.rect)

def draw(self):
Expand All @@ -83,5 +84,12 @@ def collision(self, x, y):
def set_centerx(self, centerx):
self.rect.centerx = centerx

def get_details(self):
"""Returns a dictionary of stimulus attributes."""
return dict()

def generate_new_trial(self):
"""Regenerate stimuli for a fresh trial"""


plugins.load_modules_dir(__path__[0])
23 changes: 20 additions & 3 deletions src/visiomode/stimuli/grating.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,33 @@
class Grating(stimuli.Stimulus):
form_path = "stimuli/grating.html"

def __init__(self, background, period=20, contrast=1.0, **kwargs):
def __init__(self, background, period=30, contrast=1.0, **kwargs):
super().__init__(background, **kwargs)
self.period = int(period)
self.contrast = float(contrast)

grating = Grating.sinusoid(self.width, self.height, self.period, contrast)
grating = Grating.sinusoid(self.width, self.height, self.period, self.contrast)
self.image = pg.surfarray.make_surface(grating)
self.rect = self.image.get_rect()
self.area = self.screen.get_rect()

@classmethod
def sinusoid(cls, width: int, height: int, period: int, contrast: float = 1.0):
sinusoid = Grating._sinusoid(width, height, period)
return stimuli.grayscale_array(sinusoid, contrast)

@classmethod
def _sinusoid(cls, width: int, height: int, period: int):
"""Generate a sinusoid array in numpy.
Args:
width:
height:
period:
Returns:
"""
# generate 1-D sine wave of required period
x = np.arange(height)
y = np.sin(2 * np.pi * x / period)
Expand All @@ -31,4 +47,5 @@ def sinusoid(cls, width: int, height: int, period: int, contrast: float = 1.0):

# create 2-D array of sine-wave
sinusoid = np.array([[y[j] for j in range(height)] for i in range(width)])
return stimuli.grayscale_array(sinusoid, contrast)

return sinusoid
4 changes: 2 additions & 2 deletions src/visiomode/stimuli/moving_grating.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ def __init__(self, background, period=20, freq=1.0, contrast=1.0, **kwargs):
super().__init__(background, **kwargs)

self.period = int(period)
self.contrast = float(contrast)
self.frequency = float(freq)
# Determine sign of direction based on frequency (negative => downwards, positive => upwards)
self.direction = (lambda x: (-1, 1)[x < 0])(self.frequency)
self.px_per_cycle = (
self.direction * (self.period * abs(self.frequency)) / stimuli.config.fps
)
print(self.px_per_cycle)

grating = Grating.sinusoid(
self.width, self.height + (self.period * 2), self.period, contrast
self.width, self.height + (self.period * 2), self.period, self.contrast
)
self.image = pg.surfarray.make_surface(grating).convert(self.screen)
self.rect = self.image.get_rect()
Expand Down
34 changes: 34 additions & 0 deletions src/visiomode/stimuli/variable_contrast_grating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This file is part of visiomode.
# Copyright (c) 2022 Constantinos Eleftheriou <[email protected]>
# Distributed under the terms of the MIT Licence.

import random
import pygame as pg

import visiomode.stimuli as stimuli
import visiomode.stimuli.grating as grating


class VariableContrastGrating(grating.Grating):
form_path = ""

def __init__(self, background, contrasts=(0, 0.06, 0.12, 0.25, 0.5, 1.0), **kwargs):
super().__init__(background, **kwargs)

self.contrasts = contrasts
self.sinusoid_array = grating.Grating._sinusoid(
self.width, self.height, self.period
)

self.generate_new_trial()

def generate_new_trial(self):
self.trial_contrast = random.choice(self.contrasts)

_grating = stimuli.grayscale_array(self.sinusoid_array, self.trial_contrast)
self.image = pg.surfarray.make_surface(_grating)
self.rect = self.image.get_rect()
self.area = self.screen.get_rect()

def get_details(self):
return {"trial_contrast": self.trial_contrast}
35 changes: 35 additions & 0 deletions src/visiomode/stimuli/variable_contrast_moving_grating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This file is part of visiomode.
# Copyright (c) 2022 Constantinos Eleftheriou <[email protected]>
# Distributed under the terms of the MIT Licence.

import random
import pygame as pg

import visiomode.stimuli as stimuli
import visiomode.stimuli.grating as grating
import visiomode.stimuli.moving_grating as moving_grating


class VariableContrastMovingGrating(moving_grating.MovingGrating):
form_path = ""

def __init__(self, background, contrasts=(0, 0.06, 0.12, 0.25, 0.5, 1.0), **kwargs):
super().__init__(background, **kwargs)

self.contrasts = contrasts
self.sinusoid_array = grating.Grating._sinusoid(
self.width, self.height, self.period
)

self.generate_new_trial()

def generate_new_trial(self):
self.trial_contrast = random.choice(self.contrasts)

_grating = stimuli.grayscale_array(self.sinusoid_array, self.trial_contrast)
self.image = pg.surfarray.make_surface(_grating)
self.rect = self.image.get_rect()
self.area = self.screen.get_rect()

def get_details(self):
return {"trial_contrast": self.trial_contrast}
2 changes: 1 addition & 1 deletion src/visiomode/webpanel/templates/stimuli/grating.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="col">
<label for="{{ idx or '' }}period">Period (cycles)</label>
<input id="{{ idx or '' }}period" class="form-control mb-3"
type="number" value="20"
type="number" value="30"
min="1" step="1">
</div>
<div class="col">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="col">
<label for="{{ idx or ''}}period">Period (cycles)</label>
<input id="{{ idx or ''}}period" class="form-control mb-3"
type="number" value="20"
type="number" value="30"
min="1" step="1">
</div>
<div class="col">
Expand Down

0 comments on commit e6f2286

Please sign in to comment.