石鱼仿真系统:多智能体行为建模与水下感知算法验证

发布时间:2026/10/12 17:53:23
石鱼仿真系统:多智能体行为建模与水下感知算法验证
简介这是一份面向生物行为研究者、机器人仿真开发者与Python学习者的石鱼生态模拟项目源码基于ROS Noetic分支构建将计算机科学、生物学与机器人学交叉融合用于在虚拟环境中复现石鱼的伪装、捕食、繁殖与防御等行为减少真实生态实验的干扰。压缩包共223个文件约1.7MB以205个obj三维模型文件为主体配合4个py脚本、3个scn场景文件、3张jpg贴图以及yaml、launch、xml等ROS配置与启动文件另附csv数据、md说明和license结构完整便于直接加载运行。项目涉及环境建模、物理模拟、行为算法、事件驱动与多线程并发等知识点读者可借此理解仿真系统的模块划分与数据组织方式并在此基础上扩展自己的生态模拟实验。目前已有179人学习关注适合具备一定Python基础、希望深入仿真与机器人方向的开发者参考。1. 石鱼的世界模拟从一条会“隐身”的鱼说起石鱼stonefish是珊瑚礁里最会伪装的猎手之一趴在沙地上不动时几乎和一块长满藻类的石头没有区别。我第一次在仿真里复现它的伏击行为时翻车得很彻底鱼模型贴着地面不动猎物从它嘴边游过去三次它一次都没触发攻击。后来才发现问题不在行为逻辑而在感知——仿真里的“视觉”是理想化的没有把石鱼的伪装优势建模进去猎物自然不会被骗。这就是smarc_stonefish_sims这类项目要解决的核心问题它不是一个单纯的鱼类动画而是一套围绕石鱼行为、水下感知和礁石环境构建的仿真系统。它适合三类人做多智能体行为仿真的研究者、想验证水下机器人感知算法的工程师、以及需要可复现实验环境的课程设计者。你不需要真的养一条石鱼但你需要理解它的世界是怎么被拆成可计算的模块的。2. 拆解石鱼仿真环境、行为体与感知三层架构2.1 为什么石鱼仿真不能只做“鱼模型”很多人第一反应是找一个石鱼的三维模型丢进场景里让它游动就算完成了。这种做法在演示层面够用但一旦你想验证“伏击成功率”或“伪装有效性”就会立刻卡住。原因是石鱼的行为高度依赖环境底质颜色、光照角度、水流扰动、猎物距离每一个都会改变它的决策。所以一个可用的石鱼世界模拟至少要有三层环境层礁石地形、沙地纹理、水体参数能见度、折射、悬浮颗粒。行为体层石鱼自身的状态机潜伏、调整姿态、突袭、复位以及猎物小鱼或虾的游动策略。感知层把环境信息转成行为体可读的“刺激”比如视觉对比度、运动线索、距离阈值。这三层不是并列的而是感知层把环境层的数据“翻译”给行为体层。我一般会把感知层单独抽成一个模块因为它是整个仿真里最容易出玄学问题的地方。2.2 用状态机描述石鱼的伏击行为石鱼的行为用有限状态机FSM描述最直接。下面是一个最小可运行的状态机骨架用 Python 写不依赖具体仿真引擎你可以把它嵌进自己的循环里。# stonefish_fsm.py # 石鱼伏击行为的最小状态机状态IDLE潜伏、ADJUST调整姿态、STRIKE突袭、RESET复位 import math class StonefishFSM: def __init__(self, strike_distance0.15, camouflage_threshold0.7): self.state IDLE self.strike_distance strike_distance # 触发突袭的距离阈值米 self.camouflage_threshold camouflage_threshold # 伪装有效性的最低对比度阈值 self.strike_cooldown 0.0 # 突袭后的冷却计时 def update(self, prey_distance, visual_contrast, dt): # dt时间步长单位秒 if self.strike_cooldown 0: self.strike_cooldown - dt self.state RESET return self.state if self.state IDLE: # 只有当伪装足够好且猎物进入可突袭范围才考虑调整 if visual_contrast self.camouflage_threshold and prey_distance self.strike_distance * 2: self.state ADJUST # 如果伪装被破坏对比度过高保持潜伏但不再靠近 elif visual_contrast self.camouflage_threshold: self.state IDLE elif self.state ADJUST: # 调整姿态阶段如果猎物进入突袭距离直接突袭 if prey_distance self.strike_distance: self.state STRIKE # 如果猎物跑远退回潜伏 elif prey_distance self.strike_distance * 3: self.state IDLE elif self.state STRIKE: # 突袭动作触发后进入冷却 self.strike_cooldown 1.5 # 冷却 1.5 秒模拟石鱼重新伪装的时间 self.state RESET elif self.state RESET: # 冷却结束后回到潜伏 if self.strike_cooldown 0: self.state IDLE return self.state这段代码的逻辑很直白visual_contrast越低说明石鱼和背景越像伪装越好prey_distance决定它要不要动。关键参数有两个strike_distance控制突袭的触发距离设得太大会导致石鱼频繁扑空设得太小则永远等不到猎物camouflage_threshold控制伪装有效性的判定通常需要根据环境层的纹理对比度动态调整我一般会先用 0.7 跑一遍再根据突袭成功率微调。注意这里没有把“水流”和“光照”直接写进状态机因为它们应该由感知层预处理成visual_contrast和prey_distance的修正量。状态机只做决策不做物理计算这是保持代码可维护的关键。2.3 环境层的最小参数集别一上来就堆纹理环境层最容易过度设计。我见过有人为了“真实”把礁石纹理做到 4K结果仿真跑起来帧率掉到个位数行为体根本来不及反应。对于石鱼伏击这类行为验证环境层只需要保证三件事底质颜色可区分、光照方向可调、水体能见度可量化。下面是一个环境参数表的建议值你可以直接抄进配置文件参数含义建议范围对石鱼行为的影响substrate_hue底质色调HSV 的 H30–60沙黄到褐影响伪装对比度计算light_azimuth光照方位角度0–360改变阴影方向影响猎物可见性turbidity水体浊度NTU0.5–5.0浊度越高感知距离越短particle_density悬浮颗粒密度0–0.3过高会引入视觉噪声current_speed水流速度m/s0–0.2影响猎物游动轨迹和石鱼姿态稳定性这些参数不需要全部实时更新但turbidity和light_azimuth建议在每次实验前重新采样因为它们是改变伏击成功率最明显的两个变量。我通常会用拉丁超立方采样生成 20 组参数组合跑一批仿真看突袭成功率的分布而不是只跑一组“看起来合理”的值。3. 把感知层跑通从环境数据到行为触发3.1 视觉对比度怎么算才不“玄学”石鱼的伪装本质上是视觉对比度问题。在仿真里你不能直接说“它伪装得很好”而要算出一个可比较的数值。常见做法是取石鱼体表区域和背景区域的平均颜色在 Lab 色彩空间里算欧氏距离。# contrast.py # 计算石鱼与背景的视觉对比度输入为两个区域的 RGB 均值 import numpy as np def rgb_to_lab(rgb): # 简化版 RGB 转 Lab仅用于对比度相对比较 # rgb: [0-255, 0-255, 0-255] r, g, b [x / 255.0 for x in rgb] # 先转线性 RGB def linearize(c): return c / 12.92 if c 0.04045 else ((c 0.055) / 1.055) ** 2.4 r, g, b linearize(r), linearize(g), linearize(b) # 转 XYZD65 白点 x r * 0.4124 g * 0.3576 b * 0.1805 y r * 0.2126 g * 0.7152 b * 0.0722 z r * 0.0193 g * 0.1192 b * 0.9505 # 简化转 Lab省略白点归一化细节 l 116 * (y ** (1/3)) - 16 if y 0.008856 else 903.3 * y a 500 * ((x ** (1/3)) - (y ** (1/3))) if x 0.008856 and y 0.008856 else 0 b_val 200 * ((y ** (1/3)) - (z ** (1/3))) if y 0.008856 and z 0.008856 else 0 return np.array([l, a, b_val]) def visual_contrast(fish_rgb, background_rgb): # 返回 Lab 空间的欧氏距离值越大对比度越高伪装越差 lab_fish rgb_to_lab(fish_rgb) lab_bg rgb_to_lab(background_rgb) return float(np.linalg.norm(lab_fish - lab_bg))这段代码的关键在于不要用 RGB 空间的欧氏距离因为 RGB 不是感知均匀的两个在 RGB 里距离相同的颜色人眼看起来可能差异很大。Lab 空间更接近人眼感知算出来的对比度更稳定。参数上fish_rgb和background_rgb应该取同一光照条件下的采样值否则对比度会被光照差异污染。我一般会在仿真里固定一个采样区域比如石鱼体表中心 10×10 像素背景取它正下方 20×20 像素的沙地。3.2 猎物运动线索的提取与阈值设定石鱼不是靠“看到”猎物才攻击的它更多依赖猎物产生的运动线索。在仿真里你可以用帧间差分或者光流来模拟这种线索。下面是一个简单的帧间差分示例输出运动能量值。# motion_cue.py # 从连续两帧灰度图中提取运动能量模拟石鱼对猎物运动的感知 import numpy as np def motion_energy(prev_gray, curr_gray, threshold15): # prev_gray, curr_gray: 二维 numpy 数组灰度值 0-255 diff np.abs(curr_gray.astype(np.int16) - prev_gray.astype(np.int16)) # 只保留超过阈值的像素抑制水体噪声 mask diff threshold # 运动能量 有效运动像素占比 × 平均差异强度 if mask.sum() 0: return 0.0 energy (mask.sum() / diff.size) * diff[mask].mean() return float(energy)threshold这个参数很关键。设得太低水体悬浮颗粒的微小变化会被当成猎物运动石鱼会频繁误触发设得太高猎物缓慢游动时又检测不到。我的经验是如果turbidity在 1.0 以下threshold取 10–15如果浊度更高要提到 20–25否则噪声会淹没信号。这个值没有绝对标准需要根据你的环境层参数做一次标定让猎物以已知速度游过记录motion_energy的峰值然后取峰值的 30% 作为触发阈值。3.3 把感知输出接进行为状态机感知层算出来的visual_contrast和motion_energy不能直接丢给状态机需要做一次归一化和映射。我一般会写一个简单的适配器# adapter.py # 将感知层的原始输出映射为状态机可用的输入 def adapt_perception(contrast_raw, motion_raw, contrast_max100.0, motion_max50.0): # 归一化到 0-1 contrast_norm min(contrast_raw / contrast_max, 1.0) motion_norm min(motion_raw / motion_max, 1.0) # 伪装有效性对比度越低越好所以取反 camouflage_quality 1.0 - contrast_norm # 猎物距离的代理量运动能量越高说明猎物越近或越活跃 prey_proximity motion_norm return camouflage_quality, prey_proximity这里contrast_max和motion_max是两个标定参数需要根据你的仿真场景实际取值范围来定。我通常会在场景里跑一段“无石鱼”的空白实验记录背景对比度和水体运动能量的最大值然后取略高于最大值的数作为分母。这样归一化后的值不会频繁饱和状态机的决策会更细腻。4. 避坑与排查石鱼仿真里最容易翻车的五件事4.1 石鱼一直不动猎物从嘴边游过也不攻击现象仿真跑了几分钟石鱼始终处于 IDLE 状态猎物距离已经很近了但prey_distance始终没低于strike_distance。原因最常见的是感知层的motion_energy阈值设得太高猎物的运动信号被过滤掉了导致prey_proximity一直是 0状态机收不到“猎物靠近”的输入。另一个可能是坐标系统一问题猎物和石鱼的距离算的是世界坐标但石鱼模型的原点不在体表中心导致实际距离偏大。解决先把motion_energy的threshold降到 5跑一遍看石鱼会不会动。如果动了说明是阈值问题再逐步调高到刚好不误触发的值。如果还是不动检查石鱼和猎物的坐标原点是否一致必要时在距离计算前加一个偏移量修正。4.2 石鱼频繁突袭但每次都扑空现象石鱼不断在 ADJUST 和 STRIKE 之间切换突袭次数很多但成功率为零。原因strike_distance设得太大石鱼在猎物还没进入有效攻击范围时就触发了突袭。另一个可能是strike_cooldown太短石鱼还没来得及重新伪装就再次进入 ADJUST。解决把strike_distance降到猎物体长的 1.5 倍左右先跑一组实验看成功率。如果成功率上来了但突袭次数太少再以 0.02 米为步长微调。strike_cooldown建议不低于 1.0 秒具体值可以参考石鱼实际重新伪装所需的时间仿真里取 1.5–2.0 秒比较合理。4.3 伪装对比度计算值波动很大状态机跟着抖现象visual_contrast在相邻帧之间跳动超过 20%导致石鱼在 IDLE 和 ADJUST 之间反复横跳。原因采样区域太小或者采样区域里混入了动态元素比如游过的小鱼。另外如果光照方向在仿真中实时变化背景颜色也会跟着变对比度自然不稳定。解决把采样区域扩大到石鱼体表的 1.5 倍并且对连续 5 帧的对比度做滑动平均。如果光照是动态的要么固定光照方向要么在对比度计算前先做一次白平衡归一化。我一般会在实验阶段固定光照只在需要验证光照影响时才开启动态光照。4.4 仿真跑一段时间后帧率骤降现象前几分钟正常之后帧率从 60 掉到 15 以下行为体反应明显变慢。原因环境层里的悬浮颗粒或纹理如果每帧都重新生成会迅速吃满内存和 GPU。另一个常见原因是感知层的帧间差分没有释放旧帧导致内存泄漏。解决悬浮颗粒用预生成的粒子池循环复用不要每帧新建。帧间差分只保留最近两帧旧帧显式释放。如果用的是 Python注意prev_gray和curr_gray不要被其他模块引用否则垃圾回收不会生效。4.5 换了一组环境参数后之前调好的阈值全失效现象在浊度 1.0 下调好的motion_energy阈值换到浊度 3.0 的环境后石鱼要么不动要么乱动。原因感知层的阈值是绝对阈值而水体浊度改变了信号和噪声的分布。浊度越高运动信号的衰减越严重同时悬浮颗粒带来的噪声也越强。解决不要用绝对阈值改用自适应阈值。具体做法是在每一帧里先计算整幅图像的运动能量分布取中位数加上 2 倍标准差作为动态阈值。这样无论浊度怎么变阈值都会跟着信号分布走。代价是计算量略增但比起重新标定这点开销值得。5. 进阶技巧用批量实验验证石鱼的伏击策略5.1 设计一组可对比的仿真实验单次仿真只能告诉你“这次跑通了”不能告诉你“这个策略好不好”。我一般会做批量实验固定行为状态机的参数只改变环境参数浊度、光照方位角、底质色调每组跑 50 次记录突袭成功率和平均潜伏时间。下面是一个批量实验的调度脚本骨架# batch_run.py # 批量运行石鱼仿真记录突袭成功率和潜伏时间 import itertools import numpy as np def run_single_sim(turbidity, light_azimuth, substrate_hue, max_steps3000): # 这里调用你的仿真主循环返回 (strike_count, success_count, idle_steps) # 伪代码示意实际替换为你的仿真接口 strike_count 0 success_count 0 idle_steps 0 # ... 仿真循环 ... return strike_count, success_count, idle_steps def batch_experiment(): turbidity_list [0.5, 1.5, 3.0] light_list [0, 90, 180, 270] hue_list [30, 45, 60] results [] for turb, light, hue in itertools.product(turbidity_list, light_list, hue_list): success_rates [] idle_times [] for _ in range(50): strikes, successes, idles run_single_sim(turb, light, hue) if strikes 0: success_rates.append(successes / strikes) idle_times.append(idles) results.append({ turbidity: turb, light_azimuth: light, substrate_hue: hue, mean_success_rate: np.mean(success_rates) if success_rates else 0.0, mean_idle_steps: np.mean(idle_times) }) return results这个脚本的关键在于每组参数跑 50 次而不是 1 次因为石鱼的行为有随机性猎物的游动路径、初始距离都有扰动。50 次是一个经验值再少的话均值不稳定再多的话时间成本太高。max_steps3000是单次仿真的最大步数防止死循环。5.2 从结果里读出策略边界跑完批量实验后你会得到一张三维参数表。不要只看均值要看分布。我通常会把mean_success_rate低于 0.2 的参数组合标出来这些就是石鱼伏击策略的失效边界。比如浊度 3.0 且光照方位角 180 度时如果成功率骤降说明强浊度加上逆光让石鱼的伪装优势完全失效——这本身就是一个有价值的结论而不是“仿真没跑好”。另一个技巧是看mean_idle_steps和mean_success_rate的比值。如果潜伏时间很长但成功率很低说明石鱼过于保守如果潜伏时间很短但成功率也低说明它太激进。理想的状态是潜伏时间适中成功率稳定在 0.5–0.7 之间这通常对应着比较合理的strike_distance和camouflage_threshold组合。5.3 一个我踩过的坑别在批量实验里用随机种子早期做批量实验时我为了“可复现”给每组参数固定了随机种子。结果跑出来的成功率方差极小看起来非常稳定但换一组种子后结果完全变了。后来才明白固定种子只能保证单次实验可复现不能代表策略的真实鲁棒性。正确的做法是每组参数跑多次每次用不同的随机种子然后看成功率的分布。如果分布很窄说明策略稳定如果分布很宽说明策略对初始条件太敏感需要调整参数。这个习惯我保留到现在任何行为仿真先跑 10 组不同种子的预实验看方差再决定要不要扩大样本量。希望帮到你。本文还有配套的精品资源点击获取