Fourier Descriptors Demo

Demostrates using Fourier descriptors for contour matching.

Sources

function varargout = fourier_descriptors_demo_gui()
    % create the UI
    h = buildGUI();
    if nargout > 0, varargout{1} = h; end
end

function out = noisyPolygon(pts, noise)
    if noise == 0
        out = pts;
        return;
    end
    try
        % we want reproducible random numbers
        rng('default')
    end
    pts = pts + (rand(size(pts)) * 2*noise - noise);
    out = pts(1,:);
    for i=1:size(pts,1)
        next = i + 1;
        if next > size(pts,1), next = 1; end
        u = pts(next,:) - pts(i,:);
        d = norm(u);
        a = atan2(u(2), u(1));
        step = max(d/noise, 1);
        for j=1:step:d
            pAct = u * j/d;
            r = rand() * noise;
            theta = a + rand()*2*pi;
            pNew = r*[cos(theta), sin(theta)] + pAct + pts(i,:);
            out(end+1,:) = pNew;
        end
    end
    out = fix(out);
end

function img = FDCurveMatching(p)
    % reference shape with 5 vertices
    ctr0 = [250 250; 400 250; 400 300; 250 300; 180 270];

    % noisy shape, transformed (rotate and scale)
    M = cv.getRotationMatrix2D([p.xg, p.yg], p.angle, 10/p.scale);
    ctr1 = noisyPolygon(ctr0, p.levelNoise);
    ctr1 = permute(cv.transform(permute(ctr1, [1 3 2]), M), [1 3 2]);

    % phase-shift (i.e same order just different starting point)
    n = size(ctr1,1);
    orig = fix(p.origin/100 * n);
    ctr1 = circshift(ctr1, orig, 1);

    % estimate transformation
    if true
        obj = cv.ContourFitting('FDSize',16, 'CtrSize',256);
        t = obj.estimateTransformation(ctr1, ctr0, 'FD',false);
    else
        % explicit contour sampling with 256 points
        ctr0s = cv.ContourFitting.contourSampling(ctr0, 256);
        ctr1s = cv.ContourFitting.contourSampling(ctr1, 256);
        obj = cv.ContourFitting('FDSize',16);
        t = obj.estimateTransformation(ctr1s, ctr0s, 'FD',false);
    end

    % fix t values to same range as ours: origin in (0,1)*n, angle in (0,360)
    if t(1) < 0, t(1) = 1 + t(1); end
    if t(2) < 0, t(2) = 2*pi + t(2); end
    fprintf('Transform: t=%s\n', mat2str(t,3));
    fprintf(' Origin = %f, expected %d (%d)\n', t(1)*n, orig, n);
    fprintf(' Angle  = %f, expected %d\n', t(2)*180/pi, p.angle);
    fprintf(' Scale  = %f, expected %g\n', t(3), p.scale/10);

    % apply estimated transformation to bring noisy shape to reference shape
    ctr2 = cv.ContourFitting.transformFD(ctr1, t, 'FD',false);
    ctr2 = cat(1, ctr2{:});

    % draw the three contours
    C = {ctr0, ctr1, ctr2};
    clr = [255 0 0; 0 255 0; 0 255 255];
    txt = {'reference', 'noisy', 'recovered'};

    % output image size
    rect = [0 0 500 500];
    if false
        for i=1:numel(C)
            rect = cv.Rect.union(rect, cv.boundingRect(C{i}));
        end
    end

    img = zeros([rect(3:4) 3], 'uint8');
    for i=1:numel(C)
        % legend
        img = cv.putText(img, txt{i}, [10 20*i], ...
            'Color',round(clr(i,:)*0.8), 'FontScale',0.5);
        % contour
        img = cv.drawContours(img, C{i}, 'Color',clr(i,:));
        % starting point
        img = cv.circle(img, C{i}(1,:), 5, 'Color',clr(i,:));
    end
end

function onChange(~,~,h)
    %ONCHANGE  Event handler for UI controls

    % retrieve current values from UI controls
    p = struct();
    p.levelNoise = round(get(h.slid(6), 'Value'));
    p.angle = round(get(h.slid(5), 'Value'));
    p.scale = round(get(h.slid(4), 'Value'));
    p.origin = round(get(h.slid(3), 'Value'));
    p.xg = round(get(h.slid(2), 'Value'));
    p.yg = round(get(h.slid(1), 'Value'));
    set(h.txt(1), 'String',sprintf('Yg: %d',p.yg));
    set(h.txt(2), 'String',sprintf('Xg: %d',p.xg));
    set(h.txt(3), 'String',sprintf('Origin%%: %d',p.origin));
    set(h.txt(4), 'String',sprintf('Scale: %d',p.scale));
    set(h.txt(5), 'String',sprintf('Angle: %d',p.angle));
    set(h.txt(6), 'String',sprintf('Noise: %d',p.levelNoise));

    % perform contour matching using Fourier descriptors
    img = FDCurveMatching(p);

    % show result
    set(h.img, 'CData',img);
    drawnow;
end

function h = buildGUI()
    %BUILDGUI  Creates the UI

    % canvas
    img = zeros([500 500 3], 'uint8');
    sz = size(img);

    % initial params
    % (a 45 degree rotation centered at [250,250] with a scaling of 5/10)
    p = struct();
    p.levelNoise = 6;
    p.angle = 45;
    p.scale = 5;
    p.origin = 10;
    p.xg = 250;
    p.yg = 250;

    % build the user interface (no resizing to keep it simple)
    h = struct();
    h.fig = figure('Name','FD Curve matching', ...
        'NumberTitle','off', 'Menubar','none', 'Resize','off', ...
        'Position',[200 200 sz(2) sz(1)+155-1]);
    if ~mexopencv.isOctave()
        %HACK: not implemented in Octave
        movegui(h.fig, 'center');
    end
    h.ax = axes('Parent',h.fig, 'Units','pixels', 'Position',[1 155 sz(2) sz(1)]);
    if ~mexopencv.isOctave()
        h.img = imshow(img, 'Parent',h.ax);
    else
        %HACK: https://savannah.gnu.org/bugs/index.php?45473
        axes(h.ax);
        h.img = imshow(img);
    end
    h.txt(1) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 5 100 20], 'String',sprintf('Yg: %d',p.yg));
    h.txt(2) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 30 100 20], 'String',sprintf('Xg: %d',p.xg));
    h.txt(3) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 55 100 20], 'String',sprintf('Origin%%: %d',p.origin));
    h.txt(4) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 80 100 20], 'String',sprintf('Scale: %d',p.scale));
    h.txt(5) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 105 100 20], 'String',sprintf('Angle: %d',p.angle));
    h.txt(6) = uicontrol('Parent',h.fig, 'Style','text', 'FontSize',11, ...
        'Position',[5 130 100 20], 'String',sprintf('Noise: %d',p.levelNoise));
    h.slid(1) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.yg, 'Min',150, 'Max',350, 'SliderStep',[2 20]./(350-150), ...
        'Position',[105 5 sz(2)-105-5 20]);
    h.slid(2) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.xg, 'Min',150, 'Max',350, 'SliderStep',[2 20]./(350-150), ...
        'Position',[105 30 sz(2)-105-5 20]);
    h.slid(3) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.origin, 'Min',0, 'Max',100, 'SliderStep',[1 10]./(100-0), ...
        'Position',[105 55 sz(2)-105-5 20]);
    h.slid(4) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.scale, 'Min',5, 'Max',50, 'SliderStep',[1 5]./(50-5), ...
        'Position',[105 80 sz(2)-105-5 20]);
    h.slid(5) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.angle, 'Min',0, 'Max',360, 'SliderStep',[2 20]./(360-0), ...
        'Position',[105 105 sz(2)-105-5 20]);
    h.slid(6) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',p.levelNoise, 'Min',0, 'Max',20, 'SliderStep',[1 5]./(20-0), ...
        'Position',[105 130 sz(2)-105-5 20]);

    % hook event handlers, and trigger default start
    opts = {'Interruptible','off', 'BusyAction','cancel'};
    set(h.slid, 'Callback',{@onChange,h}, opts{:});
    onChange([],[],h);
end
Transform: t=[0.0574 0.819 0.497 -55.6 60.9]
 Origin = 1.779683, expected 3 (31)
 Angle  = 46.904017, expected 45
 Scale  = 0.497066, expected 0.5