前言

学完图像处理后,自己想写这样一个程序:

输入一张图,输出此图的一笔画方案,并将路线用动态图方式展示

比如说:

输入:

输出:

一. 思路

  1. 首先,对于输入的图像,可以先进行特征提取,把图像数据转化为数学模型(矩阵)表达。
  2. 然后,根据矩阵计算该图的一笔画解。
  3. 最后,将解转化为图像表达。

计算机毕竟不是人类,虽然我们脑海中的计划没问题,但是具体到实现方式上还是涉及很多边边角角的细节。

首先,提取原图的所有点元素集合和边元素集合:

然后,两者分别按连通域拆分成多张子图:

最后,一张一笔画动态图,可通过这 13 张分开图像的合理排列,逐步叠加生成。

二. 问题与解决

将实践上的主要困难记录如下:

1. 一张图有可能一笔画,也有可能不能一笔画。不能一笔画的时候怎么处理?

一笔画问题中,是否有解与图中奇点数目有关。所谓奇点,就是那些邻接的边数为奇数的点。分三种情况

  • 没有奇点:说明此图可以一笔画
  • 一个奇点:说明此图可以一笔画,但是得选对起点,起点就是这个唯一的奇点
  • 两个奇点或以上:此图不能一笔画

注意到第二步维护一个邻接矩阵的时候,我们就可以得到图中的奇点数,所以在这一步进行处理。
如果奇点数小于等于 1,继续;否则提示无一笔解,退出。

2. 如何从一张图中提取所有点&边结构?

首先二值化,提取图像脉络图,得到预处理图 g
点的获取:用点检测算法提取所有点,按连通域分开,得到每个点
边的获取:将所有的点膨胀,用它们腐蚀 g,把原先一个连通图分割为多个连通子图,每个子图就是一条边

3. 如何存储一个邻接矩阵(获取图中的点边关系)?

用连通域
比如上面 5 张点图(设为 a1, a2, ..., a5) 和 8 张边图(b1, b2, ..., b8),做一下笛卡尔积,两两组合
然后每张合成图算一下连通度。
连通度为 1,说明该点与该边存在邻接关系;否则,说明该点该边不存在邻接关系

4. 如何从邻接矩阵计算该矩阵所代表图的一笔画序列?

关于这个问题,《离散数学》图论有一节欧拉算法
要理解这个算法,首先要了解桥的概念

桥的概念,指图中这样一条边,删除这条边会使原图分成两半。可以通过下图直观理解

两个五角星中间有三条边连接,这三条边都不是桥,因为切断之后两边保持联系
但是三条边删除任意两条,剩下的一边是桥。因为切断之后就从一个整体变成两部分。两边相互无法联系
这是我从这个项目写的另一篇文章 桥检测算法,可以检测一张图中的桥。只是这里知道邻接矩阵的情况下没有必要用它。

然后是欧拉算法

从第一点出发,选择一条非桥的边。走过这条边,然后将它从图删除。从抵达点继续出发,重复前面的过程,直到删完图中所有边。

流程并不复杂,但将书上的理论转化为代码实际困难的是下一个问题

5. 输入一个邻接矩阵,如何判断任意一条边是否为 “桥”?

这是项目最后的困难。首先凭我之前经验是搞不定的,后来查阅资料找到一本书,查到了拉普拉斯矩阵这一概念。它有一个很好的特性,刚好能解决问题

拉普拉斯矩阵,特征值中0出现的次数就是图连通区域的个数

于是,删除该边,得到新的邻接矩阵,统计一下对应拉普拉斯矩阵特征值 0 的个数就行
小于等于 1,即少了此边连通区域还是 1 个,说明非桥。否则,此边是桥

所有的 critical point 打通了,现在就是组合 api 将项目实现

三. 实现

根据思路一开始提到的 3 点,分三个模块编写代码将各自的逻辑一一实现。每个模块都是可以独立完成任务的小工具。

  • 拆图模块:输入一张图像,输出该图的邻接矩阵。并抽取离散的边帧,点帧
  • 排序模块:输入一个邻接矩阵,输出该矩阵的一笔画序列(序列数组)
  • 合成模块:输入边帧,点帧和序列数组,按指定的顺序叠加动态图

拆图模块:输入一张图像,输出该图的邻接矩阵。并抽取离散的边帧,点帧

g2m.m

%% 获取一张图像的邻接矩阵,边标号矩阵,散边图像与散点图像
%% 输入:一张图像 f 与点检测规模 scale
%% 输出:该图的邻接矩阵 gm,边索引矩阵 emap,散边图像 elabel 与散点图像 plabel
%% 注:点检测规模可不填,默认为10,当运行出错时赋一个较大的数可解决。

function [gm emap elabel plabel]=g2m(f,scale)
    if nargin==1
        scale=10;
    end
    g=f;
    g=im2bw(g);
    g=~g;
    h=bwmorph(g,'thin',inf);    %提取图像脉络

    model=zeros(size(g));
    points=catch_point(f);  % 用catch_point函数检测输入图像的红点
    if isempty(points)  %如果没红点,手工输入图像的点
        imshow(f);
        points=ginput;
        points=round(points);
        close all;
    end
    for i=1:length(points(:))/2
    model(points(i,2),points(i,1))=1;   %model为细点图像
    end
    se= strel('disk',scale);   %利用半径为5的圆盘进行点膨胀
    fse=imdilate(model,se); %fse为粗点图像
    plabel=fse;
    elabel=h.*~fse;   %elabel为去点的散边图

    [p pnum]=bwlabel(fse);
    [e enum]=bwlabel(elabel);
    % figure,imshow(p);
    % figure,imshow(e);
    point={};   %维护点元素元胞
    for i=1:pnum
        p2=p;
        p2(p2~=i)=0;
        point{i}=p2;
    end
    edge={};    %维护边元素元胞
    for i=1:enum
        e2=e;
        e2(e2~=i)=0;
        edge{i}=e2;
    end

    gm=zeros(pnum); %初始化邻接矩阵
    emap=[];    %初始化边索引矩阵
    temp={};
    for i=1:enum   %分析每一条边与所有点的邻接情况,维护邻接矩阵
        link=[];
        [~,num1]=bwlabel(edge{i});
        for j=1:pnum
            temp{j}=edge{i}+point{j};
    %         figure,imshow(temp{j});
            [~,num2]=bwlabel(temp{j});
            if num1==num2  %如果加点前后连通域个数不变,将点号计入link数组
                link=[link j];
            end
        end
    if size(link,2)==1  %环情形把j点维护为与i点相同
            link(2)=link(1); 
            gm(link(2),link(1))=gm(link(2),link(1))+1;
            emap(link(1),link(2),gm(link(1),link(2)))=i;    %维护边索引矩阵
            emap(link(2),link(1),gm(link(1),link(2)))=i;
        else
            gm(link(1),link(2))=gm(link(1),link(2))+1;  %维护邻接矩阵
            gm(link(2),link(1))=gm(link(2),link(1))+1;
            emap(link(1),link(2),gm(link(1),link(2)))=i;    %维护边索引矩阵
            emap(link(2),link(1),gm(link(1),link(2)))=i;
        end
    end
end

排序模块:输入一个邻接矩阵,输出该矩阵的一笔画序列(边序数组)

m2seq.m

%% 根据一个邻接矩阵及边标号矩阵用算法算出一笔画边序列
%% 输入:一个邻接矩阵gm及对应的边索引矩阵emap
%% 输出:边点序列

function seq=m2seq(gm,emap)
    i=1;    %因是欧拉回路,故任选第一点为起点
    pnum=length(gm);    %获取点数
    vis=zeros(1,pnum);  %点访问数组
    seq=[];
    while sum(gm(:))~=0    %重复以下步骤直到邻接矩阵为空(无边可取)
        if vis(i)==0;      %如当前访问点未写入序列,将该点写入序列并标记“已写入”
            seq=[seq -i];
            vis(i)=1;
        end
        pana=[];    %pana数组存放对一个点所连各边的性质分析
        for j=1:pnum
        pana(j)=1-is_bridge(gm,i,j);
        end
        x=find(pana==1);    %x代表非桥边的集合
        y=find(pana==0);    %y代表桥的集合
        if ~isempty(x)  %优先走非桥的边
            x=x(1);
            seq=[seq emap(i,x,gm(i,x))];
            gm(i,x)=gm(i,x)-1;
            gm(x,i)=gm(x,i)-1;
            i=x;
        elseif ~isempty(y)  %无非桥的边时只能走桥
            y=y(1);
            seq=[seq emap(i,y,gm(i,y))];
            gm(i,y)=gm(i,y)-1;
            gm(y,i)=gm(y,i)-1;
            i=y;
        else    %无路可走,说明不能一笔画,邻接矩阵不可能为空,强制跳出
            break;
        end
    end
end

合成模块:输入静态帧和序列数组,按指定的顺序叠加动态图

seq2gif.m

%% 根据散边图与散点图用顺序数组组织成gif动态图
%% 输入:散边图像elabel,散点图像plabel,以及顺序数组seq
%% 输出:一张“一笔.gif”图像
%% 第四参数可不管,若输入,可控制动态图速度,供调试用

function seq2gif(elabel,plabel,seq,speed)
    [l2,~]=bwlabel(elabel);   %为散边图标记连通域
    [l4,~]=bwlabel(plabel);   %为散点图标记连通域
    stepall=length(seq);
    frame={};   %帧图元胞
    for i=1:stepall
        if seq(i)<0     %负序列元素代表取点
            seq(i)=-seq(i);
            l=l4;
        else        %正序列元素代表取边
            l=l2;
        end
    l(l~=seq(i))=0;
    if i==1
        l3=l;
    else
        l3=l3+l;
    end
    frame{i}=l3;
    % picname=[num2str(i) '.png'];
    % imwrite(l3,picname);   %此两行按序生成多帧gif过程图像*.png
    end
    if nargin<4
        speed=0.2;
    end

    for i=1:stepall    %把多张图叠加为gif
        if i==1
            imwrite(~frame{i},'一笔.gif','gif','Loopcount',inf,'DelayTime',speed);
            else
            imwrite(~frame{i},'一笔.gif','gif','WriteMode','append','DelayTime',speed);
        end;
    end
end

然后编写库函数,为上级调用提供工具

catch_point.m

%% 获取一张图像的红点坐标
%% 输入:RGB图片矩阵f
%% 输出:所有红点(红色区域)中心坐标
function point=catch_point(f)
    h=zeros(size(f,1),size(f,2));
    h(f(:,:,1)>f(:,:,2)+f(:,:,3)+100)=1;    %搜索所有红点
    [l num]=bwlabel(h);     %统计所有红点中心,计入point数组
    stats=regionprops(l);
    point=[];
    for i=1:num
        point=[point;round(stats(i).Centroid)];
    end
end

is_bridge.m

%% 输入:邻接矩阵与其某边的两点序号  输出:如果此边不存在,返回2
%% 如果此边是桥,返回1,如果此边非桥,返回0
function judge=is_bridge(a,i,j)
    if a(i,j)==0 
        judge=2;
    else
        oparts=pcounts(a);
        a2=a;
        a2(i,j)=a2(i,j)-1;
        a2(j,i)=a2(j,i)-1;
        aparts=pcounts(a2);
    if aparts==oparts
        judge=0;
    else
        judge=1;
    end
end

pcounts.m

%% 输入:邻接矩阵  
%% 输出:连通分支数  
%% 注:第二输入意为孤立点不记为连通分支,
function parts=pcounts(A,x)
    D=diag(sum(A));
    single=length(find(sum(A)==0));
    L=D-A;  %求解原图的拉普拉斯矩阵
    n=eig(L);
    parts=length(find(n<1e-10));  %计数拉普拉斯矩阵0特征值个数
    if nargin==2
        parts=parts-single;
    end
end

最后给入口函数

main.m

f=imread('五角星.png');
[gm emap el pl]=g2m(f,10); %获取图像邻接矩阵,边索引矩阵及散边图像(R10圆盘蚀点)
seq=m2seq(gm,emap);        %根据前两个矩阵计算一笔画边序列
seq2gif(el,pl,seq);        %根据后两张图像与边序列生动态图 0.2表示每0.2s换一帧

四. 效果