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

144 lines
5.9 KiB
Matlab

function [patchProps, patchCentroidsGlobal, imgCropped, xStart, yStart] = detectPatches(img, params)
%% detectPatches
% Author: Karthik
% Date: 2025-09-12
% Version: 1.0
%
% Description:
% 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
%
% Notes:
% Optional notes, references.
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, ...
'adaptiveSensitivity', params.adaptiveSensitivity, ...
'adaptiveNeighborhoodSize', params.adaptiveNeighborhoodSize, ...
'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: Adaptive threshold
T = adaptthresh(dogResponse, opts.adaptiveSensitivity, 'NeighborhoodSize', opts.adaptiveNeighborhoodSize, 'Statistic','gaussian');
binaryMask = imbinarize(dogResponse, T);
% 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