Added feature detection routines, functional only through GUI currently.
This commit is contained in:
parent
a7c0be507c
commit
e08e0284fe
134
Data-Analyzer/+Analyzer/detectPatches.m
Normal file
134
Data-Analyzer/+Analyzer/detectPatches.m
Normal file
@ -0,0 +1,134 @@
|
||||
function [patchProps, patchCentroidsGlobal, imgCropped, xStart, yStart] = detectPatches(img, params)
|
||||
% detectPatches
|
||||
% Detect lattice patches (blobs/stripes) in a single OD image.
|
||||
% Performs background subtraction, cloud segmentation, cropping,
|
||||
% denoising, Difference-of-Gaussians filtering, patch detection, and plotting.
|
||||
%
|
||||
% INPUTS:
|
||||
% img - 2D OD image (double or converted internally)
|
||||
% params - struct of user-tunable parameters:
|
||||
% backgroundDiskFraction - fraction of image size for morphological opening
|
||||
% boundingBoxPadding - pixels of padding around cloud bounding box
|
||||
% dogGaussianSmallSigma - sigma for small Gaussian in DoG
|
||||
% dogGaussianLargeSigma - sigma for large Gaussian in DoG
|
||||
% minPeakProminence - min DoG response for thresholding
|
||||
% minPeakFraction - fraction of max DoG response for adaptive threshold
|
||||
% subpixelWindowRadius - radius for potential subpixel refinement (not used here)
|
||||
% minimumPatchArea - minimum patch area to keep
|
||||
% pixelSize - meters/pixel
|
||||
% magnification - imaging system magnification
|
||||
% hAx - axes handle for plotting
|
||||
%
|
||||
% OUTPUTS:
|
||||
% patchProps - struct array of detected patches
|
||||
% patchCentroidsGlobal - Nx2 array of patch centroids in image coordinates
|
||||
% imgCropped - cropped, background-subtracted image used for detection
|
||||
% xAxis, yAxis - physical axes in microns
|
||||
|
||||
if ~isa(img,'double')
|
||||
img = im2double(img);
|
||||
end
|
||||
[Ny, Nx] = size(img);
|
||||
|
||||
%% --- Step 1: Background subtraction & cloud mask ---
|
||||
% Morphological opening estimates slowly-varying background
|
||||
seRadius = max(3, round(min(size(img)) * params.backgroundDiskFraction));
|
||||
backgroundEstimate = imopen(img, strel('disk', seRadius));
|
||||
|
||||
% Subtract background and clamp negatives to zero
|
||||
imgCorrected = img - backgroundEstimate;
|
||||
imgCorrected(imgCorrected < 0) = 0;
|
||||
|
||||
%% --- Step 2: Cloud segmentation ---
|
||||
% Smooth image to remove high-frequency noise
|
||||
imgSmoothed = imgaussfilt(imgCorrected, round(min(size(img))/25));
|
||||
|
||||
% Threshold using Otsu to create binary cloud mask
|
||||
cloudMask = imbinarize(imgSmoothed, graythresh(imgSmoothed));
|
||||
|
||||
% Close small gaps and fill holes
|
||||
cloudMask = imclose(cloudMask, strel('disk', round(seRadius/4)));
|
||||
cloudMask = imfill(cloudMask,'holes');
|
||||
|
||||
%% --- Step 3: Largest connected region & crop ---
|
||||
CC = bwconncomp(cloudMask);
|
||||
if CC.NumObjects > 0
|
||||
stats = regionprops(CC,'Area','BoundingBox');
|
||||
[~, idxMax] = max([stats.Area]);
|
||||
bb = round(stats(idxMax).BoundingBox);
|
||||
|
||||
% Crop with padding, stay within image
|
||||
xStart = max(1, bb(1)-params.boundingBoxPadding);
|
||||
yStart = max(1, bb(2)-params.boundingBoxPadding);
|
||||
xEnd = min(Nx, bb(1)+bb(3)-1 + params.boundingBoxPadding);
|
||||
yEnd = min(Ny, bb(2)+bb(4)-1 + params.boundingBoxPadding);
|
||||
else
|
||||
% Fallback: full image
|
||||
xStart=1; yStart=1; xEnd=Nx; yEnd=Ny;
|
||||
end
|
||||
imgCropped = imgCorrected(yStart:yEnd, xStart:xEnd);
|
||||
|
||||
%% --- Step 4: Denoising ---
|
||||
% Light Gaussian blur to reduce high-frequency noise
|
||||
imgDenoised = imgaussfilt(imgCropped, 0.8);
|
||||
|
||||
%% --- Step 5: Detect lattice patches ---
|
||||
opts = struct('sigmaSmall', params.dogGaussianSmallSigma, ...
|
||||
'sigmaLarge', params.dogGaussianLargeSigma, ...
|
||||
'minPeakProminence', params.minPeakProminence, ...
|
||||
'minPatchArea', params.minimumPatchArea, ...
|
||||
'minPeakFraction', params.minPeakFraction);
|
||||
|
||||
patchProps = detectPatchesCore(imgDenoised, opts);
|
||||
|
||||
if ~isempty(patchProps)
|
||||
patchCentroidsLocal = cat(1, patchProps.Centroid);
|
||||
patchCentroidsGlobal = patchCentroidsLocal + [xStart-1, yStart-1];
|
||||
else
|
||||
patchCentroidsGlobal = [];
|
||||
end
|
||||
end
|
||||
|
||||
%% --- Helper function ---
|
||||
function patchProps = detectPatchesCore(I, opts)
|
||||
% Detect lattice patches (blobs/stripes) using Difference-of-Gaussians,
|
||||
% thresholding, and connected component analysis.
|
||||
% Returns a struct array with fields:
|
||||
% Centroid, Area, Orientation, MajorAxisLength, MinorAxisLength
|
||||
|
||||
% Step 1: Difference-of-Gaussians
|
||||
G1 = imgaussfilt(I, opts.sigmaSmall);
|
||||
G2 = imgaussfilt(I, opts.sigmaLarge);
|
||||
dogResponse = mat2gray(G1 - G2);
|
||||
|
||||
% Step 2: Threshold
|
||||
th = max(graythresh(dogResponse), opts.minPeakProminence);
|
||||
binaryMask = dogResponse > th;
|
||||
|
||||
% Step 3: Remove small specks, close small gaps
|
||||
binaryMask = bwareaopen(binaryMask, 5);
|
||||
binaryMask = imclose(binaryMask, strel('disk',1));
|
||||
|
||||
% Step 4: Connected components
|
||||
CC = bwconncomp(binaryMask);
|
||||
if CC.NumObjects == 0
|
||||
patchProps = struct('Centroid',[],'Area',[],'Orientation',[], ...
|
||||
'MajorAxisLength',[],'MinorAxisLength',[]);
|
||||
return;
|
||||
end
|
||||
|
||||
% Step 5: Extract patch properties
|
||||
patchPropsAll = regionprops(CC, dogResponse, 'Centroid','Area','Orientation','MajorAxisLength','MinorAxisLength');
|
||||
|
||||
% Step 6: Filter by minimum area and adaptive per-patch peak fraction
|
||||
maxDog = max(dogResponse(:)); % Global maximum DoG response
|
||||
keepIdx = false(1,numel(patchPropsAll));
|
||||
for k = 1:numel(patchPropsAll)
|
||||
patchPixels = CC.PixelIdxList{k};
|
||||
patchMax = max(dogResponse(patchPixels)); % Peak DoG in this patch
|
||||
if patchPropsAll(k).Area >= opts.minPatchArea && patchMax >= opts.minPeakFraction*maxDog
|
||||
keepIdx(k) = true;
|
||||
end
|
||||
end
|
||||
patchProps = patchPropsAll(keepIdx);
|
||||
end
|
||||
110
Data-Analyzer/+Analyzer/extractAndClassifyShapes.m
Normal file
110
Data-Analyzer/+Analyzer/extractAndClassifyShapes.m
Normal file
@ -0,0 +1,110 @@
|
||||
function results = extractAndClassifyShapes(imgCropped, patchProps, xStart, yStart, params)
|
||||
% extractAndClassifyShapes
|
||||
% Extracts internal shapes inside DoG-detected patches using intensity
|
||||
% thresholding, optional multi-scale DoG fusion, and Canny edge detection.
|
||||
% This function is robust to irregular shapes, filaments, and Y-shaped structures.
|
||||
%
|
||||
% INPUTS:
|
||||
% imgCropped - cropped image (DoG input)
|
||||
% patchProps - struct array from detectPatchesDoG containing patch info
|
||||
% xStart, yStart- offsets of the cropped region relative to full image
|
||||
% params - struct with the following fields:
|
||||
% shapeMinArea - minimum area of shapes in pixels
|
||||
% shapeCloseRadius - radius for morphological closing
|
||||
% shapeFillHoles - boolean; fill internal holes in shapes
|
||||
% shapeSkeletonize - boolean; compute skeletons of shapes
|
||||
% intensityThreshFraction - fraction of max intensity to threshold
|
||||
% edgeSigma - Gaussian smoothing sigma for Canny
|
||||
% edgeThresholdLow - low threshold fraction for Canny
|
||||
% edgeThresholdHigh - high threshold fraction for Canny
|
||||
%
|
||||
% OUTPUTS:
|
||||
% results - struct array with fields:
|
||||
% BW - binary mask of detected shapes (local to cropped ROI)
|
||||
% labels - labeled mask of shapes
|
||||
% props - regionprops with classification
|
||||
% skeletons - skeletons of shapes (if enabled)
|
||||
% boundaries - global boundaries in full image coordinates
|
||||
|
||||
%% --- Return empty if no patches detected ---
|
||||
if isempty(patchProps)
|
||||
results = struct('BW',{},'labels',{},'props',{},'skeletons',{},'boundaries',{});
|
||||
return;
|
||||
end
|
||||
|
||||
%% --- Preallocate result structure ---
|
||||
results = repmat(struct('BW',[],'labels',[],'props',[],'skeletons',[],'boundaries',[]), numel(patchProps), 1);
|
||||
|
||||
%% --- Loop over each detected patch ---
|
||||
for k = 1:numel(patchProps)
|
||||
|
||||
% --- Create local patch mask ---
|
||||
% Construct an elliptical mask corresponding to the detected patch
|
||||
mask = false(size(imgCropped));
|
||||
cx = patchProps(k).Centroid(1); % patch center x
|
||||
cy = patchProps(k).Centroid(2); % patch center y
|
||||
a = patchProps(k).MajorAxisLength/2; % semi-major axis
|
||||
b = patchProps(k).MinorAxisLength/2; % semi-minor axis
|
||||
phi = deg2rad(-patchProps(k).Orientation); % rotation angle in radians
|
||||
|
||||
[xx,yy] = meshgrid(1:size(imgCropped,2), 1:size(imgCropped,1));
|
||||
Xr = (xx-cx)*cos(phi) + (yy-cy)*sin(phi);
|
||||
Yr = -(xx-cx)*sin(phi) + (yy-cy)*cos(phi);
|
||||
mask((Xr/a).^2 + (Yr/b).^2 <= 1) = true; % points inside ellipse
|
||||
|
||||
% --- Apply mask to ROI ---
|
||||
% Only consider pixels inside patch ellipse
|
||||
roi = imgCropped;
|
||||
roi(~mask) = 0;
|
||||
|
||||
% --- Multi-scale Canny edge detection ---
|
||||
% Detect edges to help capture fine structure, filaments, and Y-shaped arms
|
||||
edges = edge(roi, 'Canny', [params.edgeThresholdLow, params.edgeThresholdHigh], params.edgeSigma);
|
||||
|
||||
% --- Intensity thresholding ---
|
||||
% Retain pixels above a fraction of the maximum intensity
|
||||
intMask = roi > (params.intensityThreshFraction * max(roi(:)));
|
||||
|
||||
% Only edges + intensity
|
||||
BW = edges | intMask;
|
||||
|
||||
% --- Morphological cleanup ---
|
||||
% Remove tiny fragments, close small gaps, optionally fill holes
|
||||
BW = bwareaopen(BW, params.shapeMinArea); % remove small objects
|
||||
BW = imclose(BW, strel('disk', max(1, params.shapeCloseRadius))); % smooth edges
|
||||
if params.shapeFillHoles
|
||||
BW = imfill(BW,'holes'); % fill internal holes
|
||||
end
|
||||
|
||||
% --- Label and measure region properties ---
|
||||
labels = bwlabel(BW); % assign unique labels to each connected shape
|
||||
props = regionprops(labels, 'Area','Eccentricity','Solidity','Centroid','Perimeter');
|
||||
for n = 1:numel(props)
|
||||
% Classify shape based on morphology
|
||||
if props(n).Eccentricity < 0.61
|
||||
props(n).Class = "circular/elliptical";
|
||||
elseif props(n).Solidity < 0.7
|
||||
props(n).Class = "Y-shaped/branched";
|
||||
else
|
||||
props(n).Class = "filament-like";
|
||||
end
|
||||
end
|
||||
|
||||
% --- Extract global boundaries ---
|
||||
boundaries = bwboundaries(BW);
|
||||
boundariesGlobal = cell(size(boundaries));
|
||||
for n = 1:numel(boundaries)
|
||||
b = boundaries{n};
|
||||
% Convert from local crop coordinates to full image coordinates
|
||||
b(:,1) = b(:,1) + yStart - 1;
|
||||
b(:,2) = b(:,2) + xStart - 1;
|
||||
boundariesGlobal{n} = b;
|
||||
end
|
||||
|
||||
% --- Store results ---
|
||||
results(k).BW = BW;
|
||||
results(k).labels = labels;
|
||||
results(k).props = props;
|
||||
results(k).boundaries = boundariesGlobal;
|
||||
end
|
||||
end
|
||||
253
Data-Analyzer/+Analyzer/runInteractiveFeatureDetectorGUI.m
Normal file
253
Data-Analyzer/+Analyzer/runInteractiveFeatureDetectorGUI.m
Normal file
@ -0,0 +1,253 @@
|
||||
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
|
||||
hFig = figure('Name','Interactive Feature Detector',...
|
||||
'NumberTitle','off','Position',[100 100 1275 800],...
|
||||
'KeyPressFcn',@keyPressCallback);
|
||||
|
||||
%% --- 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
|
||||
120
Data-Analyzer/+Scripts/BECToDropletsToStripes/featureDetection.m
Normal file
120
Data-Analyzer/+Scripts/BECToDropletsToStripes/featureDetection.m
Normal file
@ -0,0 +1,120 @@
|
||||
%% ===== BEC-Droplets-Stripes Settings =====
|
||||
|
||||
% Specify data location to run analysis on
|
||||
dataSources = {
|
||||
struct('sequence', 'TwoDGas', ...
|
||||
'date', '2025/06/23', ...
|
||||
'runs', [0]) % specify run numbers as a string in "" or just as a numeric value
|
||||
};
|
||||
|
||||
options = struct();
|
||||
|
||||
% File paths
|
||||
options.baseDataFolder = '//DyLabNAS/Data';
|
||||
options.FullODImagesFolder = 'E:/Data - Experiment/FullODImages/202506';
|
||||
options.measurementName = 'DropletsToStripes';
|
||||
scriptFullPath = mfilename('fullpath');
|
||||
options.saveDirectory = fileparts(scriptFullPath);
|
||||
|
||||
% Camera / imaging settings
|
||||
options.cam = 4; % 1 - ODT_1_Axis_Camera; 2 - ODT_2_Axis_Camera; 3 - Horizontal_Axis_Camera;, 4 - Vertical_Axis_Camera;
|
||||
options.angle = 0; % angle by which image will be rotated
|
||||
options.center = [1410, 2030];
|
||||
options.span = [200, 200];
|
||||
options.fraction = [0.1, 0.1];
|
||||
options.pixel_size = 5.86e-6; % in meters
|
||||
options.magnification = 24.6;
|
||||
options.ImagingMode = 'HighIntensity';
|
||||
options.PulseDuration = 5e-6; % in s
|
||||
|
||||
% Fourier analysis settings
|
||||
options.theta_min = deg2rad(0);
|
||||
options.theta_max = deg2rad(180);
|
||||
options.N_radial_bins = 500;
|
||||
options.Radial_Sigma = 2;
|
||||
options.Radial_WindowSize = 5; % odd number
|
||||
|
||||
options.k_min = 1.2771; % μm⁻¹
|
||||
options.k_max = 2.5541; % μm⁻¹
|
||||
options.N_angular_bins = 180;
|
||||
options.Angular_Threshold = 75;
|
||||
options.Angular_Sigma = 2;
|
||||
options.Angular_WindowSize = 5;
|
||||
options.zoom_size = 50;
|
||||
|
||||
% Flags
|
||||
options.skipUnshuffling = false;
|
||||
options.skipNormalization = false;
|
||||
|
||||
options.skipFringeRemoval = true;
|
||||
options.skipPreprocessing = true;
|
||||
options.skipMasking = true;
|
||||
options.skipIntensityThresholding = true;
|
||||
options.skipBinarization = true;
|
||||
|
||||
options.skipFullODImagesFolderUse = false;
|
||||
options.skipSaveData = false;
|
||||
options.skipSaveFigures = true;
|
||||
options.skipSaveProcessedOD = true;
|
||||
options.skipLivePlot = true;
|
||||
options.showProgressBar = true;
|
||||
|
||||
% Extras
|
||||
options.font = 'Bahnschrift';
|
||||
switch options.measurementName
|
||||
case 'BECToDroplets'
|
||||
options.scan_parameter = 'rot_mag_field';
|
||||
options.flipSortOrder = true;
|
||||
options.scanParameterUnits = 'gauss';
|
||||
options.titleString = 'BEC to Droplets';
|
||||
case 'BECToStripes'
|
||||
options.scan_parameter = 'rot_mag_field';
|
||||
options.flipSortOrder = true;
|
||||
options.scanParameterUnits = 'gauss';
|
||||
options.titleString = 'BEC to Stripes';
|
||||
case 'DropletsToStripes'
|
||||
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
|
||||
options.flipSortOrder = false;
|
||||
options.scanParameterUnits = 'degrees';
|
||||
options.titleString = 'Droplets to Stripes';
|
||||
case 'StripesToDroplets'
|
||||
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
|
||||
options.flipSortOrder = false;
|
||||
options.scanParameterUnits = 'degrees';
|
||||
options.titleString = 'Stripes to Droplets';
|
||||
end
|
||||
|
||||
%% ===== Collect Images and Launch Viewer =====
|
||||
|
||||
[options.selectedPath, options.folderPath] = Helper.selectDataSourcePath(dataSources, options);
|
||||
|
||||
[od_imgs, scan_parameter_values, scan_reference_values, file_list] = Helper.collectODImages(options);
|
||||
|
||||
%% Minimal pipeline: preprocess + detect patches + extract shapes
|
||||
|
||||
% ---------- user-tunable parameters ----------
|
||||
params.backgroundDiskFraction = 1/8; % Fraction of image size used for morphological opening
|
||||
params.boundingBoxPadding = 12; % Padding around detected cloud
|
||||
params.dogGaussianSmallSigma = 1.2; % Sigma for small Gaussian in Difference-of-Gaussians
|
||||
params.dogGaussianLargeSigma = 3.8; % Sigma for large Gaussian in Difference-of-Gaussians
|
||||
params.minPeakProminence = 0.02; % Threshold for DoG response
|
||||
params.minPeakFraction = 0.80; % Fraction of max DoG response to reject weak patches
|
||||
params.minimumPatchArea = 100; % Minimum area (pixels) for detected patches to be kept
|
||||
|
||||
params.shapeMinArea = 20; % Minimum shape area
|
||||
params.shapeCloseRadius = 3; % Morphological closing radius (fills holes)
|
||||
params.shapeFillHoles = false; % Ensures shapes are solid regions
|
||||
params.intensityThreshFraction = 0.45; % Fraction of max intensity to keep
|
||||
params.edgeSigma = 1.0; % Gaussian smoothing for Canny
|
||||
params.edgeThresholdLow = 0.25; % Low Canny threshold fraction
|
||||
params.edgeThresholdHigh = 0.55; % High Canny threshold fraction
|
||||
|
||||
params.pixelSize = 5.86e-6; % Physical pixel size (meters/pixel)
|
||||
params.magnification = 23.94; % Magnification factor of imaging system
|
||||
% --------------------------------------------
|
||||
|
||||
Analyzer.runInteractiveFeatureDetectorGUI(od_imgs, scan_parameter_values, file_list, options, params)
|
||||
|
||||
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ switch options.measurementName
|
||||
options.titleString = 'BEC to Stripes';
|
||||
case 'DropletsToStripes'
|
||||
options.scan_parameter = 'ps_rot_mag_fin_pol_angle';
|
||||
options.flipSortOrder = true;
|
||||
options.flipSortOrder = false;
|
||||
options.scanParameterUnits = 'degrees';
|
||||
options.titleString = 'Droplets to Stripes';
|
||||
case 'StripesToDroplets'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user