Code covered by the BSD License  

Highlights from
Accurate polygon extension

image thumbnail
from Accurate polygon extension by Krispijn Scholte
Enlarges polygon by a specified range. Also generates internal polygons caused by intersections.

extendPoly(Polygon,varargin)
function [extendedPolyCell] =  extendPoly(Polygon,varargin)
% Extends a polygon by the specified range and calculates the resulting 
% polygon with the specified resolution. Requires basic Matlab and no 
% additional toolboxes.
% 
% Usage: 
%  extendedPolygon = extendPoly ( originalPolygon , range, resolution )
%
%  mandatory:
%  ----------
%  originalPolygon =   <Nx2 double> array that represent the polygons 
%                      which are defined either CW or CCW
%  range           =   radius with which the polygon is to be extended 
%  extendedPolygon =   cellarray with CW or CCW corresponding with the input
%                      polygon
%
%  optional:
%  ---------
%  resolution      =   sets the resultion with which the circular parts of
%                      the extension are drawns. Default pi/50.
%
% Limitations:
%  - Polygon is not allowed to have crossing line sections
%    (selfintersections)
%  - Only 2D Polygons allowed
%  - Unpredictable results when negative range is specified
%
% Example:
%  Polygon = [0,0;4,0;2,6;0,4];
%  extendedPolygon = extendPoly(Polygon,0.4);
%  figure(1); clf; patch(Polygon(:,1),Polygon(:,2),'r');
%  for i=1:length(extendedPolygon)
%    toPlot=extendedPolygon{i};
%    line(toPlot(:,1),toPlot(:,2),'Color','b');
%  end
%
% -------------------------------------------------------------------------
% Version: 1.5 Date: 10-11-2008 
% Tested on Matlab 7.6.0 (R2008b)
%
% Lt. Krispijn A. Scholte
% CAMS-Force Vision, Den Helder (The Netherlands)
% email: krispijn_s@hotmail.com
% 
% Makes use of NS "InterX" algorithm for curve intersection (embedded)
% http://www.mathworks.com/matlabcentral/fileexchange/22441
% -------------------------------------------------------------------------
%
% Version history:
%  1.0  Initial (unreleased)
%  1.1  Multiple small bug fixes
%  1.2  Fixed CW/CCW detection bug
%  1.3  Added error and warning messages, automatically closes polygon if 
%       input polygon is not closed.
%  1.4  New faster CW/CCW detection method. Old method prevented returning 
%       results when the polygon has only negative x-values. 
%  1.5  Replaced selfintersection algorithm because it yielded inaccurate
%       results; added user definable resolution parameter for drawing the
%       circle pieces of the resulting polygon.

%% Verify input and generate error and warning messages if needed
%test number of input arguments; error if incorrect
if nargin < 2 || nargin > 3
    error('extendPoly:InputFormating',['Wrong number of input arguments: ',num2str(nargin),' given; 2 required.']);
end

if nargin == 2
    range = varargin{1};
    res = pi/50; % use default
    warning('extendPoly:InputFormating','No drawing resolution specified. Reverting to default (pi/50).');
else
    range = varargin{1};
    res = varargin{2};
end

%test if a negative range is specified; generate warning if so
if range < 0
    warning('extendPoly:InputFormating','Negative range specified. Results may not be correct.');
end
 
%test if the input polygon is a real polygon; error if not.
if size(unique(Polygon,'rows'),1) < 3
    error('extendPoly:InputFormating',['Specified input polygon is not a polygon (number of unique points is ',num2str(length(unique(Polygon))),').']);
end

%test input polygon is closed; if not generate warning and close polygon
if sum(single(Polygon(end,:)) == single(Polygon(1,:))) < 2 % test with single precision
    Polygon(end+1,:)=Polygon(1,:);
    warning('extendPoly:InputFormating','Input polygon is not closed.');
end

%test input polygon for self intersections (NOT ALLOWED)
P = InterX(Polygon');
SegPoints = P';

if ~isempty(SegPoints)
    
    %if one self intersection occurs it needs to be tested because this is 
    %often occurs where the polygon is closed. If the self intersection
    %point is a polygon point, then it is allowed. If it is not, then
    %generate an error
    
    if size(SegPoints,1) > 1 
       error('extendPoly:InputFormating','Input polygon has self intersections.'); 
    else
        pointMatched = false;
        
        for i=1:size(Polygon(1,:))
            if sum(single(Polygon(i,:)) == single(SegPoints(1,:))) == 2
                %x and y coordinates match, so this intersection is part of
                %the polygon (probably due to closing the polygon
                pointMatched = true;
                break
            end
        end
        
        if ~pointMatched 
            error('extendPoly:InputFormating','Input polygon has self intersections.'); 
        end
    end
end

%% Set configuration
% These numbers can be modified to improve precision or speed. Current
% values have yielded good, stable results on a wide variety of polygons,
% but feel free to adjust them if the initial results are less than
% satisfactory.
global stepSize;                    %stepsize used for defining a test circle around points to be tested
global polySize;                    %number of datapoints in the original polygon
CCW = false;                        %assumes the polygon is defined CW, which will be tested below 
extendedPoly=zeros(1,2);            %create an empty matrix for the extended polygon
j=1;                                %extendedPolygon node counter
pointTestTolerance = .997;           %tolerance for the point test
                       %resolution with which the radius around polygon points are drawn

polySize = length(Polygon(:,1));  

%determine circleTest stepsize (used very very frequently)
xscale = max(Polygon(:,1))-min(Polygon(:,1));
yscale = max(Polygon(:,2))-min(Polygon(:,2));
scaler = (sqrt(xscale^2+yscale^2))/range; %scaler is used to decrease stepSize if the range increases

if scaler < 2
    scaler = 2;
end

stepSize =.25/scaler;

%% Determine if input polygon is specified CW/CCW
% first point is drawn; test if point is in polygon and range specified
% is positive, if so -> polygon is defined CCW (program default assumes CW)

if polySize > 4
    i=round(polySize./2);
else
    i=2;
end
p0=Polygon(i,:); p1=Polygon(i-1,:); p2=Polygon(i+1,:);
Vector1=(p0-p1); Vector2=(p0-p2);
ZeroVec = zeros(size(Vector1));

while isequal(Vector1,Vector2) || isequal(Vector1,ZeroVec) || isequal(Vector2,ZeroVec);
    i=i+1;
    p1=Polygon(i-1,:); p2=Polygon(i+1,:);
    Vector1=(p0-p1); Vector2=(p0-p2);
end

% determine normals and normalise
NV1=rotateVector(Vector1, pi./2); NV2=rotateVector(Vector2, -pi./2);
NormV1=normaliseVector(NV1); NormV2=normaliseVector(NV2);

% test point is based on the two normals
pointToTest=p0+((1-pointTestTolerance)*1e-5.*normaliseVector(NormV1+NormV2));

IN = pointInPolygon (pointToTest,Polygon);

if range > 0 && IN == 1
   % Reverse point order
   CCW = true;
   Polygon = Polygon(end:-1:1,:);
else
   if range < 0 && IN == 0
      % Reverse point order
      CCW = true;
      Polygon = Polygon(end:-1:1,:);
   end
end

%% Extend the polygon by the specified range
% First test if it is a fully closed polygon (eg. first and last 
% coordinates are identical. If such is the case, remove the last entry and
% reduce the polySize

if isequal(single(Polygon(end,:)),single(Polygon(1,:)))
   Polygon = Polygon(1:1:end-1,:);
   polySize = polySize-1;    
end

for i=1:polySize
   % find the two vectors needed
   if i~=1
       if i<polySize
        p0=Polygon(i,:); p1=Polygon(i-1,:); p2=Polygon(i+1,:);   
       else
        p0=Polygon(i,:); p1=Polygon(i-1,:); p2=Polygon(1,:); %special case for i=polySize
       end
   else
        p0=Polygon(i,:); p1=Polygon(polySize,:); p2=Polygon(i+1,:); %special case for i=1
   end
   
   Vector1=(p0-p1); Vector2=(p0-p2);
   
   if ~(isequal(Vector1,Vector2) || isequal(Vector1,ZeroVec) || isequal(Vector2,ZeroVec));
   
       %determine normals and normalise and
       NV1=rotateVector(Vector1, pi./2); NV2=rotateVector(Vector2, -pi./2);
       NormV1=normaliseVector(NV1); NormV2=normaliseVector(NV2);
       %determine rotation by means of the atan2 (because sign matters!)
       
       totalRotation = vectorAngle(NormV2, NormV1);
       rotateVectors = totalRotation:-res:0;
       
       if totalRotation < pi && abs(totalRotation) > res
           
           for k=rotateVectors;
               rotAngle = k;
               newPoint = rotateVector(NormV2,rotAngle);
               nnewp = normaliseVector(newPoint);
               %only add point if it is different from the previous point
               if j > 1
                   if extendedPoly(j-1,:)~=p0+(range*nnewp)
                    extendedPoly(j,:)=p0+(range.*nnewp);j=j+1;
                   end
               else
                   extendedPoly(j,:)=p0+(range.*nnewp);j=j+1;
               end
           end

       else
           % corner < 0 degrees special case or if the total rotation is
           % smaller than the rotation resolution but only if they are
           % different from the previously addes point
           extendedPoly(j,:)=p0+(range.*NormV1);j=j+1;
           extendedPoly(j,:)=p0+(range.*NormV2);j=j+1;
       end      
   end
end

extendedPoly(end+1,:)=extendedPoly(1,:);

%% Clean up the resulting polygon
% look for self intersections then comes the jedimindtrick: how to 
% determine which segpoints to keep? A circle is drawn with a radius range 
% around the segpoint. If this circle intersects with the original polygon 
% it is removed from the list. The resulting segpoints indicate where line 
% segments of the extended polygon need to be removed. Also
% indicates whether there are addition polygons within the extended
% polygon.

if length(extendedPoly(:,1)) > 3
    P = InterX(extendedPoly');
    SegPoints = P';
else
    SegPoints = [];
end

rowsDeleted = 0;
tempSegPoints = SegPoints;

for i=1:1:size(SegPoints,1)

    testResult = newHitTest(SegPoints(i,:),Polygon,range,pointTestTolerance,res);
    
    if testResult == 1
        %remove this segpoint from the list
        tempSegPoints(i-rowsDeleted,:)=[];
        rowsDeleted = rowsDeleted+1;
    end
    
end

SegPoints = tempSegPoints;
pointsAdded=0; 

%determine on which linesections the self intersection points into are 
%they are inserted into the extended Polygon

PointsLog=[];
for j=1:size(extendedPoly,1)
    
    for i=1:1:size(SegPoints,1)
           
        Q1=extendedPoly(j,:);
        
        if j ~= size(extendedPoly,1)
            Q2=extendedPoly(j+1,:);
        else
            Q2=extendedPoly(1,:);
        end

        P=SegPoints(i,:);

        %calculate the distance from the point to the line
        distance=abs(det([Q2-Q1;P-Q1]))/norm(Q2-Q1);

        %determine if the segpoint is on the line

        if distance < eps(single(1))          
            PointsLog = [PointsLog; i, j, j+1];
        end
    end
    
end

% Reorder the matrix
newPointsLog=[];
for i=1:size(SegPoints,1)
    if PointsLog((2*i)-1,1) ~= PointsLog(2*i,1)
        addIndex = find(PointsLog(:,1) == PointsLog((2*i)-1,1));
        for j=1:size(addIndex,1)
           if addIndex(j) ~=  (2*i)-1
              %reorder the matrix 
              newPointsLog = [PointsLog(1:(2*i)-1,:);...
                  PointsLog(addIndex(j),:);...
                  PointsLog((2*i):addIndex(j)-1,:);...
                  PointsLog(addIndex(j)+1:end,:)];
           end
        end
        PointsLog = newPointsLog;
    end
end

% create matrix with removal intervals
% removeIntervals = (SegPoint to be inserted, Point in extendPoly after
% which to add, Point in extendPoly before which to add)

removeIntervals=zeros(size(SegPoints,1),3);
for i=1:size(SegPoints,1)
    removeIntervals(i,1)=PointsLog((2*i)-1,1);
    removeIntervals(i,2)=PointsLog((2*i)-1,2);
    removeIntervals(i,3)=PointsLog(2*i,3-1);
end

Segments = [removeIntervals(:,2),removeIntervals(:,3)];
SegPoints = SegPoints(removeIntervals(:,1),:);

rowsDeleted = 0;
tempSegPoints = SegPoints;
tempSegments = Segments;

for i=1:1:size(SegPoints,1)

    testResult = newHitTest(SegPoints(i,:),Polygon,range,pointTestTolerance,stepSize);
    
    if testResult == 1
        %remove this segpoint from the list
        tempSegPoints(i-rowsDeleted,:)=[];
        tempSegments(i-rowsDeleted,:)=[];
        rowsDeleted = rowsDeleted+1;
    end
    
end

SegPoints = tempSegPoints;
Segments = tempSegments;
pointsAdded=0; 

%insert self intersection points into the extended Polygon
tempPoints = extendedPoly;

for i=1:1:size(SegPoints,1)

    % reuse the segments from the self intersect algorithm
    j=Segments(i,1);
    tempPoints = [tempPoints(1:j+pointsAdded,:);SegPoints(i,:);tempPoints(j+pointsAdded+1:end,:)];
	pointsAdded = pointsAdded+1;
    j=Segments(i,2);
    tempPoints = [tempPoints(1:j+pointsAdded,:);SegPoints(i,:);tempPoints(j+pointsAdded+1:end,:)];
	pointsAdded = pointsAdded+1;

end

extendedPoly = tempPoints;

% Final check to make sure no points are in the polygon that do not belong
% there.

rowsDeleted = 0;
tempPoints = extendedPoly;

for i=1:1:size(extendedPoly,1)

    testResult = newHitTest(extendedPoly(i,:),Polygon,range,pointTestTolerance,stepSize);
    
    if testResult == 1
        %remove this point from the list
        tempPoints(i-rowsDeleted,:)=[];
        rowsDeleted = rowsDeleted+1;
    end
    
end

extendedPoly = tempPoints;

% We now have an hull polygon with possibly internal polygons due to
% intersections by extending the polygon. 

subPolyList=[];

for i=1:size(extendedPoly(:,1))
    if i ~= size(extendedPoly(:,1))
        pointToTest = 0.5.*(extendedPoly(i+1,:)+extendedPoly(i,:));
        testResult = newHitTest(pointToTest,Polygon,range,pointTestTolerance,stepSize);
        
        if testResult == 1
            % after the tested point either starts or stops a new internal
            % polygon but only if this is a segpoint
            subPolyList = [subPolyList; i];
        end
    else
        %Special case for the last point

        pointToTest = 0.5.*(extendedPoly(1,:)+extendedPoly(i,:));
        testResult = newHitTest(pointToTest,Polygon,range,pointTestTolerance,stepSize);
        
        if testResult == 1
            % after the tested point either starts or stops a new internal
            % polygon but only if this is a segpoint
            subPolyList = [subPolyList; i];
        end
    end
end

subPoly=cell(1,size(subPolyList,1));

%process the subPoly's
if ~isempty(subPolyList)
    shift = 0;
    if size(subPolyList,1) > 1
        for i=1:size(subPolyList,1)
            %first add the new poly to the subpoly list
            if shift + i + 1 > size(subPolyList,1)
                break
            end
            
            polyStart = subPolyList(i+shift,1)+1;
            polyStop = subPolyList(i+shift+1,1);

            if polyStop-polyStart > 2
                theNewPoly = extendedPoly(polyStart:polyStop-1,:);
                subPoly{shift+1}=theNewPoly;
                shift = shift + 1;
            end
            

        end
    end
end

%clean up empty cells in subPoly
if ~isempty(subPoly)
    subPoly(cellfun(@isempty,subPoly)) = [];

    %remove determined subpoly points from the hull polygon
    removeIndex = zeros(size(extendedPoly,1),1);
    for i=1:length(subPoly)
        for j=1:size(subPoly{i}(:,1))
            for k=1:size(extendedPoly,1)
                if extendedPoly(k,:)==subPoly{i}(j,:)
                    removeIndex(k,1)=1;
                end
            end
        end
    end
    extendedPoly = extendedPoly(~removeIndex(:,1),:); 
end

%% Finalising the output
% Format the output cell array. The first position is the hull polygon,
% after that the sub polygons are added. All polygons are specified
% according to the input polygon.

extendedPolyCell = cell(1,length(subPoly)+1);

if ~CCW
    extendedPolyCell{1}=extendedPoly;

    % add the sub polies only if they are real Polies (3 or more points)
    if ~isempty(subPoly)
        j=2;
        for i=1:length(subPoly)
            if size(subPoly{i},1) > 3
               extendedPolyCell{j}=subPoly{i}; j=j+1;
            end
        end
    end
else
    %input polygon was CCW defined; make sure the output is too
    
    extendedPolyCell{1}=extendedPoly(end:-1:1,:);
    
    % add the sub polies
    if ~isempty(subPoly)
        j=2;
        for i=1:length(subPoly)
            if size(subPoly{i},1) > 3
               extendedPolyCell{j}=subPoly{i}(end:-1:1,:); j=j+1;
            end
        end
    end
end

%clean up empty cells
extendedPolyCell(cellfun(@isempty,extendedPolyCell)) = [];

%close all polygons if they aren't already
for i=1:length(extendedPolyCell)
    if sum(single(extendedPolyCell{i}(end,:)) == single(extendedPolyCell{i}(1,:))) < 2
        extendedPolyCell{i}(end+1,:)=extendedPolyCell{i}(1,:);
    end
end

%% Embedded Helper functions

%-[Own functions]----------------------------------------------------------
function [rotatedVec] = rotateVector(Vector, angle)
% rotate unity vectors
rotatedVec(1)= cos(angle)*Vector(1) - sin(angle)*Vector(2);
rotatedVec(2)= sin(angle)*Vector(1) + cos(angle)*Vector(2);

function [normVec] = normaliseVector(Vector)
% normalise vector to a unity vector
nv=Vector.^2;scaler=sqrt(nv(1)+nv(2)); 
normVec=(Vector./scaler);

function [angle] = vectorAngle(Vector1,Vector2)
% determine the angle from Vector1 to Vector2
x1=Vector1(1);y1=Vector1(2);
x2=Vector2(1);y2=Vector2(2);
angle = mod(atan2(x1*y2-x2*y1,x1*x2+y1*y2),2*pi); %in radians

function [oddNodes] = pointInPolygon (point,thePolygon)
% determine if a point is in the polygon (faster than matlab "inpolygon"
% command

polyPoints=size(thePolygon,1);    % number of polygon points
oddNodes = false;

j=polyPoints;
x=point(1); y=point(2);

for i=1:polyPoints
    if (thePolygon(i,2)<y && thePolygon(j,2)>=y ||  thePolygon(j,2)<y && thePolygon(i,2)>=y)
        if (thePolygon(i,1)+(y-thePolygon(i,2))/(thePolygon(j,2)-thePolygon(i,2))*(thePolygon(j,1)-thePolygon(i,1))<x)
            oddNodes=~oddNodes;
        end
    end
    j=i; 
end

function [result] = newHitTest (point,Polygon,r,tol,stepSize)
%This function calculates whether a point is allowed.

%First is a quick test is done by calculating the distance from point to 
%each point of the polygon. If that distance is smaller than range "r", 
%the point is not allowed. This will slow down the algorithm at some 
%points, but will greatly speed it up in others because less calls to the 
%circleTest routine are needed.
polySize=size(Polygon,1);
testCounter=0;

for i=1:polySize
    d = sqrt(sum((Polygon(i,:)-point).^2));
    
    if d < tol*r
        testCounter=1;
        break
    end
end

if testCounter == 0
    circleTestResult = circleTest (point,Polygon,r,tol,stepSize);
    testCounter = circleTestResult;
end

result = testCounter;
% |    
  function [result] = circleTest (point,Polygon,r,tol,stepSize)
% draws a circle around the specified point and determines if it is in the
% Polygon or not
circlePoints=0:stepSize:2*pi+stepSize;

%test circle
xs=(tol.*r.*sin(circlePoints))+point(1);
ys=(tol.*r.*cos(circlePoints))+point(2);

%curve intersect has proven to be wrong in a number of cases, so if there
%is no intersection, do a quick point-in-polygon test to check the results
breakCounter = 0;

for j=1:length(xs)

    pInPtest = pointInPolygon ([xs(j),ys(j)],Polygon);
    
    if pInPtest == 1
       breakCounter = breakCounter + 1;
    end


    if breakCounter > 1
        break
    end
   
end

if breakCounter == 0
    result = 0;
else
    result = 1;
end

%-[From Matlab File Exchange]----------------------------------------------
function P = InterX(L1,varargin)
%INTERX Intersection of curves
%   P = INTERX(L1,L2) returns the intersection points of two curves L1 
%   and L2. The curves L1,L2 can be either closed or open and are described
%   by two-row-matrices, where each row contains its x- and y- coordinates.
%   The intersection of groups of curves (e.g. contour lines, multiply 
%   connected regions etc) can also be computed by separating them with a
%   column of NaNs as for example
%
%         L  = [x11 x12 x13 ... NaN x21 x22 x23 ...;
%               y11 y12 y13 ... NaN y21 y22 y23 ...]
%
%   P has the same structure as L1 and L2, and its rows correspond to the
%   x- and y- coordinates of the intersection points of L1 and L2. If no
%   intersections are found, the returned P is empty.
%
%   P = INTERX(L1) returns the self-intersection points of L1. To keep
%   the code simple, the points at which the curve is tangent to itself are
%   not included. P = INTERX(L1,L1) returns all the points of the curve 
%   together with any self-intersection points.
%   
%   Example:
%       t = linspace(0,2*pi);
%       r1 = sin(4*t)+2;  x1 = r1.*cos(t); y1 = r1.*sin(t);
%       r2 = sin(8*t)+2;  x2 = r2.*cos(t); y2 = r2.*sin(t);
%       P = InterX([x1;y1],[x2;y2]);
%       plot(x1,y1,x2,y2,P(1,:),P(2,:),'ro')

%   Author : NS
%   Version: 1.0, 12/12/08

%   Two words about the algorithm: Most of the code is self-explanatory.
%   The only trick lies in the calculation of C1 and C2. To be brief, this
%   is essentially the two-dimensional analog of the condition that needs
%   to be satisfied by a function F(x) that has a zero in the interval
%   [a,b], namely
%           F(a)*F(b) <= 0
%   C1 and C2 exactly do this for each segment of curves 1 and 2
%   respectively. If this condition is satisfied simultaneously for two
%   segments then we know that they will cross at some point. 
%   Each factor of the 'C' arrays is essentially a matrix containing 
%   the numerators of the signed distances between points of one curve
%   and line segments of the other.

    %...Argument checks and assignment of L2
    error(nargchk(1,2,nargin));
    if nargin == 1,
        L2 = L1;    hF = @lt;   %...Avoid the inclusion of common points
    else
        L2 = varargin{1}; hF = @le;
    end
       
    %...Preliminary stuff
    x1  = L1(1,:)';  x2 = L2(1,:);
    y1  = L1(2,:)';  y2 = L2(2,:);
    dx1 = diff(x1); dy1 = diff(y1);
    dx2 = diff(x2); dy2 = diff(y2);
    
    %...Determine 'signed distances'
    S1 = repmat(dx1.*y1(1:end-1) - dy1.*x1(1:end-1),1,size(L2,2)-1);
    S2 = repmat(dx2.*y2(1:end-1) - dy2.*x2(1:end-1),size(L1,2)-1,1);
    
    C1 = feval(hF,(kron(dx1,y2(1:end-1)) - kron(dy1,x2(1:end-1)) - S1).*...
         (kron(dx1,y2(2:end))- kron(dy1,x2(2:end)) - S1),0);       
    
    C2 = feval(hF,(kron(dx2,y1(1:end-1)) - kron(dy2,x1(1:end-1)) - S2).*...
         (kron(dx2,y1(2:end)) - kron(dy2,x1(2:end)) - S2),0);

    %...Obtain the points where an intersection is expected
    [i,j] = find(C1&C2);
    x2 = x2';dx2=dx2';y2=y2';dy2=dy2';  
    L = dy2(j).*dx1(i) - dy1(i).*dx2(j);
    i = i(L~=0); j=j(L~=0); L=L(L~=0);  %...Avoid divisions by 0
    
    %...Solve system of eqs to get the common points
    P = unique([(dx2(j).*(dx1(i).*y1(i) - dy1(i).*x1(i)) ...
         + dx1(i).*(dy2(j).*x2(j) - dx2(j).*y2(j))),...
           dy1(i).*(dy2(j).*x2(j) - dx2(j).*y2(j))...
         + dy2(j).*(dx1(i).*y1(i) - dy1(i).*x1(i))]./[L L],'rows')';

%-----------------------------------------------------------------------EOF

Contact us at files@mathworks.com