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

266 lines
11 KiB
Matlab

function runInteractiveFeatureDetectorGUI(od_imgs, scan_parameter_values, file_list, options, params)
% Interactive OD image viewer with patch detection
% Supports slider, arrow keys, editable parameters, and displays scan parameter
numImages = numel(od_imgs);
currentFrame = 1;
%% --- Create figure ---
% Try to find an existing figure by a unique tag
hFig = findobj('Type','figure','Tag','InteractiveFeatureDetector');
if isempty(hFig)
% If not found, create a new figure
hFig = figure('Name','OD Image Feature Detector', ...
'NumberTitle','off', ...
'Position',[100 100 1275 800], ...
'KeyPressFcn',@keyPressCallback, ...
'Tag','InteractiveFeatureDetector'); % <-- unique tag
else
% If figure exists, bring it to front
figure(hFig);
clf;
end
%% --- Axes ---
hAx = axes('Parent',hFig,'Position',[0.025 0.1 0.6 0.85]);
%% --- Frame slider ---
hSlider = uicontrol('Style','slider','Min',1,'Max',numImages,'Value',1,...
'SliderStep',[1/(numImages-1),10/(numImages-1)],...
'Units','normalized','Position',[0.075 0.01 0.5 0.025],...
'Callback',@(~,~) updateFrame());
%% --- Parameter names ---
paramNames = {'backgroundDiskFraction',...
'boundingBoxPadding',...
'dogGaussianSmallSigma',...
'dogGaussianLargeSigma',...
'minPeakProminence',...
'minPeakFraction',...
'minimumPatchArea',...
'shapeMinArea', ...
'shapeCloseRadius', ...
'shapeFillHoles', ...
'intensityThreshFraction', ...
'edgeSigma', ...
'edgeThresholdLow', ...
'edgeThresholdHigh', ...
'pixelSize', ...
'magnification'};
%% --- Parameter display names ---
displayParamNames = {'Background Disk Fraction',...
'Bounding Box Padding (px)',...
'DoG Small Kernel Sigma',...
'DoG Large Kernel Sigma',...
'Minimum Peak Prominence',...
'Minimum Peak Fraction',...
'Minimum Patch Area (px)',...
'Minimum Shape Area (px)', ...
'Morphological Closing Radius (px)', ...
'Fill Internal Holes (0=false,1=true)', ...
'Intensity Threshold Fraction', ...
'Canny Gaussian Smoothing Sigma', ...
'Canny Low Threshold Fraction', ...
'Canny High Threshold Fraction', ...
'Pixel Size (m/px)', ...
'Imaging System Magnification'};
%% --- Parameter explanations ---
paramDescriptions = {'Fraction of image used for background disk in morphological opening', ...
'Number of pixels to pad around detected cloud bounding box', ...
'Sigma of small Gaussian in Difference-of-Gaussians (DoG)', ...
'Sigma of large Gaussian in DoG', ...
'Minimum prominence for DoG peak detection', ...
'Minimum fraction of max DoG response for adaptive thresholding', ...
'Minimum area (pixels) for detected patches', ...
'Minimum area (pixels) for internal shapes within patches', ...
'Radius (pixels) used for morphological closing to smooth shapes', ...
'Fill internal holes to ensure solid shapes (true/false)', ...
'Fraction of max intensity used to threshold features inside patches', ...
'Gaussian smoothing sigma used in Canny edge detection', ...
'Low threshold fraction for Canny edge detector', ...
'High threshold fraction for Canny edge detector', ...
'Physical size of one pixel (meters per pixel)', ...
'Magnification factor of imaging system'};
nParams = numel(paramNames);
hEdit = gobjects(nParams,1);
for i = 1:nParams
yTop = 0.9 - 0.05*(i-1);
% Parameter name
uicontrol('Style','text','Units','normalized',...
'Position',[0.615 yTop 0.2 0.04],...
'String',displayParamNames{i},'HorizontalAlignment','left', 'FontSize', 10, 'FontWeight','bold');
% Parameter value edit box
hEdit(i) = uicontrol('Style','edit','Units','normalized',...
'Position',[0.875 yTop 0.1 0.04],...
'String',num2str(params.(paramNames{i})),...
'Callback',@(src,~) applyParams());
% Explanation text below the edit box
uicontrol('Style','text','Units','normalized',...
'Position',[0.615 yTop - 0.01 0.35 0.03],...
'String',paramDescriptions{i},...
'HorizontalAlignment','left',...
'FontSize',8,'ForegroundColor',[0.4 0.4 0.4], ...
'BackgroundColor','none');
end
% Apply button
uicontrol('Style','pushbutton','Units','normalized',...
'Position',[0.7 yTop - 0.075 0.2 0.05],...
'String','Apply',...
'BackgroundColor',[0.2 0.6 1],... % RGB values between 0 and 1
'ForegroundColor','white',... % Text color
'FontSize',14, ...
'FontWeight','bold',...
'Callback',@(~,~) applyParams());
% Initial plot
updateFrame();
%% --- Nested functions ---
function applyParams()
% Update params struct from textboxes
for j = 1:nParams
val = str2double(hEdit(j).String);
if ~isnan(val)
params.(paramNames{j}) = val;
end
end
updateFrame();
end
function updateFrame()
% Get current image
idxImg = round(get(hSlider,'Value'));
img = od_imgs{idxImg}; % numeric image
[Ny, Nx] = size(img);
dx = params.pixelSize / params.magnification;
dy = dx;
xAxis = ((1:Nx)-(Nx+1)/2) * dx * 1e6; % μm
yAxis = ((1:Ny)-(Ny+1)/2) * dy * 1e6; % μm
% Step 1: DoG detection
[patchProps, patchCentroidsGlobal, imgCropped, xStart, yStart] = Analyzer.detectPatches(img, params);
results = Analyzer.extractAndClassifyShapes(imgCropped, patchProps, xStart, yStart, params);
%% Plotting in physical units (μm) ---
cla(hAx);
imagesc(hAx, xAxis, yAxis, img);
axis(hAx,'equal','tight'); colormap(hAx, Colormaps.inferno());
set(hAx,'FontSize',14,'YDir','normal');
xlabel(hAx,'x (\mum)','FontSize',14); ylabel(hAx,'y (\mum)','FontSize',14);
hold(hAx,'on');
% Draw diagonal overlays
Helper.drawODOverlays(xAxis(1), yAxis(1), xAxis(end), yAxis(end));
Helper.drawODOverlays(xAxis(end), yAxis(1), xAxis(1), yAxis(end));
% Plot patch centroids
if ~isempty(patchCentroidsGlobal)
patchCentroidsGlobal_um = [(patchCentroidsGlobal(:,1)-(Nx+1)/2)*dx*1e6, ...
(patchCentroidsGlobal(:,2)-(Ny+1)/2)*dy*1e6];
plot(hAx, patchCentroidsGlobal_um(:,1), patchCentroidsGlobal_um(:,2), 'ro','MarkerSize',4,'LineWidth',1);
end
% Plot patch ellipses
for k = 1:numel(patchProps)
a = patchProps(k).MajorAxisLength/2 * dx * 1e6;
b = patchProps(k).MinorAxisLength/2 * dy * 1e6;
phi = deg2rad(-patchProps(k).Orientation);
theta = linspace(0,2*pi,100);
R = [cos(phi) -sin(phi); sin(phi) cos(phi)];
ellipseXY = [a*cos(theta(:)) b*sin(theta(:))]*R';
cx_um = ((patchProps(k).Centroid(1)+xStart-1)-(Nx+1)/2)*dx*1e6;
cy_um = ((patchProps(k).Centroid(2)+yStart-1)-(Ny+1)/2)*dy*1e6;
ellipseXY(:,1) = ellipseXY(:,1) + cx_um;
ellipseXY(:,2) = ellipseXY(:,2) + cy_um;
plot(hAx, ellipseXY(:,1), ellipseXY(:,2),'g-','LineWidth',1);
end
% Overlay shapes
for k = 1:numel(results)
for n = 1:numel(results(k).boundaries)
bnd = results(k).boundaries{n};
bx = ((bnd(:,2) - (Nx+1)/2)) * dx * 1e6;
by = ((bnd(:,1) - (Ny+1)/2)) * dy * 1e6;
plot(hAx, bx, by, 'c-', 'LineWidth', 1.5);
end
end
hold(hAx,'off');
% Extract only filename (without path)
[~, fname, ext] = fileparts(file_list{idxImg});
shortName = [fname, ext];
% Update figure title with shot + filename
hFig.Name = sprintf('Shot %d | %s', idxImg, shortName);
% Update figure title with image index and patch count
title(hAx, sprintf('Image %d/%d: Detected %d patches', ...
idxImg, numImages, numel(patchProps)), ...
'FontSize',16,'FontWeight','bold');
% Text handle for scan parameter display
txtHandle = text(hAx, 0.975, 0.975, '', ...
'Color', 'white', 'FontWeight', 'bold', ...
'FontSize', 24, 'Interpreter', 'tex', ...
'Units', 'normalized', ...
'HorizontalAlignment', 'right', ...
'VerticalAlignment', 'top');
% --- Display scan parameter ---
if ~isempty(scan_parameter_values)
val = scan_parameter_values(idxImg);
% Determine units
switch lower(options.scanParameterUnits)
case {'degrees','deg','°'}
unitStr = '^\circ';
interpStr = 'tex';
case {'gauss','g'}
unitStr = ' G';
interpStr = 'none';
otherwise
unitStr = options.scanParameterUnits;
interpStr = 'none';
end
txtHandle.String = sprintf('%g%s', val, unitStr);
txtHandle.Interpreter = interpStr;
end
% Update slider value
hSlider.Value = idxImg;
drawnow;
end
function keyPressCallback(event)
switch event.Key
case 'rightarrow'
if currentFrame<numImages
currentFrame=currentFrame+1;
set(hSlider,'Value',currentFrame);
updateFrame();
end
case 'leftarrow'
if currentFrame>1
currentFrame=currentFrame-1;
set(hSlider,'Value',currentFrame);
updateFrame();
end
end
end
end