简介:这份PDF面向环保监测、生态保护与计算机视觉方向的开发者及研究人员,围绕YOLOv11在野生动物追踪与栖息地分析中的落地应用展开,适合具备一定深度学习基础、希望将目标检测技术迁移到生态场景的读者参考。文档共42页,以单份PDF形式打包,压缩包约2.53MB,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,查阅体验完整流畅。内容从环保监测背景与意义切入,系统梳理YOLOv11的网络结构、检测流程与损失函数,并延伸至野生动物追踪系统与栖息地分析系统的架构设计、数据采集传输、模型训练优化、系统集成测试及案例实战,涵盖数据清洗、标注、增强、评估指标与部署流程等关键环节。已有61人学习,适合作为课程设计、毕业项目或环保监测方案的技术参考,帮助读者快速建立从算法原理到系统落地的完整认知。
1. 环保监测新方案:YOLOv11 野生动物追踪与栖息地分析系统到底能落地到什么程度
红外相机在山里蹲了三个月,回收上来 2 万多张触发照片,其中 90% 是风吹草动、光影变化和空拍,真正拍到目标动物的可能不到 800 张。更麻烦的是,同一个物种在不同角度、不同光照、不同遮挡条件下,形态差异极大,靠人工翻图不仅效率低,还容易漏掉夜行性、隐蔽性强的关键物种。YOLOv11 野生动物追踪与栖息地分析系统要解决的,就是把这个从「海量触发图」到「物种时空分布」的链路自动化。它适合做保护地监测、生物多样性评估、生态科研的团队,也适合想用 YOLOv11 做小目标检测和长时序跟踪的工程师。核心思路不复杂:用 YOLOv11 做检测和跟踪,把结果落到地理坐标和时间轴上,再叠加栖息地环境图层做分析。但真正落地时,小目标漏检、夜间红外图像对比度低、Jetson Nano 边缘端算力吃紧,每一个都是血泪坑。
2. YOLOv11 野生动物检测:从数据标注到模型选型的完整链路
2.1 为什么野生动物检测不能直接套用 COCO 预训练模型
COCO 数据集里猫狗牛羊的标注框大多在 50×50 像素以上,且以白天可见光图像为主。野生动物红外相机触发图里,目标经常只有 20×30 像素,甚至更小,背景是杂乱的灌木、岩石和热噪声。直接拿 COCO 预训练的 YOLOv11n 去推理,置信度阈值调到 0.25 时,小目标几乎全被过滤掉;调到 0.05,误检又爆炸。常见做法是先用 COCO 权重做初始化,然后在自建的野生动物数据集上做完整微调,同时把输入分辨率从默认的 640 提到 960 或 1280。YOLOv11 的 C3k2 模块和 C2PSA 注意力机制对小目标有一定增益,但前提是训练时小目标样本不能被过度下采样。
我一般会先统计标注框的宽高分布,如果中位数面积小于 32×32 像素,就强制把输入尺寸拉到 1280,并在数据增强里关掉 Mosaic 的最后 10 个 epoch,避免小目标被拼接后进一步缩小。另外,红外图像是单通道灰度图,直接复制成三通道喂给网络会引入冗余计算,可以在数据加载阶段做一次自适应直方图均衡化,再转三通道,实测 mAP@0.5 能涨 3 到 5 个点。
2.2 用 YOLOv11 训练野生动物检测模型的最小可复现命令
假设你已经把红外相机图片整理成 YOLO 格式,目录结构是 images/train、images/val、labels/train、labels/val,类别文件 data.yaml 里写了物种名。下面这条命令是我在单卡 3090 上跑通的配置,输入 1280,batch 设为 8,显存占用约 18GB。
yolo detect train \ data=wildlife.yaml \ model=yolo11m.pt \ imgsz=1280 \ epochs=300 \ batch=8 \ device=0 \ workers=8 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ cos_lr=True \ close_mosaic=10 \ patience=50 \ project=runs/wildlife \ name=yolo11m_1280逻辑说明:model 选 yolo11m 而不是 n,是因为野生动物类别间形态差异大,n 的容量不够,m 在 1280 输入下参数量约 20M,推理速度在 3090 上约 45 FPS,满足离线批量处理。imgsz=1280 是小目标优化的关键,但注意显存会翻倍,batch 要相应减小。close_mosaic=10 表示最后 10 个 epoch 关闭 Mosaic 增强,让模型在真实尺度上收敛。patience=50 是早停,避免过拟合。cos_lr 和 lrf 配合做余弦退火,对红外图像这种低对比度数据更稳。
训练完成后,验证集上的 mAP@0.5 如果低于 0.6,优先检查标注质量,而不是急着换模型。红外触发图里经常有半遮挡目标,标注时如果只标了可见部分,模型学到的特征就是残缺的,推理时自然漏检。
2.3 小目标优化的三个必调参数:imgsz、anchor、NMS
YOLOv11 默认是 anchor-free 的,但 NMS 的 IoU 阈值和置信度阈值对小目标影响极大。我一般会把 NMS 的 iou 从默认 0.7 降到 0.5,因为小目标密集时,0.7 会把相邻的两个动物框合并成一个。置信度阈值在验证阶段先设 0.001,看 PR 曲线,找到 recall 和 precision 的平衡点,再定最终推理阈值。
另外,YOLOv11 的预测后保存结果时,如果直接存 txt,小目标的坐标精度会受 stride 影响。建议在推理时加save_conf=True和save_txt=True,同时把line_width设成 1,避免画框太粗遮挡目标。下面这段 Python 代码是我常用的批量推理脚本,支持保存带置信度的 txt 和裁剪小图。
from ultralytics import YOLO import cv2 import os model = YOLO("runs/wildlife/yolo11m_1280/weights/best.pt") img_dir = "data/test_images" save_dir = "runs/inference" os.makedirs(save_dir, exist_ok=True) results = model.predict( source=img_dir, imgsz=1280, conf=0.15, iou=0.5, save=True, save_txt=True, save_conf=True, project=save_dir, name="exp", stream=True ) for r in results: img = cv2.imread(r.path) for box in r.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) cls_id = int(box.cls[0]) conf = float(box.conf[0]) if (x2 - x1) < 40 or (y2 - y1) < 40: crop = img[max(0, y1-10):min(img.shape[0], y2+10), max(0, x1-10):min(img.shape[1], x2+10)] cv2.imwrite(f"{save_dir}/crop_{cls_id}_{conf:.2f}_{os.path.basename(r.path)}", crop)逻辑说明:conf=0.15 是偏低的阈值,目的是先召回再过滤,适合后续做人工复核。iou=0.5 防止小目标框被合并。save_conf=True 让 txt 里每行多一个置信度值,方便后续按置信度分层分析。裁剪小图是为了做二次分类或人工确认,尤其是夜间红外图像,裁剪后放大看比整图看更清楚。注意 stream=True 在批量处理大目录时能省内存,但会打乱顺序,如果需要和原图严格对应,就去掉这个参数。
3. 野生动物追踪:从单帧检测到跨帧 ID 关联的工程实现
3.1 YOLOv11 目标跟踪的两种模式:内置跟踪与外部关联
YOLOv11 官方支持model.track()接口,底层用的是 ByteTrack 或 BoT-SORT。对于野生动物,我一般优先用 ByteTrack,因为它在低帧率、目标遮挡场景下比 BoT-SORT 更稳。红外相机触发视频通常只有 10 到 15 帧,目标移动快,ByteTrack 的两阶段关联能利用低分检测框维持 ID。但要注意,YOLOv11 的 track 模式默认只返回当前帧的 ID,不保存轨迹历史,需要自己写一个字典来维护 track_id 到轨迹点的映射。
如果场景里动物频繁交叉、遮挡严重,ByteTrack 也会 ID switch。这时候可以引入外部关联:用 YOLOv11 检测,再用外观特征做 ReID。常见做法是裁剪检测框,过一个轻量 ReID 网络提取 128 维特征,然后计算余弦距离做匹配。但 ReID 模型在红外图像上泛化差,我一般只在可见光视频里用,红外视频还是靠 ByteTrack 的运动模型。
3.2 用 YOLOv11 + ByteTrack 跑通一段红外视频的跟踪
下面这段代码是我在 Jetson Nano 上跑通的跟踪脚本,输入是一段 15 帧的红外触发视频,输出是带 track_id 的标注视频和轨迹 csv。
from ultralytics import YOLO import cv2 import csv model = YOLO("runs/wildlife/yolo11m_1280/weights/best.pt") video_path = "data/ir_video.mp4" cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out = cv2.VideoWriter("runs/track_output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) track_history = {} with open("runs/tracks.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["frame", "track_id", "cls", "x1", "y1", "x2", "y2", "conf"]) frame_idx = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.track(frame, persist=True, tracker="bytetrack.yaml", conf=0.2, iou=0.5, imgsz=1280, verbose=False) if results[0].boxes.id is not None: boxes = results[0].boxes.xyxy.cpu().numpy() ids = results[0].boxes.id.cpu().numpy().astype(int) clss = results[0].boxes.cls.cpu().numpy().astype(int) confs = results[0].boxes.conf.cpu().numpy() for box, tid, cls, conf in zip(boxes, ids, clss, confs): x1, y1, x2, y2 = map(int, box) writer.writerow([frame_idx, tid, cls, x1, y1, x2, y2, f"{conf:.3f}"]) track_history.setdefault(tid, []).append((frame_idx, (x1+x2)//2, (y1+y2)//2)) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 1) cv2.putText(frame, f"ID{tid} cls{cls}", (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) out.write(frame) frame_idx += 1 cap.release() out.release()逻辑说明:persist=True 是让跟踪器在帧间保持状态,否则每帧都会重新分配 ID。tracker="bytetrack.yaml" 指定跟踪配置文件,YOLOv11 自带这个文件,不用自己写。conf=0.2 比检测时略高,是为了减少低分框对跟踪的干扰。track_history 字典按 track_id 存轨迹点,后续可以用来算移动距离和速度。csv 里记录了每帧每个 ID 的框和置信度,方便做栖息地分析时按时间轴回放。
注意 Jetson Nano 上 imgsz=1280 会非常慢,实测只有 2 到 3 FPS。如果要做实时跟踪,必须把模型换成 yolo11n,输入降到 640,或者用 TensorRT 加速。Jetson Nano 部署 YOLOv11 的详细步骤里,最容易翻车的是 TensorRT 版本和 CUDA 版本不匹配,建议先用jetson_release确认环境,再按官方文档装对应版本的 ultralytics。
3.3 轨迹后处理:把 track_id 变成有生态意义的移动路径
跟踪输出的 csv 只是原始轨迹点,要变成栖息地分析可用的数据,还需要做三步:去抖、分段、关联环境。去抖用滑动平均,窗口设 3 到 5 帧,因为红外触发视频帧率低,抖动会被放大。分段是按时间间隔切分,如果两个轨迹点之间超过 30 秒没有新点,就认为是一次新的活动事件。关联环境是把每个轨迹点的像素坐标通过相机标定转成地理坐标,再叠加植被类型、海拔、坡度图层。
相机标定是这里最容易被忽略的坑。红外相机通常固定在树干或岩石上,视角倾斜,如果不做透视校正,像素坐标直接转地理坐标误差可能超过 20 米。我一般会在相机视野内放几个已知坐标的地面控制点,用 OpenCV 的findHomography算单应矩阵,再把轨迹点映射到平面坐标。如果没法放控制点,就退而求其次,用相机安装高度和俯仰角做近似投影,精度差一些,但比不做好。
4. 栖息地分析:把检测和跟踪结果落到 GIS 图层上
4.1 栖息地分析需要哪些数据层:从 NDVI 到人类活动强度
栖息地分析不是把轨迹点画在地图上就完了。要回答「这个物种偏好什么环境」,至少需要四类图层:植被指数、地形因子、水源距离、人类活动强度。植被指数常用 NDVI 或 EVI,可以从 Sentinel-2 或 Landsat 影像算,空间分辨率 10 到 30 米。地形因子包括海拔、坡度、坡向,从 DEM 数据提取。水源距离用矢量水系数据算欧氏距离。人类活动强度可以用道路密度、夜间灯光指数或居民点距离来表征。
这些图层在 GIS 里统一到同一个坐标系和分辨率,我一般用 10 米栅格。然后把每个轨迹点对应的图层值提取出来,做成一个表格,每行是一个轨迹点,列是 NDVI、海拔、坡度、水源距离、道路距离等。有了这个表,就可以做统计分析了。
4.2 用 Python 做栖息地偏好分析:从轨迹点到环境变量表
下面这段代码用 rasterio 和 geopandas 把轨迹点采样到环境图层上,输出一个 csv,后续可以直接喂给 R 或 Python 做选择指数分析。
import geopandas as gpd import rasterio import pandas as pd from shapely.geometry import Point tracks = pd.read_csv("runs/tracks.csv") tracks["geometry"] = tracks.apply( lambda r: Point((r["x1"]+r["x2"])/2, (r["y1"]+r["y2"])/2), axis=1) gdf = gpd.GeoDataFrame(tracks, crs="EPSG:4326") layers = { "ndvi": "data/ndvi.tif", "elevation": "data/dem.tif", "slope": "data/slope.tif", "water_dist": "data/water_dist.tif", "road_dist": "data/road_dist.tif" } for name, path in layers.items(): with rasterio.open(path) as src: coords = [(geom.x, geom.y) for geom in gdf.geometry] values = [v[0] for v in src.sample(coords)] gdf[name] = values gdf.drop(columns=["geometry"]).to_csv("runs/habitat_samples.csv", index=False)逻辑说明:这里假设轨迹点已经是地理坐标,如果还是像素坐标,需要先做单应变换。rasterio 的 sample 方法按点采样,要求栅格和点的坐标系一致,不一致会返回 nodata。实际使用时,先检查每个图层的 CRS,用 geopandas 的 to_crs 统一。采样后如果某些点落在 nodata 区域,会得到 None,需要做缺失值处理,我一般直接删掉这些点,因为数量通常很少。
拿到 habitat_samples.csv 后,可以用资源选择函数(RSF)或最大熵模型(MaxEnt)分析偏好。RSF 更简单,用逻辑回归,因变量是 used/available,自变量是环境变量。MaxEnt 适合只有 presence 数据的情况,但对样本量敏感,轨迹点少于 50 个时不稳定。
4.3 栖息地分析结果怎么验证:交叉验证与独立点位
栖息地模型最容易犯的错是过拟合。我一般会把轨迹点按时间分成训练集和测试集,前 70% 时间段的点做训练,后 30% 做测试。如果测试集上的 AUC 低于 0.7,说明模型泛化差,需要检查环境变量是否共线性太强,或者轨迹点空间自相关太严重。另一个验证方法是用独立相机点位的检测结果做外部验证,如果模型预测的高适宜区在独立点位上也有动物出现,说明模型可信。
注意,栖息地分析的结果不要直接当成保护决策的唯一依据。红外相机布设本身就有偏倚,比如靠近水源和道路的相机更容易拍到动物,这会让模型高估这些区域的适宜性。我一般会在分析前先做相机点位偏差校正,或者至少在报告里说明这个局限。
5. 避坑与排查:YOLOv11 野生动物系统落地时最容易翻车的五个地方
5.1 夜间红外图像漏检严重,白天正常
现象:白天可见光图像检测 mAP 0.75,夜间红外图像 mAP 掉到 0.35,小目标几乎全漏。原因:红外图像是灰度图,对比度低,动物和背景的热辐射差异小,YOLOv11 在 COCO 上预训练学到的颜色和纹理特征在红外域几乎失效。解决:在数据加载阶段做 CLAHE 自适应直方图均衡化,把灰度图转成三通道时复制均衡化后的通道,而不是简单复制原图。另外,训练时把红外和可见光图像混合,比例控制在 1:1,让模型学到域不变特征。如果红外样本太少,可以用 CycleGAN 做可见光到红外的风格迁移做数据增强,但要注意生成样本的质量,质量差的增强反而会掉点。
5.2 Jetson Nano 上推理速度只有 2 FPS,达不到实时
现象:在 3090 上 45 FPS 的 yolo11m,放到 Jetson Nano 上只有 2 到 3 FPS,视频卡顿。原因:Jetson Nano 的 GPU 算力只有 472 GFLOPS,内存带宽 25.6 GB/s,yolo11m 在 1280 输入下计算量太大。解决:换 yolo11n,输入降到 640,用 TensorRT FP16 加速。实测 yolo11n + 640 + TensorRT 能到 15 到 20 FPS,满足实时跟踪。如果必须用 m,就做模型剪枝或知识蒸馏,但工程量较大。另外,Jetson Nano 的散热是个玄学,温度超过 80 度会降频,加个风扇能稳不少。
5.3 跟踪 ID 频繁切换,轨迹断成碎片
现象:同一只动物在视频里被分配了多个 track_id,轨迹不连续。原因:ByteTrack 依赖检测框的置信度和 IoU,当动物被遮挡或快速移动时,检测框消失或偏移,跟踪器就会新建 ID。解决:降低检测阈值到 0.1,让低分框也参与关联;把 track_buffer 从默认 30 调到 60,给跟踪器更多时间找回目标;如果场景里动物频繁交叉,换 BoT-SORT 并开启 ReID。但 ReID 在红外图像上效果有限,我一般只在可见光视频里用。
5.4 栖息地分析结果和实际观察不符
现象:模型预测的高适宜区,实际相机点位却没拍到动物。原因:相机布设偏倚,或者环境图层分辨率太粗,10 米栅格掩盖了微生境差异。解决:检查相机点位是否集中在道路和水源附近,如果是,做偏差校正或分层抽样。环境图层如果只有 30 米分辨率,考虑用无人机影像生成 1 米分辨率的 NDVI 和地形。另外,轨迹点的地理定位误差也会影响结果,如果单应变换没做好,误差可能超过 20 米,直接导致环境变量提取错误。
5.5 训练 loss 震荡不收敛,mAP 卡在 0.4 上不去
现象:训练 loss 来回震荡,验证集 mAP 在 0.4 附近徘徊。原因:学习率太大,或者 batch size 太小导致梯度噪声大。解决:把 lr0 从 0.01 降到 0.001,用 AdamW 替代 SGD,加 cosine 退火。如果显存不够,batch 只能设 4 或 8,就开梯度累积,accumulate=4 等效 batch 16。另外,检查数据标注有没有漏标或错标,红外图像里半遮挡目标如果只标了可见部分,模型学到的就是残缺特征,mAP 很难上去。
6. 进阶技巧:用 YOLOv11 的预测后保存结果做长时序栖息地变化检测
前面讲的都是单次监测的分析流程,但栖息地分析真正有价值的是长时序对比。比如同一片区域,2023 年和 2024 年的动物活动范围有没有变化,是扩大了还是缩小了。这需要把多次监测的轨迹数据对齐到同一空间基准,然后做变化检测。我一般会用 YOLOv11 的预测后保存功能,把每次推理的 txt 结果按日期归档,再用 Python 做时序聚合。
具体做法是:每次推理时加save_txt=True和save_conf=True,输出目录按site_date命名。然后写一个脚本,把所有 txt 读进来,按 track_id 和日期分组,算每个物种在每个日期的活动中心点和活动范围。活动中心点用轨迹点的质心,活动范围用最小凸多边形或核密度估计的 95% 等值线。把多期的活动范围叠加,就能看出栖息地利用的变化趋势。
下面这段代码是我常用的时序聚合脚本,输入是多个日期的 tracks.csv,输出是每个物种每期的活动中心点和面积。
import pandas as pd import geopandas as gpd from shapely.geometry import MultiPoint from scipy.spatial import ConvexHull import numpy as np def activity_range(df): points = df[["x", "y"]].values if len(points) < 3: return None, 0 hull = ConvexHull(points) poly = MultiPoint(points).convex_hull return poly.centroid, poly.area all_tracks = [] for date in ["2023-05", "2023-08", "2024-05", "2024-08"]: df = pd.read_csv(f"runs/{date}/tracks.csv") df["date"] = date all_tracks.append(df) tracks = pd.concat(all_tracks) tracks["x"] = (tracks["x1"] + tracks["x2"]) / 2 tracks["y"] = (tracks["y1"] + tracks["y2"]) / 2 summary = [] for (date, cls), group in tracks.groupby(["date", "cls"]): centroid, area = activity_range(group) if centroid is not None: summary.append({ "date": date, "cls": cls, "centroid_x": centroid.x, "centroid_y": centroid.y, "area_px": area, "n_points": len(group) }) pd.DataFrame(summary).to_csv("runs/activity_summary.csv", index=False)逻辑说明:这里用凸包面积近似活动范围,优点是计算快,缺点是对离群点敏感。如果轨迹点有噪声,先做 DBSCAN 聚类去掉离群点,再算凸包。centroid 是活动中心点,多期对比可以看出中心点是否偏移。area_px 是像素面积,要转成实际面积需要相机标定的比例尺。n_points 是轨迹点数量,太少的话面积不可靠,我一般要求每个物种每期至少 20 个点。
这个方案我在一个保护区项目里跑过两年数据,发现某中型兽类的活动中心点向低海拔迁移了约 150 米,活动范围缩小了 20%。结合 NDVI 图层看,低海拔区域的植被覆盖在同期有所恢复,推测是食物资源驱动。但这个结论需要更多证据支撑,单靠轨迹数据只能提出假设。
最后说个习惯:每次跑完推理,我都会把save_txt的输出和原始图像路径一起存进一个 sqlite 数据库,字段包括日期、点位、物种、置信度、坐标。这样后面做任何时序分析,都不用重新跑模型,直接查库就行。这个习惯帮我省了至少三次重跑 2 万张图的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取