801 lines
29 KiB
Matlab
801 lines
29 KiB
Matlab
%% Extract Images
|
||
|
||
baseDir = 'D:/Results - Numerics/Data_Full3D/PhaseTransition/DTS/Point2';
|
||
JobNumber = 0;
|
||
runFolder = sprintf('Run_%03d', JobNumber);
|
||
savefileName = 'DropletsToStripes'; % Output file name
|
||
datafileName = './DropletsToStripes.mat';
|
||
reverseOrder = false; % Set this to true to reverse the theta ordering
|
||
|
||
TitleString = 'Droplets To Stripes';
|
||
|
||
%%
|
||
baseDir = 'D:/Results - Numerics/Data_Full3D/PhaseTransition/STD/Point2';
|
||
JobNumber = 0;
|
||
runFolder = sprintf('Run_%03d', JobNumber);
|
||
savefileName = 'StripesToDroplets'; % Output file name
|
||
datafileName = './StripesToDroplets.mat';
|
||
reverseOrder = true; % Set this to true to reverse the theta ordering
|
||
|
||
|
||
TitleString = 'Stripes To Droplets';
|
||
|
||
%%
|
||
folderList = dir(baseDir);
|
||
isValid = [folderList.isdir] & ~ismember({folderList.name}, {'.', '..'});
|
||
folderNames = {folderList(isValid).name};
|
||
nimgs = numel(folderNames);
|
||
|
||
% Extract theta values from folder names
|
||
PolarAngleVals = zeros(1, nimgs);
|
||
for k = 1:nimgs
|
||
tokens = regexp(folderNames{k}, 'theta_(\d{3})', 'tokens');
|
||
if isempty(tokens)
|
||
warning('No theta found in folder name: %s', folderNames{k});
|
||
PolarAngleVals(k) = NaN;
|
||
else
|
||
PolarAngleVals(k) = str2double(tokens{1}{1});
|
||
end
|
||
end
|
||
|
||
% Choose sort direction
|
||
sortDirection = 'ascend';
|
||
if reverseOrder
|
||
sortDirection = 'descend';
|
||
end
|
||
|
||
% Sort folderNames based on polar angle
|
||
[~, sortIdx] = sort(PolarAngleVals, sortDirection);
|
||
folderNames = folderNames(sortIdx);
|
||
PolarAngleVals = PolarAngleVals(sortIdx); % Optional: if you still want sorted list
|
||
|
||
imgs = cell(1, nimgs);
|
||
alphas = zeros(1, nimgs);
|
||
|
||
for k = 1:nimgs
|
||
folderName = folderNames{k};
|
||
SaveDirectory = fullfile(baseDir, folderName, runFolder);
|
||
|
||
% Extract alpha (theta) again from folder name
|
||
tokens = regexp(folderName, 'theta_(\d{3})', 'tokens');
|
||
alpha_val = str2double(tokens{1}{1});
|
||
alphas(k) = alpha_val;
|
||
|
||
matPath = fullfile(SaveDirectory, 'psi_gs.mat');
|
||
if ~isfile(matPath)
|
||
warning('Missing psi_gs.mat in %s', SaveDirectory);
|
||
continue;
|
||
end
|
||
|
||
try
|
||
Data = load(matPath, 'psi', 'Params', 'Transf', 'Observ');
|
||
catch ME
|
||
warning('Failed to load %s: %s', matPath, ME.message);
|
||
continue;
|
||
end
|
||
|
||
Params = Data.Params;
|
||
Transf = Data.Transf;
|
||
Observ = Data.Observ;
|
||
|
||
psi = Data.psi;
|
||
if isgpuarray(psi)
|
||
psi = gather(psi);
|
||
end
|
||
if isgpuarray(Observ.residual)
|
||
Observ.residual = gather(Observ.residual);
|
||
end
|
||
|
||
% Axes and projection
|
||
z = Transf.z * Params.l0 * 1e6;
|
||
dz = z(2)-z(1);
|
||
|
||
if k == 1
|
||
x = Transf.x * Params.l0 * 1e6;
|
||
y = Transf.y * Params.l0 * 1e6;
|
||
% Calculate frequency increment (frequency axes)
|
||
Nx = length(x); % grid size along X
|
||
Ny = length(y); % grid size along Y
|
||
dx = mean(diff(x)); % real space increment in the X direction (in micrometers)
|
||
dy = mean(diff(y)); % real space increment in the Y direction (in micrometers)
|
||
dvx = 1 / (Nx * dx); % reciprocal space increment in the X direction (in micrometers^-1)
|
||
dvy = 1 / (Ny * dy); % reciprocal space increment in the Y direction (in micrometers^-1)
|
||
|
||
% Create the frequency axes
|
||
vx = (-Nx/2:Nx/2-1) * dvx; % Frequency axis in X (micrometers^-1)
|
||
vy = (-Ny/2:Ny/2-1) * dvy; % Frequency axis in Y (micrometers^-1)
|
||
|
||
% Create the Wavenumber axes
|
||
kx_full = 2*pi*vx; % Wavenumber axis in X
|
||
ky_full = 2*pi*vy; % Wavenumber axis in Y
|
||
end
|
||
|
||
n = abs(psi).^2;
|
||
nxy = squeeze(trapz(n * dz, 3));
|
||
|
||
imgs{k} = nxy;
|
||
end
|
||
|
||
%% Analyze Images
|
||
|
||
skipMovieRender = true; % Set to true to disable movie creation
|
||
skipSaveFigures = false;
|
||
font = 'Bahnschrift';
|
||
|
||
skipPreprocessing = true;
|
||
skipMasking = true;
|
||
skipIntensityThresholding = true;
|
||
skipBinarization = true;
|
||
|
||
% Run Fourier analysis over images
|
||
|
||
fft_imgs = cell(1, nimgs);
|
||
angular_spectral_distribution = cell(1, nimgs);
|
||
theta_values = cell(1, nimgs);
|
||
radial_spectral_contrast = zeros(1, nimgs);
|
||
angular_spectral_weight = zeros(1, nimgs);
|
||
g2_all = cell(1, nimgs);
|
||
theta_values_all = cell(1, nimgs);
|
||
|
||
N_shots = length(imgs);
|
||
ps_list = cell(1, N_shots); % 2D power spectrum
|
||
s_k_list = cell(1, N_shots); % Radial spectrum
|
||
s_theta_list = cell(1, N_shots); % Angular spectrum
|
||
|
||
% Fourier analysis settings
|
||
|
||
% Radial Spectral Distribution
|
||
theta_min = deg2rad(0);
|
||
theta_max = deg2rad(180);
|
||
N_radial_bins = 500;
|
||
Radial_Sigma = 2;
|
||
Radial_WindowSize = 5; % Choose an odd number for a centered moving average
|
||
|
||
% Angular Spectral Distribution
|
||
k_min = 1.2771; % in μm⁻¹
|
||
k_max = 2.5541; % in μm⁻¹
|
||
N_angular_bins = 180;
|
||
Angular_Threshold = 25;
|
||
Angular_Sigma = 2;
|
||
Angular_WindowSize = 5;
|
||
|
||
zoom_size = 50; % Zoomed-in region around center
|
||
|
||
if ~skipMovieRender
|
||
% 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
|
||
end
|
||
|
||
if ~skipSaveFigures
|
||
% Define folder for saving images
|
||
saveFolder = [savefileName '_SavedFigures'];
|
||
if ~exist(saveFolder, 'dir')
|
||
mkdir(saveFolder);
|
||
end
|
||
end
|
||
% Display the cropped image
|
||
for k = 1:nimgs
|
||
IMG = imgs{k};
|
||
if ~(max(IMG(:)) > 1)
|
||
IMGFFT = NaN(size(IMG));
|
||
else
|
||
[IMGFFT, IMGPR] = computeFourierTransform(IMG, skipPreprocessing, skipMasking, skipIntensityThresholding, skipBinarization);
|
||
end
|
||
|
||
% 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
|
||
angular_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);
|
||
|
||
g2 = zeros(1, N_angular_bins); % Preallocate
|
||
|
||
for dtheta = 0:N_angular_bins-1
|
||
profile = S_theta;
|
||
profile_shifted = circshift(profile, -dtheta, 2);
|
||
|
||
num = mean(profile .* profile_shifted);
|
||
denom = mean(profile.^2);
|
||
|
||
g2(dtheta+1) = num / denom;
|
||
end
|
||
|
||
ps_list{k} = abs(fft_imgs{k}).^2; % store the power spectrum
|
||
s_k_list{k} = S_k_smoothed; % store smoothed radial spectrum
|
||
s_theta_list{k} = S_theta; % store angular spectrum
|
||
g2_all{k} = g2;
|
||
theta_values_all{k} = theta_vals;
|
||
|
||
figure(1);
|
||
clf
|
||
set(gcf,'Position',[500 100 1000 800])
|
||
t = tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'compact'); % 1x4 grid
|
||
|
||
y_min = min(y);
|
||
y_max = max(y);
|
||
x_min = min(x);
|
||
x_max = max(x);
|
||
|
||
|
||
% Display the cropped OD image
|
||
ax1 = nexttile;
|
||
imagesc(x, y, 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, ['Angle = ', num2str(alphas(k), '%.1f')], ...
|
||
'Color', 'white', 'FontWeight', 'bold', 'Interpreter', 'tex', 'FontSize', 20, 'Units', 'normalized', 'HorizontalAlignment', 'right', 'VerticalAlignment', 'top');
|
||
axis square;
|
||
hcb = colorbar;
|
||
colormap(ax1, Colormaps.plasma());
|
||
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('Density', '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
|
||
|
||
% ======= FFT POWER SPECTRUM (reciprocal space) =======
|
||
ax2 = nexttile;
|
||
imagesc(kx, ky, log(1 + ps_list{k}));
|
||
axis tight;
|
||
set(gca, 'FontSize', 14, 'YDir', 'normal')
|
||
xlabel('k_x [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
|
||
ylabel('k_y [\mum^{-1}]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
|
||
title('Power Spectrum - S(k_x,k_y)', 'Interpreter', 'tex', ...
|
||
'FontSize', 16, 'FontWeight', 'bold', 'FontName', font);
|
||
colorbar;
|
||
colormap(ax2, Colormaps.coolwarm());
|
||
|
||
% ======= ANGULAR DISTRIBUTION (S(θ)) =======
|
||
nexttile;
|
||
plot(theta_vals/pi, S_theta_norm, 'LineWidth', 2);
|
||
axis image;
|
||
set(gca, 'FontSize', 14, 'YLim', [0, 1]);
|
||
xlabel('\theta/\pi [rad]', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
|
||
ylabel('Magnitude (a.u.)', 'Interpreter', 'tex', 'FontSize', 14, 'FontName', font);
|
||
title('Angular Spectral Distribution - S(\theta)', 'Interpreter', 'tex', ...
|
||
'FontSize', 16, 'FontWeight', 'bold', 'FontName', font);
|
||
grid on; % Enable major grid
|
||
ax = gca;
|
||
ax.MinorGridLineStyle = ':'; % Optional: make minor grid dotted
|
||
ax.MinorGridColor = [0.7 0.7 0.7]; % Optional: light gray minor grid color
|
||
ax.MinorGridAlpha = 0.5; % Optional: transparency for minor grid
|
||
|
||
ax.XMinorGrid = 'on'; % Enable minor grid for x-axis
|
||
ax.YMinorGrid = 'on'; % Enable minor grid for y-axis (if desired)
|
||
|
||
% ======= ANGULAR CORRELATION g2(θ) =======
|
||
nexttile
|
||
plot(theta_vals/pi, g2, 'o-', 'LineWidth', 1.2, 'MarkerSize', 5);
|
||
set(gca, 'FontSize', 14);
|
||
ylim([0.0 1.0]); % Set y-axis limits here
|
||
hXLabel = xlabel('$\delta\theta / \pi$', 'Interpreter', 'latex');
|
||
hYLabel = ylabel('$g^{(2)}(\delta\theta)$', 'Interpreter', 'latex');
|
||
hTitle = title('Angular Correlation', '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;
|
||
|
||
if ~skipMovieRender
|
||
% 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
|
||
if ~skipSaveFigures
|
||
% Construct a filename for each image
|
||
fileNamePNG = fullfile(saveFolder, sprintf('fft_analysis_img_%03d.png', k));
|
||
|
||
% Save current figure as PNG with high resolution
|
||
print(gcf, fileNamePNG, '-dpng', '-r100'); % 300 dpi for high quality
|
||
end
|
||
if skipMovieRender & skipSaveFigures
|
||
pause(0.5);
|
||
end
|
||
end
|
||
|
||
if ~skipMovieRender
|
||
% Close the video file
|
||
close(videoFile);
|
||
disp(['Movie saved to ', savefileName]);
|
||
end
|
||
|
||
%% Track across the transition
|
||
|
||
% 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, :) = angular_spectral_distribution{k};
|
||
end
|
||
|
||
N_params = length(alphas);
|
||
% 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);
|
||
|
||
% 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_data = delta_nkr_all(i, :);
|
||
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);
|
||
denom = mean(ref.^2);
|
||
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');
|
||
end
|
||
|
||
% Plot peak offset angular correlation
|
||
figure(2);
|
||
set(gcf,'Position',[100 100 950 750])
|
||
set(gca, 'FontSize', 14); % For tick labels only
|
||
plot(alphas, ... % x-axis
|
||
mean_max_g2_values, ... % y-axis (mean)
|
||
'--o', 'LineWidth', 1.8, 'MarkerSize', 6 );
|
||
|
||
set(gca, 'FontSize', 14, 'YLim', [0, 1]);
|
||
hXLabel = xlabel('$\alpha$ (degrees)', 'Interpreter', 'latex');
|
||
hYLabel = ylabel('$\mathrm{max}[g^{(2)}_{[50,70]}(\delta\theta)]$', 'Interpreter', 'latex');
|
||
hTitle = title(TitleString, 'Interpreter', 'tex');
|
||
set([hXLabel, hYLabel], 'FontName', font);
|
||
set([hXLabel, hYLabel], 'FontSize', 14);
|
||
set(hTitle, 'FontName', font, 'FontSize', 16, 'FontWeight', 'bold');
|
||
grid on;
|
||
|
||
% Plot full angular correlation
|
||
figure(3);
|
||
clf;
|
||
set(gcf,'Position',[100 100 950 750])
|
||
hold on;
|
||
|
||
% Reconstruct theta axis from any one of the stored values
|
||
theta_vals = theta_values_all{1}; % assuming it's in radians
|
||
|
||
legend_entries = cell(nimgs, 1);
|
||
|
||
% Generate a colormap with enough unique colors
|
||
cmap = sky(nimgs); % You can also try 'jet', 'turbo', 'hot', etc.
|
||
|
||
for i = 1:nimgs
|
||
plot(theta_vals/pi, g2_all{i}, ...
|
||
'o-', 'Color', cmap(i,:), 'LineWidth', 1.2, ...
|
||
'MarkerSize', 5);
|
||
legend_entries{i} = sprintf('$\\alpha = %g^\\circ$', alphas(i));
|
||
end
|
||
|
||
ylim([0.0 1.0]); % Set y-axis limits here
|
||
set(gca, 'FontSize', 14);
|
||
hXLabel = xlabel('$\delta\theta / \pi$', 'Interpreter', 'latex');
|
||
hYLabel = ylabel('$g^{(2)}(\delta\theta)$', 'Interpreter', 'latex');
|
||
hTitle = title(TitleString, 'Interpreter', 'tex');
|
||
legend(legend_entries, 'Interpreter', 'latex', 'Location', 'bestoutside');
|
||
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(datafileName, 'alphas', 'mean_max_g2_values', 'theta_vals', 'g2_all');
|
||
|
||
%% Compare between transitions
|
||
|
||
set(0,'defaulttextInterpreter','latex')
|
||
set(groot, 'defaultAxesTickLabelInterpreter','latex'); set(groot, 'defaultLegendInterpreter','latex');
|
||
|
||
format long
|
||
|
||
font = 'Bahnschrift';
|
||
|
||
% Load data
|
||
Data = load('./DropletsToStripes.mat', 'alphas', 'mean_max_g2_values');
|
||
dts_alphas = Data.alphas;
|
||
dts_max_g2 = Data.mean_max_g2_values;
|
||
|
||
Data = load('./StripesToDroplets.mat', 'alphas', 'mean_max_g2_values');
|
||
std_alphas = Data.alphas;
|
||
std_max_g2 = Data.mean_max_g2_values;
|
||
|
||
figure(5);
|
||
set(gcf,'Position',[100 100 950 750])
|
||
plot(dts_alphas, dts_max_g2, 'o--', 'LineWidth', 1.5, 'MarkerSize', 6, 'DisplayName' , 'Droplets to Stripes');
|
||
hold on
|
||
plot(std_alphas, std_max_g2, 'o--', 'LineWidth', 1.5, 'MarkerSize', 6, 'DisplayName' , 'Stripes to Droplets');
|
||
set(gca, 'FontSize', 14); % For tick labels only
|
||
hXLabel = xlabel('$\alpha$ (degrees)', 'Interpreter', 'latex');
|
||
hYLabel = ylabel('$\mathrm{max}[g^{(2)}_{[50,70]}(\delta\theta)]$', 'Interpreter', 'latex');
|
||
hTitle = title('Change across transition', 'Interpreter', 'tex');
|
||
legend
|
||
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
|
||
|
||
%%
|
||
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 drawPSOverlays(kx, ky, r_min, r_max)
|
||
% drawFFTOverlays - Draw overlays on existing FFT plot:
|
||
% - Radial lines every 30°
|
||
% - Annular highlight with white (upper half) and gray (lower half) circles between r_min and r_max
|
||
% - Horizontal white bands at ky=0 in annulus region
|
||
% - Scale ticks and labels every 1 μm⁻¹ along each radial line
|
||
%
|
||
% Inputs:
|
||
% kx, ky - reciprocal space vectors (μm⁻¹)
|
||
% r_min - inner annulus radius offset index (integer)
|
||
% r_max - outer annulus radius offset index (integer)
|
||
%
|
||
% Example:
|
||
% hold on;
|
||
% drawFFTOverlays(kx, ky, 10, 30);
|
||
|
||
hold on
|
||
|
||
% === Overlay Radial Lines + Scales ===
|
||
[kx_grid, ky_grid] = meshgrid(kx, ky);
|
||
[~, kr_grid] = cart2pol(kx_grid, ky_grid); % kr_grid in μm⁻¹
|
||
|
||
max_kx = max(kx);
|
||
max_ky = max(ky);
|
||
|
||
for angle = 0 : pi/6 : pi
|
||
x_line = [0, max_kx] * cos(angle);
|
||
y_line = [0, max_ky] * sin(angle);
|
||
|
||
% Plot radial lines
|
||
plot(x_line, y_line, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.2);
|
||
plot(x_line, -y_line, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.2);
|
||
|
||
% Draw scale ticks along positive radial line
|
||
drawTicksAlongLine(0, 0, x_line(2), y_line(2));
|
||
|
||
% Draw scale ticks along negative radial line (reflect y)
|
||
drawTicksAlongLine(0, 0, x_line(2), -y_line(2));
|
||
end
|
||
|
||
% === Overlay Annular Highlight: White (r_min to r_max), Gray elsewhere ===
|
||
theta_full = linspace(0, 2*pi, 500);
|
||
|
||
center_x = ceil(size(kr_grid, 2) / 2);
|
||
center_y = ceil(size(kr_grid, 1) / 2);
|
||
|
||
k_min = kr_grid(center_y, center_x + r_min);
|
||
k_max = kr_grid(center_y, center_x + r_max);
|
||
|
||
% Upper half: white dashed circles
|
||
x1_upper = k_min * cos(theta_full(theta_full <= pi));
|
||
y1_upper = k_min * sin(theta_full(theta_full <= pi));
|
||
x2_upper = k_max * cos(theta_full(theta_full <= pi));
|
||
y2_upper = k_max * sin(theta_full(theta_full <= pi));
|
||
plot(x1_upper, y1_upper, 'k--', 'LineWidth', 1.2);
|
||
plot(x2_upper, y2_upper, 'k--', 'LineWidth', 1.2);
|
||
|
||
% Lower half: gray dashed circles
|
||
x1_lower = k_min * cos(theta_full(theta_full > pi));
|
||
y1_lower = k_min * sin(theta_full(theta_full > pi));
|
||
x2_lower = k_max * cos(theta_full(theta_full > pi));
|
||
y2_lower = k_max * sin(theta_full(theta_full > pi));
|
||
plot(x1_lower, y1_lower, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.0);
|
||
plot(x2_lower, y2_lower, '--', 'Color', [0.5 0.5 0.5], 'LineWidth', 1.0);
|
||
|
||
% === Highlight horizontal band across k_y = 0 ===
|
||
x_vals = kx;
|
||
xW1 = x_vals((x_vals >= -k_max) & (x_vals < -k_min));
|
||
xW2 = x_vals((x_vals > k_min) & (x_vals <= k_max));
|
||
|
||
plot(xW1, zeros(size(xW1)), 'k--', 'LineWidth', 1.2);
|
||
plot(xW2, zeros(size(xW2)), 'k--', 'LineWidth', 1.2);
|
||
|
||
hold off
|
||
|
||
|
||
% --- Nested helper function to draw ticks along a radial line ---
|
||
function drawTicksAlongLine(x_start, y_start, x_end, y_end)
|
||
% Tick parameters
|
||
tick_spacing = 1; % spacing between ticks in μm⁻¹
|
||
tick_length = 0.05 * sqrt((x_end - x_start)^2 + (y_end - y_start)^2); % relative tick length
|
||
line_color = [0.5 0.5 0.5];
|
||
tick_color = [0.5 0.5 0.5];
|
||
font_size = 8;
|
||
|
||
% Vector along the line
|
||
dx = x_end - x_start;
|
||
dy = y_end - y_start;
|
||
L = sqrt(dx^2 + dy^2);
|
||
ux = dx / L;
|
||
uy = dy / L;
|
||
|
||
% Perpendicular vector for ticks
|
||
perp_ux = -uy;
|
||
perp_uy = ux;
|
||
|
||
% Number of ticks (from 0 up to max length)
|
||
n_ticks = floor(L / tick_spacing);
|
||
|
||
for i = 1:n_ticks
|
||
% Position of tick along the line
|
||
xt = x_start + i * tick_spacing * ux;
|
||
yt = y_start + i * tick_spacing * uy;
|
||
|
||
% Tick endpoints
|
||
xt1 = xt - 0.5 * tick_length * perp_ux;
|
||
yt1 = yt - 0.5 * tick_length * perp_uy;
|
||
xt2 = xt + 0.5 * tick_length * perp_ux;
|
||
yt2 = yt + 0.5 * tick_length * perp_uy;
|
||
|
||
% Draw tick
|
||
plot([xt1 xt2], [yt1 yt2], '-', 'Color', tick_color, 'LineWidth', 1);
|
||
|
||
% Label with distance (integer)
|
||
text(xt, yt, sprintf('%d', i), ...
|
||
'Color', tick_color, ...
|
||
'FontSize', font_size, ...
|
||
'HorizontalAlignment', 'center', ...
|
||
'VerticalAlignment', 'bottom', ...
|
||
'Rotation', atan2d(dy, dx));
|
||
end
|
||
end
|
||
|
||
end |