Added feature detection routines, functional only through GUI currently.

This commit is contained in:
Karthik 2025-09-11 15:07:33 +02:00
parent a7c0be507c
commit e08e0284fe
5 changed files with 618 additions and 1 deletions

View 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

View 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

View 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

View 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)

View File

@ -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'