用代码绘制道路编号标志

用代码绘制道路编号标志
丁俊尧起因
如果你看行车记录 POV 视频的话,很容易看到这类视频都会把行经的公路编号名称,以路牌的形式贴在封面和视频内容中。
我的话,往往会做一块道路指示牌,把沿途的道路和城市写上去:
最近有人推荐我在视频里面添加道路信息之类的集成信息。
其实最开始,我做南京绕城公路环线的 POV 视频时,就想添加一个滚动的地图,放在视频下方,这样就只需要靠地图上的信息来指示当前所行道路和沿途地标了。为此,我截了三四十张高德地图上的图,花了好大劲拼接起来。最后发现地图图片太大了(宽高都到达了五位数),我的电脑剪 4K 视频本来就不太流畅,加上那张图就更不行了。
于是,我后来放弃了这种思路,转而专门追求画质。关于道路信息,就拿手机记录轨迹,然后生成轨迹动画,嵌入到视频里面。
要不是实现的效果还是不太理想,我早就写文章描述自己怎么做的了。如果设备允许,还是导航录屏比较好一些:地图自动缩放,也有道路信息和方向。除了实现方式不那么优雅,其余的没什么缺点。很可惜,苹果不支持 CarPlay 录屏,想要录屏就必须牺牲车载屏幕的功能。所以我一般都是用安卓的备用机,一次开两个导航。
当然,换了显卡之后,在电脑能够带动的情况下,还是花些时间加上道路信息比较好。
我先是参照现在的车载 HUD(说是参照,但是最多也只是灵感来源,毕竟车载 HUD 和播放记录视频还是有很大区别的),做了个很潦草的示例图:
很明显,如果要添加方位和时速(虽然上面的图里面没有)之类的信息,必然要根据轨迹信息自动化生成。其他的信息理论上也可以自动生成,但是考虑到各大地图厂商的 API 调用费用,相对于自己做视频几乎可以忽略不计的收入来说,实在是太贵,现阶段要是需要做的话,还是手工处理吧。
但是,就我的个人经验来看,如果一个流程的一部分需要自动化处理,最好将其他部分也尽可能自动化处理。比如说关于上述的信息,我的大致思路如下:
- 根据记录轨迹生成的 GPX 文件,计算出每个记录点(我设置为每秒记录一次)的方位角、瞬时速度、已走路程,加上本身记录的经纬度、时间(包括绝对时间和相对于出发时的秒数),转换为 CSV 文件
- CSV 文件添加区划、道路、限速、背景音乐等列
- 根据修改后的 CSV 文件,生成每一帧,方便后面覆盖在视频上
当然,也可以做一个集成的工具,不用导出再导入,直接在工具里面添加信息。不过,就我这种对网页前端和图形界面算是一窍不通的程度来说,还是目前的想法比较容易实现。
这里面涉及到生成交通标志的部分,尤其是道路编号标志。于是我便想到,如果能自动生成道路编号标志的话,就能大大减少工作量。这也是这篇文章介绍的部分。
当前情况
无论是高速公路还是国道、省道等道路,都有统一的编号标志规范。在 GB 5768.2 中有它们的示例、用法、文字和尺寸要求。
文字的字体也有规范,英文和数字部分很明显就是 FHWA 标准的字符:
甚至交通运输部给了对应的字体。我也是最近几天才知道的,此前一直用 Roadgeek 字体一遍遍调试。
当然,已经有人制作了绝大多数国道和高速公路的标志,从维基百科上的相关地区 / 高速公路的条目里面就能轻松找到对应图片。如果只是找几个图片的话,直接下载就行了,CC0 协议,没有版权限制,还是 SVG 格式的矢量图。
不过,如果要自动化的话,一个一个下载再调用不太现实:总有遗漏的,而且也就高速公路和国道比较全,有人愿意为爱发电做图,像省道等低等级的公路就少有人做了。我要做的话,就要做得尽量能够通用一些。
况且,如果有时间精力的话,我还想做一下其他道路标志的自动化生成——目前我的做法是在 Adobe Illustrator 绘制的,虽然没那么麻烦,但是如果能够只输入几个字符就能生成一个用在封面的道路标牌的话,还是很有吸引力的。以前有一个工具可以生成,应急的话还可以,我用过两三次,但是后来变成了邀请制,我也不喜欢加 QQ 群(大学毕业后我基本上不用 QQ 了,更何况是为了用一个工具就要加群填问卷才能拿到账号),就算了,自己也不是不会做。
Adobe Illustrator 的简称 AI 用了几十年了,但是最近几年,人工智能火起来,直接拿这个简称称呼它也就很怪了。本文如无特殊说明,所述的 AI 均指人工智能。
其实还有一个原因:上述途径得到的编号标志颜色太亮,放在视频里面格格不入。如果可以的话,我希望能够自定义颜色,换个不那么刺眼的颜色,毕竟国标里面没有规定必须使用某个色号,只是给了颜色范围和检验方法——更何况我只是做视频,还是以视频观感为重。
模板制作
为描述方便,以下的步骤并非实际经历,有合并重组。自己实现花费的时间远超描述的这些步骤。
参照国标,在 Adobe Illustrator 里面绘制好示例图像。国道、省道等可以共用一套模板(后期改一下颜色就行),高速的话就要分编号长度,以及是否包含名称,制作出一整套模板:
图上的模板我改过了:因为国标里面没有写明标志四周圆角的半径,我参照给的图片,测量计算并调整了圆角半径;而且后来我突发奇想,让国家高速的牌子和省级高速的牌子共用一套模板,文字也改成通过代码添加。
对于四位编号的高速公路,我没在国标上面找到相关的尺寸规范(除了整个面板的宽高),就拿示例图片和其他牌子调整了。
将每个图形按画板导出为 SVG 格式,样式改为内联样式。
手动给导出的 SVG 文件中的各个路径添加
class
,以便后面填色使用。比如说,国道、省道、县道、乡道可以共用一套模板,我们可以把背景和前景色分别设置为同一个
class
,在生成 SVG 时识别 class
并填相应颜色:
1 |
|
添加文字
思路简单来说就是:将所需文字导出为 SVG 格式,再将其放在指定的位置,再导出文件。
通过询问 AI,我了解到 Python 包里面,svgpathtools 能够很方便地读取 SVG 文件。
1 | from svgpathtools import svg2paths |
但是实际使用过程中,我发现它读取路径是无序的,返回的列表顺序和 SVG 实际图层关系对不上。有人修改了这个 Bug,但不知为何,原作者好久没更新了,代码也迟迟没有合并。可以在这里下载修改后的代码安装。
我在这里使用该工具,主要是为了计算文字排入的区域,转换为合适大小的文字路径:
- 根据道路类型选取模板、待填入文字的位置和宽高
- 获取每个字的路径(需要注意,获取到的路径宽高、起始坐标会有差异)
- 将字符路径等比例缩放到指定高度,假设两端对齐,计算文字之间需要留空白的宽度
- 获取到上面的信息后,变换文字路径到与模板相同的坐标系,获得待输入字符的路径,预备后面填入模板中
正常情况下,文字高度并非一成不变;只不过,考虑到目前的场景里面暂时只会有汉字、数字和若干个大写字母,故可以无脑将文字的高度设为一致。
待填入文字的位置、宽高在制作模板的时候就能够获得,作为常量写在代码里面就行。
以下函数生成待输入字符的路径。其中
char_to_svg_path(font, i)
的作用是:返回给定字体下给定文字的 SVG 字形路径,数据类型为
svgpathtools.path.Path
。关于这个后面会说。
1 | def calculate_scaled_char_info(code: str, start_x: int | float, start_y: int | float, width: int | float, height: int | float, font: str): |
对于多处文本(比如说带路名的高速公路标志),可以分别处理各处的文本,反正最后放一起就行了。
读取字形
仍然是通过 AI,我了解到,可以使用 FontForge 处理字体。但是它并没有告诉我:FontForge 虽然支持 Python 调用,但是必须通过其捆绑的 Python 来调用,你无法在其他 Python 环境直接调用 FontForge。
于是我想到了一个方法:将字体内全部字形分别导出为 SVG 文件(文件名为 Unicode 码位)。生成牌子时,选取已保存的字形就可以了。这样部署环境时,也不需要装 FontForge 了。只是过多的小文件会占用非常多的磁盘空间。
FontForge 的 GUI 界面只提供了将单个字形导出为 SVG 文件的功能,如果想导出全部的文件,必须写脚本:
1 | import fontforge |
由于调用了 FontForge,故该脚本必须通过 FontForge 捆绑的 Python 来执行。但是也只是这一个脚本。
最开始我还没发觉有什么问题。但是后来处理思源黑体的时候,我发现了一个很大的问题。
用思源黑体而非交通运输部给的字体,是因为我看到字体的版权是华文的。英文和数字显然是来源于 FHWA 标准的字符,但是汉字部分可不是。简单对比明显是华文黑体粗体,非免费字体。
考虑到汉字部分恐涉及版权纠纷,故改用思源黑体。对于编号标志,使用汉字的部分目前不可能有英文和数字,故如果仅考虑编号标志的场景的话,汉字部分使用思源黑体是可以的。
官方提供的 OTF
文件用上面的脚本处理,只能输出五十多个字符,明显不符合字体里面的字量。于是我好不容易拿
FontForge
重新导出了字体,希望换一下格式(TTF)能够解决这个问题。拿改过后的字体导出文件,确实能够导出不少字。但是试图生成标志的时候才发现,为什么连“高”这个字都没有?一看才发现,思源黑体里面的“高”(U+9AD8
)的字形来源于康熙部首的字“⾼”(U+2FBC
),如果拿上面的代码,是遍历不到
U+9AD8
的“高”的。
我对字体相关的信息不太了解,如有错误,敬请指出。
看来这种方法还是不稳定。我再次询问 AI 时,得知可以用 fontTools 包来导出某字体的某字形,而且不需要另外安装什么软件:
1 | from fontTools.pens.svgPathPen import SVGPathPen |
最后要翻转才能得到正确的字形,还是我自己发现的。
拼合模板和字符
仍然是咨询 AI 得知,可以使用 svgwrite 来更方便地生成 SVG 文件。
我写文章的时候才发现,这个工具已经不更新了……
但是,svgwrite
只能从头开始生成 SVG,不支持在已有的 SVG
基础上修改保存。所以可以通过之前读取、生成的
Path
,逐个写入,最后生成文件。
首先要新建一个和模板相同大小的文件:
1 | dwg = svgwrite.Drawing('output.svg', size=('400', '200')) |
不过,每个模板的宽高都不一样。虽然可以把模板宽高写死在代码里面,但是我还是希望能够通过读取模板宽高来得到这两个数。用以下函数解决:
1 | def get_svg_dimensions(svg_path: str): |
然后将路径逐个写入 dwg
中,同时可以根据此前维护的
class
重新填色了,参考下面的语句:
1 | for path, attr in zip(paths, attributes): |
最后保存:
1 | dwg.saveas('路径') |
这样就生成了一张 SVG 图片了。
实现效果
完整代码见 GitHub 上的 DingJunyao/gpxutil 项目里面对应的文件。不过这个项目的名字我还没有定好,如果有修改的话,直接到我的资料页面找就行了。
通过代码生成的编号标志如下所示:
目前存在的问题也很明显:
- 汉字的字形看上去比国标的偏大。不知道是什么方面的原因,大概是因为字体本身四周也有间距,我处理的时候把间距忽略掉了。但是字母和数字就没有这种问题。
- 带名字的高速路牌两边间距太小。我看了一下国标,尺寸没什么问题。
- 四位编号的路牌,“11”这种字本身就窄的编号的观感不佳。国标上的示例似乎是把大编号靠右对齐,小编号靠左对齐。但是就我现在的思路,做到这个不太容易。
不过,也是一个比较好的开始吧。