Code covered by the BSD License  

Highlights from
Costantini phase unwrapping

image thumbnail
from Costantini phase unwrapping by Bruno Luong
Implementation of Costantini's 2D unwrapping method based on network programming

cunwrap(Psi, options)
function PhiUnwrap = cunwrap(Psi, options)
% PhiUnwrap = cunwrap(Psi, options)
%
% Purpose: Costantini's 2D unwrapping based on Minimum Network Flow
%
% INPUTS:
%   - Psi: 2D-array wrapped phase (radian)
%   - options: optional structure used to modify the behavior of the 
%              unwrapping algorithm. All fields are optional.
%              The fieldname does not need to exactly case-matched.
%       Weight: array of same dimension as the Psi, with is used as
%               the positive weight of the L1 norm of the residual (to be
%               minimized). For example user can provide WEIGHT as function
%               of the interference amplitude or function of phase
%               measurement quality (e.g., fringe contrast).
%               By default: all internal pixels have the same weight of 1,
%               0.5 for edge-pixels, and 0.25 for corner-pixels. 
%       CutSize: even integer scalar, default [4]: the width (in pixel) of
%                the Gaussian kernel use to lower the weight around pixels
%                that do not fulfill rotational relation.
%                If CutSize is 0, no rotational-based weighting will be
%                carried out.
%       RoundK: Boolean, [false] by default. When ROUNDK is true, CUNWRAP
%               forces the partial derivative residuals K1/K2 to be
%               integers.
%       MaxBlockSize: scalar default [125], target linear blocksize.
%                     Set to Inf for single-block global unwrapping.
%                     Note: network flow is costly in CPU for large size.
%       Overlap: scalar default [0.25], fractional overlapping between two
%                neighboring blocks.
%       Verbose: logical [true], control printout information.
%       LPoption: LINPROG option structure as return by OPTIMSET(...).
% OUTPUT:
%   - Phi: 2D-array unwrapped phase (radian)
%
% Minimum network flow computation engine is Matlab LINPROG (optimization
%   toolbox is required)
%
% References:
%   - http://earth.esa.int/workshops/ers97/papers/costantini/
%   - Costantini, M. (1998) A novel phase unwrapping method based on network
%   programming. IEEE Tran. on Geoscience and Remote Sensing, 36, 813-821.
%
% Author: Bruno Luong <brunoluong@yahoo.com>
% History:
%   Orginal: 27-Aug-2009
%       28-Aug-2009: modification in block splitting, default size changes
%                    to 125 x 125
%       28-Aug-2009: correct a serious indexing bug

if nargin<2
    options = struct();
end

if ndims(Psi)>2
    error('CUNWRAP: input Psi must be 2D array.');
end

[ny nx] = size(Psi);

if nx<2 || ny<2
    error('CUNWRAP: size of Psi must be larger than 2');
end

roundK = getoption(options, 'roundk', false);

verbose = getoption(options, 'verbose', true);

% Default weight
w1 = ones(ny,1); w1([1 end]) = 0.5;
w2 = ones(1,nx); w2([1 end]) = 0.5;
weight = w1*w2; % tensorial product
weight = getoption(options, 'weight', weight);

% Split in smaller block size
blocksize = getoption(options, 'maxblocksize', 125);
blocksize = max(blocksize,2);

% Overlapping fraction
p = getoption(options, 'overlap', 0.25);
p = max(min(p,1),0);

% Split the linear index for each dimension
[ix blk_x] = splitidx(blocksize, nx, p);
[iy blk_y] = splitidx(blocksize, ny, p);
nbblk = length(iy)*length(ix);

mydisplay(verbose, 'Number of block(s) = %d\n', nbblk);
mydisplay(verbose, '\tEffective block size = (%d x %d)\n', blk_y, blk_x);
if nbblk>1
    mydisplay(verbose, '\tOverlapping = %2.1f%%\n', p*100);
end

% Allocate arrays
% negative/positive parts of wrapped partial derivatives
x1p = nan(ny-1, nx, class(Psi));
x1m = nan(ny-1, nx, class(Psi));
x2p = nan(ny, nx-1, class(Psi));
x2m = nan(ny, nx-1, class(Psi));

%%
% Loop over the blocks
blknum = 0;
for i=1:length(iy)
    iy0 = iy{i};
    iy1 = iy0(1:end-1);
    iy2 = iy0(1:end);
    
    for j=1:length(ix)
        ix0 = ix{j};
        ix1 = ix0(1:end);
        ix2 = ix0(1:end-1);
        
        blknum = blknum + 1;
        mydisplay(verbose, ['\n' repmat('-', 1, 60) '\n']);
        mydisplay(verbose, 'CUNWRAP: processing block #%d/%d...\n', ...
            blknum, nbblk);
        
        options.weight = weight(iy0,ix0);
        options.blknum = blknum;
        % Costantini's minimum network flow resolution for one block
        [x1p(iy1,ix1) x1m(iy1,ix1) x2p(iy2,ix2) x2m(iy2,ix2)] = ...
            cunwrap_blk(Psi(iy0,ix0), ...
            x1p(iy1,ix1), x1m(iy1,ix1),...
            x2p(iy2,ix2), x2m(iy2,ix2),...
            options);
    end
end

%%
% Compute partial derivative Psi1, eqt (1,3)
mydisplay(verbose, '\ty-derivative\n');
i = 1:(ny-1);
j = 1:nx;
[ROW_I ROW_J] = ndgrid(i,j);
I_J = sub2ind(size(Psi),ROW_I,ROW_J);
IP1_J = sub2ind(size(Psi),ROW_I+1,ROW_J);
Psi1 = Psi(IP1_J) - Psi(I_J);
% A priori knowledge that Psi1 is in [-pi,pi)
Psi1 = mod(Psi1+pi,2*pi)-pi;

% Compute partial derivative Psi2, eqt (2,4)
mydisplay(verbose, '\tx-derivative\n');
i = 1:ny;
j = 1:(nx-1);
[ROW_I ROW_J] = ndgrid(i,j);
I_J = sub2ind(size(Psi),ROW_I,ROW_J);
I_JP1 = sub2ind(size(Psi),ROW_I,ROW_J+1);
Psi2 = Psi(I_JP1) - Psi(I_J);
% A priori knowledge that Psi1 is in [-pi,pi)
Psi2 = mod(Psi2+pi,2*pi)-pi;

% Compute the derivative jumps, eqt (20,21)
k1 = x1p-x1m;
k2 = x2p-x2m;

% Round to integer solution (?)
if roundK
    mydisplay(verbose, '\tForce integer 2pi-ambiguities\n');
    k1 = round(k1);
    k2 = round(k2);
end

% Sum the jumps with the wrapped partial derivatives, eqt (10,11)
k1 = reshape(k1,[ny-1 nx]);
k2 = reshape(k2,[ny nx-1]);
k1 = k1+Psi1/(2*pi);
k2 = k2+Psi2/(2*pi);

%%
% Integrate the partial derivatives, eqt (6)
% Not sure why integrate this way is better than otherway around
mydisplay(verbose, 'Integration\n');
% Integration order, not documented
intorder = getoption(options, 'IntOrder', 1);
if intorder==1
    K = cumsum([0 k2(1,:)]);
    K = [K; k1];
    K = cumsum(K,1);
else
    % Integration is this order does not work well (still mysterious for BL)
    K = cumsum([0; k1(:,1)]);
    K = [K k2];
    K = cumsum(K,2);
end

PhiUnwrap = (2*pi)*K;

end % cunwrap

%%
function [x1p x1m x2p x2m] = cunwrap_blk(Psi, ...
                                         X1p, X1m, X2p, X2m, options)
% function [x1p x1m x2p x2m] = cunwrap_blk(Psi, ...
%                                          X1p, X1m, X2p, X2m, options)
% Costantini's minimum network flow resolution for one block

%%
% if getoption(options, 'blknum', NaN) == 8
%     save('debug.mat', 'Psi', 'X1p', 'X1m', 'X2p', 'X2m', 'options');
% end

%%                                    
[ny nx] = size(Psi);

verbose = getoption(options, 'verbose', true);

% the width (in pixel) of the Gaussian kernel to limit effect of
% patch that does not satisfy rotational relation
CutSize = getoption(options, 'cutsize', 4);

% Default weight
w1 = ones(ny,1); w1([1 end])=0.5;
w2 = ones(1,nx); w2([1 end])=0.5;
weight = w1*w2; % tensorial product
weight = getoption(options, 'weight', weight);

% Compute partial derivative Psi1, eqt (1,3)
mydisplay(verbose, '\ty-derivative\n');
i = 1:(ny-1);
j = 1:nx;
[ROW_I ROW_J] = ndgrid(i,j);
I_J = sub2ind(size(Psi),ROW_I,ROW_J);
IP1_J = sub2ind(size(Psi),ROW_I+1,ROW_J);
Psi1 = Psi(IP1_J) - Psi(I_J);
% A priori knowledge that Psi1 is in [-pi,pi)
Psi1 = mod(Psi1+pi,2*pi)-pi;

% Compute partial derivative Psi2, eqt (2,4)
mydisplay(verbose, '\tx-derivative\n');
i = 1:ny;
j = 1:(nx-1);
[ROW_I ROW_J] = ndgrid(i,j);
I_J = sub2ind(size(Psi),ROW_I,ROW_J);
I_JP1 = sub2ind(size(Psi),ROW_I,ROW_J+1);
Psi2 = Psi(I_JP1) - Psi(I_J);
% A priori knowledge that Psi1 is in [-pi,pi)
Psi2 = mod(Psi2+pi,2*pi)-pi;

%%
% The RHS is column-reshaping of a 2D array [ny-1] x [nx-1]
% Build the equality constraint RHS (eqt 17)
mydisplay(verbose, '\tConstraint RHS\n');
beq = 0;
% Compute beq
i = 1:(ny-1);
j = 1:(nx-1);
[ROW_I ROW_J] = ndgrid(i,j);
I_J = sub2ind(size(Psi1),ROW_I,ROW_J);
I_JP1 = sub2ind(size(Psi1),ROW_I,ROW_J+1);
beq = beq + (Psi1(I_JP1)-Psi1(I_J));
I_J = sub2ind(size(Psi2),ROW_I,ROW_J);
IP1_J = sub2ind(size(Psi2),ROW_I+1,ROW_J);
beq = beq - (Psi2(IP1_J)-Psi2(I_J));
beq = -1/(2*pi)*beq;
beq = round(beq);
beq = beq(:);

%%
% The vector of LP is arranged as following:
%   x := (x1p, x1m, x2p, x2m).'
%   x1p, x1m: reshaping of [ny-1] x [nx]
%   x2p, x2m: reshaping of [ny] x [nx-1]
% 
% Row index, used by all foure blocks in Aeq, beq
mydisplay(verbose, '\tConstraint matrix\n');
i = 1:(ny-1);
j = 1:(nx-1);

[ROW_I ROW_J] = ndgrid(i,j);
ROW_I_J = sub2ind([length(i) length(j)],ROW_I,ROW_J);
nS0 = (nx-1)*(ny-1);

% Use by S1p, S1m
[COL_I COL_J] = ndgrid(i,j);
COL_IJ_1 = sub2ind([length(i) length(j)+1],COL_I,COL_J);
[COL_I COL_JP1] = ndgrid(i,j+1);
COL_I_JP1 = sub2ind([length(i) length(j)+1],COL_I,COL_JP1);
nS1 = (nx)*(ny-1);

% Use by S2p, S2m
[COL_I COL_J] = ndgrid(i,j);
COL_IJ_2 = sub2ind([length(i)+1 length(j)],COL_I,COL_J);
[COL_IP1 COL_J] = ndgrid(i+1,j);
COL_IP1_J = sub2ind([length(i)+1 length(j)],COL_IP1,COL_J);
nS2 = (nx-1)*(ny);

% Build four indexes arrays that will be used to split x in four parts
% 29/08/09 bug corrected
offset1p = 0;
idx1p = offset1p+(1:nS1);
offset1m = idx1p(end);
idx1m = offset1m+(1:nS1);
offset2p = idx1m(end);
idx2p = offset2p+(1:nS2);
offset2m = idx2p(end);
idx2m = offset2m+(1:nS2);

% Equality constraint matrix (Aeq)
S1p = + sparse(ROW_I_J, COL_I_JP1,1,nS0,nS1) ...
      - sparse(ROW_I_J, COL_IJ_1,1,nS0,nS1);
S1m = -S1p; 

S2p = - sparse(ROW_I_J, COL_IP1_J,1,nS0,nS2) ...
      + sparse(ROW_I_J, COL_IJ_2,1,nS0,nS2);  
S2m = -S2p;

% Matrix of the LHS of eqt (17)
Aeq = [S1p S1m S2p S2m];
nvars = size(Aeq,2);

% Clean up
clear S1p S1m S2p S2m

%%
% force to be even
CutSize = ceil(CutSize/2)*2; 

% Modify weight to limit the effect of points that violate
% the rorational condition. The weight is graduataly increase
% around the points.
if CutSize>0
    mydisplay(verbose, '\tAdjust weight (CutSize = %d pixels)\n', CutSize);
    % Truncated Gaussian Kernel
    v = 1*linspace(-1,1,CutSize);
    [x y] = meshgrid(v,v);
    kernel = 1.1*exp(-(x.^2+y.^2));

    rotdegradation = double(reshape(beq~=0, [ny nx]-1)); % 0 or 1
    mydisplay(verbose, '\tInconsistency = %d/%d\n', ...
              sum(rotdegradation(:)), numel(beq));
    rotdegradation = conv2(rotdegradation, kernel, 'full');
    rotdegradation = rotdegradation(CutSize/2 + (0:ny-1),...
                                    CutSize/2 + (0:nx-1));
    % mininum weight threshold      
    wmin = 1e-2; % weight never goes under this value, small positive value
    rotdegradation = min(rotdegradation,1-wmin);
    weight = weight .* (1-rotdegradation);
end

c1 = 0.5*(weight(1:ny-1,:)+weight(1:ny-1,:));
c2 = 0.5*(weight(:,1:nx-1)+weight(:,1:nx-1));

% Cost vector, eqt (16)
cost = zeros(nvars,1);
cost(idx1p) = c1(:);
cost(idx1m) = c1(:);
cost(idx2p) = c2(:);
cost(idx2m) = c2(:);

%%
% Lower and upper bound, eqt (18,19)
L = zeros(nvars,1);
U = Inf(size(L)); % No upper bound, U=[];

%%
% Lock the x values to prior computed value (from calculation on other
% blocks)
i1 = find(~isnan(X1p));
i2 = find(~isnan(X2p));
mydisplay(verbose, '\tLock variables = %d/%d\n', ...
          2*(length(i1)+length(i2)), size(Aeq,2));
      
% Lock method, not documented
lockadd = 1;
lockremove = 2; %#ok
lockmethod = getoption(options, 'lockmethod', lockadd);

if lockmethod==lockadd
    % Lock matrix and values, eqt (26, 27), larger system
    L1p = sparse(1:length(i1), idx1p(i1), 1, length(i1), size(Aeq,2));
    L1m = sparse(1:length(i1), idx1m(i1), 1, length(i1), size(Aeq,2));
    L2p = sparse(1:length(i2), idx2p(i2), 1, length(i2), size(Aeq,2));
    L2m = sparse(1:length(i2), idx2m(i2), 1, length(i2), size(Aeq,2));
    
    AL = [L1p; L1m; L2p; L2m];
    bL = [X1p(i1); X1m(i1); X2p(i2); X2m(i2)];
    clear L1p L1m L2p L2m % clean up
    
    % Find the rows in Aeq with all variables locked
    ColPatch = [offset1p+COL_IJ_1(:) offset1p+COL_I_JP1(:) ...
                offset2p+COL_IJ_2(:) offset2p+COL_IP1_J(:)];
    
    [trash ColDone] = find(AL); %#ok
    remove = all(ismember(ColPatch, ColDone),2);
    Aeq = [Aeq(~remove,:); AL];
    beq = [beq(~remove,:); bL];
    
    % No need to bother with what already computed
    cost(idx1p(i1)) = 0;
    cost(idx1m(i1)) = 0;
    cost(idx2p(i2)) = 0;
    cost(idx2m(i2)) = 0;
    
    L(idx1p(i1)) = -Inf;
    L(idx1m(i1)) = -Inf;
    L(idx2p(i2)) = -Inf;
    L(idx2m(i2)) = -Inf;
    
    clear AL bL trash ColPatch ColDone remove i1 i2 % clean up
else
    % Lock by remove the overlapped variables, smaller system
    % But *seems* more affected by error propagation
    % BL think both method should be strictly equivalent (!)
    
    % remove the equality contribution from the RHS
    lock = zeros(nvars,1,class(Aeq));
    lock(idx1p(i1)) = X1p(i1);
    lock(idx1m(i1)) = X1m(i1);
    lock(idx2p(i2)) = X2p(i2);
    lock(idx2m(i2)) = X2m(i2);
    beq = beq - Aeq*lock;
    
    % Remove the variables
    vremove = [idx1p(i1) idx1m(i1) idx2p(i2) idx2m(i2)];
    % keep is use later to dispatch partial derivative
    keep = true(nvars,1); keep(vremove) = false;
    Aeq(:,vremove) = [];
    L(vremove) = []; U(vremove) = [];
    cost(vremove) = [];
    
    % Find the rows in Aeq with all variables locked
    ColPatch = [offset1p+COL_IJ_1(:) offset1p+COL_I_JP1(:) ...
                offset2p+COL_IJ_2(:) offset2p+COL_IP1_J(:)];
    
    % Remove the equations
    eremove = all(ismember(ColPatch, vremove),2);
    Aeq(eremove,:) = [];
    beq(eremove,:) = [];
    
    clear vremove eremove ColPatch lock % clean up
end

%%
% To do: implement Bertsekas/Tseng's relaxation method, ref. [20]
% Call LP solver
mydisplay(verbose, '\tMinimum network flow resolution\n');
mydisplay(verbose, '\t\tmatrix size = (%d,%d)\n', size(Aeq,1),size(Aeq,2));
if ~isempty(which('linprog'))
    % Call Matlab Linprog
    % Adjust optional LP options at your preference, see
    % http://www.mathworks.com/access/helpdesk/help/toolbox/optim/ug/linprog.html
    % http://www.mathworks.com/access/helpdesk/help/toolbox/optim/ug/optimset.html
    
    mydisplay(verbose, '\tLINPROG...\n');
    
    % BL has not checked because he does not have the right toolbox
    LPoption = getoption(options, 'LPoption', {});
    if ~iscell(LPoption)
        LPoption = {LPoption};
    end
    % LPoption = {optimset(...)}
    sol = linprog(cost,[],[],Aeq,beq,L,U,[],LPoption{:});
elseif ~isempty(which('BuildMPS'))
    
    % Here is BL Linprog, call Matlab linprog instead to get "SOL",
    % the solution of the above LP problem
    mpsfile='costantini.mps';
    
    mydisplay(verbose, '\tConversion to MPS file\n');
    Contain = BuildMPS([], [], Aeq, beq, cost, L, U, 'costantini');
    OK = SaveMPS(mpsfile,Contain);
    if ~OK
        error('CUNWRAP: Cannot write mps file');
    end
    
    PCxpath=App_path('PCx');
    [OK outfile]=invokePCx(mpsfile,PCxpath,verbose==0);
    if ~OK
        mydisplay(verbose, 'PCx path=%s\n', PCxpath);
        mydisplay(verbose, 'Cannot invoke LP solver, PCx not installed?\n');
        error('CUNWRAP: Cannot invoke PCx');
    end
    [OK sol]=readPCxoutput(outfile);
    if ~OK
        error('CUNWRAP: Cannot read PCx outfile, L1 fit might fails.');
    end
else
    error('CUNWRAP: cannot detect network flow (LP) engine');
end

%%
% Displatch the LP solution
if lockmethod==lockadd
    x = sol;
else
    x = zeros(size(keep),class(sol));
    x(keep) = sol;
end
x1p = reshape(x(idx1p), [ny-1 nx]);
x1m = reshape(x(idx1m), [ny-1 nx]);
x2p = reshape(x(idx2p), [ny nx-1]);
x2m = reshape(x(idx2m), [ny nx-1]);

end

%% Get defaut option
function value = getoption(options, name, defaultvalue)
% function value = getoption(options, name, defaultvalue)
    value = defaultvalue;
    fields = fieldnames(options);
    found = strcmpi(name,fields);
    if any(found)
        i = find(found,1,'first');
        if ~isempty(options.(fields{i}))
            value = options.(fields{i});
        end
    end
end

%%
function [ilist blocksize] = splitidx(blocksize, n, p)
% function ilist = splitidx(blocksize, n, p)
%
% return the cell array, each element is subindex of (1:n)
% The union is (1:n) with overlapping fraction is p (0<p<1)

if blocksize>=n
    ilist = {1:n};
    blocksize = n;
else
    q = 1-p;

    % Number of blocks
    k = (n/blocksize - p) / q;
    k = ceil(k);

    % Readjust the block size, float
    blocksize = n/(k*q + p);

    % first index
    firstidx = round(linspace(1,n-ceil(blocksize)+1, k));
    lastidx = round(firstidx+blocksize-1);
    lastidx(end) = n;
    % Make sure they are overlapped
    lastidx(1:end-1) = max(lastidx(1:end-1),firstidx(2:end));

    % Put the indexes of k blocks into cell array
    ilist = cell(1,length(firstidx));
    for k=1:length(ilist)
        ilist{k} = firstidx(k):lastidx(k);
    end
    
    blocksize = round(blocksize);
end
end

% function to mydisplay a text on command line window
function mydisplay(verbose, varargin)
if verbose
    fprintf(varargin{:});
end
end

Contact us at files@mathworks.com