rcicr implements reverse correlation image classification, a psychophysics technique for visualizing mental representations, for example of faces. It generates noise-based stimuli for two-image forced-choice (2IFC) tasks. From participants’ responses it then computes “classification images”, which show the visual features that drove their choices.
Installation
Install the current release from CRAN:
install.packages('rcicr')Install from GitHub to reproduce an analysis with a specific release, or to try the unreleased development version:
install.packages('remotes')
# A specific release, by tag
remotes::install_github('rdotsch/rcicr@vX.Y.Z')
# The development version at the tip of main. Record its commit SHA.
remotes::install_github('rdotsch/rcicr')
# Reinstall that exact development snapshot later
remotes::install_github('rdotsch/rcicr@<commit-sha>')Every release is tagged; the releases page lists them. Record the version you ran in your analysis script, and install that tag when you come back to the analysis. For an unreleased GitHub install, record the commit SHA instead and install that. A classification image is only reproducible with the exact code that computed it. Any release that changes numeric output says so in NEWS.md under “Reproducibility impact”.
Saved per-participant classification images with a GitHub install before 1.3.0? Check their filenames. A direct
generateCI(participants = ..., save_individual_cis = TRUE)call could save an image under another participant’s name whenever participants were not in sorted order, as withp1top10in collection order. The images are correct; only their names can be wrong. The individual-CI filename advisory explains how to tell whether you are affected and how to rename the files. CRAN releases never carried this bug.
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",
targetpath = "./cis" # where to write the CI PNG
)Every function that writes files needs its destination spelled out: stimulus_path, targetpath and zmaptargetpath have no defaults, so nothing is ever written to a directory you did not name. Pass save_as_png = FALSE to compute a classification image without writing anything.
Documentation
The function reference, the vignettes and the changelog are also online at https://rdotsch.github.io/rcicr/, so you can read them before installing.
Four vignettes ship with the package:
vignette("getting-started", package = "rcicr") # shortest working example
vignette("reverse-correlation-walkthrough", package = "rcicr") # the full method
vignette("recipes", package = "rcicr") # answers to common follow-up questions
vignette("stored-data", package = "rcicr") # every field of the .Rdata file and of a CIThe 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 whenever the package is built, so it keeps working with the current version. The recipes cover noise-only stimuli, rating scales as responses, matching the informational-value reference to dropped trials, masks, batches and group averages, and reproducing a number from an earlier version.
For example datasets and analysis scripts, see rcicr_examples.
How it works
The package has two halves. They run at different times, often months apart, and share nothing 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. It is built once and reused for every trial. generateNoiseImage() combines one random contrast weight per patch into a single noise image. generateStimuli2IFC() repeats that for every trial and writes two PNGs per trial per base face: the base image with the noise added, and with it subtracted.
2. Analysis. generateCI() loads the stimulus file and looks up the parameters of the stimuli a participant saw. It weights each by the response (1 = original chosen, -1 = inverted chosen) and averages them into one image: the classification image. After that, autoscale() puts a batch of CIs on one scale so they can be compared by eye, computeInfoVal2IFC() scores a CI against a simulated null distribution (batchComputeInfoVal2IFC() scores a list of them), and plotZmap() shows which regions carry reliable signal.
The .Rdata file is the only link between the two halves. Without it, your stimuli can only be regenerated from the seed together with every generation setting, which vignette("recipes", package = "rcicr") shows how to do and check. Back it up with your response data and keep it with anything you publish. Recomputing a classification image years later needs this file and nothing else. vignette("stored-data", package = "rcicr") lists everything in it, and in the classification image generateCI() returns.
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 macOS ARM64 alike. The one exception is the PNG that plotZmap() writes, because it is 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 gives figures that look identical but 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(), so this does not apply to them. See ?plotZmap.
Citation
citation("rcicr") gives the reference for the software. If you use the technique, also cite the method: Dotsch and Todorov (2012), and for a practical primer Brinkman, Todorov and Dotsch (2017).
Contributing
Contributions, thoughts and criticisms are welcome: open an issue. CONTRIBUTING.md explains how to set up a development environment, run the tests and open a pull request.
