rcpyci 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rcpyci/__init__.py ADDED
File without changes
rcpyci/core.py ADDED
@@ -0,0 +1,420 @@
1
+ """
2
+ File Name: core.py
3
+ Description: object to object part of the library, for any interaction and interfacing with files, check interface.py
4
+ """
5
+ import inspect
6
+ import math
7
+ import os
8
+ import random
9
+ from typing import Callable, List, Tuple
10
+
11
+ import numpy as np
12
+ from tqdm import tqdm
13
+
14
+ from .im_ops import combine, get_image_size
15
+ from .utils import get_extension_from_decorator, hash_inputs
16
+
17
+
18
+ def compute_ci(base_image: np.ndarray,
19
+ responses: np.ndarray,
20
+ stimuli_params: np.ndarray = None,
21
+ patches: np.ndarray = None,
22
+ patch_idx: np.ndarray = None,
23
+ anti_ci: bool = False,
24
+ n_trials: int = 770,
25
+ n_scales: int = 5,
26
+ gabor_sigma: int = 25,
27
+ noise_type: str = 'sinusoid',
28
+ seed: int = 1):
29
+ """
30
+ Computes classification image (CI) from given base image and stimulus responses.
31
+
32
+ Parameters:
33
+ base_image: The original base image.
34
+ responses: The responses corresponding to each pair of stimulus images.
35
+ stimuli_params: Optional parameters related to the stimuli. Default is None.
36
+ patches: Optional patches used for computing CI. Default is None.
37
+ patch_idx: Index of the patches being considered. Default is None.
38
+ anti_ci: Whether to compute anti-contrastive information (False by default).
39
+ n_trials: Number of trials to consider while computing CI. Defaults to 770.
40
+ n_scales: Number of scales to consider while computing CI. Defaults to 5.
41
+ gabor_sigma: Sigma value for the Gabor filter used in CI computation. Defaults to 25.
42
+ noise_type: Type of noise to add (e.g., "sinusoid"). Default is "sinusoid".
43
+ seed: Random seed for reproducibility. Default is 1.
44
+
45
+ Returns:
46
+ ci: The computed CI image.
47
+
48
+ Notes:
49
+ The function uses the `responses` and `base_image` as input parameters
50
+ and then computes CI based on these inputs. If `stimuli_params`, `patches`, or
51
+ `patch_idx` are provided, they will be used in the computation; otherwise,
52
+ default values will be used.
53
+ """
54
+ img_size = get_image_size(base_image)
55
+
56
+ if stimuli_params is None:
57
+ stimuli_params = __generate_stimuli_params(n_trials, n_scales, seed=seed)
58
+ if patches is None or patch_idx is None:
59
+ patches, patch_idx = __generate_noise_pattern(img_size=img_size, noise_type=noise_type, n_scales=n_scales, gabor_sigma=gabor_sigma)
60
+
61
+ if anti_ci:
62
+ stimuli_params = -stimuli_params
63
+
64
+ return __generate_ci_noise(stimuli_params, responses, patches, patch_idx)
65
+
66
+ def process_pipelines(base_image: np.ndarray,
67
+ responses: np.ndarray,
68
+ pipelines: List[Tuple[Callable, dict]],
69
+ pipeline_id: str,
70
+ experiment_path: str,
71
+ stimuli_params: np.ndarray = None,
72
+ patches: np.ndarray = None,
73
+ patch_idx: np.ndarray = None,
74
+ n_trials: int = 770,
75
+ n_scales: int = 5,
76
+ gabor_sigma: int = 25,
77
+ noise_type: str = 'sinusoid',
78
+ seed: int = 1):
79
+ """
80
+ Process multiple pipelines on a base image, responses and algorithm internals.
81
+
82
+ This function processes each pipeline in the list of tuples, where each tuple
83
+ contains a callable and its keyword arguments. The processed results from each
84
+ pipeline are returned as a list.
85
+
86
+ Parameters:
87
+ base_image: The original base image.
88
+ responses: The responses corresponding to each pair of stimulus images.
89
+ pipelines: A list of tuples, where each tuple contains a callable and its keyword arguments.
90
+ id: A unique identifier for the pipeline. Usually representing a split on participant or condition.
91
+ path: The path where the results should be saved.
92
+ stimuli_params: Optional parameters related to the stimuli. Default is None.
93
+ patches: Optional patches used for computing CI. Default is None.
94
+ patch_idx: Index of the patches being considered. Default is None.
95
+ n_trials: Number of trials to consider while computing CI. Defaults to 770.
96
+ n_scales: Number of scales to consider while computing CI. Defaults to 5.
97
+ gabor_sigma: Sigma value for the Gabor filter used in CI computation. Defaults to 25.
98
+ noise_type: Type of noise to add (e.g., "sinusoid"). Default is "sinusoid".
99
+ seed: Random seed for reproducibility. Default is 1.
100
+
101
+ Returns:
102
+ results: A list of processed results from each pipeline.
103
+
104
+ Notes:
105
+ This function processes the pipelines sequentially, using the `responses` and
106
+ `base_image` as input parameters for each pipeline. If `stimuli_params`, `patches`, or
107
+ `patch_idx` are provided, they will be used in the computation; otherwise,
108
+ default values will be used.
109
+ """
110
+ # We are also adding img_size to local kwargs, so that we can avoid recomputing it in subsequent steps
111
+ # This will ease computing hashes for input params which calculates on *ALL* input parameters
112
+ img_size = get_image_size(base_image)
113
+
114
+ # if stimuli_params is passed, no need to recompute it
115
+ if stimuli_params is None:
116
+ stimuli_params = __generate_stimuli_params(n_trials, n_scales, seed=seed)
117
+ if patches is None or patch_idx is None:
118
+ patches, patch_idx = __generate_noise_pattern(img_size=img_size, n_scales=n_scales, noise_type=noise_type, gabor_sigma=gabor_sigma)
119
+
120
+ kwargs = locals()
121
+ results = []
122
+
123
+ for (pipeline, pipeline_kwargs) in pipelines:
124
+ kwargs.update(pipeline_kwargs)
125
+
126
+ # Compute hash of relevant inputs
127
+ sig = inspect.signature(pipeline)
128
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters}
129
+ input_hash = hash_inputs(filtered_kwargs)
130
+
131
+ # Determine cache filename
132
+ if 'use_cache' in kwargs and kwargs['use_cache']:
133
+ ext = get_extension_from_decorator(pipeline)
134
+ if ext:
135
+ cache_filename = f"{kwargs['pipeline_id']}_{input_hash}.{ext}"
136
+ filtered_kwargs['cache_path'] = os.path.join(kwargs['experiment_path'], kwargs['save_folder'], cache_filename)
137
+
138
+ result = pipeline(**filtered_kwargs)
139
+ # Update kwargs with pipeline step result
140
+ kwargs.update(result)
141
+ # Append pipeline step results to result list
142
+ results.append(result)
143
+
144
+ return results
145
+
146
+ def __generate_ci_noise(stimuli, responses, patches, patch_idx):
147
+ """
148
+ Generate CI noise for the given stimuli parameters, responses, patches, and patch indices.
149
+
150
+ This function generates weighted CI noise by multiplying the stimuli parameters with the corresponding responses.
151
+ The resulting weighted CI noise is then processed using the provided post-processing pipeline.
152
+
153
+ Parameters:
154
+ - stimuli_params: A 1D NumPy array containing the stimuli parameters.
155
+ - responses: A 2D NumPy array where each row represents a response to the stimuli.
156
+ - patches: A 2D NumPy array representing the noise pattern patches.
157
+ - patch_idx: A 1D NumPy array containing the indices of the noise pattern patches.
158
+
159
+ Returns:
160
+ - The generated CI noise as a 2D NumPy array.
161
+ """
162
+ weighted = stimuli * responses
163
+ if weighted.ndim == 1:
164
+ params = weighted
165
+ else:
166
+ params = weighted.mean(axis=0)
167
+ return __generate_individual_noise_stimulus(params, patches, patch_idx)
168
+
169
+ def __generate_individual_noise_stimulus(params, patches, patch_idx):
170
+ """
171
+ Generate individual noise stimuli for CI computation.
172
+
173
+ This function generates individual noise stimuli based on the given parameters.
174
+ It takes in the following inputs:
175
+ - `params`: The weighted stimuli array
176
+ - `patches`: The noise pattern array
177
+ - `patch_idx`: The indices of the patches
178
+
179
+ Returns: A 2D array representing the individual noise stimuli
180
+ """
181
+ patch_indices = patch_idx.astype(int)
182
+ pd = patches.shape
183
+ patch_params = params[patch_indices].reshape(pd)
184
+ reshaped_matrix = (patches * patch_params).reshape((pd[0]*pd[1], pd[2]))
185
+ noise = np.mean(reshaped_matrix, axis=1).reshape(pd[:2])
186
+ return noise
187
+
188
+ def __generate_coordinate_meshgrid_for_patch(cycles, patch_size):
189
+ """
190
+ Generate a coordinate meshgrid for a given patch size.
191
+
192
+ Parameters:
193
+ cycles (int): The number of cycles.
194
+ patch_size (int): The size of the patch.
195
+
196
+ Returns:
197
+ X (numpy array): The x-coordinates of the meshgrid.
198
+ Y (numpy array): The y-coordinates of the meshgrid.
199
+ """
200
+ x, y = np.ogrid[0:cycles:patch_size*1j, 0:cycles:patch_size*1j]
201
+ X, Y = np.broadcast_arrays(x.T, y.T)
202
+ return X, Y
203
+
204
+ def __generate_sinusoid(patch_size: int, cycles: float, angle: float, phase: float, contrast: float):
205
+ """
206
+ Generate a sinusoidal pattern for the noise stimulus.
207
+
208
+ Parameters:
209
+ patch_size (int): The size of the patches.
210
+ cycles (float): The number of cycles in the sine wave.
211
+ angle (float): The angle of the sine wave in radians.
212
+ phase (float): The phase shift of the sine wave.
213
+ contrast (float): The contrast level of the sine wave.
214
+
215
+ Returns:
216
+ A 2D array representing the sinusoidal pattern.
217
+ """
218
+ X, Y = __generate_coordinate_meshgrid_for_patch(cycles, patch_size)
219
+ angle = math.radians(angle)
220
+ sinepatch = X * math.cos(angle) + Y * math.sin(angle)
221
+ sinusoid = (sinepatch * 2 * math.pi) + phase
222
+ sinusoid = contrast * np.sin(sinusoid)
223
+ return sinusoid
224
+
225
+ def __generate_gabor(patch_size:int, cycles:float, angle:float, phase:float, gabor_sigma:float, contrast:float):
226
+ """
227
+ This code defines a function that generates Gabor patches with varying parameters.
228
+ The `__generate_gabor` function takes in the patch size, cycles, angle, phase, and contrast as inputs,
229
+ and returns a 2D array representing the Gabor pattern.
230
+
231
+ Args:
232
+ patch_size (int): The size of the Gabor patch
233
+ cycles (float): The number of cycles for the sinusoid
234
+ angle (float): The angle of rotation for the sinusoid
235
+ phase (float): The phase shift for the sinusoid
236
+ contrast (float): The amplitude of the sinusoid
237
+
238
+ Returns:
239
+ A 2D array representing the Gabor pattern.
240
+
241
+ """
242
+ sinusoid = __generate_sinusoid(patch_size, cycles, angle, phase, contrast)
243
+ x0 = np.linspace(-0.5, 0.5, patch_size)
244
+ X, Y = np.meshgrid(x0, x0)
245
+ gauss_mask = np.exp(-((X ** 2 + Y ** 2) / (2 * (gabor_sigma / patch_size) ** 2)))
246
+ return gauss_mask * sinusoid
247
+
248
+ def __generate_scales(img_size: int = 512):
249
+ """
250
+ Generate scales for an image of size 'img_size'.
251
+
252
+ This function is used to generate the scales for a Gabor patch. It returns
253
+ an array where each element corresponds to the scale of a Gabor patch.
254
+
255
+ Parameters:
256
+ img_size (int): The size of the image.
257
+
258
+ Returns:
259
+ An array containing the scales for each Gabor patch.
260
+ """
261
+ x, y = np.meshgrid(np.arange(1, img_size + 1), np.arange(1, img_size + 1))
262
+ patch_size_int = np.round(x / y).astype(int)
263
+ return patch_size_int
264
+
265
+ def __generate_noise_pattern(img_size:int=512, n_scales:int=5, noise_type:str='sinusoid', gabor_sigma:float=25):
266
+ """
267
+ Generate a noise pattern for a given image size and number of scales.
268
+
269
+ This function generates a noise pattern consisting of Gabor or sinusoid patterns
270
+ at different orientations, phases, and scales. The resulting noise pattern is
271
+ used to create stimuli for visual perception experiments.
272
+
273
+ Parameters:
274
+ - img_size: The size of the image (default=512).
275
+ - n_scales: The number of scales in the noise pattern (default=5).
276
+ - noise_type: The type of noise pattern to generate ('gabor' or 'sinusoid')
277
+ (default='sinusoid').
278
+ - sigma: The standard deviation of the Gaussian mask used in the Gabor filter.
279
+
280
+ Returns:
281
+ - patches: A 3D array containing the generated noise patterns.
282
+ - patch_idx: A 3D array indexing the different patches in the noise pattern.
283
+ """
284
+ orientations = np.array([0, 30, 60, 90, 120, 150])
285
+ phases = np.array([0, np.pi/2])
286
+ scales = 2 ** np.arange(n_scales)
287
+ assert scales.dtype == np.int64
288
+
289
+ patch_sizes = __generate_scales(img_size=img_size)
290
+ nr_patches = len(scales) * len(orientations) * len(phases)
291
+
292
+ patches = np.zeros((img_size, img_size, nr_patches))
293
+ patch_idx = np.zeros((img_size, img_size, nr_patches))
294
+
295
+ co = 0
296
+ idx = 0
297
+
298
+ for scale in scales:
299
+ # iterate over each scale (i.e. 512, 256, 128, 64, 32)
300
+ patch = patch_sizes[scale - 1, img_size - 1]
301
+ for orientation in orientations:
302
+ for phase in phases:
303
+ if noise_type == 'gabor':
304
+ p = __generate_gabor(patch, 1.5, orientation, phase, gabor_sigma, 1)
305
+ else:
306
+ p = __generate_sinusoid(patch, 2, orientation, phase, 1)
307
+ # Repeat to fill scale
308
+ patches[:, :, co - 1] = np.tile(p, (scale, scale))
309
+ for col in range(1, scale + 1):
310
+ for row in range(1, scale + 1):
311
+ patch_idx[patch * (row - 1) : patch * row, patch * (col - 1):patch * col, co - 1] = idx
312
+ # Update contrast counter
313
+ idx += 1
314
+ co += 1
315
+ return patches, patch_idx
316
+
317
+ def __generate_stimuli_params(n_trials: int, n_scales: int, seed: int = 1):
318
+ """
319
+ Generate stimulus parameters.
320
+
321
+ This function generates a uniform distribution centered at 0,
322
+ which is used as the stimulus parameters in visual perception experiments.
323
+
324
+ The function takes three parameters: n_trials (the number of trials),
325
+ n_scales (the number of scales), and seed (the random seed).
326
+
327
+ Parameters:
328
+ - n_trials: The number of trials.
329
+ - n_scales: The number of scales.
330
+ - seed: The random seed.
331
+
332
+ Returns:
333
+ - stimuli_params: A 2D array containing the generated stimulus parameters.
334
+ """
335
+ # It is important to propagate the seed for both random and numpy explicitly here
336
+ # to ensure reproducibility between experiments. This would generate the exact same
337
+ # parameters used to generate the stimuli and later recreate those for creating the ci
338
+ # without the need to store or load data externally. However, if needed, the parameters
339
+ # can be sideloaded omitting the execution of this function in its caller functions
340
+ np.random.seed(seed)
341
+ random.seed(seed)
342
+ nparams = sum(6 * 2 * np.power(2, np.arange(n_scales))**2)
343
+ stimuli_params = np.random.uniform(-1, 1, size=(n_trials, nparams))
344
+ return stimuli_params
345
+
346
+ def __generate_all_noise_stimuli(n_trials: int, n_scales: int, img_size: int, noise_type: str, gabor_sigma: float, seed: int) -> np.ndarray:
347
+ """
348
+ Generate all noise stimuli.
349
+
350
+ This function generates a set of noise patterns at different orientations, phases, and scales.
351
+ The resulting noise pattern is used to create stimuli for visual perception experiments.
352
+
353
+ Parameters:
354
+ n_trials (int): The number of trials.
355
+ n_scales (int): The number of scales in the noise pattern.
356
+ img_size (int): The size of the image.
357
+ noise_type (str): The type of noise pattern to generate ('gabor' or 'sinusoid').
358
+ gabor_sigma (float): The standard deviation of the Gaussian mask used in the Gabor filter.
359
+ seed (int): The random seed.
360
+
361
+ Returns:
362
+ stimuli (np.ndarray): A 3D array containing the generated noise patterns.
363
+ """
364
+ stimuli_params = __generate_stimuli_params(n_trials, n_scales, seed)
365
+ stimuli = np.zeros((n_trials, img_size, img_size))
366
+ patches, patch_idx = __generate_noise_pattern(img_size=img_size, noise_type=noise_type, n_scales=n_scales, gabor_sigma=gabor_sigma)
367
+ for trial in tqdm(range(n_trials), desc="Processing", total=n_trials):
368
+ params = stimuli_params[trial]
369
+ stimuli[trial,:,:] = __generate_individual_noise_stimulus(params, patches, patch_idx)
370
+ return stimuli
371
+
372
+ def __generate_stimulus_image(stimulus: np.ndarray, base_face: np.ndarray):
373
+ """
374
+ Generate a stimulus image by combining the noise pattern with a base face.
375
+
376
+ This function takes two parameters:
377
+ - stimulus (np.ndarray): The generated noise pattern.
378
+ - base_face (np.ndarray): The base face to combine with the noise pattern.
379
+
380
+ Returns:
381
+ - The combined stimulus image as an np.ndarray.
382
+ """
383
+ stimulus = (stimulus + 0.3) / 0.6
384
+ return combine(stimulus, base_face)
385
+
386
+ def generate_stimuli_2ifc(base_face: np.ndarray,
387
+ n_trials: int = 770,
388
+ n_scales: int = 5,
389
+ gabor_sigma: int = 25,
390
+ noise_type: str = 'sinusoid',
391
+ seed: int = 1):
392
+ """
393
+ Generate stimuli for a 2IFC (two-interval forced-choice) experiment.
394
+
395
+ This function generates two sets of stimuli: one for the "original" interval and another for the "inverted" interval.
396
+ The images in these sets represent pairs of images which need to be presented alongside each other.
397
+
398
+ Parameters:
399
+ base_face (np.ndarray): The base face used to generate the stimuli.
400
+ n_trials (int): The number of trials in the experiment. Default is 770.
401
+ n_scales (int): The number of scales in the noise pattern. Default is 5.
402
+ sigma (float): The standard deviation of the Gaussian mask used in the Gabor filter. Default is 5.
403
+ noise_type (str): The type of noise pattern to generate ('gabor' or 'sinusoid'). Default is 'sinusoid'.
404
+ seed (int): The random seed for generating the stimuli.
405
+
406
+ Returns:
407
+ A tuple containing two sets of stimulus images: one for the original interval and another for the inverted interval.
408
+ """
409
+ img_size = get_image_size(base_face)
410
+
411
+ stimuli = __generate_all_noise_stimuli(n_trials, n_scales, img_size, noise_type, gabor_sigma, seed)
412
+ assert n_trials == stimuli.shape[0]
413
+ stimuli_ori = []
414
+ stimuli_inv = []
415
+ for trial in range(0,n_trials):
416
+ stimulus = __generate_stimulus_image(stimuli[trial, :, :], base_face)
417
+ stimulus_inverted = __generate_stimulus_image(-stimuli[trial, :, :], base_face)
418
+ stimuli_ori.append(stimulus)
419
+ stimuli_inv.append(stimulus_inverted)
420
+ return stimuli_ori, stimuli_inv
rcpyci/im_ops.py ADDED
@@ -0,0 +1,151 @@
1
+ """
2
+ File Name: im_ops.py
3
+ Description: TBD
4
+ """
5
+ import os
6
+
7
+ import numpy as np
8
+ from PIL import Image
9
+
10
+
11
+ ### save/load image operations
12
+ # this function saves an unscaled [0,1] numpy array to an image
13
+ def save_image(image: np.array, path, clip=True, scale=True, save_npy=False):
14
+ """
15
+ Saves an unscaled [0,1] numpy array to an image.
16
+
17
+ Parameters:
18
+ - image (np.array): The input numpy array. Should be of shape (height, width).
19
+ - path (str): The path where the image will be saved.
20
+ - clip (bool): Whether or not to clip the pixel values between 0 and 1.
21
+ - scale (bool): Whether or not to scale the pixel values from [0,1] to [0,255].
22
+ - save_npy (bool): Whether or not to save the unscaled numpy array as a .npy file.
23
+
24
+ Returns:
25
+ None
26
+ """
27
+ os.makedirs(os.path.dirname(path), exist_ok=True)
28
+ if save_npy:
29
+ npy_path = os.path.splitext(path)[0] + ".npy"
30
+ np.save(npy_path, image)
31
+ if clip:
32
+ image = np.clip(image, 0, 1)
33
+ if scale:
34
+ image = image * 255
35
+ Image.fromarray(image.astype(np.uint8)).save(path)
36
+
37
+ # rescaling image to utilize the full [0,255] range
38
+ def read_image(filename, grayscale=True, maximize_contrast=True):
39
+ """
40
+ Reads an image file and preprocesses it.
41
+
42
+ Parameters:
43
+ - filename (str): The path to the image file.
44
+ - grayscale (bool): Whether or not to convert the image to grayscale.
45
+ - maximize_contrast (bool): Whether or not to rescale the image to utilize the full [0,255] range.
46
+
47
+ Returns:
48
+ np.array: The preprocessed image as a numpy array.
49
+ """
50
+ img = Image.open(filename)
51
+ if grayscale:
52
+ img = img.convert('L') # Convert to grayscale
53
+ if maximize_contrast:
54
+ img = (img - np.min(img)) / (np.max(img) - np.min(img))
55
+ return np.asarray(img)
56
+
57
+ ### image processing operators
58
+ def get_image_size(image) -> int:
59
+ """
60
+ Returns the size of an image and confirms it's a square image.
61
+
62
+ This function returns the size (i.e., height and width) of a given image.
63
+
64
+ Parameters:
65
+ - image: The input image.
66
+
67
+ Returns:
68
+ int: A int containing the height and width of the image.
69
+ """
70
+ assert len(image.shape) == 2
71
+ assert image.shape[0] == image.shape[1]
72
+ return image.shape[0]
73
+
74
+ #TODO tests if adding a mask affects the computation
75
+ def apply_mask(ci: np.ndarray, mask: np.ndarray):
76
+ """
77
+ Applies a binary mask to an image.
78
+
79
+ This function applies a given binary mask to the input image ci. Pixels in the mask with value 0 are set to zero in the resulting masked image.
80
+
81
+ Parameters:
82
+ - ci (np.ndarray): The input image.
83
+ - mask (np.ndarray): A binary mask of the same shape as the input image.
84
+
85
+ Returns:
86
+ np.ndarray: The masked image.
87
+ """
88
+ if isinstance(mask, str):
89
+ mask_matrix = read_image(mask, grayscale=True)
90
+ elif isinstance(mask, np.ndarray) and mask.ndim == 2:
91
+ mask_matrix = mask
92
+ else:
93
+ raise ValueError("The mask argument is neither a path to file nor a 2D matrix!")
94
+ masked_ci = np.ma.masked_where(mask_matrix == 0, ci)
95
+ return masked_ci
96
+
97
+ #TODO logging
98
+ def apply_constant_scaling(ci: np.ndarray, constant: np.ndarray):
99
+ """Applies a constant scaling to an image.
100
+
101
+ This function applies a given constant value to the input image ci, effectively shifting its intensity values.
102
+ The new minimum and maximum pixel values are determined by the applied constant, ensuring that all pixel values remain within the 0-1 range.
103
+
104
+ Parameters:
105
+ - ci (np.ndarray): The input image.
106
+ - constant (np.ndarray): A scalar value used for scaling the input image.
107
+
108
+ Returns:
109
+ np.ndarray: The scaled image.
110
+ """
111
+ scaled = (ci + constant) / (2 * constant)
112
+ if np.any((scaled > 1.0) | (scaled < 0)):
113
+ print("Chosen constant value for constant scaling made noise "
114
+ "of classification image exceed possible intensity range "
115
+ "of pixels (<0 or >1). Choose a lower value, or clipping "
116
+ "will occur.")
117
+ return scaled
118
+
119
+ def apply_matched_scaling(ci: np.ndarray, base: np.ndarray):
120
+ min_base = np.min(base)
121
+ max_base = np.max(base)
122
+ min_ci = np.min(ci[~np.isnan(ci)])
123
+ max_ci = np.max(ci[~np.isnan(ci)])
124
+ scaled = min_base + ((max_base - min_base) * (ci - min_ci) / (max_ci - min_ci))
125
+ return scaled
126
+
127
+ def apply_independent_scaling(ci: np.ndarray):
128
+ constant = max(abs(np.nanmin(ci)), abs(np.nanmax(ci)))
129
+ scaled = (ci + constant) / (2 * constant)
130
+ return scaled
131
+
132
+ def combine(im1: np.ndarray, im2: np.ndarray):
133
+ return (im1 + im2) / 2
134
+
135
+ def find_clusters(mask, min_size:int=1):
136
+ """
137
+ Identifies contiguous clusters in a binary mask.
138
+
139
+ Args:
140
+ mask (numpy.ndarray): Binary mask where True indicates regions above threshold.
141
+ min_size (int, optional): Minimum size of clusters to retain.
142
+
143
+ Returns:
144
+ numpy.ndarray: Boolean mask of significant clusters.
145
+ """
146
+ import scipy.ndimage as ndi
147
+ labeled, _ = ndi.label(mask)
148
+ sizes = np.bincount(labeled.ravel())[1:]
149
+ mask_sizes = np.where(sizes >= min_size)[0] + 1
150
+ significant_clusters = np.isin(labeled, mask_sizes)
151
+ return significant_clusters
rcpyci/infoval.py ADDED
@@ -0,0 +1,93 @@
1
+ import numpy as np
2
+ from numpy.linalg import norm
3
+ from tqdm import tqdm
4
+
5
+ from .core import __generate_ci_noise, __generate_noise_pattern, __generate_stimuli_params
6
+ from .im_ops import get_image_size
7
+
8
+
9
+ def generate_reference_distribution_2ifc(base_image: np.ndarray,
10
+ n_trials: int = 770,
11
+ n_scales: int = 5,
12
+ gabor_sigma: int = 25,
13
+ noise_type: str = 'sinusoid',
14
+ seed: int = 1,
15
+ save_path: str = None,
16
+ stimuli_params: np.ndarray = None,
17
+ patches: np.ndarray = None,
18
+ patch_idx: np.ndarray = None,
19
+ iter: int = 10000):
20
+ """
21
+ Generate reference responses selection distribution for 2 ifc task with your stimuli params.
22
+
23
+ This function simulates random responding in a 2-IFC task with n_trials for n iterations. Default is 10000.
24
+ It generates the stimuli parameters, noise pattern, and computes the classification image (ci) norm for
25
+ each simulated response selection. The generated norms are returned as the reference distribution.
26
+ You can regenerate the stimuli parameters by providing the seed and additional parameters used to generate
27
+ the stimulus images or provide the stimuli parameter space using the stimuli_params variable.
28
+
29
+ Parameters:
30
+ base_image: np.ndarray
31
+ The base image used to generate the stimuli.
32
+ n_trials: int
33
+ Number of trials in the 2-IFC task. Defaults to 770.
34
+ n_scales: int
35
+ Number of scales for the Gabor noise pattern. Defaults to 5.
36
+ gabor_sigma: int
37
+ Sigma value for the Gabor noise pattern. Defaults to 25.
38
+ noise_type: str
39
+ Type of noise pattern to use ('sinusoid' or 'gabor'). Defaults to 'sinusoid'.
40
+ seed: int
41
+ Random seed used for generating stimuli and responses. Defaults to 1.
42
+ stimuli_params: np.ndarray
43
+ Optional parameter that can be provided directly instead of using the default value.
44
+ patches: np.ndarray
45
+ Optional patch pattern array used for generating noise. If not provided, it will be generated internally.
46
+ patch_idx: np.ndarray
47
+ Optional index array for the patch pattern. If not provided, it will be generated internally.
48
+ iter: int
49
+ Number of iterations to run the simulation. Defaults to 10000.
50
+
51
+ Returns:
52
+ reference_norms: list of float
53
+ The computed norms of the classification images (ci) for each iteration, which form the reference distribution.
54
+ """
55
+
56
+ # Simulate random responding in 2IFC task with ntrials trials across iter iterations
57
+ img_size = get_image_size(base_image)
58
+ if iter < 10000:
59
+ print("You should set iter >= 10000 for InfoVal statistic to be reliable")
60
+
61
+ if stimuli_params is None:
62
+ stimuli_params = __generate_stimuli_params(n_trials = n_trials, n_scales = n_scales, seed = seed)
63
+ if patches is None or patch_idx is None:
64
+ patches, patch_idx = __generate_noise_pattern(img_size = img_size, noise_type = noise_type, n_scales = n_scales, gabor_sigma = gabor_sigma)
65
+
66
+ reference_norms = []
67
+ for _ in tqdm(range(iter), desc="Computing reference distribution"):
68
+ # Generate random responses for this iteration
69
+ responses = np.random.choice([-1, 1], size=n_trials).reshape(n_trials, 1)
70
+ ci = __generate_ci_noise(stimuli_params, responses, patches, patch_idx)
71
+ # Compute classication image for this iteration
72
+ #ci = np.dot(stimuli, responses) / stimuli.shape[1]
73
+ # Save norm for this iteration
74
+ reference_norms.append(norm(ci, 'f'))
75
+ results = np.array(reference_norms)
76
+ if save_path:
77
+ np.save(save_path, results)
78
+ return results
79
+
80
+ def compute_info_val_2ifc(target_ci: np.ndarray, reference_norms):
81
+ # Compute reference values
82
+ ref_median = np.median(reference_norms)
83
+ ref_mad = np.median(np.abs(reference_norms - ref_median))
84
+ ref_iter = len(reference_norms)
85
+
86
+ # Compute informational value metric
87
+ cinorm = norm(target_ci, 'f')
88
+ info_val = (cinorm - ref_median) / ref_mad
89
+
90
+ # print(f"Informational value: z = {info_val} (ci norm = {cinorm}; reference median = {ref_median}; MAD = {ref_mad}; iterations = {ref_iter})")
91
+
92
+ return info_val, cinorm, ref_median, ref_mad, ref_iter
93
+