40只鸟如何优雅排进一张纸:Fugleramme的Spiral与Voids布局算法深度解析
【免费下载链接】fuglerammeBird frame for Raspberry Pi - real-time bird detection by audio, fully local AI, rendered as real, hand-cut 1800s bird illustrations. On an e-ink panel, a TV, or any screen.项目地址: https://gitcode.com/gh_mirrors/fu/fugleramme
Fugleramme 是一个运行在树莓派上的鸟鸣检测相框:它用本地 AI 识别麦克风里听到的鸟,再把每种鸟匹配到一张 19 世纪的手绘博物插画,最后把最多 40 只鸟优雅地排进一张纸。本文深度解析它背后的两套布局算法——Spiral(螺旋)与 Voids(空洞填充),以及"放不下就先缩小、再二分找回"的自动缩放机制。即使你不写代码,也能看懂这两套算法为何能把整页花园鸟排得疏密有致、互不打架。
问题从哪来:一张纸,最多40只鸟
默认配置下,Collage(拼贴)模式会展示观察窗口内听到的所有鸟,上限 40 只(settings.species_limit)。这些鸟有大有小:一只绿头鸭可能比一只柳莺重 100 倍。目标很苛刻:
- 不重叠:任意两只鸟的"实体"像素绝不重叠;
- 不出界:每只鸟(连同它的名字标签)必须完整落在纸上;
- 像样:大鸟居中、小鸟填缝,留白自然,而不是挤成一片。
忙起来的一天,花园里可能真的出现三四十种鸟。效果大致是这样:
两套算法,两种提问方式
Fugleramme 的全部打包逻辑集中在 src/fugleramme/render/packing.py,官方文档 docs/display.md 里对两种布局只有一句话的区别:
| 布局 | 一句话原理 |
|---|---|
| Spiral | 从页面中心往外一圈圈找空位,大鸟在中间,小鸟绕外圈 |
| Voids | 一次性算出整页所有合法位置,专挑"最空"的纸面落子 |
少鸟时两种布局的差异最直观——Spiral 长成中间密、四角空的"圆团",Voids 则把鸟撒向四个角落:
Spiral 布局:一圈一圈向外生长
Spiral 实现见 spiral(),思路朴素到几乎可以手绘:
- 按体重排序,最大的先落子。鸟的显示大小来自真实体重数据 assets/bird_sizes.csv(AVONET 数据集),经 sizes.py 压缩后,最重的鸟也只比最轻的大 2.5 倍左右——大鸟有排面,但小鸟不会消失。
- 沿同心圆环试探。从页面中心出发,每隔 6 像素画一个候选圆环,环上均匀取点,逐个检查"这里放得下吗"。放得下就立刻停手,绝不贪心比较。
- 先查三行,再查全图。检查一只完整插画是否碰撞要扫几千个像素,于是它先挑鸟影上的 3 条"探针行"快速试错——99% 的碰撞位置在这一步就被淘汰,成本只有整盒碰撞检查的百分之一。
最大的鸟(比如一只绿头鸭)总是最先拿到中心位置:
代价也很诚实:Spiral 是贪心的,先来的鸟抢了好位置,后来的小鸟只能在边角挤,整页会形成一个"中间饱满、四角空荡"的圆形团块。它也因此比 Voids 慢约 3 倍。
Voids 布局:一次 FFT 算出所有合法位置
Voids 把问题反过来问:不是一只鸟一个位置地试,而是一次性回答"这只鸟可以放在纸上的哪些格子"。
它的核心工具是互相关——一次 FFT(快速傅里叶变换)就能对纸上每一个偏移位置同时算出"这只鸟放这里会踩到已占用的像素吗"。见 Board.against():
- 碰撞检测:鸟的剪影与已占用页面互相关,结果小于 0.5 的偏移就是合法位置;
- 空度评分:先把"空白纸面"做成一个高斯模糊的场(某格周围越空旷值越高),再与鸟的剪影互相关——"这只鸟盖住多大面积的空纸"一步算完;
- 贴邻偏好:
halo场标记紧贴实体边缘的空格,让小鸟愿意"窝"在邻居身侧或墙角,而不是飘在中央。
于是每只鸟的成本函数是:-空度 - 贴邻系数 × 接触量 + 轻微居中拉力,取最低分的位置落子。大鸟自然铺满整张纸,小鸟钻进剩下的缝隙——包括四个角落:
工程上还有一个聪明的安全网:碰撞判断在 6×6 像素的粗格网格上做最大池化,这种粗化只会多报碰撞,绝不会漏报——细像素上的重叠必然落在双方都占用的格子内。性能大幅提升,正确性零损失。
放不下怎么办:先缩小,再二分找回来
40 只鸟加 40 个名字标签,第一版布局大概率放不下。collage.py 的处理分两步:
- 每轮缩 10%:整体尺寸乘以
0.9,最多试 20 轮,直到整页塞得下; - 二分找回:缩小必然"过缩",于是在"刚能放下"和"刚放不下"之间做二分,把浪费的空白尽量吃回来。Spiral 慢,找到首次能放下就直接停;Voids 快,允许再花 3 步二分把版面再撑大一点。
名字标签也有自己的生存保障:标签不是画完鸟再补上去,而是和鸟绑成一个整体一起参与打包(见 _with_label()),这样每个名字在几何上就"有座",永远不会压到邻居或出界。如果连鸟带名字实在塞不下,系统会退掉名字保住鸟——毕竟"鸟比空白重要"。
如何切换布局
布局由settings.layout控制,默认是spiral。在管理页(:8080/admin)的 Layout 选项里切换即可,可选值定义在 LAYOUTS:
- Spiral— grows out from the middle,适合喜欢"大鸟居中"传统图鉴感的用户;
- Voids— fills the emptiest spot first,鸟铺满整张纸,适合大屏电视展示。
两种布局打包的"鸟座"会被缓存(见 collage.py 缓存逻辑),所以切换后重算也只发生在鸟群变化时。
小结
| 机制 | 一句话总结 |
|---|---|
| 按体重排序 | 大鸟先选座,中心永远属于大鸟 |
| Spiral | 同心圆环贪心找位,简单、快 0 倍开销,页面呈圆团 |
| Voids | 一次 FFT 算全页合法位置与空度评分,角落都不浪费 |
| 探针行 | 用百分之一的成本淘汰 99% 的碰撞位置 |
| 6×6 粗格池化 | 碰撞检查只多报、不漏报 |
| 缩 10% + 二分 | 先保证放得下,再吃回浪费的纸面 |
这就是 Fugleramme 让 40 只鸟"优雅排进一张纸"的完整答案:Spiral 用一圈圈的耐心,Voids 用一次傅里叶变换的全局视野,最后再由"缩小—二分"兜底,保证无论花园里来多少访客,这一页纸总是既完整又好看。
【免费下载链接】fuglerammeBird frame for Raspberry Pi - real-time bird detection by audio, fully local AI, rendered as real, hand-cut 1800s bird illustrations. On an e-ink panel, a TV, or any screen.项目地址: https://gitcode.com/gh_mirrors/fu/fugleramme
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考