OSCR

Mapping human visual contrast sensitivity and vision loss across the visual field with model-based fMRI.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § Methods › Quantifying eccentricity- and location-dependent variations in sensitivity › Using subject-specific pRF estimates ↔ BackProjection_Slopes_Patient.m, lines 99 to 138 · score 0.93 · near periphery, 0.5–2.5 deg, 15–20 deg, 2.5–4.5 deg, 4.5–9.5 deg, 9.5–15 deg · Report a mismatch
  2. [2] § Methods › Quantifying eccentricity- and location-dependent variations in sensitivity › Using subject-specific pRF estimates ↔ BackProjection_Slopes_Controls.m, lines 160 to 200 · score 0.88 · 0.5–2.5 deg, 15–20 deg, 2.5–4.5 deg, 4.5–9.5 deg, 9.5–15 deg, fovea · Report a mismatch
  3. [3] § Methods › Quantifying eccentricity- and location-dependent variations in sensitivity › Using calibrated Benson retinotopic template atlas ↔ src/eccen_adjust/__init__.py, lines 61 to 151 · score 0.82 · foveal confluence, integrating Horton, surface area, numerical, Hoyt, match · Report a mismatch
  4. [4] § Results › Effect of eye movements on V1 cortical sensitivity ↔ BackProjection_Slopes_Controls.m, lines 160 to 200 · score 0.80 · 0.5–2.5 deg, 15–20 deg, 2.5–4.5 deg, 4.5–9.5 deg, 9.5–15 deg, eye motion · Report a mismatch
  5. [5] § Results › Spatial frequency preference changes in V1 neuronal populations across eccentricities ↔ BackProjection_Slopes_Patient.m, lines 99 to 138 · score 0.80 · 0.5–2.5 deg, 15–20 deg, 2.5–4.5 deg, 4.5–9.5 deg, 9.5–15 deg, peripheral · Report a mismatch
  6. [6] § Results › Contrast sensitivity is not equal across the visual field quadrants in V1 neurons ↔ BackProjection_Slopes_Controls.m, lines 987 to 1094 · score 0.64 · Vertical Meridian Anisotropy, Left Right Horizontal, cortical contrast sensitivity, HVA, VMA, positions · Report a mismatch
  7. [7] § Methods › Quantifying eccentricity- and location-dependent variations in sensitivity › Using calibrated Benson retinotopic template atlas ↔ src/eccen_adjust/__init__.py, lines 39 to 59 · score 0.64 · surface area, maximum eccentricities, cortical magnification, Horton, Hoyt, model · Report a mismatch
  8. [8] § Methods › Processing of the contrast sensitivity task ↔ BackProjection_Slopes_Controls.m, lines 24 to 43 · score 0.53 · FreeSurfer, SPM12, compatible, MATLAB · Report a mismatch
  9. [9] § Methods › Processing of the contrast sensitivity task ↔ BackProjection_Slopes_Patient.m, lines 23 to 42 · score 0.53 · FreeSurfer, SPM12, compatible, MATLAB · Report a mismatch
  10. [10] § Methods › Quantifying eccentricity- and location-dependent variations in sensitivity › Using calibrated Benson retinotopic template atlas ↔ src/eccen_adjust/__init__.py, lines 39 to 59 · score 0.52 · magnification model, surface area, Horton, Hoyt, 90 deg, cortical · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

MATLAB · 1,606 lines · 82 KB · MIT · 4 matches

  1. %% =========================================================================
  2. % SECTION 1 — INITIALISE ENVIRONMENT AND SET PATHS
  3. % -------------------------------------------------------------------------
  4. % This section:
  5. % • Resets the MATLAB workspace
  6. % • Detects whether MATLAB is running on Intel vs Apple Silicon macOS
  7. % • Loads the appropriate versions of SPM and FreeSurfer MATLAB toolboxes
  8. % • Prompts the user to select the project folder
  9. % =========================================================================
  10. clear all; close all; clc; commandwindow;
  11. set(0,'units','pixels');
  12. Rect = get(0,'ScreenSize'); % Monitor resolution in pixels
  13. %% Detect CPU architecture (Intel vs Apple Silicon)
  14. % Required because SPM12 and FreeSurfer ship different binaries for each.
  15. if ismac()
  16. [~,result] = system('uname -m');
  17. is_silicon_mac = strcmp(strtrim(result),'arm64'); % Apple Silicon machines report "arm64"
  18. else
  19. is_silicon_mac = false;
  20. end
  21. %% Add required toolboxes based on detected architecture
  22. % These paths MUST be customised by the user before running.
  23. % -------------------------------------------------------------------------
  24. % For Apple Silicon:
  25. % - Uses a M1/M2/M3‑compatible SPM build
  26. % - Uses FreeSurfer 7.4.1 MATLAB tools
  27. %
  28. % For Intel macOS or other systems:
  29. % - Uses standard SPM12 build
  30. % - Uses FreeSurfer 7.1.1 MATLAB tools
  31. % -------------------------------------------------------------------------
  32. if is_silicon_mac
  33. % Apple Silicon versions
  34. addpath(genpath('~/Dropbox (Personal)/PhD_UCL/Hugo_PhD/Toolboxes/spm12-M3chip'));
  35. addpath(genpath(['/Applications/freesurfer/7.4.1/matlab']))
  36. else
  37. % Intel macOS / other systems
  38. addpath(genpath('~/Dropbox (Personal)/PhD_UCL/Hugo_PhD/Toolboxes/spm12'));
  39. addpath(genpath(['/Applications/freesurfer/7.1.1/matlab']))
  40. end
  41. %% Ask user to choose the main project directory (interactive GUI)
  42. pinfo = struct();
  43. disp('SELECT MAIN PROJECT FOLDER')
  44. pinfo.Home = uigetdir(); % User selects folder containing beta-values (BetaValues)
  45. %% =========================================================================
  46. % SECTION 2 — USER INPUT & CONDITION SELECTION
  47. % -------------------------------------------------------------------------
  48. % This section:
  49. % • Lets the user choose retinotopic map source (Benson vs pRF)
  50. % • Asks whether to generate per‑subject figures
  51. % • Scans the BetaValues directory and categorises files by condition
  52. % • Provides GUI dialogs for selecting:
  53. % - Experimental condition (Low/High SF, Eye Motion, Visual Loss)
  54. % - Visit/session (if applicable)
  55. % - One or multiple subjects
  56. % • Produces a list of filenames (selectfn{indx}) for later analysis
  57. % =========================================================================
  58. Eccentricity = 20; % Maximum eccentricity to display/use (deg)
  59. ContrastToPlot = [7.5, 42.2, 60, 100]; % Tested contrast levels
  60. %% Ask which retinotopy dataset to use
  61. % Choice determines which files are matched later:
  62. % 'benson_Neuropy' → Benson retinotopic atlas outputs
  63. % 'pRF_NonEq' → Subject-specific pRF mapping outputs
  64. pRFFolderName = input('BENSON (0) OR pRF MAP (1)? ','s');
  65. switch pRFFolderName
  66. case '0'
  67. pRFFolderName = 'benson_Neuropy';
  68. case '1'
  69. pRFFolderName = 'pRF_NonEq';
  70. end
  71. %% Ask whether to generate subject-level diagnostic figures
  72. % 0 = skip (faster)
  73. % 1 = create all figures (slower but visually informative)
  74. PlotInd = str2num(input('Plot figures for each individual (0/1)? ','s'));
  75. %% =========================================================================
  76. % Collect list of available BetaValues files & classify them
  77. % =========================================================================
  78. directory_allfiles = dir(fullfile(pinfo.Home,'BetaValues'));
  79. % Each index corresponds to a specific experimental label embedded in filenames.
  80. % Using logical masks to filter subjects based on naming conventions.
  81. idx_sem = contains({directory_allfiles.name},'SEM') & contains({directory_allfiles.name},pRFFolderName); % Simulated Eye Motion
  82. idx_RingLoss = contains({directory_allfiles.name},'Ring') & contains({directory_allfiles.name},pRFFolderName); % Ring-shaped loss
  83. idx_URLoss = contains({directory_allfiles.name},'UpRight') & contains({directory_allfiles.name},pRFFolderName); % Upper-right VF loss
  84. idx_visit2 = contains({directory_allfiles.name},'RepMeas') & contains({directory_allfiles.name},pRFFolderName); % Repeat-measures session (Visit 2)
  85. idx_controls = ~contains({directory_allfiles.name},'P1') & contains({directory_allfiles.name},pRFFolderName); % Controls only
  86. % Visit 1 = everything not labelled RingLoss, URLoss, SEM, or visit2
  87. idx_visit1 = idx_controls - idx_RingLoss - idx_URLoss - idx_sem - idx_visit2;
  88. %% =========================================================================
  89. % GUI dialog: Select which experimental condition to analyse
  90. % =========================================================================
  91. answer = questdlg('Choose one condition','Condition Label','Low & High SF','SimEyeMotion','SimVisualLoss','Low & High SF');
  92. switch answer
  93. case 'Low & High SF' % CASE 1: Low vs High Spatial Frequency experiment
  94. % Ask whether this is Visit 1 or Visit 2
  95. answer2 = questdlg('1st or 2nd visit?','Session Label','Visit 1','Visit 2','Visit 1');
  96. switch answer2
  97. case 'Visit 1'
  98. selectfn = {directory_allfiles(find(idx_visit1)).name};
  99. [indx,~] = listdlg('PromptString',{'Select a file.','Multiple file can be selected at a time.',''}, ...
  100. 'SelectionMode','multiple','ListString',selectfn);
  101. case 'Visit 2'
  102. selectfn = {directory_allfiles(find(idx_visit2)).name};
  103. [indx,~] = listdlg('PromptString',{'Select a file.','Multiple file can be selected at a time.',''}, ...
  104. 'SelectionMode','multiple','ListString',selectfn);
  105. end
  106. case 'SimEyeMotion'
  107. % Eye‑movement simulation condition
  108. selectfn = {directory_allfiles(find(idx_sem)).name};
  109. [indx,~] = listdlg('PromptString',{'Select a file.','Multiple file can be selected at a time.',''}, ...
  110. 'SelectionMode','multiple','ListString',selectfn);
  111. case 'SimVisualLoss'
  112. % Choose simulated loss type
  113. answer3 = questdlg('3-8deg Ring or Upper-Right VF loss?','Loss Type','Ring','Quadrant','Ring');
  114. switch answer3
  115. case 'Ring'
  116. selectfn = {directory_allfiles(find(idx_RingLoss)).name};
  117. [indx,~] = listdlg('PromptString',{'Select a file.','Multiple file can be selected at a time.',''}, ...
  118. 'SelectionMode','multiple','ListString',selectfn);
  119. case 'Quadrant'
  120. selectfn = {directory_allfiles(find(idx_URLoss)).name};
  121. [indx,~] = listdlg('PromptString',{'Select a file.','Multiple file can be selected at a time.',''}, ...
  122. 'SelectionMode','multiple','ListString',selectfn);
  123. end
  124. end
  125. % Stop if user cancels
  126. if isempty(indx)
  127. return
  128. end
  129. %% =========================================================================
  130. % SECTION 3 — DEFINE ECCENTRICITY BINS & POLAR ANGLE WEDGE REGIONS
  131. % -------------------------------------------------------------------------
  132. % This section sets up all visual-field sampling coordinates used for
  133. % extracting vertex subsets from the cortical surface.
  134. %
  135. % Steps:
  136. % • Define eccentricity bins depending on experimental condition
  137. % • Compute bin midpoints, search radii, and tick labels
  138. % • Define polar-angle ROIs (meridians or obliques)
  139. % • Build a full set of (θ, r) seed coordinates
  140. % • Convert seed coordinates to Cartesian (x,y)
  141. % • Define search radius for each seed for vertex selection
  142. % =========================================================================
  143. %% -------------------------------------------------------------------------
  144. % ECCENTRICITY BINNING
  145. % Different experimental conditions use different eccentricity bin sizes.
  146. % -------------------------------------------------------------------------
  147. if strcmp(answer,'SimVisualLoss')
  148. % ----------------------------- RING LOSS ------------------------------
  149. if strcmp(answer3,'Ring')
  150. answer4 = questdlg('Ring Loss: 1deg binning or less detailed','Loss Binning','1deg','Less detailed','1deg');
  151. switch answer4
  152. case '1deg'
  153. % High-resolution eccentricity bins (1° width)1
  154. ECCBoundaries = round([0.5:1:20],1); % 0.5º to 20º
  155. BinModeLabel = 'PerECC';
  156. case 'Less detailed'
  157. % Low-resolution bins: {0–3°, 3–8°, 8–15°, 15–20°}
  158. BinModeLabel = '4Bins';
  159. ECCBoundaries = [0 3 8 15 20];
  160. end
  161. else
  162. % -------------------------- QUADRANT LOSS ------------------------------
  163. % Quadrant VF loss always uses 1° resolution
  164. ECCBoundaries = round([0.5:1:20],1);
  165. BinModeLabel = 'PerECC';
  166. end
  167. else
  168. % -------------------------------------------------------------------------
  169. % DEFAULT CASE — Low/High SF or SimEyeMotion
  170. % Use clinically meaningful eccentricity bands
  171. % -------------------------------------------------------------------------
  172. % These boundary definitions correspond to known anatomical regions:
  173. Fovea = [0.5,2.5];
  174. ParaFovea = [2.5,4.5];
  175. PeriFovea = [4.5,9.5];
  176. NearPeriphery1 = [9.5,15];
  177. NearPeriphery2 = [15,20];
  178. BinModeLabel = 'ClinicallyDefined';
  179. ECCBoundaries = unique([Fovea,ParaFovea,PeriFovea,NearPeriphery1,NearPeriphery2]);
  180. end
  181. %% -------------------------------------------------------------------------
  182. % Compute eccentricity bin helper variables
  183. % -------------------------------------------------------------------------
  184. BinECC = ECCBoundaries; % Bin edges
  185. StepSizeECC = diff(ECCBoundaries)/2; % Half-bin width
  186. RadiusForSearch = diff(ECCBoundaries)/2; % Search radius = half bin
  187. Radius_seedroi = ECCBoundaries(1:end-1)+StepSizeECC; % Bin midpoints
  188. BinTick = Radius_seedroi; % Used for axis ticks later
  189. %% =========================================================================
  190. % POLAR ANGLE DEFINITIONS
  191. % These define which meridians (or oblique axes) to sample from.
  192. % -------------------------------------------------------------------------
  193. % MATLAB polar-angle convention for pRF outputs:
  194. % 0 rad = Right horizontal meridian (RHM)
  195. % π/2 rad = Upper vertical meridian (UVM)
  196. % -π/2 rad = Lower vertical meridian (LVM)
  197. % π rad = Left meridian (LHM)
  198. % =========================================================================
  199. % Cardinal meridians
  200. UVM = [pi/2]; % Upper Vertical Meridian (Ventral V1 = Positive Polar Values)
  201. LVM = [-pi/2]; % Lower Vertical Meridian (Dorsal V1 = Negative Polar Values)
  202. LHM = [-pi]; % Left Horizontal Meridian (Right V1 = Values between -90:-180/180:90)
  203. RHM = [0]; % Right Horizontal Meridian (Left V1= Values between -90:0:90)
  204. % Oblique diagonals (used for quadrant loss)
  205. Diagonals = [-pi/4; pi/4; 3/4*pi; -3/4*pi]; % (Lower‑Right, Upper‑Right, Upper‑Left, Lower‑Left)
  206. %% Determine which polar-angle set to use
  207. if strcmp(answer,'SimVisualLoss')
  208. switch answer3
  209. case 'Ring'
  210. % -------------------------------------------------- RING LOSS -----
  211. PolarCoords = [LHM;UVM;RHM;LVM]; % Only cardinal meridians
  212. % Compute wedge half-width (in degrees)
  213. AngleForSearch = unique(rad2deg(diff(sort(PolarCoords(1:4)))))/2;
  214. % Replicate polar angles for each eccentricity bin
  215. th = repmat(PolarCoords(1:4),length(Radius_seedroi),1);
  216. % Colour coding for meridian patches
  217. colour = repmat([0 0.7 0.3; 0.75 0.75 0; 0 0 1; 1 0 0],length(Radius_seedroi),1);
  218. case 'Quadrant'
  219. % ---------------------------------------------- QUADRANT LOSS -----
  220. PolarCoords = [Diagonals]; % Use oblique diagonals
  221. AngleForSearch = unique(rad2deg(diff(sort(PolarCoords(1:4)))))/2;
  222. th = repmat(PolarCoords(1:4),length(Radius_seedroi),1); % -pi/4; pi/4; 3/4*pi; -3/4*pi];
  223. colour = repmat([0.75 0.75 1; 1 0.75 0; 0.75 0.75 0; 0 1 1],length(Radius_seedroi),1);
  224. end
  225. else
  226. % -------------------------------------------------------------------------
  227. % DEFAULT: Use cardinal meridians for SF or eye-movement conditions
  228. % -------------------------------------------------------------------------
  229. PolarCoords = [LHM;UVM;RHM;LVM];
  230. AngleForSearch = unique(rad2deg(diff(sort(PolarCoords(1:4)))))/2;
  231. th = repmat(PolarCoords(1:4),length(Radius_seedroi),1);
  232. colour = repmat([0 0.7 0.3; 0.75 0.75 0; 0 0 1; 1 0 0],length(Radius_seedroi),1);
  233. end
  234. % Should eccentricity search be constrained by wedge angle (i.e., take into account cortical magnification)?
  235. % If MScaling == 1 -> search region is a wedge (θ ± Δθ AND r ± Δr)
  236. % If MScaling == 0 -> search region is a circle in (x,y) / (eccentricity) space only.
  237. MScaling = 1;
  238. %% =========================================================================
  239. % Build seed coordinates in polar space (θ, r) and convert to (x,y)
  240. % =========================================================================
  241. % Create radial coordinate repeated for each polar angle
  242. r = [];
  243. for i = 1:length(Radius_seedroi)
  244. r = [r; repmat(Radius_seedroi(i),length(th)/length(Radius_seedroi),1)];
  245. end
  246. % Convert polar -> Cartesian coordinates (for visual-space mapping)
  247. [x,y] = pol2cart(th,r);
  248. x_ycoords0 = []; x_ycoords0 = [x,y];
  249. % Clean tiny negative zeros produced by floating point precision
  250. x_ycoords0(x_ycoords0 > -1e-6 & x_ycoords0 < 1e-6) = 0; % some values are -0, correct them
  251. %% =========================================================================
  252. % Single-ROI (V1‑wide) coordinate definition
  253. % Instead of multiple eccentricity bins, use 4 fixed quadrants.
  254. % =========================================================================
  255. x_ycoords0_1ROI = [-max(BinECC)/2 0; 0 max(BinECC)/2; max(BinECC)/2 0; 0 -max(BinECC)/2];
  256. r_search_1ROI = repmat(max(BinECC)/2-min(BinECC),length(x_ycoords0_1ROI),1);
  257. % Convert to polar again for convenience
  258. [th_1ROI,r_1ROI] = cart2pol(x_ycoords0_1ROI(:,1),x_ycoords0_1ROI(:,2));
  259. %% =========================================================================
  260. % Define search radius for each seed point
  261. % =========================================================================
  262. r_search = [];
  263. for i = 1:length(RadiusForSearch)
  264. r_search = [r_search; repmat(RadiusForSearch(i),size(x_ycoords0,1)/length(RadiusForSearch),1)];
  265. end
  266. %% -------------------------------------------------------------------------
  267. % Angular wedge width for M-scaling (converted to radians)
  268. % -------------------------------------------------------------------------
  269. if MScaling
  270. a_search = AngleForSearch;
  271. a_search_rad = deg2rad(a_search);
  272. end
  273. %% =========================================================================
  274. % SECTION 4 — PLOTTING OPTIONS, COLOUR MAPS, MODEL PARAMETERS, PREALLOCATION
  275. % -------------------------------------------------------------------------
  276. % This section:
  277. % • Sets default plotting styles (line types, marker types, labels)
  278. % • Lets the user choose a heatmap colour scale (incl. colour‑blind safe)
  279. % • Selects the exponent strategy for the cortical contrast sensitivity model R = a + b*C^p
  280. % • Preallocates (initialises) containers used later for:
  281. % - Eccentricity‑binned data & CI
  282. % - Polar‑angle ROI data & CI
  283. % - Fitted parameters and counts
  284. % - pRF size summaries
  285. % =========================================================================
  286. %% -------------------------------------------------------------------------
  287. % General plotting preferences
  288. % -------------------------------------------------------------------------
  289. idxFigs = 0; % Counter for figure handles
  290. YAxisLim = [-5 20]; % Default plotting range
  291. lineStyle = {'-','-','--','--'}; % One per SF condition (fallback)
  292. MarkerType = {'o','o','s','s'}; % Markers per SF condition
  293. VariableLabel = {'Beta Values'}; % Y-axis label for β estimates
  294. %% -------------------------------------------------------------------------
  295. % Heatmap colour scale selection (with an accessible palette option)
  296. % Choices:
  297. % 0: MATLAB 'hot' colormap
  298. % 1: Custom black → yellow linear gradient
  299. % 2: Colour-blind friendly palette (Paul Tol-like sequence, interpolated)
  300. % -------------------------------------------------------------------------
  301. colorchoice = str2num(input('Heatmap color scale: hot scale (0) or black-to-yellow (1) or color-blindness (2)? ','s'));
  302. if colorchoice == 1
  303. % ----- Black -> Yellow continuous gradient (256 steps)
  304. numColors = 256;
  305. colorMap = zeros(numColors, 3);
  306. blackRGB = [0, 0, 0];
  307. yellowRGB = [1, 1, 0];
  308. % Interpolate between black and yellow
  309. for i = 1:numColors
  310. colorMap(i, :) = blackRGB + (i - 1) / (numColors - 1) * (yellowRGB - blackRGB);
  311. end
  312. % NOTE: Several plotting sections set the axes background using 'colorBck'.
  313. % Define a neutral light gray here to keep behaviour consistent.
  314. colorBck = [0.75 0.75 0.75];
  315. elseif colorchoice == 2
  316. % ----- Colour‑blind safe palette (interpolated to 256 colours)
  317. % RGB anchors (approx. from a colour‑blind friendly set):
  318. colors = [
  319. 100, 143, 255; % Light blue
  320. 120, 94, 240; % Purple
  321. 220, 38, 127; % Magenta
  322. 254, 97, 0; % Orange
  323. 255, 176, 0 % Yellow
  324. ];
  325. colorBck = [0.75 0.75 0.75];
  326. % Interpolate to produce a smooth 256‑colour map
  327. positions = linspace(0, 1, size(colors, 1));
  328. N = 256;
  329. colorMap = interp1(positions, colors/255, linspace(0, 1, N));
  330. elseif colorchoice == 0
  331. % ----- MATLAB 'hot' built‑in colormap
  332. colorMap = 'hot';
  333. colorBck = [0.75 0.75 0.75];
  334. end
  335. %% -------------------------------------------------------------------------
  336. % Model fitting option for cortical contrast sensitivity exponent p in: R = a + b * C^p
  337. % Choices:
  338. % 1: Fit exponent p from the data (nonlinear fit)
  339. % 2: Fix p = 0.5 (i.e., R = a + b * sqrt(C))
  340. % -------------------------------------------------------------------------
  341. FitExponent = str2num(input('Fit exponent (1) or sqrt of C = 0.5 (2): ','s'));
  342. %% -------------------------------------------------------------------------
  343. % Preallocate / initialise containers for analysis results
  344. % (They will be populated incrementally in the subject loop)
  345. % -------------------------------------------------------------------------
  346. % -------------------- ECCENTRICITY‑BINNED DATA ----------------------------
  347. % Data_ECC(sub, bin, SF, contrast) : Mean β per bin/condition
  348. % CIBetaVals_ECC(sub, SF, bin, contrast, 2) : 95% CI on β (lower/upper)
  349. % FitParams_ECC(sub, SF, bin, [a b p]) : Mean fitted cortical contrast sensitivity params
  350. % FitParams_ECC_nvals(sub, SF, bin) : Count of contributing vertices
  351. % ECC_pRFSize(sub, SF, bin) : Mean pRF size in bin
  352. Data_ECC = [];
  353. CIBetaVals_ECC = [];
  354. FitParams_ECC = [];
  355. FitParams_ECC_nvals = [];
  356. ECC_pRFSize = [];
  357. % -------------------- POLAR‑ANGLE ROI (BIN × POLAR) -----------------------
  358. % Data_VFrois(sub, SF, contrast, bin, polar) : Mean β
  359. % FitParams_VFrois(sub, SF, bin, polar, [a b p]) : Fitted cortical contrast sensitivity params
  360. % FitParams_VFrois_nvals(sub, SF, bin, polar) : Count of vertices
  361. % VFrois_pRFSize(sub, SF, bin, polar) : Mean pRF size
  362. Data_VFrois = [];
  363. FitParams_VFrois = [];
  364. FitParams_VFrois_nvals = [];
  365. VFrois_pRFSize = [];
  366. % -------------------- SINGLE‑ROI (V1‑WIDE) POLAR INDICES ------------------
  367. % Data_VFrois_1ROI(sub, SF, contrast, polar) : Mean β, pooled V1
  368. % FitParams_VFrois_1ROI(sub, SF, polar, [a b p]) : Fitted cortical contrast sensitivity params
  369. % FitParams_VFrois_1ROI_nvals(sub, SF, polar) : Count of vertices
  370. % VFrois_1ROI_pRFSize(sub, SF, polar) : Mean pRF size
  371. Data_VFrois_1ROI = [];
  372. FitParams_VFrois_1ROI = [];
  373. FitParams_VFrois_1ROI_nvals = [];
  374. VFrois_1ROI_pRFSize = [];
  375. %% =========================================================================
  376. % SECTION 5 — SUBJECT LOOP: LOAD CSV, ORGANISE BETA DATA, FIT PER‑VERTEX
  377. % CORTICAL CONTRAST SENSITIVTY
  378. % -------------------------------------------------------------------------
  379. % For each selected subject file the script:
  380. % 1. Loads the CSV (β-values + pRF parameters)
  381. % 2. Extracts subject ID, spatial frequencies, contrasts
  382. % 3. Reshapes β-values into [SF × Contrast × Vertex]
  383. % 4. Extracts pRF eccentricity, polar angle, sigma, and R²
  384. % 5. For every vertex:
  385. % • Fit cortical contrast sensitivity model R = a + b*C^p
  386. % • Store intercept, slope, exponent, residuals
  387. % =========================================================================
  388. close all; % Reset plots for each subject
  389. for subs = 1:length(selectfn(indx))
  390. %% ---------------------------------------------------------------------
  391. % Display which subject is being processed
  392. % ----------------------------------------------------------------------
  393. tmp = []; tmp = split(selectfn{indx(subs)},'_');
  394. disp(['Subject: ' tmp{1}])
  395. %% ---------------------------------------------------------------------
  396. % Load subject CSV table
  397. % Table structure includes columns such as:
  398. % SubID, SF, ContrastLevel, Beta,
  399. % pRFecc, pRFpolar, pRFsize, pRFrsquare
  400. % ----------------------------------------------------------------------
  401. T = [];
  402. T = readtable(char(fullfile(pinfo.Home,'BetaValues',selectfn{indx(subs)})));
  403. % Basic identifiers extracted from table
  404. SubID = char(unique(T.SubID));
  405. SFLabels = unique(T.SF); % Spatial frequencies tested
  406. ContrastLabels = unique(T.ContrastLevel); % Contrast levels tested
  407. Data = T.Beta; % β-values across all conditions
  408. %% ---------------------------------------------------------------------
  409. % Determine number of vertices for a single condition
  410. % The table repeats pRF values across contrasts/SFs; we extract one block.
  411. % ----------------------------------------------------------------------
  412. nVertsPerCond = length(T.SubID([T.ContrastLevel == ContrastLabels(1) & T.SF == SFLabels(1)]));
  413. %% ---------------------------------------------------------------------
  414. % Extract pRF parameters for the first condition (unique across table)
  415. % ----------------------------------------------------------------------
  416. pRFx_y_ECC = T.pRFecc(1:nVertsPerCond); % Eccentricity (º)
  417. pRFx_y_Polar = T.pRFpolar(1:nVertsPerCond); % Polar angle (rad)
  418. pRFSigma_ECC = T.pRFsize(1:nVertsPerCond); % pRF sigma (º)
  419. pRFR2_ECC = T.pRFrsquare(1:nVertsPerCond); % Model R²
  420. %% ---------------------------------------------------------------------
  421. % Reshape β-values into [SF × Contrast × Vertex]
  422. % Then permute to keep dimensions consistent throughout analysis.
  423. % ----------------------------------------------------------------------
  424. BetaVals_ECC = reshape(Data,nVertsPerCond,length(ContrastLabels),length(SFLabels));
  425. BetaVals_ECC = permute(BetaVals_ECC,[3 2 1]); % β-values: BetaVals_ECC(SF, Contrast, Vertex)
  426. %% =========================================================================
  427. % CLASSIFY VERTICES INTO VISUAL-FIELD SEED REGIONS (ECC × POLAR)
  428. % -------------------------------------------------------------------------
  429. % For each seed coordinate (θ, r) defined in Section 3:
  430. % • Identify vertices within eccentricity radius
  431. % • Optional: restrict by polar-angle wedge (M-scaling)
  432. % • Store included vertices in vtxidx(coord).inclvtx
  433. % =========================================================================
  434. vtxidx = struct(); % Container for all seed-based vertex lists
  435. for coord = 1:size(x_ycoords0,1)
  436. % Convert seed’s Cartesian location to polar (th_seed, rseed)
  437. [a,b] = cart2pol(x_ycoords0(coord,1),x_ycoords0(coord,2));
  438. vtxidx(coord).seed = x_ycoords0(coord,:);
  439. vtxidx(coord).seed_PolarRad = a;
  440. vtxidx(coord).seed_EccDeg = b;
  441. %% -------------------------------------------------------------
  442. % Select vertices based on eccentricity + polar constraints
  443. % - If MScaling=1: apply angular wedge limits
  444. % - Else: use simple Euclidean distance in (ECC, polar)
  445. % -------------------------------------------------------------
  446. if MScaling
  447. % ---- LEFT/RIGHT HORIZONTAL MERIDIANS ----
  448. if x_ycoords0(coord,1) < 0 && x_ycoords0(coord,2) == 0 || x_ycoords0(coord,1) > 0 && x_ycoords0(coord,2) == 0
  449. k = {}; k = {find(pRFx_y_ECC >= b-r_search(coord) & pRFx_y_ECC <= b+r_search(coord) & ...
  450. abs(pRFx_y_Polar) >= abs(a)-a_search_rad & abs(pRFx_y_Polar) <= abs(a) + a_search_rad)};
  451. % ---- UPPER VERTICAL MERIDIAN ----
  452. elseif x_ycoords0(coord,1) == 0 && x_ycoords0(coord,2) > 0
  453. k = {}; k = {find(pRFx_y_ECC >= b-r_search(coord) & pRFx_y_ECC <= b+r_search(coord) & ...
  454. pRFx_y_Polar >= a-a_search_rad & pRFx_y_Polar <= a + a_search_rad)};
  455. % ---- LOWER VERTICAL MERIDIAN ----
  456. elseif x_ycoords0(coord,1) == 0 && x_ycoords0(coord,2) < 0
  457. k = {}; k = {find(pRFx_y_ECC >= b-r_search(coord) & pRFx_y_ECC <= b+r_search(coord) & ...
  458. pRFx_y_Polar <= a+a_search_rad & pRFx_y_Polar >= a - a_search_rad)};
  459. % ---- LOWER OBLIQUES (LR quadrant) ----
  460. elseif x_ycoords0(coord,1) > 0 && x_ycoords0(coord,2) < 0 || x_ycoords0(coord,1) < 0 && x_ycoords0(coord,2) < 0
  461. k = {}; k = {find(pRFx_y_ECC >= b-r_search(coord) & pRFx_y_ECC <= b+r_search(coord) & ...
  462. pRFx_y_Polar <= a + a_search_rad & pRFx_y_Polar >= a - a_search_rad)};
  463. % ---- UPPER OBLIQUES (UR quadrant) ----
  464. elseif x_ycoords0(coord,1) > 0 && x_ycoords0(coord,2) > 0 ||x_ycoords0(coord,1) < 0 && x_ycoords0(coord,2) > 0
  465. k = {}; k = {find(pRFx_y_ECC >= b-r_search(coord) & pRFx_y_ECC <= b+r_search(coord) & ...
  466. pRFx_y_Polar >= a - a_search_rad & pRFx_y_Polar <= a + a_search_rad)};
  467. end
  468. % Distance from seed in eccentricity space
  469. D = {abs(pRFx_y_ECC(cell2mat(k))-b)};
  470. else
  471. % No angular wedge — circular window in ECC space
  472. [k,D] = rangesearch(pRFx_y,x_ycoords0(coord,:),r_search(coord),'SortIndices',false);
  473. end
  474. % Final vertex indices for this ROI
  475. D = cell2mat(D);
  476. tmp = []; tmp = cell2mat(k);
  477. vtxidx(coord).inclvtx = tmp;
  478. % --- Diagnostic printout ---
  479. fprintf('Coordinates %s [%s-%sº: %s vertices]: Minimum and maximum Euclidian distance from seed points = [%s - %s]º (search radius set to %sº) \n',...
  480. num2str(coord),num2str(min(pRFx_y_ECC(vtxidx(coord).inclvtx))),num2str(max(pRFx_y_ECC(vtxidx(coord).inclvtx))),num2str(length(vtxidx(coord).inclvtx)),...
  481. num2str(min(D)),num2str(max(D)),num2str(r_search(coord)))
  482. % figure;
  483. % ax = polarscatter(pRFx_y_Polar,pRFx_y_ECC,SizePoint,[0.9 0.9 0.9],'filled');
  484. % hold on; polarscatter(pRFx_y_Polar(vtxidx(coord).inclvtx),pRFx_y_ECC(vtxidx(coord).inclvtx),50,'filled')
  485. end
  486. %% =========================================================================
  487. % PER‑VERTEX CORTICAL CONTRAST SENSITIVITY FITTING: R = a + b * C^p
  488. % -------------------------------------------------------------------------
  489. % Fit the contrast-response function for each vertex and each SF.
  490. % =========================================================================
  491. % Select contrast indices corresponding to ContrastToPlot
  492. idxC_ToPlot = find(ismember(ContrastLabels,ContrastToPlot));
  493. C = ContrastLabels(idxC_ToPlot)/100; % normalised contrast (0–1)
  494. fprintf('\n');
  495. func = @(x, C) x(1) + x(2)*C.^x(3); % Vary exponent (if FitExponent = 2; set to 0.5)
  496. % func = @(x, C) x(1) + x(2)*C.^(x(3)+x(4))./(C.^x(4)+x(5)^x(4)); % Full Naka-Rushton equation
  497. % Initialise variables for each subject (Reason: differences in vertex count from pRF structure)
  498. x_slope_intercept = [];
  499. x_resnorm = [];
  500. x_residuals = [];
  501. x_exponent = [];
  502. % Compute slope values for each CSF condition
  503. for CSFCondition = 1:size(BetaVals_ECC,1)
  504. disp(['... ' num2str(SFLabels(CSFCondition)) 'cpd: Fitting Linear Equation (R = a+b*C) – Vertex level'])
  505. % Preallocate SF-specific outputs
  506. a = nan(1,size(BetaVals_ECC,3)); % intercept
  507. b = nan(1,size(BetaVals_ECC,3)); % slope
  508. c = nan(1,size(BetaVals_ECC,3)); % residual norm
  509. p = nan(1,size(BetaVals_ECC,3)); % exponent
  510. count = 0;
  511. % Loop over vertices
  512. for ii = 1:size(BetaVals_ECC,3)
  513. if ii == 1 || mod(ii,500) == 0
  514. disp(['Vertex #' num2str(ii)])
  515. end
  516. % Extract β-values across contrast levels for this vertex & SF
  517. R = [];
  518. R = squeeze(BetaVals_ECC(CSFCondition,:,ii))';
  519. if sum(isnan(R)) < 4 % requires minimal valid data
  520. % Call custom fitting function for fitting the square-root
  521. % function to the data
  522. [x,resnorm] = dolinearfit_SqCdata(R,C,func,FitExponent);
  523. a(ii) = x(1); % intercept set to 0
  524. b(ii) = x(2); % Slope
  525. p(ii) = x(3); % Exponent (if FitExponent = 1, it is estimated, else, it is set to 0.5)
  526. c(ii) = resnorm;
  527. % Save per-vertex fits
  528. x_slope_intercept(CSFCondition,ii,:) = [a(ii) b(ii)];
  529. x_resnorm(CSFCondition,ii,:) = c(ii);
  530. x_exponent(CSFCondition,ii,:) = p(ii);
  531. else
  532. % Save per-vertex fits
  533. x_slope_intercept(CSFCondition,ii,:) = [a(ii) b(ii)];
  534. x_resnorm(CSFCondition,ii,:) = c(ii);
  535. x_exponent(CSFCondition,ii,:) = p(ii);
  536. end
  537. end
  538. end
  539. %% =========================================================================
  540. % SECTION 6 — APPLY CORTICAL CONTRAST SENSITIVITY FITS TO ECCENTRICITY BINS (+ OPTIONAL PLOTTING)
  541. % -------------------------------------------------------------------------
  542. % This section aggregates vertex-level data within eccentricity bins and:
  543. % • Computes mean β-values per contrast and bin
  544. % • Bootstraps 95% CIs for β-values (per bin & contrast)
  545. % • Averages fitted cortical contrast sensitivity parameters (a, b, p) across vertices per bin
  546. % • Optionally plots per-subject cortical contrast sensitivity per bin
  547. % • Optionally back-projects bin-level slope values onto visual field
  548. %
  549. % Note:
  550. % - The x-axis for plots uses sqrt(C) for readability (percent ticks),
  551. % while the model itself can use either p=0.5 (fixed) or p fitted.
  552. % =========================================================================
  553. %% Eccentricity: Apply Linear Equation (R = a + b*C^p) within bins
  554. if strcmp(answer,'Low & High SF') || strcmp(answer,'SimEyeMotion') || strcmp(answer3,'Ring')
  555. % Colour per SF condition (2 colours repeated if >2 SFs)
  556. colour_ecc = repmat([120 94 240; 254 97 0]/255,round(size(BetaVals_ECC,1)/2),1);
  557. % Line handle container for legends (reused across subplots)
  558. if exist('s','var'); clear s; end
  559. % Loop over spatial frequency conditions
  560. for CSFCondition = 1:size(BetaVals_ECC,1)
  561. disp(['... ' num2str(SFLabels(CSFCondition)) 'cpd: Linear Equation Fit (R = a+b*C.^p) – Eccentricity bin level'])
  562. %% Prepare figure canvas if requested
  563. if PlotInd
  564. ncols = 5; % number of subplots per row
  565. [q,r] = quorem(sym(length(BinTick)),sym(ncols));
  566. nlines = double(q);
  567. if r > 0
  568. nlines = nlines+1;
  569. end
  570. if CSFCondition == 1
  571. idxFigs = idxFigs+1;
  572. if nlines <= 4
  573. figure(idxFigs); set(gcf,'Color','w','Position',[0 0 Rect(3) Rect(4)*nlines/4]);
  574. else
  575. figure(idxFigs); set(gcf,'Color','w','Position',Rect);
  576. end
  577. end
  578. end
  579. %% Iterate through eccentricity bins
  580. for band = 1:length(BinECC)-1
  581. % --------------- Mean β per contrast (within bin) ---------------
  582. R = [];
  583. R = squeeze(nanmean(BetaVals_ECC(CSFCondition,:,pRFx_y_ECC > BinECC(band) & pRFx_y_ECC <= BinECC(band+1)),3))'; % [1 × nContrast]
  584. Data_ECC(subs,band,CSFCondition,:) = R;
  585. % --------------- 95% CI for β via bootstrap ---------------------
  586. for c = 1:size(BetaVals_ECC,2)
  587. y = []; y = squeeze(BetaVals_ECC(CSFCondition,c,pRFx_y_ECC > BinECC(band) & pRFx_y_ECC <= BinECC(band+1))); % β over vertices
  588. if sum(isnan(y)) < length(y)-4
  589. CIBetaVals_ECC(subs,CSFCondition,band,c,:) = bootci(1000,@nanmean,y); % [2×1]
  590. else
  591. CIBetaVals_ECC(subs,CSFCondition,band,c,:) = [NaN;NaN];
  592. end
  593. end
  594. % --------------- Mean fitted params (a, b, p) per bin -----------
  595. % x_slope_intercept = [a b] per vertex; x_exponent = p
  596. FitParams_ECC(subs,CSFCondition,band,:) = [squeeze(nanmean(x_slope_intercept(CSFCondition,pRFx_y_ECC > BinECC(band) & pRFx_y_ECC <= BinECC(band+1),:),2));squeeze(nanmean(x_exponent(CSFCondition,pRFx_y_ECC > BinECC(band) & pRFx_y_ECC <= BinECC(band+1))))];
  597. % Number of contributing vertices (for diagnostics/reporting)
  598. FitParams_ECC_nvals(subs,CSFCondition,band,:) = numel(x_slope_intercept(CSFCondition,pRFx_y_ECC > BinECC(band) & pRFx_y_ECC <= BinECC(band+1),2));
  599. %% -------------------------- Plot per bin -----------------------
  600. if PlotInd
  601. subplot(nlines,ncols,band); hold on
  602. % Error bars from bootstrapped CI
  603. upper = abs(R-squeeze(CIBetaVals_ECC(subs,CSFCondition,band,:,2)));
  604. lower = abs(R-squeeze(CIBetaVals_ECC(subs,CSFCondition,band,:,1)));
  605. errorbar(sqrt(C),R,lower,upper,'Marker',MarkerType{CSFCondition},'LineStyle','none','LineWidth',1,'Color',colour_ecc(CSFCondition,:));
  606. % Overlay function fit using the mean parameters for the bin
  607. C_fit = linspace(min(C), max(C), 100);
  608. R_fit = func(FitParams_ECC(subs,CSFCondition,band,:), C_fit);
  609. s(CSFCondition) = plot(sqrt(C_fit), R_fit,lineStyle{CSFCondition},'Color',colour_ecc(CSFCondition,:),'LineWidth',2);
  610. % Axis formatting
  611. if CSFCondition == size(BetaVals_ECC,1)
  612. legend([s],[num2str(SFLabels),repmat('cpd',length(SFLabels),1)],'Location','NorthWest','box','off')
  613. end
  614. ylim([-5 15])
  615. xlabel('Contrast (%)');% xlabel('Contrast^(^0^.^5^)');
  616. xlim([0 max(C)]); % From 0-1 (normalised values)
  617. xticks(sqrt(C)); % display sqrt(C) ticks for readability
  618. xticklabels(num2str(C*100)); % label in %
  619. ylabel('Response (\beta-value)');
  620. title(['Ecc:' num2str(BinECC(band)) '–' num2str(BinECC(band+1)) 'º'])
  621. set(gca,'LineWidth',2,'FontSize',14)
  622. end
  623. end
  624. end
  625. %% =========================================================================
  626. % Eccentricity — Back-project mean slope (b) to visual space (optional)
  627. % -------------------------------------------------------------------------
  628. % For each SF condition:
  629. % • Take bin-level slope estimates (b)
  630. % • Paint them onto wedge-shaped patches in (θ, r) space
  631. % • Produce a heatmap (per subject) showing spatial distribution of b
  632. % =========================================================================
  633. if PlotInd
  634. idxFigs = idxFigs+1;
  635. figure(idxFigs);
  636. if size(BetaVals_ECC,1) > 2
  637. set(gcf,'Color','w','Position', [0 0 Rect(3) Rect(4)/2])
  638. FontSize = 22;
  639. else
  640. set(gcf,'Color','w','Position', [0 0 Rect(3)/2 Rect(4)/2])
  641. FontSize = 20;
  642. end
  643. % Convert original seeds back to polar (used to draw wedge patches)
  644. [th,r] = cart2pol(x_ycoords0(:,1),x_ycoords0(:,2));
  645. CLim = [0 12]; % colour scale limits for slope
  646. for CSFCondition = 1:size(BetaVals_ECC,1)
  647. disp(['... ' num2str(SFLabels(CSFCondition)) 'cpd: Linear Equation Fit (R = a+b*C.^p) – Back Projection Eccentricity bin level'])
  648. subplot(1,size(BetaVals_ECC,1),CSFCondition)
  649. % Vector of slope values (b) per bin for this subject & SF
  650. Data = []; Data = squeeze(FitParams_ECC(subs,CSFCondition,:,2)); % (a=1, b=2, p=3)
  651. % Draw a patch per seed wedge; reuse the same Data value for
  652. % each set of 4 polar angles (LHM, UVM, RHM, LVM) across bins.
  653. idx = 0;
  654. for coord = 1:length(x_ycoords0) % LHM, RHM, UVM, LVM
  655. % Define wedge bounds around seed (th ± a_search_rad, r ± r_search)
  656. th0 = th(coord)-a_search_rad;
  657. rh0 = r(coord)-r_search(coord);
  658. th1 = th(coord)+a_search_rad;
  659. rh1 = r(coord)+r_search(coord);
  660. a1 = linspace(th0,th0);
  661. r1 = linspace(rh0,rh1);
  662. a2 = linspace(th0,th1);
  663. r2 = linspace(rh1,rh1);
  664. a3 = linspace(th1,th1);
  665. r3 = linspace(rh1,rh0);
  666. a4 = linspace(th1,th0);
  667. r4 = linspace(rh0,rh0);
  668. [X,Y]=pol2cart([a1,a2,a3,a4],[r1,r2,r3,r4]);
  669. % move to next eccentricity bin every 4 patches
  670. if mod(coord,length(PolarCoords)) == 1 % Change Data value after each 4 coordinates (Left, Right, Up, Down)
  671. idx = idx+1;
  672. end
  673. s_ecc_avg(CSFCondition,coord) = patch(X,Y,Data(idx)); % Note: patch function takes text or matrix color def
  674. s_ecc_avg(CSFCondition,coord).LineStyle = '-';
  675. s_ecc_avg(CSFCondition,coord).EdgeColor = 'k';
  676. end
  677. % Axis & colour formatting
  678. axis([-Eccentricity Eccentricity -Eccentricity Eccentricity])
  679. axis square;
  680. yticks([-Eccentricity:Eccentricity/2:Eccentricity])
  681. xticks([-Eccentricity:Eccentricity/2:Eccentricity])
  682. ax = gca;
  683. ax.Color = colorBck;
  684. title([num2str(SFLabels(CSFCondition)) 'cpd'])
  685. colormap(colorMap);
  686. cb = colorbar; % show color bar
  687. cb.Ticks = round(linspace(0,max(CLim),4),2);
  688. ylabel(cb,['Slope'],'Rotation',270,'Position',[4 max(CLim)/2 0]);
  689. set(gca,'FontSize',FontSize,'LineWidth',2,'CLim',CLim);
  690. end
  691. end
  692. end
  693. %% =========================================================================
  694. % SECTION 7 — POLAR-ANGLE ROIs: MERIDIAN AGGREGATION, ANISOTROPY, BACK-PROJ
  695. % -------------------------------------------------------------------------
  696. % This section aggregates β-values and cortical contrast sensitivity fits along polar-angle ROIs
  697. % (meridians or quadrants), computes bootstrapped CIs, and produces:
  698. % • Per-ROI contrast-response curves (with fit overlays)
  699. % • HVA (Horizontal vs Vertical), VMA (UVM vs LVM), LRHM (Left vs Right)
  700. % • V1-wide (single-ROI) pooled measures across eccentricity
  701. % • Back-projected maps of slope (b) onto visual field (polar bins)
  702. %
  703. % Notes on gating:
  704. % - We run this section for:
  705. % 'Low & High SF' OR any SimVisualLoss with answer3 defined.
  706. % - For plotting the quadrant case, the code explicitly checks
  707. % 'Low & High SF' OR 'Quadrant' (no meridian anisotropy for ring).
  708. % =========================================================================
  709. if strcmp(answer,'Low & High SF') || exist('answer3')
  710. if strcmp(answer,'Low & High SF') || strcmp(answer3,'Quadrant')
  711. % Only compute meridian ROIs for:
  712. % • Low & High SF
  713. % • SimVisualLoss with Quadrant loss
  714. CIData_VFrois = [];
  715. CIData_VFrois_1ROI = [];
  716. for CSFCondition = 1:size(BetaVals_ECC,1)
  717. %% -------------------------------------------------------------
  718. % 7.1 Aggregate across polar-angle ROIs (per eccentricity bin)
  719. % -------------------------------------------------------------
  720. % Build Data_VFrois and FitParams_VFrois by iterating through
  721. % (bin, polar) seed coordinates in the same order as vtxidx.
  722. % The loop over idxvtx walks through each seed exactly once.
  723. % -------------------------------------------------------------
  724. idxvtx = 0;
  725. for bin = 1:length(Radius_seedroi) % eccentricity bins
  726. for coord = 1:length(x_ycoords0)/length(Radius_seedroi) % polar coords
  727. idxvtx = idxvtx + 1;
  728. % -------- Mean β for each contrast within this ROI ----
  729. for c = 1:size(BetaVals_ECC,2)
  730. % disp(['Polar = ' num2str(vtxidx(idxvtx).seed_PolarRad) ', ECC = ' num2str(vtxidx(idxvtx).seed_EccDeg) ', Contrast = ' num2str(c)])
  731. % Vertex list for this seed (bin, polar)
  732. incl = []; incl = vtxidx(idxvtx).inclvtx;
  733. Data_VFrois(subs,CSFCondition,c,bin,coord) = nanmean(BetaVals_ECC(CSFCondition,c,incl),3);
  734. % 95% bootstrap CI for β (if enough valid vertices)
  735. if numel(incl) > 2
  736. validCount = []; validCount = numel(incl) - sum(isnan(squeeze(BetaVals_ECC(CSFCondition,c,incl))));
  737. if validCount>2
  738. CIData_VFrois(subs,CSFCondition,c,bin,coord,:) = bootci(1000,@nanmean,BetaVals_ECC(CSFCondition,c,incl));
  739. else
  740. CIData_VFrois(subs,CSFCondition,c,bin,coord,:) = [NaN;NaN];
  741. end
  742. else
  743. CIData_VFrois(subs,CSFCondition,c,bin,coord,:) = [NaN;NaN];
  744. end
  745. end
  746. % -------- Mean fitted cortical contrast sensitivity params within this ROI -------
  747. incl = []; incl = vtxidx(idxvtx).inclvtx;
  748. if isempty(x_slope_intercept(CSFCondition,incl,:))
  749. FitParams_VFrois(subs,CSFCondition,bin,coord,:) = [NaN; NaN; NaN];
  750. else
  751. FitParams_VFrois_nvals(subs,CSFCondition,bin,coord,:) = numel(x_slope_intercept(CSFCondition,incl,2));
  752. FitParams_VFrois(subs,CSFCondition,bin,coord,:) = [squeeze(nanmean(x_slope_intercept(CSFCondition,incl,:),2));squeeze(nanmean(x_exponent(CSFCondition,incl)))];
  753. FitParams_VFrois_nvals(subs,CSFCondition,bin,coord,:) = numel(x_slope_intercept(CSFConditionincl,2));
  754. end
  755. % -------- Mean pRF size within this ROI ----------------
  756. VFrois_pRFSize(subs,CSFCondition,bin,coord) = nanmean(pRFSigma_ECC(vtxidx(idxvtx).inclvtx));
  757. end
  758. end
  759. % -------------------------- Shapes reminder --------------------
  760. % Data_VFrois : [sub, SF, contrast, ECC bin, Polar]
  761. % CIData_VFrois : [sub, SF, contrast, ECC bin, Polar, CI(2)]
  762. % FitParams_VFrois : [sub, SF, ECC bin, Polar, [a b p]]
  763. % FitParams_VFrois_nvals : [sub, SF, ECC bin, Polar]
  764. % VFrois_pRFSize : [sub, SF, ECC bin, Polar]
  765. %% -------------------------------------------------------------
  766. % 7.2 V1 as a SINGLE ROI (pool across eccentricities)
  767. % -------------------------------------------------------------
  768. % For each polar index, pool vertices across all bin radii,
  769. % then compute β, CIs, and fitted parameters.
  770. % -------------------------------------------------------------
  771. % Ensure PolarCoords(1) is positive to match seed_PolarRad polarity
  772. if PolarCoords(1) < 0
  773. PolarCoords(1) = abs(PolarCoords(1));
  774. end
  775. for coord = 1:length(x_ycoords0_1ROI) % LHM, UVM, RHM, LVM
  776. % Gather all vertex indices for this meridian across bins
  777. IA = []; IA = {vtxidx(ismember([vtxidx.seed_PolarRad]',PolarCoords(coord))).inclvtx};
  778. % Mean β and CI (per contrast) for pooled V1-wide ROI
  779. for c = 1:size(BetaVals_ECC,2) % For each contrast level
  780. % disp(['Polar = ' num2str(PolarCoords(coord)) ', Contrast = ' num2str(c)])
  781. Data_VFrois_1ROI(subs,CSFCondition,c,coord) = nanmean(BetaVals_ECC(CSFCondition,c,vertcat(IA{:})),3);
  782. if length(vertcat(IA{:}))>2
  783. if length(vertcat(IA{:})) - sum(isnan(squeeze(BetaVals_ECC(CSFCondition,c,vertcat(IA{:})))))>2
  784. CIData_VFrois_1ROI(subs,CSFCondition,c,coord,:) = bootci(1000,@nanmean,BetaVals_ECC(CSFCondition,c,vertcat(IA{:})));
  785. else
  786. CIData_VFrois_1ROI(subs,CSFCondition,c,coord,:) = NaN;
  787. end
  788. else
  789. CIData_VFrois_1ROI(subs,CSFCondition,c,coord,:) = NaN;
  790. end
  791. end
  792. % Mean fitted cortical contrast sensitivity params and counts
  793. FitParams_VFrois_1ROI(subs,CSFCondition,coord,:) = [squeeze(nanmean(x_slope_intercept(CSFCondition,vertcat(IA{:}),:),2));squeeze(nanmean(x_exponent(CSFCondition,vertcat(IA{:}))))];
  794. FitParams_VFrois_1ROI_nvals(subs,CSFCondition,coord,:) = numel(x_slope_intercept(CSFCondition,vertcat(IA{:}),2));
  795. % Mean pRF size
  796. VFrois_1ROI_pRFSize(subs,CSFCondition,coord) = nanmean(pRFSigma_ECC(vertcat(IA{:})));
  797. end
  798. % Restore first PolarCoords sign if it was modified
  799. if PolarCoords(1) > 0
  800. PolarCoords(1) = -(PolarCoords(1));
  801. end
  802. %% -------------------------------------------------------------
  803. % 7.3 CARDINALS ONLY — add HORIZONTAL vs VERTICAL pooled entries
  804. % -------------------------------------------------------------
  805. % Adds two extra pooled polar indices:
  806. % • HM = LHM ∪ RHM
  807. % • VM = UVM ∪ LVM
  808. % Then re-compute pooled β, CI, and fitted parameters for each.
  809. % -------------------------------------------------------------
  810. if sum(ismember(PolarCoords,[LVM,UVM,LHM,RHM])) > 1
  811. % Identify vtxidx positions for HM and VM (across bins)
  812. Horizontal_1ROI = find([vtxidx.seed_PolarRad] == 0 | abs([vtxidx.seed_PolarRad]) == pi);
  813. Vertical_1ROI = find(abs([vtxidx.seed_PolarRad]) == pi/2);
  814. tmp = []; tmp = {vtxidx(Horizontal_1ROI).inclvtx};
  815. Vtx_Hori = vertcat(tmp{:});
  816. tmp = []; tmp = {vtxidx(Vertical_1ROI).inclvtx};
  817. Vtx_Vert = vertcat(tmp{:});
  818. % Append two extra pooled polar indices at the end:
  819. % index = length(PolarCoords)+1 -> HM
  820. % index = length(PolarCoords)+2 -> VM
  821. for c = 1:size(BetaVals_ECC,2) % For each contrast level
  822. Data_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+1) = nanmean(BetaVals_ECC(CSFCondition,c,Vtx_Hori),3);
  823. Data_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+2) = nanmean(BetaVals_ECC(CSFCondition,c,Vtx_Vert),3);
  824. if length(Vtx_Hori)>2
  825. if length(Vtx_Vert) - sum(isnan(squeeze(BetaVals_ECC(CSFCondition,c,Vtx_Vert))))>2
  826. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+1,:) = bootci(1000,@nanmean,BetaVals_ECC(CSFCondition,c,Vtx_Hori));
  827. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+2,:) = bootci(1000,@nanmean,BetaVals_ECC(CSFCondition,c,Vtx_Vert));
  828. else
  829. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+1,:) = NaN;
  830. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+2,:) = NaN;
  831. end
  832. else
  833. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+1,:) = NaN;
  834. CIData_VFrois_1ROI(subs,CSFCondition,c,length(PolarCoords)+2,:) = NaN;
  835. end
  836. end
  837. % Compute fitted parameters for pooled HM and VM
  838. Horizontal_1ROI = find(PolarCoords == -pi | PolarCoords == 0);
  839. Vertical_1ROI = find(abs(PolarCoords) == pi/2);
  840. % HM
  841. FitParams_VFrois_1ROI(subs,CSFCondition,length(PolarCoords)+1,:) = squeeze(nanmean(FitParams_VFrois_1ROI(subs,CSFCondition,Horizontal_1ROI,:),3));
  842. FitParams_VFrois_1ROI_nvals(subs,CSFCondition,length(PolarCoords)+1) = squeeze(nanmean(FitParams_VFrois_1ROI_nvals(subs,CSFCondition,Horizontal_1ROI),3));
  843. VFrois_1ROI_pRFSize(subs,CSFCondition,length(PolarCoords)+1) = squeeze(nanmean(VFrois_1ROI_pRFSize(subs,CSFCondition,Horizontal_1ROI),3));
  844. % VM
  845. FitParams_VFrois_1ROI(subs,CSFCondition,length(PolarCoords)+2,:) = squeeze(nanmean(FitParams_VFrois_1ROI(subs,CSFCondition,Vertical_1ROI,:),3));
  846. FitParams_VFrois_1ROI_nvals(subs,CSFCondition,length(PolarCoords)+2) = squeeze(nanmean(FitParams_VFrois_1ROI_nvals(subs,CSFCondition,Vertical_1ROI),3));
  847. VFrois_1ROI_pRFSize(subs,CSFCondition,length(PolarCoords)+2) = squeeze(nanmean(VFrois_1ROI_pRFSize(subs,CSFCondition,Vertical_1ROI),3));
  848. %% ---------------------------------------------------------
  849. % 7.4 OPTIONAL PLOTS — pRF ROI locations + cortical contrast sensitivity per anisotropy
  850. % ---------------------------------------------------------
  851. if PlotInd
  852. disp(['... ' num2str(SFLabels(CSFCondition)) 'cpd: Linear Equation Fit (R = a+b*C.^p) – Along the meridians'])
  853. % Set up figure
  854. idxFigs = idxFigs+1;
  855. figure(idxFigs);
  856. set(gcf,'Color','w','Position', [0 0 Rect(3)/2 Rect(4)])
  857. SizePoint = 20;
  858. if exist('s','var')
  859. clear s
  860. end
  861. % 3 anisotropy panels: [HVA, VMA, LRHM]
  862. for ani = 1:3
  863. subplot(3,2,ani+(ani-1));
  864. % ---- Left column: pRF locations contributing to each ROI
  865. ax = polarscatter(pRFx_y_Polar,pRFx_y_ECC,SizePoint,[0.9 0.9 0.9],'filled');
  866. PolarIdx = [];
  867. if ani == 1 % HVA
  868. colour = repmat([0.4 0.72 0.16; 0.5 0 0.5],length(Radius_seedroi),1); % [LHM, RHM], [UVM, LVM]
  869. PolarIdx(1,:) = find([vtxidx.seed_PolarRad] == 0 | abs([vtxidx.seed_PolarRad]) == pi);
  870. PolarIdx(2,:) = find(abs([vtxidx.seed_PolarRad]) == pi/2);
  871. elseif ani == 2 % VMA
  872. colour = repmat([0 0 1; 1 0 0],length(Radius_seedroi),1); % UVM, LVM
  873. PolarIdx(1,:) = find([vtxidx.seed_PolarRad] == pi/2);
  874. PolarIdx(2,:) = find([vtxidx.seed_PolarRad] == -pi/2);
  875. elseif ani == 3 % LRHM
  876. colour = repmat([0 0.7 0.3; 0.75 0.75 0],length(Radius_seedroi),1); % LHM, RHM
  877. PolarIdx(1,:) = find(abs([vtxidx.seed_PolarRad]) == pi);
  878. PolarIdx(2,:) = find([vtxidx.seed_PolarRad] == 0);
  879. end
  880. hold on
  881. for i = 1:size(PolarIdx,1)
  882. tmp = {vtxidx(PolarIdx(i,:)).inclvtx};
  883. polarscatter(pRFx_y_Polar(vertcat(tmp{:})),pRFx_y_ECC(vertcat(tmp{:})),SizePoint,colour(i,:),'filled','MarkerEdgeColor','k');
  884. end
  885. set(ax.Parent,'FontSize',14,'LineWidth',2,'Layer','top',...
  886. 'GridAlpha',0.5,'GridColor',[0.15 0.15 0.15],'MinorGridAlpha',1,'MinorGridColor',[0.15 0.15 0.15],...
  887. 'RColor',[0.15 0.15 0.15],'RTick',[2 5:5:Eccentricity],'RTickLabelRotation',0,...
  888. 'ThetaColor',[0.15 0.15 0.15],'ThetaTickLabel',{'0','30','60','90','120','150','±180','-150','-120','-90','-60','-30'})
  889. % ---- Right column: Cortical contrast sensitivity estimates (slopes) with CI & fit curves
  890. PolarIdx = [];
  891. if ani == 1
  892. disp('....... Horizontal versus Vertical Anisotropy ')
  893. colour = [255 176 0; 100 143 255]/255; % Horizontal, Vertical
  894. PolarIdx(1,:) = length(PolarCoords)+1;
  895. PolarIdx(2,:) = length(PolarCoords)+2;
  896. LegendLabel = {'HM','VM'};
  897. TitleLabel = 'HVA';
  898. elseif ani == 2
  899. disp('....... Vertical Meridians Anisotropy ')
  900. colour = [32 189 84; 212 22 180]/255; % Upper, Lower
  901. PolarIdx(1,:) = find(PolarCoords == pi/2);
  902. PolarIdx(2,:) = find(PolarCoords == -pi/2);
  903. LegendLabel = {'UVM','LVM'};
  904. TitleLabel = 'VMA';
  905. elseif ani == 3
  906. disp('....... Left-Right Horizontal Meridians Anisotropy ')
  907. colour = [0 0 0; 128 128 128]/255; % Left, Right
  908. PolarIdx(1,:) = find(PolarCoords == -pi);
  909. PolarIdx(2,:) = find(PolarCoords == 0);
  910. LegendLabel = {'LHM','RHM'};
  911. TitleLabel = 'LRHM';
  912. end
  913. % Plot mean BETA value for each coordinates ROI
  914. subplot(3,2,2*ani); hold on
  915. if exist('e','var'); clear e; end
  916. % Mean β ± 95% CI across contrasts
  917. for i = 1:size(PolarIdx,1)
  918. pos = abs(squeeze(Data_VFrois_1ROI(subs,CSFCondition,:,PolarIdx(i,:))) - squeeze(CIData_VFrois_1ROI(subs,CSFCondition,:,PolarIdx(i,:),2)));
  919. neg = abs(squeeze(Data_VFrois_1ROI(subs,CSFCondition,:,PolarIdx(i,:))) - squeeze(CIData_VFrois_1ROI(subs,CSFCondition,:,PolarIdx(i,:),1)));
  920. e(i) = errorbar(sqrt(C),squeeze(Data_VFrois_1ROI(subs,CSFCondition,:,PolarIdx(i,:))),neg,pos,MarkerType{CSFCondition},'LineWidth',1,'Color',colour(i,:));
  921. % Overlay fit using pooled V1-wide parameters
  922. C_fit = linspace(min(C), max(C), 100);
  923. func = @(x, C) x(1) + x(2)*C.^x(3); % linear fit
  924. R_fit = func(squeeze(FitParams_VFrois_1ROI(subs,CSFCondition,PolarIdx(i,:),:)), C_fit);
  925. s(CSFCondition) = plot(sqrt(C_fit), R_fit,lineStyle{CSFCondition},'Color',colour(i,:),'LineWidth',2);
  926. end
  927. % Axis Parameters
  928. ylabel(VariableLabel);
  929. legend(e,LegendLabel,'Location','NorthWest');
  930. xlabel('Contrast (%)');% xlabel('Contrast^(^0^.^5^)');
  931. xlim([0 max(C)]); % From 0-1 (normalised values)
  932. xticks(sqrt(C));
  933. xticklabels(num2str(C*100));
  934. ylim(YAxisLim); % ylim([floor(min(min(Beta_VFrois))) ceil(max(max(Beta_VFrois)))])
  935. set(gca,'FontSize',14,'LineWidth',2,'box','off','XTickLabelRotation',45)
  936. axis square;
  937. title([TitleLabel ': ' num2str(ECCBoundaries(1)) ' – ' num2str(ECCBoundaries(end)) 'º'])
  938. end
  939. end
  940. end % if cardinals present
  941. end % CSFCondition loop
  942. %% -------------------------------------------------------------
  943. % 7.5 Polar Angle — Back-project slopes per bin & polar coord
  944. % -------------------------------------------------------------
  945. % Paints fitted slope (b) values per (bin, polar) patch.
  946. % -------------------------------------------------------------
  947. if strcmp(answer,'Low & High SF') || strcmp(answer3,'Quadrant')
  948. % Build a dense response vector per vertex from ROI fits
  949. if PlotInd
  950. idxCoords = 0;
  951. Response = zeros(size(BetaVals_ECC,1),length(pRFx_y_ECC));
  952. for bin = 1:length(Radius_seedroi)
  953. for coord = 1:length(PolarCoords) % Will ignore the added values if Cardinals
  954. idxCoords = idxCoords+1;
  955. for CSFCondition = 1:size(BetaVals_ECC,1)
  956. Response(CSFCondition,vtxidx(idxCoords).inclvtx) = squeeze(FitParams_VFrois(subs,CSFCondition,bin,coord,2));
  957. end
  958. end
  959. end
  960. idxFigs = idxFigs+1;
  961. figure(idxFigs);
  962. set(gcf,'Color','w','Position',[0 0 Rect(3)/2 Rect(4)/2]);
  963. [th,r] = cart2pol(x_ycoords0(:,1),x_ycoords0(:,2));
  964. for CSFCondition = 1:size(BetaVals_ECC,1)
  965. % Arrange slope values per (polar, ecc) in matrix form
  966. Data = [];
  967. Data = squeeze(FitParams_VFrois(subs,CSFCondition,:,1:length(PolarCoords),2)); % [bin × polar × param(b)]
  968. Data = Data'; % [Polar x Eccentricity]
  969. Data(isnan(Data)) = 0; % treat missing as 0 slope
  970. subplot(1,size(BetaVals_ECC,1),CSFCondition)
  971. % Draw wedge patches for each seed
  972. for coord = 1:length(x_ycoords0) % LHM, RHM, UVM, LVM
  973. % This function creates a patch from polar coordinates
  974. th0 = th(coord)-a_search_rad;
  975. rh0 = r(coord)-r_search(coord);
  976. th1 = th(coord) + a_search_rad;
  977. rh1 = r(coord)+r_search(coord);
  978. a1 = linspace(th0,th0);
  979. r1 = linspace(rh0,rh1);
  980. a2 = linspace(th0,th1);
  981. r2 = linspace(rh1,rh1);
  982. a3 = linspace(th1,th1);
  983. r3 = linspace(rh1,rh0);
  984. a4 = linspace(th1,th0);
  985. r4 = linspace(rh0,rh0);
  986. [X,Y]=pol2cart([a1,a2,a3,a4],[r1,r2,r3,r4]);
  987. s2vf(subs,CSFCondition,coord) = patch(X,Y,Data(coord)); % Note: patch function takes text or matrix color def
  988. end
  989. axis([-Eccentricity Eccentricity -Eccentricity Eccentricity])
  990. axis square;
  991. xticks([-Eccentricity:Eccentricity/2:Eccentricity])
  992. yticks([-Eccentricity:Eccentricity/2:Eccentricity])
  993. ax = gca;
  994. ax.Color = colorBck;
  995. ax.AmbientLightColor = [0 0 0];
  996. title(strcat(num2str(SFLabels(CSFCondition)),'cpd'))
  997. colormap(colorMap);
  998. cb = colorbar; % show color bar
  999. cb.Ticks = ceil(linspace(0,max(CLim),4));
  1000. ylabel(cb,['Slope'],'Rotation',270,'Position',[4 max(CLim)/2 0]);
  1001. set(gca,'FontSize',14,'LineWidth',2,'CLim',CLim);
  1002. end
  1003. end
  1004. end
  1005. %% -------------------------------------------------------------
  1006. % 7.6 Polar Angle — Back-project (1 ROI, V1-wide)
  1007. % -------------------------------------------------------------
  1008. % For Low & High SF, pool across eccentricity and paint a
  1009. % single wedge per cardinal meridian using the V1-wide slopes.
  1010. % -------------------------------------------------------------
  1011. if strcmp(answer,'Low & High SF')
  1012. if PlotInd
  1013. % Ensure PolarCoords(1) is positive for indexing below
  1014. if PolarCoords(1) < 0; PolarCoords(1) = abs(PolarCoords(1)); end
  1015. Response_1ROI = NaN(size(BetaVals_ECC,1),length(pRFx_y_ECC));
  1016. % Assign pooled (across ecc) slope per meridian to all vertices in that meridian
  1017. for coord = 1:length(PolarCoords) % Will ignore the added values for HVA (5) and VMA (6) if Cardinals
  1018. idxCoords = find([vtxidx.seed_PolarRad] == PolarCoords(coord));
  1019. for CSFCondition = 1:size(BetaVals_ECC,1)
  1020. tmp = []; tmp = {vtxidx(idxCoords).inclvtx};
  1021. Response_1ROI(CSFCondition,vertcat(tmp{:})) = nanmean(squeeze(FitParams_VFrois(subs,CSFCondition,:,coord,2)),1);
  1022. end
  1023. end
  1024. idxFigs = idxFigs+1;
  1025. figure(idxFigs);
  1026. set(gcf,'Color','w','Position',[0 0 Rect(3)/2 Rect(4)/2]);
  1027. [th_1ROI,r_1ROI] = cart2pol(x_ycoords0_1ROI(:,1),x_ycoords0_1ROI(:,2));
  1028. for CSFCondition = 1:size(BetaVals_ECC,1)
  1029. % Pooled slope per polar (mean across bins)
  1030. Data = [];
  1031. Data = nanmean(squeeze(FitParams_VFrois(subs,CSFCondition,:,1:length(PolarCoords),2)),1);
  1032. Data = Data'; % [Polar x Eccentricity]
  1033. subplot(1,size(BetaVals_ECC,1),CSFCondition)
  1034. % Draw a single wedge per cardinal meridian
  1035. for coord = 1:length(x_ycoords0_1ROI) % LHM, RHM, UVM, LVM
  1036. % This function creates a patch from polar coordinates
  1037. th0 = th_1ROI(coord)-a_search_rad;
  1038. rh0 = r_1ROI(coord)-r_search_1ROI(coord);
  1039. th1 = th_1ROI(coord) + a_search_rad;
  1040. rh1 = r_1ROI(coord)+r_search_1ROI(coord);
  1041. a1 = linspace(th0,th0);
  1042. r1 = linspace(rh0,rh1);
  1043. a2 = linspace(th0,th1);
  1044. r2 = linspace(rh1,rh1);
  1045. a3 = linspace(th1,th1);
  1046. r3 = linspace(rh1,rh0);
  1047. a4 = linspace(th1,th0);
  1048. r4 = linspace(rh0,rh0);
  1049. [X,Y]=pol2cart([a1,a2,a3,a4],[r1,r2,r3,r4]);
  1050. s2vf(subs,CSFCondition,coord) = patch(X,Y,Data(coord)); % Note: patch function takes text or matrix color def
  1051. end
  1052. axis([-Eccentricity Eccentricity -Eccentricity Eccentricity])
  1053. axis square;
  1054. xticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1055. yticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1056. ax = gca;
  1057. ax.Color = colorBck;
  1058. title(strcat(num2str(SFLabels(CSFCondition)),'cpd'))
  1059. colormap(colorMap);
  1060. cb = colorbar; % show color bar
  1061. cb.Ticks = ceil(linspace(0,max(CLim),4));
  1062. ylabel(cb,['Slope'],'Rotation',270,'Position',[4 max(CLim)/2 0]);
  1063. set(gca,'FontSize',14,'LineWidth',2,'CLim',CLim);
  1064. end
  1065. % Restore PolarCoords sign if modified
  1066. if PolarCoords(1) > 0; PolarCoords(1) = -PolarCoords(1); end
  1067. end
  1068. end
  1069. end
  1070. end % if 'Low & High SF' || exist(answer3)
  1071. end % SECTION 7
  1072. %% =========================================================================
  1073. % SECTION 8 — GROUP LEVEL (ONLY for "Low & High SF")
  1074. % -------------------------------------------------------------------------
  1075. % This section computes group-level summaries and figures if:
  1076. % • At least 3 subjects were selected
  1077. % • The chosen condition is 'Low & High SF'
  1078. %
  1079. % It produces:
  1080. % • Group mean cortical contrast sensitivity per eccentricity bin (with 95% CI and subject overlays)
  1081. % • Group back-projection of eccentricity-binned slopes (b) to visual space
  1082. % • Group anisotropy plots (HVA/VMA/LRHM) from V1-wide pooled ROIs
  1083. % • Group back-projection of V1-wide polar slopes to visual space
  1084. %
  1085. % Shapes reminder of inputs aggregated here:
  1086. % Data_ECC(sub, bin, SF, contrast)
  1087. % FitParams_ECC(sub, SF, bin, [a b p])
  1088. %
  1089. % Data_VFrois(sub, SF, contrast, bin, polar)
  1090. % FitParams_VFrois(sub, SF, bin, polar, [a b p])
  1091. %
  1092. % Data_VFrois_1ROI(sub, SF, contrast, polar)
  1093. % FitParams_VFrois_1ROI(sub, SF, polar, [a b p])
  1094. % =========================================================================
  1095. if length(unique(selectfn(indx))) >= 3 & strcmp(answer,'Low & High SF')
  1096. idxFigs = idxFigs+1;
  1097. disp('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
  1098. disp('%%%%%%%%%%%%%%%%%%%%%%%% GROUP LEVEL %%%%%%%%%%%%%%%%%%%%%%%%')
  1099. disp('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
  1100. % Containers for group means and CIs
  1101. MeanBeta_VFrois = []; MeanCIBeta_VFrois = [];
  1102. MeanBeta_VFrois_1ROI = []; MeanCIBeta_VFrois_1ROI = [];
  1103. MeanFitParams_VFrois = []; MeanFitParams_VFrois_1ROI = [];
  1104. MeanFitParams_ECC = [];
  1105. % Use the specific contrast set selected for plotting at group level
  1106. C = ContrastToPlot'/100;
  1107. %% ---------------------------------------------------------------------
  1108. % 8.1 Group summaries for Polar ROIs across eccentricity bins
  1109. % ----------------------------------------------------------------------
  1110. for CSFCondition = 1:size(Data_VFrois,2)
  1111. % Grand mean across subjects
  1112. % Data_VFrois: [sub, SF, contrast, bin, polar]
  1113. MeanBeta_VFrois(CSFCondition,:,:,:) = squeeze(nanmean(Data_VFrois(:,CSFCondition,:,:,:),1));
  1114. MeanBeta_VFrois_1ROI(CSFCondition,:,:) = squeeze(nanmean(Data_VFrois_1ROI(:,CSFCondition,:,:),1));
  1115. MeanFitParams_VFrois(CSFCondition,:,:,:) = squeeze(nanmean(FitParams_VFrois(:,CSFCondition,:,:,:),1));
  1116. MeanFitParams_VFrois_1ROI(CSFCondition,:,:) = squeeze(nanmean(FitParams_VFrois_1ROI(:,CSFCondition,:,:),1));
  1117. % Bootstrap CI across subjects for each (contrast, bin, polar)
  1118. for bin = 1:length(Radius_seedroi)
  1119. for c = 1:length(ContrastToPlot)
  1120. for coord = 1:size(Data_VFrois,5)
  1121. % Extract subject samples for this cell
  1122. if sum(isnan(Data_VFrois(:,CSFCondition,c,bin,coord))) >= 3
  1123. MeanCIBeta_VFrois(CSFCondition,c,bin,coord,:) = NaN;
  1124. else
  1125. MeanCIBeta_VFrois(CSFCondition,c,bin,coord,:) = squeeze(bootci(1000,@nanmean,Data_VFrois(:,CSFCondition,c,bin,coord)))';
  1126. end
  1127. end
  1128. end
  1129. end
  1130. end
  1131. %% ---------------------------------------------------------------------
  1132. % 8.2 Group summaries for V1-wide pooled ROIs (single-ROI)
  1133. % ----------------------------------------------------------------------
  1134. for CSFCondition = 1:size(Data_VFrois,2)
  1135. % Grand mean across subjects
  1136. % Data_VFrois_1ROI: [sub, SF, contrast, polar]
  1137. MeanBeta_VFrois_1ROI(CSFCondition,:,:) = squeeze(nanmean(Data_VFrois_1ROI(:,CSFCondition,:,:),1));
  1138. MeanFitParams_VFrois_1ROI(CSFCondition,:,:) = squeeze(nanmean(FitParams_VFrois_1ROI(:,CSFCondition,:,:),1));
  1139. % Bootstrap CI across subjects for each (contrast, bin, polar)
  1140. for bin = 1:length(Radius_seedroi)
  1141. for c = 1:length(ContrastToPlot)
  1142. for coord = 1:size(Data_VFrois_1ROI,4)
  1143. if sum(isnan(Data_VFrois_1ROI(:,CSFCondition,c,coord))) >= 3
  1144. MeanCIBeta_VFrois_1ROI(CSFCondition,c,coord,:) = NaN;
  1145. else
  1146. MeanCIBeta_VFrois_1ROI(CSFCondition,c,coord,:) = squeeze(bootci(1000,@nanmean,Data_VFrois_1ROI(:,CSFCondition,c,coord)))';
  1147. end
  1148. end
  1149. end
  1150. end
  1151. end
  1152. %% ---------------------------------------------------------------------
  1153. % 8.3 Group summaries for Eccentricity-binned cortical contrast sensitivity
  1154. % ----------------------------------------------------------------------
  1155. MeanCIBetaVals_ECC = []; % [SF, bin, contrast, CI=2]
  1156. if exist('s','var'); clear s; end
  1157. % Colour by SF condition (two-colour palette repeated if needed)
  1158. colour_ecc = repmat([0 0 255; 255 0 0]/255,round(size(Data_ECC,3)/2),1);
  1159. for CSFCondition = 1:size(Data_ECC,3)
  1160. disp(['... ' num2str(SFLabels(CSFCondition)) 'cpd: Linear Equation Fit (R = a+b*C.^p): Eccentricity bin level'])
  1161. % Prepare grouped figure canvas (grid by eccentricity bins)
  1162. if CSFCondition == 1
  1163. idxFigs = idxFigs+1;
  1164. ncols = 5;
  1165. [q,r] = quorem(sym(length(BinTick)),sym(ncols));
  1166. nlines = double(q);
  1167. if r > 0
  1168. nlines = nlines+1;
  1169. end
  1170. if nlines <= 4
  1171. figure(idxFigs); set(gcf,'Color','w','Position',[0 0 Rect(3) Rect(4)*nlines/4]);
  1172. else
  1173. figure(idxFigs); set(gcf,'Color','w','Position',Rect);
  1174. end
  1175. end
  1176. % Loop over eccentricity bins
  1177. for band = 1:length(BinECC)-1
  1178. % Group mean β across subjects (per contrast)
  1179. % Data_ECC: [sub, bin, SF, contrast]
  1180. R = []; R = squeeze(nanmean(Data_ECC(:,band,CSFCondition,:),1));
  1181. % Group CI across subjects for β
  1182. y = []; y = squeeze(Data_ECC(:,band,CSFCondition,:));
  1183. MeanCIBetaVals_ECC(CSFCondition,band,:,:) = bootci(1000,@nanmean,y)';
  1184. % Group mean fit params across subjects (a,b,p)
  1185. MeanFitParams_ECC(CSFCondition,band,:) = squeeze(nanmean(FitParams_ECC(:,CSFCondition,band,:),1));
  1186. % ---- Plot in corresponding bin subplot ----
  1187. hold on; subplot(nlines,ncols,band);
  1188. % Error bars from group CI
  1189. upper = []; upper = abs(R-squeeze(MeanCIBetaVals_ECC(CSFCondition,band,:,2)));
  1190. lower = []; lower = abs(R-squeeze(MeanCIBetaVals_ECC(CSFCondition,band,:,1)));
  1191. errorbar(sqrt(C),R,lower,upper,'Marker','o','LineStyle','none','LineWidth',1,'Color',colour_ecc(CSFCondition,:));
  1192. % Group fit overlay
  1193. hold on;
  1194. C_fit = linspace(min(C), max(C), 100);
  1195. func = @(x, C) x(1) + x(2)*C.^x(3); % linear fit
  1196. R_fit = func(MeanFitParams_ECC(CSFCondition,band,:), C_fit);
  1197. s(CSFCondition) = plot(sqrt(C_fit), R_fit,'-','Color',colour_ecc(CSFCondition,:),'LineWidth',2);
  1198. % Individual subject fits overlaid faintly
  1199. for subs = 1:size(FitParams_ECC,1)
  1200. R_fit_ind = func(FitParams_ECC(subs,CSFCondition,band,:), C_fit);
  1201. plot(sqrt(C_fit), R_fit_ind,'-','Color',[colour_ecc(CSFCondition,:) 0.2],'LineWidth',1);
  1202. end
  1203. % Formatting
  1204. if CSFCondition == length(unique(SFLabels))
  1205. legend([s],strcat(num2str(unique(SFLabels)),repmat('cpd',length(unique(SFLabels)),1)),'Location','NorthWest','box','off')
  1206. end
  1207. xlabel('Contrast (%)');% xlabel('Contrast^(^0^.^5^)');
  1208. xlim([0 max(C)]); % From 0-1 (normalised values)
  1209. xticks(sqrt(C));
  1210. xticklabels(num2str(C*100));
  1211. ylabel('\beta-value');
  1212. ylim([-5 15]); axis square;
  1213. title(num2str(SFLabels(CSFCondition)))
  1214. title([num2str(BinECC(band)) '–' num2str(BinECC(band+1)) 'º'])
  1215. set(gca,'LineWidth',2,'FontSize',14,'box','off')
  1216. end
  1217. end
  1218. %% ---------------------------------------------------------------------
  1219. % 8.4 Group back-projection to visual space — Eccentricity bins
  1220. % ----------------------------------------------------------------------
  1221. idxFigs = idxFigs+1;
  1222. figure(idxFigs);
  1223. set(gcf,'Color','w','Position', [0 0 Rect(3) Rect(4)/3])
  1224. [th,r] = cart2pol(x_ycoords0(:,1),x_ycoords0(:,2));
  1225. YAxisLim = [0 12]; % colour scale for slopes
  1226. for CSFCondition = 1:size(MeanFitParams_ECC,1)
  1227. % Mean slope (b) across eccentricity bins for this SF
  1228. R = []; R = squeeze(nanmean(MeanFitParams_ECC(CSFCondition,:,2),1)); % slope b only
  1229. subplot(1,size(MeanFitParams_ECC,1),CSFCondition)
  1230. idx = 0;
  1231. % Draw wedge patches; advance R index every 4 polar patches
  1232. for coord = 1:length(x_ycoords0) % LHM, RHM, UVM, LVM
  1233. th0 = th(coord)-a_search_rad;
  1234. rh0 = r(coord)-r_search(coord);
  1235. th1 = th(coord)+a_search_rad;
  1236. rh1 = r(coord)+r_search(coord);
  1237. a1 = linspace(th0,th0);
  1238. r1 = linspace(rh0,rh1);
  1239. a2 = linspace(th0,th1);
  1240. r2 = linspace(rh1,rh1);
  1241. a3 = linspace(th1,th1);
  1242. r3 = linspace(rh1,rh0);
  1243. a4 = linspace(th1,th0);
  1244. r4 = linspace(rh0,rh0);
  1245. [X,Y]=pol2cart([a1,a2,a3,a4],[r1,r2,r3,r4]);
  1246. if mod(coord,4) == 1 % Change Data value after each 4 coordinates (Left, Right, Up, Down)
  1247. idx = idx+1;
  1248. s_ecc_avg(CSFCondition,coord) = patch(X,Y,R(idx)); % Note: patch function takes text or matrix color def
  1249. else
  1250. s_ecc_avg(CSFCondition,coord) = patch(X,Y,R(idx)); % Note: patch function takes text or matrix color def
  1251. end
  1252. end
  1253. % Formatting
  1254. axis([-Eccentricity Eccentricity -Eccentricity Eccentricity])
  1255. axis square;
  1256. xticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1257. yticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1258. ax = gca;
  1259. ax.Color = [1 1 1];
  1260. title(strcat(num2str(SFLabels(CSFCondition)),'cpd'))
  1261. colormap(colorMap);
  1262. cb = colorbar; % show color bar
  1263. ylabel(cb,['Slope'],'Rotation',270,'Position',[4 YAxisLim(2)/2 0]);
  1264. set(gca,'FontSize',22,'LineWidth',2,'CLim',YAxisLim);
  1265. end
  1266. %% ---------------------------------------------------------------------
  1267. % 8.5 Group anisotropy plots — V1-wide pooled (HVA, VMA, LRHM)
  1268. % ----------------------------------------------------------------------
  1269. nlines = 1;
  1270. ncols = 3;
  1271. for CSFCondition = 1:size(MeanCIBeta_VFrois_1ROI,1)
  1272. % Open figure
  1273. idxFigs = idxFigs+1;
  1274. figure(idxFigs);
  1275. idxPrevious = 0;
  1276. set(gcf,'Color','w','Position', [0 0 Rect(3)*2/3 Rect(4)/2])
  1277. FontSize = 22;
  1278. YAxisLim = [-5 15];
  1279. if exist('s','var'); clear s; end
  1280. % Three panels: HVA, VMA, LRHM
  1281. for ani = 1:3
  1282. % Plot anisotropy data
  1283. PolarIdx = [];
  1284. % Choose ROI pairs, colours, labels, titles
  1285. if ani == 1
  1286. disp(['....... ' num2str(SFLabels(CSFCondition)) 'cpd: Horizontal versus Vertical Anisotropy '])
  1287. colour = [102 184 42; 128 0 128]/255; % Horizontal, Vertical
  1288. PolarIdx(1,:) = length(PolarCoords)+1;
  1289. PolarIdx(2,:) = length(PolarCoords)+2;
  1290. LegendLabel = {'HM','VM'};
  1291. TitleLabel = 'HVA';
  1292. elseif ani == 2
  1293. disp(['....... ' num2str(SFLabels(CSFCondition)) 'cpd: Vertical Meridians Anisotropy '])
  1294. colour = [0 0 255; 255 0 0]/255; % Upper, Lower
  1295. PolarIdx(1,:) = find(PolarCoords == pi/2);
  1296. PolarIdx(2,:) = find(PolarCoords == -pi/2);
  1297. LegendLabel = {'UVM','LVM'};
  1298. TitleLabel = 'VMA';
  1299. elseif ani == 3
  1300. disp(['....... ' num2str(SFLabels(CSFCondition)) 'cpd: Left-Right Horizontal Meridians Anisotropy '])
  1301. colour = [0 0 0; 128 128 128]/255; % Left, Right
  1302. PolarIdx(1,:) = find(PolarCoords == -pi);
  1303. PolarIdx(2,:) = find(PolarCoords == 0);
  1304. LegendLabel = {'LHM','RHM'};
  1305. TitleLabel = 'LRHM';
  1306. end
  1307. subplot(nlines,ncols,ani)
  1308. if exist('e','var'); clear e; end
  1309. for i = 1:size(PolarIdx,1)
  1310. hold on
  1311. % Mean ± 95% CI across subjects
  1312. R = squeeze(MeanBeta_VFrois_1ROI(CSFCondition,:,PolarIdx(i,:)))';
  1313. pos = abs(R - squeeze(MeanCIBeta_VFrois_1ROI(CSFCondition,:,PolarIdx(i,:),2))');
  1314. neg = abs(R - squeeze(MeanCIBeta_VFrois_1ROI(CSFCondition,:,PolarIdx(i,:),1))');
  1315. e(i) = errorbar(sqrt(C),R,neg,pos,'o','LineWidth',1,'Color',colour(i,:));
  1316. % Fit using group mean params
  1317. C_fit = linspace(min(C), max(C), 100);
  1318. func = @(x, C) x(1) + x(2)*C.^x(3); % linear fit
  1319. R_fit = func(MeanFitParams_VFrois_1ROI(CSFCondition,PolarIdx(i,:),:), C_fit);
  1320. s(CSFCondition) = plot(sqrt(C_fit), R_fit,'-','Color',colour(i,:),'LineWidth',2);
  1321. % Individual subjects (faint) for visual variability
  1322. for subs = 1:size(FitParams_VFrois_1ROI,1)
  1323. R_fit_ind = func(squeeze(FitParams_VFrois_1ROI(subs,CSFCondition,PolarIdx(i,:),:)), C_fit);
  1324. plot(sqrt(C_fit), R_fit_ind,'-','Color',[colour(i,:) 0.2],'LineWidth',1);
  1325. end
  1326. end
  1327. % Formatting
  1328. l = legend(e,LegendLabel,'Location','NorthWest');
  1329. ylabel(VariableLabel); ylim(YAxisLim);
  1330. xlabel('Contrast (%)');
  1331. xlim([0 max(C)]);
  1332. xticks(sqrt(C));
  1333. xticklabels(num2str(C*100));
  1334. axis square
  1335. title([TitleLabel ': ' num2str(ECCBoundaries(bin)) '–' num2str(ECCBoundaries(bin+1)) 'º']);
  1336. set(gca,'FontSize',FontSize,'LineWidth',2,'box','off','XTickLabelRotation',45)
  1337. end
  1338. end
  1339. %% ---------------------------------------------------------------------
  1340. % 8.6 Group back-projection to visual space — V1-wide polar
  1341. % ----------------------------------------------------------------------
  1342. idxFigs = idxFigs+1;
  1343. figure(idxFigs);
  1344. set(gcf,'Color','w','Position', [0 0 Rect(3) Rect(4)/3])
  1345. [th_1ROI,r_1ROI] = cart2pol(x_ycoords0_1ROI(:,1),x_ycoords0_1ROI(:,2));
  1346. YAxisLim = [0 12];
  1347. for CSFCondition = 1:size(MeanFitParams_VFrois_1ROI,1)
  1348. % Group mean slope (b) per cardinal meridian
  1349. Data = [];
  1350. Data = squeeze(MeanFitParams_VFrois_1ROI(CSFCondition,:,2))';
  1351. % Data needs to be length(PolarCoords)*length(Radius_seedroi)
  1352. % Draw a wedge per meridian (pooled across eccentricities)
  1353. subplot(1,size(MeanFitParams_VFrois_1ROI,1),CSFCondition)
  1354. for coord = 1:length(x_ycoords0_1ROI) % LHM, RHM, UVM, LVM
  1355. % This function creates a patch from polar coordinates
  1356. th0 = th_1ROI(coord)-a_search_rad;
  1357. rh0 = r_1ROI(coord)-r_search_1ROI(coord);
  1358. th1 = th_1ROI(coord)+a_search_rad;
  1359. rh1 = r_1ROI(coord)+r_search_1ROI(coord);
  1360. a1 = linspace(th0,th0);
  1361. r1 = linspace(rh0,rh1);
  1362. a2 = linspace(th0,th1);
  1363. r2 = linspace(rh1,rh1);
  1364. a3 = linspace(th1,th1);
  1365. r3 = linspace(rh1,rh0);
  1366. a4 = linspace(th1,th0);
  1367. r4 = linspace(rh0,rh0);
  1368. [X,Y]=pol2cart([a1,a2,a3,a4],[r1,r2,r3,r4]);
  1369. s_ecc_avg(CSFCondition,coord) = patch(X,Y,Data(coord));
  1370. end
  1371. % Formatting
  1372. axis([-Eccentricity Eccentricity -Eccentricity Eccentricity])
  1373. axis square;
  1374. xticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1375. yticks([-Eccentricity:Eccentricity/2:Eccentricity])
  1376. ax = gca;
  1377. ax.Color = [1 1 1];
  1378. title(strcat(num2str(SFLabels(CSFCondition)),'cpd'))
  1379. colormap(colorMap);
  1380. cb = colorbar;
  1381. ylabel(cb,['Slope'],'Rotation',270,'Position',[4 YAxisLim(2)/2 0]);
  1382. set(gca,'FontSize',22,'LineWidth',2,'CLim',YAxisLim);
  1383. end
  1384. end % GROUP LEVEL

BackProjection_Slopes_Controls.m at commit 0014b8f, under MIT · at the source

Overview

Authors: Hugo T Chow-Wing-Bom1,2, Matteo Lisi3, Noah C Benson4, Freya Lygo-Frett5, Patrick Yu-Wai-Man1,6,7,8, Frederic Dick2,9, Roni O Maimon-Mor1, Tessa M Dekker1,2,9
  1. Institute of Ophthalmology, University College London (UCL), London, United Kingdom
  2. Birkbeck/UCL Centre for NeuroImaging, London, United Kingdom
  3. Department of Psychology, Royal Holloway, University of London, London, United Kingdom
  4. eScience Institute, University of Washington, Seattle, United States
  5. Institute of Cognitive Neuroscience, UCL, London, United Kingdom
  6. John van Geest Centre for Brain Repair and MRC Mitochondrial Biology Unit, Department of Clinical Neurosciences, University of Cambridge, Cambridge, United Kingdom
  7. Cambridge Eye Unit, Addenbrooke’s Hospital, Cambridge University Hospitals NHS Foundation Trust, Cambridge, United Kingdom
  8. Moorfields Eye Hospital NHS Foundation Trust, London, United Kingdom
  9. Department of Experimental Psychology, UCL, London, United Kingdom
Journal: eLife, volume 14, article RP105930
Dates: published online 2 October 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7554/elife.105930 · PMID 42826108 · PMCID PMC13633074 · OpenAlex W4409001708
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: fMRI (modality), human (organism), systems (subfield)
Methods: Connectivity, fMRI & imaging, Physiology & signal measures, Preprocessing
Keywords: Human
MeSH: Brain Mapping*, Contrast Sensitivity*, Magnetic Resonance Imaging*, Primary Visual Cortex*, Visual Fields*, Adult, Female, Humans, Male, Middle Aged, Scotoma, Young Adult (* major topic)
Topic: Visual perception and processing mechanisms (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Wellcome Trust (10.35802/306332); Moorfields Eye Charity (PhD Studentship GR001315)
Citations: not cited yet (Europe PMC); 53 references in the paper

Abstract

Peripheral vision is crucial for daily activities and quality of life, yet traditional measures of visual function like visual acuity primarily assess central vision. Visual field tests can evaluate peripheral vision but require extended focus and precise fixation, often challenging for patients with severe sight loss. Functional MRI (fMRI) with population receptive field (pRF) mapping offers a non-invasive way to map scotomas but relies on single contrast levels and accurate fixation. We developed an fMRI-based approach to measure contrast sensitivity across the visual field without requiring precise fixation. Combining large-field stimulation with varying spatial frequencies and contrast levels, and either pRF mapping or a structure-based retinotopic atlas, we modeled contrast sensitivity in the primary visual cortex (V1) over a large (40 deg) expanse of the visual field. In seven normally sighted participants, we characterized differences in V1 sensitivity across eccentricities and visual quadrants, finding reliable and reproducible patterns at individual and session levels. To test robustness to fixation variability, we investigated how varying levels of eye movement affected V1 sensitivity patterns in two participants: cortical sensitivity patterns were largely preserved despite eye movements, particularly at low spatial frequencies. This suggests our approach can accommodate several degrees of fixation instability, making it suitable for populations with unstable or biased fixation for whom visual field maps are harder to acquire behaviorally (e.g., patients with dense central scotoma or strabismus). Additionally, our method effectively visualized simulated and disease-linked sensitivity loss at the cortical level. Crucially, these results could be largely recovered using the structure-based retinotopic atlas, eliminating the need for pRF mapping and precise fixation, although with reduced sensitivity. This approach, integrating large-field stimulation with a retinotopic atlas, offers a promising tool for monitoring vision loss and recovery across a range of visual impairments, addressing a significant challenge in current clinical assessments.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

HugoCWB/ChowWingBomEtAl_2025_eLife

License: MIT
State: the link answers, verified on 5 October 2026
Evidence: files inventoried
Commit: 0014b8fa0797ece45b1bf5cadca99a20e4999f25, 16 April 2026
Languages: MATLAB (4)
Size: 6 files, 4 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 5 October 2026: the link answers
  • 5 October 2026: the link answers
6 files

hugocwb/adjustecctool

License: MIT
State: the link answers, verified on 5 October 2026
Evidence: files inventoried
Commit: 85bba2af3530612050f0166f20343594395e3f30, 25 March 2026
Languages: Python (2)
Size: 7 files, 2 scripts
Software Heritage: not archived
Found in: the references
Holds: README, license file, environment (Dockerfile, pyproject.toml)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (2 files), Matplotlib (1 file)
Availability: 1 check, the latest on 5 October 2026: the link answers
  • 5 October 2026: the link answers
4 files

Zenodo 18770065

License: GPL-3.0
State: the link answers, verified on 5 October 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 5 October 2026: the link answers (HTTP 200)
  • 5 October 2026: the link answers (HTTP 200)

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 6 scripts, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability

All post-processed and anonymized data are available on Zenodo (https://doi.org/10.5281/zenodo.19051439). Codes to reproduce the figures from the provided dataset are available on GitHub (https://github.com/HugoCWB/ChowWingBomEtAl_2025_eLife, copy archived at Chow-Wing-Bom, 2026b). Codes to adjust the Benson maps' eccentricity are available on GitHub (https://github.com/HugoCWB/AdjustEccTool, copy archived at Chow-Wing-Bom and Benson, 2026a). A Docker image for the Benson eccentricity adjustment toolbox is also available on Zenodo (https://doi.org/10.5281/zenodo.18770065). The original raw data cannot be shared publicly because of participant consent and ethical approval restrictions. Access can be requested by emailing the corresponding author, Dr Hugo Chow-Wing-Bom, or the senior author, Prof Tessa Dekker. Requests should include a brief project proposal and will be assessed by Prof Dekker and the study team. Access will be granted subject to a data sharing agreement between the requesting organisation and UCL.

The following datasets were generated:

Chow-Wing-Bom H, Lisi M, Benson N, Lygo-Frett F, Yu-Wai-Man P, Dick F, Maison-Mor R, Dekker T. 2026. Dataset for Chow-Wing-Bom et al. 2025, eLife. Zenodo.

Chow-Wing-Bom H, Benson N. 2026. Benson Map Eccentricity Adjustment Tool. Zenodo.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 6 October 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 8 authors, 1 keyword, 12 MeSH terms, 2 funders, 48 references.

Cite

This paper

Chow-Wing-Bom, H. T., Lisi, M., Benson, N. C., Lygo-Frett, F., Yu-Wai-Man, P., Dick, F., Maimon-Mor, R. O., & Dekker, T. M. (2026). Mapping human visual contrast sensitivity and vision loss across the visual field with model-based fMRI. eLife, 14, RP105930. https://doi.org/10.7554/elife.105930

BibTeX

@article{chowwingbom2026mapping,
author = {Chow-Wing-Bom, Hugo T and Lisi, Matteo and Benson, Noah C and Lygo-Frett, Freya and Yu-Wai-Man, Patrick and Dick, Frederic and Maimon-Mor, Roni O and Dekker, Tessa M},
title = {{Mapping human visual contrast sensitivity and vision loss across the visual field with model-based fMRI}},
journal = {eLife},
year = {2026},
month = oct,
volume = {14},
pages = {RP105930},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/elife.105930},
url = {https://doi.org/10.7554/elife.105930},
pmid = {42826108},
pmcid = {PMC13633074}
}

RIS

TY - JOUR
AU - Chow-Wing-Bom, Hugo T
AU - Lisi, Matteo
AU - Benson, Noah C
AU - Lygo-Frett, Freya
AU - Yu-Wai-Man, Patrick
AU - Dick, Frederic
AU - Maimon-Mor, Roni O
AU - Dekker, Tessa M
TI - Mapping human visual contrast sensitivity and vision loss across the visual field with model-based fMRI
T2 - eLife
J2 - eLife
PY - 2026
DA - 2026/10/02
VL - 14
SP - RP105930
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/elife.105930
UR - https://doi.org/10.7554/elife.105930
LA - en
ER -

CSL-JSON

{
"id": "10.7554/elife.105930",
"type": "article-journal",
"title": "Mapping human visual contrast sensitivity and vision loss across the visual field with model-based fMRI",
"container-title": "eLife",
"author": [
{
"family": "Chow-Wing-Bom",
"given": "Hugo T"
},
{
"family": "Lisi",
"given": "Matteo"
},
{
"family": "Benson",
"given": "Noah C"
},
{
"family": "Lygo-Frett",
"given": "Freya"
},
{
"family": "Yu-Wai-Man",
"given": "Patrick"
},
{
"family": "Dick",
"given": "Frederic"
},
{
"family": "Maimon-Mor",
"given": "Roni O"
},
{
"family": "Dekker",
"given": "Tessa M"
}
],
"container-title-short": "eLife",
"volume": "14",
"page": "RP105930",
"DOI": "10.7554/elife.105930",
"PMID": "42826108",
"PMCID": "PMC13633074",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://doi.org/10.7554/elife.105930",
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
2
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1101/2024.10.29.619403 [code]
Mapping Visual Contrast Sensitivity and Vision Loss Across the Visual Field with Model-Based fMRI
Journal: bioRxiv (preprint)
In common: Optimization Toolbox, Statistics and Machine Learning Toolbox, Matplotlib, 1 other tool, Zenodo 19051439, fMRI, 31 references, 5 authors
[2] doi:10.1038/s41467-022-31041-9 [code]
Linking individual differences in human primary visual cortex to contrast sensitivity around the visual field.
Journal: Nature communications
In common: Statistics and Machine Learning Toolbox, systems, 12 references
[3] doi:10.1371/journal.pcbi.1013599 [code]
Unpacking the V1 map: Differential covariation of preferred spatial frequency and cortical magnification across spatial dimensions.
Journal: PLoS computational biology
In common: Matplotlib, NumPy, fMRI, systems, 11 references
[4] doi:10.34133/csbj.0042 [code]
Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials.
Journal: Computational and structural biotechnology journal
In common: Matplotlib, NumPy, 3 references, 2 authors
[5] doi:10.7554/elife.84205 [code]
Cortical magnification eliminates differences in contrast sensitivity across but not around the visual field.
Journal: eLife
In common: Optimization Toolbox, Statistics and Machine Learning Toolbox, 9 references
[6] doi:10.1038/s41467-023-37280-8 [code]
Comparing retinotopic maps of children and adults reveals a late-stage change in how V1 samples the visual field.
Journal: Nature communications
In common: Statistics and Machine Learning Toolbox, Matplotlib, NumPy, systems, 8 references
[7] doi:10.1101/2025.11.27.690210 [code]
Predicting functional topography of the human visual cortex from cortical anatomy at scale
Journal: bioRxiv (preprint)
In common: Matplotlib, NumPy, systems, 5 references, author Noah C Benson
[8] doi:10.1002/hbm.70140 [code]
Biases in Volumetric Versus Surface Analyses in Population Receptive Field Mapping.
Journal: Human brain mapping
In common: Optimization Toolbox, Statistics and Machine Learning Toolbox, Matplotlib, 1 other tool, fMRI, systems, 6 references
[9] doi:10.1038/s41467-025-63795-3 [code]
Spatial attention selectively alters visual cortical representation during target anticipation.
Journal: Nature communications
In common: Optimization Toolbox, Statistics and Machine Learning Toolbox, Matplotlib, 1 other tool, 7 references
[10] doi:10.1073/pnas.2507810122
Visual adaptation stronger at the horizontal than the vertical meridian: Linking performance with V1 cortical surface area.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: 9 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.7554/elife.105930.svg)](https://openscicode.org/badge/doi/10.7554/elife.105930)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.7554/elife.105930.svg
   :target: https://openscicode.org/badge/doi/10.7554/elife.105930
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.7554/elife.105930"><img src="https://openscicode.org/badge/doi/10.7554/elife.105930.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.