function [od_imgs, scan_parameter_values, file_list] = collectODImages(options) %% Applies cropping, background subtraction, and optional fringe removal, optional unshuffling on OD image dataset % Automatically reuses in-memory full dataset if available; % otherwise, reads and processes raw HDF5 data. % % Inputs: % options - structure containing processing options: % .folderPath : path to raw HDF5 files % .saveDirectory : path to save cache (if needed) % .cam, .angle : camera selection and rotation angle % .ImagingMode, .PulseDuration : imaging parameters % .scan_parameter : name of scan parameter % .center, .span : cropping settings % .fraction : background subtraction fraction % .removeFringes : logical flag for fringe removal % .skipUnshuffling : logical flag to skip unshuffling % .scan_reference_values: reference values for unshuffling % % Outputs: % od_imgs : cell array of processed OD images % scan_parameter_values: vector of scan parameter values % file_list : cell array of file names % --- Early exit if processed data already exist AND options match --- reuseVarsExist = evalin('base', ... 'exist(''od_imgs'',''var'') && exist(''scan_parameter_values'',''var'') && exist(''file_list'',''var'') && exist(''prior_options'',''var'')'); if reuseVarsExist prior_options = evalin('base','prior_options'); % Define which fields are critical for reuse critical_fields = {'folderPath','cam','angle','ImagingMode','PulseDuration','center','span','fraction','removeFringes','skipUnshuffling','scan_reference_values'}; if ~haveOptionsChanged(options, prior_options, critical_fields) fprintf('\nReusing processed OD images, scan parameters, and file list from memory.\n'); od_imgs = evalin('base','od_imgs'); scan_parameter_values = evalin('base','scan_parameter_values'); file_list = evalin('base','file_list'); % --- Ensure figures exist if requested now --- if ~options.skipSaveOD saveODFigures(od_imgs, options.saveDirectory); end return; % ✅ safe to exit now else fprintf('\nProcessed-data-related options changed. Reprocessing full OD image dataset...\n'); end end % --- Check if the full OD dataset and scan parameters exist in workspace --- fullDataExists = evalin('base', 'exist(''full_od_imgs'', ''var'')') && ... evalin('base', 'exist(''full_bkg_imgs'', ''var'')') && ... evalin('base', 'exist(''raw_scan_parameter_values'', ''var'')') && ... evalin('base', 'exist(''raw_file_list'', ''var'')') && ... evalin('base', 'exist(''prior_options'',''var'')'); if fullDataExists % Both required datasets exist, check if raw-data options changed prior_options = evalin('base','prior_options'); % Define critical fields that affect raw-data computation critical_raw_fields = {'folderPath','cam','angle','ImagingMode','PulseDuration'}; if ~haveOptionsChanged(options, prior_options, critical_raw_fields) fprintf('\nReusing full OD image dataset and scan parameters from memory.\n'); full_od_imgs = evalin('base', 'full_od_imgs'); full_bkg_imgs = evalin('base', 'full_bkg_imgs'); raw_scan_parameter_values = evalin('base', 'raw_scan_parameter_values'); raw_file_list = evalin('base', 'raw_file_list'); else fprintf('\nRaw-data-related options changed. Recomputing full OD image dataset...\n'); [full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list] = Helper.processRawData(options); % Save raw full dataset for reuse assignin('base', 'full_od_imgs', full_od_imgs); assignin('base', 'full_bkg_imgs', full_bkg_imgs); assignin('base', 'raw_scan_parameter_values', raw_scan_parameter_values); assignin('base', 'raw_file_list', raw_file_list); fprintf('\nCompleted recomputing OD images. Stored in workspace for reuse.\n'); end else % Either dataset is missing, process raw HDF5 files completely fprintf('\nFull OD image dataset or scan parameters not found in memory.\n'); [full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list] = Helper.processRawData(options); % Save raw full dataset for reuse assignin('base', 'full_od_imgs', full_od_imgs); assignin('base', 'full_bkg_imgs', full_bkg_imgs); assignin('base', 'raw_scan_parameter_values', raw_scan_parameter_values); assignin('base', 'raw_file_list', raw_file_list); fprintf('\nCompleted computing OD images. Images will be stored in workspace for reuse.\n'); end fprintf('\nCropping and subtracting background from images...\n'); nFiles = size(full_od_imgs, 3); % --- Preallocate arrays for processed images --- absimages = zeros(options.span(1)+1, options.span(2)+1, nFiles, 'single'); refimages = zeros(options.span(1)+1, options.span(2)+1, nFiles, 'single'); % --- Progress bar --- if isfield(options, 'showProgressBar') && options.showProgressBar pb = Helper.ProgressBar(); pb.run('Progress: '); end % --- Process each image: crop and subtract background --- for k = 1:nFiles full_od_img = full_od_imgs(:,:,k); % original full OD image, never modified full_bkg_img = full_bkg_imgs(:,:,k); % original full background image, never modified if any(isnan(full_od_img(:))) absimages(:,:,k) = nan(options.span(1)+1, options.span(2)+1, 'single'); continue end if any(isnan(full_bkg_img(:))) refimages(:,:,k) = nan(options.span(1)+1, options.span(2)+1, 'single'); continue end % Crop image around the region of interest cropped_absimage = Helper.cropODImage(full_od_img, options.center, options.span); cropped_refimage = Helper.cropODImage(full_bkg_img, options.center, options.span); % Subtract background offset based on fraction processed_absimage = Helper.subtractBackgroundOffset(cropped_absimage, options.fraction); processed_refimage = Helper.subtractBackgroundOffset(cropped_refimage, options.fraction); % Store processed image (transpose to match orientation) absimages(:,:,k) = processed_absimage'; refimages(:,:,k) = processed_refimage'; % Update progress bar if isfield(options, 'showProgressBar') && options.showProgressBar progressPercent = round(k / nFiles * 100); pb.run(progressPercent); end end % Finish progress bar if isfield(options, 'showProgressBar') && options.showProgressBar pb.run(' Done!'); end % --- Optional fringe removal --- if isfield(options, 'removeFringes') && options.removeFringes fprintf('\nApplying fringe removal to processed images...\n'); optrefimages = Helper.removeFringesInImage(absimages, refimages); absimages_fringe_removed = absimages - optrefimages; od_imgs = arrayfun(@(i) absimages_fringe_removed(:,:,i), 1:nFiles, 'UniformOutput', false); fprintf('\nFringe removal completed.\n'); else od_imgs = arrayfun(@(i) absimages(:,:,i), 1:nFiles, 'UniformOutput', false); end % --- Optional unshuffling based on scan reference values --- if isfield(options, 'skipUnshuffling') && ~options.skipUnshuffling fprintf('\nReordering images according to scan parameter reference values...\n'); n_values = length(options.scan_reference_values); n_total = length(raw_scan_parameter_values); n_reps = n_total / n_values; ordered_scan_parameter_values = zeros(1, n_total); ordered_od_imgs = cell(1, n_total); ordered_file_list = cell(1, n_total); counter = 1; temp_scan_values = raw_scan_parameter_values; temp_od_imgs = od_imgs; temp_file_list = raw_file_list; for rep = 1:n_reps for val = options.scan_reference_values idx = find(temp_scan_values == val, 1, 'first'); if isempty(idx), continue; end ordered_scan_parameter_values(counter) = temp_scan_values(idx); ordered_od_imgs{counter} = temp_od_imgs{idx}; ordered_file_list{counter} = temp_file_list{idx}; temp_scan_values(idx) = NaN; temp_od_imgs{idx} = []; temp_file_list{idx} = []; counter = counter + 1; end end od_imgs = ordered_od_imgs; scan_parameter_values = ordered_scan_parameter_values; file_list = ordered_file_list; fprintf('\nImage reordering completed.\n'); else % No unshuffling: keep original order scan_parameter_values = raw_scan_parameter_values; file_list = raw_file_list; end % --- Save processed dataset and options for reuse --- assignin('base', 'od_imgs', od_imgs); assignin('base', 'scan_parameter_values', scan_parameter_values); assignin('base', 'file_list', file_list); assignin('base', 'prior_options', options); % --- Save OD images as figures if requested --- if ~options.skipSaveOD saveODFigures(od_imgs, options.saveDirectory); end fprintf('\nOD image dataset ready for further analysis.\n'); end %% --- Local helper functions --- function changed = haveOptionsChanged(options, prior_options, critical_fields) changed = false; for f = critical_fields fname = f{1}; if isfield(options, fname) && isfield(prior_options, fname) if ~isequal(options.(fname), prior_options.(fname)) changed = true; return end elseif xor(isfield(options, fname), isfield(prior_options, fname)) changed = true; return end end end function saveODFigures(od_imgs, saveDirectory) odFolder = fullfile(saveDirectory, "Results", "ODImages"); if ~exist(odFolder, 'dir') mkdir(odFolder); end nImgs = length(od_imgs); filesExist = all(arrayfun(@(k) isfile(fullfile(odFolder, sprintf('OD_img_%03d.fig', k))), 1:nImgs)); if filesExist fprintf('\nOD figures already exist in %s. Skipping save.\n', odFolder); return; end fprintf('\nSaving OD figures to %s ...\n', odFolder); for k = 1:nImgs img = od_imgs{k}; % Create invisible figure for saving hFig = figure('Visible','off'); imagesc(img); axis image off; colormap gray; fileName = fullfile(odFolder, sprintf('OD_img_%03d.fig', k)); % Save as .fig savefig(hFig, fileName); close(hFig); end fprintf('OD figures saved successfully.\n'); end