299 lines
12 KiB
Matlab
299 lines
12 KiB
Matlab
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
|