首页 / 资讯中心 / 文章详情

水下机器人仿真:石鱼世界模拟与合成数据生成实战

水下机器人仿真:石鱼世界模拟与合成数据生成实战 ★ FEATURED ARTICLE
简介这是一份面向生物行为研究者、机器人仿真开发者与Python学习者的石鱼生态模拟项目源码基于ROS Noetic分支构建将计算机图形学、物理引擎与生物行为建模融合在同一套可运行环境中帮助使用者在虚拟场景里观察石鱼的伪装、捕食、繁殖与防御等习性减少对真实生态的干扰。压缩包共223个文件约1.7MB其中205个obj模型文件构成岩石、藻类与鱼体等三维资产4个py脚本承载行为逻辑与仿真控制另有scn场景、launch启动、yaml参数、csv数据及jpg贴图等配套文件目录组织清晰便于按模块拆解学习。资源已有179人学习下载适合想研究多智能体仿真、ROS集成或强化学习行为建模的读者可从中获取环境搭建思路、物理模拟与事件驱动编程的实践参考并借助日志与可视化手段复盘模拟过程。1. 石鱼的世界模拟从一条会“隐身”的鱼说起石鱼stonefish这类底栖鱼最麻烦的地方不是它长得多奇怪而是它几乎和环境融为一体趴在沙砾、礁石、珊瑚碎屑上体色、纹理、边缘轮廓全都和背景高度相似。做水下视觉的人应该都有体会拿通用目标检测模型直接下水石鱼这类目标经常被漏检模型不是没看到而是“看到了但不敢确认”。smarc_stonefish_sims 这个方向本质上是想用仿真把“石鱼 复杂海底背景”这套组合批量造出来喂给感知、跟踪、抓取或避障算法做训练和验证。它适合三类人做水下机器人感知的、做仿真数据合成的、以及想用合成数据补真实样本不足的从业者。这一章先把这件事讲清楚它不是做一个游戏而是搭一个可控、可复现、能出标注的水下世界模拟器。2. 仿真世界怎么搭从场景分层到石鱼行为建模2.1 为什么水下仿真不能直接套陆地那套陆地仿真里地面基本是平面光照来自一个方向遮挡关系简单。水下完全不是这么回事。水体会吸收红光、散射蓝光能见度随深度和浊度变化海底地形有起伏、有悬浮颗粒、有动态焦散。如果你把陆地的仿真参数直接搬过来出来的图会“太干净”模型在仿真里学到的特征和真实水下图像对不上迁移到实机就翻车。常见做法是把水下仿真拆成四层水体介质层、海底地形层、动态生物层、传感器层。水体介质层负责颜色衰减和散射海底地形层负责几何和纹理动态生物层负责石鱼和其他生物的位姿、行为传感器层负责相机内参、畸变、噪声模型。这四层分开配置好处是你可以单独调浊度而不动地形单独换相机而不动生物行为。我一般会先固定传感器层因为相机模型一旦确定后面所有渲染和标注都围绕它来。石鱼仿真里最容易被忽略的是相机防水罩带来的畸变和边缘模糊如果这一步不建模后面检测框的精度会虚高。2.2 用分层配置搭一个最小可跑的水下场景下面这段配置用 YAML 描述一个最小水下场景包含水体、地形、一条石鱼和一台下视相机。字段命名尽量直白方便你替换成自己引擎里的参数。# underwater_scene.yaml water: depth: 12.0 # 水深单位米影响光照衰减 turbidity: 0.35 # 浊度0 清澈1 几乎不可见 absorption_rgb: [0.45, 0.12, 0.08] # 红绿蓝吸收系数红光衰减最快 scattering: 0.18 # 散射强度影响雾状感 terrain: type: sand_rock_mix # 沙砾与礁石混合底质 resolution: 0.02 # 地形网格分辨率单位米 texture_set: reef_01 # 纹理集名称按自己素材替换 roughness: 0.7 # 表面粗糙度影响焦散和反光 creature: species: stonefish count: 1 behavior: ambush # 伏击型行为大部分时间静止 camouflage_level: 0.85 # 伪装程度0 完全显眼1 完全融合 spawn_zone: [2.0, 8.0, 0.0, 4.0] # xmin xmax ymin ymax camera: type: downward resolution: [1280, 720] fov: 90 noise_model: underwater_gaussian distortion: 0.12 # 防水罩畸变系数这段配置的逻辑是先用水体参数决定“看起来像不像水下”再用地形和生物参数决定“有没有东西可看”最后用相机参数决定“拍出来是什么样”。参数说明里最需要盯的是turbidity和camouflage_level这两个直接决定石鱼在图像里是否可辨。浊度太高石鱼和背景一起糊掉伪装度太高标注框还在但人眼都难分辨训练时容易引入噪声标签。2.3 石鱼行为建模静止也是一种行为石鱼不是游来游去的鱼它大部分时间趴在海底等猎物。仿真里如果让它随机游动反而失真。常见做法是给石鱼一个状态机伏击、短距离突袭、回归伏击点。伏击状态下位姿变化极小但会有轻微的呼吸起伏和鳍部微动这些微动在近距离图像里是重要线索。# stonefish_behavior.py import random class StonefishBehavior: def __init__(self, camouflage_level0.85): self.state ambush self.camouflage camouflage_level self.timer 0.0 def step(self, dt): self.timer dt if self.state ambush: # 伏击状态只有微小起伏伪装度越高动作越小 micro_motion (1.0 - self.camouflage) * 0.002 self._apply_micro_motion(micro_motion) # 随机触发突袭概率与伪装度负相关 if random.random() 0.001 * (1.0 - self.camouflage): self.state strike self.timer 0.0 elif self.state strike: # 突袭短距离快速移动持续 0.4 秒 self._apply_strike_motion() if self.timer 0.4: self.state return self.timer 0.0 elif self.state return: # 回归伏击点速度较慢 self._apply_return_motion() if self.timer 2.0: self.state ambush self.timer 0.0 def _apply_micro_motion(self, amplitude): # 实际项目中这里对接骨骼或刚体微调 pass def _apply_strike_motion(self): pass def _apply_return_motion(self): pass这段代码的关键不是动作多复杂而是把“静止”当成一种需要建模的状态。camouflage_level同时影响微动幅度和突袭概率这样高伪装个体在图像里几乎不动低伪装个体更容易被捕捉到动态。参数上突袭概率我一般设在 0.001 到 0.005 之间太高会让石鱼显得躁动太低则整个序列没有正样本变化。2.4 标注生成别等渲染完再手工标仿真最大的优势是标注免费且精确。做法是在渲染管线里挂一个标注导出模块每帧把石鱼的 3D 包围盒投影到图像平面同时输出可见性比例。可见性低于阈值的框直接丢弃避免把被遮挡的目标当成正样本。# label_exporter.py import numpy as np def project_bbox_to_image(bbox_3d, view_matrix, proj_matrix, image_size): # bbox_3d: 8 个角点形状 (8, 3) corners np.hstack([bbox_3d, np.ones((8, 1))]) clip corners (proj_matrix view_matrix).T ndc clip[:, :3] / clip[:, 3:4] # NDC 转像素坐标 x (ndc[:, 0] * 0.5 0.5) * image_size[0] y (1.0 - (ndc[:, 1] * 0.5 0.5)) * image_size[1] xmin, xmax x.min(), x.max() ymin, ymax y.min(), y.max() # 裁剪到图像范围内 xmin max(0, xmin); ymin max(0, ymin) xmax min(image_size[0], xmax); ymax min(image_size[1], ymax) return [xmin, ymin, xmax, ymax] def visibility_ratio(bbox_2d, mask): # mask: 石鱼像素为 1其余为 0 xmin, ymin, xmax, ymax [int(v) for v in bbox_2d] region mask[ymin:ymax, xmin:xmax] if region.size 0: return 0.0 return region.sum() / region.size逻辑说明先把 3D 框投影到 2D再用渲染出的掩码算可见性。参数上可见性阈值我一般设 0.3低于这个值的框不导出。这样做的代价是正样本变少但标签质量高训练时收敛更稳。如果你做的是小目标检测可以把阈值降到 0.2但要在数据卡里注明。3. 把仿真跑起来渲染、采样与数据导出3.1 渲染循环里必须固定的三个量仿真数据要可复现随机种子、时间步长、相机位姿采样范围必须固定。我见过有人每次跑出来的数据集分布都不一样最后模型效果波动排查半天发现是相机随机位姿没锁种子。# render_loop.py import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) def sample_camera_pose(rng, x_range, y_range, z_range): x rng.uniform(*x_range) y rng.uniform(*y_range) z rng.uniform(*z_range) # 下视相机保持俯仰角在 -80 到 -100 度之间 pitch rng.uniform(-100, -80) yaw rng.uniform(0, 360) return {x: x, y: y, z: z, pitch: pitch, yaw: yaw} def render_frame(scene, camera_pose): # 实际项目中这里调用渲染引擎 rgb scene.render_rgb(camera_pose) mask scene.render_mask(camera_pose) return rgb, maskset_seed保证每次运行一致sample_camera_pose里俯仰角限制在下视范围避免相机穿到地形以下render_frame同时出 RGB 和掩码掩码用于标注可见性。参数上z_range 一般设在水底以上 0.5 到 3 米太近会穿模太远石鱼像素太少。3.2 批量采样与数据落盘单帧跑通后下一步是批量采样。常见做法是按“场景-序列-帧”三级组织每个序列内相机连续移动模拟真实航行。这样出来的数据既有单帧检测样本也有序列跟踪样本。# 批量生成 200 个序列每个序列 50 帧 python generate_dataset.py \ --scene underwater_scene.yaml \ --sequences 200 \ --frames_per_seq 50 \ --output_dir ./dataset_stonefish \ --seed 42 \ --visibility_threshold 0.3命令说明--sequences控制序列数--frames_per_seq控制每序列帧数--visibility_threshold控制标注过滤。落盘目录建议按dataset_stonefish/seq_0001/frame_0001.png和同名.txt标注组织。标注格式用 YOLO 的class x_center y_center width height归一化到 0 到 1方便直接接训练管线。3.3 数据分布检查别让石鱼全在画面中心仿真数据最容易出的问题是分布单一。如果相机采样范围太窄石鱼永远在画面中心模型学到的就是“中心有鱼”换到真实场景立刻失效。我一般会统计标注框的中心点分布和面积分布确保覆盖画面边缘和小目标。# check_distribution.py import os import numpy as np def load_boxes(label_dir): boxes [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, xc, yc, w, h map(float, parts) boxes.append([xc, yc, w, h]) return np.array(boxes) boxes load_boxes(./dataset_stonefish/labels) print(中心点 x 范围:, boxes[:, 0].min(), boxes[:, 0].max()) print(中心点 y 范围:, boxes[:, 1].min(), boxes[:, 1].max()) print(宽度中位数:, np.median(boxes[:, 2])) print(高度中位数:, np.median(boxes[:, 3]))如果中心点范围集中在 0.4 到 0.6说明相机采样太保守需要扩大 x_range 和 y_range。宽度中位数如果大于 0.3说明石鱼在画面里太大真实场景里石鱼往往只占几十个像素需要把相机拉远或降低分辨率。4. 避坑与排查石鱼仿真里最容易翻车的五件事4.1 石鱼“隐身”到标注框里全是背景现象导出的标注框位置正确但框内像素和背景几乎一样训练时损失不下降。原因camouflage_level设得太高纹理混合过度石鱼和背景的对比度低于噪声水平。解决把伪装度降到 0.6 到 0.75 之间或者在渲染后加一步局部对比度增强只作用于石鱼区域模拟真实场景中偶尔可见的轮廓。4.2 水体颜色偏绿模型学出错误的颜色先验现象仿真图整体偏绿模型在真实偏蓝图像上效果差。原因吸收系数absorption_rgb里绿色衰减设得太低红光衰减设得太高导致绿通道主导。解决参考真实水下图像统计红光吸收系数一般是绿光的 3 到 5 倍蓝光介于两者之间。调完后用直方图对比仿真和真实图的通道均值。4.3 相机穿模标注框出现负坐标现象部分帧的标注框 xmin 或 ymin 为负训练脚本报错。原因相机位姿采样时 z 值太低或者俯仰角超出地形范围相机穿到海底以下。解决在sample_camera_pose里加地形高度查询确保相机 z 大于地形高度加安全距离同时在标注导出时裁剪到图像范围并记录裁剪比例裁剪过多的帧直接丢弃。4.4 序列内石鱼瞬移跟踪算法学废现象同一序列相邻帧里石鱼位置跳变跟踪模型学出错误的运动先验。原因每帧重新随机采样石鱼位姿没有沿时间连续更新。解决石鱼位姿在序列开始时初始化之后按行为状态机连续更新突袭和回归状态的速度要限制在合理范围比如突袭速度不超过 2 米每秒。4.5 数据量够了但类别不平衡背景帧太多现象训练时正样本比例低于 1%模型偏向预测背景。原因相机采样范围太大很多帧里没有石鱼。解决在采样时做拒绝采样没有石鱼的帧以一定概率丢弃或者控制空帧比例在 20% 到 30% 之间。也可以在损失函数里对正样本加权但根子上还是数据分布问题。5. 进阶技巧用域随机化把仿真数据“喂”得更抗造域随机化是仿真数据能迁移到真实场景的关键。做法不是把参数固定成“最像真实”的那一组而是在一个合理范围内随机抖动让模型见过足够多的变化。石鱼仿真里我一般会随机化这几类参数水体浊度、光照方向与强度、海底纹理、石鱼伪装度、相机噪声。每类参数给一个区间每次序列开始时采样一次序列内保持不变这样既有变化又不会帧间闪烁。# domain_randomization.py import random def sample_domain_params(): return { turbidity: random.uniform(0.15, 0.55), light_intensity: random.uniform(0.6, 1.4), light_direction: random.uniform(0, 360), terrain_texture: random.choice([reef_01, reef_02, sand_03]), camouflage_level: random.uniform(0.55, 0.85), camera_noise: random.uniform(0.01, 0.05), }参数区间不是拍脑袋浊度上限 0.55 是因为再高石鱼就完全不可见标注失去意义伪装度下限 0.55 是保证石鱼至少有一部分轮廓可辨相机噪声上限 0.05 是避免噪声淹没小目标信号。每次序列开始调用一次sample_domain_params把结果写进该序列的元数据方便后续做消融分析。验证域随机化是否有效不能只看仿真集上的指标。我一般会留一小批真实水下图像做测试只做定性观察模型在真实图上能不能把石鱼从背景里分出来。如果仿真指标很高但真实图一塌糊涂说明随机化范围还是太窄或者真实图和仿真图之间存在没建模的差异比如悬浮颗粒的前向散射。还有一个容易被忽略的技巧在训练时把仿真图和真实图混在一个 batch 里但真实图只参与部分层的学习比如只更新检测头。这样模型既学到仿真里的大量标注又不会因为真实图太少而过拟合。这个做法在石鱼这种小目标、高伪装场景里比单纯堆仿真数据更稳。我自己踩过最深的坑是早期太追求仿真图“好看”把浊度调得很低、光照调得很亮结果模型在真实浑水里完全失效。后来养成一个习惯每调一组仿真参数先拿几张真实图做目视对比通道均值、梯度幅值、局部对比度这三个统计量对不上就不急着跑训练。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站