MiroFish智能镜面:半透镜透光率与Boids鱼群算法实战
2026/9/18 10:05:39 网站建设 项目流程

客厅那面镜子挂了两年,除了照人就是积灰,直到我把它改成了 MiroFish——一面平时安安静静当镜子用、有人走近就有一群鱼从边缘慢慢游出来的智能镜面。它不接水、不喂食、不用换滤棉,也不需要你在墙上打十几个孔走管道。整机厚度不到 6 厘米,挂在玄关或者走廊尽头,白天你几乎看不出它有什么特别,晚上关掉主灯,它就是整间屋子里最耐看的那件东西。

做这个东西的念头其实很朴素。我养过几年真鱼,知道草缸有多好看,也知道维护有多烦:换水、控温、爆藻、停电、出差三天回来满缸白点。后来我把真鱼送人了,但那种"盯着水里发呆"的松弛感我一直想要回来。MiroFish 就是我给出的答案——用一块半透镜加一块屏幕,把"鱼"这件事从物理世界挪到光学和代码里。它解决的从来不是"怎么把鱼养好",而是"怎么在不承担养鱼成本的前提下,保留看鱼的那份安静"。

这篇内容适合三类人:一类是手里有树莓派、想找个能真正挂在墙上拿得出手的项目的人;一类是做互动装置、展陈或者店铺软装,需要低成本氛围硬件的朋友;还有一类纯粹是被"魔镜"这个概念吸引、想搞明白半透镜到底怎么选、屏幕亮度到底要多少才够看的人。我不打算只给你一个"接线上电跑起来"的流程,那种东西网上到处都是。我更想把光学计算、群集算法参数、散热和电源这几个真正会翻车的环节拆开讲清楚,让你少走我那三个周末的弯路。

1. 从一句"想要一面会养鱼的镜子"到方案定稿

1.1 需求拆解:MiroFish 到底要满足哪几件事

动手之前我列了一张需求表,一共五条,后来发现这五条直接决定了后面所有的技术选型。第一条是日常状态必须像镜子,不能是一块永远亮着的屏幕挂在墙上,那样太吵,而且晚上会变成客厅里的光源污染。第二条是有人靠近才唤醒,走开后一段时间内自动回到镜面状态,这个过程要平滑,不能"啪"地一下亮起来。第三条是鱼群要像鱼群,不能是十条鱼各游各的,要有群体感、有聚散、有被惊扰后重新聚集的过程,这是整个项目里最值钱的部分。第四条是整机免维护,不用每天插拔电源,不用管系统更新,断电重启后能自己起来。第五条是外观能见人,边框不能是裸露的亚克力和热熔胶,得有一个像家具的壳子。

这五条里,第一条和第三条其实是冲突的。镜子要"像镜子",意味着环境光反射必须压过屏幕透射出来的光;而鱼群要"像鱼群",又需要屏幕有足够的亮度把细节托出来。这个矛盾最后成了整个项目最核心的设计约束,我在第 2 节会把它量化成一个亮度对比度的计算。很多人做魔镜失败,不是代码写错了,是从来没算过这笔账,做完发现白天根本看不见,然后草草归结为"屏幕不行,得换 OLED"。

第三条需求还带来一个隐藏约束:帧率稳定性比帧率高低更重要。鱼群行为算法是靠每一帧累积微小位移来产生"生命感"的,如果帧率在 60 和 25 之间乱跳,鱼就会出现忽快忽慢的抽搐。所以后面我在渲染上做了大量减法,把目标定在稳定 30 帧,而不是追求 60 帧。

1.2 三条可选路线:为什么我最终选了透射式半透镜方案

市面上能实现"镜面显示"的路子大致有三条,我实打实评估过其中两条,第三条直接放弃了。

第一条是反射式,把屏幕侧放在镜子背面,屏幕的光打到半透镜上再反射进人眼。好处是像有真实景深,你走动的时候鱼会有视差,看起来真的"在镜子里面的空间"。坏处是屏幕不在正前方,要占掉额外的框体厚度,而且屏幕边缘的光会漏进视野,做成大尺寸的时候漏光非常难看。小尺寸(比如 20 厘米以内)值得一试,我这块 300×400 毫米的板子直接排除了它。

第二条是透射式,屏幕贴在镜子正后方,光线直接穿过半透镜进入人眼,半透镜同时负责反射环境光形成镜面效果。这是目前绝大多数魔镜项目用的方案,结构最薄,只需要屏幕和镜面之间留一段距离用来散热和走线。缺点是成像平面就贴在镜面上,鱼看起来"贴"在玻璃上,缺乏纵深。但这个问题可以完全交给渲染解决——给每条鱼一个深度值,用缩放和雾化模拟出远近关系,肉眼几乎分辨不出来。

第三条是透明 OLED 或者透明 LCD,屏幕本身就是半透的,不需要额外贴镜面膜。我查完价格就关掉了页面,同尺寸成本是方案二的十倍以上,而且透明屏在暗场下的黑位一塌糊涂,镜子那部分会发灰。对于个人项目来说完全没有性价比。

最终定下来的结构是:7 寸 IPS 屏 + 3 毫米半透亚克力 + 铝型材边框,屏幕与镜面的间距定在 45 毫米。这 45 毫米不是随便写的,它同时满足三个条件:留出屏幕驱动板和走线的空间、让镜面后表面反射的重影落到视野之外、给框内空气对流留出截面。

注意:半透亚克力比玻璃轻、好加工、不怕摔,但硬度低,擦灰的时候一定要用超细纤维布加清水,酒精会让表面发乌。

1.3 模块划分与物料清单

整机分成四块:光学组件(半透镜 + 屏幕)、计算单元(树莓派)、感知组件(人体感应 + 光线传感器)、结构件(框体 + 散热 + 电源)。我在动工前把所有东西都平铺在桌子上跑通了再装的,这个习惯救了我至少两次——尤其是屏幕和树莓派之间的 HDMI 时序问题,装上之后拆一次要半小时。

模块选型参考价格备注
计算树莓派 4B 2GB约 300 元Zero 2 W 也够用,但 HDMI 输出要转接头
显示7 寸 IPS 1024×600,400 nits约 180 元带 HDMI 驱动板,支持背光调光
光学3 毫米半透亚克力,300×400 毫米约 80 元下单前一定问清透光率
感知HC-SR501 人体红外 + BH1750 光照约 25 元光照传感器用来做自适应亮度
结构铝型材边框 + 亚克力背板约 70 元45 毫米厚度
散热4010 风扇 ×1 + 防尘网约 15 元串联电阻降速降噪
电源5V/3A 适配器 + DC 母座约 40 元留足余量,屏幕瞬时电流不小
杂件铜柱、线夹、亚克力胶、风扇螺丝约 30 元别省,装的时候很缺

合计大约 740 元。这个预算下,如果只是想验证效果,可以先把半透镜放在桌上,屏幕放后面,用手电筒照着模拟环境光,先看对比度够不够,再决定要不要投入做壳子。我强烈建议先做这一步,因为半透镜一旦按尺寸裁好就退不了货。

2. 半透镜选型与亮度对比度的硬核算账

2.1 透光率和反射率到底该怎么选

半透镜的参数只有一个核心:透光率 T。反射率 R 近似等于 1 减去 T(忽略吸收),所以商家标了 T 就等于标了 R。市面上常见的规格有 T=10%、T=30%、T=50%、T=70% 几档,还有一些标注模糊的所谓"魔镜专用膜",你需要主动去问客服要实测数据,很多店给不出。

选型的逻辑非常反直觉,很多人第一反应是"透光率越高,屏幕越亮,那就选高的"。错。透光率越高,环境光反射回来的也越多,镜面的"镜感"越弱。T=70% 的板子挂上去,白天你看到的是一块发灰的玻璃后面有个亮屏幕,不是一面镜子。反过来 T=10% 的板子镜感极强,白天和普通镜子几乎没差别,但需要屏幕亮度压到非常高的水平才能穿透它。

我最后选的是T=30%。理由是它在我预期的夜间使用场景下对比度足够,同时白天的镜面感能保持在"别人进来不会觉得奇怪"的水平。如果你的 MiroFish 装在光照充足的客厅,白天也想看到鱼,那就得选 T=50% 并且配一块 800 nits 以上的屏幕,成本直接翻倍。

透光率 T镜面感(白天)对屏幕亮度要求适用场景
10%极强,几乎等同普通镜1000 nits 以上玄关镜、只做氛围不追求白天可见
30%强,细看能察觉400 至 600 nits走廊、卧室、夜间为主
50%中等,明显能看到底色300 nits 左右白天也要求可见
70%弱,更像一块玻璃200 nits 即可展陈、暗环境

2.2 亮度对比度计算:为什么"晚上才好看"是设计出来的

这一步是全文最值得抄作业的部分。你看到的最终画面亮度,等于屏幕透射过来的光加上环境光被镜面反射回来的光。要看清鱼,前者必须显著大于后者。

透射亮度很好算:L_view = L_screen × T。屏幕标称 400 cd/m²,T=0.3,那么透射亮度就是 120 cd/m²。

反射亮度稍微绕一点,用漫反射的近似公式:L_refl = E × R / π。E 是环境照度(单位勒克斯),R 是反射率(0.7),π 是圆周率。为什么会有这个 π,因为勒克斯描述的是"落到单位面积上的光通量密度",而 cd/m² 描述的是"单位面积朝单位立体角发出的光",漫反射面把光均匀撒到半个球面上,这个换算系数就是 1/π。

代入几组真实的室内照度算一下:

环境照度 E反射亮度 L_refl透射亮度 120 cd/m² 时的对比度观感
30 lux(睡前只留夜灯)6.7 cd/m²约 18:1鱼非常通透,几乎像发光
80 lux(夜间主灯调暗)17.8 cd/m²约 6.7:1舒适,鱼和水感都清楚
200 lux(普通夜间客厅)44.6 cd/m²约 2.7:1能看清,但整体发灰
400 lux(白天阴天)89.1 cd/m²约 1.3:1非常吃力,基本看不清
700 lux(白天晴天室内)155.9 cd/m²小于 1完全看不到鱼

数字一目了然:这面镜子在夜间是魔法,在白天就是一面普通镜子。我一开始有点沮丧,后来想通了——这恰恰是我要的行为。白天它老老实实当镜子,晚上它才是 MiroFish。与其花十倍成本去跟太阳对抗,不如把环境自适应做进软件里,让它在照度超过 350 lux 时主动降低屏幕亮度甚至关闭背光,既省电又延长屏幕寿命。

提示:这个计算公式里 R 取 0.7 是理想值,实际亚克力表面有散射,实测反射亮度会略低,也就是说实际观感比算出来的好一点。别因为算出来白天看不见就放弃。

2.3 自适应亮度的实现思路

有了上面的模型,自适应亮度就不是玄学调参了,而是直接的闭式计算。我在树上挂了一个 BH1750 光照传感器,每 5 秒采一次环境照度,然后按目标对比度反推需要的屏幕亮度:L_screen = k × E × R / (π × T),其中 k 是我想要的对比度目标,取 4。代入 T=0.3、R=0.7、π=3.14,化简后 L_screen ≈ 2.97 × E。也就是说环境照度 80 lux 时,屏幕给到 240 cd/m² 就够了;环境照度 200 lux 时需要 594 cd/m²。

关键点是最后一步:把计算结果往 400 这个上限夹紧,再乘一个 0.75 的舒适系数。因为屏幕在最高亮度下色偏明显、发热也大,除非必要不要顶满。我把最终的亮度映射到屏幕驱动板的 PWM 调光上,用 0 到 100 的百分比输出,实测 60% 亮度对应约 240 cd/m²,正好覆盖我的主要使用场景。

这里有个很容易踩的坑:调光用背光 PWM,不要用软件调色。很多教程教你往画面上叠一层半透明黑色遮罩来"调暗",那是在骗自己,背光功耗一分没省,屏幕该发热还是发热,而且暗部细节会被压死。找屏幕驱动板上的 BL 或者 EN 引脚,用 GPIO 输出 PWM 才是正解。

3. 框体结构、散热与供电的实操细节

3.1 厚度、视差与重影之间的取舍

半透镜到屏幕的间距 d,是我在结构上唯一反复调整过的参数。第一版我做了 25 毫米,装上去之后正面看没问题,稍微偏一点角度就能在画面里看到第二层淡淡的虚影——那是半透镜后表面反射屏幕光形成的重影。原理很简单,半透镜是亚克力板,前表面和后表面都会反光,两侧之间隔着 3 毫米板材厚度,如果有人射光打到后表面,反射回来就会形成偏移的小号虚像。

解决办法有两个。一是缩小间距,让重影偏移量小于 1 毫米,肉眼就看不出来了;二是加大间距,把重影推到视野边缘之外。我选的是后者,把 d 加到 45 毫米,同时把屏幕四周的发光区域用黑色绒布做了遮光。实测下来重影基本消失,只有在很近的距离、很偏的角度才看得出来。

顺便说个真实感受:d 从 25 加到 45,鱼看起来"贴"在玻璃上的感觉几乎没有变化,因为透射式的成像平面天然就在屏幕那一层。真正让鱼有纵深感的是渲染里的雾化和缩放,结构上折腾间距的收益远不如改这几行代码。如果你正在规划尺寸,d 取 40 到 60 毫米就够了,多留出来的空间全是给散热和走线的。

3.2 封闭框体的散热设计

这是最容易被忽略、也最容易让整个项目变短命的一环。7 寸 IPS 屏的驱动板和背光在 60% 亮度下大约耗散 3 瓦,树莓派 4B 跑图形负载大约 4 瓦,两个热源塞进一个 45 毫米厚、四周几乎密闭的盒子里,实测内部温度稳定在 55 摄氏度以上。树莓派 CPU 会降到 75 度开始降频,一降频鱼群就开始抽搐,你还以为是代码问题。

我的方案是下进上出的自然对流加上一个降速风扇。背板底部开一排直径 6 毫米的进气孔,顶部开同样面积的一排出气孔,孔外侧贴防尘网。然后装一个 4010 涡轮风扇,位置贴着出气孔往外抽。关键是给风扇串一个 33 欧姆的电阻,把转速从 5 伏时的满速降到大约 3.5 伏的水平。满速时风扇噪音在安静的卧室里非常明显,降到 3.5 伏之后基本听不见,但风量还有原来的一半左右,够用了。

实测改性后的效果:框内温度从 55 度降到 42 度,树莓派 CPU 从 72 度降到 58 度,帧率再也没有掉过。如果嫌风扇麻烦,还有一个零噪音方案:把屏幕亮度上限压到 50%,并且把树莓派限频到 1.2GHz。温度能控制在 50 度以内,代价是鱼的数量要减到 120 条左右。

注意:风扇一定要往外抽,不要往里吹。往里吹会把防尘网上的灰直接吹进框体,落在半透镜内表面上,擦都擦不到。

3.3 供电、开关机与背光控制

整机是单路 5V 供电,用 5V/3A 的适配器。这里必须提醒一句:不要用 USB 线从旧充电头上取电。屏幕驱动板在上电瞬间的浪涌电流会超过 2 安培,劣质电源会掉压,表现就是树莓派随机重启,你还以为是软件崩溃。用带 DC 母座的成品适配器,线径选 20AWG 以上的。

背光控制我用了一个 N 沟道 MOSFET(AO3400 这类就够),栅极接树莓派的 GPIO18,源极接地,漏极接屏幕驱动板的背光使能脚。这样软件里输出 PWM 就能直接控亮度开关。注意 MOSFET 的源极必须接地而不是接负载侧,这是低压侧开关的标准接法,接反了要么常亮要么烧管子。

休眠策略是这样的:人体感应超过 3 分钟没有触发,先把鱼群做一次"散场"动画——鱼慢慢游向画面边缘淡出——然后屏幕亮度在 2 秒内从当前值线性降到 0,最后关闭背光。整个过程大约 5 秒,观感上非常自然,不会给人一种"设备断电了"的突兀感。唤醒则相反,先点亮背光到低亮度,鱼从边缘游进来,亮度再爬到自适应目标值。

状态触发条件屏幕亮度鱼群表现整机功耗
休眠3 分钟无人体活动0(背光关闭)约 3.2 W
唤醒检测到人体活动渐亮至目标值从边缘游入约 6.5 W
互动轻触镜面目标值 +10%鱼群受惊散开约 7.2 W
日常持续有活动自适应计算值正常巡游约 6.5 W

算一笔电费:按唤醒状态平均 6.5 瓦、每天有效点亮 5 小时计算,一天耗电 0.033 度,一个月大约 1 度电。这个数字让我在"要不要常开"这个问题上完全没有了心理负担。

4. 鱼群算法与渲染的核心实现

4.1 用 Boids 三原则搭出群体行为

MiroFish 的灵魂是鱼群,而鱼群最省力也最像的解法是 Boids 模型。它的思想非常朴素:不给每条鱼写"剧本",只给三条局部规则,群体行为自己涌现出来。三条规则分别是分离(别撞上邻居)、对齐(跟邻居朝一个方向游)、聚合(别离群太远)。

我用 Python 加 NumPy 实现,核心是每帧算一次两两距离矩阵。有人一听 O(N²) 就慌了,但在 N 不超过 300 的场景下完全无所谓。300 条鱼的两两组合是 9 万对,NumPy 用向量化算一次不到 3 毫秒,30 帧每秒的要求下这点开销可以忽略。强行上空间哈希或者四叉树反而会因为 Python 的循环开销把速度拖得更慢。

import numpy as np def update(boids, dt, cfg): pos = boids[:, 0:2].copy() # N x 2 位置 vel = boids[:, 2:4].copy() # N x 2 速度 N = pos.shape[0] # 两两向量与距离,对角线置大值避免自己跟自己算 d = pos[:, None, :] - pos[None, :, :] dist = np.linalg.norm(d, axis=-1) np.fill_diagonal(dist, 1e9) # 分离:只考虑分离半径内的邻居,力与距离平方成反比 sep_mask = (dist < cfg['r_sep']) sep = np.zeros_like(pos) if sep_mask.any(): inv = np.where(sep_mask, 1.0 / (dist ** 2 + 1e-6), 0.0) sep = np.einsum('ij,ijk->ik', inv, d) # 对齐与聚合:只看感知半径内的邻居 nb_mask = (dist < cfg['r_nb']).astype(np.float64) cnt = nb_mask.sum(axis=1, keepdims=True) cnt[cnt == 0] = 1.0 align = np.einsum('ij,jk->ik', nb_mask, vel) / cnt - vel cohesion = np.einsum('ij,jk->ik', nb_mask, pos) / cnt - pos acc = (cfg['w_sep'] * sep + cfg['w_ali'] * align + cfg['w_coh'] * cohesion) # 转向力限幅,防止单帧加速度过大导致抖动 norm = np.linalg.norm(acc, axis=1, keepdims=True) norm[norm == 0] = 1.0 acc = acc / norm * np.minimum(norm, cfg['max_force']) vel += acc * dt # 速度限幅:低于下限会原地不动像死了,高于上限像子弹 sp = np.linalg.norm(vel, axis=1, keepdims=True) sp[sp == 0] = 1.0 vel = vel / sp * np.clip(sp, cfg['v_min'], cfg['v_max']) pos += vel * dt boids[:, 0:2] = pos boids[:, 2:4] = vel

这段代码里有几处是我踩坑之后才加上的。第一处是分离力用距离平方的倒数而不是线性倒数,因为线性倒数在邻居很近的时候力还不够大,鱼会轻微重叠,视觉上像穿模。第二处是转向力限幅,如果不限制每一帧的加速度,鱼在密集区域会突然被弹开,看起来像抽筋。第三处是速度下限,没有下限的话鱼在努力平衡时会越游越慢直到几乎停住,像飘在水里的一具标本。

参数整定我磨了很久,最后稳定下来的一套值是这样的,你要抄可以直接用:

参数取值含义与调参方向
r_sep26 px分离半径。太小会重叠,太大会让鱼群散成一盘沙
r_nb90 px感知半径。决定群体规模,太大整缸鱼会变成一团
w_sep1.6分离权重。抖得厉害就加大,挤成一团就减小
w_ali0.9对齐权重。决定鱼群是"一起朝一个方向"还是各游各的
w_coh0.6聚合权重。太大整群鱼会缩成球,太小会散到画面外
max_force90 px/s²转向力上限,直接决定动作的"柔"与"急"
v_min / v_max45 / 130 px/s速度区间,上下限比例控制在 3 倍以内最自然

4.2 边界处理:为什么"软墙加硬夹紧"比单纯推力好用

边界是最容易翻车的地方。我的第一版是用一个反向的推力把鱼往回推,力的大小跟越界深度成正比。问题是鱼群在角落里聚集时,几条鱼互相推挤,速度会瞬间叠加到很大,下一秒就贴着边界疯狂抖动,看起来像触电。

后来改成软墙加硬夹紧的组合。软墙负责"提前预判":当鱼距离边界小于 80 像素时,按 (80 - 距离) / 80 的比例施加回推力,越近推力越大。硬夹紧负责"兜底":每帧结束前,如果位置真的越界了,直接把位置钳回边界内,同时把垂直于边界的那一维速度分量乘以 -0.6 做一次衰减反弹。这样绝大多数鱼根本不会碰到硬边界,触发的都是极少数情况,抖动就消失了。

还有一个细节是边界要按鱼的个体半径内缩,而不是按画面边缘。因为鱼贴图不是质点,有长度,如果按画面边缘算,鱼头会伸到画面外去。我把左右各内缩 45 像素,上下各内缩 30 像素。

4.3 用缩放和雾化骗出纵深

透射式方案的硬伤是缺纵深,但这在渲染层完全能补回来。我的做法是给每条鱼额外维护一个 z 值,范围 0 到 1,代表离观察者的远近。z 的变化做慢速随机游走,每秒最多变化 0.1,这样鱼会有慢慢游远游近的感觉。

然后用两条映射把 z 转成视觉信号。第一条是缩放,scale = 0.55 + 0.45 × z,也就是最远的鱼是最近那条的 55% 大小。第二条是雾化,给每条鱼叠一层和背景同色的透明度,alpha = 120 + 135 × z,最远的鱼只有一半出头的可见度。再加上绘制顺序按 z 从远到近排,远处的鱼先画,近处的后画覆盖上去,遮挡关系也就对了。

真正省性能的关键在这里:绝对不要每帧对鱼的原图做缩放pygame.transform.smoothscale是个性能杀手,30 条鱼就能把树莓派拖到 20 帧。我的做法是启动时预先生成每一条鱼、每一帧动画、每一个缩放档位的贴图缓存。缩放档位取 8 档就够(z 从 0 到 1 均分),配合每帧动画缓存,启动时多花 1.5 秒,运行时直接 blit,CPU 占用能降一半以上。雾化也不用逐像素处理,用surface.set_alpha()就够用,效果完全能接受。

提示:预生成缓存的时候注意内存。50 条鱼 × 8 帧动画 × 8 个缩放档位 = 3200 张贴图,如果每张是 64×32 像素的带透明通道 Surface,大约占 26MB 内存,树莓派 2GB 完全扛得住。如果要做 200 条鱼,就得把动画帧数降到 4 帧。

4.4 帧率控制与状态机

帧率我用固定步长的思路:主循环里clock.tick(30),但传给物理更新的 dt 不是真实测量的时间,而是钳制在 1/30 以内的固定值。为什么要这么绕?因为如果某帧因为系统调度晚了 200 毫秒,你用真实 dt 去更新,鱼的位移会突然跳到 100 多像素外,视觉上就是"闪现"。钳制之后最坏情况只是那一帧鱼走得慢一点,肉眼几乎察觉不到。

FIXED_DT = 1.0 / 30.0 while running: raw_dt = clock.tick(30) / 1000.0 dt = min(raw_dt, FIXED_DT) # 关键:钳制,防止卡顿后瞬移 handle_events() if state == STATE_AWAKE: update(boids, dt, cfg) render()

状态机部分我用一个简单的四状态结构:SLEEP、WAKING、AWAKE、INTERACT。每个状态有自己的持续时间和亮度目标,状态之间靠事件驱动跳转。人体感应触发 SLEEP 到 WAKING,WAKING 用 1.2 秒把亮度爬到位再进 AWAKE,人体感应超时就反向回到 SLEEP。INTERACT 是轻触触发的临时状态,持续 1.5 秒,期间给鱼群加一个从触点位置发散的瞬时冲击力,鱼会"炸开"再慢慢聚回来——这个交互带来的反馈感比想象中强很多,来我家的人都会忍不住多拍两下镜子。

5. 常见问题与排查实录

5.1 画面发灰、看不清鱼,怎么系统排查

这是最高频的问题,而且大部分人排查方向是错的。正确的顺序是先量环境光,再算对比度,最后才怀疑硬件。拿手机装个照度计应用,站在镜子前测一下当时的照度,如果超过 300 lux,那发灰是正常物理现象,不是你接错线了。

如果是夜间照度只有 80 lux 仍然发灰,那依次查三件事。第一件是屏幕亮度是不是真的上去了,有些驱动板默认 50% 亮度,你没有从软件把它拉满,等于白白损失一半透射光。第二件是半透镜装在框里的时候有没有把屏幕的透光区遮住,我见过有人用了比屏幕大的半透镜,边缘多出来的部分反射了框内的杂光,整个画面就糊了一层。第三件才轮到怀疑镜面——用手机闪光灯贴在镜面上照,如果能看到明显的漫反射雾圈,说明亚克力表面被擦花了,只能换板子。

还有一种很迷惑的现象是画面出现细密条纹或者摩尔纹,这不是质量问题,是半透镜到屏幕的间距太近,屏幕像素栅格和亚克力表面的微小纹理形成了干涉。把间距加大到 40 毫米以上就消失了。

5.2 鱼群行为异常的三种典型症状

症状一:鱼群缩成一个球在画面中央打转。这是聚合权重过大的经典表现。所有鱼都被吸引向群体质心,质心又跟着鱼群走,形成正反馈。把 w_coh 从 0.6 降到 0.3,同时把感知半径 r_nb 从 90 缩到 60,问题立刻缓解。

症状二:鱼贴着边界高频抖动。这是边界推力和转向力上限不匹配。检查 max_force 是不是设得太大,或者改用前面讲的软墙加硬夹紧组合。还有一个隐蔽原因:如果你的鱼数量超过了 200 条,边界区域的局部密度会很高,分离力会把鱼往外推,推出去又被边界推回来,形成极限环。这时候要么减数量,要么把边界内缩距离加大。

症状三:某几条鱼明显比别人快或者慢。检查速度限幅的归一化步骤里有没有除零保护。速度正好为 0 的鱼,归一化时会算出 NaN,然后这条鱼的位置就彻底废了,表现为瞬移到画面外或者卡在原地。加一句把零速度的模长置为 1 就解决了。这个问题我在真机上复现了三次才发现,因为随机初始化很少正好给出零速度,但鱼在"急刹车"平衡的时候会出现极小的速度值,四舍五入之后就是 0。

5.3 硬件与系统层的速查表

现象最可能的原因快速验证处理办法
开机黑屏,风扇在转HDMI 时序不匹配接普通显示器看有没有输出config.txt 加 hdmi_cvt 指定分辨率
帧率周期性掉到 15CPU 过热降频读 /sys/class/thermal 温度加风扇或限频到 1.2GHz
画面亮度自己变来变去光照传感器被屏幕漏光干扰遮住传感器看是否稳定传感器开孔朝前,加遮光筒
走近不唤醒人体红外灵敏度太低看 GPIO 电平日志调灵敏度旋钮,或换毫米波模块
走开后不熄屏传感器延时旋钮设太长观察指示灯把延时旋钮拧到最短,逻辑交给软件
触摸没反应电容触摸受框体金属干扰换位置测试触摸铜箔与金属框之间加绝缘垫
系统重启后程序没起来服务依赖没配好看 systemd 日志加上重启策略和依赖声明

系统自启我用 systemd 管理,核心是配上重启策略,这样哪怕图形驱动偶发异常把程序搞崩了也能自己拉起来:

[Unit] Description=MiroFish Service After=multi-user.target [Service] ExecStart=/usr/bin/python3 /home/pi/mirofish/main.py WorkingDirectory=/home/pi/mirofish Environment=SDL_VIDEODRIVER=kmsdrm Environment=SDL_FBDEV=/dev/fb0 Restart=always RestartSec=3 User=pi [Install] WantedBy=multi-user.target

注意:用 kmsdrm 这个视频驱动需要程序有访问 DRM 设备的权限。如果日志里报权限错误,把用户加进 video 和 render 用户组,或者改用 X 环境下的 x11 驱动。能不装桌面环境就别装,Lite 版加 kmsdrm 开机只要 12 秒,装了桌面要 35 秒以上。

6. 后续我打算继续折腾的方向

现在这版 MiroFish 挂在我家走廊尽头已经稳定跑了小半年,没死过机,也没再动过参数。但有几个方向我一直惦记着。第一个是多鱼种混养,现在是单一鱼种,我想加进底栖的虾和小体型的群游鱼,让它们有不同的行为参数——虾基本不参与群集,沿缸底随机爬,这样画面层次会更丰富。实现上就是给每条鱼加一个 type 字段,不同 type 走不同的参数子集和边界约束。

第二个是基于摄像头的头部追踪视差。前面说过透射式天生没有视差,但如果用树莓派摄像头识别人脸位置,然后按观察者位置给整个鱼群做一次全局的水平像素偏移,就能骗出"鱼在缸里不同深度"的感觉。代价是要额外跑一个轻量的人脸检测,我担心会把 CPU 吃满,得先用低分辨率加跳帧试试。

第三个是季节和天气联动。接一个天气接口,外面下雨的时候画面里加一点水面波纹和水色变化,晚上下雪就换成冷色调。这是个典型的"投入很小、观感提升很大"的改动,本质上只是几组调色参数和一个网络请求,但对氛围感的作用非常明显。

最后分享一个小经验,跟技术无关但我觉得挺重要。我做完 MiroFish 之后最大的收获不是学会了 Boids 或者搞懂了半透镜的亮度模型,而是想明白了一件事:个人项目不需要追求"白天晚上都完美"。我在白天看不到鱼这件事上纠结了整整一周,甚至一度想加钱换高亮屏,后来放弃了。接受它的局限、把它的长处做到极致——晚上关掉主灯的那一刻,走廊里那面镜子上有一群鱼在慢慢游,这个画面值回了我所有的投入。功能上的"不那么全能",有时候反而是设计里最清醒的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询