rcicr

CRAN status R-CMD-check Documentation

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@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 of p1 to p10 in collection order, which sort as p1, 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.md has a shorter version under “Reproducibility impact”, and this note helps you work out which version a stored analysis actually ran. batchGenerateCI(), batchGenerateCI2IFC() and generateCI2IFC() 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.

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, 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 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 whenever the package is built, so it keeps working with the current 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, 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.

Where the code lives

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.

Anatomy of the .Rdata file

generateStimuli2IFC() 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:

Development

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 check

Contributing

Contributions, thoughts and criticisms are welcome: open an issue.

License

GPL-2