%% Parameters

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"];

folderPath     = "//DyLabNAS/Data/TwoDGas/2025/06/24/";

run            = '0001';

folderPath     = strcat(folderPath, run);

cam            = 5; 

angle          = 0;
center         = [1410, 2030];
span           = [200, 200];
fraction       = [0.1, 0.1];

pixel_size     = 5.86e-6;
removeFringes  = false;

scan_parameter      = 'ps_rot_mag_fin_pol_angle';
% scan_parameter      = 'rot_mag_field';
scan_parameter_text = 'Angle = ';
% scan_parameter_text = 'BField = ';

savefolderPath              = 'D:/Results - Experiment/B2.42G/';
savefileName                = 'StripesToDroplets';
font                        = 'Bahnschrift';

skipUnshuffling             = false;
if strcmp(savefileName, 'DropletsToStripes')
    scan_groups             = 0:5:45;
else
    scan_groups             = 45:-5:0;
end

skipPreprocessing           = true;
skipMasking                 = true;
skipIntensityThresholding   = true;
skipBinarization            = true;

%% Compute OD image, rotate and extract ROI for analysis
% Get a list of all files in the folder with the desired file name pattern.

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));
    
    refimages(:,:,k)  = subtractBackgroundOffset(cropODImage(bkg_img, center, span), fraction)';
    absimages(:,:,k)  = subtractBackgroundOffset(cropODImage(calculateODImage(atm_img, bkg_img, dark_img), center, span), fraction)';

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, 'ps_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

%% Run Fourier analysis over images

fft_imgs              = cell(1, nimgs);
spectral_contrast     = zeros(1, nimgs);
spectral_weight       = zeros(1, nimgs);

N_bins                = 180;
Threshold             = 75;
Sigma                 = 2;
N_shots               = length(od_imgs);

% Create VideoWriter object for movie
videoFile = VideoWriter([savefileName '.mp4'], 'MPEG-4');
videoFile.Quality = 100; % Set quality to maximum (0–100)
videoFile.FrameRate = 2;  % Set the frame rate (frames per second)
open(videoFile);  % Open the video file to write

% Display the cropped image
for k = 1:N_shots
    IMG             = od_imgs{k};
    [IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization);
    
    figure(1);
    clf
    set(gcf,'Position',[500 100 1000 800])
    t = tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'compact'); % 1x4 grid
    
    % Calculate the x and y limits for the cropped image
    y_min = center(1) - span(2) / 2;
    y_max = center(1) + span(2) / 2;
    x_min = center(2) - span(1) / 2;
    x_max = center(2) + span(1) / 2;
    
    % Generate x and y arrays representing the original coordinates for each pixel
    x_range = linspace(x_min, x_max, span(1));
    y_range = linspace(y_min, y_max, span(2));
    
    % Display the cropped OD image
    ax1 = nexttile;
    imagesc(x_range, y_range, IMG)
    % Define normalized positions (relative to axis limits)
    x_offset = 0.025; % 5% offset from the edges
    y_offset = 0.025; % 5% offset from the edges
    % Top-right corner (normalized axis coordinates)
    hText = text(1 - x_offset, 1 - y_offset, [scan_parameter_text, num2str(scan_parameter_values(k), '%.1f')], ...
        'Color', 'white', 'FontWeight', 'bold', 'Interpreter', 'tex', 'FontSize', 20, 'Units', 'normalized', 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top');
    axis equal tight;
    hcb = colorbar;
    colormap(ax1, 'jet');
    set(gca, 'FontSize', 14);   % For tick labels only
    hL = ylabel(hcb, 'Optical Density');
    set(hL,'Rotation',-90);
    set(gca,'YDir','normal')
    set(gca, 'YTick', linspace(y_min, y_max, 5));  % Define y ticks
    set(gca, 'YTickLabel', flip(linspace(y_min, y_max, 5)));  % Flip only the labels
    hXLabel = xlabel('x (pixels)', 'Interpreter', 'tex');
    hYLabel = ylabel('y (pixels)', 'Interpreter', 'tex');
    hTitle = title('OD Image', 'Interpreter', 'tex');
    set([hXLabel, hYLabel, hL, hText], 'FontName', font)
    set([hXLabel, hYLabel, hL], 'FontSize', 14)
    set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title
    
    % Plot the processed image
    ax2 = nexttile;
    imagesc(x_range, y_range, IMGPR)
    axis equal tight;
    hcb = colorbar;
    colormap(ax2, 'parula');
    set(gca, 'FontSize', 14);   % For tick labels only
    set(gca,'YDir','normal')
    set(gca, 'YTick', linspace(y_min, y_max, 5));  % Define y ticks
    set(gca, 'YTickLabel', flip(linspace(y_min, y_max, 5)));  % Flip only the labels
    hXLabel = xlabel('x (pixels)', 'Interpreter', 'tex');
    hYLabel = ylabel('y (pixels)', 'Interpreter', 'tex');
    hTitle = title('Processed Image', 'Interpreter', 'tex');
    
    set([hXLabel, hYLabel], 'FontName', font)
    set([hXLabel, hYLabel], 'FontSize', 14)
    set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title

    % Plot the power spectrum
    ax3 = nexttile;
    [rows, cols] = size(IMGFFT);
    zoom_size    = 50;             % Zoomed-in region around center
    mid_x        = floor(cols/2);
    mid_y        = floor(rows/2);
    fft_imgs{k}  = IMGFFT(mid_y-zoom_size:mid_y+zoom_size, mid_x-zoom_size:mid_x+zoom_size);
    imagesc(log(1 + abs(fft_imgs{k}).^2));
    % Define normalized positions (relative to axis limits)
    x_offset = 0.025; % 5% offset from the edges
    y_offset = 0.025; % 5% offset from the edges
    axis equal tight;
    hcb = colorbar;
    colormap(ax3, 'jet');
    set(gca, 'FontSize', 14);   % For tick labels only
    set(gca,'YDir','normal')
    hXLabel = xlabel('k_x', 'Interpreter', 'tex');
    hYLabel = ylabel('k_y', 'Interpreter', 'tex');
    hTitle  = title('Power Spectrum - S(k_x,k_y)', 'Interpreter', 'tex');
    set([hXLabel, hYLabel, hText], 'FontName', font)
    set([hXLabel, hYLabel], 'FontSize', 14)
    set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title

    % Plot the angular distribution
    nexttile
    spectral_contrast(k)  = computeSpectralContrast(fft_imgs{k}, 10, 25, Threshold);
    [theta_vals, S_theta] = computeNormalizedAngularSpectralDistribution(fft_imgs{k}, 10, 20, N_bins, Threshold, Sigma);
    spectral_weight(k)    = trapz(theta_vals, S_theta);
    plot(theta_vals/pi, S_theta,'Linewidth',2);
    set(gca, 'FontSize', 14);   % For tick labels only
    hXLabel = xlabel('\theta/\pi [rad]', 'Interpreter', 'tex');
    hYLabel = ylabel('Normalized magnitude (a.u.)', 'Interpreter', 'tex');
    hTitle = title('Angular Spectral Distribution - S(\theta)', 'Interpreter', 'tex');
    set([hXLabel, hYLabel, hText], 'FontName', font)
    set([hXLabel, hYLabel], 'FontSize', 14)
    set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title
    grid on
    
    drawnow
    pause(0.5)
    
    % Capture the current frame and write it to the video
    frame = getframe(gcf);  % Capture the current figure as a frame
    writeVideo(videoFile, frame);  % Write the frame to the video
end

% Close the video file
close(videoFile);

%% Track across the transition

% 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_sc = zeros(size(unique_scan_parameter_values));
stderr_sc = 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 = spectral_contrast(idx == i);
    mean_sc(i) = mean(group_vals);
    stderr_sc(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N)
end

figure(2);
set(gcf,'Position',[100 100 950 750])
errorbar(unique_scan_parameter_values, mean_sc, stderr_sc, 'o--', ...
    'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5);
set(gca, 'FontSize', 14);   % For tick labels only
hXLabel = xlabel('\alpha (degrees)', 'Interpreter', 'tex');
% hXLabel = xlabel('B_z (G)', 'Interpreter', 'tex');
hYLabel = ylabel('Spectral Contrast', 'Interpreter', 'tex');
hTitle = title('Change across transition', 'Interpreter', 'tex');
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title
grid on


% Preallocate arrays
mean_sw = zeros(size(unique_scan_parameter_values));
stderr_sw = 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 = spectral_weight(idx == i);
    mean_sw(i) = mean(group_vals);
    stderr_sw(i) = std(group_vals) / sqrt(length(group_vals)); % standard error = std / sqrt(N)
end

figure(3);
set(gcf,'Position',[100 100 950 750])
errorbar(unique_scan_parameter_values, mean_sw, stderr_sw, 'o--', ...
    'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5);
set(gca, 'FontSize', 14);   % For tick labels only
hXLabel = xlabel('\alpha (degrees)', 'Interpreter', 'tex');
% hXLabel = xlabel('B_z (G)', 'Interpreter', 'tex');
hYLabel = ylabel('Spectral Weight', 'Interpreter', 'tex');
hTitle = title('Change across transition', 'Interpreter', 'tex');
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title
grid on

save([savefolderPath savefileName '.mat'], 'unique_scan_parameter_values', 'mean_sc', 'stderr_sc', 'mean_sw', 'stderr_sw');

%% k-means Clustering

% Reshape to column vector
X               = mean_sf(:);

% Determine the number of clusters to try (you can experiment with different values here)
optimalClusters = 3;  % Based on prior knowledge or experimentation

% Set the random seed for reproducibility
rng(42);  

% Specify initialization method ('plus' is the default)
startMethod     = 'plus';  % Options: 'uniform', 'plus', 'sample'

% Apply K-means clustering with controlled initialization
[idx, C]        = kmeans(X, optimalClusters, 'Start', startMethod);

% Plot the results
figure(3);
set(gcf,'Position',[100 100 950 750])
hold on;

% Plot error bars with mean_sf and stderr_sf
errorbar(unique_scan_parameter_values, mean_sf, stderr_sf, 'o--', ...
     'LineWidth', 1.5, 'MarkerSize', 6, 'CapSize', 5);

% Scatter plot for data points (showing clusters)
scatter(unique_scan_parameter_values, X, 50, idx, 'filled');

% Get the current y-axis limits
current_ylim    = ylim;

% Generate colors for each cluster
colors          = lines(optimalClusters);  % Create distinct colors for each cluster

% Loop through each cluster and fill the regions
for i = 1:optimalClusters
    % Find indices of data points that belong to the current cluster
    clusterIdx = find(idx == i);
    
    % Find the range of x-values for this cluster
    x_min = unique_scan_parameter_values(clusterIdx(1));  % Starting x-value for the cluster
    x_max = unique_scan_parameter_values(clusterIdx(end));  % Ending x-value for the cluster
    
    % Fill the region corresponding to the cluster
    fill([x_min, x_max, x_max, x_min], ...
         [current_ylim(1), current_ylim(1), current_ylim(2), current_ylim(2)], ...
         colors(i, :), 'EdgeColor', 'none', 'FaceAlpha', 0.3);  % Add transparency
end

hXLabel = xlabel('\alpha (degrees)', 'Interpreter', 'tex');
hYLabel = ylabel('Spectral Weight', 'Interpreter', 'tex');
hTitle = title('Regimes identified by k-means clustering', 'Interpreter', 'tex');
set([hXLabel, hYLabel], 'FontName', font)
set([hXLabel, hYLabel], 'FontSize', 14)
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');  % Set font and size for title
grid on;
hold off;


%% 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 [theta_vals, S_theta] = computeNormalizedAngularSpectralDistribution(IMGFFT, r_min, r_max, num_bins, threshold, sigma)
    % Apply threshold to isolate strong peaks
    IMGFFT(IMGFFT < threshold) = 0;

    % Prepare polar coordinates
    [ny, nx]  = size(IMGFFT);
    [X, Y]    = meshgrid(1:nx, 1:ny);
    cx        = ceil(nx/2); 
    cy        = ceil(ny/2);
    R         = sqrt((X - cx).^2 + (Y - cy).^2);
    Theta     = atan2(Y - cy, X - cx); % range [-pi, pi]

    % Choose radial band
    radial_mask      = (R >= r_min) & (R <= r_max);

    % Initialize the angular structure factor array
    S_theta = zeros(1, num_bins); % Pre-allocate for 180 angle bins
    % Define the angle values for the x-axis
    theta_vals = linspace(0, pi, num_bins);

    % Loop through each angle bin
    for i = 1:num_bins
        angle_start = (i-1) * pi / num_bins;
        angle_end   = i * pi / num_bins;
        
        % Define a mask for the given angle range
        angle_mask  = (Theta >= angle_start & Theta < angle_end);
        
        bin_mask    = radial_mask & angle_mask;

        % Extract the Fourier components for the given angle
        fft_angle   = IMGFFT .* bin_mask;
        
        % Integrate the Fourier components over the radius at the angle
        S_theta(i)  = sum(sum(abs(fft_angle).^2)); % sum of squared magnitudes
    end
    
    % Create a 1D Gaussian kernel
    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); % normalize
    
    % Apply convolution (circular padding to preserve periodicity)
    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); % crop back to original size
    
    % Normalize to 1
    S_theta = S_theta / max(S_theta);
end

function contrast = computeSpectralContrast(IMGFFT, r_min, r_max, threshold)
    % Apply threshold to isolate strong peaks
    IMGFFT(IMGFFT < threshold) = 0;

    % Prepare polar coordinates
    [ny, nx] = size(IMGFFT);
    [X, Y]   = meshgrid(1:nx, 1:ny);
    cx       = ceil(nx/2); 
    cy       = ceil(ny/2);
    R        = sqrt((X - cx).^2 + (Y - cy).^2);

    % Ring region (annulus) mask
    ring_mask = (R >= r_min) & (R <= r_max);

    % Squared magnitude in the ring
    ring_power = abs(IMGFFT).^2 .* ring_mask;

    % Maximum power in the ring
    ring_max = max(ring_power(:));

    % Power at the DC component
    dc_power = abs(IMGFFT(cy, cx))^2;

    % Avoid division by zero
    if dc_power == 0
        contrast = Inf;  % or NaN or 0, depending on how you want to handle this
    else
        contrast = ring_max / dc_power;
    end
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 ret = calculateODImage(imageAtom, imageBackground, imageDark)
    % Calculate the OD image for absorption imaging.
    % :param imageAtom: The image with atoms
    % :type imageAtom: numpy array
    % :param imageBackground: The image without atoms
    % :type imageBackground: numpy array
    % :param imageDark: The image without light
    % :type imageDark: numpy array
    % :return: The OD images
    % :rtype: numpy array

    numerator   = imageBackground - imageDark;
    denominator = imageAtom - imageDark;

    numerator(numerator == 0)     = 1;
    denominator(denominator == 0) = 1;
    
    ret = -log(double(abs(denominator ./ numerator)));

    if numel(ret) == 1
        ret = ret(1);
    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

% Deprecated
%% Display Images
%{
figure(1)
clf
set(gcf,'Position',[50 50 950 750])

% Calculate the x and y limits for the cropped image
y_min = center(1) - span(2) / 2;
y_max = center(1) + span(2) / 2;
x_min = center(2) - span(1) / 2;
x_max = center(2) + span(1) / 2;

% Generate x and y arrays representing the original coordinates for each pixel
x_range = linspace(x_min, x_max, span(1));
y_range = linspace(y_min, y_max, span(2));

% Display the cropped image
for k = 1 : length(od_imgs)
    imagesc(x_range, y_range, od_imgs{k})
    axis equal tight;
    hcb = colorbar;
    hL = ylabel(hcb, 'Optical Density');
    set(hL,'Rotation',-90);
    colormap jet;
    set(gca,'CLim',[0 3.0]);
    set(gca,'YDir','normal')
    set(gca, 'YTick', linspace(y_min, y_max, 5));  % Define y ticks
    set(gca, 'YTickLabel', flip(linspace(y_min, y_max, 5)));  % Flip only the labels
    xlabel('X', 'Interpreter', 'tex');
    ylabel('Y', 'Interpreter', 'tex');

    drawnow
    pause(0.5)
end

%}

%% Angular Distribution
%{

function [theta_vals, angular_intensity] = computeAngularDistribution(IMGFFT, r_min, r_max, num_bins, threshold)

    % Apply threshold to isolate strong peaks
    IMGFFT(IMGFFT < threshold) = 0;

    % Prepare polar coordinates
    [ny, nx]  = size(IMGFFT);
    [X, Y]    = meshgrid(1:nx, 1:ny);
    cx        = ceil(nx/2); 
    cy        = ceil(ny/2);
    R         = sqrt((X - cx).^2 + (Y - cy).^2);
    Theta     = atan2(Y - cy, X - cx); % range [-pi, pi]

    % Choose radial band
    mask      = (R >= r_min) & (R <= r_max);

    % Bin intensities by angle
    theta_vals        = linspace(-pi, pi, num_bins+1);
    angular_intensity = zeros(1, num_bins);
    
    for i = 1:num_bins
        t0 = theta_vals(i);
        t1 = theta_vals(i+1);
        bin_mask = mask & (Theta >= t0) & (Theta < t1);
        tmp = mean(IMGFFT(bin_mask), 'all');
        if tmp > 50
            angular_intensity(i) = tmp;
        else
            angular_intensity(i) = 0;
        end
    end
end
%}