Calculations/Data-Analyzer/+Analyzer/analyzeG2Structures.m

299 lines
12 KiB
Matlab
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function analysis_results = analyzeG2Structures(g2_results, opts)
%% analyzeG2Structures
% Computes peak anisotropy of g² correlation matrices using ROI and boundary
% analysis, and fits an oriented two-mode Gaussian aligned with the
% structure's principal axis within the ROI.
%
% Inputs:
% g2_results : struct with fields
% - g2_matrices : cell array of 2D matrices
% - dx_phys : cell array of x-coordinates (µm)
% - dy_phys : cell array of y-coordinates (µm)
% opts : struct with fields
% - roi.center : [x0, y0] center of ROI box (µm)
% - roi.size : [w, h] width and height (µm)
% - roi.angle : rotation angle (radians, CCW)
% - threshold : fraction of peak maximum to define core (default 0.5)
% - skipLivePlot : true/false (default false)
% - fitDeviationThreshold : max normalized RMS deviation for accepting fit
% - boundaryPad : extra pixels around boundary to allow Gaussian peaks
%
% Outputs:
% analysis_results : struct with fields
% - anisotropy_vals : numeric array
% - peak_centroid : {N×1} fitted centroids [x, y]
% - ellipse_params : {N×1} fitted parameters + angle
% - boundary_coords : {N×1} [x, y] boundary points
if ~isfield(opts, "skipLivePlot"), opts.skipLivePlot = false; end
if ~isfield(opts, "threshold"), opts.threshold = 0.5; end
if ~isfield(opts, "font"), opts.font = 'Arial'; end
if ~isfield(opts, "fitDeviationThreshold"), opts.fitDeviationThreshold = 0.2; end
if ~isfield(opts, "boundaryPad"), opts.boundaryPad = 2; end
N_images = numel(g2_results.g2_matrices);
analysis_results = struct();
analysis_results.anisotropy_vals = zeros(1, N_images);
analysis_results.peak_centroid = cell(1, N_images);
analysis_results.boundary_coords = cell(1, N_images);
analysis_results.ellipse_params = cell(1, N_images);
% ROI definition
x0 = opts.roi.center(1);
y0 = opts.roi.center(2);
w = opts.roi.size(1);
h = opts.roi.size(2);
roi_theta = opts.roi.angle;
for k = 1:N_images
g2_matrix = g2_results.g2_matrices{k};
dx_phys = g2_results.dx_phys{k};
dy_phys = g2_results.dy_phys{k};
[X, Y] = meshgrid(dx_phys, dy_phys);
%% ---- Core ROI extraction (rotated rectangle) ----
Xc = X - x0;
Yc = Y - y0;
Xr = cos(roi_theta)*Xc + sin(roi_theta)*Yc;
Yr = -sin(roi_theta)*Xc + cos(roi_theta)*Yc;
roi_mask = (abs(Xr) <= w/2) & (abs(Yr) <= h/2);
x_roi = Xr(roi_mask);
y_roi = Yr(roi_mask);
z_roi = g2_matrix(roi_mask);
%% ---- Threshold ROI to define core region ----
g2_roi = zeros(size(g2_matrix));
g2_roi(roi_mask) = g2_matrix(roi_mask);
thresh_val = opts.threshold * max(g2_roi(:));
BW = g2_roi >= thresh_val;
if ~any(BW(:))
warning('No peak found in ROI for image %d', k);
analysis_results.peak_centroid{k} = [NaN, NaN];
analysis_results.anisotropy_vals(k) = NaN;
analysis_results.ellipse_params{k} = nan(1,7);
analysis_results.boundary_coords{k} = [NaN, NaN];
continue;
end
%% ---- Boundary coordinates (edge detection inside ROI) ----
B = bwboundaries(BW);
boundary = B{1};
x_bound = dx_phys(boundary(:,2));
y_bound = dy_phys(boundary(:,1));
analysis_results.boundary_coords{k} = [x_bound, y_bound];
%% ---- Compute centroid and orientation (use PCA on boundary in ROI-local frame) ----
% Transform boundary points to ROI-local coordinates (same local coords as Xr,Yr)
Xc_b = x_bound - x0;
Yc_b = y_bound - y0;
Xr_b = cos(roi_theta)*Xc_b + sin(roi_theta)*Yc_b;
Yr_b = -sin(roi_theta)*Xc_b + cos(roi_theta)*Yc_b;
% PCA on boundary points in ROI-local frame (robust and simple)
boundary_mean = [mean(Xr_b), mean(Yr_b)];
XYb = [Xr_b - boundary_mean(1), Yr_b - boundary_mean(2)];
try
coeff = pca(XYb);
major = coeff(:,1);
angle_region = atan2(major(2), major(1)); % angle in ROI-local coords (radians)
catch
% fallback: use roi_theta as orientation if PCA fails
angle_region = 0;
end
% We'll define longitudinal axis in ROI-local coordinates as angle_region,
% transverse axis is angle_region + pi/2.
%% ---- Use points inside boundary for 1D profile (use ROI-local coords) ----
% We already have axis-aligned ROI samples x_roi, y_roi, z_roi in ROI-local coords.
xData_local = x_roi;
yData_local = y_roi;
zData = z_roi;
%% ---- Interpolate onto regular axis-aligned grid in ROI-local coords ----
Nx = ceil(w*10); Ny = ceil(h*10);
xq = linspace(-w/2, w/2, Nx);
yq = linspace(-h/2, h/2, Ny);
[Xq, Yq] = meshgrid(xq, yq);
Zq = griddata(xData_local, yData_local, zData, Xq, Yq, 'cubic');
% If griddata produced NaNs in large patches, fill with nearest to avoid empty bins
if any(isnan(Zq(:)))
Zq = fillmissing(Zq,'nearest');
end
% --- Build rotated coordinates so major axis aligns with new X'
theta = angle_region; % ROI-local angle of major axis
Rrot = [cos(theta), sin(theta); -sin(theta), cos(theta)]; % rotation that maps [x;y] -> [x';y'] where x' along major
XY = [Xq(:)'; Yq(:)'];
XY_rot = Rrot * XY;
Xrot = reshape(XY_rot(1,:), size(Xq));
Yrot = reshape(XY_rot(2,:), size(Yq));
% --- Compute 1D longitudinal profile by binning along X' (major axis)
nbins_long = size(Xq,1); % choose number of bins ~ Ny (vertical resolution)
x_edges_long = linspace(min(Xrot(:)), max(Xrot(:)), nbins_long+1);
x_centers_long = 0.5*(x_edges_long(1:end-1)+x_edges_long(2:end));
prof_long = nan(1, nbins_long);
for ii = 1:nbins_long
mask_bin = Xrot >= x_edges_long(ii) & Xrot < x_edges_long(ii+1);
vals = Zq(mask_bin);
if isempty(vals)
prof_long(ii) = NaN;
else
prof_long(ii) = mean(vals,'omitnan');
end
end
% --- Compute 1D transverse profile by binning along Y' (minor axis)
nbins_trans = size(Xq,2);
y_edges_trans = linspace(min(Yrot(:)), max(Yrot(:)), nbins_trans+1);
y_centers_trans = 0.5*(y_edges_trans(1:end-1)+y_edges_trans(2:end));
prof_trans = nan(1, nbins_trans);
for ii = 1:nbins_trans
mask_bin = Yrot >= y_edges_trans(ii) & Yrot < y_edges_trans(ii+1);
vals = Zq(mask_bin);
if isempty(vals)
prof_trans(ii) = NaN;
else
prof_trans(ii) = mean(vals,'omitnan');
end
end
% --- Prepare x-axis positions for profile centers (in ROI-local units)
xcent_long = x_centers_long; % positions along major axis (µm)
xcent_trans = y_centers_trans; % positions along minor axis (µm)
% --- Remove NaNs from profiles
valid_long = ~isnan(prof_long);
x_long = xcent_long(valid_long);
y_long = prof_long(valid_long);
valid_trans = ~isnan(prof_trans);
x_trans = xcent_trans(valid_trans);
y_trans = prof_trans(valid_trans);
% --- If not enough points, skip
if numel(x_long) < 8 || numel(x_trans) < 8
warning('Too few points for longitudinal/transverse fits for image %d', k);
analysis_results.peak_centroid{k} = [NaN, NaN];
analysis_results.anisotropy_vals(k) = NaN;
analysis_results.ellipse_params{k} = nan(1,7);
continue;
end
%% ---- Fit two-Gaussian to longitudinal profile (use your working 1D fit) ----
twoGauss1D = @(p,x) ...
p(1) * exp(-0.5*((x - p(2))/max(p(3),1e-6)).^2) + ... % Gaussian 1
p(4) * exp(-0.5*((x - p(5))/max(p(6),1e-6)).^2) + ... % Gaussian 2
p(7); % offset
% initial guesses (longitudinal)
[~, mainIdxL] = max(y_long);
A1_guessL = y_long(mainIdxL);
mu1_guessL = x_long(mainIdxL);
sigma1_guessL = range(x_long)/10;
A2_guessL = 0.8 * A1_guessL;
mu2_guessL = mu1_guessL + range(x_long)/5;
sigma2_guessL = sigma1_guessL;
offset_guessL = min(y_long);
p0L = [A1_guessL, mu1_guessL, sigma1_guessL, A2_guessL, mu2_guessL, sigma2_guessL, offset_guessL];
lbL = [0, min(x_long), 1e-6, 0, min(x_long), 1e-6, -Inf];
ubL = [2, max(x_long), range(x_long), 2, max(x_long), range(x_long), Inf];
opts_lsq = optimoptions('lsqcurvefit','Display','off','MaxFunctionEvaluations',1e4,'MaxIterations',1e4);
try
pFitL = lsqcurvefit(twoGauss1D, p0L, x_long, y_long, lbL, ubL, opts_lsq);
catch
warning('Longitudinal 1D fit failed for image %d, using initial guess.', k);
pFitL = p0L;
end
%% ---- Fit two-Gaussian to transverse profile (use same logic) ----
[~, mainIdxT] = max(y_trans);
A1_guessT = y_trans(mainIdxT);
mu1_guessT = x_trans(mainIdxT);
sigma1_guessT = range(x_trans)/10;
A2_guessT = 0.8 * A1_guessT;
mu2_guessT = mu1_guessT + range(x_trans)/5;
sigma2_guessT = sigma1_guessT;
offset_guessT = min(y_trans);
p0T = [A1_guessT, mu1_guessT, sigma1_guessT, A2_guessT, mu2_guessT, sigma2_guessT, offset_guessT];
lbT = [0, min(x_trans), 1e-6, 0, min(x_trans), 1e-6, -Inf];
ubT = [2, max(x_trans), range(x_trans), 2, max(x_trans), range(x_trans), Inf];
try
pFitT = lsqcurvefit(twoGauss1D, p0T, x_trans, y_trans, lbT, ubT, opts_lsq);
catch
warning('Transverse 1D fit failed for image %d, using initial guess.', k);
pFitT = p0T;
end
%% ---- Determine anisotropy from the two fits (ratio of widths) ----
sigma_long = mean([pFitL(3), pFitL(6)]);
sigma_trans = mean([pFitT(3), pFitT(6)]);
anisotropy = sigma_trans / sigma_long; % transverse / longitudinal (ratio)
% store centroid roughly as ROI-local centroid rotated back to lab coords
centroid_local_roi = boundary_mean'; % mean of boundary in ROI-local coords
% rotate back to lab frame
rotBack = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
centroid_lab = rotBack * centroid_local_roi + [x0; y0];
centroid_lab = centroid_lab(:)';
analysis_results.peak_centroid{k} = centroid_lab;
analysis_results.anisotropy_vals(k) = anisotropy;
analysis_results.ellipse_params{k} = [pFitL, pFitT, angle_region];
%% ---- Visualization ----
if ~opts.skipLivePlot
fig = figure(100); clf;
set(fig,'Color','w','Position',[100 100 1600 450]);
tiledlayout(1,3,'Padding','compact','TileSpacing','compact');
% --- Panel 1: 2D g² map + ROI + boundary
nexttile;
imagesc(dx_phys, dy_phys, g2_matrix);
axis image; set(gca,'YDir','normal'); colormap(Colormaps.coolwarm()); colorbar; hold on;
% ROI bounding box (red dashed)
rect = [-w/2, -h/2; w/2, -h/2; w/2, h/2; -w/2, h/2; -w/2, -h/2];
R = [cos(roi_theta), -sin(roi_theta); sin(roi_theta), cos(roi_theta)];
rect_rot = (R * rect')';
plot(rect_rot(:,1)+x0, rect_rot(:,2)+y0, 'r--', 'LineWidth',2);
% Detected peak boundary (green solid)
if ~isempty(x_bound)
plot(x_bound, y_bound, 'g-', 'LineWidth',2);
end
xlabel('\Deltax (\mum)'); ylabel('\Deltay (\mum)');
title(sprintf('Image %d | 2D g^2', k));
% --- Panel 2: Longitudinal 1D profile and fit
nexttile;
plot(x_long, y_long, 'k.-', 'DisplayName','Data'); hold on;
xFine = linspace(min(x_long), max(x_long), 500);
yFitL = twoGauss1D(pFitL, xFine);
plot(xFine, yFitL, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
title('Longitudinal profile','FontName',opts.font);
xlabel('x_{longitudinal} (\mum)');
ylabel('g^2');
legend('Location','northeast'); grid on;
% --- Panel 3: Transverse 1D profile and fit
nexttile;
plot(x_trans, y_trans, 'b.-', 'DisplayName','Data'); hold on;
xFineT = linspace(min(x_trans), max(x_trans), 500);
yFitT = twoGauss1D(pFitT, xFineT);
plot(xFineT, yFitT, 'r-', 'LineWidth',1.5,'DisplayName','Fit');
title('Transverse profile','FontName',opts.font);
xlabel('x_{transverse} (\mum)');
ylabel('g^2');
legend('Location','northeast'); grid on;
drawnow;
end
end
end