From 7959824d420d70fbf0beba7b98a743fba9bbad97 Mon Sep 17 00:00:00 2001
From: Karl Hoffmann <karlhoff@mpi-cbg.de, karl@hoffmann-freiberg.de>
Date: Mon, 5 Sep 2022 13:22:56 +0200
Subject: [PATCH] Add scripts for orientation analysis

---
 .../AnalyseOpticalCupLamininOrientation.m     | 1015 +++++++++++++++++
 AnalyseOrientations/ComputeDirecty.m          |  336 ++++++
 AnalyseOrientations/ComputeDirectyGradient.m  |  118 ++
 .../ComputeDirecty_FourierAndQ.m              |  163 +++
 AnalyseOrientations/ComputeLocalAlignment.m   |   52 +
 AnalyseOrientations/ComputeQTensor.m          |   41 +
 .../LICENSE_distinguishable_colors            |   24 +
 AnalyseOrientations/LICENSE_tight_subplot     |   24 +
 AnalyseOrientations/conv2in2or3D.m            |   25 +
 AnalyseOrientations/distinguishable_colors.m  |  152 +++
 .../downSamplDirecty_ThetaStrength.m          |   74 ++
 AnalyseOrientations/loadGrayStack.m           |   23 +
 .../smoothDirecty_ThetaStrength.m             |   75 ++
 AnalyseOrientations/tight_subplot.m           |   66 ++
 LICENSE                                       |    2 +-
 README.md                                     |   13 +-
 16 files changed, 2200 insertions(+), 3 deletions(-)
 create mode 100644 AnalyseOrientations/AnalyseOpticalCupLamininOrientation.m
 create mode 100644 AnalyseOrientations/ComputeDirecty.m
 create mode 100644 AnalyseOrientations/ComputeDirectyGradient.m
 create mode 100644 AnalyseOrientations/ComputeDirecty_FourierAndQ.m
 create mode 100644 AnalyseOrientations/ComputeLocalAlignment.m
 create mode 100644 AnalyseOrientations/ComputeQTensor.m
 create mode 100644 AnalyseOrientations/LICENSE_distinguishable_colors
 create mode 100644 AnalyseOrientations/LICENSE_tight_subplot
 create mode 100644 AnalyseOrientations/conv2in2or3D.m
 create mode 100644 AnalyseOrientations/distinguishable_colors.m
 create mode 100644 AnalyseOrientations/downSamplDirecty_ThetaStrength.m
 create mode 100644 AnalyseOrientations/loadGrayStack.m
 create mode 100644 AnalyseOrientations/smoothDirecty_ThetaStrength.m
 create mode 100644 AnalyseOrientations/tight_subplot.m

diff --git a/AnalyseOrientations/AnalyseOpticalCupLamininOrientation.m b/AnalyseOrientations/AnalyseOpticalCupLamininOrientation.m
new file mode 100644
index 0000000..116a00e
--- /dev/null
+++ b/AnalyseOrientations/AnalyseOpticalCupLamininOrientation.m
@@ -0,0 +1,1015 @@
+% Analyse laminin orientations in projected data of zebrafish optical cup
+%  in data by Karen Soans, Caren Norden group, MPI-CBG
+% Note that the input to this script are derotated and max-projected ROIs 
+% from the original image stack, obtained within Fiji by the script
+% extractDerotatedSurface.py by Noreen Walker, MPI-CBG.
+% Here, orientations are inferred from small image patches, and the
+% alignment between orientations (or their coherency) is calculated locally
+% and globally across each region of interest.
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2022_09_05 
+%
+% developed using MATLAB R2019, R2020, and R2021
+%
+%%% usage %%%
+% It is assumed that regions of interest (ROIs) of the original image stack
+% were extracted, derotated by extractDerotatedSurface.py, and each saved 
+% into a folder structure 
+%   head_folder/
+%     <Stage>/
+%       <Embryo>/
+%         <RegionOfTheEye>/
+%           <DerotationConditions>/
+%             Roi<nn>_ZMAX_of_Resliced_of_Derotated_with2ndCrop.tif
+% [the pilot data had only <head_folder>/<Stage>/<Embryo>/<EyeAndChannel>/<ROI>/ ]
+% Then per-ROI observables are collected in the array  output_collected 
+% together with their relevant settings and conditions.
+%
+% Orientations are expressed as vector with coordinates (u,v) where
+% anti-podal vectors (u,v) and (-u,-v) represent the same orientation,
+% or as magnitude magn and angle theta (from the interval [0,pi]) with
+% (u,v) = +- magn * (cos theta, sin theta).
+
+
+% CHANGELOG
+% 2022_08_23 clean-up for publication
+% 2021_05_25 adapt plotting to downsampling
+% 2021_05_18 include downsampling (i) of the input grayscale image and 
+%     (ii) on the level of orientions.
+% 2021_03_26 smoothing now discouraged - replaced by downsampling (on
+%     grayscale or on orientations)
+% 2021_03_16 curate computation of theta_for_hists
+% 2021_03_12 switch to orientation estimation by the gradient method (with 
+%     gradient estimate by a Scharr operator), 
+%     introduced kernelSize as opposed to boxRadius
+% 2021_01_20 adapted to the structure of 2021 data 
+
+
+
+%% settings for orientation analysis
+% Scharr operator with kernel size 5 is default and highly recommended for
+% extremely low error in orientation estimation, see Tabelle B.11 in
+%
+%   Hanno Scharr (2000): Optimale Operatoren in der digitalen
+%   Bildverarbeitung, PhD Thesis at University Heidelberg, Germany,
+%   DOI=10.11588/heidok.00000962, http://www.ub.uni-heidelberg.de/archiv/962 
+%   
+% Alternative orientation estimation based on the tensor of inertia (tensor
+% of second moments of image intensities) and on the Fourier transform are
+% included for completeness
+method = 'gradient'; %one of "fourier", "inertia", "gradient"
+kernelSize = 5;            %only relevant for method="gradient" - allowed values are 5 (recommended), 3 and 2
+maxImageIntensity = 256;   %only relevant for method="gradient" - necessary for scaling of the gradient
+boxRadius = 10;            %only relevant for method="inertia" and method="fourier" - orientation estimation is performed in square boxes of side length 2*boxRadius+1 
+forCenterOfMass = true;    %only relevant for method="inertia" - compute second moments for center of the image patch or center of mass (image intensity)
+subtractBackgroud = true;  %only relevant for method="inertia"
+minWaveLength = 1;         %only relevant for method="fourier" - wavelength to consider for radial integration in the Fourier domain
+maxWaveLength = 10;        %only relevant for method="fourier"
+
+%% settings for optional downsampling of the grayscale image and/or the extracted orientation field
+downSamplFactorGrayscale = 1; %needs to be integer, 1=no downsampling
+downSamplFactorOrient = 1;    %needs to be integer, 1=no downsampling
+downSamplTotal = downSamplFactorGrayscale * downSamplFactorOrient;
+
+%% collect prepocessing settings in a "methodInfo"
+switch method
+    case 'fourier'
+        methodInfo = [ method '_radius' num2str(boxRadius) '_WaveLengths' num2str(minWaveLength) 'to' num2str(maxWaveLength) ];
+    case 'inertia'
+        methodInfo = [ method '_radius' num2str(boxRadius) '_' repmat('NOT', 1-forCenterOfMass) 'forCenterOfMass' '_' repmat('NOT', 1-subtractBackgroud) 'subtractBackgroud' ];
+    case 'gradient'
+        methodInfo = [ method '_kernel' num2str(kernelSize) ];
+        boxRadius = kernelSize; % use boxRadius as means to store the value of kernelSize, p.e. for usage in image erosion
+end
+methodInfo = [methodInfo '_downsample' num2str(downSamplFactorGrayscale) 'x' num2str(downSamplFactorOrient) ];
+
+
+%% settings for analysis of coherency, that is degree of local alignment betweem orientations in a box
+boxRadius_LocalAlignment = 10;  %this quantity counts in ORIGINAL pixels, defining a box of side length 1+2*boxRadius_LocalAlignment
+boxRadius_LocalAlignment_downSampled = floor( boxRadius_LocalAlignment / downSamplTotal );
+powerOfMagn_LocalAlignment = 1;
+
+myStrengthExponent = 1.0; % for smoothing
+mySigmaForDirectySmoothing = [1.0 1.0];  %kernel radius for smoothing
+
+requiredFracOfOrientInfo = 0.9; %fraction of orientations that must lay inside the region 
+  % of interest (or its foreground) for a box to be included into the statistics
+
+
+
+%% setting data folder and preparing output collection
+head_folder = '/Users/khoffman/Documents/DirectionalityDefects/processedData/2021_08_06_KarenSoans/';
+
+genericFilename = 'ZMAX_of_Resliced_of_Derotated_with2ndCrop.tif'; % according to the output of extractDerotatedSurface.py, 
+  % specify WITHOUT leading individual filename portions like 'ROI<number>_'
+
+filesToAnalyse = dir([head_folder '/**/Roi*_' genericFilename ]);
+% the levels below  head_folder  ougth to be  <Stage>/<Embryo>/<RegionOfTheEye>/<DerotationConditions>/
+% whereby <RegionOfTheEye> is one of the  ROInames = ["hollow", "inBetween", "ridge", "rim",  "back"];
+
+% prepare matrix/array for collection of statistics
+output_folder_stats = [head_folder 'collectedResults' '_' methodInfo ...
+    '_coherencyRadius' num2str(boxRadius_LocalAlignment) 'Power' num2str(powerOfMagn_LocalAlignment) '/'];
+temp = strsplit(output_folder_stats, '/');
+% within the folder, specify file to hold the statistics (its name = name of the deepest subfolder)
+output_collected = [output_folder_stats temp{end-1} '.csv'];
+
+%folder for the statistics across ROIs and plots
+if ~exist(output_folder_stats, 'dir')
+    mkdir( output_folder_stats )
+end
+
+
+%% analysis file by file (ROI by ROI)
+all_vals_mat = [];
+folders_arr = {};
+for numFile = 1:numel(filesToAnalyse)
+  input_folder = [filesToAnalyse(numFile).folder '/' ] ;
+  output_folder= input_folder;
+  
+  %% load the grayscale image
+  input_fullPath = [input_folder filesToAnalyse(numFile).name] ;
+  
+  [~, input_soleFilename, ~] = fileparts(input_fullPath);
+  leading_ROIinfo_inFilename = input_soleFilename( 1 : strfind(input_soleFilename, genericFilename(1:end-4))-1  ); %find name part that is individual to this file
+
+  [~, GrayImageStack] = loadGrayStack(input_fullPath);
+  assert(numel(size(GrayImageStack)) ==2) %2D data
+  NumSlices = 1;
+  
+  % scale image intensities to a fixed range for comparable results by the gradient 
+  if max(GrayImageStack(:))>255
+    warning('image intensities are being scaled down to <=255')
+    GrayImageStack = GrayImageStack / max(GrayImageStack(:)) ;
+  end
+  
+  % optional: show loaded data
+  %  imshow(GrayImageStack_orig / 255); title('original data')
+  
+  GrayImageStack_orig = GrayImageStack;
+  
+  
+  
+  %% optional downsampling of the grayscale image (downSamplFactorGrayscale = 1 means no downsampling)
+  % NOTE: Identification of the foreground still happens in the original image,
+  % but orientation analysis is performed in the downsampled grayscale data
+  size_y_orig = size(GrayImageStack_orig,1);
+  size_x_orig = size(GrayImageStack_orig,2);
+  assert( int8(downSamplFactorGrayscale) == downSamplFactorGrayscale, "must be integer")
+  
+  GrayImageStack_blocks = 1/(downSamplFactorGrayscale^2) ...
+    * conv2(GrayImageStack, ones(downSamplFactorGrayscale), 'valid') ;
+  
+  GrayImageStack_downSampl = GrayImageStack_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end);
+  
+  GrayImageStack = GrayImageStack_downSampl ;
+  size_y_grayBlocks = size(GrayImageStack_downSampl,1);
+  size_x_grayBlocks = size(GrayImageStack_downSampl,2);
+
+
+  %% load info on preprocessing 
+  % in particular rotations performed to check for anisotropy along the z-axis in the original imaging system 
+  % that might result from the form of the point-spread function 
+  input_preprocessingInfoPath = [input_folder leading_ROIinfo_inFilename 'logFileParameters.txt'];
+  
+  fileID = fopen(input_preprocessingInfoPath);
+  preprocessingInfo_raw = fread(fileID,'*char')';
+  fclose(fileID);
+
+  % find text location where normal vector is represented
+  pattern = 'Smallest eigen vector (V0) = normal vector of plane: (nx,ny,nz)= ';
+  textlocation = strfind(preprocessingInfo_raw, pattern);
+  if isempty(textlocation)
+    warning('no info on normal vector found')
+    NormalVecOrig = [NaN, NaN, NaN];
+  else
+    NormalVecOrig_raw = strsplit( preprocessingInfo_raw(textlocation+length(pattern) : end)); %split at whitespace and newline
+    NormalVecOrig_raw = NormalVecOrig_raw(1:3);
+    NormalVecOrig = str2double(NormalVecOrig_raw);
+    assert( abs( 1- sum(NormalVecOrig.^2) ) < 1e-5, 'Unit length violated by normal vector of the plane that was read') 
+  end
+  %NormalVecOrig is the normal vector of the plane in the original data,
+  % where the z-axis is the axis of anisotropy
+  
+  Rotation_theta_x = atan2(NormalVecOrig(2) , NormalVecOrig(3)); % equals atan(NormalVecOrig(2) / NormalVecOrig(3))
+  Rotation_theta_y = - atan2(NormalVecOrig(1) , sqrt(1-NormalVecOrig(1)^2) ); % named 'theta_yprime' in the preprocessingInfo_raw
+  
+  AnisotropyAxis = [  sin(Rotation_theta_y), ...
+                     -cos(Rotation_theta_y) * sin(Rotation_theta_x), ...
+                      cos(Rotation_theta_y) * cos(Rotation_theta_x) ] ;
+  %AnisotropyAxis is the axis of anisotropy in the rotated data, 
+  % where the z-axis is the normal axis of the plane (and this dimension is omitted by projection)
+  
+  %AnisotropyAxis is in the left-handed coordinate systems of Fiji (and MATLAB)
+  % therefore distinctino between "MATLABcoords" and righthanded "NORMALcoords"
+  AnisotropyAxis_inThePlane_MATLABcoords  = AnisotropyAxis(1:2) / sqrt(sum( AnisotropyAxis(1:2).^2)) ;
+  
+  %for the angle, take the same approach as for orientations theta: use the standard right-handed coordinate layout
+  AngleAnisotropy_inThePlane_NORMALcoords = atan2( -AnisotropyAxis(2), AnisotropyAxis(1) );  
+  
+  AngleAnisotropyProjection = acos(NormalVecOrig(3)) ; % or = acos(AnisotropyAxis(3));
+  % in 3D: angle between axis of anisotropy and direction of projection
+  
+  
+  %% orientation analysis
+  %% Prepare output arrays
+  theta = zeros( size(GrayImageStack) );
+  magn  = zeros( size(GrayImageStack) );
+  
+  %% Orientation analysis
+  time_needed_total=0;
+  for useSlice = 1:NumSlices
+    tic
+    switch method
+        case 'inertia'
+            [directy, ~] = ComputeDirecty(GrayImageStack(:,:,useSlice), boxRadius, forCenterOfMass, subtractBackgroud) ;
+        case 'fourier'
+            [directy, ~] = ComputeDirecty_FourierAndQ(GrayImageStack(:,:,useSlice), boxRadius, minWaveLength, maxWaveLength);
+        case 'gradient'
+            directy      = ComputeDirectyGradient(GrayImageStack(:,:,useSlice), kernelSize );
+        otherwise
+            warning(['method "' method '" not known'])
+    end
+    
+    theta(:,:,useSlice) = atan2( directy(:,:,2), directy(:,:,1) );
+    if strcmp(method,'gradient')
+        magn (:,:,useSlice) = directy(:,:,3) / maxImageIntensity;
+    else %'inertia', 'fourier'
+        magn (:,:,useSlice) = directy(:,:,3);
+    end
+    
+    time_needed=toc;
+    time_needed_total = time_needed_total + time_needed;
+    disp(['... done in ', num2str(time_needed), ' seconds.'])
+  end
+  disp(['All slices done in ', num2str(time_needed_total), ' seconds.'])
+  
+  
+  %% save result arrays (together with their settings) to file
+  % as checkpoint, because orientation estimation with method='inertia' and 'fourier' can take up to hours
+  filename_intermediate_output = [input_fullPath(1:end-4) '_' methodInfo '.mat'] ;
+  
+  save( filename_intermediate_output, ... 
+    'GrayImageStack_orig', ...  %the original data
+    'input_fullPath', ...  % its source
+    'NormalVecOrig', ... % the normal vector of the plane in original data, from which the applied rotation follows
+    'method', 'boxRadius', 'forCenterOfMass', 'subtractBackgroud', ...
+    'minWaveLength', 'maxWaveLength', ...  %the settings for orientation analysis
+    'theta', 'magn', ...  %the results
+    'downSamplFactorGrayscale', 'downSamplFactorOrient'  )  %information on downsampling
+  
+  %load(filename_intermediate_output)
+  
+  
+  %% find foreground = image parts sufficiently off the boundary 
+  % (determined without downsampling in the original image data of highest resolution)
+  % note that the actual data is surrounded by a dim region of polygonal
+  % shape due to the repeated cutting and projecting in Fiji 
+  % fg_closed = approximate the foreground region (after morphological closing for smoother shape and convex hull operation
+  %   as the shapes du to cutting/projecting are known to be convex polygons)
+  % fg_inner  = all those pixels, for which the complete box used for orientation estimation was on the foreground fg_closed
+  % fg_innermost = all those pixels, for which all those pixels taken into
+  %   account for local alignment had their respective box used for orientation estimation on the foreground fg_closed
+  
+  foreground_orig = imgaussfilt(GrayImageStack_orig/255, 5, 'FilterDomain','spatial', 'Padding', 0.0 );
+  foreground_orig = imbinarize(foreground_orig/max(foreground_orig(:)), 'global');  %imbinarize expects input normalised in [0,1]
+  %  imshow(foreground)
+  structureElement = strel('diamond',5);
+  fg_closed_orig = imclose(foreground_orig, structureElement);
+  %fg_closed_orig = imfill(fg_closed_orig, 'holes');
+  fg_closed_orig = bwconvhull(fg_closed_orig);
+  % compare
+  %   figure(1); imshow(fg_closed_orig)
+  %   figure(2); imshow(GrayImageStack_orig/255+0.5)
+  
+  fg_inner_orig = imerode(fg_closed_orig, strel('square', 2*boxRadius+1) );
+  %   figure(3); imshow(fg_inner_orig)
+  
+  fg_innermost_orig = imerode(fg_inner_orig, strel('square', 2*boxRadius_LocalAlignment+1) );
+  %   figure(4); imshow(fg_innermost_orig)
+  
+  %% downsample the foreground information according to the downsampling of grayscale and orientation blocks
+  % two variants: count a block in the downsampled image as forground, when
+  % (A) any of the contained pixels is foreground, or when
+  % (B) all of the contained pixels are foreground (" .."strict")
+  %% foreground downsampled to the resoution of grayscale downsampling
+  fg_closed_blocks = 1/(downSamplFactorGrayscale^2) ...
+    * conv2(fg_closed_orig, ones(downSamplFactorGrayscale), 'valid') ;
+  fg_closed        = (0 <  fg_closed_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  fg_closed_strict = (1 == fg_closed_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  
+  fg_inner_blocks = 1/(downSamplFactorGrayscale^2) ...
+    * conv2(fg_inner_orig, ones(downSamplFactorGrayscale), 'valid') ;
+  fg_inner        = (0 <  fg_inner_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  fg_inner_strict = (1 == fg_inner_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  
+  fg_innermost_blocks = 1/(downSamplFactorGrayscale^2) ...
+    * conv2(fg_innermost_orig, ones(downSamplFactorGrayscale), 'valid') ;
+  fg_innermost        = (0 <  fg_inner_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  fg_innermost_strict = (1 == fg_innermost_blocks(1:downSamplFactorGrayscale:end, 1:downSamplFactorGrayscale:end) );
+  
+  %% foreground downsampled to the (coarser) resolution which the orientation downsampling will attain
+  % downsample the full-resolution foreground information like "fg_closed_orig" 
+  % by the factor   downSamplTotal = downSamplFactorGrayscale * downSamplFactorOrient;
+  fg_closed_blocks = 1/(downSamplTotal^2) ...
+    * conv2(fg_closed_orig, ones(downSamplTotal), 'valid') ;
+  fg_closed_downSampl        = (0 <  fg_closed_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  fg_closed_downSampl_strict = (1 == fg_closed_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  
+  fg_inner_blocks = 1/(downSamplTotal^2) ...
+    * conv2(fg_inner_orig, ones(downSamplTotal), 'valid') ;
+  fg_inner_downSampl        = (0 <  fg_inner_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  fg_inner_downSampl_strict = (1 == fg_inner_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  
+  fg_innermost_blocks = 1/(downSamplTotal^2) ...
+    * conv2(fg_inner_orig, ones(downSamplTotal), 'valid') ;
+  fg_innermost_downSampl        = (0 <  fg_innermost_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  fg_innermost_downSampl_strict = (1 == fg_innermost_blocks(1:downSamplTotal:end, 1:downSamplTotal:end) );
+  
+  
+  %% smooth orientations with Gaussian kernel, and find versions restricted to the foreground
+  [theta_smoothed, magn_smoothed] = smoothDirecty_ThetaStrength(theta, magn, mySigmaForDirectySmoothing, myStrengthExponent);
+  
+  % alternative versions restrict to points in the foreground (_fg) 
+  % and those for which the image patch used for orientation estimation was fully within the foreground (_inner) 
+  % and those for which all pixels considered for local alignment had their respective box for orientation estimation fully in the foreground (_innermost)
+  %   (the last is NOT the same as "all pixels considered in the smoothing step")
+  magn_smoothed_fg = NaN (size(magn_smoothed));
+  magn_smoothed_fg(fg_closed) = magn_smoothed(fg_closed);
+  
+  magn_smoothed_fg_inner = NaN (size(magn_smoothed));
+  magn_smoothed_fg_inner(fg_inner) = magn_smoothed(fg_inner);
+  
+  magn_smoothed_fg_innermost = NaN (size(magn_smoothed));
+  magn_smoothed_fg_innermost(fg_innermost) = magn_smoothed(fg_innermost);
+  
+  %% optional: use the smoothed orientation field for further analysis
+  % basically set  theta = theta_smoothed  but assert values in the interval [0, pi]
+  % NOTE: smoothing might interfere with the subsequent analysis of alignment
+  %{
+  theta = mod(theta_smoothed, pi) ;
+  magn  = magn_smoothed;
+  %}
+  
+  
+  
+  %% optional: downsampling of the orientation field (downSamplFactorOrient = 1 means no downsampling)
+  assert( int8(downSamplFactorOrient) == downSamplFactorOrient, "must be integer")
+  
+  [theta_downSampl, magn_downSampl] = downSamplDirecty_ThetaStrength(theta, magn, downSamplFactorOrient);  % skipRows=0 and skipCols=0 fits to the grayscale downsampling in this file, strength_exponent)
+  
+  size_y_grayAndOrientBlocks = size(theta_downSampl,1);
+  size_x_grayAndOrientBlocks = size(theta_downSampl,2);
+  
+  % also downsample the grayscale intensities (again) as they will be used as weights in the check for anisotropy
+  GrayImageStack_blocksCoarse = 1/(downSamplFactorOrient^2) ...
+    * conv2(GrayImageStack_downSampl, ones(downSamplFactorOrient), 'valid') ;
+  
+  GrayImageStack_downSamplTwice = GrayImageStack_blocksCoarse(1:downSamplFactorOrient:end, 1:downSamplFactorOrient:end);
+  
+  assert( all([size_y_grayAndOrientBlocks, size_x_grayAndOrientBlocks] == size(GrayImageStack_downSamplTwice) ) )
+  
+  % use downsampled orientation field for further analysis
+  theta_orig = theta;
+  magn_orig  = magn ;
+  % basically set  theta = theta_downSampl  but assert values in the interval [0, pi]
+  theta = mod(theta_downSampl, pi) ;
+  magn  = magn_downSampl;
+  
+  
+  %% alignment as a function of space
+  %% compute local alignment in fixed box - potentially time consuming
+  % use boxRadius_LocalAlignment_downSampled that is adjusted to the downsampling performed beforehand
+  [localOrient, localCoherency] = ComputeLocalAlignment(theta,magn,boxRadius_LocalAlignment_downSampled, powerOfMagn_LocalAlignment);
+  
+  
+  %% save result arrays of local alignment (together with their settings) to file
+  % as checkpoint
+  filename_intermediate_output1 = [filename_intermediate_output(1:end-4) ...
+    '_coherencyRadius' num2str(boxRadius_LocalAlignment) 'Power' num2str(powerOfMagn_LocalAlignment) '.mat'];
+  
+  save( filename_intermediate_output1, ... 
+    'GrayImageStack_orig', ...  %the original data
+    'input_fullPath', ...  % its source
+    'NormalVecOrig', ... % the normal vector of the plane in original data, from which the applied rotation follows
+    'method', 'boxRadius', 'forCenterOfMass', 'subtractBackgroud', ...
+    'minWaveLength', 'maxWaveLength', ...  %the settings for orientation analysis
+    'theta', 'magn', ...   %the results
+    'downSamplFactorGrayscale', 'downSamplFactorOrient', ...   %information on downsampling
+    'fg_closed', 'fg_inner', 'fg_innermost', ... % different definitions of foreground
+    'boxRadius_LocalAlignment', 'boxRadius_LocalAlignment_downSampled', 'powerOfMagn_LocalAlignment', ... %the settings for local alignment analysis
+    'localOrient', 'localCoherency' )    %the results 
+  
+  %load(filename_intermediate_output1)
+  
+  
+  
+  %% local alignment restricted to foreground versions
+  % recall: this restricts to points in the foreground (_fg) 
+  % and those for which the image patch used for orientation estimation was fully within the foreground (_inner) 
+  % and those for which all pixels considered for local alignment had their respective box for orientation estimation fully in the foreground (_innermost)
+  localCoherency_fg = NaN (size(localCoherency));
+  localCoherency_fg(fg_closed_downSampl) = localCoherency(fg_closed_downSampl);
+  
+  localCoherency_fg_inner = NaN (size(localCoherency));
+  localCoherency_fg_inner(fg_inner_downSampl) = localCoherency(fg_inner_downSampl);
+  
+  localCoherency_fg_innermost = NaN (size(localCoherency));
+  localCoherency_fg_innermost(fg_innermost_downSampl) = localCoherency(fg_innermost_downSampl);
+  
+  %% compute global alignment of ALL orientations present
+  % in the whole image, in the foreground (_fg), and of those for which the
+  % image patch used for orientation estimation was fully within the foreground (_inner) 
+  [overallOrient,          overallCoherency]          = ComputeQTensor(theta, magn, powerOfMagn_LocalAlignment);
+  [overallOrient_fg,       overallCoherency_fg]       = ComputeQTensor(theta, magn.*fg_closed_downSampl, powerOfMagn_LocalAlignment);
+  [overallOrient_fg_inner, overallCoherency_fg_inner] = ComputeQTensor(theta, magn.*fg_inner_downSampl , powerOfMagn_LocalAlignment);
+  
+  
+  %% Alternatively, limit further analysis of local orientation to points 
+  % for which the neighbourhood considered contains not too much pixels without orientation information
+  OrientInfoPresent = (~isnan(theta))&(~isnan(magn));
+  OrientInfoPresent(1:boxRadius,         :) = 0;
+  OrientInfoPresent(end-boxRadius+1:end, :) = 0;
+  OrientInfoPresent(:, 1:boxRadius        ) = 0;
+  OrientInfoPresent(:, end-boxRadius+1:end) = 0;
+  LocalOrientIsReliable =    requiredFracOfOrientInfo * conv2( ones(size(theta)) , ones( 2*boxRadius_LocalAlignment_downSampled+1), 'same'  ) ...
+                             <=   conv2( OrientInfoPresent                       , ones( 2*boxRadius_LocalAlignment_downSampled+1), 'same'  )   ;
+  LocalOrientIsReliably_fg = requiredFracOfOrientInfo * conv2( ones(size(theta)) , ones( 2*boxRadius_LocalAlignment_downSampled+1), 'same'  ) ...
+                              <=   conv2( OrientInfoPresent.*fg_closed_downSampl , ones( 2*boxRadius_LocalAlignment_downSampled+1), 'same'  )   ;
+  
+  
+  [overallOrient_reliable,    overallCoherency_reliable]    = ComputeQTensor(theta, magn.*LocalOrientIsReliable, powerOfMagn_LocalAlignment);
+  [overallOrient_reliably_fg, overallCoherency_reliably_fg] = ComputeQTensor(theta, magn.*LocalOrientIsReliably_fg, powerOfMagn_LocalAlignment);
+  
+  
+  
+  %% compute alignment of orientations with axis of anisotropy in the data
+  % alignment1 = linear = absolute value of scalar product between two vectors 
+  % alignment2 = quadratic like the nematic order parameter, but with the (projected) axis of anisotropy as reference
+  % s = average over (3/2 cos^2 beta - 1/2)
+  % where beta = angle between orientation and (projected) axis of anisotropy, hence
+  % cos beta = scalar product of orientation with (projected) axis of anisotropy (when both are expressed as normalised vectors)
+  % values of s range from -0.5 (all detected orientations perpendicular to axis of anisotropy)
+  % to +1.0 (all detected orientations along the axis of anisotropy), but
+  % should be around zero (indicating little bias of the orientations by the anisotropy of imaging in the original z-axis)
+  alignment1          = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) )                         )) / nansum(nansum( isfinite(theta)                        )) ;
+  alignment1_fg       = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* fg_closed_downSampl )) ;
+  alignment1_fg_inner = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* fg_inner_downSampl  )) ;
+  
+  alignment2          = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2                         )) / nansum(nansum( isfinite(theta)                        )) ;
+  alignment2_fg       = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* fg_closed_downSampl )) ;
+  alignment2_fg_inner = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* fg_inner_downSampl  )) ;
+  
+  % versions that are weighted by magnitude of the orientation information and/or image intensity at that pixel
+  alignment1_magn          = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn                         )) / nansum(nansum( isfinite(theta) .* magn                        )) ;
+  alignment1_magn_fg       = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* magn .* fg_closed_downSampl )) ;
+  alignment1_magn_fg_inner = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* magn .* fg_inner_downSampl  )) ;
+  
+  alignment2_magn          = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn                         )) / nansum(nansum( isfinite(theta) .* magn                        )) ;
+  alignment2_magn_fg       = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* magn .* fg_closed_downSampl )) ;
+  alignment2_magn_fg_inner = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* magn .* fg_inner_downSampl  )) ;
+  
+  
+  alignment1_intens          = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* GrayImageStack_downSamplTwice                         )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice                        )) ;
+  alignment1_intens_fg       = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* GrayImageStack_downSamplTwice .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice .* fg_closed_downSampl )) ;
+  alignment1_intens_fg_inner = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* GrayImageStack_downSamplTwice .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice .* fg_inner_downSampl  )) ;
+  
+  alignment2_intens          = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* GrayImageStack_downSamplTwice                         )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice                        )) ;
+  alignment2_intens_fg       = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* GrayImageStack_downSamplTwice .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice .* fg_closed_downSampl )) ;
+  alignment2_intens_fg_inner = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* GrayImageStack_downSamplTwice .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* GrayImageStack_downSamplTwice .* fg_inner_downSampl  )) ;
+  
+  
+  alignment1_magn_intens          = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn .* GrayImageStack_downSamplTwice                         )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice                        )) ;
+  alignment1_magn_intens_fg       = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn .* GrayImageStack_downSamplTwice .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice .* fg_closed_downSampl )) ;
+  alignment1_magn_intens_fg_inner = -2/pi+ nansum(nansum(  abs( cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ) .* magn .* GrayImageStack_downSamplTwice .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice .* fg_inner_downSampl  )) ;
+  
+  alignment2_magn_intens          = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn .* GrayImageStack_downSamplTwice                         )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice                        )) ;
+  alignment2_magn_intens_fg       = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn .* GrayImageStack_downSamplTwice .* fg_closed_downSampl  )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice .* fg_closed_downSampl )) ;
+  alignment2_magn_intens_fg_inner = -0.5 +  nansum(nansum(  (cos(theta).* cos(AngleAnisotropy_inThePlane_NORMALcoords) + sin(theta) .* sin(AngleAnisotropy_inThePlane_NORMALcoords) ).^2 .* magn .* GrayImageStack_downSamplTwice .* fg_inner_downSampl   )) / nansum(nansum( isfinite(theta) .* magn .* GrayImageStack_downSamplTwice .* fg_inner_downSampl  )) ;
+  
+  
+  
+  %% collect results in structs with descriptions and values
+  %start with info on size of foreground
+  vals  = struct();
+  descr = struct();
+  
+  vals. size_x = size(theta,2);
+  descr.size_x = 'image size x';
+  
+  vals. size_y = size(theta,1);
+  descr.size_y = 'image size y';
+  
+  vals .pixels = sum(~isnan(theta(:)));
+  descr.pixels = 'pixels with orientation';
+  
+  assert(all(unique(fg_closed) == [ 0; 1]))
+  vals .pixels_fg = sum(fg_closed(:));
+  descr.pixels_fg = 'foreground pixels';
+  
+  assert(all(unique(fg_inner) == [ 0; 1]))
+  vals. pixels_fg_inner = sum(fg_inner(:));
+  descr.pixels_fg_inner = 'pixels for wich box was within foreground';
+  
+  %% add results of coherencies in structs with descriptions and values
+  vals. overallCoherency = overallCoherency;
+  descr.overallCoherency = 'coherency - all orientations';
+  vals. overallTheta = atan2(overallOrient(2), overallOrient(1));
+  descr.overallTheta = 'theta - all orientations';
+  
+  vals. overallCoherency_fg = overallCoherency_fg;
+  descr.overallCoherency_fg = 'coherency - all orientations on foreground';
+  vals. overallTheta_fg = atan2(overallOrient_fg(2), overallOrient_fg(1));
+  descr.overallTheta_fg = 'theta - all orientations on foreground';
+  
+  vals. overallCoherency_fg_inner = overallCoherency_fg_inner;
+  descr.overallCoherency_fg_inner = 'coherency - all orientations for which box was within foreground';
+  vals. overallTheta_fg_inner = atan2(overallOrient_fg_inner(2), overallOrient_fg_inner(1));
+  descr.overallTheta_fg_inner = 'theta - all orientations for which box was within foreground';
+  
+  vals. overallCoherency_reliab = overallCoherency_reliable;
+  descr.overallCoherency_reliab = 'coherency - all orientations for which box had mostly orientation information';
+  vals. overallTheta_reliab = atan2(overallOrient_reliable(2), overallOrient_reliable(1));
+  descr.overallTheta_reliab= 'theta - all orientations for which box box had mostly orientation information';
+  
+  vals. overallCoherency_reliab_fg = overallCoherency_reliably_fg;
+  descr.overallCoherency_reliab_fg = 'coherency - all orientations for which box had mostly foreground and orientation information';
+  vals. overallTheta_reliab_fg = atan2(overallOrient_reliably_fg(2), overallOrient_reliably_fg(1));
+  descr.overallTheta_reliab_fg = 'theta - all orientations for which box had mostly foreground and orientation information';
+  
+  vals. localCoherency = nanmean(localCoherency(:));
+  descr.localCoherency = 'mean local coherency (in boxes)';
+  vals. localCoherency_fg = nanmean(localCoherency_fg(:));
+  descr.localCoherency_fg = 'mean local coherency (in boxes) over foreground';
+  vals. localCoherency_fg_inner = nanmean(localCoherency_fg_inner(:));
+  descr.localCoherency_fg_inner = 'mean local coherency (in boxes) where box was within foreground';
+  
+  vals. localCoherencyGaussian = nanmean(magn_smoothed(:));
+  descr.localCoherencyGaussian = 'mean local coherency (Gaussian)';
+  vals. localCoherencyGaussian_fg = nanmean(magn_smoothed_fg(:));
+  descr.localCoherencyGaussian_fg = 'mean local coherency (Gaussian) over foreground';
+  vals. localCoherencyGaussian_fg_inner = nanmean(magn_smoothed_fg_inner(:));
+  descr.localCoherencyGaussian_fg_inner = 'mean local coherency (Gaussian) where box was within foreground';
+  
+  
+  %% add info on anisotropy of data
+  vals. normalVec_x = NormalVecOrig(1);
+  descr.normalVec_x = 'normal vector in original data n_x';
+  vals. normalVec_y = NormalVecOrig(2);
+  descr.normalVec_y = 'normal vector in original data n_y';
+  vals. normalVec_z = NormalVecOrig(3);
+  descr.normalVec_z = 'normal vector in original data n_z';
+  
+  vals. AngleAnisotropyProjection = AngleAnisotropyProjection;
+  descr.AngleAnisotropyProjection = 'angle between projection axis and anisotropy (radians)';
+  
+  vals. orientationBias_linear = alignment1_fg_inner;
+  descr.orientationBias_linear = 'bias of orientations by anisotropy - linear, where box was within foreground';
+  vals. orientationBias_quadratic = alignment2_fg_inner;
+  descr.orientationBias_quadratic = 'bias of orientations by anisotropy - quadratic (as for nematic order parameter), where box was within foreground';
+  
+  
+  %% prepare writing of data
+  vals_mat = struct2array(vals);
+  all_vals_mat = [all_vals_mat; vals_mat] ;
+  
+  folder_to_subfolders = strsplit(input_folder, filesep);
+  
+  folders_arr = [ folders_arr  ; ...
+      {input_folder} folder_to_subfolders(end-4), folder_to_subfolders(end-3), folder_to_subfolders(end-2), folder_to_subfolders(end-1), ...
+      leading_ROIinfo_inFilename];
+  %recall: the code assumes that folder_to_subfolders
+  % contains  <Stage>/<Embryo>/<RegionOfTheEye>/<DerotationConditions>/
+  %  and the different ROIs within one folder are distinguished by leading_ROIinfo_inFilename
+  
+  %% write results to text file
+  filename_intermediate_output2 = [filename_intermediate_output(1:end-4) ...
+      '_coherencyRadius' num2str(boxRadius_LocalAlignment) 'Power' num2str(powerOfMagn_LocalAlignment) '.txt'];
+  
+  dlmwrite( filename_intermediate_output2 , ['coherencies obtained from'],     'delimiter','')  %header
+  dlmwrite( filename_intermediate_output2 , [filename_intermediate_output2] ,  '-append', 'delimiter','')  % origin of data
+  dlmwrite( filename_intermediate_output2 , ['Parameters used'] ,  '-append', 'delimiter','')
+  dlmwrite( filename_intermediate_output2 , [num2str(boxRadius) , ' BoxRadius'], '-append', 'delimiter','')  % parameters used
+  dlmwrite( filename_intermediate_output2 , [num2str(boxRadius_LocalAlignment) , ' BoxRadius for Local Alignment'], '-append', 'delimiter','')  %  parameters used
+  dlmwrite( filename_intermediate_output2 , [num2str(boxRadius_LocalAlignment_downSampled) , ' BoxRadius for Local Alignment in downsampled pixels'], '-append', 'delimiter','')  %  parameters used
+  dlmwrite( filename_intermediate_output2 , [num2str(powerOfMagn_LocalAlignment), ' Power of Magnitude for Local Alignment'], '-append', 'delimiter','')  %  parameters used
+  dlmwrite( filename_intermediate_output2 , [num2str(mySigmaForDirectySmoothing) , ' sigma for smoothing'], '-append', 'delimiter','')  %  parameters used
+  dlmwrite( filename_intermediate_output2 , [num2str(myStrengthExponent), ' Power of Magnitude for smoothing'], '-append', 'delimiter','')  %  parameters used
+  dlmwrite( filename_intermediate_output2 , [' '], '-append', 'delimiter','')  %  empty line
+  
+  
+  fieldnameslist = fieldnames(vals);
+  for kk = 1:numel(fieldnameslist)
+      dlmwrite( filename_intermediate_output2 , [num2str(vals.(fieldnameslist{kk})), ' ', descr.(fieldnameslist{kk})], '-append', 'delimiter','')
+  end
+  
+  
+  
+  
+  %% plotting
+  %% Color and other conventions for plotting
+  
+  col_director     = [0.4 0.4 1.0];  %NOT 'blue', as this is too dark
+  col_localOrient = [1.0 0.4 0.4];
+  show_localOrient = false; % show locally averaged orientation that corresponds to the computed alignment
+  
+  col_anisotropy = [0.2 1 0.2] ;
+  show_anisotropy = true;
+  
+  centerQuiverOnPixels = true; %show orientations centered over their location
+  
+  savePlots = false;
+  useMagnForPlotting = false; % scale quiver plots by orientation magnitude
+  
+  %spacing12 between plotted orientation [integer, =n plots only every n-th orientation]
+  spacing12 = ceil(4.0/downSamplTotal);  % a reasonable start for ROIs of few hundred pixels
+  %spacing12 = 1
+  % {
+  %% plotting of orig image with orientation field
+  myData = GrayImageStack_orig(:,:,useSlice);
+  myData = myData/ max(GrayImageStack_orig(:)); %rescale for plotting
+  myData = 0.2+0.8*  myData;  %make the data more bright in plotting
+  
+  assert(max(myData(:)) <= 1.0, 'Rescale data!')
+  theta_slice = theta(:,:,useSlice);
+  magn_slice  = GrayImageStack_downSamplTwice .* magn (:,:,useSlice);
+  
+  [x12, y12] = meshgrid( 0.5+downSamplTotal*(-0.5+(1:size(theta_slice,2)) ), ...
+                         0.5+downSamplTotal*(-0.5+(1:size(theta_slice,1)) )  );
+  % rescale positions that are given as row/column in the arrays of data
+  % after downsampling twice, back to their positions in the original data
+  scaleToOrig_x = @(xInDownSamplTwice) 0.5+(xInDownSamplTwice-0.5)*downSamplTotal;
+  scaleToOrig_y = @(yInDownSamplTwice) 0.5+(yInDownSamplTwice-0.5)*downSamplTotal;
+  
+  u12 = magn_slice.^useMagnForPlotting .*  cos( theta_slice ) ; 
+  v12 = magn_slice.^useMagnForPlotting .* -sin( theta_slice ) ; %negative sign to transform from theta in normal coord layout to coord layout used by MATLAB (y downwards in plotting that is started by imshow() )
+  u_localOrient = localOrient(:,:,1) .* localCoherency;
+  v_localOrient = localOrient(:,:,2) .* localCoherency;
+  
+  %% plotting itself
+  Fig12 = figure('PaperOrientation', 'landscape', 'Position', [0, 0, 1200, 700]);
+  [axesHandles12, positions12] = tight_subplot(1,1,[.08 .05],[.05 .01],[.05 .02]) ;   %gap, margin_height, margin_width)
+  fig12A = axesHandles12(1);
+  
+  axes(fig12A)   %make the existing axes current
+  
+  OrigData12 = imshow(myData);  % (myData * 2 - 3);
+  hold on
+  
+  % add overlay to indicate the foreground - see https://de.mathworks.com/company/newsletters/articles/image-overlay-using-transparency.html
+  % show true-color all-red image on top
+  red = cat(3, ones(size(myData)), zeros(size(myData)), zeros(size(myData)));
+  foreground_plot = imshow(red);
+  % but set its transparency according to fg_closed_orig
+  set(foreground_plot, 'AlphaData', 0.1* fg_closed_orig)
+  
+  % orientations plotted as head-less vectors
+  quiv_onOrigData1 = quiver(x12(1:spacing12:end,1:spacing12:end), y12(1:spacing12:end,1:spacing12:end), u12(1:spacing12:end,1:spacing12:end), v12(1:spacing12:end,1:spacing12:end));
+  if centerQuiverOnPixels
+      %duplicate quiver (with opposite sign) to have coherent appearance
+      quiv_onOrigData2 = quiver(x12(1:spacing12:end,1:spacing12:end), y12(1:spacing12:end,1:spacing12:end), -u12(1:spacing12:end,1:spacing12:end), -v12(1:spacing12:end,1:spacing12:end));
+  end
+  
+  % modify quiver plot(s)
+  quiv_onOrigData1.ShowArrowHead = 'off';
+  quiv_onOrigData1.LineWidth  = 1.5;  %(default: 0.5)
+  quiv_onOrigData1.AutoScaleFactor = 1.0 ;
+  quiv_onOrigData1.Color = col_director;
+  if centerQuiverOnPixels
+      %give the duplicate quiver the same properties, especially the same color
+      quiv_onOrigData2.ShowArrowHead   = quiv_onOrigData1.ShowArrowHead   ;
+      quiv_onOrigData2.LineWidth       = quiv_onOrigData1.LineWidth       ;
+      quiv_onOrigData2.AutoScaleFactor = quiv_onOrigData1.AutoScaleFactor ;
+      quiv_onOrigData2.Color           = quiv_onOrigData1.Color           ;
+  end
+  
+  
+  if show_localOrient
+      quiv_localOrient1 = quiver(x12(1:spacing12:end,1:spacing12:end), y12(1:spacing12:end,1:spacing12:end), u_localOrient(1:spacing12:end,1:spacing12:end), -v_localOrient(1:spacing12:end,1:spacing12:end));
+      %Note negative sign on y-component to transform from theta in normal coord layout to coord layout used by MATLAB (y downwards in plotting that is started by imshow() )
+      if centerQuiverOnPixels
+          %duplicate quiver (with opposite sign) to have coherent appearance
+          quiv_localOrient2 = quiver(x12(1:spacing12:end,1:spacing12:end), y12(1:spacing12:end,1:spacing12:end), -u_localOrient(1:spacing12:end,1:spacing12:end), v_localOrient(1:spacing12:end,1:spacing12:end));
+      end
+      
+      % modify quiver plot(s)
+      quiv_localOrient1.ShowArrowHead = 'off';
+      quiv_localOrient1.LineWidth  = 0.5;  %(default: 0.5)
+      quiv_localOrient1.AutoScaleFactor = 1.0 ;
+      quiv_localOrient1.Color = col_localOrient;
+      if centerQuiverOnPixels
+          %give the duplicate quiver the same properties, especially the same color
+          quiv_localOrient2.ShowArrowHead   = quiv_localOrient1.ShowArrowHead   ;
+          quiv_localOrient2.LineWidth       = quiv_localOrient1.LineWidth       ;
+          quiv_localOrient2.AutoScaleFactor = quiv_localOrient1.AutoScaleFactor ;
+          quiv_localOrient2.Color           = quiv_localOrient1.Color           ;
+      end
+  end
+  
+  
+  if show_anisotropy
+      % plot projection of the anisotropy outside of the plot area
+      quiv_anisotropy = quiver( (size_x_orig+15)*[1 1], 0.5*(size_y_orig)*[1 1], ...
+          [AnisotropyAxis_inThePlane_MATLABcoords(1), -AnisotropyAxis_inThePlane_MATLABcoords(1)], ...
+          [AnisotropyAxis_inThePlane_MATLABcoords(2), -AnisotropyAxis_inThePlane_MATLABcoords(2)] );
+      quiv_anisotropy.ShowArrowHead = 'off';
+      quiv_anisotropy.LineWidth  = 5;
+      quiv_anisotropy.AutoScaleFactor = 10;
+      quiv_anisotropy.Color = col_anisotropy;
+      
+      text( (size_x_orig+15), 0.5*(size_y_orig)-20, ...
+          'anisotropy axis (projected)')
+      text( (size_x_orig+15), 0.5*(size_y_orig)+20, [{'bias into anisotropy direction' , 'among orientations where box was within foreground', ['linear = ',  num2str(alignment1_magn_intens_fg_inner)],  ['quadratic = ',  num2str(alignment2_magn_intens_fg_inner)]  }])
+      
+      text( (size_x_orig+15), 0.5*(size_y_orig)+60, ...
+          {'angle between', 'anisotropy and projection', ['= ' num2str(AngleAnisotropyProjection*180/pi) ' degree']} )
+      
+      
+  end
+  
+  axis equal
+  
+  title(['orig image with foreground highlighted, orientations'] )
+  
+  drawnow
+  pause(0.1)
+  if show_anisotropy
+      %make more space for title and annotations on the right side
+      currAx = gca();
+      currAx.Position = currAx.Position - [0 0 0.05 0.05];
+  end
+  
+  print(Fig12, [filename_intermediate_output(1:end-4) '_Orientations'],'-dpdf', '-r0', '-fillpage')
+  print(Fig12, [filename_intermediate_output(1:end-4) '_Orientations'],'-dpng', '-r0')
+  
+  close all
+  
+  
+  %% plotting the distribution of orientations to check for impact of anisotropy in imaging
+  %% binning
+  % bin the data explicitly to allow for weights being considered
+  % bin into interval [0,pi], then duplicate data from theta to pi+theta to represent nematics
+  NbinsPolar = 128;
+  theta_for_hists = mod(theta, pi);
+  assert ( min(theta_for_hists(:))>= 0  &  max(theta_for_hists(:))<=pi, 'theta falls  out of range [0, pi]' )
+  EdgesPolar = linspace(0, pi, NbinsPolar+1);
+  % this ensures to have all data fall into the bins (NaN bins are only due to theta value NaN)
+  assert ( EdgesPolar(1) == 0  &  EdgesPolar(end) == pi , 'Incorrect bins')
+  EdgesNematic = [EdgesPolar(1:end-1)  pi+EdgesPolar];
+  
+  whichBin = discretize(theta_for_hists, EdgesPolar);
+  
+  Freq              = zeros([1 numel(EdgesPolar)-1]);
+  Freq_fg           = zeros([1 numel(EdgesPolar)-1]);
+  Freq_fg_inner     = zeros([1 numel(EdgesPolar)-1]);
+  %Freq_fg_innermost = zeros([1 numel(EdgesPolar)-1]);
+  % _innermost does not add information, because it makes sure that coherency estimation is within the foreground
+  
+  Freq_magn           = zeros([1 numel(EdgesPolar)-1]);
+  Freq_magn_fg        = zeros([1 numel(EdgesPolar)-1]);
+  Freq_magn_fg_inner  = zeros([1 numel(EdgesPolar)-1]);
+  
+  Freq_intens               = zeros([1 numel(EdgesPolar)-1]);
+  Freq_intens_fg            = zeros([1 numel(EdgesPolar)-1]);
+  Freq_intens_fg_inner      = zeros([1 numel(EdgesPolar)-1]);
+  
+  Freq_magn_intens          = zeros([1 numel(EdgesPolar)-1]);
+  Freq_magn_intens_fg       = zeros([1 numel(EdgesPolar)-1]);
+  Freq_magn_intens_fg_inner = zeros([1 numel(EdgesPolar)-1]);
+  
+  
+  for kk=1:numel(EdgesPolar)-1
+      Freq(kk)             =    sum(   sum( (whichBin==kk) ));
+      Freq_fg(kk)          =    sum(   sum( (whichBin==kk) .* fg_closed_downSampl));
+      Freq_fg_inner(kk)    =    sum(   sum( (whichBin==kk) .* fg_inner_downSampl));
+      %Freq_fg_innermost(kk)=    sum(   sum( (whichBin==kk) .* fg_innermost_downSampl));
+      
+      Freq_magn(kk)                 = nansum(nansum( (whichBin==kk) .* magn                                   ));
+      Freq_magn_fg(kk)              = nansum(nansum( (whichBin==kk) .* magn                                   .* fg_closed_downSampl));
+      Freq_magn_fg_inner(kk)        = nansum(nansum( (whichBin==kk) .* magn                                   .* fg_inner_downSampl ));
+      
+      Freq_intens(kk)               =    sum(   sum( (whichBin==kk)          .* GrayImageStack_downSamplTwice));
+      Freq_intens_fg(kk)            =    sum(   sum( (whichBin==kk)          .* GrayImageStack_downSamplTwice .* fg_closed_downSampl));
+      Freq_intens_fg_inner(kk)      =    sum(   sum( (whichBin==kk)          .* GrayImageStack_downSamplTwice .* fg_inner_downSampl ));
+      
+      Freq_magn_intens(kk)          = nansum(nansum( (whichBin==kk) .* magn .* GrayImageStack_downSamplTwice));
+      Freq_magn_intens_fg(kk)       = nansum(nansum( (whichBin==kk) .* magn .* GrayImageStack_downSamplTwice .* fg_closed_downSampl));
+      Freq_magn_intens_fg_inner(kk) = nansum(nansum( (whichBin==kk) .* magn .* GrayImageStack_downSamplTwice .* fg_inner_downSampl ));
+  end
+  
+  %% plotting of orientation distribution itself
+  colUse_All = 'black' ;
+  colUse_fg  = [0 0.8 0]; %dark green
+  colUse_fg_inner  = 'blue';
+  colUse_fg_innermost  = 'red';
+  
+  Fig13 = figure(13);  %'PaperSize', [11 8.5]) %'Position', [100 100 300 500]);
+  Fig13.Position = [0, 0, 1200, 900] ;
+  Fig13A   = polaraxes('Units', 'normalized', 'OuterPosition', [     0  1/2  1/2  1/2]) ;
+  Fig13B   = polaraxes('Units', 'normalized', 'OuterPosition', [   1/2  1/2  1/2  1/2]) ;
+  Fig13C   = polaraxes('Units', 'normalized', 'OuterPosition', [     0    0  1/2  1/2]) ;
+  Fig13D   = polaraxes('Units', 'normalized', 'OuterPosition', [   1/2    0  1/2  1/2]) ;
+  
+  subplot(Fig13A); %makes the existing subplot current
+  hold on
+  pHist             = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq             Freq            ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_All);
+  pHist_fg          = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_fg          Freq_fg         ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg);
+  pHist_fg_inner    = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_fg_inner    Freq_fg_inner   ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg_inner);
+  %    pHist_fg_innermost= polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_fg_innermost Freq_fg_innermost], ...
+  %        'DisplayStyle','stairs', 'EdgeColor', colUse_fg_innermost);
+  
+  % add projection of the anisotropy
+  pAnisotropyAxis = polarplot( [AngleAnisotropy_inThePlane_NORMALcoords, AngleAnisotropy_inThePlane_NORMALcoords+pi], [0.8, 0.8]* max(Freq), 'Color', 'red' );
+  
+  legend([pHist, pHist_fg, pHist_fg_inner, ... %, pHist_fg_innermost,...
+      pAnisotropyAxis], ...
+      {'all orientations', 'orientations in foreground', 'where box was within foreground', ... % 'where coherency box was within foreground', ...
+      'axis of anisotropy (projected)'}, ...
+      'Location', 'southwest')
+  
+  text(-pi/4, max(Freq), [{'bias into anisotropy direction' , 'among orientations where box was within foreground', ['linear = ',  num2str(alignment1_fg_inner)],  ['quadratic = ',  num2str(alignment2_fg_inner)]  }])
+  
+  text( pi/4, 1.5*max(Freq), ...
+      {'angle between', 'anisotropy and projection', ['= ' num2str(AngleAnisotropyProjection*180/pi) ' degree']} )
+  
+  title('frequency of orientations')
+  
+  
+  subplot(Fig13B); %makes the existing subplot current
+  hold on
+  pHist_magn             = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn           Freq_magn            ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_All);
+  pHist_magn_fg          = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn_fg        Freq_magn_fg         ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg);
+  pHist_magn_fg_inner    = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn_fg_inner  Freq_magn_fg_inner   ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg_inner);
+  
+  pAnisotropyAxis = polarplot( [AngleAnisotropy_inThePlane_NORMALcoords, AngleAnisotropy_inThePlane_NORMALcoords+pi], [0.8, 0.8]* max(Freq_magn), 'Color', 'red' );
+  
+  %legend([pHist_magn, pHist_magn_fg, pHist_magn_fg_inner, ...
+  %    pAnisotropyAxis], ...
+  %    {'all orientations', 'orientations in foreground', 'where box was within foreground', ...
+  %    'axis of anisotropy (projected)'}, ...
+  %    'Location', 'southwest')
+  
+  text(-pi/4, max(Freq_magn), [{'bias into anisotropy direction' , 'among orientations where box was within foreground', ['linear = ',  num2str(alignment1_magn_fg_inner)],  ['quadratic = ',  num2str(alignment2_magn_fg_inner)]  }])
+  
+  title('frequency of orientations - magnitude weighted')
+  
+  
+  subplot(Fig13C); %makes the existing subplot current
+  hold on
+  pHist_intens             = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_intens           Freq_intens            ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_All);
+  pHist_intens_fg          = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_intens_fg        Freq_intens_fg         ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg);
+  pHist_intens_fg_inner    = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_intens_fg_inner  Freq_intens_fg_inner   ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg_inner);
+  
+  pAnisotropyAxis = polarplot( [AngleAnisotropy_inThePlane_NORMALcoords, AngleAnisotropy_inThePlane_NORMALcoords+pi], [0.8, 0.8]* max(Freq_intens), 'Color', 'red' );
+  
+  %legend([pHist_intens, pHist_intens_fg, pHist_intens_fg_inner, ...
+  %    pAnisotropyAxis], ...
+  %    {'all orientations', 'orientations in foreground', 'where box was within foreground', ...
+  %    'axis of anisotropy (projected)'}, ...
+  %    'Location', 'southwest')
+  
+  text(-pi/4, max(Freq_intens), [{'bias into anisotropy direction' , 'among orientations where box was within foreground', ['linear = ',  num2str(alignment1_intens_fg_inner)],  ['quadratic = ',  num2str(alignment2_intens_fg_inner)]  }])
+  
+  title('frequency of orientations - intensity weighted')
+  
+  
+  subplot(Fig13D); %makes the existing subplot current
+  hold on
+  pHist_magn_intens             = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn_intens           Freq_magn_intens            ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_All);
+  pHist_magn_intens_fg          = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn_intens_fg        Freq_magn_intens_fg         ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg);
+  pHist_magn_intens_fg_inner    = polarhistogram('BinEdges',EdgesNematic,'BinCounts',[Freq_magn_intens_fg_inner  Freq_magn_intens_fg_inner   ], ...
+      'DisplayStyle','stairs', 'EdgeColor', colUse_fg_inner);
+  
+  pAnisotropyAxis = polarplot( [AngleAnisotropy_inThePlane_NORMALcoords, AngleAnisotropy_inThePlane_NORMALcoords+pi], [0.8, 0.8]* max(Freq_magn_intens), 'Color', 'red' );
+  
+  %legend([pHist_magn_intens, pHist_magn_intens_fg, pHist_magn_intens_fg_inner, ...
+  %    pAnisotropyAxis], ...
+  %    {'all orientations', 'orientations in foreground', 'where box was within foreground', ...
+  %    'axis of anisotropy (projected)'}, ...
+  %    'Location', 'southwest')
+  
+  text(-pi/4, max(Freq_magn_intens), [{'bias into anisotropy direction' , 'among orientations where box was within foreground', ['linear = ',  num2str(alignment1_magn_intens_fg_inner)],  ['quadratic = ',  num2str(alignment2_magn_intens_fg_inner)]  }])
+  
+  title('frequency of orientations - magnitude and intensity weighted')
+  
+  
+  drawnow
+  pause(0.1)
+  
+  print(Fig13, [filename_intermediate_output(1:end-4) '_Anisotropy' ],'-dpdf', '-r0', '-fillpage')
+  print(Fig13, [filename_intermediate_output(1:end-4) '_Anisotropy' ],'-dpng', '-r0')
+  
+  close all
+  
+  
+  %% plotting of local coherency and locally dominant orientation
+  % NOTE: this plot is only possible at the resolution of the orientation
+  %  estimate, that is after the downsampling
+  [x14, y14] = meshgrid(1:size(localCoherency,2), 1:size(localCoherency,1) );
+  u_localOrient = localOrient(:,:,1) .* localCoherency;
+  v_localOrient = localOrient(:,:,2) .* localCoherency;
+  
+  Fig14 = figure('PaperOrientation', 'landscape', 'Position', [0, 0, 1200, 700]);
+  [axesHandles14, positions14] = tight_subplot(1,1,[.08 .05],[.05 .01],[.05 .02]) ;   %gap, margin_height, margin_width)
+  fig14A = axesHandles14(1);
+  
+  axes(fig14A)   %make the existing axes current
+  
+  
+  OrigData14 = imshow(localCoherency);  % (myData * 2 - 3);
+  hold on
+  
+  if true  % if show_localOrient
+      quiv_localOrient1 = quiver(x14(1:spacing12:end,1:spacing12:end), y14(1:spacing12:end,1:spacing12:end), u_localOrient(1:spacing12:end,1:spacing12:end), -v_localOrient(1:spacing12:end,1:spacing12:end));
+      %Note negative sign on y-component to transform from theta in normal coord layout to coord layout used by MATLAB (y downwards in plotting that is started by imshow() )
+      if centerQuiverOnPixels
+          %duplicate quiver (with opposite sign) to have coherent appearance
+          quiv_localOrient2 = quiver(x14(1:spacing12:end,1:spacing12:end), y14(1:spacing12:end,1:spacing12:end), -u_localOrient(1:spacing12:end,1:spacing12:end), v_localOrient(1:spacing12:end,1:spacing12:end));
+      end
+      
+      % modify quiver plot(s)
+      quiv_localOrient1.ShowArrowHead = 'off';
+      quiv_localOrient1.LineWidth  = 0.5;  %(default: 0.5)
+      quiv_localOrient1.AutoScaleFactor = 1.0 ;
+      quiv_localOrient1.Color = col_localOrient;
+      if centerQuiverOnPixels
+          %give the duplicate quiver the same properties, especially the same color
+          quiv_localOrient2.ShowArrowHead   = quiv_localOrient1.ShowArrowHead   ;
+          quiv_localOrient2.LineWidth       = quiv_localOrient1.LineWidth       ;
+          quiv_localOrient2.AutoScaleFactor = quiv_localOrient1.AutoScaleFactor ;
+          quiv_localOrient2.Color           = quiv_localOrient1.Color           ;
+      end
+  end
+  
+  
+  axis equal
+  
+  %{
+    cbar = colorbar;
+    cbar.Label.String = 'Local Coherency';
+  %}
+  
+  title(['locally dominant orientation (within radius = ' num2str(boxRadius_LocalAlignment) '(orig pixels) at power ' num2str(powerOfMagn_LocalAlignment) ')' ])
+  
+  drawnow
+  pause(0.1)
+  
+  print(Fig14, [filename_intermediate_output(1:end-4) '_coherencyRadius' num2str(boxRadius_LocalAlignment) 'Power' num2str(powerOfMagn_LocalAlignment) ],'-dpdf', '-r0', '-fillpage')
+  print(Fig14, [filename_intermediate_output(1:end-4) '_coherencyRadius' num2str(boxRadius_LocalAlignment) 'Power' num2str(powerOfMagn_LocalAlignment) ],'-dpng', '-r0')
+  
+  close all
+  
+  
+end
+
+
+
+%% save collected coherency for each ROI to file  output_collected  as .mat and as .csv for subsequent statistical analysis
+% basically write all_vals_mat with descr as its header, but precede by
+% information on the respective path stored in folders_arr
+%recall: the code assumes that folder_to_subfolders
+% contains  <Stage>/<Embryo>/<RegionOfTheEye>/<DerotationConditions>/
+%  and the different ROIs within one folder are distinguished by leading_ROIinfo_inFilename
+
+% save as .mat file
+save([ output_collected(1:end-4) '.mat'], 'descr', 'folders_arr', 'all_vals_mat')
+
+% save as .csv
+% write the header
+fileID = fopen(output_collected,'w');
+fprintf(fileID, ['full path' '\t' 'stage' '\t' 'embryo' '\t' 'region of the eye' '\t' ...
+    'derotation conditions' '\t' 'ROI'] );
+fieldnameslist = fieldnames(descr);
+for kk = 1:numel(fieldnameslist)
+    fprintf(fileID, [descr.(fieldnameslist{kk}) '\t'] );
+end
+fprintf(fileID, '\n');
+fclose(fileID);
+
+% write the content
+for kk=1:size(folders_arr,1)
+    %write filenames
+    fileID = fopen(output_collected,'a');
+    fprintf(fileID, [ folders_arr{kk,1} '\t' ...
+                      folders_arr{kk,2} '\t' ...
+                      folders_arr{kk,3} '\t' ...
+                      folders_arr{kk,4} '\t' ...
+                      folders_arr{kk,5} '\t' ...
+                      folders_arr{kk,6} '\t' ]);
+    fclose(fileID);
+    %write statistics
+    dlmwrite( output_collected , all_vals_mat(kk,:),  '-append', 'delimiter','\t') %collected data
+end
diff --git a/AnalyseOrientations/ComputeDirecty.m b/AnalyseOrientations/ComputeDirecty.m
new file mode 100644
index 0000000..ecc7959
--- /dev/null
+++ b/AnalyseOrientations/ComputeDirecty.m
@@ -0,0 +1,336 @@
+function [Directy, subpatchesChangeDirecty] = ComputeDirecty(anArray, boxRadius, forCenterOfMass, subtractBackground, blackBackground)
+% Compute an orientation estimate (also known as directionality) for input
+% matrix of grayscale image intensities, 
+% using the eigenvectors of the tensor of inertia (ToI) of the intensities
+% on square image patches of user-specified side length 2*boxRadius+1.
+% For input of size (m,n), the return value Directy is of size (m,n,3). 
+% For each pixel (i,j), Directy(i,j,3) contains the strength of directionality 
+% and the corresponding normalised eigenvector is returned in Directy(i,j,1:2).
+% where Directy(i,j,1) is the x-component and Directy(i,j,2) is the y-component
+% in *standard* coordinate layout (while the indexing (i,j) is in MATLAB coordinate layout).
+% 
+% The strength of the orientation is defined 
+%  as (\lambda_max-\lambda_min) / (\lambda_max+\lambda_min)
+%  where \lambda_max >= \lambda_min are the two eigenvalues of the ToI 
+% 
+% Arguments: 
+% - boxRadius [not necessarily integer, >0.5] sets the scale: The ToI is computed from a 
+% roughly circular patch with radius boxRadius, and the result is assigned to the center pixel.
+
+%  [old: The ToI is computed from a box of size 2*boxRadius+1, where boxRadius 
+%   needed to be integer, and the result was assigned to the center pixel.  ]
+
+% The return array Directy(i,j,1:3) contains NaN within distance 
+% boxRadius from the boundaries.
+% The second return value subpatchesChangeDirecty is only experimental: it
+% is the same size as the input anArray and contain logicals that 
+% indicate whether a consideration of subpatches changes the orientation
+% estimate to a roughly perpendicular direction (which can be the case
+% where the original patch touches two bright foreground objects only at 
+% its boundary). Even if its value is true, the returned Directy is not
+% changed!
+%
+% Input arguments are:
+% - forCenterOfMass [boolean, optional, default=true]: whether to base the
+% ToI on the geometric center [false] or on the center of mass (say, center
+% of intensity) [true]. 
+% - subtractBackground [boolean, optional, default=true]: Whether to 
+% subtract the minimal intensity value occuring in the current box prior to
+% the computation of ToI
+% Recommended when forCenterOfMass=true, as the background intensity does 
+% not cancel out for a point of reference unequal the geometric center
+% - blackBackground [boolean, optional, default=true]: Set true for
+% high-intensity objects on low intensity background, false for the inverse
+%
+% Iteration over the small boxes is with parallelisation (parfor)
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2022-08-25
+%
+% CHANGELOG
+% 2022-08-25 clean-up of experimental subpatchesChangeDirecty
+% 2021-03-05 leave NaN values where no orientation can be computed,
+%            especially at the image boundary (instead of setting all
+%            components of Directy to zero)
+
+%% preparations
+% sanity checks
+assert( size( size(squeeze(anArray)), 2) <=2 , 'Input array of ComputeDirecty must be essentially 2-dimensional at most' )
+assert(boxRadius > 0.5, 'Input boxRadius to ComputeDirecty must be larger than 0.5 to include more than one pixel')
+
+if min(anArray(:)) < 0
+    warning('Negative entries in input array of ComputeDirecty may cause unexpected results')
+end
+
+% set optional arguments if not provided
+if (~exist('forCenterOfMass', 'var'))
+	forCenterOfMass = true;
+end
+
+if (~exist('subtractBackground', 'var'))
+	subtractBackground = true;
+end
+
+if (~exist('blackBackground', 'var'))
+	blackBackground = true;
+end
+
+
+% Prepare a mask of a roughly circular region with radius boxRadius:
+%  Determine where a circle of radius boxRadius would intersect the boundaries of the pixels.
+%  whereby a pixel (i,j) is covering the space [i-0.5,i+0.5]x[j-0.5,j+0.5] in \mathbb{R}^2
+%  If there are intersection points, connect them linearly and compute the area of the resulting polygon
+%  The result is equal in all four quadrants.
+%  prelim_mask is the mask for the upper right quadrant, with prelim_mask(1,1) representing the pixel [-0.5,0.5]x[-0.5,0.5] around the origin
+%  Here, assume the first coordinate (i) to count horizontally and the second (j) to count vertically [this is contrary to MATLAB default]
+
+mask = CircularMask(boxRadius);
+outerBoxRadius = (size(mask,1)-1) /2;   %outerBoxRadius will preselect a square of pixels covering the [roughly] circular mask
+% alternative to restore old behaviour with square image patch:
+%{
+outerBoxRadius = ceil(boxRadius);
+mask  = ones( [2*outerBoxRadius+1, 2*outerBoxRadius+1] );
+%}
+
+
+i_max = size(anArray,1);
+j_max = size(anArray,2);
+Directy = NaN( i_max, j_max, 3 );
+subpatchesChangeDirecty = NaN ( i_max, j_max );
+
+%% computation
+% prepare kernel templates to multiply the actual intensities with
+% distances as row and column vector
+% Recall that array elements are accessed as array(row_number, column_number) in MATLAB, which is 
+% anImage(y,x) for pixel data. Take this into account for the computation of distances
+%   row distances = y-distances
+%   col distances = x-distances
+dist_row_forOrigin =           -outerBoxRadius:outerBoxRadius ;
+dist_col_forOrigin = transpose(-outerBoxRadius:outerBoxRadius);
+template_Ixx_forOrigin = repmat(dist_row_forOrigin .* dist_row_forOrigin, 2*outerBoxRadius+1, 1) ;
+template_Iyy_forOrigin = repmat(dist_col_forOrigin .* dist_col_forOrigin, 1, 2*outerBoxRadius+1) ;
+template_Ixy_forOrigin = - dist_col_forOrigin * dist_row_forOrigin;
+
+% prepare outside parfor loop
+Directy_1 = NaN(i_max,j_max);
+Directy_2 = NaN(i_max,j_max);
+Directy_3 = NaN(i_max,j_max);
+
+% pointwise tensor of inertia eigendecomposition
+parfor i=outerBoxRadius+1:i_max-outerBoxRadius
+    RowOfOrientations_1          = NaN(1,j_max);
+    RowOfOrientations_2          = NaN(1,j_max);
+    RowOfStrenths                = NaN(1,j_max);
+    RowOfsubpatchesChangeDirecty = NaN(1,j_max);
+    for j=outerBoxRadius+1:j_max-outerBoxRadius
+        localArray = anArray(i-outerBoxRadius:i+outerBoxRadius, j-outerBoxRadius:j+outerBoxRadius);
+        
+        if subtractBackground
+            localArray = localArray - min(min(localArray)) ;
+        end
+                
+        % masking
+        maskedArray = mask .* localArray;
+
+        if forCenterOfMass   
+            %compute the tensor of inertia for the center of mass  (which minimizes the eigenvalues, see parallel axis theorem [Steiner'scher Satz])
+            % this option should be combined with "subtractBackground", as the background intensity does not cancel out for point of reference unequal the geometric center
+            totalMass = sum(sum(maskedArray)) ;
+            if totalMass == 0 %if there is no "mass" (image intensity), set the "center of mass" to the geometric center
+                centerOfMass_x = 0; % corresponds to index  outerBoxRadius + 1;
+                centerOfMass_y = 0; % corresponds to index  outerBoxRadius + 1;
+            else
+                centerOfMass_x = sum(sum( maskedArray .* repmat(dist_row_forOrigin , 2*outerBoxRadius+1, 1 ) )) / totalMass ;
+                centerOfMass_y = sum(sum( maskedArray .* repmat(dist_col_forOrigin , 1, 2*outerBoxRadius+1 ) )) / totalMass ;
+            end
+            dist_row_forCoM = dist_row_forOrigin - centerOfMass_x ;
+            dist_col_forCoM = dist_col_forOrigin - centerOfMass_y ;
+            template_Ixx = repmat(dist_row_forCoM .* dist_row_forCoM, 2*outerBoxRadius+1, 1) ;
+            template_Iyy = repmat(dist_col_forCoM .* dist_col_forCoM, 1, 2*outerBoxRadius+1) ;
+            template_Ixy = - dist_col_forCoM * dist_row_forCoM;
+        else    % compute the tensor of inertia for the geometric center
+            template_Ixx = template_Ixx_forOrigin;
+            template_Iyy = template_Iyy_forOrigin;
+            template_Ixy = template_Ixy_forOrigin;
+            % set the "center of mass" to the geometric center
+            centerOfMass_x = 0; % corresponds to index  outerBoxRadius + 1;
+            centerOfMass_y = 0; % corresponds to index  outerBoxRadius + 1;
+            dist_row_forCoM = dist_row_forOrigin - centerOfMass_x ;
+            dist_col_forCoM = dist_col_forOrigin - centerOfMass_y ;            
+        end
+        tensorOfInertia = [  sum(sum(maskedArray .* template_Ixx ))  sum(sum(maskedArray .* template_Ixy ))  ; ...
+                             sum(sum(maskedArray .* template_Ixy ))  sum(sum(maskedArray .* template_Iyy )) ] ;
+        
+        [RightEigVecs, EigValVec, LeftEigVecs] = eig(tensorOfInertia, 'vector'); %the optional argument 'vector' makes the eigenvalues be returned as vector
+        [MaxEigVal, PosMaxEigVal] = max(EigValVec); 
+        EigVecOfMaxEigVal = RightEigVecs(:, PosMaxEigVal);
+        MinEigVal = min(EigValVec);
+        EigVecOfMinEigVal = RightEigVecs(:, 3-PosMaxEigVal); % extract the Eigenvector by the position explicitly computed as "the other position out of the set {1,2}"
+        
+        %{ 
+        %% experimental
+        % decide for each box whether you see bright object on dark background 
+        % or dark "object" an bright background (which may occur
+        % p.e. when the box covers the dark space between two elongated bright objects and a bit of them)
+        % NOTE: This makes additional assumptions on the amount of
+        foreground / background pixels, but the crucial point is their spatial distribution
+        
+        otsuThresh = max(maskedArray(:)) * graythresh( maskedArray/max(maskedArray(:)) );  %threshold from Otsu's method that minimizes intraclass variations - assuming there are bright (foreground) and dark (background) regions
+        if sum( maskedArray(:) > otsuThresh ) > sum( maskedArray(:) < otsuThresh )
+            %there are more pixels above the threshold => rather dark object on bright background
+            blackBackground = 1 - blackBackground;
+        end
+        % end of experimental
+        %}
+        
+        % preliminary Directionality result
+        prelimDirecty = zeros([1,3]);
+        if blackBackground
+            prelimDirecty(1:2) = EigVecOfMaxEigVal(:);  %return a vector orthogonal to the eigenvector of smallest eigenvalue
+                % because of a positive semi-definit, symmetric matrix, the eigenvectors are orthogonal, so we return the eigenvector to the larger eigenvalue          
+        else
+            prelimDirecty(1:2) = EigVecOfMinEigVal(:);  %return a vector orthogonal to the eigenvector of largest eigenvalue
+                % because of a positive semi-definit, symmetric matrix, the eigenvectors are orthogonal, so we return the eigenvector to the smaller eigenvalue
+        end
+        prelimDirecty(3) = (MaxEigVal - MinEigVal) / (MaxEigVal + MinEigVal) ;
+        
+        
+        %% experimental: check whether orientation estimate is testified by subpatches
+        % when splitting the domain orthogonal to prelimDirecty(1:2) - this helps adjust the
+        % Directy for example when intensity is distributed like a bar-bell
+        
+        % splitting orthogonal to prelimDirecty(1:2) by scalar product <0 or >0
+        % NOTE that we have to use a negative sign with the y-component prelimDirecty(1)
+        %mask_part1 = mask .* (0 < -prelimDirecty(1) * repmat(dist_row_forCoM , 2*outerBoxRadius+1, 1 ) + prelimDirecty(2) * repmat(dist_col_forCoM , 1, 2*outerBoxRadius+1 )  );
+        %mask_part2 = mask .* (0 > -prelimDirecty(1) * repmat(dist_row_forCoM , 2*outerBoxRadius+1, 1 ) + prelimDirecty(2) * repmat(dist_col_forCoM , 1, 2*outerBoxRadius+1 )  );
+        % However, the subregions above are anisotropic, which may again introduce biases
+        % Therefore define two roughly circular patches of half the boxRadius
+        % located in each of the two halfs described above
+        submask_offset_y =  -prelimDirecty(1)*0.5*boxRadius;
+        submask_offset_x =   prelimDirecty(2)*0.5*boxRadius;
+        mask_part1 = mask .* ( (0.5*boxRadius)^2 > (repmat(dist_row_forCoM + submask_offset_y , 2*outerBoxRadius+1, 1 )).^2 + ...
+                                                   (repmat(dist_col_forCoM + submask_offset_x , 1, 2*outerBoxRadius+1 )).^2  );
+        mask_part2 = mask .* ( (0.5*boxRadius)^2 > (repmat(dist_row_forCoM - submask_offset_y , 2*outerBoxRadius+1, 1 )).^2 + ...
+                                                   (repmat(dist_col_forCoM - submask_offset_x , 1, 2*outerBoxRadius+1 )).^2  );
+        
+        
+        %% analyse tensor of inertia in each half as above for the full maskedArray
+        maskedArray1 = mask_part1 .* localArray;
+        
+        % in the parts, always work for center of mass
+        %if forCenterOfMass   
+            %compute the tensor of inertia for the center of mass  (which minimizes the eigenvalues, see parallel axis theorem [Steiner'scher Satz])
+            % this option should be combined with "subtractBackground", as the background intensity does not cancel out for point of reference unequal the geometric center
+            totalMass1 = sum(sum(maskedArray1)) ;
+            if totalMass1 == 0 %if there is no mass, set the "center of mass" to the geometric center
+                centerOfMass_x1 = outerBoxRadius + 1;
+                centerOfMass_y1 = outerBoxRadius + 1;
+            else
+                centerOfMass_x1 = sum(sum( maskedArray1 .* repmat(dist_row_forOrigin , 2*outerBoxRadius+1, 1 ) )) / totalMass1 ;
+                centerOfMass_y1 = sum(sum( maskedArray1 .* repmat(dist_col_forOrigin , 1, 2*outerBoxRadius+1 ) )) / totalMass1 ;
+            end
+            dist_row_forCoM1 = dist_row_forOrigin - centerOfMass_x1 ;
+            dist_col_forCoM1 = dist_col_forOrigin - centerOfMass_y1 ;
+            template_Ixx1 = repmat(dist_row_forCoM1 .* dist_row_forCoM1, 2*outerBoxRadius+1, 1) ;
+            template_Iyy1 = repmat(dist_col_forCoM1 .* dist_col_forCoM1, 1, 2*outerBoxRadius+1) ;
+            template_Ixy1 = - dist_col_forCoM1 * dist_row_forCoM1;
+        %else    % compute the tensor of inertia for the geometric center
+        %    template_Ixx = template_Ixx_forOrigin;
+        %    template_Iyy = template_Iyy_forOrigin;
+        %    template_Ixy = template_Ixy_forOrigin;
+        %    % set the "center of mass" to the geometric center
+        %    centerOfMass_x = outerBoxRadius + 1;
+        %    centerOfMass_y = outerBoxRadius + 1;
+        %    dist_row_forCoM = dist_row_forOrigin - centerOfMass_x ;
+        %    dist_col_forCoM = dist_col_forOrigin - centerOfMass_y ;            
+        %end
+        tensorOfInertia1 = [  sum(sum(maskedArray1 .* template_Ixx1 ))  sum(sum(maskedArray1 .* template_Ixy1 ))  ; ...
+                             sum(sum(maskedArray1 .* template_Ixy1 ))  sum(sum(maskedArray1 .* template_Iyy1 )) ] ;
+        
+        [RightEigVecs1, EigValVec1, LeftEigVecs1] = eig(tensorOfInertia1, 'vector'); %the optional argument 'vector' makes the eigenvalues be returned as vector
+        [MaxEigVal1, PosMaxEigVal1] = max(EigValVec1); 
+        EigVecOfMaxEigVal1 = RightEigVecs1(:, PosMaxEigVal1);
+        MinEigVal1 = min(EigValVec1);
+        EigVecOfMinEigVal1 = RightEigVecs1(:, 3-PosMaxEigVal1); % extract the Eigenvector by the position explicitly computed as "the other position out of the set {1,2}"
+        
+        
+        
+        maskedArray2 = mask_part2 .* localArray;
+        
+        % in the parts, always work for center of mass
+        %if forCenterOfMass   
+            %compute the tensor of inertia for the center of mass  (which minimizes the eigenvalues, see parallel axis theorem [Steiner'scher Satz])
+            % this option should be combined with "subtractBackground", as the background intensity does not cancel out for point of reference unequal the geometric center
+            totalMass2 = sum(sum(maskedArray2)) ;
+            if totalMass2 == 0 %if there is no mass, set the "center of mass" to the geometric center
+                centerOfMass_x2 = outerBoxRadius + 1;
+                centerOfMass_y2 = outerBoxRadius + 1;
+            else
+                centerOfMass_x2 = sum(sum( maskedArray2 .* repmat(dist_row_forOrigin , 2*outerBoxRadius+1, 1 ) )) / totalMass2 ;
+                centerOfMass_y2 = sum(sum( maskedArray2 .* repmat(dist_col_forOrigin , 1, 2*outerBoxRadius+1 ) )) / totalMass2 ;
+            end
+            dist_row_forCoM2 = dist_row_forOrigin - centerOfMass_x2 ;
+            dist_col_forCoM2 = dist_col_forOrigin - centerOfMass_y2 ;
+            template_Ixx2 = repmat(dist_row_forCoM2 .* dist_row_forCoM2, 2*outerBoxRadius+1, 1) ;
+            template_Iyy2 = repmat(dist_col_forCoM2 .* dist_col_forCoM2, 1, 2*outerBoxRadius+1) ;
+            template_Ixy2 = - dist_col_forCoM2 * dist_row_forCoM2;
+        %else    % compute the tensor of inertia for the geometric center
+        %    template_Ixx = template_Ixx_forOrigin;
+        %    template_Iyy = template_Iyy_forOrigin;
+        %    template_Ixy = template_Ixy_forOrigin;
+        %    % set the "center of mass" to the geometric center
+        %    centerOfMass_x = outerBoxRadius + 1;
+        %    centerOfMass_y = outerBoxRadius + 1;
+        %    dist_row_forCoM = dist_row_forOrigin - centerOfMass_x ;
+        %    dist_col_forCoM = dist_col_forOrigin - centerOfMass_y ;            
+        %end
+        tensorOfInertia2 = [  sum(sum(maskedArray2 .* template_Ixx2 ))  sum(sum(maskedArray2 .* template_Ixy2 ))  ; ...
+                             sum(sum(maskedArray2 .* template_Ixy2 ))  sum(sum(maskedArray2 .* template_Iyy2 )) ] ;
+        
+        [RightEigVecs2, EigValVec2, LeftEigVecs2] = eig(tensorOfInertia2, 'vector'); %the optional argument 'vector' makes the eigenvalues be returned as vector
+        [MaxEigVal2, PosMaxEigVal2] = max(EigValVec2); 
+        EigVecOfMaxEigVal2 = RightEigVecs2(:, PosMaxEigVal2);
+        MinEigVal2 = min(EigValVec2);
+        EigVecOfMinEigVal2 = RightEigVecs2(:, 3-PosMaxEigVal2); % extract the Eigenvector by the position explicitly computed as "the other position out of the set {1,2}"
+        
+        
+        % check whether the Directy's from the parts coincide with the Directy from the full patch. 
+        % For Comparison use absolute value of the scalar product
+        if  ( abs( EigVecOfMaxEigVal(1)*EigVecOfMaxEigVal1(1) + EigVecOfMaxEigVal(2)*EigVecOfMaxEigVal1(2) ) + ...
+              abs( EigVecOfMaxEigVal(1)*EigVecOfMaxEigVal2(1) + EigVecOfMaxEigVal(2)*EigVecOfMaxEigVal2(2) )   ...
+              > ...
+              abs(-EigVecOfMaxEigVal(1)*EigVecOfMaxEigVal1(2) + EigVecOfMaxEigVal(2)*EigVecOfMaxEigVal1(1) ) + ...  % here take a vector orthogonal to EigVecOfMaxEigVal1 in scalar prduct with prelimDirecty
+              abs(-EigVecOfMaxEigVal(1)*EigVecOfMaxEigVal2(2) + EigVecOfMaxEigVal(2)*EigVecOfMaxEigVal2(1) )  )
+            % in this case, the patches confirm the result of prelimDirecty
+            RowOfsubpatchesChangeDirecty(j) = false;
+            % DO NOT ALTER the assigned orientations
+            %RowOfOrientations_1         (j) = prelimDirecty(1);
+            %RowOfOrientations_2         (j) = prelimDirecty(2);
+        else % the vector orthogonal to prelimDirecty might be a better fit, 
+            RowOfsubpatchesChangeDirecty(j) =  true;
+            % but DO NOT ASSIGN other values to to RowOfOrientations_1 (and thereby to Directy)
+            %RowOfOrientations_1         (j) =  prelimDirecty(2);
+            %RowOfOrientations_2         (j) = -prelimDirecty(1);
+
+        end
+        
+        %% assign orientations and strength 
+        %%% ALWAYS from prelimDirecty irrespective of subpatchesChangeDirecty 
+        %%% no elementwise assignments  %Directy(i, j, :) = prelimDirecty;   inside the parfor loop
+        RowOfOrientations_1 (j) = prelimDirecty(1);
+        RowOfOrientations_2 (j) = prelimDirecty(2);
+        RowOfStrenths       (j) = prelimDirecty(3);
+    end
+    
+    Directy_1(i, :) = RowOfOrientations_1;
+    Directy_2(i, :) = RowOfOrientations_2;
+    Directy_3(i, :) = RowOfStrenths;
+    subpatchesChangeDirecty(i, :) = RowOfsubpatchesChangeDirecty; 
+end
+
+Directy(:,:,1) = Directy_1;
+Directy(:,:,2) = Directy_2;
+Directy(:,:,3) = Directy_3;
+
+
diff --git a/AnalyseOrientations/ComputeDirectyGradient.m b/AnalyseOrientations/ComputeDirectyGradient.m
new file mode 100644
index 0000000..0251f14
--- /dev/null
+++ b/AnalyseOrientations/ComputeDirectyGradient.m
@@ -0,0 +1,118 @@
+function [Directy] = ComputeDirectyGradient(anArray, kernel_size)
+% Compute an orientation estimate (also known as directionality) for input
+% matrix of grayscale image intensities, 
+% using Scharr gradient estimation kernels of sizes 5 [recommended], 3 or 2 
+% that are optimized for low orientational error, see Tabelle B.11 in
+%
+%   Hanno Scharr (2010): Optimale Operatoren in der digitalen
+%   Bildverarbeitung, PhD Thesis at University Heidelberg, Germany,
+%   DOI=10.11588/heidok.00000962, http://www.ub.uni-heidelberg.de/archiv/962 
+%
+% The gradient gives the direction of steepest increase (here: of image
+% intensity), therefore the direction perpendicular to it is returned as
+% orientation of objeccts in the image.
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2022_08_25
+%
+%
+% CHANGELOG
+% 2022_08_25 clean-up
+% 2021_03_15 computation of u and v simplified
+
+
+if min(anArray(:)) < 0
+    warning('Negative entries in input array of ComputeDirecty detected, but gradient estimation not impeded.')
+end
+% do not enforce a normalization here, leave it to higher code levels
+
+image_size = size(anArray);
+   
+if (kernel_size == 5)
+    %{
+    % Compare Directionality plugin in Fiji 
+    filtering_kernel_x = [-2, -1, 0, 1, 2; -3, -2, 0, 2, 3; -4, -3, 0, 3, 4]/46;
+    filtering_kernel_x = [filtering_kernel_x ; flipud(filtering_kernel_x(1:2,:))];
+    filtering_kernel_y = rot90(filtering_kernel_x);
+    %}
+    % Use "5x5-opt" from [Scharr], see Tabelle B.11, and Abbildung 6.2. 
+    % Note error of whopping 6.7e-05 (!)
+    derivative_kernel = [21.27, 85.46,      0, -85.46, -21.27]/256;
+    smoothing_kernel  = [ 5.91, 61.77, 120.64,  61.77,   5.91]/256;
+    filtering_kernel_x= derivative_kernel .* smoothing_kernel' ;
+    filtering_kernel_y = rot90(filtering_kernel_x);
+elseif (kernel_size == 2)
+    % The simplest kernel possible. 
+    % See [Scharr], Tabelle B.11 and Abbildung 6.2 for error of 1.8e-02
+    % We here directly combine derivative ("Ableitung") and smoothing ("Glättung")
+    % NOTE that in the original work, the normalisation factor /2 is missing in the smoothing ("Glättung")
+    filtering_kernel_x = [ 1, -1; 1, -1] /2;
+    filtering_kernel_y = rot90(filtering_kernel_x);
+elseif (kernel_size == 3)
+    % See [Scharr], Tabelle B.11 and Abbildung 6.2 for error of 2.2e-03
+    % both for "3x3-int" (which is optimised for low angle error in fixed point arithmetics)
+    % and for "3x3-opt"  (which is optimised for low angle error in floating point arithmetics)
+    % We here directly combine derivative ("Ableitung") and smoothing ("Glättung")
+    % 3x3-int:  
+    %filtering_kernel_x = [47, 0, -47; 162, 0, -162; 47, 0, -47]/512;
+    % 3x3-opt
+    filtering_kernel_x = [46.84, 0, -46.84; 162.32, 0, -162.32; 46.84, 0, -46.84]/512;
+    filtering_kernel_y = rot90(filtering_kernel_x);
+else
+   error(['Please choose a kernel size 2, 3 or 5. Chosen kernel size was ' mat2str(kernel_size) '.']); 
+end
+    
+
+
+%{
+% the commented version yields derivative approximations even at the very
+% boundary pixels where the filter is not fully within the input image
+convolved_image_x = conv2(anArray, filtering_kernel_x);
+convolved_image_y = conv2(anArray, filtering_kernel_y);
+
+radius_kernel = floor((size(filtering_kernel_x,1) - 1)/2);
+
+%prune
+convolved_image_x = convolved_image_x(radius_kernel + 1 : image_size(1) + radius_kernel, ...
+    radius_kernel + 1 : image_size(2) + radius_kernel);
+convolved_image_y = convolved_image_y(radius_kernel + 1 : image_size(1) + radius_kernel, ...
+    radius_kernel + 1 : image_size(2) + radius_kernel);
+%}
+
+convolved_image_x_oversized = conv2(anArray, filtering_kernel_x);
+convolved_image_y_oversized = conv2(anArray, filtering_kernel_y);
+
+convolved_image_x = NaN(size(anArray));
+convolved_image_y = NaN(size(anArray));
+
+%leave values to NaN where the kernel was not fully within the input image
+kernel_oversize_tl = ceil ((size(filtering_kernel_x,1) - 1)/2); %how many pixels the kernel has to the top or left of its center ("center" = pixel that the filtering result is assigned to)
+kernel_oversize_br = floor((size(filtering_kernel_x,1) - 1)/2);
+
+convolved_image_x(kernel_oversize_tl+1:end-kernel_oversize_br, kernel_oversize_tl+1:end-kernel_oversize_br) = ...
+    convolved_image_x_oversized(kernel_oversize_tl+kernel_oversize_br+1 : end-kernel_oversize_tl-kernel_oversize_br, kernel_oversize_tl+kernel_oversize_br+1 : end-kernel_oversize_tl-kernel_oversize_br);
+convolved_image_y(kernel_oversize_tl+1:end-kernel_oversize_br, kernel_oversize_tl+1:end-kernel_oversize_br) = ...
+    convolved_image_y_oversized(kernel_oversize_tl+kernel_oversize_br+1 : end-kernel_oversize_tl-kernel_oversize_br, kernel_oversize_tl+kernel_oversize_br+1 : end-kernel_oversize_tl-kernel_oversize_br);
+
+
+% L2 norm
+magnitudesL2 = sqrt(convolved_image_x.^2 + convolved_image_y.^2);
+
+% normalized components of the identified direction
+%{
+angles = atan2(convolved_image_y , convolved_image_x);
+orthogonal_angles = angles + pi/2;
+u = cos(orthogonal_angles);
+v = sin(orthogonal_angles);
+%}
+% more direct computation of normalized components u and v
+u = -convolved_image_y ./magnitudesL2 ; %normalization and rotation by pi/2 in one step
+v =  convolved_image_x ./magnitudesL2 ; % therefore "-y" assigned to u, and "+x" assigned to v
+
+Directy(:,:,1) = u;
+Directy(:,:,2) = v;
+Directy(:,:,3) = magnitudesL2;
+
+end
+
diff --git a/AnalyseOrientations/ComputeDirecty_FourierAndQ.m b/AnalyseOrientations/ComputeDirecty_FourierAndQ.m
new file mode 100644
index 0000000..4e18c2d
--- /dev/null
+++ b/AnalyseOrientations/ComputeDirecty_FourierAndQ.m
@@ -0,0 +1,163 @@
+function [Directy, subpatchesChangeDirecty] = ComputeDirecty_FourierAndQ(anArray, boxRadius, minWaveLength, maxWaveLength)
+% Compute an orientation estimate (also known as directionality) for input
+% matrix of grayscale image intensities, 
+% using the Q-tensor decomposition of the Fourier transform on square image
+% patches of user-specified side length 2*boxRadius+1.
+% For input of size (m,n), the return value Directy is of size (m,n,3). 
+% For each pixel (i,j), Directy(i,j,3) contains the strength of directionality 
+% and the corresponding normalised eigenvector is returned in Directy(i,j,1:2)
+% where Directy(i,j,1) is the x-component and Directy(i,j,2) is the y-component
+% in *standard* coordinate layout (while the indexing (i,j) is in MATLAB coordinate layout).
+% 
+% The strength of the orientation is defined as the prefactor S in the
+% Q-tensor decomposition  Q = S * ( n * transpose(n) - 1/2 Id ), which is
+% also known as the scalar nematic order parameter
+% 
+% Arguments: 
+% - boxRadius sets the scale: 
+% The Fourier transform is applied to a box of size 2*boxRadius+1, where boxRadius 
+% needs to be integer, and the result is assigned to the center pixel.
+% - minWaveLength and maxWaveLength restrict to an isotropic fraction of
+% the Fourier space. (If there is no power at all in that part, the
+% returned orientation and strength are NaN)
+%
+%
+% improvement ideas: 
+% 1. The Fourier transform could be applied to a roughly circular image
+%    patch of [not necessarily integer] radius = boxRadius, and the result 
+%    is assigned to the center pixel to reduce bias by the coordinate axis
+%    (already attenuated by integration only between specified wavelengths)
+% 2. Polar Fourier Transform (p.e. PolarLab [Averbuch, Coifman, Donoho,
+%    Elad, Israeli (2004) Fast and accurate polar Fourier transform] or 
+%    NFFT [Keiner, Kunis, Potts (2009) Using NFFT 3 - a software library
+%    for various nonequispaced fast Fourier transforms]) 
+%
+% Iteration over the small boxes is with parallelisation (parfor)
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+
+
+%% preparations
+% sanity checks
+assert( size( size(squeeze(anArray)), 2) <=2 , 'Input array of ComputeDirecty must be essentially 2-dimensional at most' )
+assert(boxRadius > 0 && floor(boxRadius)==boxRadius, 'Input boxRadius to ComputeDirecty must be positive integer')
+
+assert(minWaveLength < maxWaveLength, 'Empty selection of wavelengths to consider')
+if maxWaveLength > 2*boxRadius+1
+    warning('maximal wavelength cannot be reached due to Fourier transform on box of smaller size')
+end
+if not (minWaveLength>0)
+    warning('minimal wavelength should be larger than 0')
+    % wavelength = 0 should be excluded, as otherwise the magnitude of the 
+    % constant term would be assigned to the angle q_theta=0
+    minWaveLength = eps;
+end
+
+i_max = size(anArray,1);
+j_max = size(anArray,2);
+Directy = NaN( i_max, j_max, 3 );
+subpatchesChangeDirecty = NaN( i_max, j_max );
+
+outerBoxRadius = boxRadius;
+
+%% run trial FFT2 on a patch in the middle to find best method, see https://de.mathworks.com/help/matlab/ref/fftw.html
+fftw('dwisdom',[]); %Clear any existing double-precision transform parameters.
+fftw('planner','measure'); %Set the method for optimizing Fourier transforms within the current MATLAB session to 'measure' which tries the common methods (instead of 'exhaustive' which tries ALL methods, but gives only ~5% gain [Peter Steinbach, MPI-CBG Scientific Computing Facility, based on "gearshifft", see https://github.com/mpicbg-scicomp/gearshifft, see also Steinbach, Werner (2017) gearshifft – The FFT Benchmark Suite for Heterogeneous Platforms].
+fft2( anArray( ceil(0.5*i_max)-outerBoxRadius:ceil(0.5*i_max)+outerBoxRadius, ...
+               ceil(0.5*j_max)-outerBoxRadius:ceil(0.5*j_max)+outerBoxRadius) );
+fftinfo = fftw('dwisdom'); %Assign the current double-precision transform algorithm parameters to the variable fftinfo for later use.
+fftw('dwisdom',fftinfo); %Apply the parameter information stored in the variable fftinfo to future transform function calls.
+
+%% prepare frequencies corresponding to the entries in the Fourier transform - note 0-based indexing of frequencies
+% the calculation of q_x, q_y, q_theta, q_radius  was moved out of the loop, 
+% because the size of the localArray is the same in each pass
+[q_x, q_y] = meshgrid( 0:2*outerBoxRadius, 0:2*outerBoxRadius );  
+% shift q_x and q_y to accomodate the layout of fft2 in MATLAB
+q_x = q_x - (q_x > 0.5*size(q_x,2)-0.25 ) *  size(q_x,2); % negative frequencies are equivalent to frequencies > half domain size
+q_y = q_y - (q_y > 0.5*size(q_y,1)-0.25 ) *  size(q_y,1); % adjust here to get the correct q_theta from atan2( )
+    % the -0.25 is just to have the condition "q_x >= 0.5*size(q_x,2)" robust to roundoff errors
+% express frequency in polar coordinates (q_radius, q_theta)
+q_radius= sqrt(q_y.^2 + q_x.^2) ;
+q_theta = mod( atan2(q_y, q_x), 2*pi);  %force to interval [0, 2*pi)
+%{
+%check layout of q_theta:
+imagesc(fftshift(q_theta))
+%}
+
+
+%% computation
+Directy_1 = NaN(i_max,j_max);
+Directy_2 = NaN(i_max,j_max);
+Directy_3 = NaN(i_max,j_max);
+
+parfor ii=outerBoxRadius+1:i_max-outerBoxRadius
+    RowOfOrientations_1 = NaN(1,j_max);
+    RowOfOrientations_2 = NaN(1,j_max);
+    RowOfStrenths       = NaN(1,j_max);
+    for jj=outerBoxRadius+1:j_max-outerBoxRadius
+        localArray = anArray(ii-outerBoxRadius:ii+outerBoxRadius, jj-outerBoxRadius:jj+outerBoxRadius);
+        
+        %Fourier transform
+        localArray_hat = fft2( localArray );
+        
+        
+        %% Q-tensor calculation
+        %  might be further improved by a polar Fourier transform (see refs. at header) 
+        
+        % take only points into account within the given wavenumber limits
+        
+        % the frequencies' components q_x, q_y and radial form (q_theta, q_radius) have been computed outside the loop
+        
+        % normalise the power (or the absolute value of the Fourier transform) to a distribution
+        power_to_consider = abs( localArray_hat )     .*  ( (q_radius > minWaveLength) & (q_radius < maxWaveLength)) ;
+        %power_to_consider = abs( localArray_hat ) .^2 .*  ( (q_radius > minWaveLength) & (q_radius < maxWaveLength)) ;
+        
+        % turn it into a distribution from which we compute the Q-tensor (= the second moment tensor of the distribution, except for -0.5*unitmatrix)
+        power_to_consider = power_to_consider / sum(power_to_consider(:));
+        
+        Q_xx =  sum(sum( ( cos(q_theta).^2  - 0.5 )      .*  power_to_consider  )) ;
+        Q_xy =  sum(sum(  cos(q_theta)  .* sin(q_theta)  .*  power_to_consider  )) ;        
+        
+        
+        %% eigen decomposition of the Q-tensor [Q_xx Q_xy ; Q_xy Q_yy]  where Q_yy = - Q_xx
+        % eigenvalues are  \pm \sqrt{ Q_{xx}^2 + Q_{xy}^2 }
+        %eigVal_large =   sqrt(Q_xx ^2 + Q_xy ^2) ;
+        %eigVal_small = - sqrt(Q_xx ^2 + Q_xy ^2) ;
+        eigVec_small = [ -Q_xy ; Q_xx+sqrt(Q_xx^2+Q_xy^2) ];
+        if  (Q_xy==0 && Q_xx<0)  %arithmetically, this case is equal to eigVec_small == [ 0; Q_xx+sqrt(Q_xx^2)] == [0 ; 0] for Q_xx<0  
+            % but there might be roundoff errors
+            eigVec_small = [ 1 ; 0 ];
+        else
+            eigVec_small = eigVec_small / norm(eigVec_small) ;
+        end
+        %eigVec_large = [ eigVec_small(2) ; -eigVec_small(1) ] ;
+        
+        strength = 2 * sqrt( Q_xx^2 + Q_xy^2 ); % the scalar nematic order parameter
+        
+        orientation = [eigVec_small(1)  -eigVec_small(2)]; % strongest fluctuations are into direction of eigVec_large, 
+          % therefore Directy is orthogonal to that ( = eigVec_small)
+          % however, this is in the MATLAB coordinate system (x from left to right, y from top to bottom)
+          % to fit the normal coordinate system (x from left to right, y from bottom to top ) we need to inverse the y-component
+          
+        %%% no elementwise assignments inside the parfor loop
+        %Directy(ii,jj,1:2) = orientation;  
+        %Directy(ii,jj,3) = strength;
+        %Directy(ii,jj,:) = [orientation  strength];
+        
+        RowOfOrientations_1 (jj) = orientation(1);
+        RowOfOrientations_2 (jj) = orientation(2);
+        RowOfStrenths       (jj) = strength;
+            
+    end
+    Directy_1(ii, :) = RowOfOrientations_1;
+    Directy_2(ii, :) = RowOfOrientations_2;
+    Directy_3(ii, :) = RowOfStrenths;
+    
+end
+Directy(:,:,1) = Directy_1;
+Directy(:,:,2) = Directy_2;
+Directy(:,:,3) = Directy_3;
+
+
+
diff --git a/AnalyseOrientations/ComputeLocalAlignment.m b/AnalyseOrientations/ComputeLocalAlignment.m
new file mode 100644
index 0000000..4a5db4f
--- /dev/null
+++ b/AnalyseOrientations/ComputeLocalAlignment.m
@@ -0,0 +1,52 @@
+function [dominantOrientationField, coherencyField] = ComputeLocalAlignment(theta, magn, boxRadius_LocalAlignment, powerOfMagn_LocalAlignment)
+% Compute local degree of alignment (also known as coherency) and locally 
+% dominant orientations for orientation field given by magnitude magn and
+% angle theta. Calculation is within square subpatches of side length
+% 1+2*boxRadius, with result assigned to the centeer
+%
+% Iteration over the subpatches is with parallelisation (parfor)
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2020-10-05
+
+%% preparations
+% sanity checks
+assert( all(size(theta) == size(magn)) , 'Input array of ComputeLocalAlignment must be of equal size' )
+assert(boxRadius_LocalAlignment > 0 && floor(boxRadius_LocalAlignment)==boxRadius_LocalAlignment, 'Input boxRadius to ComputeLocalAlignment must be positive integer')
+
+% set optional arguments if not provided
+if (~exist('PowerOfMagn', 'var'))
+	powerOfMagn_LocalAlignment = 1;
+end
+
+% prepare output arrays 
+i_max = size(theta,1);
+j_max = size(theta,2);
+dominantOrientationField   = zeros( i_max, j_max, 2 );
+dominantOrientationField_1 = zeros( i_max, j_max );
+dominantOrientationField_2 = zeros( i_max, j_max );
+coherencyField             = zeros( i_max, j_max );
+
+%% calculation
+parfor ii=boxRadius_LocalAlignment+1:i_max-boxRadius_LocalAlignment
+    dominantOrientationField_1_Row = zeros(1,j_max);
+    dominantOrientationField_2_Row = zeros(1,j_max);
+    coherencyField_Row             = zeros(1,j_max);
+    for jj=boxRadius_LocalAlignment+1:j_max-boxRadius_LocalAlignment
+        %[ dominantOrientationField(ii,jj,:) , coherencyField(ii,jj) ] = ...
+        [ dominantOrientation , coherency ] = ...
+            ComputeQTensor( theta(ii-boxRadius_LocalAlignment:ii+boxRadius_LocalAlignment, jj-boxRadius_LocalAlignment:jj+boxRadius_LocalAlignment), ...
+                            magn (ii-boxRadius_LocalAlignment:ii+boxRadius_LocalAlignment, jj-boxRadius_LocalAlignment:jj+boxRadius_LocalAlignment), ...
+                            powerOfMagn_LocalAlignment ) ;
+        dominantOrientationField_1_Row(jj) = dominantOrientation(1);
+        dominantOrientationField_2_Row(jj) = dominantOrientation(2);
+        coherencyField_Row(jj)             = coherency;
+    end
+    dominantOrientationField_1(ii,:) = dominantOrientationField_1_Row;
+    dominantOrientationField_2(ii,:) = dominantOrientationField_2_Row;
+    coherencyField(ii,:)             = coherencyField_Row;
+end
+
+dominantOrientationField(:,:,1) = dominantOrientationField_1;
+dominantOrientationField(:,:,2) = dominantOrientationField_2;
diff --git a/AnalyseOrientations/ComputeQTensor.m b/AnalyseOrientations/ComputeQTensor.m
new file mode 100644
index 0000000..ac41122
--- /dev/null
+++ b/AnalyseOrientations/ComputeQTensor.m
@@ -0,0 +1,41 @@
+function [dominantOrientation, coherency] = ComputeQTensor(theta, magn, PowerOfMagn)
+% Compute the Q-tensor of a set of orientations, given as magnitude magn
+% and angle theta. The function is similar to ComputeDirecty_FourierAndQ.m
+% but considers the complete input arrays rather than sliding subpatches of
+% defined size. 
+% The shape of the input does *not* matter, and NaN values are ignored (the
+% output is NaN only when all input is NaN)
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2019-04-26
+
+
+%% use magn to form a distribution (called  power_to_consider  like in ComputeDirecty_FourierAndQ.m)
+power_to_consider = magn .^ PowerOfMagn ;
+% turn it into a distribution from which we compute the Q-tensor (= the second moment tensor of the distribution, except for -0.5*unitmatrix)
+power_to_consider = power_to_consider / nansum(power_to_consider(:)) ;  %normalise
+
+Q_xx =  nansum(nansum( ( cos(theta).^2  - 0.5 )      .*  power_to_consider  )) ;
+Q_xy =  nansum(nansum(  cos(theta)  .* sin(theta)  .*  power_to_consider  )) ;
+
+
+%% eigen decomposition of the Q-tensor [Q_xx Q_xy ; Q_xy Q_yy]  where Q_yy = - Q_xx
+% eigenvalues are  \pm \sqrt{ Q_{xx}^2 + Q_{xy}^2 }
+%eigVal_large =   sqrt(Q_xx ^2 + Q_xy ^2) ;
+%eigVal_small = - sqrt(Q_xx ^2 + Q_xy ^2) ;
+eigVec_small = [ -Q_xy ; Q_xx+sqrt(Q_xx^2+Q_xy^2) ];
+if  (Q_xy==0 && Q_xx<0)  %arithmetically, this case is equal to eigVec_small == [ 0; Q_xx+sqrt(Q_xx^2)] == [0 ; 0] for Q_xx<0
+    % but there might be roundoff errors
+    eigVec_small = [ 1 ; 0 ];
+else
+    eigVec_small = eigVec_small / norm(eigVec_small) ;
+end
+%eigVec_large = [ eigVec_small(2) ; -eigVec_small(1) ] ;
+
+strength = 2 * sqrt( Q_xx^2 + Q_xy^2 ); %
+
+dominantOrientation = [ eigVec_small(2) ; -eigVec_small(1) ] ;
+%dominantOrientation = eigVec_large;
+
+coherency = strength;
\ No newline at end of file
diff --git a/AnalyseOrientations/LICENSE_distinguishable_colors b/AnalyseOrientations/LICENSE_distinguishable_colors
new file mode 100644
index 0000000..63168b1
--- /dev/null
+++ b/AnalyseOrientations/LICENSE_distinguishable_colors
@@ -0,0 +1,24 @@
+Copyright (c) 2010-2011, Tim Holy
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in
+      the documentation and/or other materials provided with the distribution
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/AnalyseOrientations/LICENSE_tight_subplot b/AnalyseOrientations/LICENSE_tight_subplot
new file mode 100644
index 0000000..342178c
--- /dev/null
+++ b/AnalyseOrientations/LICENSE_tight_subplot
@@ -0,0 +1,24 @@
+Copyright (c) 2016, Pekka Kumpulainen
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in
+      the documentation and/or other materials provided with the distribution
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/AnalyseOrientations/conv2in2or3D.m b/AnalyseOrientations/conv2in2or3D.m
new file mode 100644
index 0000000..3bab248
--- /dev/null
+++ b/AnalyseOrientations/conv2in2or3D.m
@@ -0,0 +1,25 @@
+function convolved = conv2in2or3D(array, kernel, optargs)
+% apply 2D convolution to each of the slices of a 3D array
+%
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% file generated: 2021-07-08
+% last edit: 2021-07-08
+
+
+% for preallocation, run the convolution once for the first slice
+convolved_slice1 = conv2( array(:,:,1), kernel, optargs) ;
+
+NumSlices = size(array,3);
+% if there are more than one slice, preallocate result array and perform
+% convolution in each of the remaining slices
+if NumSlices > 1
+    convolved = zeros( [size(convolved_slice1) NumSlices] );
+    convolved(:,:,1) = convolved_slice1;
+    for kk = 2:NumSlices
+         convolved(:,:,kk) = conv2( array(:,:,kk), kernel, optargs) ;
+    end
+else
+    convolved = convolved_slice1;
+end
diff --git a/AnalyseOrientations/distinguishable_colors.m b/AnalyseOrientations/distinguishable_colors.m
new file mode 100644
index 0000000..f9e9e25
--- /dev/null
+++ b/AnalyseOrientations/distinguishable_colors.m
@@ -0,0 +1,152 @@
+function colors = distinguishable_colors(n_colors,bg,func)
+% DISTINGUISHABLE_COLORS: pick colors that are maximally perceptually distinct
+%
+% When plotting a set of lines, you may want to distinguish them by color.
+% By default, Matlab chooses a small set of colors and cycles among them,
+% and so if you have more than a few lines there will be confusion about
+% which line is which. To fix this problem, one would want to be able to
+% pick a much larger set of distinct colors, where the number of colors
+% equals or exceeds the number of lines you want to plot. Because our
+% ability to distinguish among colors has limits, one should choose these
+% colors to be "maximally perceptually distinguishable."
+%
+% This function generates a set of colors which are distinguishable
+% by reference to the "Lab" color space, which more closely matches
+% human color perception than RGB. Given an initial large list of possible
+% colors, it iteratively chooses the entry in the list that is farthest (in
+% Lab space) from all previously-chosen entries. While this "greedy"
+% algorithm does not yield a global maximum, it is simple and efficient.
+% Moreover, the sequence of colors is consistent no matter how many you
+% request, which facilitates the users' ability to learn the color order
+% and avoids major changes in the appearance of plots when adding or
+% removing lines.
+%
+% Syntax:
+%   colors = distinguishable_colors(n_colors)
+% Specify the number of colors you want as a scalar, n_colors. This will
+% generate an n_colors-by-3 matrix, each row representing an RGB
+% color triple. If you don't precisely know how many you will need in
+% advance, there is no harm (other than execution time) in specifying
+% slightly more than you think you will need.
+%
+%   colors = distinguishable_colors(n_colors,bg)
+% This syntax allows you to specify the background color, to make sure that
+% your colors are also distinguishable from the background. Default value
+% is white. bg may be specified as an RGB triple or as one of the standard
+% "ColorSpec" strings. You can even specify multiple colors:
+%     bg = {'w','k'}
+% or
+%     bg = [1 1 1; 0 0 0]
+% will only produce colors that are distinguishable from both white and
+% black.
+%
+%   colors = distinguishable_colors(n_colors,bg,rgb2labfunc)
+% By default, distinguishable_colors uses the image processing toolbox's
+% color conversion functions makecform and applycform. Alternatively, you
+% can supply your own color conversion function.
+%
+% Example:
+%   c = distinguishable_colors(25);
+%   figure
+%   image(reshape(c,[1 size(c)]))
+%
+% Example using the file exchange's 'colorspace':
+%   func = @(x) colorspace('RGB->Lab',x);
+%   c = distinguishable_colors(25,'w',func);
+
+% Copyright 2010-2011 by Timothy E. Holy
+
+  % Parse the inputs
+  if (nargin < 2)
+    bg = [1 1 1];  % default white background
+  else
+    if iscell(bg)
+      % User specified a list of colors as a cell aray
+      bgc = bg;
+      for i = 1:length(bgc)
+	bgc{i} = parsecolor(bgc{i});
+      end
+      bg = cat(1,bgc{:});
+    else
+      % User specified a numeric array of colors (n-by-3)
+      bg = parsecolor(bg);
+    end
+  end
+  
+  % Generate a sizable number of RGB triples. This represents our space of
+  % possible choices. By starting in RGB space, we ensure that all of the
+  % colors can be generated by the monitor.
+  n_grid = 30;  % number of grid divisions along each axis in RGB space
+  x = linspace(0,1,n_grid);
+  [R,G,B] = ndgrid(x,x,x);
+  rgb = [R(:) G(:) B(:)];
+  if (n_colors > size(rgb,1)/3)
+    error('You can''t readily distinguish that many colors');
+  end
+  
+  % Convert to Lab color space, which more closely represents human
+  % perception
+  if (nargin > 2)
+    lab = func(rgb);
+    bglab = func(bg);
+  else
+    C = makecform('srgb2lab');
+    lab = applycform(rgb,C);
+    bglab = applycform(bg,C);
+  end
+
+  % If the user specified multiple background colors, compute distances
+  % from the candidate colors to the background colors
+  mindist2 = inf(size(rgb,1),1);
+  for i = 1:size(bglab,1)-1
+    dX = bsxfun(@minus,lab,bglab(i,:)); % displacement all colors from bg
+    dist2 = sum(dX.^2,2);  % square distance
+    mindist2 = min(dist2,mindist2);  % dist2 to closest previously-chosen color
+  end
+  
+  % Iteratively pick the color that maximizes the distance to the nearest
+  % already-picked color
+  colors = zeros(n_colors,3);
+  lastlab = bglab(end,:);   % initialize by making the "previous" color equal to background
+  for i = 1:n_colors
+    dX = bsxfun(@minus,lab,lastlab); % displacement of last from all colors on list
+    dist2 = sum(dX.^2,2);  % square distance
+    mindist2 = min(dist2,mindist2);  % dist2 to closest previously-chosen color
+    [~,index] = max(mindist2);  % find the entry farthest from all previously-chosen colors
+    colors(i,:) = rgb(index,:);  % save for output
+    lastlab = lab(index,:);  % prepare for next iteration
+  end
+end
+
+function c = parsecolor(s)
+  if ischar(s)
+    c = colorstr2rgb(s);
+  elseif isnumeric(s) && size(s,2) == 3
+    c = s;
+  else
+    error('MATLAB:InvalidColorSpec','Color specification cannot be parsed.');
+  end
+end
+
+function c = colorstr2rgb(c)
+  % Convert a color string to an RGB value.
+  % This is cribbed from Matlab's whitebg function.
+  % Why don't they make this a stand-alone function?
+  rgbspec = [1 0 0;0 1 0;0 0 1;1 1 1;0 1 1;1 0 1;1 1 0;0 0 0];
+  cspec = 'rgbwcmyk';
+  k = find(cspec==c(1));
+  if isempty(k)
+    error('MATLAB:InvalidColorString','Unknown color string.');
+  end
+  if k~=3 || length(c)==1,
+    c = rgbspec(k,:);
+  elseif length(c)>2,
+    if strcmpi(c(1:3),'bla')
+      c = [0 0 0];
+    elseif strcmpi(c(1:3),'blu')
+      c = [0 0 1];
+    else
+      error('MATLAB:UnknownColorString', 'Unknown color string.');
+    end
+  end
+end
diff --git a/AnalyseOrientations/downSamplDirecty_ThetaStrength.m b/AnalyseOrientations/downSamplDirecty_ThetaStrength.m
new file mode 100644
index 0000000..08ef5fa
--- /dev/null
+++ b/AnalyseOrientations/downSamplDirecty_ThetaStrength.m
@@ -0,0 +1,74 @@
+function [theta_downSampl, strength_downSampl] = downSamplDirecty_ThetaStrength(theta, strength, downSamplFactor, skipRows, skipCols, strength_exponent)
+% Downsampling of 2D or 3D orientation field (also known as directionality
+% field) in the FIRST TWO dimensions,
+% based on averaging in the domain of doubled angle, which is appropriate 
+% for orientations with their periodicity of pi (180 degree)
+% Input and output are of the form (angle, strength).
+% [For input in the form (x_component, y_component) a simple convolution
+% conv2( <x or y>, 1/(downSamplFactor^2)*ones(downSamplFactor) ) suffices.]
+%
+% inputs:
+% 1.equal-size scalar fields theta and strength give the orientation angle, 
+%   and a strength of the orientation. Strength must be nonnegative. 
+%   strength 0 can be used to mask out some data.
+% 2.downSamplFactor - blocks of square size downSamplFactor are used
+% 3.optional scalar values skipRows and skipCols (default: 0). The first
+%   block starts after this number of rows (skipRows) and columns
+%   (skipCols). 
+% 4.optional scalar value strength_exponent controls the weight of strength 
+%   for the averaging within each block. Typically either 1 or 2 [from what
+%   is found in the literature].  Default is 1.
+%
+% Parts of the input array, that do not fill a complete block at the last
+% rows and/or columns, are dropped. Hence the size of the output array is
+%   floor(  (size(input arrays)-[skipRows skipCols] )/downSamplFactor )
+%    
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% generated: 2021-03-26
+% last edit: 2022-08-25
+
+% CHANGELOG
+% 2021-07-08 include conv2in2or3D to allow 3d arrays to be processed
+
+
+%% preparations
+% sanity checks
+assert(all(size(theta)==size(strength)), 'Input fields "theta" and "strength" must have equal shape')
+assert(int8(downSamplFactor) == downSamplFactor, 'Input "downSamplFactor" must be integer')
+
+% set optional arguments if not provided
+if (~exist('skipRows', 'var'))
+	skipRows = 0;
+else
+    assert(int8(skipRows) == skipRows  &&  skipRows > 0, 'Input "skipRows" must be non-negative integer')
+end
+
+if (~exist('skipCols', 'var'))
+	skipCols = 0;
+else
+    assert(int8(skipCols) == skipCols  &&  skipCols > 0, 'Input "skipCols" must be non-negative integer')
+end
+
+if (~exist('strength_exponent', 'var'))
+	strength_exponent = 1;
+end
+
+%% calculations
+% convert to u and v (Cartesian coordinates)
+prefactor = strength.^strength_exponent;
+u_doubleAngle = prefactor .* cos( 2*theta );
+v_doubleAngle = prefactor .* sin( 2*theta );
+
+u_doubleAngle_blocks = 1/(downSamplFactor^2) ...
+    * conv2in2or3D(u_doubleAngle, ones(downSamplFactor), 'valid') ;
+v_doubleAngle_blocks = 1/(downSamplFactor^2) ...
+    * conv2in2or3D(v_doubleAngle, ones(downSamplFactor), 'valid') ;
+
+u_doubleAngle_downSampl = u_doubleAngle_blocks(1+skipRows:downSamplFactor:end, 1+skipCols:downSamplFactor:end, :);
+v_doubleAngle_downSampl = v_doubleAngle_blocks(1+skipRows:downSamplFactor:end, 1+skipCols:downSamplFactor:end, :);
+
+% transform back into strength and angle theta
+strength_downSampl = (u_doubleAngle_downSampl .^2 + v_doubleAngle_downSampl .^2) .^0.5 ;
+theta_downSampl    = 0.5*atan2( v_doubleAngle_downSampl, u_doubleAngle_downSampl );
diff --git a/AnalyseOrientations/loadGrayStack.m b/AnalyseOrientations/loadGrayStack.m
new file mode 100644
index 0000000..42d33d6
--- /dev/null
+++ b/AnalyseOrientations/loadGrayStack.m
@@ -0,0 +1,23 @@
+function [InfoOnImage, ImageStack] = loadGrayStack(tifPath)
+% function for loading 2D or 3D grayscale images from tif files
+% argument tifPath = the path of the image stack .tif file, in string form. 
+% Example use:
+% [InfoOnImage, ImageStack] = loadGrayStack('/Users/khoffman/Documents/ImageStack.tif')
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2018-03-29
+
+InfoOnImage=imfinfo(tifPath);
+mImage=InfoOnImage(1).Height;
+nImage=InfoOnImage(1).Width;
+NumberImages=length(InfoOnImage);
+ImageStack=zeros(mImage,nImage,NumberImages,'double');
+ 
+for i=1:NumberImages
+	ImageStack(:,:,i)=imread(tifPath,'Index',i,'Info',InfoOnImage);
+end
+
+end
+% Codes modified from https://blogs.mathworks.com/steve/2009/04/02/matlab-r2009a-imread-and-multipage-tiffs/
+% Consider also http://www.matlabtips.com/how-to-load-tiff-stacks-fast-really-fast/
diff --git a/AnalyseOrientations/smoothDirecty_ThetaStrength.m b/AnalyseOrientations/smoothDirecty_ThetaStrength.m
new file mode 100644
index 0000000..856b98e
--- /dev/null
+++ b/AnalyseOrientations/smoothDirecty_ThetaStrength.m
@@ -0,0 +1,75 @@
+function [theta_smoothed, strength_smoothed] = smoothDirecty_ThetaStrength(theta, strength, sigma, strength_exponent, padding)
+% Smoothing of orientation fields (also known as directionality fields)
+% in the domain of doubled angle, which is appropriate for orientations
+% with their periodicity of pi (180 degree)
+% Generally, any smoothing would work. Here, a Gaussian is implemented.
+% Input and output are of the form (angle, strength).
+% For input in the form (x_component, y_component) use smoothDirecty_uANDv
+%
+% inputs:
+% 1.equal-size scalar fields theta and strength give the orientation angle, 
+%   and a strength of the orientation. Strength must be nonnegative. 
+%   strength 0 can be used to mask out some data.
+% 2.non-negative sigma = standard deviation of a Gaussian to be used
+%   for smoothing. Scalar for isotropic blur or vector of 2 (or 3) nonnegative
+%   entries for 2- (or 3-) dimensional input fields
+% 3.optional scalar value strength_exponent. Typically either 1 or 2 [from
+%   what is found in the literature]
+%   Default is 1
+% 4.optional string padding. This is passed on to the Gaussian blur.
+%   Default is 'replicate'
+%
+% For each pixel, the input is converted to vector representing the doubled angle as
+%     strength.^strength_exponent .* cos( 2*theta )
+%     strength.^strength_exponent .* sin( 2*theta )
+% which is smoothed with a Gaussian of standard deviation sigma, and
+% converted back to orientation angle theta_smoothed and strength_smoothed
+%
+% Karl Hoffmann, Max Planck Institute of Molecular Cell Biology and
+% Genetics, Dresden, Germany
+% last edit: 2018-11-19
+
+
+%% preparations
+% sanity checks
+assert(all(size(theta)==size(strength)), 'Input fields "theta" and "strength" must have equal shape')
+assert(isscalar(sigma)  ||  size(size(theta),2) == size(size(sigma),2)  ,'Input "sigma" must be scalar or fit the dimensionality of the fields')
+assert(min(sigma) >= 0.0, 'Input "sigma" must be non-negative' )
+
+% set optional arguments if not provided
+if (~exist('padding', 'var'))
+	padding = 'replicate';
+end
+
+if (~exist('strength_exponent', 'var'))
+	strength_exponent = 1;
+end
+
+%% calculations
+if min(sigma) > 0
+    prefactor = strength.^strength_exponent;
+    u_doubleAngle = prefactor .* cos( 2*theta );
+    v_doubleAngle = prefactor .* sin( 2*theta );
+    
+    %smoothing itself (other than Gaussian would work as well)
+    if size(size(theta),2) == 2 % two-dimensional field
+        u_doubleAngle_smoothed = imgaussfilt(u_doubleAngle, sigma, 'FilterDomain','spatial', 'Padding', padding );
+        v_doubleAngle_smoothed = imgaussfilt(v_doubleAngle, sigma, 'FilterDomain','spatial', 'Padding', padding );
+    else
+        u_doubleAngle_smoothed = imgaussfilt3(u_doubleAngle, sigma, 'FilterDomain','spatial', 'Padding', padding );
+        v_doubleAngle_smoothed = imgaussfilt3(v_doubleAngle, sigma, 'FilterDomain','spatial', 'Padding', padding );
+    end
+    
+    % transform back into strength and angle theta
+    strength_smoothed = (u_doubleAngle_smoothed .^2 + v_doubleAngle_smoothed .^2) .^0.5 ;
+    theta_smoothed    = 0.5*atan2( v_doubleAngle_smoothed, u_doubleAngle_smoothed );
+    
+else % no smoothing
+    strength_smoothed = strength;
+    theta_smoothed    = theta;
+end
+
+end
+
+
+
diff --git a/AnalyseOrientations/tight_subplot.m b/AnalyseOrientations/tight_subplot.m
new file mode 100644
index 0000000..73e0d1f
--- /dev/null
+++ b/AnalyseOrientations/tight_subplot.m
@@ -0,0 +1,66 @@
+function [ha, pos] = tight_subplot(Nh, Nw, gap, marg_h, marg_w)
+
+% tight_subplot creates "subplot" axes with adjustable gaps and margins
+%
+% [ha, pos] = tight_subplot(Nh, Nw, gap, marg_h, marg_w)
+%
+%   in:  Nh      number of axes in hight (vertical direction)
+%        Nw      number of axes in width (horizontaldirection)
+%        gap     gaps between the axes in normalized units (0...1)
+%                   or [gap_h gap_w] for different gaps in height and width 
+%        marg_h  margins in height in normalized units (0...1)
+%                   or [lower upper] for different lower and upper margins 
+%        marg_w  margins in width in normalized units (0...1)
+%                   or [left right] for different left and right margins 
+%
+%  out:  ha     array of handles of the axes objects
+%                   starting from upper left corner, going row-wise as in
+%                   subplot
+%        pos    positions of the axes objects
+%
+%  Example: ha = tight_subplot(3,2,[.01 .03],[.1 .01],[.01 .01])
+%           for ii = 1:6; axes(ha(ii)); plot(randn(10,ii)); end
+%           set(ha(1:4),'XTickLabel',''); set(ha,'YTickLabel','')
+
+% Pekka Kumpulainen 21.5.2012   @tut.fi
+% Tampere University of Technology / Automation Science and Engineering
+
+
+if nargin<3; gap = .02; end
+if nargin<4 || isempty(marg_h); marg_h = .05; end
+if nargin<5; marg_w = .05; end
+
+if numel(gap)==1; 
+    gap = [gap gap];
+end
+if numel(marg_w)==1; 
+    marg_w = [marg_w marg_w];
+end
+if numel(marg_h)==1; 
+    marg_h = [marg_h marg_h];
+end
+
+axh = (1-sum(marg_h)-(Nh-1)*gap(1))/Nh; 
+axw = (1-sum(marg_w)-(Nw-1)*gap(2))/Nw;
+
+py = 1-marg_h(2)-axh; 
+
+% ha = zeros(Nh*Nw,1);
+ii = 0;
+for ih = 1:Nh
+    px = marg_w(1);
+    
+    for ix = 1:Nw
+        ii = ii+1;
+        ha(ii) = axes('Units','normalized', ...
+            'Position',[px py axw axh], ...
+            'XTickLabel','', ...
+            'YTickLabel','');
+        px = px+axw+gap(2);
+    end
+    py = py-axh-gap(1);
+end
+if nargout > 1
+    pos = get(ha,'Position');
+end
+ha = ha(:);
diff --git a/LICENSE b/LICENSE
index 4e28ecd..67203fa 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2021, Scientific Computing
+Copyright (c) 2021, Scientific Computing, and 2020-2022, Karl Hoffmann
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/README.md b/README.md
index a4bee1f..d03d6fd 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,20 @@ This repository contains the scripts and documentation associated to the publica
 **Matrix topology guides collective cell migration in vivo**  
 Karen Grace Soans, Ana Patricia Ramos, Jaydeep Sidhaye, Abhijeet Krishna, Anastasia Solomatina, Karl Hoffmann, Raimund Schlüßler, Jochen Guck, Ivo. F Sbalzarini, Carl Modes, Caren Norden
 
-## Script
+## Scripts
 **`extractDerotatedSurface_v1.0_threshHuang.py`**
 
 Goal: Extract a projection of a tilted surface. This Fiji script extracts a projection of a tilted surface of derotated selected regions of the image (optical cup) to align the surface as much as possible with the xy plane. The surface is then extracted (height map + reslicing) and max-projected.
 
+**`AnalyseOrientations/AnalyseOpticalCupLamininOrientation.m`**
+
+Goal: Estimate locally dominant orientation of fibrous structures in two-dimensional images, and calculate their coherency (i.e. degree of alignment) both locally in a sliding window and globally across the foreground in each region of interest. 
+The script loops over two-dimensional images organised in a folder structure `head_folder/<Stage>/<Embryo>/<RegionOfTheEye>/<DerotationConditions>/Roi<nn>_ZMAX_of_Resliced_of_Derotated_with2ndCrop.tif` (with each single image obtained by `extractDerotatedSurface_v1.0_threshHuang.py` for the publication), and collects statistics across them.
+
+`Code related to the spring network`
+
+is available from https://git.mpi-cbg.de/krishna/laminin_network.
+
 ## Usage & Disclaimer
 The scripts are provided as is, without the intention that an interested reader will be able to run them directly. The authors share their scripts here, not a ready-to-use tool. Still, they should provide enough technical detail to allow replicating the analyses.
 
@@ -16,7 +25,7 @@ The scripts are provided as is, without the intention that an interested reader
 The scripts are provided as is under [BSD3 License](https://opensource.org/licenses/BSD-3-Clause).
 
 ## Support
-For further support please contact [Norden Lab](https://gulbenkian.pt/ciencia/research-groups/cnorden/) (mail: cnorden@igc.gulbenkian.pt) or Scientific Computing Facility (mail: scicomp@mpi-cbg.de).
+For further support please contact [Norden Lab](https://gulbenkian.pt/ciencia/research-groups/cnorden/) (mail: cnorden@igc.gulbenkian.pt), Karl Hoffmann (mail: karlhoff@mpi-cbg.de) or Scientific Computing Facility (mail: scicomp@mpi-cbg.de).
 
 ## DOI
 This repository is resolvable via:  https://dx.doi.org/21.11101/0000-0007-F43E-1 with the [GWDG](https://www.gwdg.de/en/application-services/persistent-identifier-pid/epic-pid-search) assigned PID: 21.11101/0000-0007-F43E-1
-- 
GitLab