Wiener Deconvolution for Image Deblurring

Sample shows how DFT can be used to perform Weiner deconvolution of an image with user-defined point spread function (PSF).

Use controls to adjust PSF parameters, and swtich between linear/cirular PSF.

See also: deconvwnr, https://www.mathworks.com/help/images/image-restoration-deblurring.html

Sources:

function varargout = weiner_deconvolution_demo_gui(im)
    % load grayscale image
    if nargin < 1
        im = fullfile(mexopencv.root(), 'test', 'licenseplate_motion.jpg');
        if exist(im, 'file') ~= 2
            disp('Downloading image...')
            url = 'https://cdn.rawgit.com/opencv/opencv/3.2.0/samples/data/licenseplate_motion.jpg';
            urlwrite(url, im);
        end
        img = cv.imread(im, 'Grayscale',true);
    elseif ischar(im)
        img = cv.imread(im, 'Grayscale',true);
    else
        img = im;
    end
    img = single(img) / 255;  % convert 8-bit to floating-point precision

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

function app = initApp(img)
    %INIT_APP  Initialize options and state structure

    app = struct();

    % options
    app.snr = 25;       % signal/noise ratio in dB
    app.angle = 135;    % blur angle in degrees [0,180]
    app.d = 22;         % blur diameter [1,ksz]
    app.ksz = 65;       % kernel size

    % process image and compute its DFT
    app.img0 = img;
    app.img = blur_edge(img);
    app.IMG = cv.dft(app.img, 'ComplexOutput',true);
end

function img = blur_edge(img, d)
    %BLUR_EDGE  Blur image edges to reduce ringing effect in deblurred image
    %
    %     img = blur_edge(img)
    %     img = blur_edge(img, d)
    %
    % ## Input
    % * __img__ input image
    % * __d__ gaussian size, default 31
    %
    % ## Output
    % * __img__ output image
    %
    % See also: edgetaper
    %

    if nargin < 2, d = 31; end
    img_blur = cv.copyMakeBorder(img, [d d d d], 'BorderType','Wrap');
    img_blur = cv.GaussianBlur(img_blur, 'KSize',[d d]*2+1, 'SigmaX',-1);
    img_blur = img_blur(d+1:end-d, d+1:end-d);

    [h,w] = size(img);
    [y,x] = ndgrid(1:h, 1:w);
    D = min(cat(3, x, w-x+1, y, h-y+1), [], 3);
    a = min(single(D)/d, 1);

    img = img.*a + img_blur.*(1-a);
end

function kern = motion_kernel(ang, d, sz)
    %MOTION_KERNEL  Create linear motion filter
    %
    %     kern = motion_kernel(ang, d)
    %     kern = motion_kernel(ang, d, sz)
    %
    % ## Input
    % * __ang__ linear motion angle
    % * __d__ linear motion length
    % * __sz__ kernel size, default 65
    %
    % ## Output
    % * __kern__ kernel
    %
    % See also: fspecial (motion)
    %

    if nargin < 3, sz = 65; end
    sz2 = floor(sz / 2);
    A = [cos(ang) -sin(ang); sin(ang) cos(ang)];
    A(:,3) = [sz2; sz2] - A*[(d-1)*0.5; 0];
    kern = ones(1,d,'single');
    kern = cv.warpAffine(kern, A, 'DSize',[sz sz], 'Interpolation','Cubic');
end

function kern = defocus_kernel(d, sz)
    %DEFOCUS_KERNEL  Create circular defocus kernel
    %
    %     kern = defocus_kernel(d)
    %     kern = defocus_kernel(d, sz)
    %
    % ## Input
    % * __d__ circular motion diameter
    % * __sz__ kernel size, default 65
    %
    % ## Output
    % * __kern__ kernel
    %
    % See also: fspecial (gaussian)
    %

    if nargin < 2, sz = 65; end
    kern = zeros(sz,sz,'uint8');
    kern = cv.circle(kern, [sz sz], d, ...
        'Color',255, 'Thickness','Filled', 'LineType','AA', 'Shift',1);
    kern = single(kern) / 255;
end

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

    % retrieve current values from UI controls
    psf_type = get(h.pop, 'Value');
    psf_viz = get(h.cbox, 'Value') == get(h.cbox, 'Max');
    app.snr = round(get(h.slid(1), 'Value'));
    app.d = round(get(h.slid(2), 'Value'));
    app.angle = round(get(h.slid(3), 'Value'));
    set(h.txt(1), 'String',sprintf('SNR (dB): %d', app.snr));
    set(h.txt(2), 'String',sprintf('Diameter: %d', app.d));
    set(h.txt(3), 'String',sprintf('Angle: %d', app.angle));

    if psf_type == 3
        % show original blurred image
        res = app.img0;
    else
        % linear/cirular PSF
        if psf_type == 1
            psf = motion_kernel(deg2rad(app.angle), app.d, app.ksz);
        else
            psf = defocus_kernel(app.d, app.ksz);
        end

        % deconvolution
        noise = 10^(-0.1 * app.snr);
        [kh,kw] = size(psf);
        PSF = zeros(size(app.img), class(app.img));
        PSF(1:kh, 1:kw) = psf / sum(psf(:));
        PSF = cv.dft(PSF, 'ComplexOutput',true, 'NonzeroRows',kh);
        PSF = bsxfun(@rdivide, PSF, sum(PSF.^2, 3) + noise);
        res = cv.mulSpectrums(app.IMG, PSF);
        res = cv.dft(res, 'Inverse',true, 'Scale',true, 'RealOutput',true);
        res = circshift(res, floor(-[kh kw]/2));

        % visualize PSF kernel (overlaid on top of output image)
        if psf_viz
            res(1:kh,1:kw) = psf;
        end
    end

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

function h = buildGUI(app)
    %BUILDGUI  Creates the UI

    % parameters
    sz = size(app.img0);
    sz(2) = max(sz(2), 300);  % minimum figure width

    % build the user interface (no resizing to keep it simple)
    h = struct();
    h.fig = figure('Name','Deconvolution', ...
        'NumberTitle','off', 'Menubar','none', 'Resize','off', ...
        'Position',[200 200 sz(2) sz(1)+105-1]);
    if ~mexopencv.isOctave()
        %HACK: not implemented in Octave
        movegui(h.fig, 'center');
    end
    h.ax = axes('Parent',h.fig, 'Units','pixels', 'Position',[1 105 sz(2) sz(1)]);
    if ~mexopencv.isOctave()
        h.img = imshow(app.img0, 'Parent',h.ax);
    else
        %HACK: https://savannah.gnu.org/bugs/index.php?45473
        axes(h.ax);
        h.img = imshow(app.img0);
    end
    h.txt(1) = uicontrol('Parent',h.fig, 'Style','text', ...
        'FontSize',11, 'HorizontalAlignment','left', ...
        'Position',[5 5 100 20], 'String','SNR (dB):');
    h.txt(2) = uicontrol('Parent',h.fig, 'Style','text', ...
        'FontSize',11, 'HorizontalAlignment','left', ...
        'Position',[5 30 100 20], 'String','Diameter:');
    h.txt(3) = uicontrol('Parent',h.fig, 'Style','text', ...
        'FontSize',11, 'HorizontalAlignment','left', ...
        'Position',[5 55 100 20], 'String','Angle:');
    h.txt(4) = uicontrol('Parent',h.fig, 'Style','text', ...
        'FontSize',11, 'HorizontalAlignment','left', ...
        'Position',[5 80 100 20], 'String','PSF:');
    h.slid(1) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',app.snr, 'Min',0, 'Max',50, 'SliderStep',[1 5]./(50-0), ...
        'Position',[105 5 sz(2)-105-5 20]);
    h.slid(2) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',app.d, 'Min',1, 'Max',50, 'SliderStep',[1 5]./(50-1), ...
        'Position',[105 30 sz(2)-105-5 20]);
    h.slid(3) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',app.angle, 'Min',0, 'Max',180, 'SliderStep',[1 10]./(180-0), ...
        'Position',[105 55 sz(2)-105-5 20]);
    h.pop = uicontrol('Parent',h.fig, 'Style','popupmenu', ...
        'Position',[105 80 100 20], 'String',{'Linear','Circular','-None-'});
    h.cbox = uicontrol('Parent',h.fig, 'Style','checkbox', ...
        'Position',[210 80 100 20], 'Value',1, 'String','Show PSF');

    % hook event handlers, and trigger default start
    set([h.slid, h.pop, h.cbox], 'Callback',{@onChange,h,app}, ...
        'Interruptible','off', 'BusyAction','cancel');
    onChange([],[],h,app);
end