
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.
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@v1.3.0')
# 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 before 1.3.0? Check their filenames. With
generateCI(participants = ..., save_individual_cis = TRUE), an image could be saved under another participant’s name. This happened wherever the order in which participants appear in your data differs from their sorted order. For text IDs that includes the ordinary case ofp1top10in collection order, which sort asp1, p10, p2, .... The images themselves are correct, so the fix is renaming files, not re-running anything. The individual-CI filename advisory explains how to tell whether you are affected, what it did to an analysis, and how to recover.NEWS.mdhas a shorter version under “Reproducibility impact”, and this note helps you work out which version a stored analysis actually ran.batchGenerateCI(),batchGenerateCI2IFC()andgenerateCI2IFC()were never affected.
Version 1.3.0 returned rcicr to CRAN. The package had
been archived in 2021 because email to an old maintainer address
bounced; nothing was wrong with the package itself.
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.
The function reference, both vignettes and the changelog are also online at https://rdotsch.github.io/rcicr/, so you can read them before installing.
Two vignettes ship with the package:
vignette("getting-started", package = "rcicr") # shortest working example
vignette("reverse-correlation-walkthrough", package = "rcicr") # the full methodThe 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.
For example datasets and analysis scripts, see rcicr_examples.
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, and plotZmap() shows which regions carry
reliable signal.
The .Rdata file is the only link between the two
halves. Without it, nothing about your stimuli can be
recovered: not from the PNGs, and not from the seed alone. 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.
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.
This section is for reading the source. Apart from
generateCI() itself, every function in the table below is
internal: not exported, so your scripts cannot call it.
For the public functions, see the function reference or
help(package = "rcicr").
The usual starting point is generateCI(). Its body is
one call per step (validate, load, select, compute, present, return),
and the steps live in these files, grouped by concern:
| file | what is in it |
|---|---|
R/generateCI.R |
generateCI() itself, plus the presentation helpers
hasMask(), applyMask(),
applyScaling(), combine(),
saveToImage() |
R/rdata.R |
reading and guarding .Rdata files:
loadStimulusParams(), captureArgs(),
rdataWriterNote() |
R/ci-inputs.R |
turning the caller’s arguments into a parameter matrix:
coerceTrialVectors(), selectBaseImage(),
aggregateResponses(),
selectStimulusParams() |
R/ci-compute.R |
computeParticipantCIs(): one CI per participant, plus
their average |
R/zmap-compute.R |
computeZmapQuick() and
computeZmapTTest() |
R/parallel.R |
the foreach backend: default_ncores(),
startBackend(), progressOption(),
stopClusterSafely() |
The mask helpers live in R/generateCI.R rather than in a
file of their own because plotZmap() uses them too: masking
a z-map and masking a CI are the same operation.
.Rdata
filegenerateStimuli2IFC() writes one file named
<label>_seed_<seed>_time_<timestamp>.Rdata.
load() it and you get these objects:
| 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 the 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, for reference. The InfoVal reference
distribution reads the saved p (or s in old
files) and stimuli_params directly and uses
n_trials to select trial rows; it never rebuilds the basis
from these settings. |
seed |
The RNG seed. Regenerating the stimuli from it also needs the same
generation settings and the same RNGkind(), which the file
does not record. |
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. |
The first time you compute an informational value,
computeInfoVal2IFC() and
generateReferenceDistribution2IFC() add
fields to the same file. Which ones depends on whether the base images
share one parameter matrix:
| Object | What it is |
|---|---|
reference_norms |
The simulated null distribution: the norms of iter
classification images built from random responses. Stored because
simulating it is slow. Written when the base images share one parameter
matrix. |
reference_norms_seed |
The response_seed those norms were drawn with, or
NULL for the default stream. Added in 1.2.0. |
reference_norms_source |
What reference_norms was built from:
"saved_noise" means the file’s own saved parameters and
basis. A default-stream reference (reference_norms_seed
absent or NULL) without this marker and a matching
reference_norms_fingerprint is rebuilt and, if the file is
writable, saved. A reference drawn with a response_seed is
kept; if it was built from an incorrect reconstruction, regenerate it
yourself before recomputing InfoVal. Added in 1.4.0. |
reference_norms_fingerprint |
A full copy of the norms, as list(norms = ...),
compared with identical(). It exists because an older rcicr
can keep the marker while replacing the norms; if the copy no longer
matches, a default-stream reference is rebuilt. It adds about 80 KB for
10,000 norms before compression. Added in 1.4.0. |
reference_norms_by_base |
Written instead of the fields above when the base images have
different parameter matrices, because each base then needs a
null built from its own saved noise. A named list, one entry per base,
each holding that base’s norms, response_seed,
source and fingerprint (same meaning as
above). A reference_norms already in such a file is left in
place and ignored. Added in 1.4.0. |
Before you write code against this file:
nscales and sigma arrived in 1.1.0 and
noise_type earlier. The reference distribution uses the
saved basis, so it works when those fields are missing.generator_version is unreliable in older
files. It was hardcoded as '0.4.0' until 1.2.0, so
every file written by 0.4.0 through 1.1.0 claims to be 0.4.0.
p$generator_version has always held the real value. Compare
versions with numeric_version(), never as text.On a fresh Ubuntu machine without a compiler or R package library,
tools/dev-setup.sh sets both up; see CONTRIBUTING.md
→ “Getting set up”.
devtools::load_all() # load the package for interactive development
devtools::test() # run the test suite
devtools::check() # full CRAN-style checkContributions, thoughts and criticisms are welcome: open an issue.
GPL-2