Bugfixes and updates to save functionality.

This commit is contained in:
Karthik 2025-08-29 20:16:13 +02:00
parent 93210fb5e6
commit 7d196f4bb0
3 changed files with 241 additions and 120 deletions

View File

@ -38,7 +38,7 @@ function [od_imgs, scan_parameter_values, file_list] = collectODImages(options)
% --- Save OD images to disk if requested ---
if ~options.skipSaveProcessedOD
saveOD(od_imgs, options);
saveProcessedOD(od_imgs, options);
end
return; % bypass cropping/background subtraction
@ -51,14 +51,13 @@ function [od_imgs, scan_parameter_values, file_list] = collectODImages(options)
if isfield(options, 'FullODImagesFolder') && ...
~isempty(options.FullODImagesFolder) && ...
isfolder(options.FullODImagesFolder)
% User-specified parent folder
fullodimage_folders = dir(fullfile(options.FullODImagesFolder, 'FullODImages_*'));
full_od_image_parent_folder = dir(fullfile(options.FullODImagesFolder, 'FullODImages_*'));
elseif isfield(options, 'saveDirectory') && isfolder(options.saveDirectory)
full_od_image_parent_folder = dir(fullfile(options.saveDirectory, 'FullODImages_*'));
else
% Default to saveDirectory
fullodimage_folders = dir(fullfile(options.saveDirectory, 'FullODImages_*'));
full_od_image_parent_folder = '';
end
% --- Specific sequence, data and run ---
dataSource = makeDataSourceStruct(options.folderPath);
@ -73,88 +72,143 @@ function [od_imgs, scan_parameter_values, file_list] = collectODImages(options)
[options.SAVE_TO_WORKSPACE, ~] = Helper.estimateDatasetMemory(dataSource, options);
end
fullodimage_folder = [];
full_od_image_subfolder = [];
% --- Prepare full_od_imgs, full_bkg_imgs, scan values, file list ---
if fullDataExists
% --- Case 1: Already in workspace ---
fprintf('\n[INFO] Reusing 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');
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');
nFiles = size(full_od_imgs,3);
fprintf('\n[INFO] Cropping and subtracting background from images...\n');
elseif ~options.SAVE_TO_WORKSPACE || (~isempty(fullodimage_folders) || (isfield(options,'selectedPath') && isfolder(options.selectedPath)))
else
matched = false;
full_od_image_subfolder = [];
useFullODFolders = ~isfield(options,'skipFullODImagesFolderUse') || ~options.skipFullODImagesFolderUse;
% --- Use selectedPath directly if it exists and full OD usage is enabled ---
if isfield(options, 'selectedPath') && isfolder(options.selectedPath) ...
&& (~isfield(options, 'skipFullODImagesFolderUse') || ~options.skipFullODImagesFolderUse)
fullodimage_folder = options.selectedPath;
matched = true;
fprintf('\n[INFO] Using selected full OD images subfolder: %s\n', fullodimage_folder);
else
% --- Otherwise, search among available full OD image folders ---
for r = 1:numel(fullodimage_folders)
metaPath = fullfile(fullodimage_folders(r).folder, fullodimage_folders(r).name, 'metadata.mat');
if ~isfile(metaPath), continue; end
S = load(metaPath,'metadata');
% --- If user provided selectedPath and it is a folder, disambiguate its meaning ---
if isfield(options,'selectedPath') && isfolder(options.selectedPath)
selPath = options.selectedPath;
% Compare only measurementName and folder path via dataSource struct
mdDataSource = makeDataSourceStruct(S.metadata.options.folderPath);
currentDataSource = makeDataSourceStruct(options.folderPath);
% --- Determine if selectedPath looks like a FullODImages folder ---
selIsFullOD = false;
if isfile(fullfile(selPath,'metadata.mat'))
selIsFullOD = true;
else
% exact match to any discovered fullodimage_folders entry
if ~isempty(full_od_image_parent_folder)
for r = 1:numel(full_od_image_parent_folder)
cand = fullfile(full_od_image_parent_folder(r).folder, full_od_image_parent_folder(r).name);
if strcmp(cand, selPath)
selIsFullOD = true;
break;
end
end
end
end
if isfield(S.metadata.options,'measurementName') && isfield(options,'measurementName') && ...
strcmp(S.metadata.options.measurementName, options.measurementName) && ...
isequal(mdDataSource, currentDataSource)
fullodimage_folder = fullfile(fullodimage_folders(r).folder, fullodimage_folders(r).name);
if selIsFullOD && useFullODFolders
% --- selectedPath is explicitly the full OD images folder ---
full_od_image_subfolder = selPath;
matched = true;
fprintf('\n[INFO] Using selected full OD images subfolder: %s\n', full_od_image_subfolder);
else
% --- selectedPath appears to be a raw-data folder (not a full-OD folder) ---
fprintf('\n[INFO] Selected path appears to be raw data: %s\n', selPath);
% If user forces recompute -> recompute from selected raw path
if isfield(options,'skipFullODImagesFolderUse') && options.skipFullODImagesFolderUse
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list, full_od_image_subfolder, nFiles] = ...
recomputeODImages(options, dataSource, selPath);
matched = true;
fprintf('\n[INFO] Found matching full OD images subfolder: %s\n', fullodimage_folders(r).name);
break;
else
% Try to find an existing full-OD folder whose metadata references this raw path
found = false;
if ~isempty(full_od_image_parent_folder) && useFullODFolders
for r = 1:numel(full_od_image_parent_folder)
metaPath = fullfile(full_od_image_parent_folder(r).folder, full_od_image_parent_folder(r).name, 'metadata.mat');
if ~isfile(metaPath), continue; end
S = load(metaPath,'metadata');
% Compare data source from metadata to the selected raw path
mdDataSource = makeDataSourceStruct(S.metadata.options.folderPath);
selDataSource = makeDataSourceStruct(selPath);
if isfield(S.metadata.options,'measurementName') && isfield(options,'measurementName') && ...
strcmp(S.metadata.options.measurementName, options.measurementName) && ...
isequal(mdDataSource, selDataSource)
full_od_image_subfolder = fullfile(full_od_image_parent_folder(r).folder, full_od_image_parent_folder(r).name);
matched = true;
found = true;
fprintf('\n[INFO] Found matching full OD images subfolder for selected raw path: %s\n', full_od_image_parent_folder(r).name);
break;
end
end
end
% If no matching full-OD folder found, recompute from the selected raw path
if ~found
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list, full_od_image_subfolder, nFiles] = ...
recomputeODImages(options, dataSource, selPath);
matched = true;
end
end
end
else
% --- No selectedPath provided: either force recompute or search among fullodimage_folders ---
if isfield(options,'skipFullODImagesFolderUse') && options.skipFullODImagesFolderUse
% forced recompute
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list, full_od_image_subfolder, nFiles] = ...
recomputeODImages(options, dataSource);
matched = true;
else
% Search for existing matching full-OD folder based on options.folderPath
if ~isempty(full_od_image_parent_folder) && useFullODFolders
for r = 1:numel(full_od_image_parent_folder)
metaPath = fullfile(full_od_image_parent_folder(r).folder, full_od_image_parent_folder(r).name,'metadata.mat');
if ~isfile(metaPath), continue; end
S = load(metaPath,'metadata');
mdDataSource = makeDataSourceStruct(S.metadata.options.folderPath);
currentDataSource = makeDataSourceStruct(options.folderPath);
if isfield(S.metadata.options,'measurementName') && isfield(options,'measurementName') && ...
strcmp(S.metadata.options.measurementName, options.measurementName) && ...
isequal(mdDataSource, currentDataSource)
full_od_image_subfolder = fullfile(full_od_image_parent_folder(r).folder, full_od_image_parent_folder(r).name);
matched = true;
fprintf('\n[INFO] Found matching full OD images subfolder: %s\n', full_od_image_parent_folder(r).name);
break;
end
end
end
end
end
% --- If still not matched, recompute from raw (fallback) ---
if ~matched
fprintf('\n[INFO] No matching full OD images subfolder for this run found.\n');
[~, ~, ~, ~] = Helper.processRawData(options);
fprintf('\n[INFO] Completed computing OD images. Images will be stored on disk for reuse.\n');
if isempty(full_od_image_parent_folder) && ~useFullODFolders
fprintf('\n[INFO] No full OD images found in workspace or on disk. Computing from raw...\n');
else
fprintf('\n[INFO] No matching full OD images subfolder found. Recomputing from raw...\n');
end
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list, full_od_image_subfolder, nFiles] = ...
recomputeODImages(options, dataSource);
end
% --- Load mat files from determined folder ---
mat_files = dir(fullfile(fullodimage_folder,'*.mat'));
mat_files = mat_files(~strcmp({mat_files.name}, 'metadata.mat')); % Exclude metadata.mat
nFiles = numel(mat_files);
raw_scan_parameter_values = zeros(1,nFiles);
raw_file_list = strings(1,nFiles);
fprintf('\n[INFO] Cropping and subtracting background from images in full OD images folder on disk...\n');
else
fprintf('\n[INFO] No full OD images found in either the workspace or on disk.\n');
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list] = Helper.processRawData(options);
if options.SAVE_TO_WORKSPACE
nFiles = size(full_od_imgs,3);
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('\n[INFO] Completed computing OD images. Images stored in workspace for reuse.\n');
fprintf('\n[INFO] Cropping and subtracting background from images...\n');
else
fprintf('\n[INFO] Completed computing OD images. Images stored on disk for reuse.\n');
runID = sprintf('%s_%s_Run%04d', ...
dataSource{1}.sequence, ...
strrep(dataSource{1}.date,'/','-'), ...
dataSource{1}.runs);
fullodimage_folder = fullfile(options.saveDirectory, ['FullODImages_' runID]);
mat_files = dir(fullfile(fullodimage_folder,'*.mat'));
mat_files = mat_files(~strcmp({mat_files.name}, 'metadata.mat')); % Exclude metadata.mat
nFiles = numel(mat_files);
raw_scan_parameter_values = zeros(1,nFiles);
raw_file_list = strings(1,nFiles);
% --- If a folder was determined, load its contents (listing) ---
if ~isempty(full_od_image_subfolder) && useFullODFolders
[raw_scan_parameter_values, raw_file_list, nFiles] = prepareDiskListing(full_od_image_subfolder);
fprintf('\n[INFO] Cropping and subtracting background from images in full OD images folder on disk...\n');
end
end
% --- Unified cropping & background subtraction ---
absimages = zeros(options.span(1)+1, options.span(2)+1, nFiles, 'single');
refimages = zeros(options.span(1)+1, options.span(2)+1, nFiles, 'single');
@ -166,12 +220,12 @@ function [od_imgs, scan_parameter_values, file_list] = collectODImages(options)
end
for k = 1:nFiles
if fullDataExists || ~isempty(fullodimage_folder)
if fullDataExists || ~isempty(full_od_image_subfolder)
if fullDataExists
od_mat = full_od_imgs(:,:,k);
bkg_mat = full_bkg_imgs(:,:,k);
else
data = load(fullfile(fullodimage_folder, mat_files(k).name));
data = load(fullfile(full_od_image_subfolder, mat_files(k).name));
od_mat = data.OD;
bkg_mat = data.BKG;
raw_scan_parameter_values(k) = data.Scan;
@ -261,7 +315,7 @@ function [od_imgs, scan_parameter_values, file_list] = collectODImages(options)
% --- Save OD images to disk if requested ---
if ~options.skipSaveProcessedOD
saveOD(od_imgs, options);
saveProcessedOD(od_imgs, options);
end
fprintf('\n[INFO] OD image dataset ready for further analysis.\n');
@ -283,74 +337,78 @@ function changed = haveOptionsChanged(options, prior_options, critical_fields)
end
end
function saveOD(od_img, options)
% Saves a cell array of OD images (with metadata) to .mat files
nImgs = numel(od_img);
function saveProcessedOD(od_imgs, options)
% Saves a cell array of processed OD images (with metadata) to .mat files
% od_imgs is expected to be a cell array of structs with fields: OD, Scan, File
%
% Inputs:
% od_imgs - cell array of structs: OD (2D array), Scan (scalar), File (string)
% options - struct containing folderPath and either FullODImagesFolder or saveDirectory
nImgs = numel(od_imgs);
if nImgs == 0
error('[ERROR] No images found.');
end
% --- Determine image size ---
[ny, nx] = size(od_img{1}.OD);
[ny, nx] = size(od_imgs{1}.OD);
% --- Create uniquely identified full OD image folder ---
% --- Create unique folder name ---
dataSource = makeDataSourceStruct(options.folderPath);
runID = sprintf('%s_%s_Run%04d', ...
dataSource{1}.sequence, ...
strrep(dataSource{1}.date,'/','-'), ...
dataSource{1}.runs);
% --- Determine parent folder for FullODImages ---
if isfield(options, 'FullODImagesFoldersPath') && ...
~isempty(options.FullODImagesFoldersPath) && ...
isfolder(options.FullODImagesFoldersPath)
parentFolder = options.FullODImagesFoldersPath;
else
% --- Determine parent folder ---
if isfield(options, 'FullODImagesFolder') && ~isempty(options.FullODImagesFolder) && isfolder(options.FullODImagesFolder)
parentFolder = options.FullODImagesFolder;
elseif isfield(options, 'saveDirectory') && isfolder(options.saveDirectory)
parentFolder = options.saveDirectory;
else
parentFolder = pwd; % fallback to current folder
end
% --- Create uniquely identified full OD image folder ---
fullODImageFolder = fullfile(parentFolder, ['FullODImages_' runID]);
if ~exist(fullODImageFolder,'dir'), mkdir(fullODImageFolder); end
fprintf('\n[INFO] Creating folder of full OD images on disk: %s\n', fullODImageFolder);
% --- Create ProcessedODImages folder ---
processedFolder = fullfile(parentFolder, ['ProcessedODImages_' runID]);
if ~exist(processedFolder, 'dir')
mkdir(processedFolder);
end
fprintf('\n[INFO] Saving processed OD images in folder: %s\n', processedFolder);
% --- Check if everything already exists ---
filesExist = all(arrayfun(@(k) ...
isfile(fullfile(fullODImageFolder, sprintf('Image_%04d.mat', k))), 1:nImgs)) ...
&& isfile(fullfile(fullODImageFolder,'metadata.mat'));
% --- Check if files already exist ---
filesExist = all(arrayfun(@(k) isfile(fullfile(processedFolder, sprintf('Image_%04d.mat', k))), 1:nImgs)) ...
&& isfile(fullfile(processedFolder,'metadata.mat'));
if filesExist
fprintf('\n[INFO] OD .mat files already exist in %s. Skipping save.\n', fullODImageFolder);
fprintf('\n[INFO] Processed OD .mat files already exist in %s. Skipping save.\n', processedFolder);
return;
end
% --- Save metadata for this run ---
% --- Save metadata ---
metadata.options = options;
metadata.timestamp = datetime; % record analysis time
metadata.runID = runID; % traceable to experiment run
metadata.timestamp = datetime;
metadata.runID = runID;
metadata.imageSize = [ny, nx];
metadata.fileList = string(cellfun(@(c) c.File, od_img, 'UniformOutput', false));
save(fullfile(fullODImageFolder,'metadata.mat'),'metadata','-v7.3');
metadata.fileList = string(cellfun(@(c) c.File, od_imgs, 'UniformOutput', false));
save(fullfile(processedFolder,'metadata.mat'), 'metadata', '-v7.3');
% --- Save each image ---
% --- Save each image as a struct with OD, Scan, File ---
for k = 1:nImgs
% Expecting od_img{k} to be a struct with fields: OD, BKG, Scan, File
if ~isstruct(od_img{k}) || ...
~all(isfield(od_img{k}, {'OD','BKG','Scan','File'}))
error('od_img{%d} must be a struct with fields OD, BKG, Scan, File.', k);
imgStruct = od_imgs{k};
if ~isstruct(imgStruct) || ~all(isfield(imgStruct, {'OD','Scan','File'}))
error('od_imgs{%d} must be a struct with fields OD, Scan, File.', k);
end
OD = single(od_img{k}.OD);
BKG = single(od_img{k}.BKG);
Scan = single(od_img{k}.Scan);
File = string(od_img{k}.File);
OD = single(imgStruct.OD);
Scan = single(imgStruct.Scan);
File = string(imgStruct.File);
matFilePath = fullfile(fullODImageFolder, sprintf('Image_%04d.mat', k));
save(matFilePath, 'OD','BKG','Scan','File','-v7.3');
matFilePath = fullfile(processedFolder, sprintf('Image_%04d.mat', k));
save(matFilePath, 'OD','Scan','File','-v7.3');
end
fprintf('[INFO] OD .mat files and metadata saved successfully.\n');
fprintf('[INFO] Processed OD .mat files and metadata saved successfully.\n');
end
function dataSource = makeDataSourceStruct(folderPath)
@ -382,3 +440,55 @@ function dataSource = makeDataSourceStruct(folderPath)
'runs', runNum)
};
end
function [full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list, fullodimage_folder, nFiles] = recomputeODImages(options, dataSource, selectedPath)
% recomputeODImages: central recompute routine
% optional argument selectedPath (if provided) will be used as options.folderPath for processing
if nargin < 3
selectedPath = '';
end
fprintf('\n[INFO] Computing OD images from raw data...\n');
% make a local copy of options so we can override folderPath if selectedPath supplied
opts = options;
if ~isempty(selectedPath)
% assume the Helper.processRawData uses opts.folderPath (or opts.selectedPath) override folderPath to selectedPath
opts.folderPath = selectedPath;
end
[full_od_imgs, full_bkg_imgs, raw_scan_parameter_values, raw_file_list] = Helper.processRawData(opts);
if opts.SAVE_TO_WORKSPACE
nFiles = size(full_od_imgs,3);
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);
fullodimage_folder = [];
fprintf('\n[INFO] Completed computing OD images. Stored in workspace for reuse.\n');
fprintf('\n[INFO] Cropping and subtracting background from images...\n');
else
% Use dataSource to construct a run-folder (keeps naming consistent)
fullodimage_folder = createRunFolder(options, dataSource);
[raw_scan_parameter_values, raw_file_list, nFiles] = prepareDiskListing(fullodimage_folder);
fprintf('\n[INFO] Completed computing OD images. Stored on disk for reuse.\n');
end
end
function fullodimage_folder = createRunFolder(options, dataSource)
runID = sprintf('%s_%s_Run%04d', ...
dataSource{1}.sequence, ...
strrep(dataSource{1}.date,'/','-'), ...
dataSource{1}.runs);
fullodimage_folder = fullfile(options.saveDirectory, ['FullODImages_' runID]);
end
function [raw_scan_parameter_values, raw_file_list, nFiles] = prepareDiskListing(folder)
mat_files = dir(fullfile(folder,'*.mat'));
mat_files = mat_files(~strcmp({mat_files.name},'metadata.mat')); % exclude metadata
nFiles = numel(mat_files);
raw_scan_parameter_values = zeros(1,nFiles);
raw_file_list = strings(1,nFiles);
end

View File

@ -30,10 +30,10 @@ function [SAVE_TO_WORKSPACE, runMemoryGB] = estimateDatasetMemory(dataSources, o
if runBytes > 0.75 * availableRAM
SAVE_TO_WORKSPACE = false;
fprintf('[INFO] Selected run %s estimated size %.2f GB exceeds 75%% of available RAM. Saving to disk.\n', ...
fprintf('\n[INFO] Selected run %s estimated size %.2f GB exceeds 75%% of available RAM. Saving to disk.\n', ...
runFolder, runBytes/1e9);
else
fprintf('[INFO] Selected run %s estimated size %.2f GB fits in memory.\n', ...
fprintf('\n[INFO] Selected run %s estimated size %.2f GB fits in memory.\n', ...
runFolder, runBytes/1e9);
end
end

View File

@ -3,7 +3,7 @@ function [selectedPath, folderPath] = selectDataSourcePath(dataSources, options)
%
% Inputs:
% dataSources - cell array of structs with fields: sequence, date, runs
% options - struct, may contain: baseDataFolder, FullODImagesFolder, skipFullODImagesFolderUse
% options - struct, may contain: baseDataFolder, FullODImagesFolder, skipFullODImagesFolderUse, saveDirectory
%
% Outputs:
% selectedPath - actual folder to use (raw or FullOD)
@ -26,28 +26,39 @@ function [selectedPath, folderPath] = selectDataSourcePath(dataSources, options)
end
end
% --- Resolve FullODImagesFolder parent location ---
if isfield(options, 'FullODImagesFolder') && ...
~isempty(options.FullODImagesFolder) && ...
isfolder(options.FullODImagesFolder)
full_od_image_parent_folder = options.FullODImagesFolder;
elseif isfield(options, 'saveDirectory') && isfolder(options.saveDirectory)
full_od_image_parent_folder = options.saveDirectory;
else
full_od_image_parent_folder = '';
end
% --- Determine whether FullODImagesFolder should be used ---
useFullOD = false;
if ~isempty(allPaths)
if isfield(options,'FullODImagesFolder') && isfolder(options.FullODImagesFolder)
if ~isempty(full_od_image_parent_folder)
if ~isfield(options,'skipFullODImagesFolderUse') || ~options.skipFullODImagesFolderUse
fprintf('\n[INFO] Both raw data folder (%s) and full OD Images folder (%s) found.\n', ...
options.baseDataFolder, options.FullODImagesFolder);
options.baseDataFolder, full_od_image_parent_folder);
fprintf('[INFO] Prioritizing full OD Images folder (set skipFullODImagesFolderUse=true to override).\n');
useFullOD = true;
else
fprintf('\n[INFO] Both raw data folder (%s) and full OD Images folder (%s) found.\n', ...
options.baseDataFolder, options.FullODImagesFolder);
options.baseDataFolder, full_od_image_parent_folder);
fprintf('[INFO] Prioritizing raw data folder (set skipFullODImagesFolderUse=false to override).\n');
end
else
fprintf('\n[INFO] Using raw data folder(s) since full OD images not found or not specified.\n');
fprintf('[INFO] Using raw data folder(s) since full OD images not found or not specified.\n');
end
elseif isfield(options,'FullODImagesFolder') && isfolder(options.FullODImagesFolder)
if ~options.skipFullODImagesFolderUse
elseif ~isempty(full_od_image_parent_folder)
if ~isfield(options,'skipFullODImagesFolderUse') || ~options.skipFullODImagesFolderUse
useFullOD = true;
fprintf('\n[INFO] Raw data folder(s) not found but found full OD Images folder which will be used.\n');
allPaths{end+1} = options.FullODImagesFolder;
allPaths{end+1} = full_od_image_parent_folder;
else
error('Raw data folder(s) not found, found full OD Images folder which cannot be used (set skipFullODImagesFolderUse=false to override). Aborting.\n');
end
@ -64,7 +75,7 @@ function [selectedPath, folderPath] = selectDataSourcePath(dataSources, options)
ds = dataSources{i};
for run = ds.runs
expectedName = sprintf('FullODImages_%s_%s_Run%04d', ds.sequence, strrep(ds.date,'/','-'), run);
candidateFolder = fullfile(options.FullODImagesFolder, expectedName);
candidateFolder = fullfile(full_od_image_parent_folder, expectedName);
if isfolder(candidateFolder)
matchedPaths{end+1} = candidateFolder;
end
@ -73,7 +84,7 @@ function [selectedPath, folderPath] = selectDataSourcePath(dataSources, options)
if ~isempty(matchedPaths)
allPaths = matchedPaths;
else
error('No valid paths for data found. Aborting.');
error('No valid FullODImages_* subfolders found under %s. Aborting.', full_od_image_parent_folder);
end
end