306 lines
12 KiB
Matlab
306 lines
12 KiB
Matlab
function runInteractiveFeatureDetectorGUI(od_imgs, scan_parameter_values, file_list, options, params)
|
|
%% runInteractiveFeatureDetectorGUI
|
|
% Author: Karthik
|
|
% Date: 2025-09-12
|
|
% Version: 1.0
|
|
%
|
|
% Description:
|
|
% Interactive OD image viewer with patch detection
|
|
% Supports slider, arrow keys, editable parameters, and displays scan parameter
|
|
%
|
|
% Notes:
|
|
% Optional notes, references.
|
|
|
|
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',...
|
|
'adaptiveSensitivity', ... % .
|
|
'adaptiveNeighborhoodSize', ... % Larger → smoother masks, less sensitive to noise.
|
|
'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',...
|
|
'Adaptive threshold sensitivity',...
|
|
'Window size for adaptive threshold', ...
|
|
'Minimum Patch 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', ...
|
|
'Global threshold that accounts for spatial variation of intensity', ...
|
|
'Local window size over which threshold is computed', ...
|
|
'Minimum fraction of max DoG response for detected patches', ...
|
|
'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');
|
|
|
|
%% --- Generalized unit handling ---
|
|
% Extract parameter row for this shot
|
|
if iscell(scan_parameter_values)
|
|
% Multi-parameter scan stored as cell array of row vectors
|
|
param_row = scan_parameter_values{idxImg};
|
|
else
|
|
% Numeric vector / matrix
|
|
param_row = scan_parameter_values(idxImg,:);
|
|
end
|
|
|
|
% Wrap single unit string into a cell if needed
|
|
if ischar(options.scanParameterUnits) || isstring(options.scanParameterUnits)
|
|
unitList = {char(options.scanParameterUnits)};
|
|
else
|
|
unitList = options.scanParameterUnits; % assume cell array
|
|
end
|
|
|
|
% Ensure units list is long enough
|
|
if numel(unitList) < numel(param_row)
|
|
unitList(end+1:numel(param_row)) = {''}; % pad with empty units
|
|
end
|
|
|
|
% Build text lines for each parameter
|
|
txtLines = cell(1, numel(param_row));
|
|
for j = 1:numel(param_row)
|
|
[unitSuffix, txtInterpreter] = getUnitInfo(unitList{j});
|
|
txtLines{j} = sprintf('%.2f%s', param_row(j), unitSuffix);
|
|
end
|
|
|
|
% Join multiple parameters with newline
|
|
txtHandle.String = strjoin(txtLines, '\n');
|
|
txtHandle.Interpreter = txtInterpreter; % use last parameter's interpreter
|
|
|
|
% 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
|
|
|
|
%% --- Helper function ---
|
|
function [unitSuffix, txtInterpreter] = getUnitInfo(u)
|
|
switch lower(u)
|
|
case {'degrees','deg','°'}
|
|
unitSuffix = '^\circ';
|
|
txtInterpreter = 'tex';
|
|
case {'gauss','g'}
|
|
unitSuffix = ' G';
|
|
txtInterpreter = 'none';
|
|
otherwise
|
|
unitSuffix = '';
|
|
txtInterpreter = 'none';
|
|
end
|
|
end |