144 lines
5.9 KiB
Matlab
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
|