function results = analyzeFolder(options)
    % Ensure required fields are defined in options
    arguments
        options.scan_parameter (1,:) char
        options.scan_groups (1,:) double
        options.cam (1,1) double
        options.angle (1,1) double
        options.center (1,2) double
        options.span (1,2) double
        options.fraction (1,2) double
        options.ImagingMode (1,:) char
        options.PulseDuration (1,1) double
        options.removeFringes (1,1) logical
        options.skipUnshuffling (1,1) logical
        options.pixel_size (1,1) double
        options.magnification (1,1) double
        options.zoom_size (1,1) double
        options.r_min (1,1) double
        options.r_max (1,1) double
        options.N_angular_bins (1,1) double
        options.Angular_Threshold (1,1) double
        options.Angular_Sigma (1,1) double
        options.Angular_WindowSize (1,1) double
        options.theta_min (1,1) double
        options.theta_max (1,1) double
        options.N_radial_bins (1,1) double
        options.Radial_Sigma (1,1) double
        options.Radial_WindowSize (1,1) double
        options.k_min (1,1) double
        options.k_max (1,1) double
        options.skipPreprocessing (1,1) logical
        options.skipMasking (1,1) logical
        options.skipIntensityThresholding (1,1) logical
        options.skipBinarization (1,1) logical
        options.folderPath (1,:) char
    end

    % Assign variables from options
    scan_parameter = options.scan_parameter;
    scan_groups = options.scan_groups;
    folderPath = options.folderPath;
    center = options.center;
    span = options.span;
    fraction = options.fraction;
    ImagingMode = options.ImagingMode;
    PulseDuration = options.PulseDuration;
    removeFringes = options.removeFringes;
    skipUnshuffling = options.skipUnshuffling;
    pixel_size = options.pixel_size;
    magnification = options.magnification;
    zoom_size = options.zoom_size;
    r_min = options.r_min;
    r_max = options.r_max;
    N_angular_bins = options.N_angular_bins;
    Angular_Threshold = options.Angular_Threshold;
    Angular_Sigma = options.Angular_Sigma;
    Angular_WindowSize = options.Angular_WindowSize;
    theta_min = options.theta_min;
    theta_max = options.theta_max;
    N_radial_bins = options.N_radial_bins;
    Radial_Sigma = options.Radial_Sigma;
    Radial_WindowSize = options.Radial_WindowSize;
    k_min = options.k_min;
    k_max = options.k_max;
    skipPreprocessing = options.skipPreprocessing;
    skipMasking = options.skipMasking;
    skipIntensityThresholding = options.skipIntensityThresholding;
    skipBinarization = options.skipBinarization;
    cam = options.cam;
    angle = options.angle;

    % Load images and analyze them (keep using the cleaned body of your original function)
    % Fix the incorrect usage of 'cam' and 'angle' not defined locally
    groupList = ["/images/MOT_3D_Camera/in_situ_absorption", 
                 "/images/ODT_1_Axis_Camera/in_situ_absorption", 
                 "/images/ODT_2_Axis_Camera/in_situ_absorption", 
                 "/images/Horizontal_Axis_Camera/in_situ_absorption", 
                 "/images/Vertical_Axis_Camera/in_situ_absorption"];

    filePattern                    = fullfile(folderPath, '*.h5');
    files                          = dir(filePattern);
    refimages                      = zeros(span(1) + 1, span(2) + 1, length(files));
    absimages                      = zeros(span(1) + 1, span(2) + 1, length(files));
    
    for k = 1 : length(files)
        baseFileName = files(k).name;
        fullFileName = fullfile(files(k).folder, baseFileName);
        
        fprintf(1, 'Now reading %s\n', fullFileName);
        
        atm_img  = double(imrotate(h5read(fullFileName, append(groupList(cam), "/atoms")), angle));
        bkg_img  = double(imrotate(h5read(fullFileName, append(groupList(cam), "/background")), angle));
        dark_img = double(imrotate(h5read(fullFileName, append(groupList(cam), "/dark")), angle));
        
        if (isempty(atm_img)  && isa(atm_img, 'double')) || ...
           (isempty(bkg_img)  && isa(bkg_img, 'double')) || ...
           (isempty(dark_img) && isa(dark_img, 'double'))
            
            refimages(:,:,k) = nan(size(refimages(:,:,k)));  % fill with NaNs
            absimages(:,:,k) = nan(size(absimages(:,:,k)));
        else
            refimages(:,:,k)  = subtractBackgroundOffset(cropODImage(bkg_img, center, span), fraction)';
            absimages(:,:,k)  = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img, ImagingMode, PulseDuration), center, span), fraction)';
        end
    end
    
    % ===== Fringe removal =====
    
    if removeFringes
        optrefimages               = removefringesInImage(absimages, refimages);
        absimages_fringe_removed   = absimages(:, :, :) - optrefimages(:, :, :);
        
        nimgs                      = size(absimages_fringe_removed,3);
        od_imgs                    = cell(1, nimgs);
        for i = 1:nimgs
            od_imgs{i}             = absimages_fringe_removed(:, :, i);
        end
    else
        nimgs                      = size(absimages(:, :, :),3);
        od_imgs                    = cell(1, nimgs);
        for i = 1:nimgs
            od_imgs{i}             = absimages(:, :, i);
        end
    end
    
    % ===== Get rotation angles =====
    scan_parameter_values   = zeros(1, length(files));
    
    % Get information about the '/globals' group
    for k = 1 : length(files)
        baseFileName = files(k).name;
        fullFileName = fullfile(files(k).folder, baseFileName);
        info = h5info(fullFileName, '/globals');
        for i = 1:length(info.Attributes)
            if strcmp(info.Attributes(i).Name, scan_parameter)
                if strcmp(scan_parameter, 'rot_mag_fin_pol_angle')
                    scan_parameter_values(k) = 180 - info.Attributes(i).Value;
                else
                    scan_parameter_values(k) = info.Attributes(i).Value;
                end
            end
        end
    end
    
    % ===== Unshuffle if necessary to do so  =====
    
    if ~skipUnshuffling
        n_values = length(scan_groups);
        n_total  = length(scan_parameter_values);
        
        % Infer number of repetitions
        n_reps   = n_total / n_values;
        
        % Preallocate ordered arrays
        ordered_scan_values = zeros(1, n_total);
        ordered_od_imgs = cell(1, n_total);
        
        counter = 1;
        
        for rep = 1:n_reps
            for val = scan_groups
                % Find the next unused match for this val
                idx = find(scan_parameter_values == val, 1, 'first');
        
                % Assign and remove from list to avoid duplicates
                ordered_scan_values(counter) = scan_parameter_values(idx);
                ordered_od_imgs{counter} = od_imgs{idx};
        
                % Mark as used by removing
                scan_parameter_values(idx) = NaN;  % NaN is safe since original values are 0:5:45
                od_imgs{idx} = [];  % empty cell so it won't be matched again
        
                counter = counter + 1;
            end
        end
        
        % Now assign back
        scan_parameter_values = ordered_scan_values;
        od_imgs = ordered_od_imgs;
    end
    % Extract quantities
    fft_imgs                  = cell(1, nimgs);
    spectral_distribution     = cell(1, nimgs);
    theta_values              = cell(1, nimgs);
    radial_spectral_contrast  = zeros(1, nimgs);
    angular_spectral_weight   = zeros(1, nimgs);
    N_shots                   = length(od_imgs);
    
    for k = 1:N_shots
        IMG                   = od_imgs{k};
        if ~(max(IMG(:)) > 1)
            IMGFFT            = NaN(size(IMG));
        else
            [IMGFFT, IMGPR]   = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization);
        end
        
        % Size of original image (in pixels)
        [Ny, Nx]              = size(IMG);
        
        % Real-space pixel size in micrometers after magnification
        dx                    = pixel_size / magnification;
        dy                    = dx;  % assuming square pixels
        
        % Real-space axes
        x                     = ((1:Nx) - ceil(Nx/2)) * dx * 1E6;
        y                     = ((1:Ny) - ceil(Ny/2)) * dy * 1E6;
        
        % Reciprocal space increments (frequency domain, μm⁻¹)
        dvx                   = 1 / (Nx * dx);
        dvy                   = 1 / (Ny * dy);
        
        % Frequency axes
        vx                    = (-floor(Nx/2):ceil(Nx/2)-1) * dvx;
        vy                    = (-floor(Ny/2):ceil(Ny/2)-1) * dvy;
        
        % Wavenumber axes
        kx_full               = 2 * pi * vx * 1E-6;  % μm⁻¹
        ky_full               = 2 * pi * vy * 1E-6;
        
        % Crop FFT image around center
        mid_x                 = floor(Nx/2);
        mid_y                 = floor(Ny/2);
        fft_imgs{k}           = IMGFFT(mid_y-zoom_size:mid_y+zoom_size, mid_x-zoom_size:mid_x+zoom_size);
        
        % Crop wavenumber axes to match fft_imgs{k}
        kx                    = kx_full(mid_x - zoom_size : mid_x + zoom_size);
        ky                    = ky_full(mid_y - zoom_size : mid_y + zoom_size);
        
        [theta_vals, S_theta]        = computeAngularSpectralDistribution(fft_imgs{k}, kx, ky, k_min, k_max, N_angular_bins, Angular_Threshold, Angular_Sigma, []);
        [k_rho_vals, S_k]            = computeRadialSpectralDistribution(fft_imgs{k}, kx, ky, theta_min, theta_max, N_radial_bins);
        S_k_smoothed                 = movmean(S_k, Radial_WindowSize);  % Compute moving average (use convolution) or use conv for more control
        spectral_distribution{k}     = S_theta;
        theta_values{k}              = theta_vals;
        radial_spectral_contrast(k)  = computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max);
        S_theta_norm                 = S_theta / max(S_theta); % Normalize to 1
        angular_spectral_weight(k)   = trapz(theta_vals, S_theta_norm);
    end
    
    % Assuming scan_parameter_values and spectral_weight are column vectors (or row vectors of same length)
    [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values);
    
    % Preallocate arrays
    mean_rsc          = zeros(size(unique_scan_parameter_values));
    stderr_rsc        = zeros(size(unique_scan_parameter_values));
    
    % Loop through each unique theta and compute mean and standard error
    for i = 1:length(unique_scan_parameter_values)
        group_vals    = radial_spectral_contrast(idx == i);
        mean_rsc(i)   = mean(group_vals, 'omitnan');
        stderr_rsc(i) = std(group_vals, 'omitnan') / sqrt(length(group_vals)); % standard error = std / sqrt(N)
    end
    
    % Preallocate arrays
    mean_asw          = zeros(size(unique_scan_parameter_values));
    stderr_asw        = zeros(size(unique_scan_parameter_values));
    
    % Loop through each unique theta and compute mean and standard error
    for i = 1:length(unique_scan_parameter_values)
        group_vals    = angular_spectral_weight(idx == i);
        mean_asw(i)   = mean(group_vals, 'omitnan');
        stderr_asw(i) = std(group_vals, 'omitnan') / sqrt(length(group_vals)); % standard error = std / sqrt(N)
    end
    
    % Convert spectral distribution to matrix (N_shots x N_angular_bins)
    delta_nkr_all = zeros(N_shots, N_angular_bins);
    for k = 1:N_shots
        delta_nkr_all(k, :) = spectral_distribution{k};
    end
    
    % Group by scan parameter values (e.g., alpha, angle, etc.)
    [unique_scan_parameter_values, ~, idx] = unique(scan_parameter_values);
    N_params = length(unique_scan_parameter_values);
    
    % Define angular range and conversion
    angle_range     = 180;
    angle_per_bin   = angle_range / N_angular_bins;
    max_peak_angle  = 180;
    max_peak_bin    = round(max_peak_angle / angle_per_bin);
    
    % Parameters for search
    window_size     = 10;
    angle_threshold = 100;
    
    % Initialize containers for final results
    mean_max_g2_values       = zeros(1, N_params);
    mean_max_g2_angle_values = zeros(1, N_params);
    var_max_g2_values        = zeros(1, N_params);
    var_max_g2_angle_values  = zeros(1, N_params);
    std_error_g2_values      = zeros(1, N_params);
    
    % Also store raw data per group
    g2_all_per_group         = cell(1, N_params);
    angle_all_per_group      = cell(1, N_params);
    
    for i = 1:N_params
        group_idx      = find(idx == i);
        group_data     = delta_nkr_all(group_idx, :);
        N_reps         = size(group_data, 1);
    
        g2_values         = zeros(1, N_reps);
        angle_at_max_g2   = zeros(1, N_reps);
    
        for j = 1:N_reps
            profile                = group_data(j, :);
    
            % Restrict search to 0–60° for highest peak
            restricted_profile     = profile(1:max_peak_bin);
            [~, peak_idx_rel]      = max(restricted_profile);
            peak_idx               = peak_idx_rel;
            peak_angle             = (peak_idx - 1) * angle_per_bin;
    
            if peak_angle < angle_threshold
                offsets            = round(50 / angle_per_bin) : round(70 / angle_per_bin);
            else
                offsets            = -round(70 / angle_per_bin) : -round(50 / angle_per_bin);
            end
    
            ref_window             = mod((peak_idx - window_size):(peak_idx + window_size) - 1, N_angular_bins) + 1;
            ref                    = profile(ref_window);
    
            correlations           = zeros(size(offsets));
            angles                 = zeros(size(offsets));
    
            for k = 1:length(offsets)
                shifted_idx        = mod(peak_idx + offsets(k) - 1, N_angular_bins) + 1;
                sec_window         = mod((shifted_idx - window_size):(shifted_idx + window_size) - 1, N_angular_bins) + 1;
                sec                = profile(sec_window);
    
                num                = mean(ref .* sec, 'omitnan');
                denom              = mean(ref.^2, 'omitnan');
                g2                 = num / denom;
    
                correlations(k)    = g2;
                angles(k)          = mod((peak_idx - 1 + offsets(k)) * angle_per_bin, angle_range);
            end
    
            [max_corr, max_idx]    = max(correlations);
            g2_values(j)           = max_corr;
            angle_at_max_g2(j)     = angles(max_idx);
        end
        % Store raw values
        g2_all_per_group{i}      = g2_values;
        angle_all_per_group{i}   = angle_at_max_g2;
    
        % Final stats
        mean_max_g2_values(i)      = mean(g2_values, 'omitnan');
        var_max_g2_values(i)       = var(g2_values, 0, 'omitnan');
        mean_max_g2_angle_values(i)= mean(angle_at_max_g2, 'omitnan');
        var_max_g2_angle_values(i) = var(angle_at_max_g2, 0, 'omitnan');
        n_i                        = numel(g2_all_per_group{i});  % Number of repetitions for this param
        std_error_g2_values(i)     = sqrt(var_max_g2_values(i) / n_i);
    end

    results.folderPath              = folderPath;
    results.scan_parameter          = scan_parameter;
    results.scan_groups             = scan_groups;
    
    results.mean_max_g2_values      = mean_max_g2_values;
    results.std_error_g2_values     = std_error_g2_values;
    results.mean_max_g2_angle       = mean_max_g2_angle_values;
    results.radial_spectral_contrast= mean_rsc;
    results.angular_spectral_weight = mean_asw;
end

%% Helper Functions
function [IMGFFT, IMGPR] = computeFourierTransform(I, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization)
    % computeFourierSpectrum - Computes the 2D Fourier power spectrum 
    % of binarized and enhanced lattice image features, with optional central mask.
    %
    % Inputs:
    %   I           - Grayscale or RGB image matrix
    %
    % Output:
    %   F_mag       - 2D Fourier power spectrum (shifted)
    
    if ~skipPreprocessing
        % Preprocessing: Denoise
        filtered             = imgaussfilt(I, 10);
        IMGPR                = I - filtered; % adjust sigma as needed
    else
        IMGPR                = I;
    end
    
    if ~skipMasking
        [rows, cols]     = size(IMGPR);
        [X, Y]           = meshgrid(1:cols, 1:rows);
        % Elliptical mask parameters
        cx               = cols / 2;
        cy               = rows / 2;
        
        % Shifted coordinates
        x                = X - cx;
        y                = Y - cy;
        
        % Ellipse semi-axes
        rx               = 0.4 * cols;
        ry               = 0.2 * rows;
        
        % Rotation angle in degrees -> radians
        theta_deg        = 30;        % Adjust as needed
        theta            = deg2rad(theta_deg);
        
        % Rotated ellipse equation
        cos_t            = cos(theta);
        sin_t            = sin(theta);
        
        x_rot            = (x * cos_t + y * sin_t);
        y_rot            = (-x * sin_t + y * cos_t);
        
        ellipseMask      = (x_rot.^2) / rx^2 + (y_rot.^2) / ry^2 <= 1;
        
        % Apply cutout mask
        IMGPR            = IMGPR .* ellipseMask;
    end

    if ~skipIntensityThresholding
        % Apply global intensity threshold mask
        intensity_thresh = 0.20; 
        intensity_mask   = IMGPR > intensity_thresh;
        IMGPR            = IMGPR .* intensity_mask;
    end

    if ~skipBinarization
        % Adaptive binarization and cleanup
        IMGPR            = imbinarize(IMGPR, 'adaptive', 'Sensitivity', 0.0);
        IMGPR            = imdilate(IMGPR, strel('disk', 2));
        IMGPR            = imerode(IMGPR, strel('disk', 1));
        IMGPR            = imfill(IMGPR, 'holes');
        F                = fft2(double(IMGPR)); % Compute 2D Fourier Transform
        IMGFFT           = abs(fftshift(F))';  % Shift zero frequency to center
    else
        F                = fft2(double(IMGPR)); % Compute 2D Fourier Transform
        IMGFFT           = abs(fftshift(F))';  % Shift zero frequency to center
    end
end

function [k_rho_vals, S_radial] = computeRadialSpectralDistribution(IMGFFT, kx, ky, thetamin, thetamax, num_bins)
    % IMGFFT    : 2D FFT image (fftshifted and cropped)
    % kx, ky    : 1D physical wavenumber axes [μm⁻¹] matching FFT size
    % thetamin  : Minimum angle (in radians)
    % thetamax  : Maximum angle (in radians)
    % num_bins  : Number of radial bins

    [KX, KY] = meshgrid(kx, ky);
    K_rho = sqrt(KX.^2 + KY.^2);
    Theta = atan2(KY, KX);

    if thetamin < thetamax
        angle_mask = (Theta >= thetamin) & (Theta <= thetamax);
    else
        angle_mask = (Theta >= thetamin) | (Theta <= thetamax);
    end

    power_spectrum = abs(IMGFFT).^2;

    r_min = min(K_rho(angle_mask));
    r_max = max(K_rho(angle_mask));
    r_edges = linspace(r_min, r_max, num_bins + 1);
    k_rho_vals = 0.5 * (r_edges(1:end-1) + r_edges(2:end));
    S_radial = zeros(1, num_bins);

    for i = 1:num_bins
        r_low = r_edges(i);
        r_high = r_edges(i + 1);
        radial_mask = (K_rho >= r_low) & (K_rho < r_high);
        full_mask = radial_mask & angle_mask;
        S_radial(i) = sum(power_spectrum(full_mask));
    end
end

function [theta_vals, S_theta] = computeAngularSpectralDistribution(IMGFFT, kx, ky, k_min, k_max, num_bins, threshold, sigma, windowSize)
    % Apply threshold to isolate strong peaks
    IMGFFT(IMGFFT < threshold) = 0;

    % Create wavenumber meshgrid
    [KX, KY] = meshgrid(kx, ky);
    Kmag     = sqrt(KX.^2 + KY.^2);       % radial wavenumber magnitude
    Theta    = atan2(KY, KX);             % range [-pi, pi]

    % Restrict to radial band in wavenumber space
    radial_mask = (Kmag >= k_min) & (Kmag <= k_max);

    % Initialize angular structure factor
    S_theta    = zeros(1, num_bins);
    theta_vals = linspace(0, pi, num_bins);  % only 0 to pi due to symmetry

    % Loop over angular bins
    for i = 1:num_bins
        angle_start = (i - 1) * pi / num_bins;
        angle_end   = i * pi / num_bins;
        angle_mask  = (Theta >= angle_start) & (Theta < angle_end);
        bin_mask    = radial_mask & angle_mask;
        fft_angle   = IMGFFT .* bin_mask;
        S_theta(i)  = sum(sum(abs(fft_angle).^2));
    end

    % Optional smoothing
    if exist('sigma', 'var') && ~isempty(sigma)
        % Gaussian smoothing
        half_width   = ceil(3 * sigma);
        x            = -half_width:half_width;
        gauss_kernel = exp(-x.^2 / (2 * sigma^2));
        gauss_kernel = gauss_kernel / sum(gauss_kernel);

        % Circular convolution
        S_theta = conv([S_theta(end - half_width + 1:end), S_theta, S_theta(1:half_width)], ...
                       gauss_kernel, 'same');
        S_theta = S_theta(half_width + 1:end - half_width);
    elseif exist('windowSize', 'var') && ~isempty(windowSize)
        % Moving average smoothing
        pad    = floor(windowSize / 2);
        kernel = ones(1, windowSize) / windowSize;
        S_theta = conv([S_theta(end - pad + 1:end), S_theta, S_theta(1:pad)], kernel, 'same');
        S_theta = S_theta(pad + 1:end - pad);
    end
end

function contrast = computeRadialSpectralContrast(k_rho_vals, S_k_smoothed, k_min, k_max)
%   Computes the ratio of the peak in S_k_smoothed within [k_min, k_max]
%   to the value at (or near) k = 0.

    % Ensure inputs are column vectors
    k_rho_vals     = k_rho_vals(:);
    S_k_smoothed   = S_k_smoothed(:);

    % Step 1: Find index of k ≈ 0
    [~, idx_k0] = min(abs(k_rho_vals));  % Closest to zero
    S_k0 = S_k_smoothed(idx_k0);

    % Step 2: Find indices in specified k-range
    in_range = (k_rho_vals >= k_min) & (k_rho_vals <= k_max);
    
    if ~any(in_range)
        warning('No values found in the specified k-range. Returning NaN.');
        contrast = NaN;
        return;
    end

    % Step 3: Find peak value in the specified k-range
    S_k_peak = max(S_k_smoothed(in_range));

    % Step 4: Compute contrast
    contrast = S_k_peak / S_k0;

end

function ret = getBkgOffsetFromCorners(img, x_fraction, y_fraction)
    % image must be a 2D numerical array
    [dim1, dim2] = size(img);

    s1 = img(1:round(dim1 * y_fraction), 1:round(dim2 * x_fraction)); 
    s2 = img(1:round(dim1 * y_fraction), round(dim2 - dim2 * x_fraction):dim2);
    s3 = img(round(dim1 - dim1 * y_fraction):dim1, 1:round(dim2 * x_fraction));
    s4 = img(round(dim1 - dim1 * y_fraction):dim1, round(dim2 - dim2 * x_fraction):dim2);

    ret = mean([mean(s1(:)), mean(s2(:)), mean(s3(:)), mean(s4(:))]);
end

function ret = subtractBackgroundOffset(img, fraction)
    % Remove the background from the image.
    % :param dataArray: The image
    % :type dataArray: xarray DataArray
    % :param x_fraction: The fraction of the pixels used in x axis
    % :type x_fraction: float
    % :param y_fraction: The fraction of the pixels used in y axis
    % :type y_fraction: float
    % :return: The image after removing background
    % :rtype: xarray DataArray
    
    x_fraction = fraction(1);
    y_fraction = fraction(2);
    offset = getBkgOffsetFromCorners(img, x_fraction, y_fraction);
    ret    = img - offset;
end

function ret = cropODImage(img, center, span)
    % Crop the image according to the region of interest (ROI).
    % :param dataSet: The images
    % :type dataSet: xarray DataArray or DataSet
    % :param center: The center of region of interest (ROI)
    % :type center: tuple
    % :param span: The span of region of interest (ROI)
    % :type span: tuple
    % :return: The cropped images
    % :rtype: xarray DataArray or DataSet

    x_start = floor(center(1) - span(1) / 2);
    x_end   = floor(center(1) + span(1) / 2);
    y_start = floor(center(2) - span(2) / 2);
    y_end   = floor(center(2) + span(2) / 2);

    ret = img(y_start:y_end, x_start:x_end);
end

function imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime)
%CALCULATEODIMAGE Calculates the optical density (OD) image for absorption imaging.
%
%   imageOD = calculateODImage(imageAtom, imageBackground, imageDark, mode, exposureTime)
%
%   Inputs:
%       imageAtom       - Image with atoms
%       imageBackground - Image without atoms
%       imageDark       - Image without light
%       mode            - 'LowIntensity' (default) or 'HighIntensity'
%       exposureTime    - Required only for 'HighIntensity' [in seconds]
%
%   Output:
%       imageOD         - Computed OD image
%

    arguments
        imageAtom       (:,:) {mustBeNumeric}
        imageBackground (:,:) {mustBeNumeric}
        imageDark       (:,:) {mustBeNumeric}
        mode            char {mustBeMember(mode, {'LowIntensity', 'HighIntensity'})} = 'LowIntensity'
        exposureTime    double = NaN
    end

    % Compute numerator and denominator
    numerator   = imageBackground - imageDark;
    denominator = imageAtom - imageDark;

    % Avoid division by zero
    numerator(numerator == 0)     = 1;
    denominator(denominator == 0) = 1;

    % Calculate OD based on mode
    switch mode
        case 'LowIntensity'
            imageOD = -log(abs(denominator ./ numerator));
            
        case 'HighIntensity'
            if isnan(exposureTime)
                error('Exposure time must be provided for HighIntensity mode.');
            end
            imageOD = abs(denominator ./ numerator);
            imageOD = -log(imageOD) + (numerator - denominator) ./ (7000 * (exposureTime / 5e-6));
    end

end

function [optrefimages] = removefringesInImage(absimages, refimages, bgmask)
    % removefringesInImage - Fringe removal and noise reduction from absorption images.
    % Creates an optimal reference image for each absorption image in a set as 
    % a linear combination of reference images, with coefficients chosen to 
    % minimize the least-squares residuals between each absorption image and 
    % the optimal reference image. The coefficients are obtained by solving a 
    % linear set of equations using matrix inverse by LU decomposition.
    %
    % Application of the algorithm is described in C. F. Ockeloen et al, Improved 
    % detection of small atom numbers through image processing, arXiv:1007.2136 (2010).
    %
    % Syntax:
    %    [optrefimages] = removefringesInImage(absimages,refimages,bgmask);
    %
    % Required inputs:
    %    absimages    - Absorption image data, 
    %                   typically 16 bit grayscale images
    %    refimages    - Raw reference image data
    %       absimages and refimages are both cell arrays containing
    %       2D array data. The number of refimages can differ from the 
    %       number of absimages.
    %
    % Optional inputs:
    %    bgmask       - Array specifying background region used, 
    %                   1=background, 0=data. Defaults to all ones.
    % Outputs:
    %    optrefimages - Cell array of optimal reference images, 
    %                   equal in size to absimages.
    %
    
    % Dependencies: none
    %
    % Authors: Shannon Whitlock, Caspar Ockeloen
    % Reference: C. F. Ockeloen, A. F. Tauschinsky, R. J. C. Spreeuw, and
    %            S. Whitlock, Improved detection of small atom numbers through 
    %            image processing, arXiv:1007.2136
    % Email: 
    % May 2009; Last revision: 11 August 2010
    
    % Process inputs
    
    % Set variables, and flatten absorption and reference images
    nimgs  = size(absimages,3);
    nimgsR = size(refimages,3);
    xdim = size(absimages(:,:,1),2);
    ydim = size(absimages(:,:,1),1);
    
    R = single(reshape(refimages,xdim*ydim,nimgsR));
    A = single(reshape(absimages,xdim*ydim,nimgs));
    optrefimages=zeros(size(absimages)); % preallocate
    
    if not(exist('bgmask','var')); bgmask=ones(ydim,xdim); end
    k = find(bgmask(:)==1);  % Index k specifying background region
    
    % Ensure there are no duplicate reference images
    % R=unique(R','rows')'; % comment this line if you run out of memory
    
    % Decompose B = R*R' using singular value or LU decomposition
    [L,U,p] = lu(R(k,:)'*R(k,:),'vector');       % LU decomposition
    
    for j=1:nimgs
        b=R(k,:)'*A(k,j);
        % Obtain coefficients c which minimise least-square residuals  
        lower.LT = true; upper.UT = true;
        c = linsolve(U,linsolve(L,b(p,:),lower),upper);
    
        % Compute optimised reference image
        optrefimages(:,:,j)=reshape(R*c,[ydim xdim]);
    end
end