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 currentFrame1 currentFrame=currentFrame-1; set(hSlider,'Value',currentFrame); updateFrame(); end end end end