利用matlab将位图转为SVG矢量图_位图转svg-程序员宅基地

技术标签: matlab  位图转矢量图  svg  矢量图  位图  


惯例声明:本人没有相关的工程应用经验,只是纯粹对相关算法感兴趣才写此博客。所以如果有错误,欢迎在评论区指正,不胜感激。本文主要关注于算法的实现,对于实际应用等问题本人没有任何经验,所以也不再涉及。

0 前言

位图转矢量图的方法有很多,有的是利用线条特征转换,有的是利用颜色特征转换。根据目的来说,各有优缺点。
线条特征转换主要根据线条的走势,利用直线或贝塞尔曲线之类的线条拟合原位图,多用于线框文字类的位图图片。基于颜色特征的则关注位图的颜色分布,利用多边形或曲线围成的图形来拟合各个色块组成的图像。

本文主要算法为利用颜色特征进行转换。
算法思路为:1图片中值滤波,减少噪声。2将图片的颜色提取,并减少图形的颜色。3将不同颜色代表的图层进行图形分割,分割为若干个独立的多边形。4将这些多边形转换为矢量图的多边形,赋予颜色。

1 算法思路

1.1 读取图片

以matlab自带图片pepper
在这里插入图片描述

1.2 中值滤波

滤掉噪声,去除某些零星的颜色分布,使得生成的颜色能够代表整个图形。
因此,采用中值滤波。滤波效果如下:
在这里插入图片描述

1.3 中值滤波

利用rgb2ind函数,将图片的颜色数量减少到指定的数量。此时生成一个X索引矩阵,和RGB格式的map地图。X中每一个值代表一个颜色,具体颜色可以根据索引在map里找到。使得之后多边形的数量和颜色为一个有限值。
在这里插入图片描述

1.4 去除孤立的像素

可以看到1.3背景还有很多噪点形状的像素块,比如图片上方的绒布和图片下方的绒布。
这些像素块是由于颜色介于两者之间,导致分布极为零星破碎。

因此,为了减少多边形数量,我们找到这些孤立的像素,使得它的颜色等于周边像素的颜色。
方法为遍历每一个像素,如果该像素与周边8个像素中少于2个相同,则代表该像素比较孤立,应该同化为周围的像素。

去除之后的结果如下,可以看到幕布的的颜色变为了几个整颜色。水果上的某些噪点类似的颜色也被剔除。
在这里插入图片描述

1.5 提取单独的颜色

将1.4得到的新的X索引进行单独的颜色提取。以BW=(X==1)为例,此时提取出来了一个只有0和1的图像。
根据1.4的结果对比,可以看到是深红色的图形被提取了出来。

这也可以看做是图像分割。

在这里插入图片描述

1.6 找出二值图像中所有的连接体

将上面的图形区域分割,把每一个连通区域都找到,并进行边缘的记录。
当然,matlab有专门的函数来处理这个问题,分别为bwconncomp进行分割,boundary进行边缘的提取。

然而boundary提取边缘的时候,只能进行凸包提取,中间的孔洞和凹陷很容易忽略掉。

所以为了将这种影响减少到最小,我们把面积大的图形先绘制,面积小的图形后绘制。这样如果出现孔洞没有被算作边缘时,先画大图当背景,之后面积小的孔洞在后面绘制时,就会显示在这个大图图层的上方,不被影响。

之后boundary提取边缘得到的就是最终绘制矢量图所需的多边形了。

在这里插入图片描述

1.7 将提取出来的每个多边形膨胀

之后,按照多边形面积大小依次绘制多边形,并填充颜色。

可以发现,每个多边形之间会出现缝隙。这是因为在提取多边形的时候,是按照像素点中心处的坐标提取,和实际位图像素的边界相差约0.5个像素。相邻的多边形每个都相差0.5个像素,就会导致最终出现大约1个像素左右宽的缝隙。

如下图所示:
在这里插入图片描述
因此,需要对每一个多边形进行膨胀,在不改变图形的外形条件下,向四周均匀膨胀1个像素左右的距离。

原理采用基于多边形顶点来进行膨胀,如下图所示:
在这里插入图片描述
把图形言逆时针依次标号,蓝色向量为沿着标号方向,指向下一个点的向量。红色向量为上一个蓝色向量平移过来的向量,也就是说,红色向量2等由蓝色向量1平移得到,红色向量3等由蓝色向量2平移得到。

我们规定膨胀的方向为,向着红色向量方向走一步,再向着蓝色向量方向相反的方向走一步,如点4处的示意。

如果遇到图形凹陷,膨胀的方向与凸点相反。判断凹陷点和凸出点的方法为,计算红色向量与蓝色向量之间的夹角(逆时针),超过180°则为凹点。这里可以用叉乘来间接计算出sin值,来进行判断。

当然最终结果不能保证边缘与原图像平行。如果想要平行,需要在最终合成的向量那里除以sin值。但是后来发现实际效果并不好,它会把某些特别尖的尖点过于放大。

matlab中实际效果大致如下:
在这里插入图片描述

实际应用到图形中的效果如下:
在这里插入图片描述
可以看到基本做到了严丝合缝的效果。

1.8 输出为SVG格式

之所以选择SVG格式,是因为它利用XML语言,可以直接用txt等文本软件读取,方便matlab的编程写入。
基本语法为:

<svg width="500" height="500"> 
具体内容
</svg> 

这里用到的主要对象为多边形polygon,fill代表填充颜色,stroke代表线条颜色,stroke-width代表线条宽度,points代表组成多边形的点,x,y一组,首尾相连。

<polygon fill="rgb(186,157,45)" stroke="rgb(186,157,45)" stroke-width="1" points="326,220 326,222 328,222 328,220 326,220"/> 

2 最终结果展示

前面的例子中,利用matlab自带的图片尝试了svg转换。虽然不是卡通风格,但是也验证了程序的通用性。

这里采用https://getavataaars.com/网站随机头像生成器,作为另一个示例。这张相比于前面的照片案例,更适合转换为位图。
在这里插入图片描述
标准的png格式位图如上所示。
然而CSDN不支持SVG格式的图像,所以采用截图导出的方式展示SVG格式。
在这里插入图片描述
这里背景是黑色,是因为程序暂时还没有识别透明色的这个功能。所以把透明的背景默认为了黑色。

有些棱角、尖点有些失真,这是由于之前采用了中值滤波造成的。对于这种画风简单,色块清晰的图片,没必要采用中值滤波。

下图为不采用中值滤波处理后,生成的图片。可以看到原本的细节几乎都能够较好的保存下来。眼睛处的圆形有些不光滑,这是多边形拟合的缺陷。骷髅嘴那里有些内部锐角消失,这是由于boundary函数进行边缘提取的时候,难以识别凹陷所导致的。
在这里插入图片描述
总体来说,效果还是达到了理想中的效果。

3 完整的Matlab代码

clear
clc
close all
%把位图转换为矢量图

%% 1初始设置
%导入图片
IM_Origin=imread('peppers.png'); 

%导出名称
filename='peppers_hyh.svg';

%输出的颜色数量(约多约接近原图,不过一般卡通图本身颜色数量就不多,所以也没必要太多)
ColorNum=27;

%原始图片
figure()
imshow(IM_Origin)

%中值滤波(对于简单图案,可以不进行滤波)
IM_Origin=IM_RGB_medfilt(IM_Origin,5);%中值滤波,去除噪点

%% 2图像处理
%图像基本信息
IH=size(IM_Origin,1);%图像的高度
IW=size(IM_Origin,2);%图像的宽度
IN=IH*IW;%图像的像素总数

%图片颜色量化
%[X,cmap]=rgb2ind(IM_Origin,0.25,'nodither');
[X,cmap]=rgb2ind(IM_Origin,ColorNum,'nodither');

%去除掉周围只有2个像素的图像
X=Del_1_Pix(X);
X=Del_1_Pix(X);

%生成去除单独像素后的图像(预想结果图)
figure()
IM_Reduce2 = ind2rgb(X,cmap);
imshow(IM_Reduce2)

%对每一个颜色进行分割,保存轮廓与颜色信息
temp=0;
for k=1:size(cmap,1)
    BW_k=(X==(k-1));%索引对应的分别为0到(N-1),所以要减1
    C_BW_K=bwconncomp(BW_k);%进行区域分割识别
    Pix_Area=C_BW_K.PixelIdxList;
    N_Area=numel(Pix_Area);
    for m=1:N_Area
        Pix_Area_m=Pix_Area{
    m};%找到对应的区域
        [Pix_Area_m_X,Pix_Area_m_Y]=ind2sub([IH,IW],Pix_Area_m);%转化为坐标
        B_id=boundary(Pix_Area_m_X,Pix_Area_m_Y,0.9);%提取边缘,找出边界索引
        BD_Area_m_X=Pix_Area_m_X(B_id);%边界的x坐标
        BD_Area_m_Y=Pix_Area_m_Y(B_id);%边界的y坐标
        %保存
        temp=temp+1;%临时计数用
        PolySave(temp).AreaX=BD_Area_m_X;
        PolySave(temp).AreaY=BD_Area_m_Y;
        PolySave(temp).Color=cmap(k,:);
        PolySave(temp).AreaSum=numel(Pix_Area_m);%计算面积
    end
end
%按照面积排序
N_Polygon=numel(PolySave);
Area_Max2Min=zeros(N_Polygon,1);
Area_List=zeros(N_Polygon,1);
for k=1:N_Polygon
    Area_List(k)=PolySave(k).AreaSum;%提取出面积
end
[~,Area_Max2Min]=sort(Area_List,'descend');


%将每个多边形向外扩展1个像素
for k=1:N_Polygon
    if ~isempty(PolySave(k).AreaX)
        if Area_List(k)<9
            R=0.5;
        elseif Area_List(k)<16
            R=1;
        else
            R=1.5;
        end
        [xE,yE]=PolyExpand(PolySave(k).AreaX,PolySave(k).AreaY,R);
        xE(xE<1)=1;xE(xE>IH)=IH;%防止超出画布边界
        yE(yE<1)=1;yE(yE>IW)=IW;
        PolySave(k).AreaX=xE;%保存
        PolySave(k).AreaY=yE;
    end
end

%模拟利用多边形绘制矢量图(实际结果图)
figure()
set(gca,'YDir','reverse');
xlim([1,IW]);ylim([1,IH]);
axis equal
hold on
for k=1:N_Polygon
    ID_k=Area_Max2Min(k);
    fill(PolySave(ID_k).AreaY,PolySave(ID_k).AreaX,PolySave(ID_k).Color,'EdgeColor','none')
end
hold off


%% 3保存为svg文件
%创建文件
f_id=fopen(filename,'w');

s=['<svg width="',num2str(IW),'" height="',num2str(IH),'">'];
fprintf(f_id,'%s \r\n',s);
%中间添加各个多边形
for k=1:N_Polygon
    ID_k=Area_Max2Min(k);
    xSVG_k=PolySave(ID_k).AreaY;
    ySVG_k=PolySave(ID_k).AreaX;
    cSVG_k=PolySave(ID_k).Color;
    if numel(xSVG_k)>3
        %坐标转换
        str_sum='';
        for m=1:numel(xSVG_k)
            str_1=[num2str(xSVG_k(m)),',',num2str(ySVG_k(m))];
            str_sum=[str_sum,str_1,' '];
        end
        str_1=[num2str(xSVG_k(1)),',',num2str(ySVG_k(1))];
        str_sum=[str_sum,str_1];
        %颜色转换
        str_1=['rgb(',num2str(cSVG_k(1)*255),',',num2str(cSVG_k(2)*255),',',num2str(cSVG_k(3)*255),')'];
        s=['<polygon fill="',str_1,'" stroke="',str_1,'" stroke-width="1" points="',str_sum,'"/>'];
        fprintf(f_id,'%s \r\n',s);
    end

end
%结束
s=['</svg>'];
fprintf(f_id,'%s \r\n',s);
fclose(f_id);



%% 其它用到的函数
function IM2=IM_RGB_medfilt(IM1,windows)
%RGB图片的中值滤波
IM_R=medfilt2(IM1(:,:,1),[windows,windows]);%5×5的中值滤波
IM_G=medfilt2(IM1(:,:,2),[windows,windows]);
IM_B=medfilt2(IM1(:,:,3),[windows,windows]);
IM2=uint8(zeros(size(IM1)));
IM2(:,:,1)=IM_R;IM2(:,:,2)=IM_G;IM2(:,:,3)=IM_B;
end

function X3=Del_1_Pix(X)
%去除孤立的像素。如果该像素周围只有2个和它一样的像素的话,就认为它是孤立的
%耗时较长
%图像基本信息
IH=size(X,1);%图像的高度
IW=size(X,2);%图像的宽度
IN=IH*IW;%图像的像素总数
%X=double(X);不能double,浮点数从1索引,整数从0索引,不一样
X2=NaN(IH+2,IW+2);%X2只用于检索
X2(2:end-1,2:end-1)=X;
X3=X;%X3只用于更改
for k=1:IN
    [k1,k2]=ind2sub([IH,IW],k);
    k1=k1+1;k2=k2+1;%由于X2在周围加了一圈,所以索引值也要加1
    %读取周边的9个数值,NaN为辅助值
    X_Id9=X2(k1-1:k1+1,k2-1:k2+1);
    X_IdC=X_Id9(2,2);%中央的点读取
    X_Id9(2,2)=NaN;
    %删除所有的inf
    X_Id9_2=X_Id9(:);
    [NaN_Id9,~]=find(isnan(X_Id9_2));
    X_Id9_2(NaN_Id9,:)=[];
    %判断中间值和周围值是否只存在一个相等的
    if sum(X_Id9_2==X_IdC)<=2
        %如果是,把这个孤立像素替换为周围的某个点
        X3(k)=mode(X_Id9_2);
    end
end
end



function [xE,yE]=PolyExpand(x,y,R)
%将每个多边形向外扩展1个像素
%将每个顶点,沿着相邻两个边 所合成的矢量方向,移动。如果落在图形内部,则反向
%多边形不能自相交
x=x(:);
y=y(:);
%判断是否收尾相交封闭
if x(1)==x(end) && y(1)==y(end)
    x2=x;
    y2=y;
else
    x2=[x;x(1)];
    y2=[y;y(1)];
end
%把点按照指定方向进行排列
Area_xy=trapz(x2,y2);
if Area_xy>0
    x2=flipud(x2);
    y2=flipud(y2);
end
N=numel(x2);
%计算每个点与下一个点相连的向量
Dx=diff(x2);
Dy=diff(y2);
%开始按方向逐点移动
x3=[];y3=[];
for k=1:N-1
    %计算向量a和向量b
    if k==1
        Da=[Dx(1),Dy(1)];
        Db=[Dx(N-1),Dy(N-1)];
    else
        Da=[Dx(k),Dy(k)];
        Db=[Dx(k-1),Dy(k-1)];
    end
    %归一化
    Da=Da/norm(Da);
    Db=Db/norm(Db);
    %如果向量a和向量b差积大于0,则证明是个凹陷点
    Dc=cross([Da,0],[Db,0]);
    Dab_Sin=Dc(3);
    %进行扩展判断
    if Dab_Sin>0
        DR=Da-Db;%如果是凹点
        xy3=[x2(k),y2(k)]+R*DR;
        %xy3=[x2(k),y2(k)]+R*DR/abs(Dab_Sin);保形膨胀,但是实际效果反而不好
        x3=[x3;xy3(1)];
        y3=[y3,xy3(2)];
    elseif Dab_Sin<0
        DR=Db-Da;%如果是凸点
        xy3=[x2(k),y2(k)]+R*DR;
        %xy3=[x2(k),y2(k)]+R*DR/abs(Dab_Sin);保形膨胀,但是实际效果反而不好
        x3=[x3;xy3(1)];
        y3=[y3,xy3(2)];
    elseif Dc(3)==0
        ;%直接跳过
    end
end
xE=x3;
yE=y3;
end
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42943114/article/details/114452416

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签