This repo contains the code for a vision science experiment investigating how human perception changes across the visual field using behavioral experiments and computational models inspired by the earlys tages of visual processing. We use these models to investigate what people cannot see, an approach that has a long history of vision science. If we know what information people are insensitive to, we can discard it or randomize it, and the resulting image should appear unchanged from the original.
See the preprint or the VSS 2023 poster for scientific details. You may also be interested in the website we put together for browsing through the synthesized images, and the OSF for bulk downloading the images or the behavioral data. Finally, you may be interested in plenoptic, a software package for generating metamers (and more!) for your own models.
If you re-use some component of this project in an academic publication, see the citation section for how to credit us.
- Usage
- Setup
- Directory structure
- Usage details
- Known issues
- Notes on reproducibility
- Related repos
- Citation
- References
The data and code for this project are shared with the primary goal of enabling reproduction of the results presented in the associated paper. Novel analyses should be possible, but no guarantees.
To that end, we provide several entrypoints into the data for re-running the analysis, with a script to automate their download and proper arrangement.
As a note: snakemake
commands create files. I recommend adding -n
to any
snakemake
command when you run it for the first time. This will do a "dry
run", so you can see what steps snakemake
will take, without running anything.
The following steps will walk you through downloading the fully-processed data and recreating the figures, read further on in this README for details:
- Clone this repo.
- Because we use git submodules, you'll also need to run the following two
lines:
git submodule sync git submodule update --init --recursive
- Because we use git submodules, you'll also need to run the following two
lines:
- Open
config.yml
and modify theDATA_DIR
path to wherever you wish to download the data (see config.yml section for details on this file). - Install the required software:
- Install miniconda on your system for python 3.7.
- Install mamba:
conda install mamba -n base -c conda-forge
- Navigate to this directory and run
mamba env create -f environment.yml
to install the environment. - Run
conda activate metamers
to activate the python environment. - Additionally, install inkscape, version equal to or greater 1.0.2. It seems like that, with inkscape version 1.2.2, all images are embedded at full resolution, leading to a massive increase in file size. I'm not sure what change causes this. It works as intended with inkscape 1.1
- Check if you have
rsync
available (you probably do) and install it if you don't (probably best to do so via a package manager).
- Run
python download_data.py synthesis_input mcmc_fits figure_input
to download the data required to create the papers in the main figure (this is about 20GB). - Run
snakemake -k -j N reports/paper_figures/fig-XX.svg
(whereN
is the number of cores to use in parallel) to recreate a given figure from the paper (note the number must be 0-padded, i.e.,fig-01.svg
, notfig-1.svg
). These will end up in thereports/paper_figures/
directory. Note that they are svgs, a vector file format. If your default image viewer cannot open them, your browser can. They can be converted to pdfs using inkscape or Adobe Illustrator. - If you wish to create all the figures from the main body of the text, run
snakemake -k -j N main_paper_figures
. If one job fails, this will continue to run the others (that's what the-k
flag means).
If you wish to create the figures in the appendix as well:
- Download the additional data required:
python download_data.py stimuli freeman2011_check_output
(this is about 14GB). - Run
snakemake -k -j N reports/paper_figures/fig-AY-XX.svg
(whereXX
must again be 0-padded, butY
does not) to create figureXX
from appendixY
orsnakemake -k -j N appendix_figures
to create all the figures from appendices 1 through 5. - The figures in appendix 6 have been split off because they require an
additional 24GB data set, so to create these:
- Download the additional data:
python download_data.py mcmc_compare
. - Create a single figure with
snakemake -k -j N reports/paper_figures/fig-A6-XX.svg
or all of them withsnakemake -k -j N appendix_figures_mcmc_compare
.
- Download the additional data:
Some notes about the above:
- The workflow for figure creation looks intimidating: because parallelization
is easy, I split up the process into many small jobs. Therefore, there's ~100
jobs for each of the above
main_paper_figures
andappendix_figures
. Don't worry! They generally don't take that much time. - The complete workflow is very long (going back to preparing the images for
metamer synthesis), and so sometimes
snakemake
can take a long time to determine what to run. This problem can get exacerbated if the file modification timestamps get thrown off, so thatsnakemake
thinks it needs to re-create some of the existing files. To limit the search space and forcesnakemake
to only consider figure-related rules, use the includedreports/figure_rules.txt
and the--allowed-rules
flag:cat reports/figure_rules.txt | xargs snakemake -prk main_paper_figures --allowed-rules
. You can pass any argument tosnakemake
, as long as the command ends with--allowed-rules
. - Several of the figures in the paper (e.g., figure 4) include example metamers
or other large images. We link these images into the figure, rather than
embed them, until the very end, in order to reduce file size. Embedding them
requires
inkscape
and an attached display (so it cannot be run on e.g., a compute cluster). You can do all the steps except embedding the large images by appending_no_embed
to the file or target name. So, you would createreports/paper_figures/fig-04_no_embed.svg
rather thanreports/paper_figures/fig-04.svg
to create that single figure, or callsnakemake -k -j N main_paper_figures_no_embed
/snakemake -k -j N appendix_figures_no_embed
to create all of the main paper / appendix figures without embedding.- This allows you to run everything except the embedding on one machine that
may be more powerful but lack a display (such as a compute cluster), and
then finish up on e.g., your laptop. However, the paths used to link
images will almost certainly not work when moving to a different
machine, so if you view
fig-04_no_embed.svg
, you will see empty red squares where the images should go. When embedding the images, we correct the paths, so this is not a problem. - It is possible that
snakemake
will get confused when you switch machines and decide that it wants to re-run steps because the file modification timestamps appear out of order (this might happen, in particular, because ofTEXTURE_DIR
, which is used at the very beginning of the workflow; point it to something old or non-existant to avoid this!). To prevent this, use the same trick as above: append--allowed-rules embed_bitmaps_into_figures main_figures appendix_figures
to anysnakemake
command to ensure that it will only run the embedding rule (and themain_figures
/appendix_figures
rules).
- This allows you to run everything except the embedding on one machine that
may be more powerful but lack a display (such as a compute cluster), and
then finish up on e.g., your laptop. However, the paths used to link
images will almost certainly not work when moving to a different
machine, so if you view
Reproducing someone else's research code is hard and, in all likelihood, you'll run into some problem. If that happens, please open an issue on this repo, with as much info about your machine and the steps you've taken as possible, and I'll try to help you fix the problem.
To understand what the snakemake
command is doing, see the What's going
on?
section I wrote in the readme for another project (here's the zenodo
doi in case that disappears).
I have focused on enabling others to recreate the figures, but you should be able to use this repo to do everything in the paper. In particular, you might want to:
We've put together a website where you can browse all the metamers synthesized for this project, filtering and sorting by their metadata.
If you'd like to bulk download all of them, you can do so from the OSF page, see its README for how they're organized.
I don't recommend using this repo to do this unless you're trying to do exactly what I did (and even so, see here). If you want to synthesize your own metamers, see plenoptic for a better tested, better documented, and more general implementation of metamer synthesis (plus more!).
But if you still want to try to synthesize some metamers using the code in this
repo, download the figure_input
data set and look at the path of the
downloaded metamers. You can use snakemake to create metamers like that, and
most parts of the path are options related to metamer synthesis, see
METAMER_TEMPLATE_PATH
in config.yml
, as well as the create_metamers
rule
in Snakefile
to get a sense for what these are.
You can also use the foveated_metamers.utils.generate_metamer_paths
function
to generate the list of paths that I used for metamers in this experiment (it
can also be called from the command-line: python -m foveated_metamers.utils
).
See the command-line's helpstring or the function's docstring for details, but
here's an example: in order to get the path for one of the energy model metamers
created with the llama target image for each scaling value used in the original
vs. synth white noise comparison, run: python -m foveated_metamers.utils V1 --ref_image llama --seed_n 0
. Note that this prints out the files on a single
line, so you may want to redirect the output to a file for later viewing (append
> mets.txt
) or split it up for easier viewing (append | tr ' ' '\n'
).
To recreate any of my metamers, you'll also need to download the
synthesis_input
data set, which includes the target images we used, as well as
statistics used for normalizing the models' representation.
You should also be aware that the pooling windows are very large once you get
below scaling=0.1
, so I would start with a larger window size. It is also
strongly recommended to use a GPU, which will greatly speed up synthesis.
The OSF project contains a video of a single training
run shown to participants before performing the energy
model original vs. synthesized comparison task. In it, participants view the
metamers for two target images (tiles
and azulejos
) at the smallest and
largest scaling values for this comparison (.063
and .27
), comparing them
against the original image. Participants receive feedback in the training (the
central dot turns green when they answer correctly) and are told their
performance at the end of the run; no feedback was given in the actual
experiment. It was expected that participants would get close to 100% on the
easy trials (large scaling) and close to 50% on the hard trials (small scaling).
If you wish to try the experiment yourself, set up your environment for the
experiment and download the experiment training
tarball: python download_data.py experiment_training
. You can then follow the
instructions in the Training section of this readme (note that you
won't be able to use the example_images.py
script; if you're interested in
this, open an issue and I'll rework it).
Note that the stimuli won't be rescaled by default: unless you have the exact
same experimental set up as me (screen size 3840 by 2160 pixels, viewing
distance of 40cm, 48.5 pixels per degree), you'll need to use the -p
and
-d
flags when calling experiment.py
to specify the size of your screen in
pixels and degrees.
Also note that your monitor should be gamma-corrected to have a linear relationship between luminance and pixel value.
First, Set up your environment for the experiment and
download the stimuli: python download_data.py stimuli
.
You may also want to download the files used in the training and try that out.
For a given model and comparison, the full expeirment consists of 3 sessions, with 5 runs each. A single session lasts about an hour, with small breaks built in between runs, each of which lasts about 10 minutes. Each session contains 5 target images, so that each subject sees 15 of the total 20. All subjects see the first 10, then the final 10 are split into two groups, with even-numbered subjects seeing the first group, odd-numbered the second.
You'll need to generate presentation indices (which define what order the images
are presented in; the ones for the training task are included in their tarball).
To do so, use snakemake: snakemake -prk {DATA_DIR}/stimuli/{model_name}/task-split_comp-{comp}/{subj_name}/{subj_name}_task-split_comp-{comp}_idx_sess-{sess_num}_run-{run_num}.npy
,
where:
{DATA_DIR}
: theDATA_DIR
field from theconfig.yml
file{model_name}
: eitherRGC_norm_gaussian
(for the luminance model) orV1_norm_s6_gaussian
(energy){comp}
: one ofmet
,ref
,met-natural
,ref-natural
orref-downsample-2
. This should match the{comp}
wildcard from the stimulus file you downloaded.{subj_name}
: has the formsub-##
, where##
a 0-padded integer. If this integer lies between 0 and 7 (inclusive), this will be the same presentation order as used in our experiment.{sess_num}
: 0-padded itneger between 0 and 2 (inclusive). The session determines which set of 5 target images are included.{run_num}
: 0-padded integer between 0 and 4 (inclusive). Each run contains 3 target images, so thatrun-01
contains target images{A,B,C}
,run-02
contains{B,C,D}
,run-03
contains{C,D,E}
,run-04
contains{D,E,A}
, andrun-05
contains{E,A,B}
.
You'll probably want to generate all the indices for a subject for a given model
and comparison at once. You can do that by generating the dummy file: snakemake -prk {DATA_DIR}/stimuli/{model_name}/task-split_comp-{comp}/{subj_name}/{subj_name}_task-split_comp-{comp}_idx_tmp.txt
.
Then read the Run experiment section of this readme.
Note that the stimuli won't be rescaled by default: unless you have the exact
same experimental set up as me (screen size 3840 by 2160 pixels, viewing
distance of 40cm, 48.5 pixels per degree), you'll need to use the -p
and
-d
flags when calling experiment.py
to specify the size of your screen in
pixels and degrees.
And note that the above options allow you to run the experiment on a setup that has a different screen size (both in pixels and in degrees) than the intended one, the metamers were created with this specific set up in mind. Things should be approximately correct on a different setup (in particular, double-check that images are cropped, not stretched, when presented on a smaller monitor), but there's no guarantee. If you run this experiment, with these stimuli, on a different setup, my guess is that the psychophysical curves will look different, but that their critical scaling values should approximately match; that is, there's no guarantee that all scaling values will give images that will be equally confusable on different setups, but the maximum scaling value that leads to 50% accuracy should be about the same. The more different the viewing conditions, the less likely that this will hold.
Also note that your monitor should be gamma-corrected to have a linear relationship between luminance and pixel value.
Follow the first three steps in the usage section, so that you have
setup your environment and specified DATA_DIR
. Then, download the behavioral
data: python download_data.py behavioral_data
.
From here, it's up to you. If you'd like to use your own procedure, the OSF
readme describes the most relevant columns of the
behavioral .csv
files.
If you'd like to use my procedure (using MCMC to fit a hierarchical Bayesian
model, fitting all images and subjects simultaneously but each metamer model and
comparison separately), you can do so using snakemake
: snakemake -prk {DATA_DIR}/mcmc/{model_name}/task-split_comp-{comp}/task-split_comp-{comp}_mcmc_{mcmc_model}_step-{step_size}_prob-{accept_prob}_depth-{tree_depth}_c-{num_chains}_d-{num_draws}_w-{num_warmup}_s-{seed}.nc
,
where:
{DATA_DIR}
: theDATA_DIR
field from theconfig.yml
file{model_name}
: eitherRGC_norm_gaussian
(for the luminance model) orV1_norm_s6_gaussian
(energy){comp}
: one ofmet
,ref
,met-natural
,ref-natural
orref-downsample-2
, depending on which comparison you wish to fit.{mcmc_model}
: which hierarchical model to fit to the data.partially-pooled
is used in the main body of the paper, see the Methods section for details on it and appendix 6 for details on the other two. Only themet
andref
comparisons have more than one subject, so the models are interchangeable for the rest.partially-pooled
: fit image-level and subject-level effects, no interaction between them.unpooled
: fit each psychophysical curve separately, no group effects.partially-pooled-interactions
: fit image-level and subject-level effects, plus an interation term.
{seed}
: computational seed for reproducibility, must be an integer.- the rest are all hyperparameters for MCMC, see numpyro documentation and examples for more details.
Note that I've had issues guaranteeing exact reproducibility, even with the same seed. I have not been able to track down why this is.
If you refit the curves using MCMC, you'll want to check the diagnostics,
effective sample size (ESS) and .nc
with
_diagnostics.png
). Vethari et al., 2021
recommend that you look for
You can download my fits to the data (python download_data.py mcmc_fits
for
the partially pooled model, python download_data.py mcmc_compare
for the other
two) or use foveated_metamers.utils.get_mcmc_hyperparams
to see what
hyper-parameters I used to fit each comparison.
The analyses were all run on Linux (Ubuntu, Fedora, and CentOS, several different releases). Everything should work on Macs. For Windows, I would suggest looking into the Windows Subsystem for Linux, as Windows is very different from the others.
The python environment discussed below are required regardless of what you wish to do. There are several additional requirements that are optional:
- If you are using
download_data.py
, you'll needrsync
, which you probably already have. If not, it's probably best to grab it via your OS's package manager. - If you are synthesizing metamers, you'll need
ffmpeg
. This is probably also already installed on your machine, but if not, a static build is probably the easiest way to go. - If you're using GPUs for image synthesis, you'll need the python package
dotlockfile
as well:pip install dotlockfile
. - For embedding images into our figures (the last step of figure creation for
about a third of the figures), you need inkscape,
version equal to or greater than 1.0.2.
- We also need to know the location of your inkscape preference file. The default is probably correct, but see section config.yml for more details.
- Appendix 3 contains a small comparison between the pooling windows used in
this project and those found in Freeman and Simoncelli, 2011. If you wish to
generate these windows yourself, you will need MATLAB along with Jeremy
Freeman's metamer code and the
matlabPyrTools
toolbox. You may also download the windows from the OSF (
python download_data freeman2011_check_output
). See below for more details.
There's a separate python environment for running the environment, so install that if you're planning on running the experiment.
This has been run and tested with version 3.7, unsure about more recent versions.
- Install miniconda with the appropriate python version.
- Install mamba:
conda install mamba -n base -c conda-forge
(I recommend using mamba instead of conda to install the environment because conda tends to hang while attempting to solve the environment). - After cloning this repo, run the following from this directory to grab the git submodules:
git submodule sync git submodule update --init --recursive
- In this directory, run
mamba env create -f environment.yml
- If everything works, type
conda activate metamers
to activate this environment.
As of fall 2022, the packages that I know have version requirements are
specified in environment.yml
. However, in case updates to the dependencies
break something, I've included the output of the pip freeze
command from two
different machines (my laptop and Flatiron's compute cluster) at
reports/pip_freeze_laptop.txt
and reports/pip_freeze_hpc.txt
, so a working
environment is cached (note that this includes more packages than specified in
environment.yml
, because it includes all their dependencies as well).
I am also running tests on github actions to check that the following parts of the workflow run: the data download script, the included notebook, metamer synthesis, the pooling models, and fitting the MCMC curves. For all of these, I'm just testing that they can run, not the full workflow completes or that I get the proper outcome, but that should be sufficient to see what package versions are compatible. (I'm not testing figure creation, because I couldn't come up with a way to do that didn't require more storage than is available for free from Github runners --- if you have a solution for this, I'd love to hear it).
This repo includes one notebook, for examining the differences between this project and Freeman and Simoncelli, 2011. You can view the cached output of the notebook online, or you can install jupyter locally and run the notebook.
There are two main ways of installing jupyter locally:
- Install jupyter in this
metamers
environment:
conda activate metamers
mamba install -c conda-forge jupyterlab
This is easy but, if you have multiple conda environments and want to use Jupyter notebooks in each of them, it will take up a lot of space.
- Use nb_conda_kernels:
# activate your 'base' environment, the default one created by miniconda
conda activate
# install jupyter lab and nb_conda_kernels in your base environment
mamba install -c conda-forge jupyterlab
mamba install nb_conda_kernels
# install ipykernel in the calibration environment
mamba install -n metamers ipykernel
This is a bit more complicated, but means you only have one installation of jupyter lab on your machine.
Install miniconda and mamba as described above (you probably don't need the submodules, but they won't hurt), then run:
mamba env create -f environment-psychopy.yml
Then, to activate, run conda activate psypy
.
PsychoPy provides multiple backends. I'm now using the pyglet
backend, but
I've occasionally had issues with a weird XF86VidModeGetGammaRamp failed
error. That's just something
to be aware of.
This environment-psychopy.yml
file pins all the package versions but I didn't
spend a lot of time trying to figure out what exactly was necessary or worry
about the versions overly-much. That is to say, I think as long as you can get a
working psychopy install with the pyglet
backend, the code should work. So you
could try just to install psychopy
, pyglet
, h5py
(used for storing
behavioral data) and numpy
(stimuli are saved as numpy arrays) and go from
there.
We use images from the authors' personal collection and the UPenn Natural Image Database as the targets for our metamer generation. This is because we need images that are large, linear (i.e., their pixel intensities are proportional to photon count, as you get from an image that has not been processed in any way), and openly-licensed.
- Authors' personal collection:
- WFB: azulejos, tiles, bike, graffiti, llama, terraces
- EPS: ivy, nyc, rocks, boats, gnarled, lettuce
- UPenn Natural Image Database: treetop (cd01A/DSC_0033), grooming (cd02A/DSC_0011), palm (cd02A/DSC_0043), leaves (cd12A/DSC_0030), portrait (cd58A/DSC_0001), troop (cd58A/DSC_0008).
- Unpublished photos from David Brainard: quad (EXPOSURE_ASC/DSC_0014), highway (SNAPSHOTS/DSC_0200).
.tiff
files of all these images can be downloaded from the OSF
page. They have been demosaiced (using DCRAW) but
otherwise untouched from the raw files. For image synthesis they were converted
to 16-bit grayscale png files, cropped / expanded to 2048 by 2600 pixels (Prof.
Brainard's photos and those from the UPenn Natural Image Database were 2014
pixels tall and so a small amount of reflection padding was used on these
photos), and rescaled so all pixel values lay between .05 and .95. These png
files are found in the synthesis_input
tarball on the OSF
page.
The data for this project is available on its OSF page (almost everything) and NYU's Faculty Digital Archive (the files required for appendix 6, which are the fits using the alternative MCMC models). The OSF readme describes its contents in detail.
We also provide the download_data.py
script, which downloads and arranges the
data into the structure that snakemake
expects. The following data sets can be
downloaded with this script:
synthesis_input
: the inputs required for synthesizing model metamers, this includes the original images whose model representation our metamers match and the set of statistics used to normalize model responses.stimuli
: numpy arrays containing the model metamers used as stimuli in our experiment for each model and comparison separately, along with csv files with their metadata.behavioral_data
: csv files containing participant responses, along with metadata (each model and comparison separate).mcmc_fits
:.nc
files containing the fits of the partially pooled hierarchical model to behavior, for each model and comparison separatelyfigure_input
: miscellaneous files not contained in the other data sets required to create the figures in the paper (some of the model metamers, etc.)freeman2011_check_input
,freeman2011_check_output
: the input and output files for a brief comparison between our pooling windows and energy model metamers and those presented in Freeman and Simoncelli, 2011. See below for more details.experiment_training
: some files used to train participants, can also be viewed in order to get a sense for how the experiment was structured. See above for details.mcmc_compare
:.nc
files containing the fits of the alternative MCMC models to behavior, as well as the csv files evaluating each MCMC models performance, used for appendix 6.
To download one of the above, call python download_data.py {TARGET_DATASET}
,
replacing {TARGET_DATASET}
with one of the above. Make sure you have specified
DATA_DIR
in config.yml
first. The script will give you an estimate of how
large each data set is and ask if you would like to continue.
There are several components found on the OSF page that cannot be downloaded
using download_data.py
. These are made available because they might be useful
for others, but I do not expect them to be used by anyone with this repo,
because they are not necessary for reproducing the figures.
This is configuration file containing options used by snakemake
. You need to
modify the first one, DATA_DIR
, and might need to modify the other paths in
that first section. All the other options should be left unmodified. The file is
commented explaining what each of the options are.
Note that, unless stated otherwise, you cannot use ~
in any of the paths in this
file (you must write out the full path to your home directory, e.g.,
/home/billbrod
or /Users/billbrod
). Also, the paths should probably not have
capital letters -- there's a discrepancy between how Mac and Linux handle
capital letters in paths, which might create problems.
Snakefile
: used by snakemake to determine how to create the files for this project. Handles everything except the experiment.foveated_metamers/
: library of functions used in this projectcreate_metamers.py
: creates metamers.stimuli.py
: assembles the various metamer images into format required for running the experiment.distances.py
: finds distance in model space between images in an efficient way.experiment.py
: runs experiment.analysis.py
: basic analyses of behavioral data (gets raw behavioral data into format that can fit by psychophysical curves).curve_fit.py
: fits psychophysical curves to real or simulated data usingpytorch
. We didn't end up using this method of fitting the curves.simulate.py
: simulate behavioral data, for checkingcurve_fit.py
performance, as well as how many trials are required.mcmc.py
: use Markov Chain Monte Carlo (MCMC) to fit a probabilistic model of psychophysical curves withnumpyro
. This is how the curves presented in the paper were fit.statistics.py
: compute some other image statistics (heterogeneity, Fourier amplitude spectra, etc).plotting.py
: plotting functions.figures.py
: creates various figures.compose_figures.py
: combines plots (as created by functions infigures.py
) into multi-panel figures.other_data.py
: functions to fit a line (hinged or not) to the Dacey 1992 data, which gives the receptive field size of retinal ganglion cells. This also usesnumpyro
and so looks fairly similar tomcmc.py
.create_mad_images.py
: synthesize Maximally-Differentiating images (as in Wang and Simoncelli, 2008), to highlight mean-squared error remaining in human metamers.create_other_synth.py
: other ways to synthesize images to highlight mean-squared error remaining in human metamers.observer_model.py
: first steps towards an observer model to predict human performance when images are not metamers. Did not end up making much progress, so this is not present in the paper.utils.py
: various utility functions.style.py
: code for styling the figures.
extra_packages/
: additional python code used by this repo. The bits that live here were originally part of plenoptic, but were pulled out because it's a bad idea for a research project to be so heavily reliant on a project currently under development.pooling-windows
: git submodule that points to this repo, containing the pooling windows we use.plenoptic_part
: contains the models and metamer synthesis code (as well as some utilities) that were pulled out of plenoptic, branching at this commit (I used git filter-repo and so the history should be preserved). While the model code (and some of the utilities) have been deleted fromplenoptic
and are unique to this repo, the synthesis code here is a modified version of the one in plenoptic. If you wish to use synthesis for your own work use the plenoptic version, which is regularly tested and supported.
notebooks/
: jupyter notebooks for investigating this project in more detail.Freeman_Check.ipynb
: notebook checking that our windows are the same size as those from Freeman and Simoncelli, 2011 (and thus that the models' scaling parameter has the same meaning); see below for more details.
examples_images.py
: script to open up some example images to show participants before the experiment (see Training section for how to use).download_data.py
: script to download and arrange data for reproducing results and figures. See Download data for how to use.matlab/
: two matlab scripts using external matlab libraries. Neither are necessary: one is used to generate the windows from the Freeman and Simoncelli, 2011 paper (the output of which can be downloaded usingdownload_data.py
) and the other generates some LGN-like image statistics that we didn't end up using.data/
: contains some data files.Dacey1992_RGC.csv
: csv containing data from figure 2B of Dacey and Petersen, 1992, extracted using WebPlotDigitizer on July 15, 2021. To recreate that figure, use the snakemake ruledacey_figure
. Note that we did not separate the data into nasal field and temporal, upper, and lower fields, as the paper does.checksums.json
: json file containing BLAKE2b hashes for the files downloadable viadownload_data.py
, so we can check they downloaded corectly.
reports/
: contains a variety of figure-related files.figures/
: these are figure components that I use when putting the figures together. They fall into two categories: schematics that are copied as is, with no changes (e.g., image space schematics, experiment schematic), and templates that we embed images into (e.g., the example metamer figures).paper_figures/
: these are the actual figures used in the paper, as created by thesnakemake
file. There are none in the github repo, see Usage section for details on how to create them.figure_rules.txt
: this is a list of snakemake rules that create figures (rather than analyze the data). It can be used to limit snakemake's search of possible analysis paths. See Usage for details on how to use.pip_freeze_laptop.txt
,pip_freeze_hpc.txt
: the outputs ofpip freeze
on two different machines, showing working environments as of fall 2022.
tests/test_models.py
: contains a small number of tests of the pooling models, ran weekly and on every push (alongside other tests).environment-psychopy.yml
,environment.yml
: yml files defining conda environment for the experiment (usingpsychopy
) and for everything. See Setup section for how to use.greene.json
,rusty.json
: json files defining how snakemake should communicate with NYU's and Flatiron's SLURM clusters, respectively (works with the snakemake-slurm profile). I have written a section on how to use snakemake with a SLURM cluster in a readme for a different project, and may write something in more detail at some point. Reach out if you have questions.config.yml
: yml configuration file, defining paths, metamer path template, and some configuration for experiment structure, see here for details.
The following sections contain some extra details that may be useful.
This project uses a modification of the pooling windows first described in
Freeman and Simoncelli, 2011. We include some code to check our reimplementation
of the windows and the extension to use Gaussians instead of raised-cosine
falloffs. Basically, we want to make sure that our windows are the same size --
identical reimplementation is not important, but we want to make sure that the
models' scaling parameter has the same interpretation; it should be the ratio
between the eccentricity and the radial diameter of the windows at half-max
amplitude. To do so, we include a notebook notebooks/Freeman_Check.ipynb
, as
well as some snakemake rules.
We check two things: that our windows' scaling parameter has the same meaning as
that in the original paper, and that our V1 metamers look approximately the
same. You can view this by looking at the Freeman_Check
notebook and its
cached outputs directly. If you wish to run the notebook locally, you'll need to
download the input files (python download_data.py freeman2011_check_input
) and
then either download the files required (python download_data.py freeman2011_check_output
) or generate them yourself (snakemake -prk freeman_check
). Note that generating them yourself will require MATLAB with the
Freeman metamer and
matlabPyrTools
toolboxes (set their paths correctly in config.yml
)
Make sure you have Jupyter installed, then navigate to the
notebooks/
directory on your terminal and activate the environment you install
jupyter into (metamers
or base
, depending on how you installed it), then run
jupyter
and open up the notebook. If you used the nb_conda_kernels
method,
you should be prompted to select your kernel the first time you open a notebook:
select the one named "metamers".
A portion of the results presented in this notebook are also found in one of the paper's appendices.
These are the notes I wrote for myself so I remembered how to run the experiment and could teach others.
To run the experiment, make sure that the stimuli array and presentation indices have been generated and are at the appropriate path. It's recommended that you use a chin-rest or bite bar to guarantee that your subject remains fixated on the center of the image; the results of the experiment rely very heavily on the subject's and model's foveations being identical.
To teach the subject about the experiment, we want to introduce them to the structure of the task and the images used. The first one probably only needs to be done the first time a given subject is collecting data for each model / comparison, the second should be done at the beginning of each session.
(The following paths all assume that DATA_DIR
is ~/Desktop/metamers
, replace
that with your actual path.)
- First, run a simple training run (if you haven't already, first run
python download_data.py experiment_training
to get the required files):conda activate psypy
python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/training_noise/stimuli_comp-{comp}.npy sub-training 0 ; python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/training_{model}/stimuli_comp-{comp}.npy sub-training 0
where{comp}
depends on which comparison you're running, and{model}
isRGC_norm_gaussian
orV1_norm_s6_gaussian
, depending on which you're running.- Explanatory text will appear on screen, answer any questions.
- This will run two separate training runs, both about one or two minutes, with feedback between each trial (the fixation dot will turn green if they answered correctly) and at the end of the run (overall performance).
- The first one will just be comparing natural to noise images and so the subject should get 100%. The goal of this one is to explain the basic structure of the experiment.
- The second will have two metamers, one easy and one hard, for each of two reference images. They should get 100% on the easy one, and about chance on the hard. The goal of this one is to show what the task is like with metamers and give them a feeling for what they may look like.
- Run:
conda activate metamers
python example_images.py {model} {subj_name} {sess_num}
where{model}
isV1
orRGC
depending on which model you're running, and{subj_name}
and{sess_num}
give the name of the subject and number of this session, respectively.- This will open up three image viewers. Each has all 5 reference images the subject will see this session. One shows the reference images themselves, one the metamers with the lowest scaling value, and one the metamers with the highest scaling value (all linear, not gamma-corrected).
- Allow the participant to flip between these images at their leisure, so they understand what the images will look like.
- Note: this will not work unless you have the metamers for this comparison at the locations where they end up after synthesis (you probably don't). If you're interested in using this script, open an issue and I'll try to rework it.
Unlike previous experiments, we use a split-screen task. Each trial lasts 1.4 seconds and is structured like so:
|Image 1 | Blank |Image 2 |Response| Blank |
|--------|--------|--------|--------|--------|
|200 msec|500 msec|200 msec| |500 msec|
Image 1 will consist of a single image divided vertically at the center by a
gray bar. One half of image 2 will be the same as image 1, and the other half
will have changed. The two images involved are either two metamers with the same
scaling value (if comp=met
or met-natural
) or a metamer and the reference
image it is based on (if comp=ref
or ref-natural
). The subject's task is to
say whether the left or the right half changed. They have as long as they need
to respond and receive no feedback.
To run the experiment:
- Activate the
psypy
environment:conda activate psypy
- Start the experiment script from the command line:
python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/{model}/stimuli_comp-{comp}.npy {subj_name} {sess_num}
where{model}, {subj_name}, {sess_num}, {comp}
are as described in the training section.- There are several other arguments the experiment script can take,
run
python foveated_metamers/experiment.py -h
to see them, and see the other arguments section for more information.
- Explain the task to the subject, as given in the example below (similar text will also appear on screen before each run for the participant to read)
- When the subject is ready, press the space bar to begin the task.
- You can press the space bar at any point to pause it, but the pause won't happen until the end of the current trial, so don't press it a bunch of times because it doesn't seem to be working. However, try not to pause the experiment at all.
- You can press q/esc to quit, but don't do this unless truly necessary.
- There will be a break half-way through the block. The subject can get up, walk, and stretch during this period, but remind them to take no more than a minute. When they're ready to begin again, press the space bar to resume.
- The data will be saved on every trial, so if you do need to quit out, all is not lost. If you restart from the same run, we'll pick up where we left off.
- The above command will loop through all five runs for a given session. To do a
particular set of runs pass
-r {run_1} {run_2} ... {run_n}
to the call toexperiment.py
(where{run_1}
through{run_n}
are 0-indexed integers specifying the runs to include). For example, if you quit out on the third run and wanted to finish that one and then do runs 4 and 5, pass:-r 2 3 4
. If you just want to finish that run, you'd only pass-r 2
.
Recommended explanation to subjects:
In this experiment, you'll be asked to complete what we call an "2-Alternative Forced Choice task": you'll view an image, split in half, and then, after a brief delay, a second image, also split in half. One half of the second image will be the same as the first, but the other half will have changed. Your task is to press the left or right button to say which half you think changed. All the images will be presented for a very brief period of time, so pay attention. Sometimes, the two images will be very similar; sometimes, they'll be very different. For the very similar images, we expect the task to be hard. Just do your best!
You'll be comparing natural and synthesized images. The first image can be either natural or synthesized, so pay attention! You will receive no feedback, either during or after the run.
For this experiment, fixate on the center of the image the whole time and try not to move your eyes.
The run will last for about twelve minutes, but there will be a break halfway through. During the break, you can move away from the device, walk around, and stretch, but please don't take more than a minute.
This part will not be shown on screen, and so is important:
You'll complete 5 runs total. After each run, there will be a brief pause, and then the instruction text will appear again, to start the next run. You can take a break at this point, and press the spacebar when you're ready to begin the next run.
The experiment.py
takes several optional arguments, several of which
are probably relevant in order to re-run this on a different
experiment set up:
--screen
/-s
: one integer which indicate which screens to use.--screen_size_pix
/-p
: two integers which indicate the size of the screen(s) in pixels .--screen_size_deg
/-d
: a single float which gives the length of the longest screen side in degrees.
For more details on the other arguments, run python foveated_metamers/experiment.py -h
to see the full docstring.
NOTE: While the above options allow you to run the experiment on a setup that has a different screen size (both in pixels and in degrees) than the intended one, the metamers were created with this specific set up in mind. Things should be approximately correct on a different setup (in particular, double-check that images are cropped, not stretched, when presented on a smaller monitor), but there's no guarantee. If you run this experiment, with these stimuli, on a different setup, my guess is that the psychophysical curves will look different, but that their critical scaling values should approximately match; that is, there's no guarantee that all scaling values will give images that will be equally confusable on different setups, but the maximum scaling value that leads to 50% accuracy should be about the same. The more different the viewing conditions, the less likely that this will hold.
The following is a checklist for how to run the experiment. Print it out and keep it by the computer.
Every time:
- Make sure monitor is using the correct icc profile (
linear-profile
; everything should look weirdly washed out). If not, hit the super key (the Windows key on a Windows keyboard) and typeicc
, open up the color manager and enable the linear profile.
First session only (on later sessions, ask if they need a refresher):
-
Show the participant the set up and show the participants the wipes and say they can use them to wipe down the chinrest and button box.
-
Tell the participant:
In this task, a natural image will briefly flash on screen, followed by a gray screen, followed by another image. Half of that second image will be the same as the first, half will have changed. Your task is to say which half has changed, using these buttons to say "left" or "right". You have as long as you'd like to respond, and you will not receive feedback. There will be a pause halfway through, as well as between runs; take a break and press the center button (labeled "space") to continue when you're ready. You won't press the buttons in the bottom row.
- Train the participant. Say:
Now, we'll do two brief training runs, each of which will last about a minute. In the first, you'll be comparing natural images and noise; the goal is so you understand the basic structure of the experiment. In the second, you'll be comparing those same natural images to some of the stimuli from the experiment; some will be easy, some hard. You'll receive feedback at the end of the run, to make sure you understand the task. I'll remain in the room to answer any questions.
There will be fixation dot in the center of some explanatory text at the beginning, use that to center yourself.
- Run (replace
{model}
withV1_norm_s6_gaussian
orRGC_norm_gaussian
):
conda activate psypy
python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/training_noise/stimuli_comp-ref.npy sub-training 0 ; python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/training_{model}/stimuli_comp-ref.npy sub-training 0
- Answer any questions.
Every time:
- Show the participant the images they'll see this session, replacing
{model}
withV1
orRGC
(no need to use the full name), and{subj_name}
and{sess_num}
as appropriate:
conda activate metamers
python example_images.py {model} {subj_name} {sess_num}
- Say the following and answer any questions:
These are the natural images you'll be seeing this session, as well as some easy and hard stimuli. You can look through them for as long as you'd like.
-
Ask if they have any questions before the experiment.
-
Say:
This will run through all 5 runs for this session. Each should take you 10 to 12 minutes. Come get me when you're done. As a reminder, you have as long as you'd like to respond, and you won't receive any feedback.
- Run, replacing
{model}
,{subj_name}
,{sess_num}
as above:
conda activate psypy
python foveated_metamers/experiment.py ~/Desktop/metamers/stimuli/{model}/stimuli_comp-ref.npy {subj_name} {sess_num}
- When using multiprocessing (as done when fitting the psychophysical curves)
from the command-line, I get
OMP: Error #13: Assertion faliure at z_Linux_util.cpp(2361)
on my Ubuntu 18.04 laptop. As reported here, this is a known issue, and the solution appears to be to set an environmental variable: runningexport KMP_INIT_AT_FORK=FALSE
in the open terminal will fix the problem. Strangely, this doesn't appear to be a problem in a Jupyter notebook, but it does fromIPython
or thesnakemake
calls. I tried to set the environmental variable from within Snakefile, but I can't make that work. Running the calls withuse_multiproc=False
will also work, though it will obviously be much slower. - When trying to use the
embed_bitmaps_into_figure
rule on a drive mounted usingrclone
(I had my data stored on a Google Drive that I was usingrclone
to mount on my laptop), I got a'Bad file descriptor'
error from python when it tried to write the snakemake log at the end of the step. It appears to be this issue, adding the--vfs-cache-mode writes
flag to therclone mount
command worked (though I also had to give myself full permissions on the rclone cache folder:sudo chmod -R 777 ~/.cache/rclone
). - As of June 2023, there appears to be some issue with svgutils and inkscape,
where any svg file that is the output of my
compose_figures
rule cannot be opened by inkscape (attempting to do so leads inkscape to immediately crash). This means that none of the files in thecompose_figures
directory can be used as inputs to theembed_bitmaps_into_figures
rule (but those infigures
can be). I'm unclear why this is happening now, but have been unable to track it down.
The intention of sharing this code is to allow for the reproduction of the figures in the resulting paper. This is the code I used to synthesize the metamers used in the experiment, and you can use it to do so, but there are a couple things you should be aware of:
- Results will not be identical on CPUs and GPUs. See PyTorch's notes on this.
- I used stochastic weight averaging (SWA) for the energy model metamers. SWA
seems to reduce the final loss by averaging metamer pixel values as we get
near convergence (see
here
for more details). However, the version of SWA I used to generated the
metamers for the experiment was from
torchcontrib
, which was archived in 2020 and is no longer maintained (github repo). In May 2022, I noticed that the torchcontrib SWA implementation no longer worked on my tests with the most recent versions of python (3.10) and pytorch (1.12), so I updated my code to work with the pytorch SWA implementation. The resulting metamers are not identical to the ones produced before, but they are similar in both visual quality and loss, and I believe they would be indistinguishable in a 2AFC task. - The metamer synthesis code found here (in
extra_packages/plenoptic_part
) was very much a work in progress throughout this whole project and ended up becoming a tangled rats nest, as is the case for most research code.
For all the above reasons, I am sharing the synthesized metamers used in this experiment and recommend you use them directly if you need the exact images I used (to replicate my behavioral results, for example). If you wish to synthesize new metamers, whether using your own model or even using the ones from this paper, I strongly recommend you use the metamer synthesis code found in plenoptic, which is actively maintained and tested, though it is not identical to the procedure used here. Most important, it does not include a SWA implementation and probably will never include one, but I would be happy to help come up with how to add it in an extension or a fork.
If you would like to generate your own metamers, see plenoptic, a python library for image synthesis, including metamers, MAD Competition, eigendistortions, and geodesics.
If you would like to use the pooling windows, see
pooling-windows.
This includes pytorch implementations of the Gaussian windows from this project,
as well as the raised-cosine windows from Freeman and Simoncelli, 2011. The
README describes how to use them for creating a version of the pooled luminance
and energy models used in this project. Feel free to use the models in this
repo, but the simpler version from that README may better suit your needs. The
version in this repo includes a bunch of helper code, including for creating
plots and the starts of paths not taken. The only important thing missing from
the pooling-windows
repo is normalization -- look for the normalize_dict
attribute in extra_packages/plenoptic_part/simulate/ventral_stream.py
to see
how I implemented that, and feel free to reach out if you have trouble.
You may also be interested in the code used by two other papers that this project references a lot, Freeman and Simoncelli, 2011 and Wallis et al., 2019. If you wish to use the Freeman code, note the bug in window creation pointed out by Wallis et al. (discussed in their appendix 1).
If you use the data, code, or stimuli from this project in an academic
publication, please cite the
preprint. If you use the code,
please additionally cite the zenodo
doi for the corresponding release
(e.g., v1.0-biorxiv
corresponds to the DOI
https://doi.org/10.5281/zenodo.7948552
).
-
Dacey, D. M., & Petersen, M. R. (1992). Dendritic field size and morphology of midget and parasol ganglion cells of the human retina. Proceedings of the National Academy of Sciences, 89(20), 9666–9670. http://dx.doi.org/10.1073/pnas.89.20.9666
-
Freeman, J., & Simoncelli, E. P. (2011). Metamers of the ventral stream. Nature Neuroscience, 14(9), 1195–1201. http://dx.doi.org/10.1038/nn.2889
-
Vehtari, A., Gelman, A., Simpson, D., Carpenter, B., & Paul-Christian B"urkner (2021). Rank-normalization, folding, and localization: an improved
$R$ for assessing convergence of mcmc (with discussion). Bayesian Analysis, 16(2), 667–718. http://dx.doi.org/10.1214/20-BA1221 -
Wallis, T. S., Funke, C. M., Ecker, A. S., Gatys, L. A., Wichmann, F. A., & Bethge, M. (2019). Image content is more important than bouma's law for scene metamers. eLife, 8(), . http://dx.doi.org/10.7554/elife.42512
-
Wang, Z., & Simoncelli, E. P. (2008). Maximum differentiation (MAD) competition: A methodology for comparing computational models of perceptual discriminability. Journal of Vision, 8(12), 1–13. http://dx.doi.org/10.1167/8.12.8