Skip to content

Repository files navigation

rcicr

CRAN status R-CMD-check

rcicr implements reverse correlation image classification, a technique from psychophysics for visualizing internal mental representations (for example, of faces). It generates noise-based stimuli for two-image-forced-choice (2IFC) perceptual tasks, and computes "classification images" from participants' responses that reveal what visual features drove their choices.

Installation

Install from GitHub:

install.packages('remotes')
remotes::install_github('rdotsch/rcicr')

install.packages('rcicr') does not currently work. The package was archived on CRAN on 2021-06-08 because email to the maintainer had become undeliverable — an old university address that stopped working. The code was never the problem, and the maintainer address has since been updated. Returning to CRAN is being worked on; until then, GitHub is the only source. The last CRAN release was 0.3.4.1, which is several years behind.

Quick example

A minimal 2IFC workflow: generate stimuli from a base face, then turn collected responses into a classification image.

library(rcicr)

# 1. Generate stimuli: writes an original + inverted noise-blended PNG per
#    trial to stimulus_path, plus an .Rdata file that later analysis needs.
generateStimuli2IFC(
  base_face_files = list(face = "path/to/base_face.jpg"),
  n_trials        = 770,
  img_size        = 512,
  stimulus_path   = "./stimuli",
  seed            = 1
)

# 2. After running the task and collecting responses (1 = original chosen,
#    -1 = inverted chosen), compute the classification image:
generateCI(
  stimuli   = 1:770,                # stimulus numbers, in presentation order
  responses = my_responses,         # 1 / -1 vector, same order as `stimuli`
  baseimage = "face",               # key used in base_face_files above
  rdata     = "./stimuli/rcic_seed_1_time_....Rdata"
)

Documentation

Two vignettes ship with the package:

vignette("getting-started", package = "rcicr")  # shortest working example
vignette("reverse-correlation-walkthrough", package = "rcicr")  # the full method

The walkthrough covers designing a study, generating stimuli, computing classification images for several participants, choosing a scaling method, and telling signal from noise. Its code runs when the package is built, so it cannot drift out of date.

For example datasets and analysis scripts, see rcicr_examples. There is also an older Medium post covering similar ground; the vignette above supersedes it and is the version kept current with the code.

How it works

The package is two halves that run at different times — often months apart — and share no state except one file on disk.

base face image(s) ─┐
                    ├─> generateStimuli2IFC() ─> stimulus PNGs + <label>_seed_<n>_time_<ts>.Rdata
     random noise ──┘                                        │
                                                             │  (run your experiment)
                             participant responses ──────────┤
                                                             ▼
                                       generateCI() / generateCI2IFC() ──> classification image
                                                             │
                                        ┌────────────────────┼────────────────────┐
                                        ▼                    ▼                    ▼
                                  autoscale()      computeInfoVal2IFC()      plotZmap()

1. Stimulus generation. generateNoisePattern() builds the noise basis — a stack of sinusoid (or Gabor) patches at several orientations, phases and spatial scales. This is built once and reused for every trial. generateNoiseImage() then combines one random contrast weight per patch into a single noise image, and generateStimuli2IFC() runs that loop over trials, writing two PNGs per trial per base face: the noise blended with the base image, and its inverted counterpart.

2. Analysis. generateCI() loads the stimulus file, looks up the parameters of the stimuli a participant actually saw, weights each by their response (1 = original chosen, -1 = inverted chosen), and averages them into one image — the classification image. From there, autoscale() makes a batch of CIs visually comparable, computeInfoVal2IFC() scores one against a simulated null distribution, and plotZmap() shows which regions carry reliable signal.

The .Rdata file is the only link between the two halves. Nothing about your stimuli is recoverable without it — not from the PNGs, not from the seed alone. Back it up with your response data, and keep it alongside anything you publish: recomputing a classification image years later needs this file and nothing else.

Compare numbers, not figures, across machines. Classification images, scaling, informational value and z-scores are ordinary R arithmetic and do not depend on your operating system — the test suite pins them to fixed values and they hold on Linux and on macOS ARM64 alike. The one exception is the PNG written by plotZmap(), the only function here that draws through a graphics device: devices differ between platforms in colour management and in whether they write an alpha channel, so the same z-map yields visibly identical figures whose files are not byte-identical. A z-map image that differs pixel-for-pixel on a colleague's machine is not a different result. Every other PNG the package writes comes straight from the pixel array via png::writePNG() and is unaffected. See ?plotZmap.

Anatomy of the .Rdata file

generateStimuli2IFC() writes one file named <label>_seed_<seed>_time_<timestamp>.Rdata. load() it and you get these objects (sizes shown for a 3-trial, 32px, nscales = 2 example):

Object What it is
p The noise basis. A list of patches (an img_size × img_size × 12·nscales array of sinusoid/Gabor layers), patchIdx (which parameter drives each pixel of each layer), noise_type, and generator_version. This is the expensive part and the reason the file exists.
stimuli_params Named list, one entry per base image, each an n_trials × nparams matrix of contrast weights in [-1, 1]. Row i is the noise of stimulus i — this is what generateCI() looks up and weights by responses.
base_faces Named list of the base images as greyscale matrices, after contrast maximization. The actual pixels, not paths, so the file is self-contained.
base_face_files The paths they were read from, for reference.
img_size, n_trials, nscales, sigma, noise_type The generation parameters. generateReferenceDistribution2IFC() re-reads these to rebuild the same noise basis when simulating a null distribution, so they must describe the stimuli exactly.
seed The RNG seed. Regenerating with the same seed and parameters reproduces the identical stimulus set.
use_same_parameters Whether every base image shared one parameter set (TRUE) or each got its own.
label, stimulus_path What the files were called and where they were written.
generator_version The rcicr version that wrote the file — see the caveat below.
trial A leftover loop counter, equal to n_trials. Carries no information; ignore it.

computeInfoVal2IFC() and generateReferenceDistribution2IFC() add two more fields to the same file the first time you compute an informational value:

Object What it is
reference_norms The simulated null distribution — the norms of iter classification images built from random responses. Cached here because simulating it is expensive.
reference_norms_seed The response_seed those norms were drawn with (NULL for the default stream). Added in 1.2.0.

Two things worth knowing before you write code against this file:

  • The contract is append-only. Fields get added across versions and are never renamed or repurposed, so newer rcicr reads older files. The converse does not hold: nscales and sigma were only added in 1.1.0, and noise_type earlier still, so functions warn rather than guess when reading a file that predates them.
  • generator_version is unreliable on older files. It was a hardcoded '0.4.0' string until 1.2.0, so any file written between 0.4.0 and 1.1.0 claims to be 0.4.0 whatever wrote it. p$generator_version has always held the real value. Compare versions with numeric_version() semantics, never as text.

Development

devtools::load_all()   # load the package for interactive development
devtools::test()       # run the test suite
devtools::check()      # full CRAN-style check

Contributing

Contributions, thoughts, and criticisms are very welcome — please open an issue.

License

GPL-2

About

Reverse correlation image classification using R

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages